Add ability to dismiss notifications

This commit is contained in:
Joseph Schorr 2014-07-28 18:23:46 -04:00
parent 34fc279092
commit 32b2ecdfa6
9 changed files with 162 additions and 11 deletions

View file

@ -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):

View file

@ -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

View file

@ -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 {

View file

@ -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;

View file

@ -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>

View file

@ -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.

View file

@ -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)

View file

@ -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)