Merge lp:~cjwatson/lazr.restful/range-factory into lp:lazr.restful
- range-factory
- Merge into trunk
Proposed by
Colin Watson
Status: | Needs review |
---|---|
Proposed branch: | lp:~cjwatson/lazr.restful/range-factory |
Merge into: | lp:lazr.restful |
Diff against target: |
415 lines (+146/-16) 9 files modified
setup.py (+1/-1) src/lazr/restful/_operation.py (+10/-1) src/lazr/restful/_resource.py (+16/-3) src/lazr/restful/declarations.py (+11/-5) src/lazr/restful/docs/webservice-declarations.txt (+47/-2) src/lazr/restful/docs/webservice.txt (+51/-3) src/lazr/restful/fields.py (+2/-1) src/lazr/restful/interfaces/_fields.py (+5/-0) src/lazr/restful/interfaces/_rest.py (+3/-0) |
To merge this branch: | bzr merge lp:~cjwatson/lazr.restful/range-factory |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
LAZR Developers | Pending | ||
Review via email: mp+355966@code.launchpad.net |
Commit message
Allow declaring a custom range factory for use with returned collections.
Description of the change
Integrating this into Launchpad's StormRangeFactory may still require a bit more care, and I'd like to have at least one use case there that definitely works before landing this, but I might as well get this up for review in the meantime.
To post a comment you must log in.
Unmerged revisions
- 212. By Colin Watson
-
Get ScopedCollectio
n.range_ factory_ class from the corresponding field. - 211. By Colin Watson
-
Merge trunk.
- 210. By Colin Watson
-
Merge trunk.
- 209. By Colin Watson
-
Allow declaring a custom range factory for use with returned collections.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'setup.py' | |||
2 | --- setup.py 2018-02-21 14:46:13 +0000 | |||
3 | +++ setup.py 2018-10-02 06:38:17 +0000 | |||
4 | @@ -56,7 +56,7 @@ | |||
5 | 56 | 'docutils>=0.3.7', | 56 | 'docutils>=0.3.7', |
6 | 57 | 'epydoc', # used by wadl generation | 57 | 'epydoc', # used by wadl generation |
7 | 58 | 'grokcore.component==1.6', | 58 | 'grokcore.component==1.6', |
9 | 59 | 'lazr.batchnavigator>=1.2.0-dev', | 59 | 'lazr.batchnavigator>=1.2.3', |
10 | 60 | 'lazr.delegates>=2.0.3', | 60 | 'lazr.delegates>=2.0.3', |
11 | 61 | 'lazr.enum', | 61 | 'lazr.enum', |
12 | 62 | 'lazr.lifecycle', | 62 | 'lazr.lifecycle', |
13 | 63 | 63 | ||
14 | === modified file 'src/lazr/restful/_operation.py' | |||
15 | --- src/lazr/restful/_operation.py 2018-04-23 12:42:00 +0000 | |||
16 | +++ src/lazr/restful/_operation.py 2018-10-02 06:38:17 +0000 | |||
17 | @@ -118,7 +118,9 @@ | |||
18 | 118 | if self.total_size_only: | 118 | if self.total_size_only: |
19 | 119 | result = self.get_total_size(result) | 119 | result = self.get_total_size(result) |
20 | 120 | else: | 120 | else: |
22 | 121 | result = self.batch(result, self.request) + '}' | 121 | result = self.batch( |
23 | 122 | result, self.request, | ||
24 | 123 | range_factory=self.get_range_factory(result)) + '}' | ||
25 | 122 | else: | 124 | else: |
26 | 123 | # Serialize the result to JSON. Any embedded entries will be | 125 | # Serialize the result to JSON. Any embedded entries will be |
27 | 124 | # automatically serialized. | 126 | # automatically serialized. |
28 | @@ -163,6 +165,13 @@ | |||
29 | 163 | # Any other objects (eg. Entries) are not batched. | 165 | # Any other objects (eg. Entries) are not batched. |
30 | 164 | return False | 166 | return False |
31 | 165 | 167 | ||
32 | 168 | def get_range_factory(self, result): | ||
33 | 169 | """The custom range factory to use for this response, if any.""" | ||
34 | 170 | if (ICollectionField.providedBy(self.return_type) and | ||
35 | 171 | self.return_type.range_factory_class is not None): | ||
36 | 172 | return self.return_type.range_factory_class(result) | ||
37 | 173 | return None | ||
38 | 174 | |||
39 | 166 | def validate(self): | 175 | def validate(self): |
40 | 167 | """Validate incoming arguments against the operation schema. | 176 | """Validate incoming arguments against the operation schema. |
41 | 168 | 177 | ||
42 | 169 | 178 | ||
43 | === modified file 'src/lazr/restful/_resource.py' | |||
44 | --- src/lazr/restful/_resource.py 2018-10-01 08:55:53 +0000 | |||
45 | +++ src/lazr/restful/_resource.py 2018-10-02 06:38:17 +0000 | |||
46 | @@ -611,7 +611,7 @@ | |||
47 | 611 | return simplejson.dumps(len(entries)) | 611 | return simplejson.dumps(len(entries)) |
48 | 612 | 612 | ||
49 | 613 | 613 | ||
51 | 614 | def batch(self, entries, request): | 614 | def batch(self, entries, request, range_factory=None): |
52 | 615 | """Prepare a batch from a (possibly huge) list of entries. | 615 | """Prepare a batch from a (possibly huge) list of entries. |
53 | 616 | 616 | ||
54 | 617 | :return: a JSON string representing a hash: | 617 | :return: a JSON string representing a hash: |
55 | @@ -635,7 +635,8 @@ | |||
56 | 635 | """ | 635 | """ |
57 | 636 | if not hasattr(entries, '__len__'): | 636 | if not hasattr(entries, '__len__'): |
58 | 637 | entries = IFiniteSequence(entries) | 637 | entries = IFiniteSequence(entries) |
60 | 638 | navigator = WebServiceBatchNavigator(entries, request) | 638 | navigator = WebServiceBatchNavigator( |
61 | 639 | entries, request, range_factory=range_factory) | ||
62 | 639 | 640 | ||
63 | 640 | view_permission = getUtility(IWebServiceConfiguration).view_permission | 641 | view_permission = getUtility(IWebServiceConfiguration).view_permission |
64 | 641 | batch = { 'start' : navigator.batch.start } | 642 | batch = { 'start' : navigator.batch.start } |
65 | @@ -1817,7 +1818,12 @@ | |||
66 | 1817 | entries = self.collection.find() | 1818 | entries = self.collection.find() |
67 | 1818 | if request is None: | 1819 | if request is None: |
68 | 1819 | request = self.request | 1820 | request = self.request |
70 | 1820 | result = super(CollectionResource, self).batch(entries, request) | 1821 | if self.collection.range_factory_class is None: |
71 | 1822 | range_factory = None | ||
72 | 1823 | else: | ||
73 | 1824 | range_factory = self.collection.range_factory_class(entries) | ||
74 | 1825 | result = super(CollectionResource, self).batch( | ||
75 | 1826 | entries, request, range_factory=range_factory) | ||
76 | 1821 | result += ( | 1827 | result += ( |
77 | 1822 | ', "resource_type_link" : ' + simplejson.dumps(self.type_url)) | 1828 | ', "resource_type_link" : ' + simplejson.dumps(self.type_url)) |
78 | 1823 | return result | 1829 | return result |
79 | @@ -2089,6 +2095,8 @@ | |||
80 | 2089 | class Collection: | 2095 | class Collection: |
81 | 2090 | """A collection of entries.""" | 2096 | """A collection of entries.""" |
82 | 2091 | 2097 | ||
83 | 2098 | range_factory_class = None | ||
84 | 2099 | |||
85 | 2092 | def __init__(self, context, request): | 2100 | def __init__(self, context, request): |
86 | 2093 | """Associate the entry with some database model object.""" | 2101 | """Associate the entry with some database model object.""" |
87 | 2094 | self.context = context | 2102 | self.context = context |
88 | @@ -2124,6 +2132,11 @@ | |||
89 | 2124 | return getGlobalSiteManager().adapters.lookup( | 2132 | return getGlobalSiteManager().adapters.lookup( |
90 | 2125 | (model_schema, request_interface), IEntry).schema | 2133 | (model_schema, request_interface), IEntry).schema |
91 | 2126 | 2134 | ||
92 | 2135 | @property | ||
93 | 2136 | def range_factory_class(self): | ||
94 | 2137 | """See `ICollection`.""" | ||
95 | 2138 | return self.relationship.range_factory_class | ||
96 | 2139 | |||
97 | 2127 | def find(self): | 2140 | def find(self): |
98 | 2128 | """See `ICollection`.""" | 2141 | """See `ICollection`.""" |
99 | 2129 | return self.collection | 2142 | return self.collection |
100 | 2130 | 2143 | ||
101 | === modified file 'src/lazr/restful/declarations.py' | |||
102 | --- src/lazr/restful/declarations.py 2018-09-28 15:46:34 +0000 | |||
103 | +++ src/lazr/restful/declarations.py 2018-10-02 06:38:17 +0000 | |||
104 | @@ -354,7 +354,7 @@ | |||
105 | 354 | return field | 354 | return field |
106 | 355 | 355 | ||
107 | 356 | 356 | ||
109 | 357 | def export_as_webservice_collection(entry_schema): | 357 | def export_as_webservice_collection(entry_schema, range_factory_class=None): |
110 | 358 | """Mark the interface as exported on the web service as a collection. | 358 | """Mark the interface as exported on the web service as a collection. |
111 | 359 | 359 | ||
112 | 360 | :raises TypeError: if the interface doesn't have a method decorated with | 360 | :raises TypeError: if the interface doesn't have a method decorated with |
113 | @@ -369,7 +369,8 @@ | |||
114 | 369 | # check it. | 369 | # check it. |
115 | 370 | tags = _get_interface_tags() | 370 | tags = _get_interface_tags() |
116 | 371 | tags[LAZR_WEBSERVICE_EXPORTED] = dict( | 371 | tags[LAZR_WEBSERVICE_EXPORTED] = dict( |
118 | 372 | type=COLLECTION_TYPE, collection_entry_schema=entry_schema) | 372 | type=COLLECTION_TYPE, collection_entry_schema=entry_schema, |
119 | 373 | collection_range_factory_class=range_factory_class) | ||
120 | 373 | 374 | ||
121 | 374 | def mark_collection(interface): | 375 | def mark_collection(interface): |
122 | 375 | """Class advisor that tags the interface once it is created.""" | 376 | """Class advisor that tags the interface once it is created.""" |
123 | @@ -846,15 +847,18 @@ | |||
124 | 846 | """Specify that the exported operation returns a collection. | 847 | """Specify that the exported operation returns a collection. |
125 | 847 | 848 | ||
126 | 848 | The decorator takes one required argument, "schema", an interface that's | 849 | The decorator takes one required argument, "schema", an interface that's |
128 | 849 | been exported as an entry. | 850 | been exported as an entry, and one optional argument, |
129 | 851 | "range_factory_class", which specifies a custom class implementing | ||
130 | 852 | IRangeFactory to be used to adapt results for this collection. | ||
131 | 850 | """ | 853 | """ |
133 | 851 | def __init__(self, schema): | 854 | def __init__(self, schema, range_factory_class=None): |
134 | 852 | _check_called_from_interface_def('%s()' % self.__class__.__name__) | 855 | _check_called_from_interface_def('%s()' % self.__class__.__name__) |
135 | 853 | if not IInterface.providedBy(schema): | 856 | if not IInterface.providedBy(schema): |
136 | 854 | raise TypeError('Collection value type %s does not provide ' | 857 | raise TypeError('Collection value type %s does not provide ' |
137 | 855 | 'IInterface.' % schema) | 858 | 'IInterface.' % schema) |
138 | 856 | self.return_type = CollectionField( | 859 | self.return_type = CollectionField( |
140 | 857 | value_type=Reference(schema=schema)) | 860 | value_type=Reference(schema=schema), |
141 | 861 | range_factory_class=range_factory_class) | ||
142 | 858 | 862 | ||
143 | 859 | def annotate_method(self, method, annotations): | 863 | def annotate_method(self, method, annotations): |
144 | 860 | annotations['return_type'] = self.return_type | 864 | annotations['return_type'] = self.return_type |
145 | @@ -1355,8 +1359,10 @@ | |||
146 | 1355 | "version '%s'." % (interface.__name__, version)) | 1359 | "version '%s'." % (interface.__name__, version)) |
147 | 1356 | method_name, params = default_content_by_version[version] | 1360 | method_name, params = default_content_by_version[version] |
148 | 1357 | entry_schema = tag['collection_entry_schema'] | 1361 | entry_schema = tag['collection_entry_schema'] |
149 | 1362 | range_factory_class = tag['collection_range_factory_class'] | ||
150 | 1358 | class_dict = { | 1363 | class_dict = { |
151 | 1359 | 'entry_schema' : CollectionEntrySchema(entry_schema), | 1364 | 'entry_schema' : CollectionEntrySchema(entry_schema), |
152 | 1365 | 'range_factory_class': range_factory_class, | ||
153 | 1360 | 'method_name': method_name, | 1366 | 'method_name': method_name, |
154 | 1361 | 'params': params, | 1367 | 'params': params, |
155 | 1362 | '__doc__': interface.__doc__, | 1368 | '__doc__': interface.__doc__, |
156 | 1363 | 1369 | ||
157 | === modified file 'src/lazr/restful/docs/webservice-declarations.txt' | |||
158 | --- src/lazr/restful/docs/webservice-declarations.txt 2018-09-28 15:41:05 +0000 | |||
159 | +++ src/lazr/restful/docs/webservice-declarations.txt 2018-10-02 06:38:17 +0000 | |||
160 | @@ -215,6 +215,7 @@ | |||
161 | 215 | >>> print_export_tag(IBookSet) | 215 | >>> print_export_tag(IBookSet) |
162 | 216 | collection_default_content: {None: ('getAllBooks', {})} | 216 | collection_default_content: {None: ('getAllBooks', {})} |
163 | 217 | collection_entry_schema: <InterfaceClass __builtin__.IBook> | 217 | collection_entry_schema: <InterfaceClass __builtin__.IBook> |
164 | 218 | collection_range_factory_class: None | ||
165 | 218 | type: 'collection' | 219 | type: 'collection' |
166 | 219 | 220 | ||
167 | 220 | >>> print_export_tag(ICheckedOutBookSet) | 221 | >>> print_export_tag(ICheckedOutBookSet) |
168 | @@ -222,6 +223,7 @@ | |||
169 | 222 | {'title': '', | 223 | {'title': '', |
170 | 223 | 'user': <class '...REQUEST_USER'>})} | 224 | 'user': <class '...REQUEST_USER'>})} |
171 | 224 | collection_entry_schema: <InterfaceClass __builtin__.IBook> | 225 | collection_entry_schema: <InterfaceClass __builtin__.IBook> |
172 | 226 | collection_range_factory_class: None | ||
173 | 225 | type: 'collection' | 227 | type: 'collection' |
174 | 226 | 228 | ||
175 | 227 | The entry schema for a collection must be provided and must be an | 229 | The entry schema for a collection must be provided and must be an |
176 | @@ -231,7 +233,7 @@ | |||
177 | 231 | ... export_as_webservice_collection() | 233 | ... export_as_webservice_collection() |
178 | 232 | Traceback (most recent call last): | 234 | Traceback (most recent call last): |
179 | 233 | ... | 235 | ... |
181 | 234 | TypeError: export_as_webservice_collection() takes exactly 1 | 236 | TypeError: export_as_webservice_collection() takes at least 1 |
182 | 235 | argument (0 given) | 237 | argument (0 given) |
183 | 236 | 238 | ||
184 | 237 | >>> class InvalidEntrySchema(Interface): | 239 | >>> class InvalidEntrySchema(Interface): |
185 | @@ -355,12 +357,25 @@ | |||
186 | 355 | a single entry; @operation_returns_collection_of is used when the | 357 | a single entry; @operation_returns_collection_of is used when the |
187 | 356 | operation returns a collection of entries. | 358 | operation returns a collection of entries. |
188 | 357 | 359 | ||
189 | 360 | >>> from lazr.batchnavigator import ListRangeFactory | ||
190 | 358 | >>> from lazr.restful.declarations import ( | 361 | >>> from lazr.restful.declarations import ( |
191 | 359 | ... export_operation_as, export_factory_operation, | 362 | ... export_operation_as, export_factory_operation, |
192 | 360 | ... export_read_operation, operation_parameters, | 363 | ... export_read_operation, operation_parameters, |
193 | 361 | ... operation_returns_entry, operation_returns_collection_of, | 364 | ... operation_returns_entry, operation_returns_collection_of, |
194 | 362 | ... rename_parameters_as) | 365 | ... rename_parameters_as) |
195 | 363 | >>> from lazr.restful.interface import copy_field | 366 | >>> from lazr.restful.interface import copy_field |
196 | 367 | |||
197 | 368 | >>> class RecordingRangeFactory(ListRangeFactory): | ||
198 | 369 | ... calls = [] | ||
199 | 370 | ... | ||
200 | 371 | ... def getEndpointMemos(self, batch): | ||
201 | 372 | ... self.calls.append('getEndpointMemos') | ||
202 | 373 | ... return ListRangeFactory.getEndpointMemos(self, batch) | ||
203 | 374 | ... | ||
204 | 375 | ... def getSlice(self, size, memo, forwards): | ||
205 | 376 | ... self.calls.append(('getSlice', size, memo, forwards)) | ||
206 | 377 | ... return ListRangeFactory.getSlice(self, size, memo, forwards) | ||
207 | 378 | |||
208 | 364 | >>> class IBookSetOnSteroids(IBookSet): | 379 | >>> class IBookSetOnSteroids(IBookSet): |
209 | 365 | ... """IBookSet supporting some methods.""" | 380 | ... """IBookSet supporting some methods.""" |
210 | 366 | ... export_as_webservice_collection(IBook) | 381 | ... export_as_webservice_collection(IBook) |
211 | @@ -375,6 +390,17 @@ | |||
212 | 375 | ... | 390 | ... |
213 | 376 | ... @operation_parameters( | 391 | ... @operation_parameters( |
214 | 377 | ... text=copy_field(IBook['title'], title=u'Text to search for.')) | 392 | ... text=copy_field(IBook['title'], title=u'Text to search for.')) |
215 | 393 | ... @operation_returns_collection_of( | ||
216 | 394 | ... IBook, range_factory_class=RecordingRangeFactory) | ||
217 | 395 | ... @export_read_operation() | ||
218 | 396 | ... def searchBookTitlesCustomRange(text): | ||
219 | 397 | ... """Return list of books whose titles contain 'text'. | ||
220 | 398 | ... | ||
221 | 399 | ... This method uses a custom range factory. | ||
222 | 400 | ... """ | ||
223 | 401 | ... | ||
224 | 402 | ... @operation_parameters( | ||
225 | 403 | ... text=copy_field(IBook['title'], title=u'Text to search for.')) | ||
226 | 378 | ... @operation_returns_entry(IBook) | 404 | ... @operation_returns_entry(IBook) |
227 | 379 | ... @export_read_operation() | 405 | ... @export_read_operation() |
228 | 380 | ... def bestMatch(text): | 406 | ... def bestMatch(text): |
229 | @@ -442,6 +468,22 @@ | |||
230 | 442 | params: {'text': <...TextLine...>} | 468 | params: {'text': <...TextLine...>} |
231 | 443 | return_type: <lazr.restful.fields.CollectionField object...> | 469 | return_type: <lazr.restful.fields.CollectionField object...> |
232 | 444 | type: 'read_operation' | 470 | type: 'read_operation' |
233 | 471 | >>> print IBookSetOnSteroids['searchBookTitles'].getTaggedValue( | ||
234 | 472 | ... 'lazr.restful.exported')['return_type'].range_factory_class | ||
235 | 473 | None | ||
236 | 474 | |||
237 | 475 | The 'searchBookTitlesCustomRange' method is just the same, except that it | ||
238 | 476 | uses a custom range factory. | ||
239 | 477 | |||
240 | 478 | >>> print_export_tag(IBookSetOnSteroids['searchBookTitlesCustomRange']) | ||
241 | 479 | as: 'searchBookTitlesCustomRange' | ||
242 | 480 | call_with: {} | ||
243 | 481 | params: {'text': <...TextLine...>} | ||
244 | 482 | return_type: <lazr.restful.fields.CollectionField object...> | ||
245 | 483 | type: 'read_operation' | ||
246 | 484 | >>> print IBookSetOnSteroids['searchBookTitlesCustomRange'].getTaggedValue( | ||
247 | 485 | ... 'lazr.restful.exported')['return_type'].range_factory_class | ||
248 | 486 | <class 'RecordingRangeFactory'> | ||
249 | 445 | 487 | ||
250 | 446 | The 'bestMatch' method returns an entry. | 488 | The 'bestMatch' method returns an entry. |
251 | 447 | 489 | ||
252 | @@ -1187,6 +1229,9 @@ | |||
253 | 1187 | ... def searchBookTitles(self, text): | 1229 | ... def searchBookTitles(self, text): |
254 | 1188 | ... return self.result | 1230 | ... return self.result |
255 | 1189 | ... | 1231 | ... |
256 | 1232 | ... def searchBookTitlesCustomRange(self, text): | ||
257 | 1233 | ... return self.result | ||
258 | 1234 | ... | ||
259 | 1190 | ... def new(self, author, base_price, title): | 1235 | ... def new(self, author, base_price, title): |
260 | 1191 | ... return Book(author, title, base_price, "unknown") | 1236 | ... return Book(author, title, base_price, "unknown") |
261 | 1192 | 1237 | ||
262 | @@ -2584,7 +2629,7 @@ | |||
263 | 2584 | 2629 | ||
264 | 2585 | >>> print debug_proxy(ProxyFactory(collection_adapter)) | 2630 | >>> print debug_proxy(ProxyFactory(collection_adapter)) |
265 | 2586 | zope.security._proxy._Proxy (using zope.security.checker.Checker) | 2631 | zope.security._proxy._Proxy (using zope.security.checker.Checker) |
267 | 2587 | public: entry_schema, find | 2632 | public: entry_schema, find, range_factory_class |
268 | 2588 | 2633 | ||
269 | 2589 | >>> print debug_proxy(ProxyFactory(read_method_adapter)) | 2634 | >>> print debug_proxy(ProxyFactory(read_method_adapter)) |
270 | 2590 | zope.security._proxy._Proxy (using zope.security.checker.Checker) | 2635 | zope.security._proxy._Proxy (using zope.security.checker.Checker) |
271 | 2591 | 2636 | ||
272 | === modified file 'src/lazr/restful/docs/webservice.txt' | |||
273 | --- src/lazr/restful/docs/webservice.txt 2018-10-01 08:55:53 +0000 | |||
274 | +++ src/lazr/restful/docs/webservice.txt 2018-10-02 06:38:17 +0000 | |||
275 | @@ -609,13 +609,27 @@ | |||
276 | 609 | ... singular="recipe", plural="recipes", | 609 | ... singular="recipe", plural="recipes", |
277 | 610 | ... publish_web_link=True)) | 610 | ... publish_web_link=True)) |
278 | 611 | 611 | ||
279 | 612 | >>> from lazr.batchnavigator import ListRangeFactory | ||
280 | 613 | >>> class RecordingRangeFactory(ListRangeFactory): | ||
281 | 614 | ... calls = [] | ||
282 | 615 | ... | ||
283 | 616 | ... def getEndpointMemos(self, batch): | ||
284 | 617 | ... self.calls.append('getEndpointMemos') | ||
285 | 618 | ... return ListRangeFactory.getEndpointMemos(self, batch) | ||
286 | 619 | ... | ||
287 | 620 | ... def getSlice(self, size, memo, forwards): | ||
288 | 621 | ... self.calls.append(('getSlice', size, memo, forwards)) | ||
289 | 622 | ... return ListRangeFactory.getSlice(self, size, memo, forwards) | ||
290 | 623 | |||
291 | 612 | >>> from lazr.restful.fields import ReferenceChoice | 624 | >>> from lazr.restful.fields import ReferenceChoice |
292 | 613 | >>> class ICookbookEntry(IEntry): | 625 | >>> class ICookbookEntry(IEntry): |
293 | 614 | ... name = TextLine(title=u"Name", required=True) | 626 | ... name = TextLine(title=u"Name", required=True) |
294 | 615 | ... cuisine = TextLine(title=u"Cuisine", required=False, default=None) | 627 | ... cuisine = TextLine(title=u"Cuisine", required=False, default=None) |
295 | 616 | ... author = ReferenceChoice( | 628 | ... author = ReferenceChoice( |
296 | 617 | ... schema=IAuthor, vocabulary=AuthorVocabulary()) | 629 | ... schema=IAuthor, vocabulary=AuthorVocabulary()) |
298 | 618 | ... recipes = CollectionField(value_type=Reference(schema=IRecipe)) | 630 | ... recipes = CollectionField( |
299 | 631 | ... value_type=Reference(schema=IRecipe), | ||
300 | 632 | ... range_factory_class=RecordingRangeFactory) | ||
301 | 619 | ... comments = CollectionField(value_type=Reference(schema=IComment)) | 633 | ... comments = CollectionField(value_type=Reference(schema=IComment)) |
302 | 620 | ... cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.") | 634 | ... cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.") |
303 | 621 | ... taggedValue( | 635 | ... taggedValue( |
304 | @@ -785,10 +799,12 @@ | |||
305 | 785 | ... """ | 799 | ... """ |
306 | 786 | ... | 800 | ... |
307 | 787 | ... entry_schema = ICookbookEntry | 801 | ... entry_schema = ICookbookEntry |
308 | 802 | ... range_factory_class = RecordingRangeFactory | ||
309 | 788 | ... | 803 | ... |
310 | 789 | ... def find(self): | 804 | ... def find(self): |
313 | 790 | ... """Find all the cookbooks.""" | 805 | ... """Find all the cookbooks.""" |
314 | 791 | ... return self.context.getAll() | 806 | ... return self.context.getAll() |
315 | 807 | |||
316 | 792 | >>> sm.registerAdapter(CookbookCollection, | 808 | >>> sm.registerAdapter(CookbookCollection, |
317 | 793 | ... (ICookbookSet, IWebServiceClientRequest), | 809 | ... (ICookbookSet, IWebServiceClientRequest), |
318 | 794 | ... provided=ICollection) | 810 | ... provided=ICollection) |
319 | @@ -1190,6 +1206,8 @@ | |||
320 | 1190 | But if we ask for a page size of two, we can see how pagination | 1206 | But if we ask for a page size of two, we can see how pagination |
321 | 1191 | works. Here's page one, with two cookbooks on it. | 1207 | works. Here's page one, with two cookbooks on it. |
322 | 1192 | 1208 | ||
323 | 1209 | >>> RecordingRangeFactory.calls = [] | ||
324 | 1210 | |||
325 | 1193 | >>> request = create_web_service_request( | 1211 | >>> request = create_web_service_request( |
326 | 1194 | ... '/beta/cookbooks', environ={'QUERY_STRING' : 'ws.size=2'}) | 1212 | ... '/beta/cookbooks', environ={'QUERY_STRING' : 'ws.size=2'}) |
327 | 1195 | >>> collection = request.traverse(app) | 1213 | >>> collection = request.traverse(app) |
328 | @@ -1222,6 +1240,36 @@ | |||
329 | 1222 | >>> len(representation['entries']) | 1240 | >>> len(representation['entries']) |
330 | 1223 | 1 | 1241 | 1 |
331 | 1224 | 1242 | ||
332 | 1243 | Serving this collection uses the custom range factory. | ||
333 | 1244 | |||
334 | 1245 | >>> 'getEndpointMemos' in RecordingRangeFactory.calls | ||
335 | 1246 | True | ||
336 | 1247 | |||
337 | 1248 | Similarly, serving a scoped collection uses the custom range factory, if one | ||
338 | 1249 | is defined. | ||
339 | 1250 | |||
340 | 1251 | >>> RecordingRangeFactory.calls = [] | ||
341 | 1252 | |||
342 | 1253 | >>> request = create_web_service_request( | ||
343 | 1254 | ... '/beta/cookbooks/The Joy of Cooking/recipes', | ||
344 | 1255 | ... environ={'QUERY_STRING': 'ws.size=1'}) | ||
345 | 1256 | >>> collection = request.traverse(app) | ||
346 | 1257 | >>> representation = load_json(collection()) | ||
347 | 1258 | |||
348 | 1259 | >>> sorted(representation.keys()) | ||
349 | 1260 | [u'entries', u'next_collection_link', u'resource_type_link', | ||
350 | 1261 | u'start', u'total_size'] | ||
351 | 1262 | >>> representation['next_collection_link'] | ||
352 | 1263 | u'http://api.cookbooks.dev/beta/cookbooks/The%20Joy%20of%20Cooking/recipes?ws.size=1&memo=1&ws.start=1' | ||
353 | 1264 | >>> len(representation['entries']) | ||
354 | 1265 | 1 | ||
355 | 1266 | >>> representation['total_size'] | ||
356 | 1267 | 2 | ||
357 | 1268 | |||
358 | 1269 | >>> 'getEndpointMemos' in RecordingRangeFactory.calls | ||
359 | 1270 | True | ||
360 | 1271 | |||
361 | 1272 | |||
362 | 1225 | Custom operations | 1273 | Custom operations |
363 | 1226 | ================= | 1274 | ================= |
364 | 1227 | 1275 | ||
365 | 1228 | 1276 | ||
366 | === modified file 'src/lazr/restful/fields.py' | |||
367 | --- src/lazr/restful/fields.py 2016-02-17 01:07:21 +0000 | |||
368 | +++ src/lazr/restful/fields.py 2018-10-02 06:38:17 +0000 | |||
369 | @@ -25,7 +25,7 @@ | |||
370 | 25 | # has a _type of list, and we don't want to have to implement list | 25 | # has a _type of list, and we don't want to have to implement list |
371 | 26 | # semantics for this class. | 26 | # semantics for this class. |
372 | 27 | 27 | ||
374 | 28 | def __init__(self, *args, **kwargs): | 28 | def __init__(self, range_factory_class=None, *args, **kwargs): |
375 | 29 | """A generic collection field. | 29 | """A generic collection field. |
376 | 30 | 30 | ||
377 | 31 | The readonly property defaults to True since these fields are usually | 31 | The readonly property defaults to True since these fields are usually |
378 | @@ -34,6 +34,7 @@ | |||
379 | 34 | """ | 34 | """ |
380 | 35 | kwargs.setdefault('readonly', True) | 35 | kwargs.setdefault('readonly', True) |
381 | 36 | super(CollectionField, self).__init__(*args, **kwargs) | 36 | super(CollectionField, self).__init__(*args, **kwargs) |
382 | 37 | self.range_factory_class = range_factory_class | ||
383 | 37 | 38 | ||
384 | 38 | 39 | ||
385 | 39 | @implementer(IReference) | 40 | @implementer(IReference) |
386 | 40 | 41 | ||
387 | === modified file 'src/lazr/restful/interfaces/_fields.py' | |||
388 | --- src/lazr/restful/interfaces/_fields.py 2011-01-19 22:29:32 +0000 | |||
389 | +++ src/lazr/restful/interfaces/_fields.py 2018-10-02 06:38:17 +0000 | |||
390 | @@ -34,6 +34,11 @@ | |||
391 | 34 | All iterables satisfy this collection field. | 34 | All iterables satisfy this collection field. |
392 | 35 | """ | 35 | """ |
393 | 36 | 36 | ||
394 | 37 | range_factory_class = Attribute( | ||
395 | 38 | "A class implementing IRangeFactory to be used to adapt results " | ||
396 | 39 | "for this collection field, or None.") | ||
397 | 40 | |||
398 | 41 | |||
399 | 37 | class IReference(IObject): | 42 | class IReference(IObject): |
400 | 38 | """A reference to an object providing a particular schema. | 43 | """A reference to an object providing a particular schema. |
401 | 39 | 44 | ||
402 | 40 | 45 | ||
403 | === modified file 'src/lazr/restful/interfaces/_rest.py' | |||
404 | --- src/lazr/restful/interfaces/_rest.py 2011-03-31 01:13:59 +0000 | |||
405 | +++ src/lazr/restful/interfaces/_rest.py 2018-10-02 06:38:17 +0000 | |||
406 | @@ -224,6 +224,9 @@ | |||
407 | 224 | """A collection, driven by an ICollectionResource.""" | 224 | """A collection, driven by an ICollectionResource.""" |
408 | 225 | 225 | ||
409 | 226 | entry_schema = Attribute("The schema for this collection's entries.") | 226 | entry_schema = Attribute("The schema for this collection's entries.") |
410 | 227 | range_factory_class = Attribute( | ||
411 | 228 | "A class implementing IRangeFactory to be used to adapt results " | ||
412 | 229 | "for this collection, or None.") | ||
413 | 227 | 230 | ||
414 | 228 | def find(): | 231 | def find(): |
415 | 229 | """Retrieve all entries in the collection under the given scope. | 232 | """Retrieve all entries in the collection under the given scope. |