Merge pull request #13 from coreos-inc/star

Star
This commit is contained in:
Jimmy Zelinskie 2015-03-02 13:38:47 -05:00
commit c967623ab1
19 changed files with 801 additions and 100 deletions

View file

@ -6,6 +6,7 @@ import time
from random import SystemRandom
from datetime import datetime
from peewee import *
from data.read_slave import ReadSlaveModel
from sqlalchemy.engine.url import make_url
from data.read_slave import ReadSlaveModel
@ -287,7 +288,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
@ -301,6 +302,20 @@ class Repository(BaseModel):
super(Repository, self).delete_instance(recursive=False, delete_nullable=False)
class Star(BaseModel):
user = ForeignKeyField(User, index=True)
repository = ForeignKeyField(Repository, index=True)
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)
@ -615,4 +630,4 @@ all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission,
ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification,
RepositoryAuthorizedEmail, ImageStorageTransformation, DerivedImageStorage,
TeamMemberInvite, ImageStorageSignature, ImageStorageSignatureKind,
AccessTokenKind]
AccessTokenKind, Star]

View file

@ -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 = '4ef04c61fcf9'
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')

View file

@ -18,7 +18,7 @@ from data.database import (User, Repository, Image, AccessToken, Role, Repositor
DerivedImageStorage, ImageStorageTransformation, random_string_generator,
db, BUILD_PHASE, QuayUserField, ImageStorageSignature, QueueItem,
ImageStorageSignatureKind, validate_database_url, db_for_update,
AccessTokenKind)
AccessTokenKind, Star)
from peewee import JOIN_LEFT_OUTER, fn
from util.validation import (validate_username, validate_email, validate_password,
INVALID_PASSWORD_MESSAGE)
@ -813,7 +813,6 @@ def _visible_repository_query(username=None, include_public=True, limit=None,
.join(RepositoryPermission, JOIN_LEFT_OUTER))
query = _filter_to_repos_for_user(query, username, namespace, include_public)
if page:
query = query.paginate(page, limit)
elif limit:
@ -822,8 +821,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):
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')
@ -2540,3 +2538,49 @@ 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
.delete()
.where(Star.repository == repository.id, Star.user == user.id)
.execute())
except Star.DoesNotExist:
raise DataModelException('Star not found.')
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

View file

@ -102,24 +102,20 @@ 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
if get_authenticated_user():
starred_repos = model.get_user_starred_repositories(get_authenticated_user())
star_lookup = set([repo.id for repo in starred_repos])
if args['private']:
username = get_authenticated_user().username
response = {}
@ -132,6 +128,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
@ -271,6 +277,4 @@ class RepositoryVisibility(RepositoryParamResource):
log_action('change_repo_visibility', namespace,
{'repo': repository, 'visibility': values['visibility']},
repo=repo)
return {
'success': True
}
return {'success': True}

View file

@ -4,12 +4,14 @@ 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,
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
@ -673,3 +675,87 @@ 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'
}
}
}
}
@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)
@require_user_admin
def get(self, args):
""" List all starred repositories. """
page = args['page']
limit = args['limit']
starred_repos = 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)
@nickname('createStar')
@validate_json_request('NewStarredRepository')
@require_user_admin
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:
try:
model.star_repository(user, repo)
except IntegrityError:
pass
return {
'namespace': namespace,
'repository': repository,
}, 201
@resource('/v1/user/starred/<repopath:repository>')
@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):
user = get_authenticated_user()
repo = model.get_repository(namespace, repository)
if repo:
model.unstar_repository(user, repo)
return 'Deleted', 204

View file

@ -161,6 +161,11 @@ def confirm_invite():
def repository(path):
return index('')
@web.route('/starred/')
@no_cache
def starred():
return index('')
@web.route('/security/')
@no_cache

View file

