Merge lp:~leonardr/lazr.restful/web-link into lp:lazr.restful

Proposed by Tim Penhey
Status: Merged
Approved by: Tim Penhey
Approved revision: 142
Merged at revision: 165
Proposed branch: lp:~leonardr/lazr.restful/web-link
Merge into: lp:lazr.restful
Diff against target: 322 lines (+155/-30)
6 files modified
src/lazr/restful/NEWS.txt (+8/-0)
src/lazr/restful/_resource.py (+23/-3)
src/lazr/restful/example/wsgi/tests/introduction.txt (+2/-1)
src/lazr/restful/interfaces/_rest.py (+10/-0)
src/lazr/restful/tests/test_webservice.py (+111/-25)
src/lazr/restful/version.txt (+1/-1)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/web-link
Reviewer Review Type Date Requested Status
Tim Penhey (community) Approve
Steve Kowalik (community) code* Approve
Review via email: mp+46992@code.launchpad.net

Description of the change

Often each entry published by a web service corresponds to an entry on some website. This is the case with the Launchpad web service. It's useful for a web service consumer to know the link to the corresponding object on a website. This branch makes it possible to get the website URL of an object, assuming there's a way to convert a web service request into a website request.

When writing tests I refactored some common code that creates a web service request to an entry resource, so that the test can make assertions about the entry resource.

To post a comment you must log in.
lp:~leonardr/lazr.restful/web-link updated
139. By Leonard Richardson

Added IWebBrowserOriginatingRequest.

140. By Leonard Richardson

Added tests.

141. By Leonard Richardson

Removed web_link references from web services that don't have a web_link.

142. By Leonard Richardson

Merge from trunk, update NEWS.

Revision history for this message
Steve Kowalik (stevenk) wrote :

This looks like great work, thanks Leonard.

My only comment is the import on line 68 of the diff, why is that inline?

review: Approve (code*)
Revision history for this message
Tim Penhey (thumper) :
review: Approve
lp:~leonardr/lazr.restful/web-link updated
143. By Leonard Richardson

Replaced IObject with IReference, allowing us to get rid of obsolete code.

144. By Leonard Richardson

Stop coddling people who use IObject, part 2

145. By Leonard Richardson

Removed code that was moved into another branch.

146. By Leonard Richardson

Merge with parent.

147. By Leonard Richardson

Merge with trunk.

148. By Leonard Richardson

Fixed imports.

149. By Leonard Richardson

