Merge lp:~leonardr/lazr.restful/more-tests-for-combined-representations into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Approved by: Leonard Richardson
Approved revision: 185
Merged at revision: 174
Proposed branch: lp:~leonardr/lazr.restful/more-tests-for-combined-representations
Merge into: lp:lazr.restful
Prerequisite: lp:~leonardr/lazr.restful/include-html-field-representations
Diff against target: 252 lines (+83/-19)
4 files modified
src/lazr/restful/_resource.py (+28/-11)
src/lazr/restful/example/base/tests/field.txt (+5/-2)
src/lazr/restful/tales.py (+4/-2)
src/lazr/restful/tests/test_webservice.py (+46/-4)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/more-tests-for-combined-representations
Reviewer Review Type Date Requested Status
Leonard Richardson (community) Approve
j.c.sackett (community) code* Approve
Curtis Hovey Pending
Review via email: mp+50202@code.launchpad.net

Description of the change

This branch fixes some edge cases and adds some tests that I didn't add to https://code.launchpad.net/~leonardr/lazr.restful/include-html-field-representations/+merge/50174 to keep the code smaller.

1. The ETag for the application/json;include=html representation is the same as the ETag for the application/json representation.

2. If you PUT or PATCH an application/json;include=html representation, lazr.restful will ignore your value for the lp_html attribute. If you PUT the same document as application/json, lazr.restful will complain that you tried to modify a nonexistent attribute.

3. Tested that a request for application/json;include=html sets the parameter in the Content-Type header.

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

This looks good.

review: Approve (code*)
184. By Leonard Richardson

Merge from parent.

185. By Leonard Richardson

When dumping an entry to the web layer, dump a JSON+XHTML representation, not a JSON representation.

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

