From eb956e5b7d3b1e546505d17fe5e60c81d6b9c6a0 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Wed, 19 Nov 2014 14:50:56 -0500 Subject: [PATCH 01/36] initial work on adding models for starring repos. I'm sick of using `git stash`. --- data/database.py | 20 ++++- .../versions/3b668be15dc0_add_stars.py | 42 ++++++++++ data/model/legacy.py | 51 ++++++++++++ endpoints/api/repository.py | 23 +++--- endpoints/api/user.py | 77 ++++++++++++++++++- 5 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 data/migrations/versions/3b668be15dc0_add_stars.py diff --git a/data/database.py b/data/database.py index b49c8a594..51eeabf6f 100644 --- a/data/database.py +++ b/data/database.py @@ -4,7 +4,9 @@ import uuid from random import SystemRandom from datetime import datetime -from peewee import * +from peewee import (Proxy, MySQLDatabase, SqliteDatabase, PostgresqlDatabase, fn, CharField, + BooleanField, IntegerField, DateTimeField, ForeignKeyField, TextField, + BigIntegerField) from data.read_slave import ReadSlaveModel from sqlalchemy.engine.url import make_url from util.names import urn_generator @@ -265,6 +267,20 @@ class Repository(BaseModel): super(Repository, self).delete_instance(recursive=False, delete_nullable=False) +class Star(BaseModel): + user = ForeignKeyField(User, index=True, related_name="stars") + repository = ForeignKeyField(Repository, index=True, related_name="stargazers") + created = DateTimeField(default=datetime.now) + + class Meta: + database = db + read_slaves = (read_slave,) + indexes = ( + # create a unique index on user and repository + (('user', 'repository'), True), + ) + + class Role(BaseModel): name = CharField(index=True, unique=True) @@ -550,4 +566,4 @@ all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Notification, ImageStorageLocation, ImageStoragePlacement, ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification, RepositoryAuthorizedEmail, ImageStorageTransformation, DerivedImageStorage, - TeamMemberInvite] + TeamMemberInvite, Star] diff --git a/data/migrations/versions/3b668be15dc0_add_stars.py b/data/migrations/versions/3b668be15dc0_add_stars.py new file mode 100644 index 000000000..b2f6545ef --- /dev/null +++ b/data/migrations/versions/3b668be15dc0_add_stars.py @@ -0,0 +1,42 @@ +"""add stars + +Revision ID: 3b668be15dc0 +Revises: 204abf14783d +Create Date: 2014-11-14 14:11:18.687340 + +""" + +# revision identifiers, used by Alembic. +revision = '3b668be15dc0' +down_revision = '204abf14783d' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('star', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('repository_id', sa.Integer(), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], name=op.f('fk_star_repository_id_repository')), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_star_user_id_user')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_star')) + ) + op.create_index('star_repository_id', 'star', ['repository_id'], unique=False) + op.create_index('star_user_id', 'star', ['user_id'], unique=False) + op.create_index('star_user_id_repository_id', 'star', ['user_id', 'repository_id'], unique=True) + ### end Alembic commands ### + + +def downgrade(tables): + ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('fk_star_repository_id_repository', 'star', type_='foreignkey') + op.drop_constraint('fk_star_user_id_user', 'star', type_='foreignkey') + op.drop_index('star_user_id_repository_id', table_name='star') + op.drop_index('star_user_id', table_name='star') + op.drop_index('star_repository_id', table_name='star') + op.drop_table('star') + ### end Alembic commands ### diff --git a/data/model/legacy.py b/data/model/legacy.py index a5c779871..e7d05a9aa 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -2385,3 +2385,54 @@ def archivable_buildlogs_query(): .where((RepositoryBuild.phase == BUILD_PHASE.COMPLETE) | (RepositoryBuild.phase == BUILD_PHASE.ERROR) | (RepositoryBuild.started < presumed_dead_date), RepositoryBuild.logs_archived == False)) + + +def star_repository(user, repository): + """ Stars a repository. """ + star = Star.create(user=user.id, repository=repository.id) + star.save() + + +def unstar_repository(user, repository): + """ Unstars a repository. """ + try: + star = (Star + .select() + .join(Repository) + .switch(Star) + .join(User) + .where(Repository.id == repository.id, User.id == user.id) + .get()) + except Star.DoesNotExist: + raise DataModelException('Star not found.') + + star.delete_instance() + + +def get_user_starred_repositories(user, limit=None, page=None): + """ Retrieves all of the repositories a user has starred. """ + query = (Repository + .select() + .join(Star) + .join(User) + .where(User.id == user.id) + .order_by(Star.created)) + + if page and limit: + query = query.paginate(page, limit) + elif limit: + query = query.limit(limit) + + return query + + +def repository_is_starred(user, repository): + """ Determines whether a user has starred a repository or not. """ + try: + (Star + .select() + .where(Star.repository == repository.id, Star.user == user.id) + .get()) + return True + except Star.DoesNotExist: + return False diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index 0a3acdcd7..9302f26a0 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -102,21 +102,13 @@ class RepositoryList(ApiResource): @query_param('limit', 'Limit on the number of results (int)', type=int) @query_param('namespace', 'Namespace to use when querying for org repositories.', type=str) @query_param('public', 'Whether to include public repositories.', type=truthy_bool, default=True) - @query_param('private', 'Whether to inlcude private repositories.', type=truthy_bool, + @query_param('private', 'Whether to include private repositories.', type=truthy_bool, default=True) @query_param('sort', 'Whether to sort the results.', type=truthy_bool, default=False) @query_param('count', 'Whether to include a count of the total number of results available.', type=truthy_bool, default=False) def get(self, args): """Fetch the list of repositories under a variety of situations.""" - def repo_view(repo_obj): - return { - 'namespace': repo_obj.namespace_user.username, - 'name': repo_obj.name, - 'description': repo_obj.description, - 'is_public': repo_obj.visibility.name == 'public', - } - username = None if get_authenticated_user() and args['private']: username = get_authenticated_user().username @@ -141,6 +133,15 @@ class RepositoryList(ApiResource): return response +def repo_view(repo_obj): + return { + 'namespace': repo_obj.namespace_user.username, + 'name': repo_obj.name, + 'description': repo_obj.description, + 'is_public': repo_obj.visibility.name == 'public', + } + + @resource('/v1/repository/') @path_param('repository', 'The full path of the repository. e.g. namespace/name') class Repository(RepositoryParamResource): @@ -271,6 +272,4 @@ class RepositoryVisibility(RepositoryParamResource): log_action('change_repo_visibility', namespace, {'repo': repository, 'visibility': values['visibility']}, repo=repo) - return { - 'success': True - } + return {'success': True} diff --git a/endpoints/api/user.py b/endpoints/api/user.py index b713b3ff8..cacf111be 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -9,10 +9,12 @@ from app import app, billing as stripe, authentication, avatar from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, log_action, internal_only, NotFound, require_user_admin, parse_args, query_param, InvalidToken, require_scope, format_date, hide_if, show_if, - license_error, require_fresh_login, path_param, define_json_response) + license_error, require_fresh_login, path_param, define_json_response, + RepositoryParamResource) from endpoints.api.subscribe import subscribe from endpoints.common import common_login from endpoints.api.team import try_accept_invite +from endpoints.api.repository import repo_view from data import model from data.billing import get_plan @@ -247,7 +249,6 @@ class User(ApiResource): raise request_error(message='Username is already in use') model.change_username(user, new_username) - except model.InvalidPasswordException, ex: raise request_error(exception=ex) @@ -663,3 +664,75 @@ class UserAuthorization(ApiResource): access_token.delete_instance(recursive=True, delete_nullable=True) return 'Deleted', 204 + +@resource('/v1/user/starred') +class StarredRepositoryList(ApiResource): + """ Operations for creating and listing starred repositories. """ + schemas = { + 'NewStarredRepository': { + 'id': 'NewStarredRepository', + 'type': 'object', + 'required': [ + 'namespace', + 'repository', + ], + 'properties': { + 'namespace': { + 'type': 'string', + 'description': 'Namespace in which the repository belongs', + }, + 'repository': { + 'type': 'string', + 'description': 'Repository name' + } + } + } + } + + @require_scope(scopes.READ_REPO) + @nickname('listStarredRepos') + @parse_args + @query_param('page', 'Offset page number. (int)', type=int) + @query_param('limit', 'Limit on the number of results (int)', type=int) + def get(self): + """ List all starred repositories. """ + page = args['page'] + limit = args['limit'] + starred_repos = list(get_user_starred_repositories(get_authenticated_user(), page=page, limit=limit)) + return {'repositories': [repo_view(repo) for repo in starred_repos]} + + @require_scope(scopes.READ_REPO) + @nickname('createStar') + @validate_json_request('NewStarredRepository') + def post(self): + """ Star a repository. """ + user = get_authenticated_user() + req = request.get_json() + namespace = req['namespace'] + repository = req['repository'] + repo = model.get_repository(namespace, repository) + if repo: + model.star_repository(user, repo) + log_action('star_repository', user.username, namespace, + {'repo': repository, 'namespace': namespace}) + return { + 'namespace': namespace, + 'repository': repository, + }, 201 + + raise NotFound() + +@resource('/v1/user/starred/') +class StarredRepository(RepositoryParamResource): + """ Operations for managing a specific starred repository. """ + @nickname('deleteStar') + def delete(self, namespace, repository): + user = get_authenticated_user() + repo = model.get_repository(namespace, repository) + if repo: + model.unstar_repository(user, repo) + log_action('unstar_repository', user.username, namespace, + {'repo': repository, 'namespace': namespace}) + return 'Deleted', 204 + + raise NotFound() From aa4903c3cd8375dfa4a1501e0ead45096e098ac1 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Tue, 25 Nov 2014 13:25:49 -0500 Subject: [PATCH 02/36] add docs for star repo api --- endpoints/api/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/endpoints/api/user.py b/endpoints/api/user.py index cacf111be..33c36cabf 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -723,6 +723,7 @@ class StarredRepositoryList(ApiResource): raise NotFound() @resource('/v1/user/starred/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') class StarredRepository(RepositoryParamResource): """ Operations for managing a specific starred repository. """ @nickname('deleteStar') From 08d84298e2243a786a48aee06770c4e33e380ecc Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Sun, 30 Nov 2014 17:45:05 -0500 Subject: [PATCH 03/36] Fix indentation in star migration. --- .../versions/3b668be15dc0_add_stars.py | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/data/migrations/versions/3b668be15dc0_add_stars.py b/data/migrations/versions/3b668be15dc0_add_stars.py index b2f6545ef..47f1f1a3a 100644 --- a/data/migrations/versions/3b668be15dc0_add_stars.py +++ b/data/migrations/versions/3b668be15dc0_add_stars.py @@ -15,28 +15,24 @@ import sqlalchemy as sa from sqlalchemy.dialects import mysql def upgrade(tables): - ### commands auto generated by Alembic - please adjust! ### - op.create_table('star', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('repository_id', sa.Integer(), nullable=False), - sa.Column('created', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], name=op.f('fk_star_repository_id_repository')), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_star_user_id_user')), - sa.PrimaryKeyConstraint('id', name=op.f('pk_star')) - ) - op.create_index('star_repository_id', 'star', ['repository_id'], unique=False) - op.create_index('star_user_id', 'star', ['user_id'], unique=False) - op.create_index('star_user_id_repository_id', 'star', ['user_id', 'repository_id'], unique=True) - ### end Alembic commands ### + op.create_table('star', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('repository_id', sa.Integer(), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], name=op.f('fk_star_repository_id_repository')), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_star_user_id_user')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_star')) + ) + op.create_index('star_repository_id', 'star', ['repository_id'], unique=False) + op.create_index('star_user_id', 'star', ['user_id'], unique=False) + op.create_index('star_user_id_repository_id', 'star', ['user_id', 'repository_id'], unique=True) def downgrade(tables): - ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('fk_star_repository_id_repository', 'star', type_='foreignkey') - op.drop_constraint('fk_star_user_id_user', 'star', type_='foreignkey') - op.drop_index('star_user_id_repository_id', table_name='star') - op.drop_index('star_user_id', table_name='star') - op.drop_index('star_repository_id', table_name='star') - op.drop_table('star') - ### end Alembic commands ### + op.drop_constraint('fk_star_repository_id_repository', 'star', type_='foreignkey') + op.drop_constraint('fk_star_user_id_user', 'star', type_='foreignkey') + op.drop_index('star_user_id_repository_id', table_name='star') + op.drop_index('star_user_id', table_name='star') + op.drop_index('star_repository_id', table_name='star') + op.drop_table('star') From bd9f529e380a45808f08191becb3937d32228c0d Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Sun, 30 Nov 2014 17:45:31 -0500 Subject: [PATCH 04/36] Add missing Star import. --- data/model/legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/model/legacy.py b/data/model/legacy.py index e7d05a9aa..52b7ce5d9 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -14,7 +14,7 @@ from data.database import (User, Repository, Image, AccessToken, Role, Repositor ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification, RepositoryAuthorizedEmail, TeamMemberInvite, DerivedImageStorage, ImageStorageTransformation, random_string_generator, - db, BUILD_PHASE, QuayUserField) + db, BUILD_PHASE, QuayUserField, Star) from peewee import JOIN_LEFT_OUTER, fn from util.validation import (validate_username, validate_email, validate_password, INVALID_PASSWORD_MESSAGE) From 4f5a78ca2c6734b70c37ee7b4716bcbf81f056da Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Sun, 30 Nov 2014 17:46:24 -0500 Subject: [PATCH 05/36] Add missing args param. --- endpoints/api/user.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 33c36cabf..427cb5887 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -694,11 +694,13 @@ class StarredRepositoryList(ApiResource): @parse_args @query_param('page', 'Offset page number. (int)', type=int) @query_param('limit', 'Limit on the number of results (int)', type=int) - def get(self): + def get(self, args): """ List all starred repositories. """ page = args['page'] limit = args['limit'] - starred_repos = list(get_user_starred_repositories(get_authenticated_user(), page=page, limit=limit)) + starred_repos = list(model.get_user_starred_repositories(get_authenticated_user(), + page=page, + limit=limit)) return {'repositories': [repo_view(repo) for repo in starred_repos]} @require_scope(scopes.READ_REPO) From 97b605ca8db04f38ef13ea22f61b74ecd0c7867c Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Wed, 3 Dec 2014 12:20:51 -0800 Subject: [PATCH 06/36] Fix starring after rebase. --- data/database.py | 2 +- .../versions/2088f2b81010_add_stars.py | 40 ++++++++++++++++++ .../versions/3b668be15dc0_add_stars.py | 38 ----------------- test/data/test.db | Bin 251904 -> 257024 bytes 4 files changed, 41 insertions(+), 39 deletions(-) create mode 100644 data/migrations/versions/2088f2b81010_add_stars.py delete mode 100644 data/migrations/versions/3b668be15dc0_add_stars.py diff --git a/data/database.py b/data/database.py index 51eeabf6f..e687a67f2 100644 --- a/data/database.py +++ b/data/database.py @@ -253,7 +253,7 @@ class Repository(BaseModel): # Therefore, we define our own deletion order here and use the dependency system to verify it. ordered_dependencies = [RepositoryAuthorizedEmail, RepositoryTag, Image, LogEntry, RepositoryBuild, RepositoryBuildTrigger, RepositoryNotification, - RepositoryPermission, AccessToken] + RepositoryPermission, AccessToken, Star] for query, fk in self.dependencies(search_nullable=True): model = fk.model_class diff --git a/data/migrations/versions/2088f2b81010_add_stars.py b/data/migrations/versions/2088f2b81010_add_stars.py new file mode 100644 index 000000000..d8c75966c --- /dev/null +++ b/data/migrations/versions/2088f2b81010_add_stars.py @@ -0,0 +1,40 @@ +"""add stars + +Revision ID: 2088f2b81010 +Revises: 1c5b738283a5 +Create Date: 2014-12-02 17:45:00.707498 + +""" + +# revision identifiers, used by Alembic. +revision = '2088f2b81010' +down_revision = '1c5b738283a5' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +def upgrade(tables): + op.create_table('star', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('repository_id', sa.Integer(), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], name=op.f('fk_star_repository_id_repository')), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_star_user_id_user')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_star')) + ) + with op.batch_alter_table('star', schema=None) as batch_op: + batch_op.create_index('star_repository_id', ['repository_id'], unique=False) + batch_op.create_index('star_user_id', ['user_id'], unique=False) + batch_op.create_index('star_user_id_repository_id', ['user_id', 'repository_id'], unique=True) + +def downgrade(tables): + op.drop_constraint('fk_star_repository_id_repository', 'star', type_='foreignkey') + op.drop_constraint('fk_star_user_id_user', 'star', type_='foreignkey') + with op.batch_alter_table('star', schema=None) as batch_op: + batch_op.drop_index('star_user_id_repository_id') + batch_op.drop_index('star_user_id') + batch_op.drop_index('star_repository_id') + + op.drop_table('star') diff --git a/data/migrations/versions/3b668be15dc0_add_stars.py b/data/migrations/versions/3b668be15dc0_add_stars.py deleted file mode 100644 index 47f1f1a3a..000000000 --- a/data/migrations/versions/3b668be15dc0_add_stars.py +++ /dev/null @@ -1,38 +0,0 @@ -"""add stars - -Revision ID: 3b668be15dc0 -Revises: 204abf14783d -Create Date: 2014-11-14 14:11:18.687340 - -""" - -# revision identifiers, used by Alembic. -revision = '3b668be15dc0' -down_revision = '204abf14783d' - -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import mysql - -def upgrade(tables): - op.create_table('star', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('repository_id', sa.Integer(), nullable=False), - sa.Column('created', sa.DateTime(), nullable=False), - sa.ForeignKeyConstraint(['repository_id'], ['repository.id'], name=op.f('fk_star_repository_id_repository')), - sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('fk_star_user_id_user')), - sa.PrimaryKeyConstraint('id', name=op.f('pk_star')) - ) - op.create_index('star_repository_id', 'star', ['repository_id'], unique=False) - op.create_index('star_user_id', 'star', ['user_id'], unique=False) - op.create_index('star_user_id_repository_id', 'star', ['user_id', 'repository_id'], unique=True) - - -def downgrade(tables): - op.drop_constraint('fk_star_repository_id_repository', 'star', type_='foreignkey') - op.drop_constraint('fk_star_user_id_user', 'star', type_='foreignkey') - op.drop_index('star_user_id_repository_id', table_name='star') - op.drop_index('star_user_id', table_name='star') - op.drop_index('star_repository_id', table_name='star') - op.drop_table('star') diff --git a/test/data/test.db b/test/data/test.db index cdaa66e57f768e5e4463ddc644aca0f4add444b4..68f54f0a1d3043188d68d7459addaf31b55f2b08 100644 GIT binary patch delta 11546 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 delta 11098 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 From 5a484cfe11545ff0f7ac97b35818df8a8a90f1ec Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Thu, 11 Dec 2014 15:06:30 -0500 Subject: [PATCH 07/36] Initial redesigned UI for repo listings w/ stars. --- data/model/legacy.py | 15 +-- endpoints/api/repository.py | 3 +- endpoints/api/user.py | 21 +++- endpoints/web.py | 5 + static/css/quay.css | 98 ++++++++++++++---- static/js/app.js | 1 + static/js/controllers.js | 82 +++++++++++++-- static/partials/repo-list.html | 183 ++++++++++++++++++++++----------- static/partials/starred.html | 3 + test/data/test.db | Bin 257024 -> 257024 bytes 10 files changed, 308 insertions(+), 103 deletions(-) create mode 100644 static/partials/starred.html diff --git a/data/model/legacy.py b/data/model/legacy.py index 52b7ce5d9..b712a5271 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -778,9 +778,9 @@ def get_visible_repository_count(username=None, include_public=True, def get_visible_repositories(username=None, include_public=True, page=None, - limit=None, sort=False, namespace=None): + limit=None, sort=False, namespace=None, include_starred=True): query = _visible_repository_query(username=username, include_public=include_public, page=page, - limit=limit, namespace=namespace, + limit=limit, namespace=namespace, include_starred=include_starred, select_models=[Repository, Namespace, Visibility]) if sort: @@ -793,7 +793,7 @@ def get_visible_repositories(username=None, include_public=True, page=None, def _visible_repository_query(username=None, include_public=True, limit=None, - page=None, namespace=None, select_models=[]): + page=None, namespace=None, include_starred=True, select_models=[]): query = (Repository .select(*select_models) # MySQL/RDS complains is there are selected models for counts. .distinct() @@ -803,8 +803,7 @@ def _visible_repository_query(username=None, include_public=True, limit=None, .switch(Repository) .join(RepositoryPermission, JOIN_LEFT_OUTER)) - query = _filter_to_repos_for_user(query, username, namespace, include_public) - + query = _filter_to_repos_for_user(query, username, namespace, include_public, include_starred) if page: query = query.paginate(page, limit) elif limit: @@ -814,7 +813,7 @@ def _visible_repository_query(username=None, include_public=True, limit=None, def _filter_to_repos_for_user(query, username=None, namespace=None, - include_public=True): + include_public=True, include_starred=True): if not include_public and not username: return Repository.select().where(Repository.id == '-1') @@ -825,6 +824,7 @@ def _filter_to_repos_for_user(query, username=None, namespace=None, AdminTeam = Team.alias() AdminTeamMember = TeamMember.alias() AdminUser = User.alias() + UserThroughStar = User.alias() query = (query .switch(RepositoryPermission) @@ -844,6 +844,9 @@ def _filter_to_repos_for_user(query, username=None, namespace=None, where_clause = ((User.username == username) | (UserThroughTeam.username == username) | ((AdminUser.username == username) & (TeamRole.name == 'admin'))) + if not include_starred: + subquery = Repository.select().join(Star).join(User).where(User.username == username).alias() + where_clause = where_clause & ~(Repository.id << subquery) if namespace: where_clause = where_clause & (Namespace.username == namespace) diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index 9302f26a0..488053014 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -107,6 +107,7 @@ class RepositoryList(ApiResource): @query_param('sort', 'Whether to sort the results.', type=truthy_bool, default=False) @query_param('count', 'Whether to include a count of the total number of results available.', type=truthy_bool, default=False) + @query_param('starred', 'Whether or not to include starred repositories', type=truthy_bool, default=True) def get(self, args): """Fetch the list of repositories under a variety of situations.""" username = None @@ -123,7 +124,7 @@ class RepositoryList(ApiResource): repo_query = model.get_visible_repositories(username, limit=args['limit'], page=args['page'], include_public=args['public'], sort=args['sort'], - namespace=args['namespace']) + namespace=args['namespace'], include_starred=args['starred']) response['repositories'] = [repo_view(repo) for repo in repo_query if (repo.visibility.name == 'public' or diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 427cb5887..1d12d84b6 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -4,6 +4,7 @@ import json from flask import request from flask.ext.login import logout_user from flask.ext.principal import identity_changed, AnonymousIdentity +from peewee import IntegrityError from app import app, billing as stripe, authentication, avatar from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, @@ -713,10 +714,17 @@ class StarredRepositoryList(ApiResource): namespace = req['namespace'] repository = req['repository'] repo = model.get_repository(namespace, repository) + if repo: - model.star_repository(user, repo) - log_action('star_repository', user.username, namespace, - {'repo': repository, 'namespace': namespace}) + try: + model.star_repository(user, repo) + except IntegrityError: + pass + + #TODO(jzelinskie): log this action + #log_action('star_repository', user.username, namespace, + # {'repo': repository, 'namespace': namespace}) + return { 'namespace': namespace, 'repository': repository, @@ -732,10 +740,13 @@ class StarredRepository(RepositoryParamResource): def delete(self, namespace, repository): user = get_authenticated_user() repo = model.get_repository(namespace, repository) + if repo: model.unstar_repository(user, repo) - log_action('unstar_repository', user.username, namespace, - {'repo': repository, 'namespace': namespace}) + + #TODO(jzelinskie): log this action + #log_action('unstar_repository', user.username, namespace, + # {'repo': repository, 'namespace': namespace}) return 'Deleted', 204 raise NotFound() diff --git a/endpoints/web.py b/endpoints/web.py index 4717f7d40..998675ac4 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -139,6 +139,11 @@ def confirm_invite(): def repository(path): return index('') +@web.route('/starred/') +@no_cache +def starred(): + return index('') + @web.route('/security/') @no_cache diff --git a/static/css/quay.css b/static/css/quay.css index d15ca1cb4..13bc91905 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -932,6 +932,8 @@ i.toggle-icon:hover { } .repo-circle { + color: #999; + display: inline-block; position: relative; background: #eee; padding: 4px; @@ -939,7 +941,6 @@ i.toggle-icon:hover { display: inline-block; width: 46px; height: 46px; - text-align: center; } .repo-circle.no-background { @@ -950,11 +951,11 @@ i.toggle-icon:hover { } .repo-circle .fa-hdd-o { - font-size: 36px; + font-size: 1.7em; } .repo-circle.no-background .fa-hdd-o { - font-size: 30px; + font-size: 1.7em; } .repo-circle .fa-lock { @@ -962,18 +963,18 @@ i.toggle-icon:hover { bottom: -2px; right: -4px; background: rgb(253, 191, 191); - width: 20px; + width: 16px; display: inline-block; border-radius: 50%; text-align: center; - height: 20px; - line-height: 21px; - font-size: 16px !important; + height: 16px; + line-height: 16px; + font-size: 12px !important; } .repo-circle.no-background .fa-lock { - bottom: -2px; - right: -6px; + bottom: 5px; + right: 7px; color: #444; } @@ -2480,10 +2481,41 @@ p.editable:hover i { cursor: pointer; } +.empty-primary-msg { + font-size: 18px; + margin-bottom: 30px; + text-align: center; +} + +.empty-secondary-msg { + font-size: 14px; + color: #999; + text-align: center; + margin-bottom: 10px; +} + .repo-list { margin-bottom: 40px; } +.repo-list-title { + margin-bottom: 30px; + margin-top: 10px; + line-height: 24px; + font-size: 18px; +} + +.repo-list-title a { + font-size: 18px; + margin: 0; + display: inline-block; +} + +.repo-list-title i { + display: inline-block; + margin-right: 5px; +} + .repo-list .button-bar-right { float: right; } @@ -2503,11 +2535,42 @@ p.editable:hover i { margin-right: 10px; } +.repo-panel { + padding: 20px; + border: 1px solid #eee; + margin-bottom: 30px; +} + +.panel-body.starred { + background: -moz-linear-gradient(top, rgba(255,240,188,1) 0%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0.49) 51%, rgba(255,255,255,0) 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,240,188,1)), color-stop(50%,rgba(255,255,255,0.5)), color-stop(51%,rgba(255,255,255,0.49)), color-stop(100%,rgba(255,255,255,0))); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, rgba(255,240,188,1) 0%,rgba(255,255,255,0.5) 50%,rgba(255,255,255,0.49) 51%,rgba(255,255,255,0) 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, rgba(255,240,188,1) 0%,rgba(255,255,255,0.5) 50%,rgba(255,255,255,0.49) 51%,rgba(255,255,255,0) 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, rgba(255,240,188,1) 0%,rgba(255,255,255,0.5) 50%,rgba(255,255,255,0.49) 51%,rgba(255,255,255,0) 100%); /* IE10+ */ + background: linear-gradient(to bottom, rgba(255,240,188,1) 0%,rgba(255,255,255,0.5) 50%,rgba(255,255,255,0.49) 51%,rgba(255,255,255,0) 100%); /* W3C */ + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fff0bc', endColorstr='#00ffffff',GradientType=0 ); /* IE6-9 */ +} + +.star-icon { + color: #ddd; + display: block; + font-size: 1.2em; + text-align: right; + line-height: 2em; +} + +.star-icon:hover { + cursor: pointer; + cursor: hand; +} + +.star-icon.starred { + color: #ffba6d; +} + .repo-listing { display: block; - margin-bottom: 20px; border-bottom: 1px solid #eee; - padding: 10px; font-size: 14px; line-height: normal; } @@ -2521,18 +2584,13 @@ p.editable:hover i { margin-bottom: 0px; } -.repo-listing a { - font-size: 1.5em; -} - -.repo-listing i { - color: #999; - display: inline-block; - margin-right: 6px; +.repo-panel-repo-link { + font-size: 1.2em; } .repo-listing .description { - padding-left: 44px; + font-size: 0.91em; + padding-top: 13px; } diff --git a/static/js/app.js b/static/js/app.js index 5bb37a338..1a84fef29 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2267,6 +2267,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl, pageClass: 'landing-page'}). + when ('/starred/', {title: 'Starred Repositories', templateUrl: '/static/partials/starred.html', controller: StarCtrl}). otherwise({redirectTo: '/'}); }]). config(function(RestangularProvider) { diff --git a/static/js/controllers.js b/static/js/controllers.js index 9bea2ebdb..16e48df3f 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -237,14 +237,52 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { $scope.publicPageCount = null; // Monitor changes in the user. - UserService.updateUserIn($scope, function() { - loadMyRepos($scope.namespace); + UserService.load(function() { + console.log("updateUserIn"); + var user = UserService.currentUser(); + $scope.namespaces = [user]; + for (var i = 0; i < user.organizations.length; i++) { + $scope.namespaces.push(user.organizations[i]); + } + loadStarredRepos(); + loadRepos(); + console.log($scope.namespaces); }); + // Monitor changes in the namespace. - $scope.$watch('namespace', function(namespace) { - loadMyRepos(namespace); - }); + //$scope.$watch('namespace', function(namespace) { + // loadStarredRepos($scope.namespace) + // loadRepos(); + //}); + + + $scope.starRepo = function(repo) { + var data = { + 'namespace': repo.namespace, + 'repository': repo.name + }; + ApiService.createStar(data).then(function(result) { + loadStarredRepos($scope.namespace); + loadRepos($scope.namespace); + }, function(result) { + loadStarredRepos(); + loadRepos(); + }); + }; + + $scope.unstarRepo = function(repo) { + var data = { + 'repository': repo.namespace + '/' + repo.name + }; + ApiService.deleteStar(null, data).then(function(result) { + loadStarredRepos($scope.namespace); + loadRepos($scope.namespace); + }, function(result) { + loadStarredRepos($scope.namespace); + loadRepos(); + }); + }; $scope.movePublicPage = function(increment) { if ($scope.publicPageCount == null) { @@ -263,18 +301,36 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { loadPublicRepos(); }; - var loadMyRepos = function(namespace) { - if (!$scope.user || $scope.user.anonymous || !namespace) { + var loadStarredRepos = function() { + if (!$scope.user || $scope.user.anonymous) { return; } - var options = {'public': false, 'sort': true, 'namespace': namespace}; - - $scope.user_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { + $scope.starred_repositories = ApiService.listStarredReposAsResource().get(function(resp) { return resp.repositories; }); }; + var loadRepos = function() { + if ($scope.namespaces.length == 0 || $scope.user.anonymous) { + return; + } + + for (var i = 0; i < $scope.namespaces.length; i++) { + var namespace = $scope.namespaces[i]; + var namespaceName = namespace.username || namespace.name; + var options = { + 'public': false, + 'sort': true, + 'namespace': namespaceName, + 'starred': false, + }; + namespace.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { + return resp.repositories; + }); + } + }; + var loadPublicRepos = function() { var options = { 'public': true, @@ -293,7 +349,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { }); }; - loadPublicRepos(); + //loadPublicRepos(); } function LandingCtrl($scope, UserService, ApiService, Features, Config) { @@ -401,6 +457,10 @@ function LandingCtrl($scope, UserService, ApiService, Features, Config) { }; } +function StarCtrl($scope) { + $scope.test = "hello"; +} + function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout, Config) { $scope.Config = Config; diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index 3e69f3d1a..268aedcde 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -1,73 +1,136 @@ -
-
-
-
- - - - - - +
+
+ +
+
+ Users +
+ - -
- -

