commit
c967623ab1
19 changed files with 801 additions and 100 deletions
|
@ -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]
|
||||
|
|
40
data/migrations/versions/2088f2b81010_add_stars.py
Normal file
40
data/migrations/versions/2088f2b81010_add_stars.py
Normal 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')
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
131
static/css/directives/repo-list-grid.css
Normal file
131
static/css/directives/repo-list-grid.css
Normal 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;
|
||||
}
|
7
static/css/directives/repo-list.css
Normal file
7
static/css/directives/repo-list.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
.repo-list-sidebar .button-bar-right {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.repo-list-sidebar .panel .panel-body .fa-gear {
|
||||
float: right;
|
||||
}
|
56
static/directives/repo-list-grid.html
Normal file
56
static/directives/repo-list-grid.html
Normal 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>
|
|
@ -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));
|
||||
};
|
||||
|
|
18
static/js/directives/ui/repo-list-grid.js
Normal file
18
static/js/directives/ui/repo-list-grid.js
Normal 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'
|
||||
},
|
||||
};
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -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 = {
|
||||
|
|
74
static/partials/old-repo-list.html
Normal file
74
static/partials/old-repo-list.html
Normal 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>
|
|
@ -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>
|
||||
|
||||
|
|
3
static/partials/starred.html
Normal file
3
static/partials/starred.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div ng-show="test">
|
||||
{{ test }}
|
||||
</div>
|
Binary file not shown.
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Reference in a new issue