This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/endpoints/api/search.py

298 lines
9.5 KiB
Python
Raw Normal View History

from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource,
require_scope, path_param)
from data import model
from auth.permissions import (OrganizationMemberPermission, ViewTeamPermission,
ReadRepositoryPermission, UserAdminPermission,
AdministerOrganizationPermission, ReadRepositoryPermission)
from auth.auth_context import get_authenticated_user
from auth import scopes
2015-04-06 23:17:18 +00:00
from app import avatar, get_app_url
from operator import itemgetter
from stringscore import liquidmetal
from util.names import parse_robot_username
2015-04-06 23:17:18 +00:00
import math
@resource('/v1/entities/<prefix>')
class EntitySearch(ApiResource):
""" Resource for searching entities. """
@path_param('prefix', 'The prefix of the entities being looked up')
@parse_args
@query_param('namespace', 'Namespace to use when querying for org entities.', type=str,
default='')
@query_param('includeTeams', 'Whether to include team names.', type=truthy_bool, default=False)
@query_param('includeOrgs', 'Whether to include orgs names.', type=truthy_bool, default=False)
@nickname('getMatchingEntities')
def get(self, args, prefix):
""" Get a list of entities that match the specified prefix. """
teams = []
org_data = []
namespace_name = args['namespace']
robot_namespace = None
organization = None
try:
organization = model.get_organization(namespace_name)
# namespace name was an org
permission = OrganizationMemberPermission(namespace_name)
if permission.can():
robot_namespace = namespace_name
if args['includeTeams']:
2014-11-24 21:07:38 +00:00
teams = model.get_matching_teams(prefix, organization)
if args['includeOrgs'] and AdministerOrganizationPermission(namespace_name) \
and namespace_name.startswith(prefix):
org_data = [{
'name': namespace_name,
'kind': 'org',
'is_org_member': True,
'avatar': avatar.get_data_for_org(organization),
}]
except model.InvalidOrganizationException:
# namespace name was a user
user = get_authenticated_user()
if user and user.username == namespace_name:
# Check if there is admin user permissions (login only)
admin_permission = UserAdminPermission(user.username)
if admin_permission.can():
robot_namespace = namespace_name
users = model.get_matching_users(prefix, robot_namespace, organization)
def entity_team_view(team):
result = {
'name': team.name,
'kind': 'team',
'is_org_member': True,
'avatar': avatar.get_data_for_team(team)
}
return result
def user_view(user):
user_json = {
'name': user.username,
'kind': 'user',
'is_robot': user.robot,
'avatar': avatar.get_data_for_user(user)
}
if organization is not None:
user_json['is_org_member'] = user.robot or user.is_org_member
return user_json
team_data = [entity_team_view(team) for team in teams]
user_data = [user_view(user) for user in users]
return {
'results': team_data + user_data + org_data
}
def team_view(orgname, team):
view_permission = ViewTeamPermission(orgname, team.name)
role = model.get_team_org_role(team).name
return {
'id': team.id,
'name': team.name,
'description': team.description,
'can_view': view_permission.can(),
'role': role
}
@resource('/v1/find/repository')
class FindRepositories(ApiResource):
""" Resource for finding repositories. """
@parse_args
@query_param('query', 'The prefix to use when querying for repositories.', type=str, default='')
@require_scope(scopes.READ_REPO)
@nickname('findRepos')
def get(self, args):
""" Get a list of repositories that match the specified prefix query. """
prefix = args['query']
def repo_view(repo):
return {
'namespace': repo.namespace_user.username,
'name': repo.name,
'description': repo.description
}
username = None
user = get_authenticated_user()
if user is not None:
username = user.username
matching = model.get_matching_repositories(prefix, username)
return {
'repositories': [repo_view(repo) for repo in matching
2014-11-24 21:07:38 +00:00
if (repo.visibility.name == 'public' or
ReadRepositoryPermission(repo.namespace_user.username, repo.name).can())]
}
2015-04-08 21:41:08 +00:00
2015-04-20 16:51:47 +00:00
def search_entity_view(username, entity, get_short_name=None):
2015-04-08 21:41:08 +00:00
kind = 'user'
avatar_data = avatar.get_data_for_user(entity)
href = '/user/' + entity.username
if entity.organization:
kind = 'organization'
avatar_data = avatar.get_data_for_org(entity)
href = '/organization/' + entity.username
elif entity.robot:
parts = parse_robot_username(entity.username)
if parts[0] == username:
href = '/user/' + username + '?tab=robots&showRobot=' + entity.username
else:
href = '/organization/' + parts[0] + '?tab=robots&showRobot=' + entity.username
kind = 'robot'
avatar_data = None
2015-04-20 16:51:47 +00:00
data = {
2015-04-08 21:41:08 +00:00
'kind': kind,
'avatar': avatar_data,
'name': entity.username,
'score': 1,
'href': href
}
2015-04-20 16:51:47 +00:00
if get_short_name:
data['short_name'] = get_short_name(entity.username)
return data
2015-04-08 21:41:08 +00:00
def conduct_team_search(username, query, encountered_teams, results):
""" Finds the matching teams where the user is a member. """
matching_teams = model.get_matching_user_teams(query, get_authenticated_user(), limit=5)
for team in matching_teams:
if team.id in encountered_teams:
continue
encountered_teams.add(team.id)
results.append({
'kind': 'team',
'name': team.name,
'organization': search_entity_view(username, team.organization),
'avatar': avatar.get_data_for_team(team),
'score': 2,
'href': '/organization/' + team.organization.username + '/teams/' + team.name
})
def conduct_admined_team_search(username, query, encountered_teams, results):
""" Finds matching teams in orgs admined by the user. """
matching_teams = model.get_matching_admined_teams(query, get_authenticated_user(), limit=5)
for team in matching_teams:
if team.id in encountered_teams:
continue
encountered_teams.add(team.id)
results.append({
'kind': 'team',
'name': team.name,
'organization': search_entity_view(username, team.organization),
'avatar': avatar.get_data_for_team(team),
'score': 2,
'href': '/organization/' + team.organization.username + '/teams/' + team.name
})
def conduct_repo_search(username, query, results):
""" Finds matching repositories. """
def can_read(repository):
if repository.is_public:
return True
return ReadRepositoryPermission(repository.namespace_user.username, repository.name).can()
only_public = username is None
matching_repos = model.get_sorted_matching_repositories(query, only_public, can_read, limit=5)
2015-04-08 21:41:08 +00:00
for repo in matching_repos:
repo_score = math.log(repo.count or 1, 10) or 1
2015-04-08 21:41:08 +00:00
# If the repository is under the user's namespace, give it 20% more weight.
2015-04-08 21:41:08 +00:00
namespace = repo.namespace_user.username
if OrganizationMemberPermission(namespace).can() or namespace == username:
repo_score = repo_score * 1.2
2015-04-08 21:41:08 +00:00
results.append({
'kind': 'repository',
'namespace': search_entity_view(username, repo.namespace_user),
'name': repo.name,
'description': repo.description,
'is_public': repo.is_public,
2015-04-08 21:41:08 +00:00
'score': repo_score,
'href': '/repository/' + repo.namespace_user.username + '/' + repo.name
})
def conduct_namespace_search(username, query, results):
""" Finds matching users and organizations. """
matching_entities = model.get_matching_user_namespaces(query, username, limit=5)
2015-04-08 21:41:08 +00:00
for entity in matching_entities:
results.append(search_entity_view(username, entity))
def conduct_robot_search(username, query, results):
""" Finds matching robot accounts. """
2015-04-20 16:51:47 +00:00
def get_short_name(name):
return parse_robot_username(name)[1]
matching_robots = model.get_matching_robots(query, username, limit=5)
for robot in matching_robots:
2015-04-20 16:51:47 +00:00
results.append(search_entity_view(username, robot, get_short_name))
2015-04-08 21:41:08 +00:00
@resource('/v1/find/all')
class ConductSearch(ApiResource):
""" Resource for finding users, repositories, teams, etc. """
@parse_args
@query_param('query', 'The search query.', type=str, default='')
@require_scope(scopes.READ_REPO)
@nickname('conductSearch')
def get(self, args):
""" Get a list of entities and resources that match the specified query. """
query = args['query']
if not query:
return {'results': []}
username = None
results = []
if get_authenticated_user():
username = get_authenticated_user().username
# Search for teams.
encountered_teams = set()
conduct_team_search(username, query, encountered_teams, results)
conduct_admined_team_search(username, query, encountered_teams, results)
# Search for robot accounts.
conduct_robot_search(username, query, results)
2015-04-08 21:41:08 +00:00
# Search for repos.
conduct_repo_search(username, query, results)
# Search for users and orgs.
conduct_namespace_search(username, query, results)
2015-04-08 21:41:08 +00:00
# Modify the results' scores via how close the query term is to each result's name.
for result in results:
2015-04-20 16:51:47 +00:00
name = result.get('short_name', result['name'])
2015-04-20 17:00:56 +00:00
lm_score = liquidmetal.score(name, query) or 0.5
result['score'] = result['score'] * lm_score
2015-04-08 21:41:08 +00:00
return {'results': sorted(results, key=itemgetter('score'), reverse=True)}