Your Repositories

-

Repositories

- -
- -
-
- - - {{repository.namespace}}/{{repository.name}} +
+
+ Organizations +
+
- - -
-
-

You don't have any repositories yet!

-

This organization doesn't have any repositories, or you have not been provided access.

- Click here to learn how to create a repository
-
-
-

Top Public Repositories

-
-
- - - {{repository.namespace}}/{{repository.name}} - -
+
+
+
+
+ + Starred +
+
+
+ +
+
+
You haven't starred any repositories yet.
+
Stars allow you to easily access your favorite repositories.
+
+
+
+
+
+
+
-
- - +
+ + +
+
+
+
+ + {{ namespace.username }} +
+ +
+
+ +
+
There aren't any repositories yet.
+
This origanization does't have any repositories, or you have not been provided access. Create a new repository +
+ + +
+
+

You don't have any repositories yet!

+

This organization doesn't have any repositories, or you have not been provided access.

+ Click here to learn how to create a repository +
+
+
+
+
+
diff --git a/static/partials/starred.html b/static/partials/starred.html new file mode 100644 index 000000000..b07e1bad9 --- /dev/null +++ b/static/partials/starred.html @@ -0,0 +1,3 @@ +
+ {{ test }} +
diff --git a/test/data/test.db b/test/data/test.db index 68f54f0a1d3043188d68d7459addaf31b55f2b08..0dea8501f9ef5c530ff9594cc6f9265ea189bdd4 100644 GIT binary patch delta 8925 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 E02I;qJpcdz delta 8915 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 Date: Mon, 29 Dec 2014 14:11:46 -0500 Subject: [PATCH 08/36] remove 'include_starred' option on repo listings --- data/model/legacy.py | 15 +++++---------- endpoints/api/repository.py | 3 +-- static/js/controllers.js | 15 ++++----------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/data/model/legacy.py b/data/model/legacy.py index b712a5271..a399fb465 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -778,9 +778,9 @@ def get_visible_repository_count(username=None, include_public=True, def get_visible_repositories(username=None, include_public=True, page=None, - limit=None, sort=False, namespace=None, include_starred=True): + limit=None, sort=False, namespace=None): query = _visible_repository_query(username=username, include_public=include_public, page=page, - limit=limit, namespace=namespace, include_starred=include_starred, + limit=limit, namespace=namespace, select_models=[Repository, Namespace, Visibility]) if sort: @@ -793,7 +793,7 @@ def get_visible_repositories(username=None, include_public=True, page=None, def _visible_repository_query(username=None, include_public=True, limit=None, - page=None, namespace=None, include_starred=True, select_models=[]): + page=None, namespace=None, select_models=[]): query = (Repository .select(*select_models) # MySQL/RDS complains is there are selected models for counts. .distinct() @@ -803,7 +803,7 @@ def _visible_repository_query(username=None, include_public=True, limit=None, .switch(Repository) .join(RepositoryPermission, JOIN_LEFT_OUTER)) - query = _filter_to_repos_for_user(query, username, namespace, include_public, include_starred) + query = _filter_to_repos_for_user(query, username, namespace, include_public) if page: query = query.paginate(page, limit) elif limit: @@ -812,8 +812,7 @@ def _visible_repository_query(username=None, include_public=True, limit=None, return query -def _filter_to_repos_for_user(query, username=None, namespace=None, - include_public=True, include_starred=True): +def _filter_to_repos_for_user(query, username=None, namespace=None, include_public=True): if not include_public and not username: return Repository.select().where(Repository.id == '-1') @@ -824,7 +823,6 @@ def _filter_to_repos_for_user(query, username=None, namespace=None, AdminTeam = Team.alias() AdminTeamMember = TeamMember.alias() AdminUser = User.alias() - UserThroughStar = User.alias() query = (query .switch(RepositoryPermission) @@ -844,9 +842,6 @@ def _filter_to_repos_for_user(query, username=None, namespace=None, where_clause = ((User.username == username) | (UserThroughTeam.username == username) | ((AdminUser.username == username) & (TeamRole.name == 'admin'))) - if not include_starred: - subquery = Repository.select().join(Star).join(User).where(User.username == username).alias() - where_clause = where_clause & ~(Repository.id << subquery) if namespace: where_clause = where_clause & (Namespace.username == namespace) diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index 488053014..9302f26a0 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -107,7 +107,6 @@ class RepositoryList(ApiResource): @query_param('sort', 'Whether to sort the results.', type=truthy_bool, default=False) @query_param('count', 'Whether to include a count of the total number of results available.', type=truthy_bool, default=False) - @query_param('starred', 'Whether or not to include starred repositories', type=truthy_bool, default=True) def get(self, args): """Fetch the list of repositories under a variety of situations.""" username = None @@ -124,7 +123,7 @@ class RepositoryList(ApiResource): repo_query = model.get_visible_repositories(username, limit=args['limit'], page=args['page'], include_public=args['public'], sort=args['sort'], - namespace=args['namespace'], include_starred=args['starred']) + namespace=args['namespace']) response['repositories'] = [repo_view(repo) for repo in repo_query if (repo.visibility.name == 'public' or diff --git a/static/js/controllers.js b/static/js/controllers.js index 16e48df3f..bce9928d6 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -236,9 +236,8 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { $scope.page = 1; $scope.publicPageCount = null; - // Monitor changes in the user. + // Grab user namespaces UserService.load(function() { - console.log("updateUserIn"); var user = UserService.currentUser(); $scope.namespaces = [user]; for (var i = 0; i < user.organizations.length; i++) { @@ -246,7 +245,6 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { } loadStarredRepos(); loadRepos(); - console.log($scope.namespaces); }); @@ -263,11 +261,9 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { 'repository': repo.name }; ApiService.createStar(data).then(function(result) { - loadStarredRepos($scope.namespace); - loadRepos($scope.namespace); + loadStarredRepos(); }, function(result) { loadStarredRepos(); - loadRepos(); }); }; @@ -276,11 +272,9 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { 'repository': repo.namespace + '/' + repo.name }; ApiService.deleteStar(null, data).then(function(result) { - loadStarredRepos($scope.namespace); - loadRepos($scope.namespace); + loadStarredRepos(); }, function(result) { - loadStarredRepos($scope.namespace); - loadRepos(); + loadStarredRepos(); }); }; @@ -323,7 +317,6 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { 'public': false, 'sort': true, 'namespace': namespaceName, - 'starred': false, }; namespace.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { return resp.repositories; From 17751eced94bd88a71555d23eb8188acf2e07a3f Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Mon, 29 Dec 2014 15:39:04 -0500 Subject: [PATCH 09/36] css overflow ellipsis to titles and descriptions --- static/css/quay.css | 12 ++++++++++++ static/partials/repo-list.html | 16 ++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/static/css/quay.css b/static/css/quay.css index 13bc91905..e62666b73 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -2541,6 +2541,12 @@ p.editable:hover i { margin-bottom: 30px; } +.repo-panel-title-row { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + .panel-body.starred { background: -moz-linear-gradient(top, rgba(255,240,188,1) 0%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0.49) 51%, rgba(255,255,255,0) 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,240,188,1)), color-stop(50%,rgba(255,255,255,0.5)), color-stop(51%,rgba(255,255,255,0.49)), color-stop(100%,rgba(255,255,255,0))); /* Chrome,Safari4+ */ @@ -2593,6 +2599,12 @@ p.editable:hover i { padding-top: 13px; } +.repo-listing .description p:last-child { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + .repo-build .build-id:before { content: "Build ID: " diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index 268aedcde..312995e30 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -50,18 +50,18 @@
@@ -98,11 +98,11 @@
-
-
There aren't any repositories yet.
-
This origanization does't have any repositories, or you have not been provided access. Create a new repository +
This namespace doesn't have any viewable repositories.
+
You may be seeing this because you do not have permission to view any repositories. If you have permission, try creating a new repository.
From 8464b54ad93b3d4a710c65c3db4b493bca714c6f Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Tue, 30 Dec 2014 15:07:14 -0500 Subject: [PATCH 10/36] star status shown in normal repo listings --- endpoints/api/repository.py | 21 ++++++++++++--------- endpoints/api/user.py | 9 ++++++++- static/js/controllers.js | 4 ++++ static/partials/repo-list.html | 5 +++-- test/data/test.db | Bin 257024 -> 257024 bytes 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index 9302f26a0..12d4fef44 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -112,6 +112,8 @@ class RepositoryList(ApiResource): username = None if get_authenticated_user() and args['private']: username = get_authenticated_user().username + starred_repos = model.get_user_starred_repositories(get_authenticated_user()) + star_lookup = set([repo.id for repo in starred_repos]) response = {} @@ -124,6 +126,16 @@ class RepositoryList(ApiResource): repo_query = model.get_visible_repositories(username, limit=args['limit'], page=args['page'], include_public=args['public'], sort=args['sort'], namespace=args['namespace']) + def repo_view(repo_obj): + repo = { + 'namespace': repo_obj.namespace_user.username, + 'name': repo_obj.name, + 'description': repo_obj.description, + 'is_public': repo_obj.visibility.name == 'public', + } + if get_authenticated_user(): + repo['is_starred'] = repo_obj.id in star_lookup + return repo response['repositories'] = [repo_view(repo) for repo in repo_query if (repo.visibility.name == 'public' or @@ -133,15 +145,6 @@ class RepositoryList(ApiResource): return response -def repo_view(repo_obj): - return { - 'namespace': repo_obj.namespace_user.username, - 'name': repo_obj.name, - 'description': repo_obj.description, - 'is_public': repo_obj.visibility.name == 'public', - } - - @resource('/v1/repository/') @path_param('repository', 'The full path of the repository. e.g. namespace/name') class Repository(RepositoryParamResource): diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 1d12d84b6..cddca5b93 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -15,7 +15,6 @@ from endpoints.api import (ApiResource, nickname, resource, validate_json_reques from endpoints.api.subscribe import subscribe from endpoints.common import common_login from endpoints.api.team import try_accept_invite -from endpoints.api.repository import repo_view from data import model from data.billing import get_plan @@ -702,6 +701,14 @@ class StarredRepositoryList(ApiResource): starred_repos = list(model.get_user_starred_repositories(get_authenticated_user(), page=page, limit=limit)) + def repo_view(repo_obj): + return { + 'namespace': repo_obj.namespace_user.username, + 'name': repo_obj.name, + 'description': repo_obj.description, + 'is_public': repo_obj.visibility.name == 'public', + } + return {'repositories': [repo_view(repo) for repo in starred_repos]} @require_scope(scopes.READ_REPO) diff --git a/static/js/controllers.js b/static/js/controllers.js index bce9928d6..6cd17c30c 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -262,8 +262,10 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { }; ApiService.createStar(data).then(function(result) { loadStarredRepos(); + loadRepos(); }, function(result) { loadStarredRepos(); + loadRepos(); }); }; @@ -273,8 +275,10 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { }; ApiService.deleteStar(null, data).then(function(result) { loadStarredRepos(); + loadRepos(); }, function(result) { loadStarredRepos(); + loadRepos(); }); }; diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index 312995e30..fff1c966b 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -96,7 +96,7 @@
-
+
@@ -106,7 +106,8 @@
- + +
diff --git a/test/data/test.db b/test/data/test.db index 0dea8501f9ef5c530ff9594cc6f9265ea189bdd4..94762ac06ecba290290fc61220753a9ab4de18d0 100644 GIT binary patch delta 722 zcma))O=uHQ6oucrXKH@OYOM=@XvQGLg`Lby62}BYq@eh-5X60JP1D#m2}z`gh|NMo zXuu*kv^zl%!9^jBLu)~b=q7GNv;{Q~6+|sm6cyWmz9i61Hu4sCanAehefOR5<@WLA z_M5x7HMzT&Tlv0HAHP%d-~#ioZiX4wZFy(;&cprWV6Gi}_-r;YnjVSKnMP4Ev2-e% z$fYuuRAnfaOJ}2jKs=Efx_C|T;ZdBZ)5Q$~ql9wpmdVuYSuSEC6JaOJ8V2>PhT)>J9t5_1iG9Ke8 zKH~%4p@_Nq3j}Nux0FK?b;|afhfYdeG9{Vl_XKxw4TCs_eQ?niD$@fhP*QxuDY6P3 zbp0LsAj0t6QKTSgC7QMMQ)O6~PNm zwiJr!AVhnq)z(njML`@&g`%h(#8$BAP%47HWYp=v`F?*nr(;za{>og$6TpZzr=up8lmV( zG?R9{b~n?W?r>bUkj)j+`IPJC{B*|Qs!;cZm&$kgey8tHRq!3*6+AbU&G}g`|9+dK zw6e%7ZD^h@(X3d46i0*Bz?r}mE^$_B>^nehNGhTzszcfTKPl#fG}Z*xtiGlNO!tM& z@(Y&c(+%}BX_N(a@eO5M;~y??g5%W|ep6*-a92d*qo`!IPV&289Wj50sK#uDx%+{} z2Z3|^2rg{lGrr;!zp!r;5jGV$<2;sjBgVZlp?ON+7+>%f;}}3M9zpSM-r^;m3SRk~ j&2XHx*%>2c4&$sQ7UOIe Date: Wed, 7 Jan 2015 17:42:52 -0500 Subject: [PATCH 11/36] Hide

for empty text from markdown. --- static/js/app.js | 5 ++++- static/partials/repo-list.html | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index 1a84fef29..63eee0d6e 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -120,7 +120,10 @@ function createOrganizationTeam(ApiService, orgname, teamname, callback) { } function getMarkedDown(string) { - return Markdown.getSanitizingConverter().makeHtml(string || ''); + // This automatically puts text into paragraph tags, which may or may not effect + // the style of a page. For consistency, we make sure empty text is paragraph tags. + var html = Markdown.getSanitizingConverter().makeHtml(string || ''); + return html == '' ? '

easter egg

' : html; } diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index fff1c966b..532d976eb 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -1,5 +1,5 @@
-
+
From 0aae47fa7c5316230e166010f0d157d781620d2c Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Thu, 8 Jan 2015 17:31:18 -0500 Subject: [PATCH 14/36] remove public repo code;redirect to signin instead --- static/js/controllers.js | 56 +++++----------------------------------- 1 file changed, 7 insertions(+), 49 deletions(-) diff --git a/static/js/controllers.js b/static/js/controllers.js index 41a97c58f..ebcbf2313 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -231,7 +231,7 @@ function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService, Conf }; } -function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { +function RepoListCtrl($scope, $location, $sanitize, Restangular, UserService, ApiService) { $scope.namespace = null; $scope.page = 1; $scope.publicPageCount = null; @@ -246,16 +246,11 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { } loadStarredRepos(); loadRepos(); + } else { + $location.path('signin'); } }); - - // Monitor changes in the namespace. - //$scope.$watch('namespace', function(namespace) { - // loadStarredRepos($scope.namespace) - // loadRepos(); - //}); - $scope.toggleStar = function(repo) { if (repo.is_starred) { unstarRepo(repo); @@ -270,7 +265,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { 'repository': repo.name }; ApiService.createStar(data).then(function(result) { - addToStarred(repo); + updateReposAfterStar(repo); }, function(result) { // TODO(jzelinskie): have some kind of pop-up for star failure }); @@ -281,7 +276,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { 'repository': repo.namespace + '/' + repo.name }; ApiService.deleteStar(null, data).then(function(result) { - removeFromStarred(repo); + updateReposAfterUnstar(repo); }, function(result) { // TODO(jzelinskie): have some kind of pop-up for star failure }); @@ -301,7 +296,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { return repoIndex != -1 ? namespace.repositories.value[repoIndex] : null; } - var addToStarred = function(repository) { + var updateReposAfterStar = function(repository) { $scope.starred_repositories.value.push(repository); var repo = findRepoInList(repository.namespace, repository.name); @@ -310,7 +305,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { } } - var removeFromStarred = function(repository) { + var updateReposAfterUnstar = function(repository) { // Remove from the starred listings var index = $scope.starred_repositories.value.map(function (r) { return r.namespace + '/' + r.name; @@ -324,23 +319,6 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { } }; - $scope.movePublicPage = function(increment) { - if ($scope.publicPageCount == null) { - return; - } - - $scope.page += increment; - if ($scope.page < 1) { - $scope.page = 1; - } - - if ($scope.page > $scope.publicPageCount) { - $scope.page = $scope.publicPageCount; - } - - loadPublicRepos(); - }; - var loadStarredRepos = function() { if (!$scope.user || $scope.user.anonymous) { return; @@ -372,26 +350,6 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { }); } }; - - var loadPublicRepos = function() { - var options = { - 'public': true, - 'private': false, - 'sort': true, - 'limit': 10, - 'page': $scope.page, - 'count': $scope.page == 1 - }; - - $scope.public_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { - if (resp.count) { - $scope.publicPageCount = Math.ceil(resp.count / 10); - } - return resp.repositories; - }); - }; - - //loadPublicRepos(); } function LandingCtrl($scope, UserService, ApiService, Features, Config) { From 897cfbefd6235f5c04f7d7281eee17de6b7492da Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Fri, 9 Jan 2015 15:55:20 -0500 Subject: [PATCH 15/36] comment and cleanup repolistctrl --- static/js/controllers.js | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/static/js/controllers.js b/static/js/controllers.js index ebcbf2313..53de635f0 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -231,12 +231,13 @@ function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService, Conf }; } -function RepoListCtrl($scope, $location, $sanitize, Restangular, UserService, ApiService) { +function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { $scope.namespace = null; $scope.page = 1; $scope.publicPageCount = null; - // Grab user namespaces + // When loading the UserService, if the user is logged in, create a list of + // relevant namespaces for later collecting relevant repositories. UserService.load(function() { var user = UserService.currentUser(); if (!user.anonymous) { @@ -244,13 +245,26 @@ function RepoListCtrl($scope, $location, $sanitize, Restangular, UserService, Ap for (var i = 0; i < user.organizations.length; i++) { $scope.namespaces.push(user.organizations[i]); } - loadStarredRepos(); - loadRepos(); - } else { - $location.path('signin'); + //loadStarredRepos(); + //loadRepos(); } }); + // If someone signs in on this page, we have to watch the user and re-load + // their repositories after they've signed in to actually have any content + // on the page. + $scope.$watch(function(scope) { return scope.user }, + function(user) { + if (!user.anonymous) { + $scope.namespaces = [user]; + for (var i = 0; i < user.organizations.length; i++) { + $scope.namespaces.push(user.organizations[i]); + } + loadStarredRepos(); + loadRepos(); + } + }); + $scope.toggleStar = function(repo) { if (repo.is_starred) { unstarRepo(repo); @@ -282,6 +296,7 @@ function RepoListCtrl($scope, $location, $sanitize, Restangular, UserService, Ap }); }; + // Finds a repository within the list of namespaces attached to $scope. var findRepoInList = function(repoNamespace, repoName) { var namespaceIndex = $scope.namespaces.map(function (n) { return n.username || n.name; @@ -296,6 +311,8 @@ function RepoListCtrl($scope, $location, $sanitize, Restangular, UserService, Ap return repoIndex != -1 ? namespace.repositories.value[repoIndex] : null; } + // Add a starred repository to the list starred repository list and make + // sure it appears starred elsewhere on the page. var updateReposAfterStar = function(repository) { $scope.starred_repositories.value.push(repository); @@ -305,9 +322,11 @@ function RepoListCtrl($scope, $location, $sanitize, Restangular, UserService, Ap } } + // Remove a repository from the starred repository list and make sure that + // it doesn't appear starred elsewhere on the page. var updateReposAfterUnstar = function(repository) { // Remove from the starred listings - var index = $scope.starred_repositories.value.map(function (r) { + var index = $scope.starred_repositories.value.map(function(r) { return r.namespace + '/' + r.name; }).indexOf(repository.namespace + '/' + repository.name); $scope.starred_repositories.value.splice(index, 1); @@ -325,17 +344,20 @@ function RepoListCtrl($scope, $location, $sanitize, Restangular, UserService, Ap } $scope.starred_repositories = ApiService.listStarredReposAsResource().get(function(resp) { - return resp.repositories.map(function (repo) { + return resp.repositories.map(function(repo) { repo.is_starred = true; return repo; }); }); }; + // Iterate over all of the $scope.namespaces and collect their respective + // repositories. var loadRepos = function() { if ($scope.namespaces.length == 0 || $scope.user.anonymous) { return; } + console.log('load repos called'); for (var i = 0; i < $scope.namespaces.length; i++) { var namespace = $scope.namespaces[i]; From 5ed0e162a7c94baf736f9db7eb6741e74f5cde8f Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Fri, 9 Jan 2015 15:55:44 -0500 Subject: [PATCH 16/36] fix empty repo listings & indentation --- static/partials/repo-list.html | 66 ++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index b0e1ddc3d..8dc81ddeb 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -1,3 +1,12 @@ + +
@@ -92,44 +101,39 @@ {{ namespace.name }}
-
-
-
-
-
-
-
- - - {{ repository.namespace }}/{{ repository.name }} - +
+
+ -
-
-
This namespace doesn't have any viewable repositories.
-
You may be seeing this because you do not have permission to view any repositories. If you have permission, try creating a new repository.
-
- - -
-
-