@ -0,0 +1,131 @@
.repo-panel-title-row .repo-circle {
color: #999;
display: inline-block;
position: relative;
background: #eee;
padding: 4px;
border-radius: 50%;
display: inline-block;
width: 46px;
height: 46px;
}
.repo-panel-title-row .repo-circle .fa-hdd-o {
font-size: 1.7em;
}
.repo-panel-title-row .repo-circle.no-background .fa-hdd-o {
font-size: 1.7em;
}
.repo-panel-title-row .repo-circle .fa-lock {
width: 16px;
height: 16px;
line-height: 16px;
font-size: 12px !important;
}
.repo-panel-title-row .repo-circle.no-background .fa-lock {
bottom: 5px;
right: 2px;
}
.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-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-title .starred {
color: #ffba6d;
}
.repo-panel {
padding: 20px;
border: 1px solid #eee;
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) 5%, 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(5%,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) 5%,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) 5%,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) 5%,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) 5%,rgba(255,255,255,0.49) 51%,rgba(255,255,255,0) 100%); /* W3C */
}
.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;
}
.new-repo-listing {
display: block;
border-bottom: 1px solid #eee;
font-size: 14px;
line-height: normal;
padding-bottom: 30px;
}
.new-repo-listing .description {
font-size: 0.91em;
padding-top: 13px;
}
.new-repo-listing .description {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.repo-panel-repo-link {
font-size: 1.2em;
}
.repo-list-grid {
padding-top: 10px;
}

View file

@ -0,0 +1,7 @@
.repo-list-sidebar .button-bar-right {
margin-bottom: 20px;
}
.repo-list-sidebar .panel .panel-body .fa-gear {
float: right;
}

View file

@ -0,0 +1,56 @@
<div class="resource-view" resource="repositories">
<div class="new-repo-listing">
<!-- Titles -->
<div ng-if="starred" class="repo-list-title">
<i class="fa fa-star starred"></i>
Starred
</div>
<div ng-if="!starred && user.username == namespace.username" class="repo-list-title">
<span class="avatar" size="24" hash="namespace.avatar"></span>
{{ namespace.username }}
</div>
<div ng-if="!starred && user.username != namespace.username" class="repo-list-title">
<span class="avatar" size="24" hash="namespace.avatar"></span>
<a href="/organization/{{ namespace.name }}">{{ namespace.name }}</a>
</div>
<!-- Repositories -->
<div>
<div ng-if="repositories.length > 0">
<div class="row">
<div class="col-lg-4 col-md-6 col-sm-6 col-xs-12" ng-repeat="repository in repositories">
<div class="panel panel-default">
<div class="panel-body" ng-class="repository.is_starred ? 'starred' : ''">
<div class="row">
<div class="col-lg-10 col-md-10 col-sm-10 col-xs-10 repo-panel-title-row">
<span class="repo-icon repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}" class="repo-panel-repo-link"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{ repository.namespace }}/{{ repository.name }}
</a>
</div>
<div class="col-lg-2 col-md-2 col-sm-2 col-xs-2">
<i ng-class="repository.is_starred ? 'starred fa fa-star' : 'fa fa-star-o'" class="star-icon" ng-click="toggleStar({repository: repository})"></i>
</div>
</div>
<div class="description markdown-view" content="repository.description" first-line-only="true" placeholder-needed="true"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Empty Messages -->
<div ng-if="starred && repositories.length == 0">
<div class="empty-primary-msg">You haven't starred any repositories yet.</div>
<div class="empty-secondary-msg">Stars allow you to easily access your favorite repositories.</div>
</div>
<div ng-if="!starred && repositories.length == 0">
<div class="empty-primary-msg">This namespace doesn't have any viewable repositories.</div>
<div class="empty-secondary-msg">Either no repositories exist yet or you may not have permission to view any. If you have permission, try <a href="/new">creating a new repository</a>.</div>
</div>
</div>
</div>
</div>

View file

@ -10,12 +10,14 @@ angular.module('quay').directive('markdownView', function () {
restrict: 'C',
scope: {
'content': '=content',
'firstLineOnly': '=firstLineOnly'
'firstLineOnly': '=firstLineOnly',
'placeholderNeeded': '=placeholderNeeded'
},
controller: function($scope, $element, $sce, UtilService) {
$scope.getMarkedDown = function(content, firstLineOnly) {
if (firstLineOnly) {
return $sce.trustAsHtml(UtilService.getFirstMarkdownLineAsText(content));
console.log($scope.placeholderNeeded);
return $sce.trustAsHtml(UtilService.getFirstMarkdownLineAsText(content, $scope.placeholderNeeded));
}
return $sce.trustAsHtml(UtilService.getMarkedDown(content));
};

View file

@ -0,0 +1,18 @@
/**
* An element that displays a list of repositories in a grid.
*
*/
angular.module('quay').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'
},
};
});

View file

