Merge ~cjwatson/launchpad:export-polls into launchpad:master
- Git
- lp:~cjwatson/launchpad
- export-polls
- Merge into master
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
Approved revision: | 9ed8b9916b05ae312e8edf9c968a65a65dc48bd7 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~cjwatson/launchpad:export-polls |
Merge into: | launchpad:master |
Diff against target: |
610 lines (+302/-41) 9 files modified
lib/lp/app/browser/launchpad.py (+2/-0) lib/lp/registry/adapters.py (+6/-3) lib/lp/registry/browser/configure.zcml (+5/-0) lib/lp/registry/enums.py (+31/-0) lib/lp/registry/interfaces/poll.py (+78/-30) lib/lp/registry/interfaces/webservice.py (+6/-0) lib/lp/registry/model/poll.py (+36/-8) lib/lp/registry/tests/test_poll.py (+129/-0) lib/lp/services/webservice/wadl-to-refhtml.xsl (+9/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tom Wardill (community) | Approve | ||
Review via email: mp+401056@code.launchpad.net |
Commit message
Export a simple read-only API for polls
Description of the change
This is just enough to allow us to deal with poll spam more easily. The internal interface for selecting the ordering of poll results changes a bit, but existing web UI views should remain unchanged.
We don't need to consider privacy, as the validator on `Poll.team_id` already constrains polls to be owned only by public teams.
To post a comment you must log in.
Revision history for this message
Tom Wardill (twom) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/app/browser/launchpad.py b/lib/lp/app/browser/launchpad.py |
2 | index 7388562..ebeb6d5 100644 |
3 | --- a/lib/lp/app/browser/launchpad.py |
4 | +++ b/lib/lp/app/browser/launchpad.py |
5 | @@ -114,6 +114,7 @@ from lp.registry.interfaces.karma import IKarmaActionSet |
6 | from lp.registry.interfaces.nameblacklist import INameBlacklistSet |
7 | from lp.registry.interfaces.person import IPersonSet |
8 | from lp.registry.interfaces.pillar import IPillarNameSet |
9 | +from lp.registry.interfaces.poll import IPollSet |
10 | from lp.registry.interfaces.product import ( |
11 | InvalidProductName, |
12 | IProduct, |
13 | @@ -867,6 +868,7 @@ class LaunchpadRootNavigation(Navigation): |
14 | 'package-sets': IPackagesetSet, |
15 | 'people': IPersonSet, |
16 | 'pillars': IPillarNameSet, |
17 | + '+polls': IPollSet, |
18 | '+processors': IProcessorSet, |
19 | 'projects': IProductSet, |
20 | 'projectgroups': IProjectGroupSet, |
21 | diff --git a/lib/lp/registry/adapters.py b/lib/lp/registry/adapters.py |
22 | index e5016c4..beba4a7 100644 |
23 | --- a/lib/lp/registry/adapters.py |
24 | +++ b/lib/lp/registry/adapters.py |
25 | @@ -18,6 +18,7 @@ from zope.component.interfaces import ComponentLookupError |
26 | from zope.interface import implementer |
27 | |
28 | from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet |
29 | +from lp.registry.enums import PollSort |
30 | from lp.registry.interfaces.poll import ( |
31 | IPollSet, |
32 | IPollSubset, |
33 | @@ -96,14 +97,16 @@ class PollSubset: |
34 | assert self.team is not None, ( |
35 | 'team cannot be None to call this method.') |
36 | return getUtility(IPollSet).findByTeam( |
37 | - self.team, [PollStatus.OPEN], order_by='datecloses', when=when) |
38 | + self.team, [PollStatus.OPEN], |
39 | + order_by=PollSort.CLOSING, when=when) |
40 | |
41 | def getClosedPolls(self, when=None): |
42 | """See IPollSubset.""" |
43 | assert self.team is not None, ( |
44 | 'team cannot be None to call this method.') |
45 | return getUtility(IPollSet).findByTeam( |
46 | - self.team, [PollStatus.CLOSED], order_by='datecloses', when=when) |
47 | + self.team, [PollStatus.CLOSED], |
48 | + order_by=PollSort.CLOSING, when=when) |
49 | |
50 | def getNotYetOpenedPolls(self, when=None): |
51 | """See IPollSubset.""" |
52 | @@ -111,7 +114,7 @@ class PollSubset: |
53 | 'team cannot be None to call this method.') |
54 | return getUtility(IPollSet).findByTeam( |
55 | self.team, [PollStatus.NOT_YET_OPENED], |
56 | - order_by='dateopens', when=when) |
57 | + order_by=PollSort.OPENING, when=when) |
58 | |
59 | |
60 | def productseries_to_product(productseries): |
61 | diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml |
62 | index cdaa3da..8895a76 100644 |
63 | --- a/lib/lp/registry/browser/configure.zcml |
64 | +++ b/lib/lp/registry/browser/configure.zcml |
65 | @@ -757,6 +757,11 @@ |
66 | path_expression="string:+poll/${name}" |
67 | attribute_to_parent="team" |
68 | /> |
69 | + <browser:url |
70 | + for="lp.registry.interfaces.poll.IPollSet" |
71 | + path_expression="string:+polls" |
72 | + parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot" |
73 | + /> |
74 | <browser:pages |
75 | for="lp.registry.interfaces.poll.IPoll" |
76 | class="lp.registry.browser.poll.PollView" |
77 | diff --git a/lib/lp/registry/enums.py b/lib/lp/registry/enums.py |
78 | index 0ca6af9..1e99d85 100644 |
79 | --- a/lib/lp/registry/enums.py |
80 | +++ b/lib/lp/registry/enums.py |
81 | @@ -14,6 +14,7 @@ __all__ = [ |
82 | 'INCLUSIVE_TEAM_POLICY', |
83 | 'PersonTransferJobType', |
84 | 'PersonVisibility', |
85 | + 'PollSort', |
86 | 'ProductJobType', |
87 | 'VCSType', |
88 | 'SharingPermission', |
89 | @@ -25,6 +26,8 @@ __all__ = [ |
90 | from lazr.enum import ( |
91 | DBEnumeratedType, |
92 | DBItem, |
93 | + EnumeratedType, |
94 | + Item, |
95 | ) |
96 | |
97 | |
98 | @@ -461,3 +464,31 @@ class DistributionDefaultTraversalPolicy(DBEnumeratedType): |
99 | The default traversal from a distribution is used for OCI projects |
100 | in that distribution. |
101 | """) |
102 | + |
103 | + |
104 | +class PollSort(EnumeratedType): |
105 | + """Choices for how to sort polls.""" |
106 | + |
107 | + OLDEST_FIRST = Item(""" |
108 | + oldest first |
109 | + |
110 | + Sort polls from oldest to newest. |
111 | + """) |
112 | + |
113 | + NEWEST_FIRST = Item(""" |
114 | + newest first |
115 | + |
116 | + Sort polls from newest to oldest. |
117 | + """) |
118 | + |
119 | + OPENING = Item(""" |
120 | + by opening date |
121 | + |
122 | + Sort polls with the earliest opening date first. |
123 | + """) |
124 | + |
125 | + CLOSING = Item(""" |
126 | + by closing date |
127 | + |
128 | + Sort polls with the earliest closing date first. |
129 | + """) |
130 | diff --git a/lib/lp/registry/interfaces/poll.py b/lib/lp/registry/interfaces/poll.py |
131 | index 6b894a4..f73aeb8 100644 |
132 | --- a/lib/lp/registry/interfaces/poll.py |
133 | +++ b/lib/lp/registry/interfaces/poll.py |
134 | @@ -27,7 +27,18 @@ from lazr.enum import ( |
135 | DBEnumeratedType, |
136 | DBItem, |
137 | ) |
138 | -from lazr.restful.declarations import error_status |
139 | +from lazr.restful.declarations import ( |
140 | + collection_default_content, |
141 | + error_status, |
142 | + export_read_operation, |
143 | + exported, |
144 | + exported_as_webservice_collection, |
145 | + exported_as_webservice_entry, |
146 | + operation_for_version, |
147 | + operation_parameters, |
148 | + operation_returns_collection_of, |
149 | + ) |
150 | +from lazr.restful.fields import Reference |
151 | import pytz |
152 | from six.moves import http_client |
153 | from zope.component import getUtility |
154 | @@ -42,12 +53,14 @@ from zope.schema import ( |
155 | Choice, |
156 | Datetime, |
157 | Int, |
158 | + List, |
159 | Text, |
160 | TextLine, |
161 | ) |
162 | |
163 | from lp import _ |
164 | from lp.app.validators.name import name_validator |
165 | +from lp.registry.enums import PollSort |
166 | from lp.registry.interfaces.person import ITeam |
167 | from lp.services.fields import ContentNameField |
168 | |
169 | @@ -120,53 +133,55 @@ class CannotCreatePoll(Exception): |
170 | pass |
171 | |
172 | |
173 | +@exported_as_webservice_entry(as_of="beta") |
174 | class IPoll(Interface): |
175 | """A poll for a given proposition in a team.""" |
176 | |
177 | id = Int(title=_('The unique ID'), required=True, readonly=True) |
178 | |
179 | - team = Int( |
180 | + team = exported(Reference( |
181 | + ITeam, |
182 | title=_('The team that this poll refers to.'), required=True, |
183 | - readonly=True) |
184 | + readonly=True)) |
185 | |
186 | - name = PollNameField( |
187 | + name = exported(PollNameField( |
188 | title=_('The unique name of this poll'), |
189 | description=_('A short unique name, beginning with a lower-case ' |
190 | 'letter or number, and containing only letters, ' |
191 | 'numbers, dots, hyphens, or plus signs.'), |
192 | - required=True, readonly=False, constraint=name_validator) |
193 | + required=True, readonly=False, constraint=name_validator)) |
194 | |
195 | - title = TextLine( |
196 | - title=_('The title of this poll'), required=True, readonly=False) |
197 | + title = exported(TextLine( |
198 | + title=_('The title of this poll'), required=True, readonly=False)) |
199 | |
200 | - dateopens = Datetime( |
201 | + dateopens = exported(Datetime( |
202 | title=_('The date and time when this poll opens'), required=True, |
203 | - readonly=False) |
204 | + readonly=False)) |
205 | |
206 | - datecloses = Datetime( |
207 | + datecloses = exported(Datetime( |
208 | title=_('The date and time when this poll closes'), required=True, |
209 | - readonly=False) |
210 | + readonly=False)) |
211 | |
212 | - proposition = Text( |
213 | + proposition = exported(Text( |
214 | title=_('The proposition that is going to be voted'), required=True, |
215 | - readonly=False) |
216 | + readonly=False)) |
217 | |
218 | - type = Choice( |
219 | + type = exported(Choice( |
220 | title=_('The type of this poll'), required=True, |
221 | readonly=False, vocabulary=PollAlgorithm, |
222 | - default=PollAlgorithm.CONDORCET) |
223 | + default=PollAlgorithm.CONDORCET)) |
224 | |
225 | - allowspoilt = Bool( |
226 | + allowspoilt = exported(Bool( |
227 | title=_('Users can spoil their votes?'), |
228 | description=_( |
229 | 'Allow users to leave the ballot blank (i.e. cast a vote for ' |
230 | '"None of the above")'), |
231 | - required=True, readonly=False, default=True) |
232 | + required=True, readonly=False, default=True)) |
233 | |
234 | - secrecy = Choice( |
235 | + secrecy = exported(Choice( |
236 | title=_('The secrecy of the Poll'), required=True, |
237 | readonly=False, vocabulary=PollSecrecy, |
238 | - default=PollSecrecy.SECRET) |
239 | + default=PollSecrecy.SECRET)) |
240 | |
241 | @invariant |
242 | def saneDates(poll): |
243 | @@ -297,6 +312,7 @@ class IPoll(Interface): |
244 | """ |
245 | |
246 | |
247 | +@exported_as_webservice_collection(IPoll) |
248 | class IPollSet(Interface): |
249 | """The set of Poll objects.""" |
250 | |
251 | @@ -304,19 +320,44 @@ class IPollSet(Interface): |
252 | secrecy, allowspoilt, poll_type=PollAlgorithm.SIMPLE): |
253 | """Create a new Poll for the given team.""" |
254 | |
255 | - def findByTeam(team, status=PollStatus.ALL, order_by=None, when=None): |
256 | - """Return all Polls for the given team, filtered by status. |
257 | - |
258 | - :status: is a sequence containing as many values as you want from |
259 | - PollStatus. |
260 | + @operation_parameters( |
261 | + team=Reference(ITeam, title=_("Team"), required=False), |
262 | + status=List( |
263 | + title=_("Poll statuses"), |
264 | + description=_( |
265 | + "A list of one or more of 'open', 'closed', or " |
266 | + "'not-yet-opened'. Defaults to all statuses."), |
267 | + value_type=Choice(values=PollStatus.ALL), min_length=1, |
268 | + required=False), |
269 | + order_by=Choice( |
270 | + title=_("Sort order"), vocabulary=PollSort, required=False)) |
271 | + @operation_returns_collection_of(IPoll) |
272 | + @export_read_operation() |
273 | + @operation_for_version("devel") |
274 | + def find(team=None, status=None, order_by=PollSort.NEWEST_FIRST, |
275 | + when=None): |
276 | + """Search for polls. |
277 | + |
278 | + :param team: An optional `ITeam` to filter by. |
279 | + :param status: A collection containing as many values as you want |
280 | + from PollStatus. Defaults to `PollStatus.ALL`. |
281 | + :param order_by: An optional `PollSort` item indicating how to sort |
282 | + the results. Defaults to `PollSort.NEWEST_FIRST`. |
283 | + :param when: Used only by tests, to filter for polls open at a |
284 | + specific date. |
285 | + """ |
286 | |
287 | - :order_by: can be either a string with the column name you want to |
288 | - sort or a list of column names as strings. |
289 | - If no order_by is specified the results will be ordered using the |
290 | - default ordering specified in Poll.sortingColumns. |
291 | + def findByTeam(team, status=None, order_by=PollSort.NEWEST_FIRST, |
292 | + when=None): |
293 | + """Return all Polls for the given team, filtered by status. |
294 | |
295 | - The optional :when argument is used only by our tests, to test if the |
296 | - poll is/was/will-be open at a specific date. |
297 | + :param team: A `ITeam` to filter by. |
298 | + :param status: A collection containing as many values as you want |
299 | + from PollStatus. Defaults to `PollStatus.ALL`. |
300 | + :param order_by: An optional `PollSort` item indicating how to sort |
301 | + the results. Defaults to `PollSort.NEWEST_FIRST`. |
302 | + :param when: Used only by tests, to filter for polls open at a |
303 | + specific date. |
304 | """ |
305 | |
306 | def getByTeamAndName(team, name, default=None): |
307 | @@ -325,6 +366,13 @@ class IPollSet(Interface): |
308 | Return :default if there's no Poll with this name for that team. |
309 | """ |
310 | |
311 | + @collection_default_content() |
312 | + def emptyList(): |
313 | + """Return an empty collection of polls. |
314 | + |
315 | + This only exists to keep lazr.restful happy. |
316 | + """ |
317 | + |
318 | |
319 | class IPollSubset(Interface): |
320 | """The set of Poll objects for a given team.""" |
321 | diff --git a/lib/lp/registry/interfaces/webservice.py b/lib/lp/registry/interfaces/webservice.py |
322 | index aab6c21..c27edda 100644 |
323 | --- a/lib/lp/registry/interfaces/webservice.py |
324 | +++ b/lib/lp/registry/interfaces/webservice.py |
325 | @@ -22,6 +22,8 @@ __all__ = [ |
326 | 'IPersonSet', |
327 | 'IPillar', |
328 | 'IPillarNameSet', |
329 | + 'IPoll', |
330 | + 'IPollSet', |
331 | 'IProduct', |
332 | 'IProductRelease', |
333 | 'IProductReleaseFile', |
334 | @@ -83,6 +85,10 @@ from lp.registry.interfaces.pillar import ( |
335 | IPillar, |
336 | IPillarNameSet, |
337 | ) |
338 | +from lp.registry.interfaces.poll import ( |
339 | + IPoll, |
340 | + IPollSet, |
341 | + ) |
342 | from lp.registry.interfaces.product import ( |
343 | IProduct, |
344 | IProductSet, |
345 | diff --git a/lib/lp/registry/model/poll.py b/lib/lp/registry/model/poll.py |
346 | index 419a620..a4fe45d 100644 |
347 | --- a/lib/lp/registry/model/poll.py |
348 | +++ b/lib/lp/registry/model/poll.py |
349 | @@ -22,6 +22,7 @@ from storm.locals import ( |
350 | And, |
351 | Bool, |
352 | DateTime, |
353 | + Desc, |
354 | Int, |
355 | Or, |
356 | Reference, |
357 | @@ -31,6 +32,7 @@ from storm.locals import ( |
358 | from zope.component import getUtility |
359 | from zope.interface import implementer |
360 | |
361 | +from lp.registry.enums import PollSort |
362 | from lp.registry.interfaces.person import validate_public_person |
363 | from lp.registry.interfaces.poll import ( |
364 | CannotCreatePoll, |
365 | @@ -309,15 +311,27 @@ class PollSet: |
366 | IStore(Poll).add(poll) |
367 | return poll |
368 | |
369 | - def findByTeam(self, team, status=PollStatus.ALL, order_by=None, |
370 | - when=None): |
371 | + @staticmethod |
372 | + def _convertPollSortToOrderBy(sort_by): |
373 | + """Compute a value to pass to `order_by` on a poll collection. |
374 | + |
375 | + :param sort_by: An item from the `PollSort` enumeration. |
376 | + """ |
377 | + return { |
378 | + PollSort.OLDEST_FIRST: [Poll.id], |
379 | + PollSort.NEWEST_FIRST: [Desc(Poll.id)], |
380 | + PollSort.OPENING: [Poll.dateopens, Poll.id], |
381 | + PollSort.CLOSING: [Poll.datecloses, Poll.id], |
382 | + }[sort_by] |
383 | + |
384 | + def find(self, team=None, status=None, order_by=PollSort.NEWEST_FIRST, |
385 | + when=None): |
386 | """See IPollSet.""" |
387 | + if status is None: |
388 | + status = PollStatus.ALL |
389 | if when is None: |
390 | when = datetime.now(pytz.timezone('UTC')) |
391 | |
392 | - if order_by is None: |
393 | - order_by = Poll.sortingColumns |
394 | - |
395 | status = set(status) |
396 | status_clauses = [] |
397 | if PollStatus.OPEN in status: |
398 | @@ -330,16 +344,30 @@ class PollSet: |
399 | |
400 | assert len(status_clauses) > 0, "No poll statuses were selected" |
401 | |
402 | - results = IStore(Poll).find( |
403 | - Poll, Poll.team == team, Or(*status_clauses)) |
404 | + clauses = [] |
405 | + if team is not None: |
406 | + clauses.append(Poll.team == team) |
407 | + clauses.append(Or(*status_clauses)) |
408 | + |
409 | + results = IStore(Poll).find(Poll, *clauses) |
410 | |
411 | - return results.order_by(order_by) |
412 | + return results.order_by(self._convertPollSortToOrderBy(order_by)) |
413 | + |
414 | + def findByTeam(self, team, status=None, order_by=PollSort.NEWEST_FIRST, |
415 | + when=None): |
416 | + """See IPollSet.""" |
417 | + return self.find( |
418 | + team=team, status=status, order_by=order_by, when=when) |
419 | |
420 | def getByTeamAndName(self, team, name, default=None): |
421 | """See IPollSet.""" |
422 | poll = IStore(Poll).find(Poll, team=team, name=name).one() |
423 | return poll if poll is not None else default |
424 | |
425 | + def emptyList(self): |
426 | + """See IPollSet.""" |
427 | + return [] |
428 | + |
429 | |
430 | @implementer(IPollOption) |
431 | class PollOption(StormBase): |
432 | diff --git a/lib/lp/registry/tests/test_poll.py b/lib/lp/registry/tests/test_poll.py |
433 | index e2297fc..77591dc 100644 |
434 | --- a/lib/lp/registry/tests/test_poll.py |
435 | +++ b/lib/lp/registry/tests/test_poll.py |
436 | @@ -7,14 +7,32 @@ from datetime import ( |
437 | datetime, |
438 | timedelta, |
439 | ) |
440 | +from operator import attrgetter |
441 | |
442 | import pytz |
443 | +from testtools.matchers import ( |
444 | + ContainsDict, |
445 | + Equals, |
446 | + MatchesListwise, |
447 | + ) |
448 | +from zope.component import getUtility |
449 | |
450 | +from lp.registry.interfaces.poll import ( |
451 | + IPollSet, |
452 | + PollSecrecy, |
453 | + ) |
454 | +from lp.registry.model.poll import Poll |
455 | +from lp.services.database.interfaces import IStore |
456 | +from lp.services.webapp.interfaces import OAuthPermission |
457 | from lp.testing import ( |
458 | + api_url, |
459 | login, |
460 | + login_person, |
461 | + logout, |
462 | TestCaseWithFactory, |
463 | ) |
464 | from lp.testing.layers import LaunchpadFunctionalLayer |
465 | +from lp.testing.pages import webservice_for_person |
466 | |
467 | |
468 | class TestPoll(TestCaseWithFactory): |
469 | @@ -31,3 +49,114 @@ class TestPoll(TestCaseWithFactory): |
470 | # Force closing of the poll so that we can call getWinners(). |
471 | poll.datecloses = datetime.now(pytz.UTC) |
472 | self.assertIsNone(poll.getWinners(), poll.getWinners()) |
473 | + |
474 | + |
475 | +class MatchesPollAPI(ContainsDict): |
476 | + |
477 | + def __init__(self, webservice, poll): |
478 | + super(MatchesPollAPI, self).__init__({ |
479 | + "team_link": Equals(webservice.getAbsoluteUrl(api_url(poll.team))), |
480 | + "name": Equals(poll.name), |
481 | + "title": Equals(poll.title), |
482 | + "dateopens": Equals(poll.dateopens.isoformat()), |
483 | + "datecloses": Equals(poll.datecloses.isoformat()), |
484 | + "proposition": Equals(poll.proposition), |
485 | + "type": Equals(poll.type.title), |
486 | + "allowspoilt": Equals(poll.allowspoilt), |
487 | + "secrecy": Equals(poll.secrecy.title), |
488 | + }) |
489 | + |
490 | + |
491 | +class TestPollWebservice(TestCaseWithFactory): |
492 | + layer = LaunchpadFunctionalLayer |
493 | + |
494 | + def setUp(self): |
495 | + super(TestPollWebservice, self).setUp() |
496 | + self.person = self.factory.makePerson() |
497 | + self.pushConfig("launchpad", default_batch_size=50) |
498 | + |
499 | + def makePolls(self): |
500 | + teams = [self.factory.makeTeam() for _ in range(3)] |
501 | + polls = [] |
502 | + for team in teams: |
503 | + for offset in (-8, -1, 1): |
504 | + dateopens = datetime.now(pytz.UTC) + timedelta(days=offset) |
505 | + datecloses = dateopens + timedelta(days=7) |
506 | + polls.append(getUtility(IPollSet).new( |
507 | + team=team, name=self.factory.getUniqueUnicode(), |
508 | + title=self.factory.getUniqueUnicode(), |
509 | + proposition=self.factory.getUniqueUnicode(), |
510 | + dateopens=dateopens, datecloses=datecloses, |
511 | + secrecy=PollSecrecy.SECRET, allowspoilt=True, |
512 | + check_permissions=False)) |
513 | + return polls |
514 | + |
515 | + def test_find_all(self): |
516 | + polls = list(IStore(Poll).find(Poll)) + self.makePolls() |
517 | + webservice = webservice_for_person( |
518 | + self.person, permission=OAuthPermission.READ_PUBLIC) |
519 | + webservice.default_api_version = "devel" |
520 | + logout() |
521 | + response = webservice.named_get("/+polls", "find") |
522 | + login_person(self.person) |
523 | + self.assertEqual(200, response.status) |
524 | + self.assertThat(response.jsonBody()["entries"], MatchesListwise([ |
525 | + MatchesPollAPI(webservice, poll) |
526 | + for poll in sorted(polls, key=attrgetter("id"), reverse=True) |
527 | + ])) |
528 | + |
529 | + def test_find_all_ordered(self): |
530 | + polls = list(IStore(Poll).find(Poll)) + self.makePolls() |
531 | + webservice = webservice_for_person( |
532 | + self.person, permission=OAuthPermission.READ_PUBLIC) |
533 | + webservice.default_api_version = "devel" |
534 | + logout() |
535 | + response = webservice.named_get( |
536 | + "/+polls", "find", order_by="by opening date") |
537 | + login_person(self.person) |
538 | + self.assertEqual(200, response.status) |
539 | + self.assertThat(response.jsonBody()["entries"], MatchesListwise([ |
540 | + MatchesPollAPI(webservice, poll) |
541 | + for poll in sorted(polls, key=attrgetter("dateopens", "id")) |
542 | + ])) |
543 | + |
544 | + def test_find_by_team(self): |
545 | + polls = self.makePolls() |
546 | + team_url = api_url(polls[0].team) |
547 | + webservice = webservice_for_person( |
548 | + self.person, permission=OAuthPermission.READ_PUBLIC) |
549 | + webservice.default_api_version = "devel" |
550 | + logout() |
551 | + response = webservice.named_get("/+polls", "find", team=team_url) |
552 | + login_person(self.person) |
553 | + self.assertEqual(200, response.status) |
554 | + self.assertThat(response.jsonBody()["entries"], MatchesListwise([ |
555 | + MatchesPollAPI(webservice, poll) |
556 | + for poll in sorted(polls[:3], key=attrgetter("id"), reverse=True) |
557 | + ])) |
558 | + |
559 | + def test_find_by_team_and_status(self): |
560 | + polls = self.makePolls() |
561 | + team_url = api_url(polls[0].team) |
562 | + webservice = webservice_for_person( |
563 | + self.person, permission=OAuthPermission.READ_PUBLIC) |
564 | + webservice.default_api_version = "devel" |
565 | + logout() |
566 | + response = webservice.named_get( |
567 | + "/+polls", "find", team=team_url, |
568 | + status=["open", "not-yet-opened"]) |
569 | + login_person(self.person) |
570 | + self.assertEqual(200, response.status) |
571 | + self.assertThat(response.jsonBody()["entries"], MatchesListwise([ |
572 | + MatchesPollAPI(webservice, poll) |
573 | + for poll in sorted(polls[1:3], key=attrgetter("id"), reverse=True) |
574 | + ])) |
575 | + logout() |
576 | + response = webservice.named_get( |
577 | + "/+polls", "find", team=team_url, status=["closed", "open"]) |
578 | + login_person(self.person) |
579 | + self.assertEqual(200, response.status) |
580 | + self.assertThat(response.jsonBody()["entries"], MatchesListwise([ |
581 | + MatchesPollAPI(webservice, poll) |
582 | + for poll in sorted(polls[:2], key=attrgetter("id"), reverse=True) |
583 | + ])) |
584 | diff --git a/lib/lp/services/webservice/wadl-to-refhtml.xsl b/lib/lp/services/webservice/wadl-to-refhtml.xsl |
585 | index 331f09b..ee5e2e6 100644 |
586 | --- a/lib/lp/services/webservice/wadl-to-refhtml.xsl |
587 | +++ b/lib/lp/services/webservice/wadl-to-refhtml.xsl |
588 | @@ -475,6 +475,12 @@ |
589 | <xsl:when test="@id = 'pillars'"> |
590 | <xsl:text>/pillars</xsl:text> |
591 | </xsl:when> |
592 | + <xsl:when test="@id = 'poll'"> |
593 | + <xsl:text>/~</xsl:text> |
594 | + <var><team.name></var> |
595 | + <xsl:text>/+poll/</xsl:text> |
596 | + <var><poll.name></var> |
597 | + </xsl:when> |
598 | <xsl:when test="@id = 'processor'"> |
599 | <xsl:text>/+processors/</xsl:text> |
600 | <var><processor.name></var> |
601 | @@ -732,6 +738,9 @@ |
602 | <xsl:template name="find-root-object-uri"> |
603 | <xsl:value-of select="$base"/> |
604 | <xsl:choose> |
605 | + <xsl:when test="@id = 'polls'"> |
606 | + <xsl:text>/+polls</xsl:text> |
607 | + </xsl:when> |
608 | <xsl:when test="@id = 'snap_bases'"> |
609 | <xsl:text>/+snap-bases</xsl:text> |
610 | </xsl:when> |