Merge lp:~allenap/maas/notifications-websocket-listener into lp:~maas-committers/maas/trunk
- notifications-websocket-listener
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | 5655 |
Proposed branch: | lp:~allenap/maas/notifications-websocket-listener |
Merge into: | lp:~maas-committers/maas/trunk |
Prerequisite: | lp:~allenap/maas/blockdev-sudo-non-interactive |
Diff against target: |
332 lines (+216/-3) 6 files modified
src/maasserver/models/notification.py (+8/-0) src/maasserver/models/tests/test_notification.py (+31/-0) src/maasserver/websockets/handlers/__init__.py (+2/-0) src/maasserver/websockets/handlers/notification.py (+32/-0) src/maasserver/websockets/handlers/tests/test_notification.py (+138/-1) src/maasserver/websockets/tests/test_protocol.py (+5/-2) |
To merge this branch: | bzr merge lp:~allenap/maas/notifications-websocket-listener |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike Pontillo (community) | Approve | ||
Review via email: mp+315356@code.launchpad.net |
Commit message
WebSocket listener behaviour for Notification.
Description of the change
Gavin Panella (allenap) wrote : | # |
Thanks!
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~allenap/maas/notifications-websocket-listener into lp:maas failed. Below is the output from the failed tests.
Get:1 http://
Hit:2 http://
Get:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Fetched 1,044 kB in 0s (1,728 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubun
distro-info is already the newest version (0.14build1).
git is already the newest version (1:2.7.4-0ubuntu1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
pxelinux is already the newest version (3:6.03+
python-formencode is already the ...
Preview Diff
1 | === modified file 'src/maasserver/models/notification.py' |
2 | --- src/maasserver/models/notification.py 2017-01-09 15:44:55 +0000 |
3 | +++ src/maasserver/models/notification.py 2017-01-24 08:41:46 +0000 |
4 | @@ -134,6 +134,14 @@ |
5 | """Render this notification's message using its context.""" |
6 | return self.message.format(**self.context) |
7 | |
8 | + def is_relevant_to(self, user): |
9 | + """Is this notification relevant to the given user?""" |
10 | + return user is not None and ( |
11 | + (self.user_id is not None and self.user_id == user.id) or |
12 | + (self.users and not user.is_superuser) or |
13 | + (self.admins and user.is_superuser) |
14 | + ) |
15 | + |
16 | def dismiss(self, user): |
17 | """Dismiss this notification. |
18 | |
19 | |
20 | === modified file 'src/maasserver/models/tests/test_notification.py' |
21 | --- src/maasserver/models/tests/test_notification.py 2017-01-09 15:44:55 +0000 |
22 | +++ src/maasserver/models/tests/test_notification.py 2017-01-24 08:41:46 +0000 |
23 | @@ -228,6 +228,37 @@ |
24 | self.assertThat(notification.id, Is(None)) |
25 | self.assertThat(Notification.objects.all(), HasLength(0)) |
26 | |
27 | + def test_is_relevant_to_user(self): |
28 | + user = factory.make_User() |
29 | + user2 = factory.make_User() |
30 | + admin = factory.make_admin() |
31 | + |
32 | + Yes, No = Is(True), Is(False) |
33 | + |
34 | + notification_to_user = Notification(user=user) |
35 | + self.assertThat(notification_to_user.is_relevant_to(None), No) |
36 | + self.assertThat(notification_to_user.is_relevant_to(user), Yes) |
37 | + self.assertThat(notification_to_user.is_relevant_to(user2), No) |
38 | + self.assertThat(notification_to_user.is_relevant_to(admin), No) |
39 | + |
40 | + notification_to_users = Notification(users=True) |
41 | + self.assertThat(notification_to_users.is_relevant_to(None), No) |
42 | + self.assertThat(notification_to_users.is_relevant_to(user), Yes) |
43 | + self.assertThat(notification_to_users.is_relevant_to(user2), Yes) |
44 | + self.assertThat(notification_to_users.is_relevant_to(admin), No) |
45 | + |
46 | + notification_to_admins = Notification(admins=True) |
47 | + self.assertThat(notification_to_admins.is_relevant_to(None), No) |
48 | + self.assertThat(notification_to_admins.is_relevant_to(user), No) |
49 | + self.assertThat(notification_to_admins.is_relevant_to(user2), No) |
50 | + self.assertThat(notification_to_admins.is_relevant_to(admin), Yes) |
51 | + |
52 | + notification_to_all = Notification(users=True, admins=True) |
53 | + self.assertThat(notification_to_all.is_relevant_to(None), No) |
54 | + self.assertThat(notification_to_all.is_relevant_to(user), Yes) |
55 | + self.assertThat(notification_to_all.is_relevant_to(user2), Yes) |
56 | + self.assertThat(notification_to_all.is_relevant_to(admin), Yes) |
57 | + |
58 | |
59 | class TestNotificationRepresentation(MAASServerTestCase): |
60 | """Tests for the `Notification` representation.""" |
61 | |
62 | === modified file 'src/maasserver/websockets/handlers/__init__.py' |
63 | --- src/maasserver/websockets/handlers/__init__.py 2017-01-23 11:24:56 +0000 |
64 | +++ src/maasserver/websockets/handlers/__init__.py 2017-01-24 08:41:46 +0000 |
65 | @@ -19,6 +19,7 @@ |
66 | "IPRangeHandler", |
67 | "IPRangeHandler", |
68 | "MachineHandler", |
69 | + "NotificationHandler", |
70 | "PackageRepositoryHandler", |
71 | "ServiceHandler", |
72 | "SpaceHandler", |
73 | @@ -43,6 +44,7 @@ |
74 | from maasserver.websockets.handlers.general import GeneralHandler |
75 | from maasserver.websockets.handlers.iprange import IPRangeHandler |
76 | from maasserver.websockets.handlers.machine import MachineHandler |
77 | +from maasserver.websockets.handlers.notification import NotificationHandler |
78 | from maasserver.websockets.handlers.packagerepository import ( |
79 | PackageRepositoryHandler, |
80 | ) |
81 | |
82 | === modified file 'src/maasserver/websockets/handlers/notification.py' |
83 | --- src/maasserver/websockets/handlers/notification.py 2017-01-09 16:19:02 +0000 |
84 | +++ src/maasserver/websockets/handlers/notification.py 2017-01-24 08:41:46 +0000 |
85 | @@ -8,6 +8,7 @@ |
86 | ] |
87 | |
88 | from maasserver.models.notification import Notification |
89 | +from maasserver.utils.orm import reload_object |
90 | from maasserver.websockets.handlers.timestampedmodel import ( |
91 | TimestampedModelHandler, |
92 | ) |
93 | @@ -19,6 +20,7 @@ |
94 | object_class = Notification |
95 | allowed_methods = {'list', 'get', 'dismiss'} |
96 | exclude = list_exclude = {"context"} |
97 | + listen_channels = {'notification', 'notificationdismissal'} |
98 | |
99 | def get_queryset(self): |
100 | """Return `Notifications` for the current user.""" |
101 | @@ -39,3 +41,33 @@ |
102 | notifications = self.get_queryset().filter(id__in=ids) |
103 | for notification in notifications: |
104 | notification.dismiss(self.user) |
105 | + |
106 | + def on_listen(self, channel, action, pk): |
107 | + """Intercept `on_listen` because dismissals must be handled specially. |
108 | + |
109 | + The docstring for `on_listen` suggests that handlers should override |
110 | + `listen` instead of `on_listen`. However, `Handler.on_listen` makes an |
111 | + assumption about notifications from the database that does not hold |
112 | + here: dismissals are notified as "$notification_id:$user_id" strings, |
113 | + not as simply "$notification_id". |
114 | + """ |
115 | + if channel == "notification": |
116 | + return super().on_listen(channel, action, pk) |
117 | + elif channel == "notificationdismissal": |
118 | + pk, user_id = map(int, pk.split(":")) |
119 | + if self.user.id == user_id: |
120 | + # Send a dismissal as a delete of the notification. |
121 | + return super().on_listen("notification", "delete", pk) |
122 | + else: |
123 | + return None # Not relevant to this user. |
124 | + else: |
125 | + return None # Channel not recognised. |
126 | + |
127 | + def listen(self, channel, action, pk): |
128 | + """Only deal with notifications that are relevant to the user.""" |
129 | + notification = super().listen(channel, action, pk) |
130 | + user = reload_object(self.user) # Check for up-to-date privs. |
131 | + if notification.is_relevant_to(user): |
132 | + return notification |
133 | + else: |
134 | + return None |
135 | |
136 | === modified file 'src/maasserver/websockets/handlers/tests/test_notification.py' |
137 | --- src/maasserver/websockets/handlers/tests/test_notification.py 2017-01-09 16:19:02 +0000 |
138 | +++ src/maasserver/websockets/handlers/tests/test_notification.py 2017-01-24 08:41:46 +0000 |
139 | @@ -5,16 +5,31 @@ |
140 | |
141 | __all__ = [] |
142 | |
143 | +from itertools import product |
144 | +import random |
145 | +from unittest.mock import sentinel |
146 | + |
147 | +from maasserver.models import User |
148 | from maasserver.models.notification import NotificationDismissal |
149 | from maasserver.testing.factory import factory |
150 | from maasserver.testing.testcase import MAASServerTestCase |
151 | -from maasserver.websockets.base import dehydrate_datetime |
152 | +from maasserver.websockets.base import ( |
153 | + dehydrate_datetime, |
154 | + Handler, |
155 | +) |
156 | from maasserver.websockets.handlers.notification import NotificationHandler |
157 | +from maastesting.matchers import ( |
158 | + MockCalledOnceWith, |
159 | + MockNotCalled, |
160 | +) |
161 | +from testscenarios import multiply_scenarios |
162 | from testtools.matchers import ( |
163 | AfterPreprocessing, |
164 | AllMatch, |
165 | Equals, |
166 | Is, |
167 | + IsInstance, |
168 | + MatchesAll, |
169 | MatchesDict, |
170 | MatchesListwise, |
171 | Not, |
172 | @@ -113,3 +128,125 @@ |
173 | self.assertThat(notifications, AllMatch(Not(HasBeenDismissedBy(user)))) |
174 | handler.dismiss({"id": [str(ntfn.id) for ntfn in notifications]}) |
175 | self.assertThat(notifications, AllMatch(HasBeenDismissedBy(user))) |
176 | + |
177 | + |
178 | +class TestNotificationHandlerListening(MAASServerTestCase): |
179 | + """Tests for `NotificationHandler` listening to database messages.""" |
180 | + |
181 | + def test_on_listen_for_notification_up_calls(self): |
182 | + super_on_listen = self.patch(Handler, "on_listen") |
183 | + super_on_listen.return_value = sentinel.on_listen |
184 | + |
185 | + user = factory.make_User() |
186 | + handler = NotificationHandler(user, {}) |
187 | + |
188 | + self.assertThat( |
189 | + handler.on_listen("notification", sentinel.action, sentinel.pk), |
190 | + Is(sentinel.on_listen)) |
191 | + self.assertThat( |
192 | + super_on_listen, MockCalledOnceWith( |
193 | + "notification", sentinel.action, sentinel.pk)) |
194 | + |
195 | + def test_on_listen_for_dismissal_up_calls_with_delete(self): |
196 | + super_on_listen = self.patch(Handler, "on_listen") |
197 | + super_on_listen.return_value = sentinel.on_listen |
198 | + |
199 | + user = factory.make_User() |
200 | + handler = NotificationHandler(user, {}) |
201 | + notification = factory.make_Notification(user=user) |
202 | + |
203 | + # A dismissal notification from the database. |
204 | + dismissal = "%d:%d" % (notification.id, user.id) |
205 | + |
206 | + self.assertThat( |
207 | + handler.on_listen( |
208 | + "notificationdismissal", sentinel.action, dismissal), |
209 | + Is(sentinel.on_listen)) |
210 | + self.assertThat( |
211 | + super_on_listen, MockCalledOnceWith( |
212 | + "notification", "delete", notification.id)) |
213 | + |
214 | + def test_on_listen_for_dismissal_for_other_user_does_nothing(self): |
215 | + super_on_listen = self.patch(Handler, "on_listen") |
216 | + super_on_listen.return_value = sentinel.on_listen |
217 | + |
218 | + user = factory.make_User() |
219 | + handler = NotificationHandler(user, {}) |
220 | + notification = factory.make_Notification(user=user) |
221 | + |
222 | + # A dismissal notification from the database FOR ANOTHER USER. |
223 | + dismissal = "%d:%d" % (notification.id, random.randrange(1, 99999)) |
224 | + |
225 | + self.assertThat(handler.on_listen( |
226 | + "notificationdismissal", sentinel.action, dismissal), Is(None)) |
227 | + self.assertThat(super_on_listen, MockNotCalled()) |
228 | + |
229 | + def test_listen_reloads_user(self): |
230 | + admin = factory.make_admin() |
231 | + handler = NotificationHandler(admin, {}) |
232 | + notification = factory.make_Notification(admins=True) |
233 | + |
234 | + # The notification is currently relevant to `admin`. |
235 | + self.assertThat(handler.listen( |
236 | + "notification", "create", notification.id), Equals(notification)) |
237 | + |
238 | + # Change `admin` to a regular user in the database. |
239 | + admin_alt = User.objects.get(id=admin.id) |
240 | + admin_alt.is_superuser = False |
241 | + admin_alt.save() |
242 | + |
243 | + # The change is noticed because `handler.listen` reloads the user, |
244 | + # meaning the notification is no longer relevant. |
245 | + self.assertThat(handler.listen( |
246 | + "notification", "create", notification.id), Is(None)) |
247 | + |
248 | + |
249 | +class TestNotificationHandlerListeningScenarios(MAASServerTestCase): |
250 | + """Tests for `NotificationHandler` listening to database messages.""" |
251 | + |
252 | + scenarios_users = ( |
253 | + ("user", dict(make_user=factory.make_User)), |
254 | + ("admin", dict(make_user=factory.make_admin)), |
255 | + ) |
256 | + |
257 | + scenarios_notifications = ( |
258 | + ("to-user=%s;to-users=%s;to-admins=%s" % scenario, |
259 | + dict(zip(("to_user", "to_users", "to_admins"), scenario))) |
260 | + for scenario in product( |
261 | + (False, True, "Other"), # To specific user. |
262 | + (False, True), # To all users. |
263 | + (False, True), # To all admins. |
264 | + ) |
265 | + ) |
266 | + |
267 | + scenarios = multiply_scenarios( |
268 | + scenarios_users, scenarios_notifications) |
269 | + |
270 | + def test_on_listen(self): |
271 | + user = self.make_user() |
272 | + |
273 | + if self.to_user is False: |
274 | + to_user = None |
275 | + elif self.to_user is True: |
276 | + to_user = user |
277 | + else: |
278 | + to_user = factory.make_User() |
279 | + |
280 | + notification = factory.make_Notification( |
281 | + user=to_user, users=self.to_users, admins=self.to_admins) |
282 | + |
283 | + if notification.is_relevant_to(user): |
284 | + expected = MatchesAll( |
285 | + IsInstance(tuple), |
286 | + MatchesListwise(( |
287 | + Equals("notification"), Equals("create"), |
288 | + MatchesRenderedNotification(notification), |
289 | + )), |
290 | + first_only=True, |
291 | + ) |
292 | + else: |
293 | + expected = Is(None) |
294 | + |
295 | + handler = NotificationHandler(user, {}) |
296 | + self.assertThat(handler.on_listen( |
297 | + "notification", "create", notification.id), expected) |
298 | |
299 | === modified file 'src/maasserver/websockets/tests/test_protocol.py' |
300 | --- src/maasserver/websockets/tests/test_protocol.py 2016-10-26 15:38:28 +0000 |
301 | +++ src/maasserver/websockets/tests/test_protocol.py 2017-01-24 08:41:46 +0000 |
302 | @@ -697,6 +697,8 @@ |
303 | "fabric", |
304 | "iprange", |
305 | "machine", |
306 | + "notification", |
307 | + "notificationdismissal", |
308 | "packagerepository", |
309 | "service", |
310 | "space", |
311 | @@ -710,18 +712,19 @@ |
312 | ) |
313 | |
314 | ALL_HANDLERS = ( |
315 | - "config", |
316 | "bootresource", |
317 | + "config", |
318 | "controller", |
319 | "device", |
320 | + "dhcpsnippet", |
321 | "discovery", |
322 | - "dhcpsnippet", |
323 | "domain", |
324 | "event", |
325 | "fabric", |
326 | "general", |
327 | "iprange", |
328 | "machine", |
329 | + "notification", |
330 | "packagerepository", |
331 | "service", |
332 | "space", |
Looks good to me. Just one thing below to consider changing.