Adding in the delete api and the delete and create UI.

This commit is contained in:
Charlton Austin 2016-10-11 15:09:38 -04:00
parent 43063272bb
commit 97d644d95d
12 changed files with 277 additions and 12 deletions

View file

@ -180,6 +180,8 @@ TEST=true python -m test.test_api_usage -f SuiteName
PYTHONPATH=. alembic upgrade head PYTHONPATH=. alembic upgrade head
# You can also rebuild your local sqlite db image from initdb.py using # You can also rebuild your local sqlite db image from initdb.py using
# And once you have a migration you should do this and check in the
# changes to share your migration with others.
rm test/data/test.db rm test/data/test.db
python initdb.py python initdb.py
``` ```

View file

@ -776,6 +776,7 @@ class LogEntry(BaseModel):
class Messages(BaseModel): class Messages(BaseModel):
content = TextField() content = TextField()
uuid = CharField(default=uuid_generator, index=True)
class RepositoryActionCount(BaseModel): class RepositoryActionCount(BaseModel):
repository = ForeignKeyField(Repository) repository = ForeignKeyField(Repository)

View file

@ -0,0 +1,23 @@
"""Add uuid to messages
Revision ID: c156deb8845d
Revises: a3002f7638d5
Create Date: 2016-10-11 15:44:29.450181
"""
# revision identifiers, used by Alembic.
revision = 'c156deb8845d'
down_revision = 'a3002f7638d5'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
def upgrade(tables):
op.add_column('messages', sa.Column('uuid', sa.String(length=36), nullable=True))
def downgrade(tables):
op.drop_column('messages', 'uuid')

View file

@ -11,3 +11,9 @@ def create(messages):
for message in messages: for message in messages:
inserted.append(Messages.create(content=message['content'])) inserted.append(Messages.create(content=message['content']))
return inserted return inserted
def delete_message(uuids):
"""Delete message from the database"""
if not uuids:
return
Messages.delete().where(Messages.uuid << uuids).execute()

View file

