Severity and Markdown support in MOTD

[Delivers #133555165]
This commit is contained in:
Joseph Schorr 2017-01-17 16:41:24 -05:00
parent 0f203b01d3
commit 3106504f39
13 changed files with 234 additions and 36 deletions

View file

@ -828,11 +828,6 @@ class LogEntry(BaseModel):
(('repository', 'datetime', 'kind'), False), (('repository', 'datetime', 'kind'), False),
) )
class Messages(BaseModel):
content = TextField()
# TODO: This should be non-nullable and indexed
uuid = CharField(default=uuid_generator, max_length=36, null=True)
class RepositoryActionCount(BaseModel): class RepositoryActionCount(BaseModel):
repository = ForeignKeyField(Repository) repository = ForeignKeyField(Repository)
@ -1022,6 +1017,13 @@ class MediaType(BaseModel):
name = CharField(index=True, unique=True) name = CharField(index=True, unique=True)
class Messages(BaseModel):
content = TextField()
uuid = CharField(default=uuid_generator, max_length=36, index=True)
severity = CharField(default='info', index=True)
media_type = ForeignKeyField(MediaType)
class LabelSourceType(BaseModel): class LabelSourceType(BaseModel):
""" LabelSourceType is an enumeration of the possible sources for a label. """ """ LabelSourceType is an enumeration of the possible sources for a label. """
name = CharField(index=True, unique=True) name = CharField(index=True, unique=True)

View file

@ -0,0 +1,54 @@
"""Add severity and media_type to global messages
Revision ID: 3e8cc74a1e7b
Revises: fc47c1ec019f
Create Date: 2017-01-17 16:22:28.584237
"""
# revision identifiers, used by Alembic.
revision = '3e8cc74a1e7b'
down_revision = 'fc47c1ec019f'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
def upgrade(tables):
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('messages', sa.Column('media_type_id', sa.Integer(), nullable=False, server_default='1'))
op.add_column('messages', sa.Column('severity', sa.String(length=255), nullable=False, server_default='info'))
op.alter_column('messages', 'uuid',
existing_type=mysql.VARCHAR(length=36),
server_default='',
nullable=False)
op.create_index('messages_media_type_id', 'messages', ['media_type_id'], unique=False)
op.create_index('messages_severity', 'messages', ['severity'], unique=False)
op.create_index('messages_uuid', 'messages', ['uuid'], unique=False)
op.create_foreign_key(op.f('fk_messages_media_type_id_mediatype'), 'messages', 'mediatype', ['media_type_id'], ['id'])
# ### end Alembic commands ###
op.bulk_insert(tables.mediatype,
[
{'name': 'text/markdown'},
])
def downgrade(tables):
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f('fk_messages_media_type_id_mediatype'), 'messages', type_='foreignkey')
op.drop_index('messages_uuid', table_name='messages')
op.drop_index('messages_severity', table_name='messages')
op.drop_index('messages_media_type_id', table_name='messages')
op.alter_column('messages', 'uuid',
existing_type=mysql.VARCHAR(length=36),
nullable=True)
op.drop_column('messages', 'severity')
op.drop_column('messages', 'media_type_id')
# ### end Alembic commands ###
op.execute(tables
.mediatype
.delete()
.where(tables.
mediatype.c.name == op.inline_literal('text/markdown')))

View file

@ -1,15 +1,20 @@
from data.database import Messages from data.database import Messages, MediaType
def get_messages(): def get_messages():
"""Query the data base for messages and returns a container of database message objects""" """Query the data base for messages and returns a container of database message objects"""
return Messages.select() return Messages.select(Messages, MediaType).join(MediaType)
def create(messages): def create(messages):
"""Insert messages into the database.""" """Insert messages into the database."""
inserted = [] inserted = []
for message in messages: for message in messages:
inserted.append(Messages.create(content=message['content'])) severity = message['severity']
media_type_name = message['media_type']
media_type = MediaType.get(name=media_type_name)
inserted.append(Messages.create(content=message['content'], media_type=media_type,
severity=severity))
return inserted return inserted
def delete_message(uuids): def delete_message(uuids):

View file

@ -35,6 +35,16 @@ class GlobalUserMessages(ApiResource):
'type': 'string', 'type': 'string',
'description': 'The actual message', 'description': 'The actual message',
}, },
'media_type': {
'type': 'string',
'description': 'The media type of the message',
'enum': ['text/plain', 'text/markdown'],
},
'severity': {
'type': 'string',
'description': 'The severity of the message',
'enum': ['info', 'warning', 'error'],
},
}, },
}, },
}, },
@ -53,6 +63,16 @@ class GlobalUserMessages(ApiResource):
'type': 'string', 'type': 'string',
'description': 'The actual message', 'description': 'The actual message',
}, },
'media_type': {
'type': 'string',
'description': 'The media type of the message',
'enum': ['text/plain', 'text/markdown'],
},
'severity': {
'type': 'string',
'description': 'The severity of the message',
'enum': ['info', 'warning', 'error'],
},
}, },
}, },
}, },
@ -104,4 +124,6 @@ def message_view(message):
return { return {
'uuid': message.uuid, 'uuid': message.uuid,
'content': message.content, 'content': message.content,
'severity': message.severity,
'media_type': message.media_type.name,
} }

