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

Proposed by Leonard Richardson
Status: Merged
Approved by: Tim Penhey
Approved revision: 164
Merged at revision: 169
Proposed branch: lp:~leonardr/lazr.restful/web-link
Merge into: lp:lazr.restful
Diff against target: 339 lines (+145/-34)
6 files modified
src/lazr/restful/_resource.py (+20/-13)
src/lazr/restful/docs/webservice.txt (+25/-10)
src/lazr/restful/publisher.py (+1/-1)
src/lazr/restful/tales.py (+4/-0)
src/lazr/restful/templates/wadl-root.pt (+7/-0)
src/lazr/restful/tests/test_webservice.py (+88/-10)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/web-link
Reviewer Review Type Date Requested Status
Tim Penhey (community) Approve
Review via email: mp+48052@code.launchpad.net

Description of the change

This branch fixes numerous errors and oversights in my initial implementation of the web_link parameter, which caused test failures and other problems as I integrated web_link into Launchpad.

1. If you send a PUT or PATCH request that mentions web_link, you'll get an error if and only if you tried to _change_ the web_link.

2. I created a property 'publish_web_link' in the entry adapter utility which is used consistently when deciding whether or not a given entry type supports web_link.

3. I fixed a bug in browser_request_to_web_service_request which was taking the incoming body stream, reading it, and using the resulting string as the 'body' of the new request. Now we just pass the stream right along into the new request.

4. I added a 'web_link' <param> tag to the WADL definition of every resource type that publishes web_link. This will tell lazr.restfulclient about the web links, so they can actually be used.

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

> if self.adapter_utility.publish_web_link:
> # Objects in the web service correspond to pages on some website.
> # Provide the link to the corresponding page on the website.
> browser_request = IWebBrowserOriginatingRequest(self.request)

Can we be sure here that there is an adapter for request to a
IWebBrowserOriginatingRequest?

I'm pretty sure that you can rewrite:
  queryAdapter(web_service_request, IWebBrowserOriginatingRequest)
as
  IWebBrowserOriginatingRequest(web_service_request, None)

Not sure if you think this is better or not.

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

If there is no IWebBrowserOriginatingRequest adapter, then publish_web_link will always return false, so we can be sure.

IWebBrowserOriginatingRequest(web_service_request, None) is nicer. I'll change it.

lp:~leonardr/lazr.restful/web-link updated
164. By Leonard Richardson

Improved the adaptation from a web service request to a website request.