Updated NEWS.

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-01-20 20:25:31 +0000
3+++ src/lazr/restful/NEWS.txt 2011-01-21 18:26:23 +0000
4@@ -2,6 +2,14 @@
5 NEWS for lazr.restful
6 =====================
7
8+0.16.0 (2010-01-21)
9+===================
10+
11+If each entry in the web service corresponds to some object on a
12+website, and there's a way of converting a web service request into a
13+website request, the web service will now provide website links for
14+each entry.
15+
16 0.15.2 (2010-01-20)
17 ===================
18
19
20=== modified file 'src/lazr/restful/_resource.py'
21--- src/lazr/restful/_resource.py 2011-01-19 19:00:10 +0000
22+++ src/lazr/restful/_resource.py 2011-01-21 18:26:23 +0000
23@@ -47,9 +47,16 @@
24 from zope.app.pagetemplate.engine import TrustedAppPT
25 from zope import component
26 from zope.component import (
27- adapts, getAdapters, getAllUtilitiesRegisteredFor,
28- getGlobalSiteManager, getMultiAdapter, getSiteManager, getUtility,
29- queryMultiAdapter)
30+ adapts,
31+ getAdapters,
32+ getAllUtilitiesRegisteredFor,
33+ getGlobalSiteManager,
34+ getMultiAdapter,
35+ getSiteManager,
36+ getUtility,
37+ queryAdapter,
38+ queryMultiAdapter,
39+ )
40 from zope.component.interfaces import ComponentLookupError
41 from zope.event import notify
42 from zope.publisher.http import init_status_codes, status_reasons
43@@ -90,6 +97,7 @@
44 LAZR_WEBSERVICE_NAME)
45 from lazr.restful.utils import (
46 extract_write_portion,
47+ get_current_browser_request,
48 get_current_web_service_request,
49 sorted_named_things,
50 )
51@@ -989,6 +997,12 @@
52 errors.append(modified_read_only_attribute % 'self_link')
53 del changeset['self_link']
54
55+ if 'web_link' in changeset:
56+ if changeset['web_link'] != absoluteURL(
57+ self.entry.context, get_current_browser_request()):
58+ errors.append(modified_read_only_attribute % 'web_link')
59+ del changeset['web_link']
60+
61 if 'resource_type_link' in changeset:
62 if changeset['resource_type_link'] != self.type_url:
63 errors.append(modified_read_only_attribute %
64@@ -1400,6 +1414,12 @@
65 """
66 data = {}
67 data['self_link'] = absoluteURL(self.context, self.request)
68+ from lazr.restful.interfaces import IWebBrowserOriginatingRequest
69+ browser_request = queryAdapter(self.request, IWebBrowserOriginatingRequest)
70+ if browser_request is not None:
71+ # Objects in the web server correspond to objects on some website.
72+ # Provide the link to the correspnding object on the website.
73+ data['web_link'] = absoluteURL(self.context, browser_request)
74 data['resource_type_link'] = self.type_url
75 unmarshalled_field_values = {}
76 for name, field in getFieldsInOrder(self.entry.schema):
77
78=== modified file 'src/lazr/restful/example/wsgi/tests/introduction.txt'
79--- src/lazr/restful/example/wsgi/tests/introduction.txt 2009-07-07 17:09:31 +0000
80+++ src/lazr/restful/example/wsgi/tests/introduction.txt 2011-01-21 18:26:23 +0000
81@@ -27,7 +27,8 @@
82
83 >>> entry_resource = webservice.get("/pairs/1").jsonBody()
84 >>> sorted(entry_resource.keys())
85- [u'http_etag', u'key', u'resource_type_link', u'self_link', u'value']
86+ [u'http_etag', u'key', u'resource_type_link', u'self_link',
87+ u'value']
88
89 You can get a field resource.
90
91
92=== modified file 'src/lazr/restful/interfaces/_rest.py'
93--- src/lazr/restful/interfaces/_rest.py 2010-09-06 19:25:17 +0000
94+++ src/lazr/restful/interfaces/_rest.py 2011-01-21 18:26:23 +0000
95@@ -50,6 +50,7 @@
96 'IWebBrowserInitiatedRequest',
97 'LAZR_WEBSERVICE_NAME',
98 'LAZR_WEBSERVICE_NS',
99+ 'IWebBrowserOriginatingRequest',
100 'IWebServiceClientRequest',
101 'IWebServiceLayer',
102 'IWebServiceVersion',
103@@ -270,6 +271,15 @@
104 "other end of the link.")
105
106
107+class IWebBrowserOriginatingRequest(Interface):
108+ """A browser request to an object also published on the web service.
109+
110+ A web framework may define an adapter for this interface, allowing
111+ the web service to include, as part of an entry's representation,
112+ a link to that same entry as found on the website.
113+ """
114+
115+
116 class IWebServiceClientRequest(IBrowserRequest):
117 """Interface for requests to the web service."""
118 version = Attribute("The version of the web service that the client "
119
120=== modified file 'src/lazr/restful/tests/test_webservice.py'
121--- src/lazr/restful/tests/test_webservice.py 2010-10-25 16:31:09 +0000
122+++ src/lazr/restful/tests/test_webservice.py 2011-01-21 18:26:23 +0000
123@@ -4,6 +4,7 @@
124
125 __metaclass__ = type
126
127+from contextlib import contextmanager
128 from cStringIO import StringIO
129 from operator import attrgetter
130 from textwrap import dedent
131@@ -16,15 +17,26 @@
132 from zope.interface.interface import InterfaceClass
133 from zope.publisher.browser import TestRequest
134 from zope.schema import Choice, Date, Datetime, TextLine
135-from zope.security.management import newInteraction, queryInteraction
136+from zope.security.management import (
137+ endInteraction,
138+ newInteraction,
139+ queryInteraction,
140+ )
141 from zope.traversing.browser.interfaces import IAbsoluteURL
142
143 from lazr.enum import EnumeratedType, Item
144 from lazr.restful import ResourceOperation
145 from lazr.restful.fields import Reference
146 from lazr.restful.interfaces import (
147- ICollection, IEntry, IResourceGETOperation, IServiceRootResource,
148- IWebServiceConfiguration, IWebServiceClientRequest, IWebServiceVersion)
149+ ICollection,
150+ IEntry,
151+ IResourceGETOperation,
152+ IServiceRootResource,
153+ IWebBrowserOriginatingRequest,
154+ IWebServiceConfiguration,
155+ IWebServiceClientRequest,
156+ IWebServiceVersion,
157+ )
158 from lazr.restful import EntryResource, ResourceGETOperation
159 from lazr.restful.declarations import (
160 exported, export_as_webservice_entry, LAZR_WEBSERVICE_NAME, annotate_exported_methods)
161@@ -127,17 +139,19 @@
162
163 class DummyAbsoluteURL:
164 """Implements IAbsoluteURL for when you don't care what the URL is."""
165+ URL = 'http://dummyurl/'
166+
167 def __init__(self, *args):
168 pass
169
170 def __str__(self):
171- return 'http://dummyurl/'
172+ return self.URL
173
174 __call__ = __str__
175
176
177 class EntryTestCase(WebServiceTestCase):
178- """A test suite for entries."""
179+ """A test suite that defines an entry class."""
180
181 testmodule_objects = [HasRestrictedField, IHasRestrictedField]
182
183@@ -147,6 +161,81 @@
184 DummyAbsoluteURL, [IHasRestrictedField, IWebServiceClientRequest],
185 IAbsoluteURL)
186
187+ @contextmanager
188+ def entry_resource(self):
189+ """Create a request to an entry resource, and yield the resource."""
190+ entry_class = get_resource_factory(IHasRestrictedField, IEntry)
191+ request = getUtility(IWebServiceConfiguration).createRequest(
192+ StringIO(""), {})
193+ newInteraction(request)
194+
195+ data_object = HasRestrictedField("")
196+ entry = entry_class(data_object, request)
197+ resource = EntryResource(data_object, request)
198+
199+ yield resource
200+
201+ endInteraction()
202+
203+
204+class TestEntryRead(EntryTestCase):
205+
206+ def test_entry_includes_web_link_when_available(self):
207+ # If a web service request can be adapted to a web*site* request,
208+ # the representation of an entry will include a link to the
209+ # corresponding entry on the website.
210+ #
211+ # This is useful when each entry published by the web service
212+ # has a human-readable page on some corresponding website. The
213+ # web service can publish links to the website for use by Ajax
214+ # clients or for other human-interaction purposes.
215+
216+ # Let's suppose we can use a helper function to convert a web
217+ # service request to a website request.
218+ class DummyWebsiteRequest:
219+ """A request to the website, as opposed to the web service."""
220+ implements(IWebBrowserOriginatingRequest)
221+
222+ def web_service_request_to_website_request(service_request):
223+ """Create a corresponding request to the website."""
224+ return DummyWebsiteRequest()
225+
226+ getGlobalSiteManager().registerAdapter(
227+ web_service_request_to_website_request,
228+ [IWebServiceClientRequest], IWebBrowserOriginatingRequest)
229+
230+ # Let's also suppose that an IHasRestrictedField has a
231+ # different URL on the website than on the web service.
232+ class DummyWebsiteURL(DummyAbsoluteURL):
233+ """A web-centric implementation of the dummy URL."""
234+ URL = 'http://www.website.url/'
235+
236+ getGlobalSiteManager().registerAdapter(
237+ DummyWebsiteURL,
238+ [IHasRestrictedField, IWebBrowserOriginatingRequest],
239+ IAbsoluteURL)
240+
241+ # Now a representation of IHasRestrictedField includes a
242+ # 'web_link'.
243+ with self.entry_resource() as resource:
244+ representation = resource.toDataForJSON()
245+ self.assertEquals(representation['self_link'], DummyAbsoluteURL.URL)
246+ self.assertEquals(representation['web_link'], DummyWebsiteURL.URL)
247+
248+ def test_entry_omits_web_link_when_not_available(self):
249+ # When there is no way of turning a webservice request into a
250+ # website request, the 'web_link' attribute is missing from
251+ # entry representations.
252+
253+ with self.entry_resource() as resource:
254+ representation = resource.toDataForJSON()
255+ self.assertEquals(
256+ representation['self_link'], DummyAbsoluteURL.URL)
257+ self.assertFalse('web_link' in representation)
258+
259+
260+class TestEntryWrite(EntryTestCase):
261+
262 def test_applyChanges_binds_to_resource_context(self):
263 """Make sure applyChanges binds fields to the resource context.
264
265@@ -155,26 +244,21 @@
266 InterfaceRestrictedField is bound to an object that doesn't
267 expose the right interface, it will raise an exception.
268 """
269- entry_class = get_resource_factory(IHasRestrictedField, IEntry)
270- request = getUtility(IWebServiceConfiguration).createRequest(
271- StringIO(""), {})
272-
273- data_object = HasRestrictedField("")
274- entry = entry_class(data_object, request)
275- resource = EntryResource(data_object, request)
276- entry.schema['a_field'].restrict_to_interface = IHasRestrictedField
277- self.assertEquals(resource.entry.a_field, '')
278- resource.applyChanges({'a_field': u'a_value'})
279- self.assertEquals(resource.entry.a_field, 'a_value')
280-
281- # Make sure that IHasRestrictedField itself works correctly.
282- class IOtherInterface(Interface):
283- """An interface not provided by IHasRestrictedField."""
284- pass
285- entry.schema['a_field'].restrict_to_interface = IOtherInterface
286- self.assertRaises(AssertionError, resource.applyChanges,
287- {'a_field': u'a_new_value'})
288- self.assertEquals(resource.entry.a_field, 'a_value')
289+ with self.entry_resource() as resource:
290+ entry = resource.entry
291+ entry.schema['a_field'].restrict_to_interface = IHasRestrictedField
292+ self.assertEquals(entry.a_field, '')
293+ resource.applyChanges({'a_field': u'a_value'})
294+ self.assertEquals(entry.a_field, 'a_value')
295+
296+ # Make sure that IHasRestrictedField itself works correctly.
297+ class IOtherInterface(Interface):
298+ """An interface not provided by IHasRestrictedField."""
299+ pass
300+ entry.schema['a_field'].restrict_to_interface = IOtherInterface
301+ self.assertRaises(AssertionError, resource.applyChanges,
302+ {'a_field': u'a_new_value'})
303+ self.assertEquals(resource.entry.a_field, 'a_value')
304
305
306 class UnicodeChoice(EnumeratedType):
307@@ -459,6 +543,8 @@
308 self.assertEquals("2.0", webservice_request.version)
309 self.assertTrue(marker_20.providedBy(webservice_request))
310
311+ endInteraction()
312+
313
314 def additional_tests():
315 return unittest.TestLoader().loadTestsFromName(__name__)
316
317=== modified file 'src/lazr/restful/version.txt'
318--- src/lazr/restful/version.txt 2011-01-20 20:25:31 +0000
319+++ src/lazr/restful/version.txt 2011-01-21 18:26:23 +0000
320@@ -1,1 +1,1 @@
321-0.15.2
322+0.16.0

Subscribers

People subscribed via source and target branches