You don't have any repositories yet!

-

This organization doesn't have any repositories, or you have not been provided access.

- Click here to learn how to create a repository +
+
+
This namespace doesn't have any viewable repositories.
+
Either no repositories exist yet or you may not have permission to view any. If you have permission, try creating a new repository.
+
+
+
+
+
+
-
From 2ed56f04c9b7a99f3593fcbb09fbdaa812aec847 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Mon, 12 Jan 2015 12:05:58 -0500 Subject: [PATCH 17/36] gruntfile: recursively include *.js in js dir This CL also begins placing controllers into their own directory and individual files. --- grunt/Gruntfile.js | 2 +- static/js/controllers.js | 143 ------------------------------ static/js/controllers/repolist.js | 142 +++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 144 deletions(-) create mode 100644 static/js/controllers/repolist.js diff --git a/grunt/Gruntfile.js b/grunt/Gruntfile.js index 526252bd1..5dd381e8d 100644 --- a/grunt/Gruntfile.js +++ b/grunt/Gruntfile.js @@ -25,7 +25,7 @@ module.exports = function(grunt) { }, }, build: { - src: ['../static/lib/**/*.js', '../static/js/*.js', '../static/dist/template-cache.js'], + src: ['../static/lib/**/*.js', '../static/js/**/*.js', '../static/dist/template-cache.js'], dest: '../static/dist/<%= pkg.name %>.js' } }, diff --git a/static/js/controllers.js b/static/js/controllers.js index 53de635f0..df183a844 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -231,149 +231,6 @@ function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService, Conf }; } -function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { - $scope.namespace = null; - $scope.page = 1; - $scope.publicPageCount = null; - - // When loading the UserService, if the user is logged in, create a list of - // relevant namespaces for later collecting relevant repositories. - UserService.load(function() { - var user = UserService.currentUser(); - if (!user.anonymous) { - $scope.namespaces = [user]; - for (var i = 0; i < user.organizations.length; i++) { - $scope.namespaces.push(user.organizations[i]); - } - //loadStarredRepos(); - //loadRepos(); - } - }); - - // If someone signs in on this page, we have to watch the user and re-load - // their repositories after they've signed in to actually have any content - // on the page. - $scope.$watch(function(scope) { return scope.user }, - function(user) { - if (!user.anonymous) { - $scope.namespaces = [user]; - for (var i = 0; i < user.organizations.length; i++) { - $scope.namespaces.push(user.organizations[i]); - } - loadStarredRepos(); - loadRepos(); - } - }); - - $scope.toggleStar = function(repo) { - if (repo.is_starred) { - unstarRepo(repo); - } else { - starRepo(repo); - } - } - - var starRepo = function(repo) { - var data = { - 'namespace': repo.namespace, - 'repository': repo.name - }; - ApiService.createStar(data).then(function(result) { - updateReposAfterStar(repo); - }, function(result) { - // TODO(jzelinskie): have some kind of pop-up for star failure - }); - }; - - var unstarRepo = function(repo) { - var data = { - 'repository': repo.namespace + '/' + repo.name - }; - ApiService.deleteStar(null, data).then(function(result) { - updateReposAfterUnstar(repo); - }, function(result) { - // TODO(jzelinskie): have some kind of pop-up for star failure - }); - }; - - // Finds a repository within the list of namespaces attached to $scope. - var findRepoInList = function(repoNamespace, repoName) { - var namespaceIndex = $scope.namespaces.map(function (n) { - return n.username || n.name; - }).indexOf(repoNamespace); - - var namespace = $scope.namespaces[namespaceIndex] - - var repoIndex = namespace.repositories.value.map(function (r) { - return r.namespace + '/' + r.name; - }).indexOf(repoNamespace + '/' + repoName); - - return repoIndex != -1 ? namespace.repositories.value[repoIndex] : null; - } - - // Add a starred repository to the list starred repository list and make - // sure it appears starred elsewhere on the page. - var updateReposAfterStar = function(repository) { - $scope.starred_repositories.value.push(repository); - - var repo = findRepoInList(repository.namespace, repository.name); - if (repo != null) { - repo.is_starred = true; - } - } - - // Remove a repository from the starred repository list and make sure that - // it doesn't appear starred elsewhere on the page. - var updateReposAfterUnstar = function(repository) { - // Remove from the starred listings - var index = $scope.starred_repositories.value.map(function(r) { - return r.namespace + '/' + r.name; - }).indexOf(repository.namespace + '/' + repository.name); - $scope.starred_repositories.value.splice(index, 1); - - // Set repo from the normal listings to unstarred. - var repo = findRepoInList(repository.namespace, repository.name); - if (repo != null) { - repo.is_starred = false; - } - }; - - var loadStarredRepos = function() { - if (!$scope.user || $scope.user.anonymous) { - return; - } - - $scope.starred_repositories = ApiService.listStarredReposAsResource().get(function(resp) { - return resp.repositories.map(function(repo) { - repo.is_starred = true; - return repo; - }); - }); - }; - - // Iterate over all of the $scope.namespaces and collect their respective - // repositories. - var loadRepos = function() { - if ($scope.namespaces.length == 0 || $scope.user.anonymous) { - return; - } - console.log('load repos called'); - - for (var i = 0; i < $scope.namespaces.length; i++) { - var namespace = $scope.namespaces[i]; - var namespaceName = namespace.username || namespace.name; - var options = { - 'public': false, - 'sort': true, - 'namespace': namespaceName, - }; - namespace.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { - return resp.repositories; - }); - } - }; -} - function LandingCtrl($scope, UserService, ApiService, Features, Config) { $scope.namespace = null; $scope.currentScreenshot = 'repo-view'; diff --git a/static/js/controllers/repolist.js b/static/js/controllers/repolist.js new file mode 100644 index 000000000..f1d196570 --- /dev/null +++ b/static/js/controllers/repolist.js @@ -0,0 +1,142 @@ +function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { + $scope.namespace = null; + $scope.page = 1; + $scope.publicPageCount = null; + + // When loading the UserService, if the user is logged in, create a list of + // relevant namespaces for later collecting relevant repositories. + UserService.load(function() { + var user = UserService.currentUser(); + if (!user.anonymous) { + $scope.namespaces = [user]; + for (var i = 0; i < user.organizations.length; i++) { + $scope.namespaces.push(user.organizations[i]); + } + //loadStarredRepos(); + //loadRepos(); + } + }); + + // If someone signs in on this page, we have to watch the user and re-load + // their repositories after they've signed in to actually have any content + // on the page. + $scope.$watch(function(scope) { return scope.user }, + function(user) { + if (!user.anonymous) { + $scope.namespaces = [user]; + for (var i = 0; i < user.organizations.length; i++) { + $scope.namespaces.push(user.organizations[i]); + } + loadStarredRepos(); + loadRepos(); + } + }); + + $scope.toggleStar = function(repo) { + if (repo.is_starred) { + unstarRepo(repo); + } else { + starRepo(repo); + } + } + + var starRepo = function(repo) { + var data = { + 'namespace': repo.namespace, + 'repository': repo.name + }; + ApiService.createStar(data).then(function(result) { + updateReposAfterStar(repo); + }, function(result) { + // TODO(jzelinskie): have some kind of pop-up for star failure + }); + }; + + var unstarRepo = function(repo) { + var data = { + 'repository': repo.namespace + '/' + repo.name + }; + ApiService.deleteStar(null, data).then(function(result) { + updateReposAfterUnstar(repo); + }, function(result) { + // TODO(jzelinskie): have some kind of pop-up for star failure + }); + }; + + // Finds a repository within the list of namespaces attached to $scope. + var findRepoInList = function(repoNamespace, repoName) { + var namespaceIndex = $scope.namespaces.map(function (n) { + return n.username || n.name; + }).indexOf(repoNamespace); + + var namespace = $scope.namespaces[namespaceIndex] + + var repoIndex = namespace.repositories.value.map(function (r) { + return r.namespace + '/' + r.name; + }).indexOf(repoNamespace + '/' + repoName); + + return repoIndex != -1 ? namespace.repositories.value[repoIndex] : null; + } + + // Add a starred repository to the list starred repository list and make + // sure it appears starred elsewhere on the page. + var updateReposAfterStar = function(repository) { + $scope.starred_repositories.value.push(repository); + + var repo = findRepoInList(repository.namespace, repository.name); + if (repo != null) { + repo.is_starred = true; + } + } + + // Remove a repository from the starred repository list and make sure that + // it doesn't appear starred elsewhere on the page. + var updateReposAfterUnstar = function(repository) { + // Remove from the starred listings + var index = $scope.starred_repositories.value.map(function(r) { + return r.namespace + '/' + r.name; + }).indexOf(repository.namespace + '/' + repository.name); + $scope.starred_repositories.value.splice(index, 1); + + // Set repo from the normal listings to unstarred. + var repo = findRepoInList(repository.namespace, repository.name); + if (repo != null) { + repo.is_starred = false; + } + }; + + var loadStarredRepos = function() { + if (!$scope.user || $scope.user.anonymous) { + return; + } + + $scope.starred_repositories = ApiService.listStarredReposAsResource().get(function(resp) { + return resp.repositories.map(function(repo) { + repo.is_starred = true; + return repo; + }); + }); + }; + + // Iterate over all of the $scope.namespaces and collect their respective + // repositories. + var loadRepos = function() { + if ($scope.namespaces.length == 0 || $scope.user.anonymous) { + return; + } + console.log('load repos called'); + + for (var i = 0; i < $scope.namespaces.length; i++) { + var namespace = $scope.namespaces[i]; + var namespaceName = namespace.username || namespace.name; + var options = { + 'public': false, + 'sort': true, + 'namespace': namespaceName, + }; + namespace.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { + return resp.repositories; + }); + } + }; +} From 5f2bff43e7f21d33ee315bb6a99e6539f4e4244e Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Mon, 12 Jan 2015 12:08:08 -0500 Subject: [PATCH 18/36] Use avatars instead of fontawesome in repo list. --- static/js/controllers/repolist.js | 1 - static/partials/repo-list.html | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/static/js/controllers/repolist.js b/static/js/controllers/repolist.js index f1d196570..61b3db426 100644 --- a/static/js/controllers/repolist.js +++ b/static/js/controllers/repolist.js @@ -124,7 +124,6 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { if ($scope.namespaces.length == 0 || $scope.user.anonymous) { return; } - console.log('load repos called'); for (var i = 0; i < $scope.namespaces.length; i++) { var namespace = $scope.namespaces[i]; diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index 8dc81ddeb..4d53b515a 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -94,11 +94,11 @@
- + {{ namespace.username }}
From 3da0228aaaaa0e29cf511fe68c3918fbbbd85d85 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Mon, 12 Jan 2015 17:43:36 -0500 Subject: [PATCH 19/36] Add repo-list-grid directive --- static/directives/repo-list-grid.html | 59 ++++++++++++++ static/js/app.js | 1 - static/js/controllers/repolist.js | 8 +- static/js/directives/repo-list-grid.js | 14 ++++ static/partials/repo-list.html | 108 +++++-------------------- 5 files changed, 95 insertions(+), 95 deletions(-) create mode 100644 static/directives/repo-list-grid.html create mode 100644 static/js/directives/repo-list-grid.js diff --git a/static/directives/repo-list-grid.html b/static/directives/repo-list-grid.html new file mode 100644 index 000000000..92c9b9b6d --- /dev/null +++ b/static/directives/repo-list-grid.html @@ -0,0 +1,59 @@ +
+
+ +
+ + Starred +
+
+ + {{ namespace.username }} +
+ + + +
+ + + +
+
You haven't starred any repositories yet.
+
Stars allow you to easily access your favorite repositories.
+
+
+
This namespace doesn't have any viewable repositories.
+
Either no repositories exist yet or you may not have permission to view any. If you have permission, try creating a new repository.
+
+ + +
+
+
+
+
+ +
+
diff --git a/static/js/app.js b/static/js/app.js index 63eee0d6e..30eb7be92 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -6663,7 +6663,6 @@ quayApp.directive('tagSpecificImagesView', function () { return directiveDefinitionObject; }); - quayApp.directive('fallbackSrc', function () { return { restrict: 'A', diff --git a/static/js/controllers/repolist.js b/static/js/controllers/repolist.js index 61b3db426..50243603e 100644 --- a/static/js/controllers/repolist.js +++ b/static/js/controllers/repolist.js @@ -47,9 +47,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { }; ApiService.createStar(data).then(function(result) { updateReposAfterStar(repo); - }, function(result) { - // TODO(jzelinskie): have some kind of pop-up for star failure - }); + }, ApiService.errorDisplay('Could not star repository')); }; var unstarRepo = function(repo) { @@ -58,9 +56,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { }; ApiService.deleteStar(null, data).then(function(result) { updateReposAfterUnstar(repo); - }, function(result) { - // TODO(jzelinskie): have some kind of pop-up for star failure - }); + }, ApiService.errorDisplay('Could not unstar repository')); }; // Finds a repository within the list of namespaces attached to $scope. diff --git a/static/js/directives/repo-list-grid.js b/static/js/directives/repo-list-grid.js new file mode 100644 index 000000000..3f936e6f5 --- /dev/null +++ b/static/js/directives/repo-list-grid.js @@ -0,0 +1,14 @@ +quayApp.directive('repoListGrid', function() { + return { + templateUrl: '/static/directives/repo-list-grid.html', + priority: 0, + restrict: 'C', + scope: { + repositories: '=repositories', + starred: '=starred', + user: "=user", + namespace: '=namespace', + toggleStar: '&toggleStar' + }, + }; +}); diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index 4d53b515a..4671c579d 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -1,14 +1,22 @@ + -
+ +
+ +
+ + +
Users @@ -28,6 +38,8 @@
+ +
Organizations @@ -46,95 +58,15 @@
+
-
-
-
- - Starred -
-
-
- -
-
-
You haven't starred any repositories yet.
-
Stars allow you to easily access your favorite repositories.
-
-
-
-
-
-
-
-
-
+ +
- +
-
-
-
- - {{ namespace.username }} -
- -
- -
-
-
This namespace doesn't have any viewable repositories.
-
Either no repositories exist yet or you may not have permission to view any. If you have permission, try creating a new repository.
-
-
-
-
-
-
-
-
+
+
From 378043427982675b3e9fa30222e4863fdba5acb3 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Thu, 19 Feb 2015 17:03:36 -0500 Subject: [PATCH 20/36] endpoints.api.user: require useradmin for star ops --- endpoints/api/user.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 993ff52c2..f7a088c15 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -705,6 +705,7 @@ class StarredRepositoryList(ApiResource): @parse_args @query_param('page', 'Offset page number. (int)', type=int) @query_param('limit', 'Limit on the number of results (int)', type=int) + @require_user_admin def get(self, args): """ List all starred repositories. """ page = args['page'] @@ -725,6 +726,7 @@ class StarredRepositoryList(ApiResource): @require_scope(scopes.READ_REPO) @nickname('createStar') @validate_json_request('NewStarredRepository') + @require_user_admin def post(self): """ Star a repository. """ user = get_authenticated_user() @@ -755,6 +757,7 @@ class StarredRepositoryList(ApiResource): class StarredRepository(RepositoryParamResource): """ Operations for managing a specific starred repository. """ @nickname('deleteStar') + @require_user_admin def delete(self, namespace, repository): user = get_authenticated_user() repo = model.get_repository(namespace, repository) From 7fbbf14e49861680df3dc279dc2177809ee0ac16 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Thu, 19 Feb 2015 17:10:27 -0500 Subject: [PATCH 21/36] migrations: resolve branched migration tree This moves the migration adding stars to HEAD. --- data/migrations/versions/2088f2b81010_add_stars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/migrations/versions/2088f2b81010_add_stars.py b/data/migrations/versions/2088f2b81010_add_stars.py index d8c75966c..27539f702 100644 --- a/data/migrations/versions/2088f2b81010_add_stars.py +++ b/data/migrations/versions/2088f2b81010_add_stars.py @@ -8,7 +8,7 @@ Create Date: 2014-12-02 17:45:00.707498 # revision identifiers, used by Alembic. revision = '2088f2b81010' -down_revision = '1c5b738283a5' +down_revision = '4ef04c61fcf9' from alembic import op import sqlalchemy as sa From 46832676f7666bbd3b61c094ba38223ce3a10181 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Thu, 19 Feb 2015 17:46:44 -0500 Subject: [PATCH 22/36] testdb: re-run initdb --- test/data/test.db | Bin 729088 -> 270336 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/data/test.db b/test/data/test.db index 4da80c978b4df80b01a42e5de32d324dd0ae295b..96291ff49a2c6880f541a801438b63e2728a4cec 100644 GIT binary patch delta 27280 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 C`KosS delta 27270 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 KGyQ*!3i^M&PY?M3 From 2914a5da9686007e71f69f4da8ad12854d45b69a Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Fri, 20 Feb 2015 15:11:41 -0500 Subject: [PATCH 23/36] tests: add test for star/unstar repo --- test/test_api_usage.py | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/test/test_api_usage.py b/test/test_api_usage.py index c0cdf767f..6dacb72ff 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -28,7 +28,7 @@ from endpoints.api.repoemail import RepositoryAuthorizedEmail from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User, UserAuthorizationList, UserAuthorization, UserNotification, - UserNotificationList) + UserNotificationList, StarredRepositoryList, StarredRepository) from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList @@ -224,6 +224,42 @@ class TestLoggedInUser(ApiTestCase): assert json['anonymous'] == False assert json['username'] == READ_ACCESS_USER +class TestUserStarredRepositoryList(ApiTestCase): + def test_get_stars_guest(self): + self.getJsonResponse(StarredRepositoryList, expected_code=401) + + def test_get_stars_user(self): + self.login(READ_ACCESS_USER) + self.getJsonResponse(StarredRepositoryList, expected_code=200) + + def test_star_repo_guest(self): + self.postJsonResponse(StarredRepositoryList, + data={ + 'namespace': 'public', + 'repository': 'publicrepo', + }, + expected_code=401) + + def test_unstar_repo_guest(self): + self.deleteResponse(StarredRepository, params=dict(repository='public/publicrepo'), expected_code=401) + + def test_star_and_unstar_repo_user(self): + self.login(READ_ACCESS_USER) + json = self.getJsonResponse(StarredRepositoryList) + assert json['repositories'] == [] + + json = self.postJsonResponse(StarredRepositoryList, + data={ + 'namespace': 'public', + 'repository': 'publicrepo', + }, + expected_code=201) + assert json['namespace'] == 'public' + assert json['repository'] == 'publicrepo' + + self.deleteResponse(StarredRepository, params=dict(repository='public/publicrepo'), expected_code=204) + + class TestUserNotification(ApiTestCase): def test_get(self): @@ -726,7 +762,7 @@ class TestUpdateOrganizationTeam(ApiTestCase): self.putJsonResponse(OrganizationTeam, params=dict(orgname=ORGANIZATION, teamname='owners'), - data=dict(role = 'creator'), + data=dict(role='creator'), expected_code=400) def test_createnewteam(self): @@ -1304,7 +1340,6 @@ class TestGetRepository(ApiTestCase): self.assertEquals(True, json['is_organization']) - class TestRepositoryBuildResource(ApiTestCase): def test_cancel_invalidbuild(self): self.login(ADMIN_ACCESS_USER) From 35a2414d854a1988f089d66fe83e7b5c519d1897 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Mon, 23 Feb 2015 14:23:32 -0500 Subject: [PATCH 24/36] tests: star security tests --- endpoints/api/user.py | 6 +---- test/test_api_security.py | 54 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/endpoints/api/user.py b/endpoints/api/user.py index f7a088c15..3284e4371 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -700,7 +700,6 @@ class StarredRepositoryList(ApiResource): } } - @require_scope(scopes.READ_REPO) @nickname('listStarredRepos') @parse_args @query_param('page', 'Offset page number. (int)', type=int) @@ -750,12 +749,11 @@ class StarredRepositoryList(ApiResource): 'repository': repository, }, 201 - raise NotFound() - @resource('/v1/user/starred/') @path_param('repository', 'The full path of the repository. e.g. namespace/name') class StarredRepository(RepositoryParamResource): """ Operations for managing a specific starred repository. """ + @nickname('deleteStar') @require_user_admin def delete(self, namespace, repository): @@ -769,5 +767,3 @@ class StarredRepository(RepositoryParamResource): #log_action('unstar_repository', user.username, namespace, # {'repo': repository, 'namespace': namespace}) return 'Deleted', 204 - - raise NotFound() diff --git a/test/test_api_security.py b/test/test_api_security.py index 6cc790fbc..7f047c270 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -26,7 +26,7 @@ from endpoints.api.repoemail import RepositoryAuthorizedEmail from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout, Signin, User, UserAuthorizationList, UserAuthorization, UserNotification, - VerifyUser, DetachExternal) + VerifyUser, DetachExternal, StarredRepositoryList, StarredRepository) from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs @@ -132,6 +132,58 @@ class TestFindRepositories(ApiTestCase): +class TestUserStarredRepositoryList(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(StarredRepositoryList) + + def test_get_anonymous(self): + self._run_test('GET', 401, None, None) + + def test_get_freshuser(self): + self._run_test('GET', 200, 'freshuser', None) + + def test_get_reader(self): + self._run_test('GET', 200, 'reader', None) + + def test_get_devtable(self): + self._run_test('GET', 200, 'devtable', None) + + def test_post_anonymous(self): + self._run_test('POST', 401, None, {u'namespace': 'public', + u'repository': 'publicrepo'}) + + def test_post_freshuser(self): + self._run_test('POST', 201, 'freshuser', {u'namespace': 'public', + u'repository': 'publicrepo'}) + + def test_post_reader(self): + self._run_test('POST', 201, 'reader', {u'namespace': 'public', + u'repository': 'publicrepo'}) + + def test_post_devtable(self): + self._run_test('POST', 201, 'devtable', {u'namespace': 'public', + u'repository': 'publicrepo'}) + + +class TestUserStarredRepository(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(StarredRepository, repository="public/publicrepo") + + def test_delete_anonymous(self): + self._run_test('DELETE', 401, None, None) + + def test_delete_freshuser(self): + self._run_test('DELETE', 400, 'freshuser', None) + + def test_delete_reader(self): + self._run_test('DELETE', 400, 'reader', None) + + def test_delete_devtable(self): + self._run_test('DELETE', 400, 'devtable', None) + + class TestUserNotification(ApiTestCase): def setUp(self): ApiTestCase.setUp(self) From 6bff3449aa875ac3fb2b4e543f923e1cb13fcab7 Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Mon, 23 Feb 2015 15:22:41 -0500 Subject: [PATCH 25/36] js: continue merging pagesnew --- static/js/pages/repo-list.js | 149 ++++++++++++++++++++++++++++- static/partials/old-repo-list.html | 74 ++++++++++++++ static/partials/repo-list.html | 2 +- 3 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 static/partials/old-repo-list.html diff --git a/static/js/pages/repo-list.js b/static/js/pages/repo-list.js index 354a12895..bb3087b71 100644 --- a/static/js/pages/repo-list.js +++ b/static/js/pages/repo-list.js @@ -4,16 +4,161 @@ */ angular.module('quayPages').config(['pages', function(pages) { pages.create('repo-list', 'repo-list.html', RepoListCtrl, { + 'newLayout': true, 'title': 'Repositories', 'description': 'View and manage Docker repositories' - }); + }, ['layout']) + + pages.create('repo-list', 'old-repo-list.html', OldRepoListCtrl, { + 'title': 'Repositories', + 'description': 'View and manage Docker repositories' + }, ['old-layout']); }]); + function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { $scope.namespace = null; $scope.page = 1; $scope.publicPageCount = null; + // When loading the UserService, if the user is logged in, create a list of + // relevant namespaces for later collecting relevant repositories. + UserService.load(function() { + var user = UserService.currentUser(); + if (!user.anonymous) { + $scope.namespaces = [user]; + for (var i = 0; i < user.organizations.length; i++) { + $scope.namespaces.push(user.organizations[i]); + } + //loadStarredRepos(); + //loadRepos(); + } + }); + + // If someone signs in on this page, we have to watch the user and re-load + // their repositories after they've signed in to actually have any content + // on the page. + $scope.$watch(function(scope) { return scope.user }, + function(user) { + if (!user.anonymous) { + $scope.namespaces = [user]; + for (var i = 0; i < user.organizations.length; i++) { + $scope.namespaces.push(user.organizations[i]); + } + loadStarredRepos(); + loadRepos(); + } + }); + + $scope.toggleStar = function(repo) { + if (repo.is_starred) { + unstarRepo(repo); + } else { + starRepo(repo); + } + } + + var starRepo = function(repo) { + var data = { + 'namespace': repo.namespace, + 'repository': repo.name + }; + ApiService.createStar(data).then(function(result) { + updateReposAfterStar(repo); + }, ApiService.errorDisplay('Could not star repository')); + }; + + var unstarRepo = function(repo) { + var data = { + 'repository': repo.namespace + '/' + repo.name + }; + ApiService.deleteStar(null, data).then(function(result) { + updateReposAfterUnstar(repo); + }, ApiService.errorDisplay('Could not unstar repository')); + }; + + // Finds a repository within the list of namespaces attached to $scope. + var findRepoInList = function(repoNamespace, repoName) { + var namespaceIndex = $scope.namespaces.map(function (n) { + return n.username || n.name; + }).indexOf(repoNamespace); + + var namespace = $scope.namespaces[namespaceIndex] + + var repoIndex = namespace.repositories.value.map(function (r) { + return r.namespace + '/' + r.name; + }).indexOf(repoNamespace + '/' + repoName); + + return repoIndex != -1 ? namespace.repositories.value[repoIndex] : null; + } + + // Add a starred repository to the list starred repository list and make + // sure it appears starred elsewhere on the page. + var updateReposAfterStar = function(repository) { + $scope.starred_repositories.value.push(repository); + + var repo = findRepoInList(repository.namespace, repository.name); + if (repo != null) { + repo.is_starred = true; + } + } + + // Remove a repository from the starred repository list and make sure that + // it doesn't appear starred elsewhere on the page. + var updateReposAfterUnstar = function(repository) { + // Remove from the starred listings + var index = $scope.starred_repositories.value.map(function(r) { + return r.namespace + '/' + r.name; + }).indexOf(repository.namespace + '/' + repository.name); + $scope.starred_repositories.value.splice(index, 1); + + // Set repo from the normal listings to unstarred. + var repo = findRepoInList(repository.namespace, repository.name); + if (repo != null) { + repo.is_starred = false; + } + }; + + var loadStarredRepos = function() { + if (!$scope.user || $scope.user.anonymous) { + return; + } + + $scope.starred_repositories = ApiService.listStarredReposAsResource().get(function(resp) { + return resp.repositories.map(function(repo) { + repo.is_starred = true; + return repo; + }); + }); + }; + + // Iterate over all of the $scope.namespaces and collect their respective + // repositories. + var loadRepos = function() { + if ($scope.namespaces.length == 0 || $scope.user.anonymous) { + return; + } + + for (var i = 0; i < $scope.namespaces.length; i++) { + var namespace = $scope.namespaces[i]; + var namespaceName = namespace.username || namespace.name; + var options = { + 'public': false, + 'sort': true, + 'namespace': namespaceName, + }; + namespace.repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { + return resp.repositories; + }); + } + }; + } + + function OldRepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { + $scope.namespace = null; + $scope.page = 1; + $scope.publicPageCount = null; + // Monitor changes in the user. UserService.updateUserIn($scope, function() { loadMyRepos($scope.namespace); @@ -73,4 +218,4 @@ loadPublicRepos(); } -})(); \ No newline at end of file +})(); diff --git a/static/partials/old-repo-list.html b/static/partials/old-repo-list.html new file mode 100644 index 000000000..ab482de76 --- /dev/null +++ b/static/partials/old-repo-list.html @@ -0,0 +1,74 @@ +
+
+ + +

Your Repositories

+

Repositories

+ +
+ + + + +
+
+

You don't have any repositories yet!

+

This organization doesn't have any repositories, or you have not been provided access.

+ Click here to learn how to create a repository +
+
+
+ +
+ +
+

Top Public Repositories

+ +
+
diff --git a/static/partials/repo-list.html b/static/partials/repo-list.html index 4671c579d..f0fd45aae 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -1,5 +1,5 @@ -