@ -4,12 +4,128 @@
*/
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) {
function RepoListCtrl($scope, $sanitize, $q, Restangular, UserService, ApiService) {
$scope.namespace = null;
$scope.page = 1;
$scope.publicPageCount = null;
$scope.all_repositories = {};
// 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]);
}
}
});
// 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) {
repo.is_starred = true;
$scope.starred_repositories.value.push(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) {
repo.is_starred = false;
$scope.starred_repositories.value = $scope.starred_repositories.value.filter(function(repo) {
return repo.is_starred;
});
}, ApiService.errorDisplay('Could not unstar repository'));
};
// Finds a duplicate repo if it exists. If it doesn't, inserts the repo.
var findDuplicateRepo = function(repo) {
var found = $scope.all_repositories[repo.namespace + '/' + repo.name];
if (found != undefined) {
return found;
} else {
$scope.all_repositories[repo.namespace + '/' + repo.name] = repo;
}
};
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 = findDuplicateRepo(repo);
repo.is_starred = true;
return repo;
});
});
};
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.map(findDuplicateRepo);
});
}
};
}
function OldRepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) {
$scope.namespace = null;
$scope.page = 1;
$scope.publicPageCount = null;
@ -73,4 +189,4 @@
loadPublicRepos();
}
})();
})();

View file

