Merge lp:~wallyworld/lazr.restful/propogate-notifications into lp:lazr.restful

Proposed by Ian Booth
Status: Rejected
Rejected by: Ian Booth
Proposed branch: lp:~wallyworld/lazr.restful/propogate-notifications
Merge into: lp:lazr.restful
Diff against target: 271 lines (+139/-2)
8 files modified
src/lazr/restful/NEWS.txt (+9/-0)
src/lazr/restful/_resource.py (+14/-0)
src/lazr/restful/example/base/configure.zcml (+7/-0)
src/lazr/restful/example/base/traversal.py (+8/-0)
src/lazr/restful/interfaces/_rest.py (+16/-0)
src/lazr/restful/simple.py (+30/-1)
src/lazr/restful/tests/test_webservice.py (+54/-0)
src/lazr/restful/version.txt (+1/-1)
To merge this branch: bzr merge lp:~wallyworld/lazr.restful/propogate-notifications
Reviewer Review Type Date Requested Status
Leonard Richardson (community) Disapprove
Review via email: mp+54340@code.launchpad.net

Commit message

When constructing the data object from an EntryResource to return to the client, include any informational, warning, error messages the server side call may have attached to the request.response object.

Description of the change

When constructing the data object from an EntryResource to return to the client, include any informational, warning, error messages the server side call may have attached to the request.response object.

To post a comment you must log in.
186. By Ian Booth

Set notifications to empty list rather than undefined when there are none

Revision history for this message
Leonard Richardson (leonardr) wrote :

I have a couple concerns about this:

1. You're assuming that request.response.notifications exists, even though IHTTPRequest doesn't define a 'response' attribute and IHTTPResponse doesn't define a 'notifications' attribute. We already make the assumption that request.response exists, all over the place, and I'm pretty sure that's a Zope convention. So I'm not worried about that.

But where does 'notifications' come from? That's defined by INotificationsResponse in Launchpad. We can't put Launchpad-specific code in lazr.restful, because it's used by other projects. You have a couple options:

1A: You can move INotificationsResponse into lazr.restful, and only handle the notifications if the response can be adapted to INotificationsResponse. You can then write a stub implementation and test the notifications code in lazr.restful. Users who have no need for this can just choose not to register an adapter from IHTTPResponse to INotificationsResponse.

1B. You can move this code into Launchpad, and make the handling of notifications a Launchpad-specific enhancement to the lazr.restful web service. I recommend 1A instead.

2. These notifications aren't part of the description of the object, so they don't belong in its JSON representation. In the motivating case, bug 698138, the notification is "foo did not previously have any assigned bugs in barproject". That has nothing to do with the bug that was assigned. It's a notification about the *act* of setting that bug's assignee.

Just to drive this home a little more: what if a named operation has no return value, but the process of handling the request results in a notification? What if a DELETE request spawns a notification? You've got no place to set that notification. What if a named operation returns a *list* of entries and also sets some notifications? Your branch is going to give every entry a copy of the same notifications.