@ -820,12 +820,10 @@ class SuperUserServiceKeyApproval(ApiResource):
abort(403) abort(403)
@resource('/v1/messages') @resource('/v1/messages')
@show_if(features.SUPER_USERS) @show_if(features.SUPER_USERS)
class SuperUserMessages(ApiResource): class SuperUserMessages(ApiResource):
""" Resource for getting a list of super user messages """ """ Resource for getting a list of super user messages """
schemas = { schemas = {
'GetMessage': { 'GetMessage': {
'id': 'GetMessage', 'id': 'GetMessage',
@ -838,8 +836,8 @@ class SuperUserMessages(ApiResource):
'itemType': { 'itemType': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'id': { 'uuid': {
'type': 'integer', 'type': 'string',
'description': 'The message id', 'description': 'The message id',
}, },
'content': { 'content': {
@ -877,6 +875,7 @@ class SuperUserMessages(ApiResource):
'messages': [message_view(m) for m in model.message.get_messages()], 'messages': [message_view(m) for m in model.message.get_messages()],
} }
@require_fresh_login
@verify_not_prod @verify_not_prod
@nickname('createMessages') @nickname('createMessages')
@validate_json_request('CreateMessage') @validate_json_request('CreateMessage')
@ -886,8 +885,29 @@ class SuperUserMessages(ApiResource):
if SuperUserPermission().can(): if SuperUserPermission().can():
model.message.create([request.get_json()['message']]) model.message.create([request.get_json()['message']])
return make_response('', 201) return make_response('', 201)
abort(403)
@resource('/v1/message/<uuid>')
@show_if(features.SUPER_USERS)
class SuperUserMessage(ApiResource):
""" Resource for managing individual messages """
@require_fresh_login
@verify_not_prod
@nickname('deleteGlobalMessage')
@require_scope(scopes.SUPERUSER)
def delete(self, uuid):
""" Delete a message """
if SuperUserPermission().can():
model.message.delete_message([uuid])
return make_response('', 204)
abort(403) abort(403)
def message_view(message): def message_view(message):
return {'id': message.id, 'content': message.content} return {
'uuid': message.uuid,
'content': message.content,
}

View file

@ -0,0 +1,90 @@
<!-- Messages tab -->
<div class="global-message-tab-element">
<div class="cor-loader" ng-show="!messages"></div>
<div ng-show="messages">
<div class="manager-header" header-title="Messages">
<button class="create-button btn btn-primary" ng-click="showCreateMessage()">
<i class="fa fa-plus" style="margin-right: 6px;"></i>Create Message
</button>
</div>
<table class="cor-table">
<thead>
<td>Message</td>
<td style="options-cols"></td>
</thead>
<tr ng-repeat="message in messages" class="user-row">
<td>
{{ message.content }}
</td>
<td class="options-col">
<span class="cor-options-menu">
<span class="cor-option" option-click="showDeleteMessage(message.uuid)">
<i class="fa fa-times"></i> Delete Message
</span>
</span>
</td>
</tr>
</table>
</div><!-- Messages tab -->
<!-- Modal delete message dialog -->
<div class="co-dialog modal fade" id="confirmDeleteMessageModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Delete Message?</h4>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="deleteMessage(messageToDelete)">Delete Message</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal create message dialog -->
<div class="co-dialog modal fade" id="createMessageModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Create new message</h4>
</div>
<form name="createMessageForm" ng-submit="createNewMessage()">
<div class="modal-body" ng-show="createdMessage">
<table class="table">
<thead>
<th>Message</th>
</thead>
<tr class="user-row">
<td>{{ createdMessage.content }}</td>
</tr>
</table>
</div>
<div class="modal-body" ng-show="creatingMessage">
<div class="cor-loader"></div>
</div>
<div class="modal-body" ng-show="!creatingMessage && !createdMessage">
<div class="form-group">
<label>Message</label>
<input class="form-control" type="text" ng-model="newMessage.content" required>
</div>
</div>
<div class="modal-footer" ng-show="createdMessage">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
<div class="modal-footer" ng-show="!creatingMessage && !createdMessage">
<button class="btn btn-primary" type="submit" ng-disabled="!createMessageForm.$valid">
Create Message
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</form>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -0,0 +1,81 @@
/**
* An element for managing global messages.
*/
angular.module('quay').directive('globalMessageTab', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/global-message-tab.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'isEnabled': '=isEnabled'
},
controller: function ($scope, $element, ApiService) {
$scope.newMessage = {};
$scope.creatingMessage = false;
$scope.showCreateMessage = function () {
$scope.createdMessage = null;
$('#createMessageModal').modal('show');
};
$scope.createNewMessage = function () {
$scope.creatingMessage = true;
$scope.createdMessage = null;
var errorHandler = ApiService.errorDisplay('Cannot create message', function () {
$scope.creatingMessage = false;
$('#createMessageModal').modal('hide');
});
var data = {
message: $scope.newMessage
};
ApiService.createMessages(data, null).then(function (resp) {
$scope.creatingMessage = false;
$scope.createdMessage = {content: $scope.newMessage.content};
$scope.newMessage = {};
$scope.loadMessageInternal();
}, errorHandler)
};
$scope.showDeleteMessage = function (uuid) {
$scope.messageToDelete = uuid;
$('#confirmDeleteMessageModal').modal({});
};
$scope.deleteMessage = function (uuid) {
$('#confirmDeleteMessageModal').modal('hide');
ApiService.deleteGlobalMessage(null, {uuid: uuid}).then(function (resp) {
$scope.loadMessageInternal();
}, ApiService.errorDisplay('Can not delete message'));
};
$scope.loadMessageOfTheDay = function () {
if ($scope.messages) {
return;
}
$scope.loadMessageInternal();
};
$scope.loadMessageInternal = function () {
ApiService.getMessages().then(function (resp) {
$scope.messages = resp['messages'];
}, function (resp) {
$scope.messages = [];
$scope.messagesErrors = ApiService.getErrorMessage(resp);
});
};
$scope.$watch('isEnabled', function (value) {
if (value) {
$scope.loadMessageInternal();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -31,8 +31,12 @@
$scope.csrf_token = encodeURIComponent(window.__token); $scope.csrf_token = encodeURIComponent(window.__token);
$scope.currentConfig = null; $scope.currentConfig = null;
$scope.serviceKeysActive = false; $scope.serviceKeysActive = false;
$scope.globalMessagesActive = false;
$scope.takeOwnershipInfo = null; $scope.takeOwnershipInfo = null;
$scope.loadMessageOfTheDay = function () {
$scope.globalMessagesActive = true;
};
$scope.configurationSaved = function(config) { $scope.configurationSaved = function(config) {
$scope.currentConfig = config; $scope.currentConfig = config;
$scope.requiresRestart = true; $scope.requiresRestart = true;
@ -69,7 +73,7 @@
var errorHandler = ApiService.errorDisplay('Cannot load system logs. Please contact support.', var errorHandler = ApiService.errorDisplay('Cannot load system logs. Please contact support.',
function() { function() {
callback(false); callback(false);
}) });
ApiService.getSystemLogs(null, params, /* background */true).then(function(resp) { ApiService.getSystemLogs(null, params, /* background */true).then(function(resp) {
$scope.debugLogs = resp['logs']; $scope.debugLogs = resp['logs'];
@ -95,7 +99,7 @@
ApiService.getChangeLog().then(function(resp) { ApiService.getChangeLog().then(function(resp) {
$scope.changeLog = resp; $scope.changeLog = resp;
}, ApiService.errorDisplay('Cannot load change log. Please contact support.')) }, ApiService.errorDisplay('Cannot load change log. Please contact support.'))
} };
$scope.loadUsageLogs = function() { $scope.loadUsageLogs = function() {
$scope.logsCounter++; $scope.logsCounter++;

View file

@ -41,6 +41,10 @@
tab-init="loadConfig()"> tab-init="loadConfig()">
<i class="fa fa-cog"></i> <i class="fa fa-cog"></i>
</span> </span>
<span class="cor-tab hidden-xs" tab-title="Globally visible user messages" tab-target="#message-of-the-day"
tab-init="loadMessageOfTheDay()">
<i class="fa fa-newspaper-o"></i>
</span>
</div> <!-- /cor-tabs --> </div> <!-- /cor-tabs -->
<div class="cor-tab-content"> <div class="cor-tab-content">
@ -50,6 +54,12 @@
configuration-saved="configurationSaved(config)"></div> configuration-saved="configurationSaved(config)"></div>
</div> </div>
<!-- Messages tab -->
<div id="message-of-the-day" class="tab-pane">
<div class="global-message-tab" is-enabled="globalMessagesActive"></div>
</div> <!-- Messages tab -->
<!-- Service keys tab --> <!-- Service keys tab -->
<div id="servicekeys" class="tab-pane"> <div id="servicekeys" class="tab-pane">
<div class="service-keys-manager" is-enabled="serviceKeysActive"></div> <div class="service-keys-manager" is-enabled="serviceKeysActive"></div>
@ -269,7 +279,7 @@
</div><!-- /.modal --> </div><!-- /.modal -->
<!-- Modal message dialog --> <!-- Modal create user dialog -->
<div class="co-dialog modal fade" id="createUserModal"> <div class="co-dialog modal fade" id="createUserModal">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">

Binary file not shown.

View file

@ -51,7 +51,7 @@ from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserMana
SuperUserOrganizationManagement, SuperUserOrganizationList, SuperUserOrganizationManagement, SuperUserOrganizationList,
SuperUserAggregateLogs, SuperUserServiceKeyManagement, SuperUserAggregateLogs, SuperUserServiceKeyManagement,
SuperUserServiceKey, SuperUserServiceKeyApproval, SuperUserServiceKey, SuperUserServiceKeyApproval,
SuperUserTakeOwnership, SuperUserMessages) SuperUserTakeOwnership, SuperUserMessages, SuperUserMessage)
from endpoints.api.secscan import RepositoryImageSecurity from endpoints.api.secscan import RepositoryImageSecurity
from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel from endpoints.api.manifest import RepositoryManifestLabels, ManageRepositoryManifestLabel
@ -4200,6 +4200,7 @@ class TestSuperUserManagement(ApiTestCase):
def test_delete_devtable(self): def test_delete_devtable(self):
self._run_test('DELETE', 204, 'devtable', None) self._run_test('DELETE', 204, 'devtable', None)
class TestSuperUserMessages(ApiTestCase): class TestSuperUserMessages(ApiTestCase):
def setUp(self): def setUp(self):
ApiTestCase.setUp(self) ApiTestCase.setUp(self)
@ -4219,7 +4220,7 @@ class TestSuperUserMessages(ApiTestCase):
def test_post_anonymous(self): def test_post_anonymous(self):
self._run_test('POST', 403, None, dict(message={"content": "new message"})) self._run_test('POST', 401, None, dict(message={"content": "new message"}))
def test_post_freshuser(self): def test_post_freshuser(self):
self._run_test('POST', 403, 'freshuser', dict(message={"content": "new message"})) self._run_test('POST', 403, 'freshuser', dict(message={"content": "new message"}))
@ -4231,6 +4232,24 @@ class TestSuperUserMessages(ApiTestCase):
self._run_test('POST', 201, 'devtable', dict(message={"content": "new message"})) self._run_test('POST', 201, 'devtable', dict(message={"content": "new message"}))
class TestSuperUserMessage(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(SuperUserMessage, uuid='1234')
def test_delete_anonymous(self):
self._run_test('DELETE', 401, None, None)
def test_delete_freshuser(self):
self._run_test('DELETE', 403, 'freshuser', None)
def test_delete_reader(self):
self._run_test('DELETE', 403, 'reader', None)
def test_delete_devtable(self):
self._run_test('DELETE', 204, 'devtable', None)
class TestUserInvoiceFieldList(ApiTestCase): class TestUserInvoiceFieldList(ApiTestCase):
def setUp(self): def setUp(self):
ApiTestCase.setUp(self) ApiTestCase.setUp(self)

View file

@ -66,7 +66,7 @@ from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPe
from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement, from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement,
SuperUserServiceKeyManagement, SuperUserServiceKey, SuperUserServiceKeyManagement, SuperUserServiceKey,
SuperUserServiceKeyApproval, SuperUserTakeOwnership, SuperUserServiceKeyApproval, SuperUserTakeOwnership,
SuperUserMessages) SuperUserMessages, SuperUserMessage)
from endpoints.api.secscan import RepositoryImageSecurity from endpoints.api.secscan import RepositoryImageSecurity
from endpoints.api.suconfig import (SuperUserRegistryStatus, SuperUserConfig, SuperUserConfigFile, from endpoints.api.suconfig import (SuperUserRegistryStatus, SuperUserConfig, SuperUserConfigFile,
SuperUserCreateInitialSuperUser) SuperUserCreateInitialSuperUser)
@ -4290,7 +4290,16 @@ class TestSuperUserManagement(ApiTestCase):
self.assertEquals(len(json['messages']), 2) self.assertEquals(len(json['messages']), 2)
self.assertEquals(json['messages'][1]["content"], "new message") self.assertEquals(json['messages'][1]["content"], "new message")
self.assertNotEqual(json['messages'][0]["content"], json['messages'][1]["content"]) self.assertNotEqual(json['messages'][0]["content"], json['messages'][1]["content"])
self.assertEquals(json['messages'][1]["id"], 2) self.assertTrue(json['messages'][1]["uuid"])
def test_delete_message(self):
self.login(ADMIN_ACCESS_USER)
json = self.getJsonResponse(SuperUserMessages)
self.deleteResponse(SuperUserMessage, {"uuid": json['messages'][0]['uuid']}, 204)
json = self.getJsonResponse(SuperUserMessages)
self.assertEquals(len(json['messages']), 0)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()