diff --git a/data/model/legacy.py b/data/model/legacy.py index e84535ec6..1997ec512 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -961,11 +961,11 @@ def _get_public_repo_visibility(): return _public_repo_visibility_cache -def get_matching_repositories(repo_term, username=None, limit=10): +def get_matching_repositories(repo_term, username=None, limit=10, include_public=True): namespace_term = repo_term name_term = repo_term - visible = get_visible_repositories(username) + visible = get_visible_repositories(username, include_public=include_public) search_clauses = (Repository.name ** ('%' + name_term + '%') | Namespace.username ** ('%' + namespace_term + '%')) @@ -981,14 +981,18 @@ def get_matching_repositories(repo_term, username=None, limit=10): return visible.where(search_clauses).limit(limit) + def get_repository_pull_counts(repositories): repo_pull = LogEntryKind.get(name = 'pull_repo') + if not repositories: + return [] + last_month = datetime.now() - timedelta(weeks=4) return (Repository.select(Repository.id, fn.Count(LogEntry.id)) .where(Repository.id << [r.id for r in repositories]) .join(LogEntry, JOIN_LEFT_OUTER) - .where(LogEntry.kind == repo_pull) - .group_by(LogEntry.repository) + .where(LogEntry.kind == repo_pull, LogEntry.datetime >= last_month) + .group_by(Repository.id, LogEntry.id) .tuples()) def change_password(user, new_password): diff --git a/endpoints/api/search.py b/endpoints/api/search.py index 9e835445e..94472b775 100644 --- a/endpoints/api/search.py +++ b/endpoints/api/search.py @@ -105,128 +105,6 @@ class EntitySearch(ApiResource): } -@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 = [] - - def entity_view(entity): - 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 - - return { - 'kind': kind, - 'avatar': avatar_data, - 'name': entity.username, - 'score': 1, - 'href': href - } - - if get_authenticated_user(): - username = get_authenticated_user().username - - # Find the matching teams where the user is a member. - encountered_teams = set() - - 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': entity_view(team.organization), - 'avatar': avatar.get_data_for_team(team), - 'score': 2, - 'href': '/organization/' + team.organization.username + '/teams/' + team.name - }) - - # Find 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': entity_view(team.organization), - 'avatar': avatar.get_data_for_team(team), - 'score': 2, - 'href': '/organization/' + team.organization.username + '/teams/' + team.name - }) - - - # Find the matching repositories. - matching_repos = model.get_matching_repositories(query, username, limit=5) - matching_repo_counts = {t[0]: t[1] for t in model.get_repository_pull_counts(matching_repos)} - - for repo in matching_repos: - results.append({ - 'kind': 'repository', - 'namespace': entity_view(repo.namespace_user), - 'name': repo.name, - 'description': repo.description, - 'is_public': repo.visibility.name == 'public', - 'score': math.log(matching_repo_counts.get(repo.id, 1), 10) or 1, - 'href': '/repository/' + repo.namespace_user.username + '/' + repo.name - }) - - - # Find the matching users, robots and organizations. - matching_entities = model.get_matching_entities(query) - entity_count = 0 - for entity in matching_entities: - # If the entity is a robot, filter it to only match those that are under the current - # user or can be administered by the organization. - if entity.robot: - orgname = parse_robot_username(entity.username)[0] - if not AdministerOrganizationPermission(orgname).can() and not orgname == username: - continue - - results.append(entity_view(entity)) - entity_count = entity_count + 1 - if entity_count >= 5: - break - - - for result in results: - result['score'] = result['score'] * liquidmetal.score(result['name'], query) - - return {'results': sorted(results, key=itemgetter('score'), reverse=True)} - - @resource('/v1/find/repository') class FindRepositories(ApiResource): """ Resource for finding repositories. """ @@ -256,3 +134,149 @@ class FindRepositories(ApiResource): if (repo.visibility.name == 'public' or ReadRepositoryPermission(repo.namespace_user.username, repo.name).can())] } + + + +def search_entity_view(username, entity): + 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 + + return { + 'kind': kind, + 'avatar': avatar_data, + 'name': entity.username, + 'score': 1, + 'href': href + } + + +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. """ + matching_repos = list(model.get_matching_repositories(query, username, limit=5)) + matching_repo_counts = {t[0]: t[1] for t in model.get_repository_pull_counts(matching_repos)} + + for repo in matching_repos: + repo_score = math.log(matching_repo_counts.get(repo.id, 1), 10) or 1 + + # If the repository is under the user's namespace, give it 50% more weight. + namespace = repo.namespace_user.username + if OrganizationMemberPermission(namespace).can() or namespace == username: + repo_score = repo_score * 1.5 + + results.append({ + 'kind': 'repository', + 'namespace': search_entity_view(username, repo.namespace_user), + 'name': repo.name, + 'description': repo.description, + 'is_public': repo.visibility.name == 'public', + 'score': repo_score, + 'href': '/repository/' + repo.namespace_user.username + '/' + repo.name + }) + + +def conduct_entity_search(username, query, results): + """ Finds matching users, robots and organizations. """ + matching_entities = model.get_matching_entities(query) + entity_count = 0 + for entity in matching_entities: + # If the entity is a robot, filter it to only match those that are under the current + # user or can be administered by the organization. + if entity.robot: + orgname = parse_robot_username(entity.username)[0] + if not AdministerOrganizationPermission(orgname).can() and not orgname == username: + continue + + results.append(search_entity_view(username, entity)) + entity_count = entity_count + 1 + if entity_count >= 5: + break + + +@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 repos. + conduct_repo_search(username, query, results) + + # Search for users, orgs and robots. + conduct_entity_search(username, query, results) + + # Modify the results' scores via how close the query term is to each result's name. + for result in results: + result['score'] = result['score'] * liquidmetal.score(result['name'], query) + + return {'results': sorted(results, key=itemgetter('score'), reverse=True)}