Add ability to dismiss notifications
This commit is contained in:
parent
34fc279092
commit
32b2ecdfa6
9 changed files with 162 additions and 11 deletions
|
@ -363,7 +363,7 @@ class Notification(BaseModel):
|
|||
target = ForeignKeyField(User, index=True)
|
||||
metadata_json = TextField(default='{}')
|
||||
created = DateTimeField(default=datetime.now, index=True)
|
||||
|
||||
dismissed = BooleanField(default=False)
|
||||
|
||||
|
||||
class ExternalNotificationEvent(BaseModel):
|
||||
|
|
|
@ -1699,7 +1699,15 @@ def create_unique_notification(kind_name, target, metadata={}):
|
|||
create_notification(kind_name, target, metadata)
|
||||
|
||||
|
||||
def list_notifications(user, kind_name=None):
|
||||
def lookup_notification(user, uuid):
|
||||
results = list(list_notifications(user, id_filter=uuid, include_dismissed=True))
|
||||
if not results:
|
||||
return None
|
||||
|
||||
return results[0]
|
||||
|
||||
|
||||
def list_notifications(user, kind_name=None, id_filter=None, include_dismissed=False):
|
||||
Org = User.alias()
|
||||
AdminTeam = Team.alias()
|
||||
AdminTeamMember = TeamMember.alias()
|
||||
|
@ -1722,6 +1730,9 @@ def list_notifications(user, kind_name=None):
|
|||
((AdminUser.id == user) & (TeamRole.name == 'admin')))
|
||||
.order_by(Notification.created)
|
||||
.desc())
|
||||
|
||||
if not include_dismissed:
|
||||
query = query.switch(Notification).where(Notification.dismissed == False)
|
||||
|
||||
if kind_name:
|
||||
query = (query
|
||||
|
@ -1729,6 +1740,11 @@ def list_notifications(user, kind_name=None):
|
|||
.join(NotificationKind)
|
||||
.where(NotificationKind.name == kind_name))
|
||||
|
||||
if id_filter:
|
||||
query = (query
|
||||
.switch(Notification)
|
||||
.where(Notification.uuid == id_filter))
|
||||
|
||||
return query
|
||||
|
||||
|
||||
|
|
|
@ -74,10 +74,12 @@ def user_view(user):
|
|||
|
||||
def notification_view(notification):
|
||||
return {
|
||||
'id': notification.uuid,
|
||||
'organization': notification.target.username if notification.target.organization else None,
|
||||
'kind': notification.kind.name,
|
||||
'created': format_date(notification.created),
|
||||
'metadata': json.loads(notification.metadata_json),
|
||||
'dismissed': notification.dismissed
|
||||
}
|
||||
|
||||
|
||||
|
@ -409,6 +411,46 @@ class UserNotificationList(ApiResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/user/notifications/<uuid>')
|
||||
@internal_only
|
||||
class UserNotification(ApiResource):
|
||||
schemas = {
|
||||
'UpdateNotification': {
|
||||
'id': 'UpdateNotification',
|
||||
'type': 'object',
|
||||
'description': 'Information for updating a notification',
|
||||
'properties': {
|
||||
'dismissed': {
|
||||
'type': 'boolean',
|
||||
'description': 'Whether the notification is dismissed by the user',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@require_user_admin
|
||||
@nickname('getUserNotification')
|
||||
def get(self, uuid):
|
||||
notification = model.lookup_notification(get_authenticated_user(), uuid)
|
||||
if not notification:
|
||||
raise NotFound()
|
||||
|
||||
return notification_view(notification)
|
||||
|
||||
@require_user_admin
|
||||
@nickname('updateUserNotification')
|
||||
@validate_json_request('UpdateNotification')
|
||||
def put(self, uuid):
|
||||
notification = model.lookup_notification(get_authenticated_user(), uuid)
|
||||
if not notification:
|
||||
raise NotFound()
|
||||
|
||||
notification.dismissed = request.get_json().get('dismissed', False)
|
||||
notification.save()
|
||||
|
||||
return notification_view(notification)
|
||||
|
||||
|
||||
def authorization_view(access_token):
|
||||
oauth_app = access_token.application
|
||||
return {
|
||||
|
|
|
@ -89,6 +89,11 @@ nav.navbar-default .navbar-nav>li>a {
|
|||
background: rgba(66, 139, 202, 0.1);
|
||||
}
|
||||
|
||||
.notification-view-element .right-controls {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dockerfile-path {
|
||||
margin-top: 10px;
|
||||
padding: 20px;
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
<img src="//www.gravatar.com/avatar/{{ getGravatar(notification.organization) }}?s=24&d=identicon" />
|
||||
<span class="orgname">{{ notification.organization }}</span>
|
||||
</div>
|
||||
<div class="datetime">{{ parseDate(notification.created) | date:'medium'}}</div>
|
||||
</div>
|
||||
<div class="datetime">{{ parseDate(notification.created) | date:'medium'}}</div>
|
||||
<div class="right-controls">
|
||||
<a href="javascript:void(0)" ng-if="canDismiss(notification)" ng-click="dismissNotification(notification)">
|
||||
Dismiss Notification
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1084,7 +1084,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
'test_notification': {
|
||||
'level': 'primary',
|
||||
'message': 'This notification is a long message for testing',
|
||||
'page': '/about/'
|
||||
'page': '/about/',
|
||||
'dismissable': true
|
||||
},
|
||||
'password_required': {
|
||||
'level': 'error',
|
||||
|
@ -1121,31 +1122,53 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
|||
'message': 'Repository {repository} has been pushed with the following tags updated: {updated_tags}',
|
||||
'page': function(metadata) {
|
||||
return '/repository/' + metadata.repository;
|
||||
}
|
||||
},
|
||||
'dismissable': true
|
||||
},
|
||||
'build_queued': {
|
||||
'level': 'info',
|
||||
'message': 'A build has been queued for repository {repository}',
|
||||
'page': function(metadata) {
|
||||
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||||
}
|
||||
},
|
||||
'dismissable': true
|
||||
},
|
||||
'build_start': {
|
||||
'level': 'info',
|
||||
'message': 'A build has been started for repository {repository}',
|
||||
'page': function(metadata) {
|
||||
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||||
}
|
||||
},
|
||||
'dismissable': true
|
||||
},
|
||||
'build_failure': {
|
||||
'level': 'error',
|
||||
'message': 'A build has failed for repository {repository}',
|
||||
'page': function(metadata) {
|
||||
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||||
}
|
||||
},
|
||||
'dismissable': true
|
||||
}
|
||||
};
|
||||
|
||||
notificationService.dismissNotification = function(notification) {
|
||||
notification.dismissed = true;
|
||||
var params = {
|
||||
'uuid': notification.id
|
||||
};
|
||||
|
||||
ApiService.updateUserNotification(notification, params);
|
||||
|
||||
var index = $.inArray(notification, notificationService.notifications);
|
||||
if (index >= 0) {
|
||||
notificationService.notifications.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
notificationService.canDismiss = function(notification) {
|
||||
return !!notificationKinds[notification['kind']]['dismissable'];
|
||||
};
|
||||
|
||||
notificationService.getPage = function(notification) {
|
||||
var page = notificationKinds[notification['kind']]['page'];
|
||||
if (typeof page != 'string') {
|
||||
|
@ -4907,7 +4930,7 @@ quayApp.directive('notificationView', function () {
|
|||
'notification': '=notification',
|
||||
'parent': '=parent'
|
||||
},
|
||||
controller: function($scope, $element, $window, $location, UserService, NotificationService) {
|
||||
controller: function($scope, $element, $window, $location, UserService, NotificationService, ApiService) {
|
||||
var stringStartsWith = function (str, prefix) {
|
||||
return str.slice(0, prefix.length) == prefix;
|
||||
};
|
||||
|
@ -4943,6 +4966,14 @@ quayApp.directive('notificationView', function () {
|
|||
}
|
||||
};
|
||||
|
||||
$scope.dismissNotification = function(notification) {
|
||||
NotificationService.dismissNotification(notification);
|
||||
};
|
||||
|
||||
$scope.canDismiss = function(notification) {
|
||||
return NotificationService.canDismiss(notification);
|
||||
};
|
||||
|
||||
$scope.getClass = function(notification) {
|
||||
return NotificationService.getClass(notification);
|
||||
};
|
||||
|
|
Binary file not shown.
|
@ -21,7 +21,7 @@ from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, Bu
|
|||
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)
|
||||
Signin, User, UserAuthorizationList, UserAuthorization, UserNotification)
|
||||
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
||||
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
||||
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
|
||||
|
@ -123,6 +123,37 @@ class TestFindRepositories(ApiTestCase):
|
|||
self._run_test('GET', 200, 'devtable', None)
|
||||
|
||||
|
||||
|
||||
class TestUserNotification(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(UserNotification, uuid='someuuid')
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 404, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 404, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 404, 'devtable', None)
|
||||
|
||||
def test_put_anonymous(self):
|
||||
self._run_test('PUT', 401, None, {})
|
||||
|
||||
def test_put_freshuser(self):
|
||||
self._run_test('PUT', 404, 'freshuser', {})
|
||||
|
||||
def test_put_reader(self):
|
||||
self._run_test('PUT', 404, 'reader', {})
|
||||
|
||||
def test_put_devtable(self):
|
||||
self._run_test('PUT', 404, 'devtable', {})
|
||||
|
||||
|
||||
class TestUserInvoiceList(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
|
|
|
@ -23,7 +23,8 @@ from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, Bu
|
|||
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)
|
||||
UserAuthorizationList, UserAuthorization, UserNotification,
|
||||
UserNotificationList)
|
||||
|
||||
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
|
||||
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
||||
|
@ -208,6 +209,26 @@ class TestLoggedInUser(ApiTestCase):
|
|||
assert json['username'] == READ_ACCESS_USER
|
||||
|
||||
|
||||
class TestUserNotification(ApiTestCase):
|
||||
def test_get(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
json = self.getJsonResponse(UserNotificationList)
|
||||
|
||||
# Make sure each notification can be retrieved.
|
||||
for notification in json['notifications']:
|
||||
njson = self.getJsonResponse(UserNotification, params=dict(uuid=notification['id']))
|
||||
self.assertEquals(notification['id'], njson['id'])
|
||||
|
||||
# Update a notification.
|
||||
assert json['notifications']
|
||||
assert not json['notifications'][0]['dismissed']
|
||||
|
||||
pjson = self.putJsonResponse(UserNotification, params=dict(uuid=notification['id']),
|
||||
data=dict(dismissed=True))
|
||||
|
||||
self.assertEquals(True, pjson['dismissed'])
|
||||
|
||||
|
||||
class TestGetUserPrivateAllowed(ApiTestCase):
|
||||
def test_nonallowed(self):
|
||||
self.login(READ_ACCESS_USER)
|
||||
|
|
Reference in a new issue