View file

@ -395,6 +395,7 @@ def initialize_database():
MediaType.create(name='text/plain') MediaType.create(name='text/plain')
MediaType.create(name='application/json') MediaType.create(name='application/json')
MediaType.create(name='text/markdown')
LabelSourceType.create(name='manifest') LabelSourceType.create(name='manifest')
LabelSourceType.create(name='api', mutable=True) LabelSourceType.create(name='api', mutable=True)
@ -798,7 +799,11 @@ def populate_database(minimal=False, with_storage=False):
'trigger_id': trigger.uuid, 'config': json.loads(trigger.config), 'trigger_id': trigger.uuid, 'config': json.loads(trigger.config),
'service': trigger.service.name}) 'service': trigger.service.name})
model.message.create([{'content': 'We love you, Quay customers!'}]) model.message.create([{'content': 'We love you, Quay customers!', 'severity': 'info',
'media_type': 'text/plain'}])
model.message.create([{'content': 'This is a **development** install of Quay',
'severity': 'warning', 'media_type': 'text/markdown'}])
fake_queue = WorkQueue('fakequeue', tf) fake_queue = WorkQueue('fakequeue', tf)
fake_queue.put(['canonical', 'job', 'name'], '{}') fake_queue.put(['canonical', 'job', 'name'], '{}')

View file

@ -0,0 +1,29 @@
.global-message-tab-element .message-content p {
display: inline-block;
margin: 0px;
}
.global-message-tab-element .fa {
margin-right: 4px;
}
.global-message-tab-element .ci-stop {
color: red;
}
.global-message-tab-element .fa-exclamation-triangle {
color: #E4C212;
}
.global-message-tab-element .fa-info-circle {
color: #124fd8;
}
.global-message-tab-element label {
margin-top: 20px;
font-size: 16px;
}
.global-message-tab-element label:first-child {
margin-top: 4px;
}

View file

