Add a history view to the tags page. Next step will add the ability to revert back in time
This commit is contained in:
parent
703f48f194
commit
f8c80f7d11
10 changed files with 492 additions and 13 deletions
|
@ -1762,6 +1762,16 @@ def _tag_alive(query, now_ts=None):
|
|||
(RepositoryTag.lifetime_end_ts > now_ts))
|
||||
|
||||
|
||||
def list_repository_tag_history(repository, limit=100):
|
||||
query = (RepositoryTag
|
||||
.select(RepositoryTag, Image)
|
||||
.join(Image)
|
||||
.where(RepositoryTag.repository == repository)
|
||||
.order_by(RepositoryTag.lifetime_start_ts.desc())
|
||||
.limit(limit))
|
||||
return query
|
||||
|
||||
|
||||
def list_repository_tags(namespace_name, repository_name, include_hidden=False,
|
||||
include_storage=False):
|
||||
|
||||
|
|
|
@ -1,12 +1,45 @@
|
|||
from flask import request
|
||||
from flask import request, abort
|
||||
|
||||
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
|
||||
RepositoryParamResource, log_action, NotFound, validate_json_request,
|
||||
path_param)
|
||||
path_param, format_date)
|
||||
from endpoints.api.image import image_view
|
||||
from data import model
|
||||
from auth.auth_context import get_authenticated_user
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tag/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class ListRepositoryTags(RepositoryParamResource):
|
||||
""" Resource for listing repository tags. """
|
||||
|
||||
@require_repo_write
|
||||
@nickname('listRepoTags')
|
||||
def get(self, namespace, repository):
|
||||
repo = model.get_repository(namespace, repository)
|
||||
if not repo:
|
||||
abort(404)
|
||||
|
||||
def tag_view(tag):
|
||||
tag_info = {
|
||||
'name': tag.name,
|
||||
'docker_image_id': tag.image.docker_image_id,
|
||||
}
|
||||
|
||||
if tag.lifetime_start_ts > 0:
|
||||
tag_info['start_ts'] = tag.lifetime_start_ts
|
||||
|
||||
if tag.lifetime_end_ts > 0:
|
||||
tag_info['end_ts'] = tag.lifetime_end_ts
|
||||
|
||||
return tag_info
|
||||
|
||||
tags = model.list_repository_tag_history(repo, limit=100)
|
||||
return {'tags': [tag_view(tag) for tag in tags]}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
|
|
16
initdb.py
16
initdb.py
|
@ -57,7 +57,7 @@ def __gen_image_uuid(repo, image_num):
|
|||
|
||||
|
||||
global_image_num = [0]
|
||||
def __create_subtree(repo, structure, creator_username, parent):
|
||||
def __create_subtree(repo, structure, creator_username, parent, tag_map):
|
||||
num_nodes, subtrees, last_node_tags = structure
|
||||
|
||||
# create the nodes
|
||||
|
@ -102,12 +102,18 @@ def __create_subtree(repo, structure, creator_username, parent):
|
|||
tag = model.create_or_update_tag(repo.namespace_user.username, repo.name, tag_name,
|
||||
new_image.docker_image_id)
|
||||
|
||||
tag_map[tag_name] = tag
|
||||
|
||||
for tag_name in last_node_tags:
|
||||
if tag_name[0] == '#':
|
||||
tag.lifetime_end_ts = get_epoch_timestamp() - 1
|
||||
tag = tag_map[tag_name]
|
||||
tag.name = tag_name[1:]
|
||||
tag.lifetime_end_ts = tag_map[tag_name[1:]].lifetime_start_ts
|
||||
tag.lifetime_start_ts = tag.lifetime_end_ts - 10
|
||||
tag.save()
|
||||
|
||||
for subtree in subtrees:
|
||||
__create_subtree(repo, subtree, creator_username, new_image)
|
||||
__create_subtree(repo, subtree, creator_username, new_image, tag_map)
|
||||
|
||||
|
||||
def __generate_repository(user, name, description, is_public, permissions,
|
||||
|
@ -127,9 +133,9 @@ def __generate_repository(user, name, description, is_public, permissions,
|
|||
|
||||
if isinstance(structure, list):
|
||||
for s in structure:
|
||||
__create_subtree(repo, s, user.username, None)
|
||||
__create_subtree(repo, s, user.username, None, {})
|
||||
else:
|
||||
__create_subtree(repo, structure, user.username, None)
|
||||
__create_subtree(repo, structure, user.username, None, {})
|
||||
|
||||
return repo
|
||||
|
||||
|
|
|
@ -68,4 +68,131 @@
|
|||
.repo-panel-tags-element .options-col .fa-download {
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-list {
|
||||
margin: 10px;
|
||||
border-left: 2px solid #eee;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry {
|
||||
position:relative;
|
||||
margin-top: 20px;
|
||||
padding-left: 26px;
|
||||
|
||||
transition: all 350ms ease-in-out;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-text {
|
||||
transition: transform 350ms ease-in-out, opacity 350ms ease-in-out;
|
||||
overflow: hidden;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.filtered-mismatch {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.filtered-mismatch .history-text {
|
||||
height: 18px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.filtered-mismatch .history-icon {
|
||||
opacity: 0.5;
|
||||
transform: scale(0.5, 0.5);
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-date-break {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-date-break:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #ccc;
|
||||
top: 4px;
|
||||
left: -7px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-icon {
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 33px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
background: #ccc;
|
||||
|
||||
position: absolute;
|
||||
left: -17px;
|
||||
top: -4px;
|
||||
display: inline-block;
|
||||
|
||||
transition: all 350ms ease-in-out;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.move .history-icon:before {
|
||||
content: "\f061";
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.create .history-icon:before {
|
||||
content: "\f02b";
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.delete .history-icon:before {
|
||||
content: "\f014";
|
||||
font-family: FontAwesome;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.move .history-icon {
|
||||
background-color: #1f77b4;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.create .history-icon {
|
||||
background-color: #98df8a;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry.delete .history-icon {
|
||||
background-color: #ff9896;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-icon .fa-tag {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .tag-span {
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
background: #eee;
|
||||
padding-right: 6px;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .tag-span.checked {
|
||||
background: #F6FCFF;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .tag-span:before {
|
||||
content: "\f02b";
|
||||
font-family: FontAwesome;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-description {
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.repo-panel-tags-element .history-entry .history-datetime {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
4
static/css/directives/ui/image-link.css
Normal file
4
static/css/directives/ui/image-link.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.image-link a {
|
||||
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||
font-size: 12px;
|
||||
}
|
2
static/directives/image-link.html
Normal file
2
static/directives/image-link.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<a href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ imageId }}"
|
||||
class="image-link-element">{{ imageId.substr(0, 12) }}</a>
|
|
@ -1,6 +1,72 @@
|
|||
<div class="repo-panel-tags-element">
|
||||
<div class="tab-header-controls">
|
||||
<div class="btn-group btn-group-sm" ng-show="repository.can_write">
|
||||
<button class="btn" ng-class="!showingHistory ? 'btn-primary active' : 'btn-default'" ng-click="showHistory(false)">
|
||||
<i class="fa fa-tags"></i>Current Tags
|
||||
</button>
|
||||
<button class="btn" ng-class="showingHistory ? 'btn-info active' : 'btn-default'" ng-click="showHistory(true)">
|
||||
<i class="fa fa-history"></i>History
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="tab-header">Repository Tags</h3>
|
||||
<div class="resource-view" resource="imagesResource" error-message="'Could not load images'">
|
||||
|
||||
<!-- History view -->
|
||||
<div ng-show="showingHistory">
|
||||
<div ng-show="showingHistory">
|
||||
<div class="cor-loader" ng-if="!tagHistoryData"></div>
|
||||
</div>
|
||||
|
||||
<div style="height: 40px;">
|
||||
<span class="co-filter-box" style="float:right">
|
||||
<input class="form-control" type="text" ng-model="options.historyFilter" placeholder="Filter History...">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="history-list">
|
||||
<div class="empty" ng-if="!tagHistoryData.length">
|
||||
<div class="empty-primary-msg">This repository is empty.</div>
|
||||
<div class="empty-secondary-msg">Push a tag or initiate a build to populate this repository.</div>
|
||||
</div>
|
||||
|
||||
<div class="history-entry" ng-repeat="entry in tagHistoryData"
|
||||
ng-class="getEntryClasses(entry, options.historyFilter)">
|
||||
<div class="history-date-break" ng-if="entry.date_break">
|
||||
{{ entry.date | amDateFormat:'dddd, MMMM Do YYYY' }}
|
||||
</div>
|
||||
<div ng-if="!entry.date_break">
|
||||
<div class="history-icon"></div>
|
||||
<div class="history-text">
|
||||
<div class="history-description">
|
||||
<span class="tag-span"
|
||||
ng-class="isChecked(entry.tag_name, checkedTags.checked) ? 'checked' : ''"
|
||||
ng-click="showHistory(true, entry.tag_name)">{{ entry.tag_name }}</span>
|
||||
<span ng-switch on="entry.action">
|
||||
<span ng-switch-when="create">
|
||||
was created pointing to image <span class="image-link" repository="repository" image-id="entry.docker_image_id"></span>
|
||||
</span>
|
||||
<span ng-switch-when="delete">
|
||||
was deleted
|
||||
</span>
|
||||
<span ng-switch-when="move">
|
||||
was moved to image
|
||||
<span class="image-link" repository="repository" image-id="entry.docker_image_id"></span>
|
||||
from image
|
||||
<span class="image-link" repository="repository" image-id="entry.old_docker_image_id"></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="history-datetime">{{ entry.time | amDateFormat:'dddd, MMMM Do YYYY, h:mm:ss a' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Normal View -->
|
||||
<div class="resource-view" resource="imagesResource" error-message="'Could not load images'"
|
||||
ng-show="!showingHistory">
|
||||
<div class="co-check-bar">
|
||||
<span class="cor-checkable-menu" controller="checkedTags">
|
||||
<div class="cor-checkable-menu-item" item-filter="allTagFilter">
|
||||
|
@ -23,7 +89,12 @@
|
|||
<a href="javascript:void(0)" class="btn btn-default" ng-click="setTab('changes')">
|
||||
<i class="fa fa-code-fork"></i> Visualize
|
||||
</a>
|
||||
<button class="btn btn-default"
|
||||
<a href="javascript:void(0)" class="btn btn-default"
|
||||
ng-click="showHistory(true, getTagNames(checkedTags.checked))"
|
||||
ng-if="repository.can_write">
|
||||
<i class="fa fa-history"></i> View History
|
||||
</a>
|
||||
<button class="btn btn-primary"
|
||||
ng-click="askDeleteMultipleTags(checkedTags.checked)"
|
||||
ng-if="repository.can_write">
|
||||
<i class="fa fa-times"></i> Delete
|
||||
|
@ -69,9 +140,7 @@
|
|||
</td>
|
||||
<td>{{ tag.size | bytes }}</td>
|
||||
<td class="image-id-col">
|
||||
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}/image/{{ tag.image_id }}">
|
||||
{{ tag.image_id.substr(0, 12) }}
|
||||
</a>
|
||||
<span class="image-link" repoository="repository" image-id="tag.image_id"></span>
|
||||
</td>
|
||||
<td class="image-track" ng-repeat="it in imageTracks">
|
||||
<span class="image-track-dot" ng-if="it.image_id == tag.image_id"
|
||||
|
@ -86,6 +155,10 @@
|
|||
</td>
|
||||
<td class="options-col">
|
||||
<span class="cor-options-menu" ng-if="repository.can_write">
|
||||
<span class="cor-option" option-click="showHistory(true, tag.name)"
|
||||
ng-if="tag.last_modified">
|
||||
<i class="fa fa-history"></i> View Tag History
|
||||
</span>
|
||||
<span class="cor-option" option-click="askDeleteTag(tag.name)">
|
||||
<i class="fa fa-times"></i> Delete Tag
|
||||
</span>
|
||||
|
|
|
@ -25,6 +25,7 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
|
||||
$scope.iterationState = {};
|
||||
$scope.tagActionHandler = null;
|
||||
$scope.showingHistory = false;
|
||||
|
||||
var setTagState = function() {
|
||||
if (!$scope.repository || !$scope.selectedTags) { return; }
|
||||
|
@ -118,8 +119,142 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
|
||||
// Process each of the tags.
|
||||
setTagState();
|
||||
|
||||
if ($scope.showingHistory) {
|
||||
loadTimeline();
|
||||
}
|
||||
});
|
||||
|
||||
var loadTimeline = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||
};
|
||||
|
||||
ApiService.listRepoTags(null, params).then(function(resp) {
|
||||
var tagData = [];
|
||||
var currentTags = {};
|
||||
|
||||
resp.tags.forEach(function(tag) {
|
||||
var tagName = tag.name;
|
||||
var imageId = tag.docker_image_id;
|
||||
var oldImageId = null;
|
||||
|
||||
if (tag.end_ts) {
|
||||
var action = 'delete';
|
||||
|
||||
// If the end time matches the existing start time for this tag, then this is a move
|
||||
// instead of a delete.
|
||||
var currentTime = tag.end_ts * 1000;
|
||||
if (currentTags[tagName] && currentTags[tagName].start_ts == tag.end_ts) {
|
||||
action = 'move';
|
||||
|
||||
// Remove the create.
|
||||
var index = tagData.indexOf(currentTags[tagName]);
|
||||
var createEntry = tagData.splice(index, 1)[0];
|
||||
|
||||
imageId = createEntry.docker_image_id;
|
||||
oldImageId = tag.docker_image_id;
|
||||
}
|
||||
|
||||
// Add the delete/move.
|
||||
tagData.push({
|
||||
'tag_name': tagName,
|
||||
'action': action,
|
||||
'start_ts': tag.start_ts,
|
||||
'end_ts': tag.end_ts,
|
||||
'time': currentTime,
|
||||
'docker_image_id': imageId,
|
||||
'old_docker_image_id': oldImageId
|
||||
})
|
||||
}
|
||||
|
||||
if (tag.start_ts) {
|
||||
var currentTime = tag.start_ts * 1000;
|
||||
var create = {
|
||||
'tag_name': tagName,
|
||||
'action': 'create',
|
||||
'start_ts': tag.start_ts,
|
||||
'end_ts': tag.end_ts,
|
||||
'time': currentTime,
|
||||
'docker_image_id': tag.docker_image_id,
|
||||
'old_docker_image_id': ''
|
||||
};
|
||||
|
||||
tagData.push(create);
|
||||
currentTags[tagName] = create;
|
||||
}
|
||||
});
|
||||
|
||||
tagData.sort(function(a, b) {
|
||||
return b.time - a.time;
|
||||
});
|
||||
|
||||
for (var i = tagData.length - 1; i >= 1; --i) {
|
||||
var current = tagData[i];
|
||||
var next = tagData[i - 1];
|
||||
|
||||
if (new Date(current.time).getDate() != new Date(next.time).getDate()) {
|
||||
tagData.splice(i - 1, 0, {
|
||||
'date_break': true,
|
||||
'date': new Date(current.time)
|
||||
});
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (tagData.length > 0) {
|
||||
tagData.splice(0, 0, {
|
||||
'date_break': true,
|
||||
'date': new Date(tagData[0].time)
|
||||
});
|
||||
}
|
||||
|
||||
$scope.tagHistoryData = tagData;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getEntryClasses = function(entry, historyFilter) {
|
||||
var classes = entry.action + ' ';
|
||||
if (!historyFilter || !entry.action) {
|
||||
return classes;
|
||||
}
|
||||
|
||||
var parts = (historyFilter || '').split(',');
|
||||
var isMatch = parts.some(function(part) {
|
||||
if (part && entry.tag_name) {
|
||||
isMatch = entry.tag_name.indexOf(part) >= 0;
|
||||
isMatch = isMatch || entry.docker_image_id.indexOf(part) >= 0;
|
||||
isMatch = isMatch || entry.old_docker_image_id.indexOf(part) >= 0;
|
||||
return isMatch;
|
||||
}
|
||||
});
|
||||
|
||||
classes += isMatch ? 'filtered-match' : 'filtered-mismatch';
|
||||
return classes;
|
||||
};
|
||||
|
||||
$scope.showHistory = function(value, opt_tagname) {
|
||||
if (opt_tagname) {
|
||||
$scope.options.historyFilter = opt_tagname;
|
||||
} else {
|
||||
$scope.options.historyFilter = '';
|
||||
}
|
||||
|
||||
if ($scope.showingHistory == value) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.showingHistory = value;
|
||||
|
||||
if ($scope.showingHistory) {
|
||||
loadTimeline();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleHistory = function() {
|
||||
$scope.showHistory(!$scope.showingHistory);
|
||||
};
|
||||
|
||||
$scope.trackLineClass = function(index, track_info) {
|
||||
var startIndex = $.inArray(track_info.tags[0], $scope.tags);
|
||||
var endIndex = $.inArray(track_info.tags[track_info.tags.length - 1], $scope.tags);
|
||||
|
@ -206,6 +341,22 @@ angular.module('quay').directive('repoPanelTags', function () {
|
|||
return $scope.imageIDFilter(it.image_id, tag);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getTagNames = function(checked) {
|
||||
var names = checked.map(function(tag) {
|
||||
return tag.name;
|
||||
});
|
||||
|
||||
return names.join(',');
|
||||
};
|
||||
|
||||
$scope.isChecked = function(tagName, checked) {
|
||||
return checked.some(function(tag) {
|
||||
if (tag.name == tagName) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
|
19
static/js/directives/ui/image-link.js
Normal file
19
static/js/directives/ui/image-link.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* An element which displays a link to a repository image.
|
||||
*/
|
||||
angular.module('quay').directive('imageLink', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/image-link.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'imageId': '=imageId'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -11,7 +11,7 @@ from initdb import setup_database_for_testing, finished_database_for_testing
|
|||
from endpoints.api import api_bp, api
|
||||
|
||||
from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite
|
||||
from endpoints.api.tag import RepositoryTagImages, RepositoryTag
|
||||
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, ListRepositoryTags
|
||||
from endpoints.api.search import FindRepositories, EntitySearch
|
||||
from endpoints.api.image import RepositoryImageChanges, RepositoryImage, RepositoryImageList
|
||||
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
||||
|
@ -1753,6 +1753,60 @@ class TestRepositoryBuildLogsS5j8BuynlargeOrgrepo(ApiTestCase):
|
|||
self._run_test('GET', 400, 'devtable', None)
|
||||
|
||||
|
||||
class TestListRepositoryTagsTn96PublicPublicrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(ListRepositoryTags, tag="TN96", repository="public/publicrepo")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 403, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 403, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 403, 'devtable', None)
|
||||
|
||||
|
||||
class TestListRepositoryTagsTn96DevtableShared(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(ListRepositoryTags, tag="TN96", repository="devtable/shared")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 403, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 403, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 200, 'devtable', None)
|
||||
|
||||
|
||||
class TestListRepositoryTagsTn96BuynlargeOrgrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(ListRepositoryTags, tag="TN96", repository="buynlarge/orgrepo")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 403, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 403, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 200, 'devtable', None)
|
||||
|
||||
|
||||
class TestRepositoryTagImagesTn96PublicPublicrepo(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
|
|
Reference in a new issue