From 32b2ecdfa65f213be01b0abbe3a6180fed49a3a4 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 28 Jul 2014 18:23:46 -0400 Subject: [PATCH] Add ability to dismiss notifications --- data/database.py | 2 +- data/model/legacy.py | 18 +++++++++- endpoints/api/user.py | 42 ++++++++++++++++++++++ static/css/quay.css | 5 +++ static/directives/notification-view.html | 7 +++- static/js/app.js | 43 +++++++++++++++++++---- test/data/test.db | Bin 614400 -> 614400 bytes test/test_api_security.py | 33 ++++++++++++++++- test/test_api_usage.py | 23 +++++++++++- 9 files changed, 162 insertions(+), 11 deletions(-) diff --git a/data/database.py b/data/database.py index 88d4e26d6..76a0af9df 100644 --- a/data/database.py +++ b/data/database.py @@ -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): diff --git a/data/model/legacy.py b/data/model/legacy.py index 4d8d28146..c34c0ec3e 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -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 diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 3f54dbf2a..3d79a806d 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -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/') +@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 { diff --git a/static/css/quay.css b/static/css/quay.css index 3e9c4a746..431927b47 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -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; diff --git a/static/directives/notification-view.html b/static/directives/notification-view.html index 6327a5df8..f03133e55 100644 --- a/static/directives/notification-view.html +++ b/static/directives/notification-view.html @@ -6,6 +6,11 @@ {{ notification.organization }} -
{{ parseDate(notification.created) | date:'medium'}}
+ +
{{ parseDate(notification.created) | date:'medium'}}
+ diff --git a/static/js/app.js b/static/js/app.js index 535d23823..b66f5c628 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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); }; diff --git a/test/data/test.db b/test/data/test.db index 56572db7ea745a673e8b126fb43a82fe956d3e89..5946e00d81b9d8ed0e44ed7fd7762b90e2f3ec6f 100644 GIT binary patch delta 5941 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 A4FCWD delta 5928 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 diff --git a/test/test_api_security.py b/test/test_api_security.py index 5f012a8e6..5b3e5612d 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -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) diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 55280d2f5..c91005c5c 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -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)