@ -10,11 +10,16 @@ angular.module('quay').factory('UtilService', ['$sanitize', function($sanitize)
};
utilService.getMarkedDown = function(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
return html = Markdown.getSanitizingConverter().makeHtml(string || '');
};
utilService.getFirstMarkdownLineAsText = function(commentString) {
if (!commentString) { return ''; }
utilService.getFirstMarkdownLineAsText = function(commentString, placeholderNeeded) {
if (!commentString) {
if (placeholderNeeded) {
return '<p style="visibility:hidden">placeholder</p>';
}
return '';
}
var lines = commentString.split('\n');
var MARKDOWN_CHARS = {

View file

@ -0,0 +1,74 @@
<div class="cor-container">
<div class="repo-list" ng-show="!user.anonymous">
<div ng-class="user.organizations.length ? 'section-header' : ''">
<div class="button-bar-right">
<a href="/new/">
<button class="btn btn-success">
<i class="fa fa-upload user-tool" data-title="Create new repository"></i>
Create Repository
</button>
</a>
<a href="/organization/{{ namespace }}" ng-show="namespace != user.username">
<button class="btn btn-default">
<i class="fa fa-group user-tool"></i>
View Organization
</button>
</a>
</div>
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
</div>
<h3 ng-show="namespace == user.username">Your Repositories</h3>
<h3 ng-show="namespace != user.username">Repositories</h3>
<div class="resource-view" resource="user_repositories">
<!-- User/Org has repositories -->
<div ng-show="user_repositories.value.length > 0">
<div class="repo-listing" ng-repeat="repository in user_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
</div>
<!-- User/Org has no repositories -->
<div ng-show="user_repositories.value.length == 0" style="padding:20px;">
<div class="alert alert-info">
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
<h4 ng-show="namespace != user.username">This organization doesn't have any repositories, or you have not been provided access.</h4>
<a href="http://docs.quay.io/solution/getting-started.html"><b>Click here</b> to learn how to create a repository</a>
</div>
</div>
</div>
</div>
<div class="repo-list">
<h3>Top Public Repositories</h3>
<div class="resource-view" resource="public_repositories">
<div class="repo-listing" ng-repeat="repository in public_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
<div class="page-controls">
<button class="btn btn-default" data-title="Previous Page" bs-tooltip="title" ng-show="page > 1"
ng-click="movePublicPage(-1)">
<i class="fa fa-chevron-left"></i>
</button>
<button class="btn btn-default" data-title="Next Page" bs-tooltip="title" ng-show="page < publicPageCount"
ng-click="movePublicPage(1)">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>

View file

@ -1,74 +1,81 @@
<div class="cor-container">
<div class="repo-list" ng-show="!user.anonymous">
<div ng-class="user.organizations.length ? 'section-header' : ''">
<div class="button-bar-right">
<a href="/new/">
<button class="btn btn-success">
<i class="fa fa-upload user-tool" data-title="Create new repository"></i>
Create Repository
</button>
</a>
<a href="/organization/{{ namespace }}" ng-show="namespace != user.username">
<button class="btn btn-default">
<i class="fa fa-group user-tool"></i>
View Organization
</button>
</a>
</div>
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
</div>
<h3 ng-show="namespace == user.username">Your Repositories</h3>
<h3 ng-show="namespace != user.username">Repositories</h3>
<div class="resource-view" resource="user_repositories">
<!-- User/Org has repositories -->
<div ng-show="user_repositories.value.length > 0">
<div class="repo-listing" ng-repeat="repository in user_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
</div>
</div>
<!-- User/Org has no repositories -->
<div ng-show="user_repositories.value.length == 0" style="padding:20px;">
<div class="alert alert-info">
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
<h4 ng-show="namespace != user.username">This organization doesn't have any repositories, or you have not been provided access.</h4>
<a href="http://docs.quay.io/solution/getting-started.html"><b>Click here</b> to learn how to create a repository</a>
</div>
</div>
</div>
<div class="page-content">
<div class="cor-title">
<span class="cor-title-link"></span>
<span class="cor-title-content">Repositories</span>
</div>
<div class="co-main-content-panel">
<!-- The user is not logged in -->
<div ng-if="user.anonymous" class="cor-container signin-container">
<div class="repo-list">
<h3>Top Public Repositories</h3>
<div class="resource-view" resource="public_repositories">
<div class="repo-listing" ng-repeat="repository in public_repositories.value">
<span class="repo-circle no-background" repo="repository"></span>
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}"
data-repo="{{repository.namespace}}/{{ repository.name }}">
{{repository.namespace}}/{{repository.name}}
</a>
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
<!-- Sign In -->
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<div class="empty-primary-msg">You must be signed in to view repositories.</div>
<div class="user-setup" redirect-url="redirectUrl"></div>
</div>
</div>
<div class="page-controls">
<button class="btn btn-default" data-title="Previous Page" bs-tooltip="title" ng-show="page > 1"
ng-click="movePublicPage(-1)">
<i class="fa fa-chevron-left"></i>
</button>
<button class="btn btn-default" data-title="Next Page" bs-tooltip="title" ng-show="page < publicPageCount"
ng-click="movePublicPage(1)">
<i class="fa fa-chevron-right"></i>
</button>
</div>
<!-- The user is logged in -->
<div class="row" ng-if="!user.anonymous">
<!-- Side Panel -->
<div class="repo-list-sidebar col-lg-3 col-lg-push-9 col-md-3 col-md-push-9 col-sm-4 col-sm-push-8 col-xs-12">
<!-- Create Repository Button -->
<div class="button-bar-right">
<a href="/new/">
<button class="btn btn-success">
<i class="fa fa-upload user-tool" data-title="Create new repository"></i>
Create Repository
</button>
</a>
</div>
<!-- Users Panel -->
<div class="panel panel-default">
<div class="panel-heading">
Users
</div>
<div class="panel-body">
<a href="javascript:void(0)">
<span class="avatar" size="24" hash="user.avatar"></span>
{{ user.username }}
</a>
</div>
</div>
<!-- Organizations Panel -->
<div class="panel panel-default">
<div class="panel-heading">
Organizations
</div>
<div class="panel-body">
<div ng-repeat="org in user.organizations">
<a href="/organization/{{ org.name }}">
<span class="avatar" size="24" hash="org.avatar"></span>
{{ org.name }}
</a>
<a href="/organization/{{ org.name }}/admin">
<i class="fa fa-gear" style="color:#000"></i>
</a>
</div>
</div>
</div>
</div>
<!-- Repository Listings -->
<div class="col-lg-9 col-lg-pull-3 col-md-9 col-md-pull-3 col-sm-8 col-sm-pull-4 col-xs-12">
<!-- Starred Repository Listing -->
<div class="repo-list-grid" repositories="starred_repositories.value" starred="true" toggle-star="toggleStar(repository)"></div>
<!-- User and Org Repository Listings -->
<div ng-repeat="namespace in namespaces">
<div class="repo-list-grid" repositories="namespace.repositories.value" starred="false" user="user" namespace="namespace" toggle-star="toggleStar(repository)"></div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,3 @@
<div ng-show="test">
{{ test }}
</div>

Binary file not shown.

View file

@ -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', 204, 'freshuser', None)
def test_delete_reader(self):
self._run_test('DELETE', 204, 'reader', None)
def test_delete_devtable(self):
self._run_test('DELETE', 204, 'devtable', None)
class TestUserNotification(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)

View file

@ -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
@ -225,6 +225,43 @@ class TestLoggedInUser(ApiTestCase):
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_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)
json = self.getJsonResponse(StarredRepositoryList)
assert json['repositories'] == []
class TestUserNotification(ApiTestCase):
def test_get(self):
self.login(ADMIN_ACCESS_USER)
@ -726,7 +763,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 +1341,6 @@ class TestGetRepository(ApiTestCase):
self.assertEquals(True, json['is_organization'])
class TestRepositoryBuildResource(ApiTestCase):
def test_cancel_invalidbuild(self):
self.login(ADMIN_ACCESS_USER)