@ -0,0 +1,34 @@
.quay-message-bar-element .markdown-view p {
margin: 0px;
display: inline-block;
}
.quay-message-bar-element .quay-service-status-description.warning {
background: #FFFBF0;
color: black;
}
.quay-message-bar-element .quay-service-status-description.warning:before {
font-family: FontAwesome;
content: "\f071";
font-size: 22px;
color: #E4C212;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
.quay-message-bar-element .quay-service-status-description.error {
background: #FFF0F0;
color: black;
}
.quay-message-bar-element .quay-service-status-description.error:before {
font-family: core-icons;
content: "\f109";
font-size: 22px;
color: red;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}

View file

@ -11,12 +11,26 @@
<table class="cor-table"> <table class="cor-table">
<thead> <thead>
<td>Message</td> <td>Message</td>
<td>Severity</td>
<td style="options-cols"></td> <td style="options-cols"></td>
</thead> </thead>
<tr ng-repeat="message in messages" class="user-row"> <tr ng-repeat="message in messages" class="user-row">
<td> <td class="message-content">
{{ message.content }} <span ng-switch on="message.media_type">
<span ng-switch-when="text/markdown">
<span class="markdown-view" content="message.content"></span>
</span>
<span ng-switch-default>{{ message.content }}</span>
</span>
</td>
<td class="message-severity" ng-class="message.severity">
<span ng-switch on="message.severity">
<i class="fa fa-exclamation-triangle" ng-switch-when="warning"></i>
<i class="fa ci-stop" ng-switch-when="error"></i>
<i class="fa fa-info-circle" ng-switch-default></i>
</span>
{{ message.severity }}
</td> </td>
<td class="options-col"> <td class="options-col">
<span class="cor-options-menu"> <span class="cor-options-menu">
@ -54,25 +68,21 @@
<h4 class="modal-title">Create new message</h4> <h4 class="modal-title">Create new message</h4>
</div> </div>
<form name="createMessageForm" ng-submit="createNewMessage()"> <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="modal-body" ng-show="creatingMessage">
<div class="cor-loader"></div> <div class="cor-loader"></div>
</div> </div>
<div class="modal-body" ng-show="!creatingMessage && !createdMessage"> <div class="modal-body" ng-show="!creatingMessage && !createdMessage">
<div class="form-group"> <div class="form-group">
<label>Message</label> <label>Severity</label>
<input class="form-control" type="text" ng-model="newMessage.content" required> <select class="form-control" ng-model="newMessage.severity">
</div> <option value="info">Normal (Info)</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
</select>
<label>Message</label>
<div class="markdown-editor" content="newMessage.content"></div>
</div>
</div> </div>
<div class="modal-footer" ng-show="createdMessage"> <div class="modal-footer" ng-show="createdMessage">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>

View file

@ -1,5 +1,12 @@
<div class="announcement inline" ng-show="messages.length"> <div class="announcement inline quay-message-bar-element" ng-show="messages.length">
<div ng-repeat="message in messages"> <div ng-repeat="message in messages">
<div class="quay-service-status-description">{{ message.content }}</div> <div class="quay-service-status-description" ng-class="message.severity">
<span ng-switch on="message.media_type">
<span ng-switch-when="text/markdown">
<span class="markdown-view" content="message.content"></span>
</span>
<span ng-switch-default>{{ message.content }}</span>
</span>
</div>
</div> </div>
</div> </div>

View file

@ -12,7 +12,10 @@ angular.module('quay').directive('globalMessageTab', function () {
'isEnabled': '=isEnabled' 'isEnabled': '=isEnabled'
}, },
controller: function ($scope, $element, ApiService) { controller: function ($scope, $element, ApiService) {
$scope.newMessage = {}; $scope.newMessage = {
'media_type': 'text/markdown',
'severity': 'info'
};
$scope.creatingMessage = false; $scope.creatingMessage = false;
$scope.showCreateMessage = function () { $scope.showCreateMessage = function () {
@ -30,13 +33,17 @@ angular.module('quay').directive('globalMessageTab', function () {
}); });
var data = { var data = {
message: $scope.newMessage 'message': $scope.newMessage
}; };
ApiService.createGlobalMessage(data, null).then(function (resp) { ApiService.createGlobalMessage(data, null).then(function (resp) {
$scope.creatingMessage = false; $scope.creatingMessage = false;
$scope.createdMessage = {content: $scope.newMessage.content}; $scope.newMessage = {
$scope.newMessage = {}; 'media_type': 'text/markdown',
'severity': 'info'
};
$('#createMessageModal').modal('hide');
$scope.loadMessageInternal(); $scope.loadMessageInternal();
}, errorHandler) }, errorHandler)
}; };

Binary file not shown.

View file

@ -4295,6 +4295,25 @@ class TestSuperUserManagement(ApiTestCase):
self._run_test('DELETE', 204, 'devtable', None) self._run_test('DELETE', 204, 'devtable', None)
class TestSuperUserMessages(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(GlobalUserMessages)
self.message = {'message': {'content': '', 'severity': 'info', 'media_type': 'text/plain'}}
def test_post_anonymous(self):
self._run_test('POST', 401, None, None)
def test_post_freshuser(self):
self._run_test('POST', 403, 'freshuser', self.message)
def test_post_reader(self):
self._run_test('POST', 403, 'reader', self.message)
def test_post_devtable(self):
self._run_test('POST', 201, 'devtable', self.message)
class TestSuperUserMessage(ApiTestCase): class TestSuperUserMessage(ApiTestCase):
def setUp(self): def setUp(self):
ApiTestCase.setUp(self) ApiTestCase.setUp(self)

View file

@ -4733,14 +4733,18 @@ class TestSuperUserManagement(ApiTestCase):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
# Create a message # Create a message
self.postResponse(GlobalUserMessages, data=dict(message={"content": "new message"}), expected_code=201) message = {"content": "new message", "severity": "info", "media_type": "text/plain"}
self.postResponse(GlobalUserMessages, data=dict(message=message), expected_code=201)
json = self.getJsonResponse(GlobalUserMessages) json = self.getJsonResponse(GlobalUserMessages)
self.assertEquals(len(json['messages']), 3)
self.assertEquals(len(json['messages']), 2) self.assertEquals(json['messages'][2]["content"], "new message")
self.assertEquals(json['messages'][1]["content"], "new message") self.assertEquals(json['messages'][2]["severity"], "info")
self.assertNotEqual(json['messages'][0]["content"], json['messages'][1]["content"]) self.assertEquals(json['messages'][2]["media_type"], "text/plain")
self.assertTrue(json['messages'][1]["uuid"])
self.assertNotEqual(json['messages'][0]["content"], json['messages'][2]["content"])
self.assertTrue(json['messages'][2]["uuid"])
def test_delete_message(self): def test_delete_message(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
@ -4749,7 +4753,7 @@ class TestSuperUserManagement(ApiTestCase):
json = self.getJsonResponse(GlobalUserMessages) json = self.getJsonResponse(GlobalUserMessages)
self.assertEquals(len(json['messages']), 0) self.assertEquals(len(json['messages']), 1)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()