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)
|
target = ForeignKeyField(User, index=True)
|
||||||
metadata_json = TextField(default='{}')
|
metadata_json = TextField(default='{}')
|
||||||
created = DateTimeField(default=datetime.now, index=True)
|
created = DateTimeField(default=datetime.now, index=True)
|
||||||
|
dismissed = BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
class ExternalNotificationEvent(BaseModel):
|
class ExternalNotificationEvent(BaseModel):
|
||||||
|
|
|
@ -1699,7 +1699,15 @@ def create_unique_notification(kind_name, target, metadata={}):
|
||||||
create_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()
|
Org = User.alias()
|
||||||
AdminTeam = Team.alias()
|
AdminTeam = Team.alias()
|
||||||
AdminTeamMember = TeamMember.alias()
|
AdminTeamMember = TeamMember.alias()
|
||||||
|
@ -1723,12 +1731,20 @@ def list_notifications(user, kind_name=None):
|
||||||
.order_by(Notification.created)
|
.order_by(Notification.created)
|
||||||
.desc())
|
.desc())
|
||||||
|
|
||||||
|
if not include_dismissed:
|
||||||
|
query = query.switch(Notification).where(Notification.dismissed == False)
|
||||||
|
|
||||||
if kind_name:
|
if kind_name:
|
||||||
query = (query
|
query = (query
|
||||||
.switch(Notification)
|
.switch(Notification)
|
||||||
.join(NotificationKind)
|
.join(NotificationKind)
|
||||||
.where(NotificationKind.name == kind_name))
|
.where(NotificationKind.name == kind_name))
|
||||||
|
|
||||||
|
if id_filter:
|
||||||
|
query = (query
|
||||||
|
.switch(Notification)
|
||||||
|
.where(Notification.uuid == id_filter))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -74,10 +74,12 @@ def user_view(user):
|
||||||
|
|
||||||
def notification_view(notification):
|
def notification_view(notification):
|
||||||
return {
|
return {
|
||||||
|
'id': notification.uuid,
|
||||||
'organization': notification.target.username if notification.target.organization else None,
|
'organization': notification.target.username if notification.target.organization else None,
|
||||||
'kind': notification.kind.name,
|
'kind': notification.kind.name,
|
||||||
'created': format_date(notification.created),
|
'created': format_date(notification.created),
|
||||||
'metadata': json.loads(notification.metadata_json),
|
'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):
|
def authorization_view(access_token):
|
||||||
oauth_app = access_token.application
|
oauth_app = access_token.application
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -89,6 +89,11 @@ nav.navbar-default .navbar-nav>li>a {
|
||||||
background: rgba(66, 139, 202, 0.1);
|
background: rgba(66, 139, 202, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-view-element .right-controls {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.dockerfile-path {
|
.dockerfile-path {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|
|
@ -6,6 +6,11 @@
|
||||||
<img src="//www.gravatar.com/avatar/{{ getGravatar(notification.organization) }}?s=24&d=identicon" />
|
<img src="//www.gravatar.com/avatar/{{ getGravatar(notification.organization) }}?s=24&d=identicon" />
|
||||||
<span class="orgname">{{ notification.organization }}</span>
|
<span class="orgname">{{ notification.organization }}</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1084,7 +1084,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
|
||||||
'test_notification': {
|
'test_notification': {
|
||||||
'level': 'primary',
|
'level': 'primary',
|
||||||
'message': 'This notification is a long message for testing',
|
'message': 'This notification is a long message for testing',
|
||||||
'page': '/about/'
|
'page': '/about/',
|
||||||
|
'dismissable': true
|
||||||
},
|
},
|
||||||
'password_required': {
|
'password_required': {
|
||||||
'level': 'error',
|
'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}',
|
'message': 'Repository {repository} has been pushed with the following tags updated: {updated_tags}',
|
||||||
'page': function(metadata) {
|
'page': function(metadata) {
|
||||||
return '/repository/' + metadata.repository;
|
return '/repository/' + metadata.repository;
|
||||||
}
|
},
|
||||||
|
'dismissable': true
|
||||||
},
|
},
|
||||||
'build_queued': {
|
'build_queued': {
|
||||||
'level': 'info',
|
'level': 'info',
|
||||||
'message': 'A build has been queued for repository {repository}',
|
'message': 'A build has been queued for repository {repository}',
|
||||||
'page': function(metadata) {
|
'page': function(metadata) {
|
||||||
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||||||
}
|
},
|
||||||
|
'dismissable': true
|
||||||
},
|
},
|
||||||
'build_start': {
|
'build_start': {
|
||||||
'level': 'info',
|
'level': 'info',
|
||||||
'message': 'A build has been started for repository {repository}',
|
'message': 'A build has been started for repository {repository}',
|
||||||
'page': function(metadata) {
|
'page': function(metadata) {
|
||||||
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
||||||
}
|
},
|
||||||
|
'dismissable': true
|
||||||
},
|
},
|
||||||
'build_failure': {
|
'build_failure': {
|
||||||
'level': 'error',
|
'level': 'error',
|
||||||
'message': 'A build has failed for repository {repository}',
|
'message': 'A build has failed for repository {repository}',
|
||||||
'page': function(metadata) {
|
'page': function(metadata) {
|
||||||
return '/repository/' + metadata.repository + '/build?current=' + metadata.build_id;
|
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) {
|
notificationService.getPage = function(notification) {
|
||||||
var page = notificationKinds[notification['kind']]['page'];
|
var page = notificationKinds[notification['kind']]['page'];
|
||||||
if (typeof page != 'string') {
|
if (typeof page != 'string') {
|
||||||
|
@ -4907,7 +4930,7 @@ quayApp.directive('notificationView', function () {
|
||||||
'notification': '=notification',
|
'notification': '=notification',
|
||||||
'parent': '=parent'
|
'parent': '=parent'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $window, $location, UserService, NotificationService) {
|
controller: function($scope, $element, $window, $location, UserService, NotificationService, ApiService) {
|
||||||
var stringStartsWith = function (str, prefix) {
|
var stringStartsWith = function (str, prefix) {
|
||||||
return str.slice(0, prefix.length) == 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) {
|
$scope.getClass = function(notification) {
|
||||||
return NotificationService.getClass(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.repoemail import RepositoryAuthorizedEmail
|
||||||
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
||||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
|
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.repotoken import RepositoryToken, RepositoryTokenList
|
||||||
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
||||||
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
|
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
|
||||||
|
@ -123,6 +123,37 @@ class TestFindRepositories(ApiTestCase):
|
||||||
self._run_test('GET', 200, 'devtable', None)
|
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):
|
class TestUserInvoiceList(ApiTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
ApiTestCase.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.repoemail import RepositoryAuthorizedEmail
|
||||||
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
|
||||||
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
|
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.repotoken import RepositoryToken, RepositoryTokenList
|
||||||
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
|
||||||
|
@ -208,6 +209,26 @@ class TestLoggedInUser(ApiTestCase):
|
||||||
assert json['username'] == READ_ACCESS_USER
|
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):
|
class TestGetUserPrivateAllowed(ApiTestCase):
|
||||||
def test_nonallowed(self):
|
def test_nonallowed(self):
|
||||||
self.login(READ_ACCESS_USER)
|
self.login(READ_ACCESS_USER)
|
||||||
|
|
Reference in a new issue