I'm self-approving this. It's almost entirely new tests.

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-02-17 20:19:31 +0000
3+++ src/lazr/restful/_resource.py 2011-02-17 20:19:31 +0000
4@@ -407,6 +407,11 @@
5 such as the construction of a representation.
6 """
7 # Try to find a cached value.
8+ if media_type == self.JSON_PLUS_XHTML_TYPE:
9+ # The JSON+HTML representation will never change
10+ # unless the JSON representation also changes or the
11+ # software is upgraded. Use the same ETag for both.
12+ media_type = self.JSON_TYPE
13 etag = self.etags_by_media_type.get(media_type)
14 if etag is not None:
15 return etag
16@@ -971,7 +976,7 @@
17 return None, "Entity-body was not a well-formed JSON document."
18 return h, None
19
20- def applyChanges(self, changeset):
21+ def applyChanges(self, changeset, media_type=None):
22 """Apply a dictionary of key-value pairs as changes to an entry.
23
24 :param changeset: A dictionary. Should come from an incoming
25@@ -980,12 +985,13 @@
26 :return: If there was an error, a string error message to be
27 propagated to the client.
28 """
29+ media_type = media_type or self.JSON_TYPE
30 changeset = copy.copy(changeset)
31 validated_changeset = []
32 errors = []
33
34- # The self link and resource type link aren't part of the
35- # schema, so they're handled separately.
36+ # Some fields aren't part of the schema, so they're handled
37+ # separately.
38 modified_read_only_attribute = (u"%s: You tried to modify a "
39 "read-only attribute.")
40 if 'self_link' in changeset:
41@@ -1011,11 +1017,17 @@
42 del changeset['resource_type_link']
43
44 if 'http_etag' in changeset:
45- if changeset['http_etag'] != self.getETag(self.JSON_TYPE):
46+ if changeset['http_etag'] != self.getETag(media_type):
47 errors.append(modified_read_only_attribute %
48 'http_etag')
49 del changeset['http_etag']
50
51+ if 'lp_html' in changeset:
52+ if media_type == self.JSON_PLUS_XHTML_TYPE:
53+ # The HTML portion of the representation is read-only,
54+ # so ignore it.
55+ del changeset['lp_html']
56+
57 # For every field in the schema, see if there's a corresponding
58 # field in the changeset.
59 for name, field in get_entry_fields_in_write_order(self.entry):
60@@ -1140,9 +1152,8 @@
61 # fields that don't correspond to some field in the
62 # schema. They're all errors.
63 for invalid_field in changeset.keys():
64- if not invalid_field.endswith('_html'):
65- errors.append(u"%s: You tried to modify a nonexistent "
66- "attribute." % invalid_field)
67+ errors.append(u"%s: You tried to modify a nonexistent "
68+ "attribute." % invalid_field)
69
70 # If there were errors, display them and send a status of 400.
71 if len(errors) > 0:
72@@ -1244,7 +1255,7 @@
73 if value is None:
74 return value, error
75 changeset = {self.context.name : value}
76- return self.applyChanges(changeset)
77+ return self.applyChanges(changeset, media_type)
78
79 do_PATCH = do_PUT
80
81@@ -1421,7 +1432,8 @@
82 if media_type.startswith(self.JSON_TYPE):
83 repr_name, repr_value, html_renderer = (
84 self._field_with_html_renderer(name, field))
85- if media_type == self.JSON_PLUS_XHTML_TYPE and html_renderer != _default_html_renderer:
86+ if (media_type == self.JSON_PLUS_XHTML_TYPE
87+ and html_renderer != _default_html_renderer):
88 # This field has an unusual HTML renderer.
89 if repr_value == self.REDACTED_VALUE:
90 # The data is redacted, so the HTML
91@@ -1443,6 +1455,11 @@
92
93 if len(html_renderings) > 0:
94 data['lp_html'] = html_renderings
95+ elif media_type == self.JSON_PLUS_XHTML_TYPE:
96+ # The client requested application/json;include=lp_html,
97+ # but there's no HTML to deliver. Set the Content-Type to
98+ # regular application/json.
99+ self.request.response.setHeader('Content-Type', self.JSON_TYPE)
100 etag = self.getETag(media_type, unmarshalled_field_values)
101 data['http_etag'] = etag
102 return data
103@@ -1532,7 +1549,7 @@
104 return ("You didn't specify a value for the attribute '%s'."
105 % repr_name)
106
107- return self.applyChanges(changeset)
108+ return self.applyChanges(changeset, media_type)
109
110
111 def do_PATCH(self, media_type, representation):
112@@ -1540,7 +1557,7 @@
113 changeset, error = self.processAsJSONHash(media_type, representation)
114 if error is not None:
115 return error
116- return self.applyChanges(changeset)
117+ return self.applyChanges(changeset, media_type)
118
119 def _applyChangesPostHook(self):
120 """See `EntryManipulatingResource`."""
121
122=== modified file 'src/lazr/restful/example/base/tests/field.txt'
123--- src/lazr/restful/example/base/tests/field.txt 2011-02-17 20:19:31 +0000
124+++ src/lazr/restful/example/base/tests/field.txt 2011-02-17 20:19:31 +0000
125@@ -333,11 +333,14 @@
126 You can get a combined JSON/HTML representation of an entry by setting
127 the "include=lp_html" parameter on the application/json media type.
128
129- >>> json = webservice.get(
130- ... cookbook_url, 'application/json;include=lp_html').jsonBody()
131+ >>> response = webservice.get(
132+ ... cookbook_url, 'application/json;include=lp_html')
133+ >>> print response.getheader("Content-Type")
134+ application/json;include=lp_html
135
136 The cookbook's description is a normal JSON representation...
137
138+ >>> json = response.jsonBody()
139 >>> print json['description']
140 Description
141
142
143=== modified file 'src/lazr/restful/tales.py'
144--- src/lazr/restful/tales.py 2011-01-25 13:40:34 +0000
145+++ src/lazr/restful/tales.py 2011-02-17 20:19:31 +0000
146@@ -178,9 +178,11 @@
147 if queryMultiAdapter((self.context, request), IEntry):
148 resource = EntryResource(self.context, request)
149 else:
150- # Just dump it as JSON.
151+ # Just dump it as JSON+XHTML
152 resource = self.context
153- return simplejson.dumps(resource, cls=ResourceJSONEncoder)
154+ return simplejson.dumps(
155+ resource, cls=ResourceJSONEncoder,
156+ media_type=EntryResource.JSON_PLUS_XHTML_TYPE)
157
158
159 class WadlResourceAPI(RESTUtilityBase):
160
161=== modified file 'src/lazr/restful/tests/test_webservice.py'
162--- src/lazr/restful/tests/test_webservice.py 2011-02-17 20:19:31 +0000
163+++ src/lazr/restful/tests/test_webservice.py 2011-02-17 20:19:31 +0000
164@@ -399,8 +399,8 @@
165
166 with self.entry_resource(IHasOneField, HasOneField, "") as resource:
167 existing_web_link = resource.toDataForJSON()['web_link']
168- representation = simplejson.loads(resource.applyChanges(
169- {'web_link': existing_web_link}))
170+ representation = simplejson.loads(
171+ resource.applyChanges({'web_link': existing_web_link}))
172 self.assertEquals(representation['web_link'], existing_web_link)
173
174
175@@ -476,12 +476,20 @@
176 IHasTwoFields, ITextLine, simple_renderer, name)
177
178 @contextmanager
179- def resource(self, value_1="value 1", value_2 = "value 2"):
180+ def resource(self, value_1="value 1", value_2="value 2"):
181 """Simplify the entry_resource call."""
182 with self.entry_resource(
183- IHasTwoFields, HasTwoFields, value_1, value_2) as resource:
184+ IHasTwoFields, HasTwoFields,
185+ unicode(value_1), unicode(value_2)) as resource:
186 yield resource
187
188+ def test_web_layer_includes_html_with_json_representation(self):
189+ self.register_html_field_renderer()
190+ with self.resource() as resource:
191+ tales_string = test_tales(
192+ "entry/webservice:json", entry=resource.entry.context)
193+ self.assertTrue("lp_html" in tales_string)
194+
195 def test_normal_json_representation_omits_lp_html(self):
196 self.default_media_type = "application/json"
197 self.register_html_field_renderer()
198@@ -493,6 +501,9 @@
199 with self.resource() as resource:
200 json = simplejson.loads(resource.do_GET())
201 self.assertFalse('lp_html' in json)
202+ self.assertEquals(
203+ resource.request.response.getHeader("Content-Type"),
204+ "application/json")
205
206 def test_field_specific_html_renderer_shows_up_in_lp_html(self):
207 self.register_html_field_renderer("a_field")
208@@ -501,6 +512,9 @@
209 html = json['lp_html']
210 self.assertEquals(
211 html['a_field'], simple_renderer(resource.entry.a_field))
212+ self.assertEquals(
213+ resource.request.response.getHeader("Content-Type"),
214+ resource.JSON_PLUS_XHTML_TYPE)
215
216 def test_html_renderer_for_class_renders_all_fields_of_that_class(self):
217 self.register_html_field_renderer()
218@@ -513,6 +527,34 @@
219 html['another_field'],
220 simple_renderer(resource.entry.another_field))
221
222+ def test_json_plus_html_etag_is_json_etag(self):
223+ self.register_html_field_renderer()
224+ with self.resource() as resource:
225+ etag_1 = resource.getETag(resource.JSON_TYPE)
226+ etag_2 = resource.getETag(resource.JSON_PLUS_XHTML_TYPE)
227+ self.assertEquals(etag_1, etag_2)
228+
229+ def test_acceptchanges_ignores_lp_html_for_json_plus_html_type(self):
230+ # The lp_html portion of the representation is ignored during
231+ # writes.
232+ self.register_html_field_renderer()
233+ json = None
234+ with self.resource() as resource:
235+ json_plus_xhtml = resource.JSON_PLUS_XHTML_TYPE
236+ json = simplejson.loads(unicode(
237+ resource._representation(json_plus_xhtml)))
238+ resource.applyChanges(json, json_plus_xhtml)
239+ self.assertEquals(resource.request.response.getStatus(), 209)
240+
241+ def test_acceptchanges_does_not_ignore_lp_html_for_bare_json_type(self):
242+ self.register_html_field_renderer()
243+ json = None
244+ with self.resource() as resource:
245+ json = simplejson.loads(unicode(
246+ resource._representation(resource.JSON_PLUS_XHTML_TYPE)))
247+ resource.applyChanges(json, resource.JSON_TYPE)
248+ self.assertEquals(resource.request.response.getStatus(), 400)
249+
250
251 class UnicodeChoice(EnumeratedType):
252 """A choice between an ASCII value and a Unicode value."""

Subscribers

People subscribed via source and target branches