Merge lp:~allenap/maas/notifications-dismissal into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 5622
Proposed branch: lp:~allenap/maas/notifications-dismissal
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~allenap/maas/notifications-model
Diff against target: 211 lines (+160/-0)
3 files modified
src/maasserver/migrations/builtin/maasserver/0104_notifications_dismissals.py (+27/-0)
src/maasserver/models/notification.py (+51/-0)
src/maasserver/models/tests/test_notification.py (+82/-0)
To merge this branch: bzr merge lp:~allenap/maas/notifications-dismissal
Reviewer Review Type Date Requested Status
Lee Trager (community) Approve
Review via email: mp+314097@code.launchpad.net

Commit message

Finding and dismissing notifications.

To post a comment you must log in.
Revision history for this message
Lee Trager (ltrager) wrote :

LGTM. I noticed that between this branch and its prerequisite you have three seperate migrations. Would it make sense to merge them into one migration for trunk?

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

> I noticed that between this branch and its prerequisite you have three
> seperate migrations. Would it make sense to merge them into one
> migration for trunk?

I consolidated the first couple that appear in the prerequisite, but
left this one separate. I've also reordered them after Mike landed a
migration in his subnet space branch.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'src/maasserver/migrations/builtin/maasserver/0104_notifications_dismissals.py'
2--- src/maasserver/migrations/builtin/maasserver/0104_notifications_dismissals.py 1970-01-01 00:00:00 +0000
3+++ src/maasserver/migrations/builtin/maasserver/0104_notifications_dismissals.py 2017-01-05 09:51:37 +0000
4@@ -0,0 +1,27 @@
5+# -*- coding: utf-8 -*-
6+from __future__ import unicode_literals
7+
8+from django.conf import settings
9+from django.db import (
10+ migrations,
11+ models,
12+)
13+
14+
15+class Migration(migrations.Migration):
16+
17+ dependencies = [
18+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
19+ ('maasserver', '0103_notifications'),
20+ ]
21+
22+ operations = [
23+ migrations.CreateModel(
24+ name='NotificationDismissal',
25+ fields=[
26+ ('id', models.AutoField(auto_created=True, verbose_name='ID', serialize=False, primary_key=True)),
27+ ('notification', models.ForeignKey(to='maasserver.Notification')),
28+ ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
29+ ],
30+ ),
31+ ]
32
33=== modified file 'src/maasserver/models/notification.py'
34--- src/maasserver/models/notification.py 2017-01-05 09:51:36 +0000
35+++ src/maasserver/models/notification.py 2017-01-05 09:51:37 +0000
36@@ -13,6 +13,7 @@
37 CharField,
38 ForeignKey,
39 Manager,
40+ Model,
41 TextField,
42 )
43 from maasserver import DefaultMeta
44@@ -62,6 +63,26 @@
45 ident=ident, user=user, users=users, admins=admins,
46 message=message, context={} if context is None else context)
47
48+ def find_for_user(self, user):
49+ """Find notifications for the given user.
50+
51+ :return: A `RawQuerySet` of `Notification` instances that haven't been
52+ dismissed by `user`.
53+ """
54+ if user.is_superuser:
55+ where = "notification.users OR notification.admins"
56+ else:
57+ where = "notification.users"
58+ return self.raw(self._sql_find_for_user % where, [user.id, user.id])
59+
60+ _sql_find_for_user = """\
61+ SELECT notification.* FROM maasserver_notification AS notification
62+ LEFT OUTER JOIN maasserver_notificationdismissal AS dismissal ON
63+ (dismissal.notification_id = notification.id AND dismissal.user_id = %%s)
64+ WHERE (notification.user_id = %%s OR %s) AND dismissal.id IS NULL
65+ ORDER BY notification.updated, notification.id
66+ """
67+
68
69 class Notification(CleanSave, TimestampedModel):
70 """A notification message.
71@@ -101,6 +122,36 @@
72 """Render this notification's message using its context."""
73 return self.message.format(**self.context)
74
75+ def dismiss(self, user):
76+ """Dismiss this notification.
77+
78+ :param user: The user dismissing this notification.
79+ """
80+ NotificationDismissal.objects.get_or_create(
81+ notification=self, user=user)
82+
83 def clean(self):
84 super(Notification, self).clean()
85 self.render() # Just check it works.
86+
87+ def __repr__(self):
88+ return "<Notification user=%s users=%r admins=%r %r>" % (
89+ ("None" if self.user is None else repr(self.user.username)),
90+ self.users, self.admins, self.render(),
91+ )
92+
93+
94+class NotificationDismissal(Model):
95+ """A notification dismissal.
96+
97+ :ivar notification: The notification which has been dismissed.
98+ :ivar user: The user that has dismissed the linked notification.
99+ """
100+
101+ class Meta(DefaultMeta):
102+ """Needed for South to recognize this model."""
103+
104+ objects = Manager()
105+
106+ notification = ForeignKey(Notification, null=False, blank=False)
107+ user = ForeignKey(User, null=False, blank=False)
108
109=== modified file 'src/maasserver/models/tests/test_notification.py'
110--- src/maasserver/models/tests/test_notification.py 2017-01-05 09:51:36 +0000
111+++ src/maasserver/models/tests/test_notification.py 2017-01-05 09:51:37 +0000
112@@ -12,6 +12,7 @@
113 from maasserver.testing.testcase import MAASServerTestCase
114 from maasserver.utils.orm import reload_object
115 from testtools.matchers import (
116+ AfterPreprocessing,
117 Equals,
118 HasLength,
119 Is,
120@@ -150,6 +151,55 @@
121 HasLength(1))
122
123
124+class TestFindingAndDismissingNotifications(MAASServerTestCase):
125+ """Tests for finding and dismissing notifications."""
126+
127+ def notify(self, user):
128+ message = factory.make_name("message")
129+ return (
130+ Notification.objects.create_for_user(message, user),
131+ Notification.objects.create_for_users(message),
132+ Notification.objects.create_for_admins(message),
133+ )
134+
135+ def assertNotifications(self, user, notifications):
136+ self.assertThat(
137+ Notification.objects.find_for_user(user),
138+ AfterPreprocessing(list, Equals(notifications)))
139+
140+ def test_find_and_dismiss_notifications_for_user(self):
141+ user = factory.make_User()
142+ n_for_user, n_for_users, n_for_admins = self.notify(user)
143+ self.assertNotifications(user, [n_for_user, n_for_users])
144+ n_for_user.dismiss(user)
145+ self.assertNotifications(user, [n_for_users])
146+ n_for_users.dismiss(user)
147+ self.assertNotifications(user, [])
148+
149+ def test_find_and_dismiss_notifications_for_users(self):
150+ user = factory.make_User("user")
151+ user2 = factory.make_User("user2")
152+ n_for_user, n_for_users, n_for_admins = self.notify(user)
153+ self.assertNotifications(user, [n_for_user, n_for_users])
154+ self.assertNotifications(user2, [n_for_users])
155+ n_for_users.dismiss(user2)
156+ self.assertNotifications(user, [n_for_user, n_for_users])
157+ self.assertNotifications(user2, [])
158+
159+ def test_find_and_dismiss_notifications_for_admins(self):
160+ user = factory.make_User("user")
161+ admin = factory.make_admin("admin")
162+ n_for_user, n_for_users, n_for_admins = self.notify(user)
163+ self.assertNotifications(user, [n_for_user, n_for_users])
164+ self.assertNotifications(admin, [n_for_users, n_for_admins])
165+ n_for_users.dismiss(admin)
166+ self.assertNotifications(user, [n_for_user, n_for_users])
167+ self.assertNotifications(admin, [n_for_admins])
168+ n_for_admins.dismiss(admin)
169+ self.assertNotifications(user, [n_for_user, n_for_users])
170+ self.assertNotifications(admin, [])
171+
172+
173 class TestNotification(MAASServerTestCase):
174 """Tests for the `Notification`."""
175
176@@ -171,3 +221,35 @@
177 self.assertThat(str(error), Equals(repr("thing")))
178 self.assertThat(notification.id, Is(None))
179 self.assertThat(Notification.objects.all(), HasLength(0))
180+
181+
182+class TestNotificationRepresentation(MAASServerTestCase):
183+ """Tests for the `Notification` representation."""
184+
185+ def test_for_user(self):
186+ notification = Notification(
187+ user=factory.make_User("foobar"),
188+ message="The cat in the {place}",
189+ context=dict(place="bear trap"))
190+ self.assertThat(
191+ notification, AfterPreprocessing(repr, Equals(
192+ "<Notification user='foobar' users=False admins=False "
193+ "'The cat in the bear trap'>")))
194+
195+ def test_for_users(self):
196+ notification = Notification(
197+ users=True, message="The cat in the {place}",
198+ context=dict(place="blender"))
199+ self.assertThat(
200+ notification, AfterPreprocessing(repr, Equals(
201+ "<Notification user=None users=True admins=False "
202+ "'The cat in the blender'>")))
203+
204+ def test_for_admins(self):
205+ notification = Notification(
206+ admins=True, message="The cat in the {place}",
207+ context=dict(place="lava pit"))
208+ self.assertThat(
209+ notification, AfterPreprocessing(repr, Equals(
210+ "<Notification user=None users=False admins=True "
211+ "'The cat in the lava pit'>")))