From 578add3b9e6d1d0890bc1a20d038345269a9427e Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 12 Mar 2014 16:05:32 -0400 Subject: [PATCH] Finish basic notifications system and verify it works for the "password_required" notification. --- data/model.py | 2 +- initdb.py | 3 + static/css/quay.css | 45 ++++++++++++-- static/directives/notification-bar.html | 11 ++-- static/directives/notification-view.html | 7 +++ static/js/app.js | 72 ++++++++++++++++++++++- test/data/test.db | Bin 483328 -> 483328 bytes 7 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 static/directives/notification-view.html diff --git a/data/model.py b/data/model.py index 9eec9f841..99e3e52f7 100644 --- a/data/model.py +++ b/data/model.py @@ -1566,4 +1566,4 @@ def list_notifications(user, kind=None): def delete_notifications_by_kind(user, kind): kind_ref = NotificationKind.get(name=kind) - Notification.delete().where(Notification.user == user, Notification.kind == kind_ref).execute() + Notification.delete().where(Notification.notification_user == user, Notification.kind == kind_ref).execute() diff --git a/initdb.py b/initdb.py index 561d98992..78f694f67 100644 --- a/initdb.py +++ b/initdb.py @@ -266,6 +266,9 @@ def populate_database(): new_user_4.verified = True new_user_4.save() + new_user_5 = model.create_user('unverified', 'password', 'no5@thanks.com') + new_user_5.save() + reader = model.create_user('reader', 'password', 'no1@thanks.com') reader.verified = True reader.save() diff --git a/static/css/quay.css b/static/css/quay.css index dc80d0608..f706a8c5e 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -9,6 +9,43 @@ } } +.notification-view-element { + cursor: pointer; + margin-bottom: 10px; + border-bottom: 1px solid #eee; + padding-bottom: 10px; + position: relative; + max-width: 320px; +} + +.notification-view-element .circle { + position: absolute; + top: 14px; + left: 0px; + + width: 12px; + height: 12px; + display: inline-block; + border-radius: 50%; +} + +.notification-view-element .datetime { + margin-top: 10px; + font-size: 12px; + color: #aaa; + text-align: right; +} + +.notification-view-element .container { + padding: 10px; + border-radius: 6px; + margin-left: 16px; +} + +.notification-view-element .container:hover { + background: rgba(66, 139, 202, 0.1); +} + .dockerfile-path { margin-top: 10px; padding: 20px; @@ -507,22 +544,22 @@ i.toggle-icon:hover { min-width: 200px; } -.user-notification.notification-primary { +.notification-primary { background: #428bca; color: white; } -.user-notification.notification-info { +.notification-info { color: black; background: #d9edf7; } -.user-notification.notification-warning { +.notification-warning { color: #8a6d3b; background: #fcf8e3; } -.user-notification.notification-error { +.notification-error { background: red; } diff --git a/static/directives/notification-bar.html b/static/directives/notification-bar.html index f0f5f3363..5d25a40b4 100644 --- a/static/directives/notification-bar.html +++ b/static/directives/notification-bar.html @@ -3,12 +3,13 @@
-

Some title

-
-
- +
+
+
+
+
diff --git a/static/directives/notification-view.html b/static/directives/notification-view.html new file mode 100644 index 000000000..6d5751995 --- /dev/null +++ b/static/directives/notification-view.html @@ -0,0 +1,7 @@ +
+
+
{{ getMessage(notification) }}
+
{{ parseDate(notification.created) | date:'medium'}}
+
+
+
diff --git a/static/js/app.js b/static/js/app.js index 63fd27024..acf97b97d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -494,10 +494,26 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu 'test_notification': { 'level': 'primary', 'summary': 'This is a test notification', - 'message': 'This notification is a long message for testing' + 'message': 'This notification is a long message for testing', + 'page': '/about/' + }, + 'password_required': { + 'level': 'error', + 'summary': 'A password is needed for your account', + 'message': 'In order to begin pushing and pulling repositories to Quay.io, a password must be set for your account', + 'page': '/user?tab=password' } }; + notificationService.getPage = function(notification) { + return notificationKinds[notification['kind']]['page']; + }; + + notificationService.getMessage = function(notification) { + var kindInfo = notificationKinds[notification['kind']]; + return StringBuilderService.buildString(kindInfo['message'], notification['metadata']); + }; + notificationService.getSummary = function(notification) { var kindInfo = notificationKinds[notification['kind']]; return StringBuilderService.buildString(kindInfo['summary'], notification['metadata']); @@ -512,16 +528,25 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu return summaries.join('
'); }; + notificationService.getClass = function(notification) { + return 'notification-' + notificationKinds[notification['kind']]['level']; + }; + notificationService.getClasses = function(notifications) { var classes = []; for (var i = 0; i < notifications.length; ++i) { var notification = notifications[i]; - classes.push('notification-' + notificationKinds[notification['kind']]['level']); + classes.push(notificationService.getClass(notification)); } return classes.join(' '); }; notificationService.update = function() { + var user = UserService.currentUser(); + if (!user || user.anonymous) { + return; + } + ApiService.listUserNotifications().then(function(resp) { notificationService.notifications = resp['notifications']; notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications); @@ -3320,6 +3345,49 @@ quayApp.directive('buildProgress', function () { }); +quayApp.directive('notificationView', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/notification-view.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'notification': '=notification', + 'parent': '=parent' + }, + controller: function($scope, $element, $location, NotificationService) { + $scope.getMessage = function(notification) { + return NotificationService.getMessage(notification); + }; + + $scope.parseDate = function(dateString) { + return Date.parse(dateString); + }; + + $scope.showNotification = function() { + var url = NotificationService.getPage($scope.notification); + if (url) { + var parts = url.split('?') + $location.path(parts[0]); + + if (parts.length > 1) { + $location.search(parts[1]); + } + + $scope.parent.$hide(); + } + }; + + $scope.getClass = function(notification) { + return NotificationService.getClass(notification); + }; + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('dockerfileBuildDialog', function () { var directiveDefinitionObject = { priority: 0, diff --git a/test/data/test.db b/test/data/test.db index dd1b56708da5e40345ece6be2635fa96e48055fe..168be98baff4437e0f89db90e86cf92e73f6cebe 100644 GIT binary patch delta 6464 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