Revision history for this message
Tim Penhey (thumper) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/_resource.py'
2--- src/lazr/restful/_resource.py 2011-01-26 16:50:13 +0000
3+++ src/lazr/restful/_resource.py 2011-02-01 16:44:09 +0000
4@@ -54,7 +54,6 @@
5 getMultiAdapter,
6 getSiteManager,
7 getUtility,
8- queryAdapter,
9 queryMultiAdapter,
10 )
11 from zope.component.interfaces import ComponentLookupError
12@@ -1021,10 +1020,14 @@
13 del changeset['self_link']
14
15 if 'web_link' in changeset:
16- if changeset['web_link'] != absoluteURL(
17- self.entry.context, get_current_browser_request()):
18- errors.append(modified_read_only_attribute % 'web_link')
19- del changeset['web_link']
20+ browser_request = IWebBrowserOriginatingRequest(
21+ self.request, None)
22+ if browser_request is not None:
23+ existing_web_link = absoluteURL(
24+ self.entry.context, browser_request)
25+ if changeset['web_link'] != existing_web_link:
26+ errors.append(modified_read_only_attribute % 'web_link')
27+ del changeset['web_link']
28
29 if 'resource_type_link' in changeset:
30 if changeset['resource_type_link'] != self.type_url:
31@@ -1428,12 +1431,10 @@
32 """
33 data = {}
34 data['self_link'] = absoluteURL(self.context, self.request)
35- browser_request = queryAdapter(
36- self.request, IWebBrowserOriginatingRequest)
37- if (browser_request is not None
38- and self.adapter_utility.publish_web_link):
39- # Objects in the web server correspond to objects on some website.
40- # Provide the link to the correspnding object on the website.
41+ if self.adapter_utility.publish_web_link:
42+ # Objects in the web service correspond to pages on some website.
43+ # Provide the link to the corresponding page on the website.
44+ browser_request = IWebBrowserOriginatingRequest(self.request)
45 data['web_link'] = absoluteURL(self.context, browser_request)
46 data['resource_type_link'] = self.type_url
47 unmarshalled_field_values = {}
48@@ -2169,8 +2170,14 @@
49
50 @property
51 def publish_web_link(self):
52- """Should this object type have a web_link published?"""
53- return self._get_tagged_value('publish_web_link')
54+ """Return true if this entry should have a web_link."""
55+ # If we can't adapt a web service request to a website
56+ # request, we shouldn't publish a web_link for *any* entry.
57+ web_service_request = get_current_web_service_request()
58+ website_request = IWebBrowserOriginatingRequest(
59+ web_service_request, None)
60+ return website_request is not None and self._get_tagged_value(
61+ 'publish_web_link')
62
63 @property
64 def singular_type(self):
65
66=== modified file 'src/lazr/restful/docs/webservice.txt'
67--- src/lazr/restful/docs/webservice.txt 2011-01-21 21:12:17 +0000
68+++ src/lazr/restful/docs/webservice.txt 2011-02-01 16:44:09 +0000
69@@ -555,13 +555,19 @@
70 >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME
71 >>> class IAuthorEntry(IAuthor, IEntry):
72 ... """The part of an author we expose through the web service."""
73- ... taggedValue(LAZR_WEBSERVICE_NAME, dict(singular="author",
74- ... plural="authors"))
75+ ... taggedValue(
76+ ... LAZR_WEBSERVICE_NAME,
77+ ... dict(
78+ ... singular="author", plural="authors",
79+ ... publish_web_link=True))
80
81 >>> class ICommentEntry(IComment, IEntry):
82 ... """The part of a comment we expose through the web service."""
83- ... taggedValue(LAZR_WEBSERVICE_NAME,
84- ... dict(singular="comment", plural="comments"))
85+ ... taggedValue(
86+ ... LAZR_WEBSERVICE_NAME,
87+ ... dict(
88+ ... singular="comment", plural="comments",
89+ ... publish_web_link=True))
90
91 Most of the time, it doesn't work to expose to the web service the same data
92 model we expose internally. Usually there are fields we don't want to expose,
93@@ -581,8 +587,11 @@
94 >>> class IDishEntry(IEntry):
95 ... "The part of a dish that we expose through the web service."
96 ... recipes = CollectionField(value_type=Reference(schema=IRecipe))
97- ... taggedValue(LAZR_WEBSERVICE_NAME,
98- ... dict(singular="dish", plural="dishes"))
99+ ... taggedValue(
100+ ... LAZR_WEBSERVICE_NAME,
101+ ... dict(
102+ ... singular="dish", plural="dishes",
103+ ... publish_web_link=True))
104
105 In the following code block we define an interface that exposes the underlying
106 ``Recipe``'s name but not its ID. References to associated objects (like the
107@@ -595,8 +604,11 @@
108 ... dish = Reference(schema=IDish)
109 ... instructions = Text(title=u"Name", required=True)
110 ... comments = CollectionField(value_type=Reference(schema=IComment))
111- ... taggedValue(LAZR_WEBSERVICE_NAME,
112- ... dict(singular="recipe", plural="recipes"))
113+ ... taggedValue(
114+ ... LAZR_WEBSERVICE_NAME,
115+ ... dict(
116+ ... singular="recipe", plural="recipes",
117+ ... publish_web_link=True))
118
119 >>> from lazr.restful.fields import ReferenceChoice
120 >>> class ICookbookEntry(IEntry):
121@@ -607,8 +619,11 @@
122 ... recipes = CollectionField(value_type=Reference(schema=IRecipe))
123 ... comments = CollectionField(value_type=Reference(schema=IComment))
124 ... cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.")
125- ... taggedValue(LAZR_WEBSERVICE_NAME,
126- ... dict(singular="cookbook", plural="cookbooks"))
127+ ... taggedValue(
128+ ... LAZR_WEBSERVICE_NAME,
129+ ... dict(
130+ ... singular="cookbook", plural="cookbooks",
131+ ... publish_web_link=True))
132
133 The ``author`` field is a choice between ``Author`` objects. To make sure
134 that the ``Author`` objects are properly marshalled to JSON, we need to
135
136=== modified file 'src/lazr/restful/publisher.py'
137--- src/lazr/restful/publisher.py 2011-01-21 21:12:17 +0000
138+++ src/lazr/restful/publisher.py 2011-02-01 16:44:09 +0000
139@@ -305,7 +305,7 @@
140 if web_service_version is None:
141 web_service_version = config.active_versions[-1]
142
143- body = website_request.bodyStream.getCacheStream().read()
144+ body = website_request.bodyStream.getCacheStream()
145 environ = dict(website_request.environment)
146 # Zope picks up on SERVER_URL when setting the _app_server attribute
147 # of the new request.
148
149=== modified file 'src/lazr/restful/tales.py'
150--- src/lazr/restful/tales.py 2011-01-21 21:12:17 +0000
151+++ src/lazr/restful/tales.py 2011-02-01 16:44:09 +0000
152@@ -441,6 +441,10 @@
153 return self.utility.entry_page_representation_id
154
155 @property
156+ def publish_web_link(self):
157+ return self.utility.publish_web_link
158+
159+ @property
160 def all_fields(self):
161 "Return all schema fields for the object."
162 return [field for name, field in
163
164=== modified file 'src/lazr/restful/templates/wadl-root.pt'
165--- src/lazr/restful/templates/wadl-root.pt 2010-08-09 20:05:08 +0000
166+++ src/lazr/restful/templates/wadl-root.pt 2011-02-01 16:44:09 +0000
167@@ -223,6 +223,13 @@
168 <wadl:doc>The canonical link to this resource.</wadl:doc>
169 <link tal:attributes="resource_type context/wadl_entry:type_link" />
170 </param>
171+ <param style="plain" name="web_link" path="$['web_link']"
172+ tal:condition="context/wadl_entry:publish_web_link">
173+ <wadl:doc>
174+ The canonical human-addressable web link to this resource.
175+ </wadl:doc>
176+ <link />
177+ </param>
178 <param style="plain" name="resource_type_link"
179 path="$['resource_type_link']">
180 <wadl:doc>
181
182=== modified file 'src/lazr/restful/tests/test_webservice.py'
183--- src/lazr/restful/tests/test_webservice.py 2011-01-26 16:50:13 +0000
184+++ src/lazr/restful/tests/test_webservice.py 2011-02-01 16:44:09 +0000
185@@ -6,10 +6,12 @@
186
187 from contextlib import contextmanager
188 from cStringIO import StringIO
189+from lxml import etree
190 from operator import attrgetter
191 from textwrap import dedent
192 import random
193 import re
194+import simplejson
195 import unittest
196
197 from zope.component import getGlobalSiteManager, getUtility
198@@ -24,6 +26,8 @@
199 )
200 from zope.traversing.browser.interfaces import IAbsoluteURL
201
202+from wadllib.application import Application
203+
204 from lazr.enum import EnumeratedType, Item
205 from lazr.restful import (
206 EntryField,
207@@ -133,6 +137,8 @@
208 class EntryTestCase(WebServiceTestCase):
209 """A test suite that defines an entry class."""
210
211+ WADL_NS = "{http://research.sun.com/wadl/2006/10}"
212+
213 class DummyWebsiteRequest:
214 """A request to the website, as opposed to the web service."""
215 implements(IWebBrowserOriginatingRequest)
216@@ -142,20 +148,29 @@
217 URL = 'http://www.website.url/'
218
219 @contextmanager
220+ def request(self):
221+ request = getUtility(IWebServiceConfiguration).createRequest(
222+ StringIO(""), {})
223+ newInteraction(request)
224+ yield request
225+ endInteraction()
226+
227+ @property
228+ def wadl(self):
229+ """Get a parsed WADL description of the web service."""
230+ with self.request() as request:
231+ return request.publication.application.toWADL().encode('utf-8')
232+
233+ @contextmanager
234 def entry_resource(self, entry_interface, entry_implementation):
235 """Create a request to an entry resource, and yield the resource."""
236 entry_class = get_resource_factory(entry_interface, IEntry)
237- request = getUtility(IWebServiceConfiguration).createRequest(
238- StringIO(""), {})
239- newInteraction(request)
240-
241 data_object = entry_implementation("")
242- entry = entry_class(data_object, request)
243- resource = EntryResource(data_object, request)
244-
245- yield resource
246-
247- endInteraction()
248+
249+ with self.request() as request:
250+ entry = entry_class(data_object, request)
251+ resource = EntryResource(data_object, request)
252+ yield resource
253
254 def _register_url_adapter(self, entry_interface):
255 """Register an IAbsoluteURL implementation for an interface."""
256@@ -221,6 +236,26 @@
257 self.assertEquals(
258 representation['web_link'], self.DummyWebsiteURL.URL)
259
260+ def test_wadl_includes_web_link_when_available(self):
261+ # If an entry includes a web_link, this information will
262+ # show up in the WADL description of the entry.
263+ service_root = "https://webservice_test/2.0/"
264+ self._register_website_url_space(IHasOneField)
265+
266+ doc = etree.parse(StringIO(self.wadl))
267+ # Verify that the 'has_one_field-full' representation includes
268+ # a 'web_link' param.
269+ representation = [
270+ rep for rep in doc.findall('%srepresentation' % self.WADL_NS)
271+ if rep.get('id') == 'has_one_field-full'][0]
272+ param = [
273+ param for param in representation.findall(
274+ '%sparam' % self.WADL_NS)
275+ if param.get('name') == 'web_link'][0]
276+
277+ # Verify that the 'web_link' param includes a 'link' tag.
278+ self.assertFalse(param.find('%slink' % self.WADL_NS) is None)
279+
280 def test_entry_omits_web_link_when_not_available(self):
281 # When there is no way of turning a webservice request into a
282 # website request, the 'web_link' attribute is missing from
283@@ -233,6 +268,13 @@
284 representation['self_link'], DummyAbsoluteURL.URL)
285 self.assertFalse('web_link' in representation)
286
287+ def test_wadl_omits_web_link_when_not_available(self):
288+ # When there is no way of turning a webservice request into a
289+ # website request, the 'web_link' attribute is missing from
290+ # WADL descriptions of entries.
291+ self._register_url_adapter(IHasOneField)
292+ self.assertFalse('web_link' in self.wadl)
293+
294
295 class IHasNoWebLink(Interface):
296 """An entry that does not publish a web_link."""
297@@ -292,6 +334,42 @@
298
299 class TestEntryWrite(EntryTestCase):
300
301+ testmodule_objects = [IHasOneField, HasOneField]
302+
303+
304+ def test_applyChanges_rejects_nonexistent_web_link(self):
305+ # If web_link is not published, applyChanges rejects a request
306+ # that references it.
307+ with self.entry_resource(IHasOneField, HasOneField) as resource:
308+ errors = resource.applyChanges({'web_link': u'some_value'})
309+ self.assertEquals(
310+ errors,
311+ 'web_link: You tried to modify a nonexistent attribute.')
312+
313+ def test_applyChanges_rejects_changed_web_link(self):
314+ """applyChanges rejects an attempt to change web_link ."""
315+ self._register_website_url_space(IHasOneField)
316+
317+ with self.entry_resource(IHasOneField, HasOneField) as resource:
318+ errors = resource.applyChanges({'web_link': u'some_value'})
319+ self.assertEquals(
320+ errors,
321+ 'web_link: You tried to modify a read-only attribute.')
322+
323+ def test_applyChanges_accepts_unchanged_web_link(self):
324+ # applyChanges accepts a reference to web_link, as long as the
325+ # value isn't actually being changed.
326+ self._register_website_url_space(IHasOneField)
327+
328+ with self.entry_resource(IHasOneField, HasOneField) as resource:
329+ existing_web_link = resource.toDataForJSON()['web_link']
330+ representation = simplejson.loads(resource.applyChanges(
331+ {'web_link': existing_web_link}))
332+ self.assertEquals(representation['web_link'], existing_web_link)
333+
334+
335+class TestEntryWriteForRestrictedField(EntryTestCase):
336+
337 testmodule_objects = [IHasRestrictedField, HasRestrictedField]
338
339 def test_applyChanges_binds_to_resource_context(self):

Subscribers

People subscribed via source and target branches