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
=== modified file 'src/lazr/restful/_resource.py'
--- src/lazr/restful/_resource.py 2011-01-26 16:50:13 +0000
+++ src/lazr/restful/_resource.py 2011-02-01 16:44:09 +0000
@@ -54,7 +54,6 @@
54 getMultiAdapter,54 getMultiAdapter,
55 getSiteManager,55 getSiteManager,
56 getUtility,56 getUtility,
57 queryAdapter,
58 queryMultiAdapter,57 queryMultiAdapter,
59 )58 )
60from zope.component.interfaces import ComponentLookupError59from zope.component.interfaces import ComponentLookupError
@@ -1021,10 +1020,14 @@
1021 del changeset['self_link']1020 del changeset['self_link']
10221021
1023 if 'web_link' in changeset:1022 if 'web_link' in changeset:
1024 if changeset['web_link'] != absoluteURL(1023 browser_request = IWebBrowserOriginatingRequest(
1025 self.entry.context, get_current_browser_request()):1024 self.request, None)
1026 errors.append(modified_read_only_attribute % 'web_link')1025 if browser_request is not None:
1027 del changeset['web_link']1026 existing_web_link = absoluteURL(
1027 self.entry.context, browser_request)
1028 if changeset['web_link'] != existing_web_link:
1029 errors.append(modified_read_only_attribute % 'web_link')
1030 del changeset['web_link']
10281031
1029 if 'resource_type_link' in changeset:1032 if 'resource_type_link' in changeset:
1030 if changeset['resource_type_link'] != self.type_url:1033 if changeset['resource_type_link'] != self.type_url:
@@ -1428,12 +1431,10 @@
1428 """1431 """
1429 data = {}1432 data = {}
1430 data['self_link'] = absoluteURL(self.context, self.request)1433 data['self_link'] = absoluteURL(self.context, self.request)
1431 browser_request = queryAdapter(1434 if self.adapter_utility.publish_web_link:
1432 self.request, IWebBrowserOriginatingRequest)1435 # Objects in the web service correspond to pages on some website.
1433 if (browser_request is not None1436 # Provide the link to the corresponding page on the website.
1434 and self.adapter_utility.publish_web_link):1437 browser_request = IWebBrowserOriginatingRequest(self.request)
1435 # Objects in the web server correspond to objects on some website.
1436 # Provide the link to the correspnding object on the website.
1437 data['web_link'] = absoluteURL(self.context, browser_request)1438 data['web_link'] = absoluteURL(self.context, browser_request)
1438 data['resource_type_link'] = self.type_url1439 data['resource_type_link'] = self.type_url
1439 unmarshalled_field_values = {}1440 unmarshalled_field_values = {}
@@ -2169,8 +2170,14 @@
21692170
2170 @property2171 @property
2171 def publish_web_link(self):2172 def publish_web_link(self):
2172 """Should this object type have a web_link published?"""2173 """Return true if this entry should have a web_link."""
2173 return self._get_tagged_value('publish_web_link')2174 # If we can't adapt a web service request to a website
2175 # request, we shouldn't publish a web_link for *any* entry.
2176 web_service_request = get_current_web_service_request()
2177 website_request = IWebBrowserOriginatingRequest(
2178 web_service_request, None)
2179 return website_request is not None and self._get_tagged_value(
2180 'publish_web_link')
21742181
2175 @property2182 @property
2176 def singular_type(self):2183 def singular_type(self):
21772184
=== modified file 'src/lazr/restful/docs/webservice.txt'
--- src/lazr/restful/docs/webservice.txt 2011-01-21 21:12:17 +0000
+++ src/lazr/restful/docs/webservice.txt 2011-02-01 16:44:09 +0000
@@ -555,13 +555,19 @@
555 >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME555 >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME
556 >>> class IAuthorEntry(IAuthor, IEntry):556 >>> class IAuthorEntry(IAuthor, IEntry):
557 ... """The part of an author we expose through the web service."""557 ... """The part of an author we expose through the web service."""
558 ... taggedValue(LAZR_WEBSERVICE_NAME, dict(singular="author",558 ... taggedValue(
559 ... plural="authors"))559 ... LAZR_WEBSERVICE_NAME,
560 ... dict(
561 ... singular="author", plural="authors",
562 ... publish_web_link=True))
560563
561 >>> class ICommentEntry(IComment, IEntry):564 >>> class ICommentEntry(IComment, IEntry):
562 ... """The part of a comment we expose through the web service."""565 ... """The part of a comment we expose through the web service."""
563 ... taggedValue(LAZR_WEBSERVICE_NAME,566 ... taggedValue(
564 ... dict(singular="comment", plural="comments"))567 ... LAZR_WEBSERVICE_NAME,
568 ... dict(
569 ... singular="comment", plural="comments",
570 ... publish_web_link=True))
565571
566Most of the time, it doesn't work to expose to the web service the same data572Most of the time, it doesn't work to expose to the web service the same data
567model we expose internally. Usually there are fields we don't want to expose,573model we expose internally. Usually there are fields we don't want to expose,
@@ -581,8 +587,11 @@
581 >>> class IDishEntry(IEntry):587 >>> class IDishEntry(IEntry):
582 ... "The part of a dish that we expose through the web service."588 ... "The part of a dish that we expose through the web service."
583 ... recipes = CollectionField(value_type=Reference(schema=IRecipe))589 ... recipes = CollectionField(value_type=Reference(schema=IRecipe))
584 ... taggedValue(LAZR_WEBSERVICE_NAME,590 ... taggedValue(
585 ... dict(singular="dish", plural="dishes"))591 ... LAZR_WEBSERVICE_NAME,
592 ... dict(
593 ... singular="dish", plural="dishes",
594 ... publish_web_link=True))
586595
587In the following code block we define an interface that exposes the underlying596In the following code block we define an interface that exposes the underlying
588``Recipe``'s name but not its ID. References to associated objects (like the597``Recipe``'s name but not its ID. References to associated objects (like the
@@ -595,8 +604,11 @@
595 ... dish = Reference(schema=IDish)604 ... dish = Reference(schema=IDish)
596 ... instructions = Text(title=u"Name", required=True)605 ... instructions = Text(title=u"Name", required=True)
597 ... comments = CollectionField(value_type=Reference(schema=IComment))606 ... comments = CollectionField(value_type=Reference(schema=IComment))
598 ... taggedValue(LAZR_WEBSERVICE_NAME,607 ... taggedValue(
599 ... dict(singular="recipe", plural="recipes"))608 ... LAZR_WEBSERVICE_NAME,
609 ... dict(
610 ... singular="recipe", plural="recipes",
611 ... publish_web_link=True))
600612
601 >>> from lazr.restful.fields import ReferenceChoice613 >>> from lazr.restful.fields import ReferenceChoice
602 >>> class ICookbookEntry(IEntry):614 >>> class ICookbookEntry(IEntry):
@@ -607,8 +619,11 @@
607 ... recipes = CollectionField(value_type=Reference(schema=IRecipe))619 ... recipes = CollectionField(value_type=Reference(schema=IRecipe))
608 ... comments = CollectionField(value_type=Reference(schema=IComment))620 ... comments = CollectionField(value_type=Reference(schema=IComment))
609 ... cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.")621 ... cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.")
610 ... taggedValue(LAZR_WEBSERVICE_NAME,622 ... taggedValue(
611 ... dict(singular="cookbook", plural="cookbooks"))623 ... LAZR_WEBSERVICE_NAME,
624 ... dict(
625 ... singular="cookbook", plural="cookbooks",
626 ... publish_web_link=True))
612627
613The ``author`` field is a choice between ``Author`` objects. To make sure628The ``author`` field is a choice between ``Author`` objects. To make sure
614that the ``Author`` objects are properly marshalled to JSON, we need to629that the ``Author`` objects are properly marshalled to JSON, we need to
615630
=== modified file 'src/lazr/restful/publisher.py'
--- src/lazr/restful/publisher.py 2011-01-21 21:12:17 +0000
+++ src/lazr/restful/publisher.py 2011-02-01 16:44:09 +0000
@@ -305,7 +305,7 @@
305 if web_service_version is None:305 if web_service_version is None:
306 web_service_version = config.active_versions[-1]306 web_service_version = config.active_versions[-1]
307307
308 body = website_request.bodyStream.getCacheStream().read()308 body = website_request.bodyStream.getCacheStream()
309 environ = dict(website_request.environment)309 environ = dict(website_request.environment)
310 # Zope picks up on SERVER_URL when setting the _app_server attribute310 # Zope picks up on SERVER_URL when setting the _app_server attribute
311 # of the new request.311 # of the new request.
312312
=== modified file 'src/lazr/restful/tales.py'
--- src/lazr/restful/tales.py 2011-01-21 21:12:17 +0000
+++ src/lazr/restful/tales.py 2011-02-01 16:44:09 +0000
@@ -441,6 +441,10 @@
441 return self.utility.entry_page_representation_id441 return self.utility.entry_page_representation_id
442442
443 @property443 @property
444 def publish_web_link(self):
445 return self.utility.publish_web_link
446
447 @property
444 def all_fields(self):448 def all_fields(self):
445 "Return all schema fields for the object."449 "Return all schema fields for the object."
446 return [field for name, field in450 return [field for name, field in
447451
=== modified file 'src/lazr/restful/templates/wadl-root.pt'
--- src/lazr/restful/templates/wadl-root.pt 2010-08-09 20:05:08 +0000
+++ src/lazr/restful/templates/wadl-root.pt 2011-02-01 16:44:09 +0000
@@ -223,6 +223,13 @@
223 <wadl:doc>The canonical link to this resource.</wadl:doc>223 <wadl:doc>The canonical link to this resource.</wadl:doc>
224 <link tal:attributes="resource_type context/wadl_entry:type_link" />224 <link tal:attributes="resource_type context/wadl_entry:type_link" />
225 </param>225 </param>
226 <param style="plain" name="web_link" path="$['web_link']"
227 tal:condition="context/wadl_entry:publish_web_link">
228 <wadl:doc>
229 The canonical human-addressable web link to this resource.
230 </wadl:doc>
231 <link />
232 </param>
226 <param style="plain" name="resource_type_link"233 <param style="plain" name="resource_type_link"
227 path="$['resource_type_link']">234 path="$['resource_type_link']">
228 <wadl:doc>235 <wadl:doc>
229236
=== modified file 'src/lazr/restful/tests/test_webservice.py'
--- src/lazr/restful/tests/test_webservice.py 2011-01-26 16:50:13 +0000
+++ src/lazr/restful/tests/test_webservice.py 2011-02-01 16:44:09 +0000
@@ -6,10 +6,12 @@
66
7from contextlib import contextmanager7from contextlib import contextmanager
8from cStringIO import StringIO8from cStringIO import StringIO
9from lxml import etree
9from operator import attrgetter10from operator import attrgetter
10from textwrap import dedent11from textwrap import dedent
11import random12import random
12import re13import re
14import simplejson
13import unittest15import unittest
1416
15from zope.component import getGlobalSiteManager, getUtility17from zope.component import getGlobalSiteManager, getUtility
@@ -24,6 +26,8 @@
24 )26 )
25from zope.traversing.browser.interfaces import IAbsoluteURL27from zope.traversing.browser.interfaces import IAbsoluteURL
2628
29from wadllib.application import Application
30
27from lazr.enum import EnumeratedType, Item31from lazr.enum import EnumeratedType, Item
28from lazr.restful import (32from lazr.restful import (
29 EntryField,33 EntryField,
@@ -133,6 +137,8 @@
133class EntryTestCase(WebServiceTestCase):137class EntryTestCase(WebServiceTestCase):
134 """A test suite that defines an entry class."""138 """A test suite that defines an entry class."""
135139
140 WADL_NS = "{http://research.sun.com/wadl/2006/10}"
141
136 class DummyWebsiteRequest:142 class DummyWebsiteRequest:
137 """A request to the website, as opposed to the web service."""143 """A request to the website, as opposed to the web service."""
138 implements(IWebBrowserOriginatingRequest)144 implements(IWebBrowserOriginatingRequest)
@@ -142,20 +148,29 @@
142 URL = 'http://www.website.url/'148 URL = 'http://www.website.url/'
143149
144 @contextmanager150 @contextmanager
151 def request(self):
152 request = getUtility(IWebServiceConfiguration).createRequest(
153 StringIO(""), {})
154 newInteraction(request)
155 yield request
156 endInteraction()
157
158 @property
159 def wadl(self):
160 """Get a parsed WADL description of the web service."""
161 with self.request() as request:
162 return request.publication.application.toWADL().encode('utf-8')
163
164 @contextmanager
145 def entry_resource(self, entry_interface, entry_implementation):165 def entry_resource(self, entry_interface, entry_implementation):
146 """Create a request to an entry resource, and yield the resource."""166 """Create a request to an entry resource, and yield the resource."""
147 entry_class = get_resource_factory(entry_interface, IEntry)167 entry_class = get_resource_factory(entry_interface, IEntry)
148 request = getUtility(IWebServiceConfiguration).createRequest(
149 StringIO(""), {})
150 newInteraction(request)
151
152 data_object = entry_implementation("")168 data_object = entry_implementation("")
153 entry = entry_class(data_object, request)169
154 resource = EntryResource(data_object, request)170 with self.request() as request:
155171 entry = entry_class(data_object, request)
156 yield resource172 resource = EntryResource(data_object, request)
157173 yield resource
158 endInteraction()
159174
160 def _register_url_adapter(self, entry_interface):175 def _register_url_adapter(self, entry_interface):
161 """Register an IAbsoluteURL implementation for an interface."""176 """Register an IAbsoluteURL implementation for an interface."""
@@ -221,6 +236,26 @@
221 self.assertEquals(236 self.assertEquals(
222 representation['web_link'], self.DummyWebsiteURL.URL)237 representation['web_link'], self.DummyWebsiteURL.URL)
223238
239 def test_wadl_includes_web_link_when_available(self):
240 # If an entry includes a web_link, this information will
241 # show up in the WADL description of the entry.
242 service_root = "https://webservice_test/2.0/"
243 self._register_website_url_space(IHasOneField)
244
245 doc = etree.parse(StringIO(self.wadl))
246 # Verify that the 'has_one_field-full' representation includes
247 # a 'web_link' param.
248 representation = [
249 rep for rep in doc.findall('%srepresentation' % self.WADL_NS)
250 if rep.get('id') == 'has_one_field-full'][0]
251 param = [
252 param for param in representation.findall(
253 '%sparam' % self.WADL_NS)
254 if param.get('name') == 'web_link'][0]
255
256 # Verify that the 'web_link' param includes a 'link' tag.
257 self.assertFalse(param.find('%slink' % self.WADL_NS) is None)
258
224 def test_entry_omits_web_link_when_not_available(self):259 def test_entry_omits_web_link_when_not_available(self):
225 # When there is no way of turning a webservice request into a260 # When there is no way of turning a webservice request into a
226 # website request, the 'web_link' attribute is missing from261 # website request, the 'web_link' attribute is missing from
@@ -233,6 +268,13 @@
233 representation['self_link'], DummyAbsoluteURL.URL)268 representation['self_link'], DummyAbsoluteURL.URL)
234 self.assertFalse('web_link' in representation)269 self.assertFalse('web_link' in representation)
235270
271 def test_wadl_omits_web_link_when_not_available(self):
272 # When there is no way of turning a webservice request into a
273 # website request, the 'web_link' attribute is missing from
274 # WADL descriptions of entries.
275 self._register_url_adapter(IHasOneField)
276 self.assertFalse('web_link' in self.wadl)
277
236278
237class IHasNoWebLink(Interface):279class IHasNoWebLink(Interface):
238 """An entry that does not publish a web_link."""280 """An entry that does not publish a web_link."""
@@ -292,6 +334,42 @@
292334
293class TestEntryWrite(EntryTestCase):335class TestEntryWrite(EntryTestCase):
294336
337 testmodule_objects = [IHasOneField, HasOneField]
338
339
340 def test_applyChanges_rejects_nonexistent_web_link(self):
341 # If web_link is not published, applyChanges rejects a request
342 # that references it.
343 with self.entry_resource(IHasOneField, HasOneField) as resource:
344 errors = resource.applyChanges({'web_link': u'some_value'})
345 self.assertEquals(
346 errors,
347 'web_link: You tried to modify a nonexistent attribute.')
348
349 def test_applyChanges_rejects_changed_web_link(self):
350 """applyChanges rejects an attempt to change web_link ."""
351 self._register_website_url_space(IHasOneField)
352
353 with self.entry_resource(IHasOneField, HasOneField) as resource:
354 errors = resource.applyChanges({'web_link': u'some_value'})
355 self.assertEquals(
356 errors,
357 'web_link: You tried to modify a read-only attribute.')
358
359 def test_applyChanges_accepts_unchanged_web_link(self):
360 # applyChanges accepts a reference to web_link, as long as the
361 # value isn't actually being changed.
362 self._register_website_url_space(IHasOneField)
363
364 with self.entry_resource(IHasOneField, HasOneField) as resource:
365 existing_web_link = resource.toDataForJSON()['web_link']
366 representation = simplejson.loads(resource.applyChanges(
367 {'web_link': existing_web_link}))
368 self.assertEquals(representation['web_link'], existing_web_link)
369
370
371class TestEntryWriteForRestrictedField(EntryTestCase):
372
295 testmodule_objects = [IHasRestrictedField, HasRestrictedField]373 testmodule_objects = [IHasRestrictedField, HasRestrictedField]
296374
297 def test_applyChanges_binds_to_resource_context(self):375 def test_applyChanges_binds_to_resource_context(self):

Subscribers

People subscribed via source and target branches