These notifications should be set at the _request_ level. I suggest picking an HTTP header name like X-Lazr-Notification and setting a value for that header for each notification. (On the client side, XHR should be able to handle multiple values for the same header, but I'm not sure.)

If you make this a feature of lazr.restful, you should also change lazr.restfulclient to be able to access these notifications. If you make this a feature of the Launchpad web service, you should change launchpadlib. (I'm not sure how to do this since lazr.restfulclient abstracts away the request/response cycle. Maybe notifications should just be stored in an instance-wide queue.)

Revision history for this message
Leonard Richardson (leonardr) :
review: Disapprove
187. By Ian Booth

Merge from trunk

188. By Ian Booth

Add notifications support using adapter

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/NEWS.txt'
2--- src/lazr/restful/NEWS.txt 2011-03-23 16:30:43 +0000
3+++ src/lazr/restful/NEWS.txt 2011-03-24 12:11:52 +0000
4@@ -2,6 +2,15 @@
5 NEWS for lazr.restful
6 =====================
7
8+0.18.1 (2011-03-24)
9+===================
10+
11+The webservice may define an adapter which is used, after an operation on a
12+read-write resource, to provide notifications consisting of (level, message)
13+tuples. Any notifications are inserted into the response header using the
14+'XHR-Notifications' key. They may then be used by the caller to provide extra
15+information to the user about the completed request.
16+
17 0.18.0 (2011-03-23)
18 ===================
19
20
21=== modified file 'src/lazr/restful/_resource.py'
22--- src/lazr/restful/_resource.py 2011-03-15 17:13:48 +0000
23+++ src/lazr/restful/_resource.py 2011-03-24 12:11:52 +0000
24@@ -100,6 +100,7 @@
25 IFieldMarshaller,
26 IHTTPResource,
27 IJSONPublishable,
28+ INotificationsProvider,
29 IReference,
30 IReferenceChoice,
31 IRepresentationCache,
32@@ -942,6 +943,19 @@
33 # status codes using exceptions). Let the publisher
34 # handle it.
35 raise e
36+
37+ # If the webservice has defined an INotificationsProvider adaptor, use
38+ # it to include with the response the relevant notification messages
39+ # and their severity levels.
40+ notifications_provider = INotificationsProvider(self.request, None)
41+ notifications = []
42+ if (notifications_provider is not None
43+ and notifications_provider.notifications):
44+ notifications = ([(notification.level, notification.message)
45+ for notification in notifications_provider.notifications])
46+ json_notifications =simplejson.dumps(notifications)
47+ self.request.response.setHeader(
48+ 'XHR-Notifications', json_notifications)
49 return result
50
51
52
53=== modified file 'src/lazr/restful/example/base/configure.zcml'
54--- src/lazr/restful/example/base/configure.zcml 2011-02-02 14:15:24 +0000
55+++ src/lazr/restful/example/base/configure.zcml 2011-03-24 12:11:52 +0000
56@@ -21,6 +21,13 @@
57 provides="zope.traversing.browser.interfaces.IAbsoluteURL"
58 factory="lazr.restful.testing.webservice.DummyAbsoluteURL" />
59
60+ <!--Registering this adaptor will allow server side notifications to be
61+ sent to the caller.-->
62+ <adapter
63+ for="lazr.restful.interfaces.IWebServiceClientRequest"
64+ provides="lazr.restful.interfaces.INotificationsProvider"
65+ factory="lazr.restful.example.base.traversal.web_service_request_to_notification_request"
66+ />
67
68 <!--Security configuration-->
69
70
71=== modified file 'src/lazr/restful/example/base/traversal.py'
72--- src/lazr/restful/example/base/traversal.py 2011-02-02 14:15:24 +0000
73+++ src/lazr/restful/example/base/traversal.py 2011-03-24 12:11:52 +0000
74@@ -25,6 +25,7 @@
75 from lazr.restful.simple import (
76 RootResourceAbsoluteURL,
77 SimulatedWebsiteRequest,
78+ SimulatedWebsiteRequestWithNotifications,
79 )
80 from lazr.restful import RedirectResource
81
82@@ -36,6 +37,13 @@
83 return SimulatedWebsiteRequest(body, environ)
84
85
86+def web_service_request_to_notification_request(web_service_request):
87+ """Create a simulated website request for providing notifications."""
88+ body = web_service_request.bodyStream.getCacheStream()
89+ environ = dict(web_service_request.environment)
90+ return SimulatedWebsiteRequestWithNotifications(body, environ)
91+
92+
93 class RootAbsoluteURL(RootResourceAbsoluteURL):
94 """A technique for generating the service's root URL.
95
96
97=== modified file 'src/lazr/restful/interfaces/_rest.py'
98--- src/lazr/restful/interfaces/_rest.py 2011-03-15 14:13:10 +0000
99+++ src/lazr/restful/interfaces/_rest.py 2011-03-24 12:11:52 +0000
100@@ -32,6 +32,7 @@
101 'IFieldMarshaller',
102 'IFileLibrarian',
103 'IHTTPResource',
104+ 'INotificationsProvider',
105 'IJSONPublishable',
106 'IJSONRequestCache',
107 'IRepresentationCache',
108@@ -276,6 +277,21 @@
109 """
110
111
112+class INotificationsProvider(Interface):
113+ """A response object which contains notifications.
114+
115+ A web framework may define an adapter for this interface, allowing
116+ the web service to provide an implementation having a 'notifications'
117+ property, providing a list of (level, message) tuples which can be
118+ used by the caller to display extra information about the completed
119+ request. 'level' matches the standard logging levels:
120+ DEBUG = logging.DEBUG # A debugging message
121+ INFO = logging.INFO # simple confirmation of a change
122+ WARNING = logging.WARNING # action will not be successful unless you ...
123+ ERROR = logging.ERROR # the previous action did not succeed, and why
124+ """
125+
126+
127 class IWebServiceClientRequest(IBrowserRequest):
128 """Interface for requests to the web service."""
129 version = Attribute("The version of the web service that the client "
130
131=== modified file 'src/lazr/restful/simple.py'
132--- src/lazr/restful/simple.py 2011-02-02 14:15:24 +0000
133+++ src/lazr/restful/simple.py 2011-03-24 12:11:52 +0000
134@@ -13,9 +13,13 @@
135 'RootResource',
136 'RootResourceAbsoluteURL',
137 'SimulatedWebsiteRequest',
138+ 'SimulatedWebsiteRequestWithNotifications',
139 'TraverseWithGet',
140 ]
141
142+import cgi
143+import collections
144+import logging
145 import traceback
146 import urllib
147
148@@ -37,6 +41,7 @@
149 from lazr.restful import (
150 EntryAdapterUtility, HTTPResource, ServiceRootResource)
151 from lazr.restful.interfaces import (
152+ INotificationsProvider,
153 IRepresentationCache,
154 IServiceRootResource,
155 ITopLevelEntryLink,
156@@ -189,7 +194,31 @@
157 other class that implements IWebBrowserOriginatingRequest), you
158 can take advantage of the web_url feature.
159 """
160- implements(IWebBrowserOriginatingRequest)
161+ implements(IWebBrowserOriginatingRequest, INotificationsProvider)
162+
163+
164+Notification = collections.namedtuple(
165+ 'Notification', ['level', 'message'])
166+
167+
168+class SimulatedWebsiteRequestWithNotifications(SimulatedWebsiteRequest):
169+ """A (simulated) request to a website as opposed to a web service.
170+
171+ If you can adapt a web service request to this class (or some
172+ other class that implements INotificationsProvider), you
173+ can send notifications to the caller.
174+ """
175+ implements(INotificationsProvider)
176+
177+ _notifications = []
178+
179+ @property
180+ def notifications(self):
181+ return self._notifications
182+
183+ def addNotification(self, msg, level=logging.INFO):
184+ self.notifications.append(
185+ Notification(level, cgi.escape(msg)))
186
187
188 class RootResource(ServiceRootResource, TraverseWithGet):
189
190=== modified file 'src/lazr/restful/tests/test_webservice.py'
191--- src/lazr/restful/tests/test_webservice.py 2011-03-07 15:16:57 +0000
192+++ src/lazr/restful/tests/test_webservice.py 2011-03-24 12:11:52 +0000
193@@ -9,6 +9,7 @@
194 from lxml import etree
195 from operator import attrgetter
196 from textwrap import dedent
197+import logging
198 import random
199 import re
200 import simplejson
201@@ -39,6 +40,7 @@
202 ICollection,
203 IEntry,
204 IFieldHTMLRenderer,
205+ INotificationsProvider,
206 IResourceGETOperation,
207 IServiceRootResource,
208 IWebBrowserOriginatingRequest,
209@@ -908,3 +910,55 @@
210 return ResourceOperation(None, request)
211
212
213+class NotificationsProviderTest(EntryTestCase):
214+ """Test that notifcations are included in the response headers."""
215+
216+ testmodule_objects = [HasTwoFields, IHasTwoFields]
217+
218+ class DummyWebsiteRequestWithNotifications:
219+ """A request to the website, as opposed to the web service."""
220+ implements(INotificationsProvider)
221+
222+ @property
223+ def notifications(self):
224+ return [(logging.INFO, "Informational"),
225+ (logging.WARNING, "Warning")
226+ ]
227+
228+ def setUp(self):
229+ super(NotificationsProviderTest, self).setUp()
230+ self.default_media_type = "application/json;include=lp_html"
231+ self._register_url_adapter(IHasTwoFields)
232+ self._register_notification_adapter()
233+
234+ def _register_notification_adapter(self):
235+ """Simulates a service where an entry corresponds to a web page."""
236+
237+ # First, create a converter from web service requests to
238+ # web service requests with notifications.
239+ def web_service_request_to_notification_request(service_request):
240+ """Create a corresponding request to the website."""
241+ return self.DummyWebsiteRequestWithNotifications()
242+
243+ getGlobalSiteManager().registerAdapter(
244+ web_service_request_to_notification_request,
245+ [IWebServiceClientRequest], INotificationsProvider)
246+
247+ @contextmanager
248+ def resource(self, value_1="value 1", value_2="value 2"):
249+ """Simplify the entry_resource call."""
250+ with self.entry_resource(
251+ IHasTwoFields, HasTwoFields,
252+ unicode(value_1), unicode(value_2)) as resource:
253+ yield resource
254+
255+ def test_response_notifications(self):
256+ with self.resource() as resource:
257+ resource.do_GET()
258+ notifications = simplejson.loads(
259+ resource.request.response.getHeader("XHR-Notifications"))
260+ expected_notifications = [
261+ (logging.INFO, "Informational"),
262+ (logging.WARNING, "Warning")
263+ ]
264+ self.assertEquals(notifications, expected_notifications)
265
266=== modified file 'src/lazr/restful/version.txt'
267--- src/lazr/restful/version.txt 2011-03-21 14:00:50 +0000
268+++ src/lazr/restful/version.txt 2011-03-24 12:11:52 +0000
269@@ -1,1 +1,1 @@
270-0.18.0
271+0.18.1

Subscribers

People subscribed via source and target branches