Merge ~cjwatson/launchpad:export-polls into launchpad: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)
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
diff --git a/lib/lp/app/browser/launchpad.py b/lib/lp/app/browser/launchpad.py
index 7388562..ebeb6d5 100644
--- a/lib/lp/app/browser/launchpad.py
+++ b/lib/lp/app/browser/launchpad.py
@@ -114,6 +114,7 @@ from lp.registry.interfaces.karma import IKarmaActionSet
114from lp.registry.interfaces.nameblacklist import INameBlacklistSet114from lp.registry.interfaces.nameblacklist import INameBlacklistSet
115from lp.registry.interfaces.person import IPersonSet115from lp.registry.interfaces.person import IPersonSet
116from lp.registry.interfaces.pillar import IPillarNameSet116from lp.registry.interfaces.pillar import IPillarNameSet
117from lp.registry.interfaces.poll import IPollSet
117from lp.registry.interfaces.product import (118from lp.registry.interfaces.product import (
118 InvalidProductName,119 InvalidProductName,
119 IProduct,120 IProduct,
@@ -867,6 +868,7 @@ class LaunchpadRootNavigation(Navigation):
867 'package-sets': IPackagesetSet,868 'package-sets': IPackagesetSet,
868 'people': IPersonSet,869 'people': IPersonSet,
869 'pillars': IPillarNameSet,870 'pillars': IPillarNameSet,
871 '+polls': IPollSet,
870 '+processors': IProcessorSet,872 '+processors': IProcessorSet,
871 'projects': IProductSet,873 'projects': IProductSet,
872 'projectgroups': IProjectGroupSet,874 'projectgroups': IProjectGroupSet,
diff --git a/lib/lp/registry/adapters.py b/lib/lp/registry/adapters.py
index e5016c4..beba4a7 100644
--- a/lib/lp/registry/adapters.py
+++ b/lib/lp/registry/adapters.py
@@ -18,6 +18,7 @@ from zope.component.interfaces import ComponentLookupError
18from zope.interface import implementer18from zope.interface import implementer
1919
20from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet20from lp.archivepublisher.interfaces.publisherconfig import IPublisherConfigSet
21from lp.registry.enums import PollSort
21from lp.registry.interfaces.poll import (22from lp.registry.interfaces.poll import (
22 IPollSet,23 IPollSet,
23 IPollSubset,24 IPollSubset,
@@ -96,14 +97,16 @@ class PollSubset:
96 assert self.team is not None, (97 assert self.team is not None, (
97 'team cannot be None to call this method.')98 'team cannot be None to call this method.')
98 return getUtility(IPollSet).findByTeam(99 return getUtility(IPollSet).findByTeam(
99 self.team, [PollStatus.OPEN], order_by='datecloses', when=when)100 self.team, [PollStatus.OPEN],
101 order_by=PollSort.CLOSING, when=when)
100102
101 def getClosedPolls(self, when=None):103 def getClosedPolls(self, when=None):
102 """See IPollSubset."""104 """See IPollSubset."""
103 assert self.team is not None, (105 assert self.team is not None, (
104 'team cannot be None to call this method.')106 'team cannot be None to call this method.')
105 return getUtility(IPollSet).findByTeam(107 return getUtility(IPollSet).findByTeam(
106 self.team, [PollStatus.CLOSED], order_by='datecloses', when=when)108 self.team, [PollStatus.CLOSED],
109 order_by=PollSort.CLOSING, when=when)
107110
108 def getNotYetOpenedPolls(self, when=None):111 def getNotYetOpenedPolls(self, when=None):
109 """See IPollSubset."""112 """See IPollSubset."""
@@ -111,7 +114,7 @@ class PollSubset:
111 'team cannot be None to call this method.')114 'team cannot be None to call this method.')
112 return getUtility(IPollSet).findByTeam(115 return getUtility(IPollSet).findByTeam(
113 self.team, [PollStatus.NOT_YET_OPENED],116 self.team, [PollStatus.NOT_YET_OPENED],
114 order_by='dateopens', when=when)117 order_by=PollSort.OPENING, when=when)
115118
116119
117def productseries_to_product(productseries):120def productseries_to_product(productseries):
diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
index cdaa3da..8895a76 100644
--- a/lib/lp/registry/browser/configure.zcml
+++ b/lib/lp/registry/browser/configure.zcml
@@ -757,6 +757,11 @@
757 path_expression="string:+poll/${name}"757 path_expression="string:+poll/${name}"
758 attribute_to_parent="team"758 attribute_to_parent="team"
759 />759 />
760 <browser:url
761 for="lp.registry.interfaces.poll.IPollSet"
762 path_expression="string:+polls"
763 parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot"
764 />
760 <browser:pages765 <browser:pages
761 for="lp.registry.interfaces.poll.IPoll"766 for="lp.registry.interfaces.poll.IPoll"
762 class="lp.registry.browser.poll.PollView"767 class="lp.registry.browser.poll.PollView"
diff --git a/lib/lp/registry/enums.py b/lib/lp/registry/enums.py
index 0ca6af9..1e99d85 100644
--- a/lib/lp/registry/enums.py
+++ b/lib/lp/registry/enums.py
@@ -14,6 +14,7 @@ __all__ = [
14 'INCLUSIVE_TEAM_POLICY',14 'INCLUSIVE_TEAM_POLICY',
15 'PersonTransferJobType',15 'PersonTransferJobType',
16 'PersonVisibility',16 'PersonVisibility',
17 'PollSort',
17 'ProductJobType',18 'ProductJobType',
18 'VCSType',19 'VCSType',
19 'SharingPermission',20 'SharingPermission',
@@ -25,6 +26,8 @@ __all__ = [
25from lazr.enum import (26from lazr.enum import (
26 DBEnumeratedType,27 DBEnumeratedType,
27 DBItem,28 DBItem,
29 EnumeratedType,
30 Item,
28 )31 )
2932
3033
@@ -461,3 +464,31 @@ class DistributionDefaultTraversalPolicy(DBEnumeratedType):
461 The default traversal from a distribution is used for OCI projects464 The default traversal from a distribution is used for OCI projects
462 in that distribution.465 in that distribution.
463 """)466 """)
467
468
469class PollSort(EnumeratedType):
470 """Choices for how to sort polls."""
471
472 OLDEST_FIRST = Item("""
473 oldest first
474
475 Sort polls from oldest to newest.
476 """)
477
478 NEWEST_FIRST = Item("""
479 newest first
480
481 Sort polls from newest to oldest.
482 """)
483
484 OPENING = Item("""
485 by opening date
486
487 Sort polls with the earliest opening date first.
488 """)
489
490 CLOSING = Item("""
491 by closing date
492
493 Sort polls with the earliest closing date first.
494 """)
diff --git a/lib/lp/registry/interfaces/poll.py b/lib/lp/registry/interfaces/poll.py
index 6b894a4..f73aeb8 100644
--- a/lib/lp/registry/interfaces/poll.py
+++ b/lib/lp/registry/interfaces/poll.py
@@ -27,7 +27,18 @@ from lazr.enum import (
27 DBEnumeratedType,27 DBEnumeratedType,
28 DBItem,28 DBItem,
29 )29 )
30from lazr.restful.declarations import error_status30from lazr.restful.declarations import (
31 collection_default_content,
32 error_status,
33 export_read_operation,
34 exported,
35 exported_as_webservice_collection,
36 exported_as_webservice_entry,
37 operation_for_version,
38 operation_parameters,
39 operation_returns_collection_of,
40 )
41from lazr.restful.fields import Reference
31import pytz42import pytz
32from six.moves import http_client43from six.moves import http_client
33from zope.component import getUtility44from zope.component import getUtility
@@ -42,12 +53,14 @@ from zope.schema import (
42 Choice,53 Choice,
43 Datetime,54 Datetime,
44 Int,55 Int,
56 List,
45 Text,57 Text,
46 TextLine,58 TextLine,
47 )59 )
4860
49from lp import _61from lp import _
50from lp.app.validators.name import name_validator62from lp.app.validators.name import name_validator
63from lp.registry.enums import PollSort
51from lp.registry.interfaces.person import ITeam64from lp.registry.interfaces.person import ITeam
52from lp.services.fields import ContentNameField65from lp.services.fields import ContentNameField
5366
@@ -120,53 +133,55 @@ class CannotCreatePoll(Exception):
120 pass133 pass
121134
122135
136@exported_as_webservice_entry(as_of="beta")
123class IPoll(Interface):137class IPoll(Interface):
124 """A poll for a given proposition in a team."""138 """A poll for a given proposition in a team."""
125139
126 id = Int(title=_('The unique ID'), required=True, readonly=True)140 id = Int(title=_('The unique ID'), required=True, readonly=True)
127141
128 team = Int(142 team = exported(Reference(
143 ITeam,
129 title=_('The team that this poll refers to.'), required=True,144 title=_('The team that this poll refers to.'), required=True,
130 readonly=True)145 readonly=True))
131146
132 name = PollNameField(147 name = exported(PollNameField(
133 title=_('The unique name of this poll'),148 title=_('The unique name of this poll'),
134 description=_('A short unique name, beginning with a lower-case '149 description=_('A short unique name, beginning with a lower-case '
135 'letter or number, and containing only letters, '150 'letter or number, and containing only letters, '
136 'numbers, dots, hyphens, or plus signs.'),151 'numbers, dots, hyphens, or plus signs.'),
137 required=True, readonly=False, constraint=name_validator)152 required=True, readonly=False, constraint=name_validator))
138153
139 title = TextLine(154 title = exported(TextLine(
140 title=_('The title of this poll'), required=True, readonly=False)155 title=_('The title of this poll'), required=True, readonly=False))
141156
142 dateopens = Datetime(157 dateopens = exported(Datetime(
143 title=_('The date and time when this poll opens'), required=True,158 title=_('The date and time when this poll opens'), required=True,
144 readonly=False)159 readonly=False))
145160
146 datecloses = Datetime(161 datecloses = exported(Datetime(
147 title=_('The date and time when this poll closes'), required=True,162 title=_('The date and time when this poll closes'), required=True,
148 readonly=False)163 readonly=False))
149164
150 proposition = Text(165 proposition = exported(Text(
151 title=_('The proposition that is going to be voted'), required=True,166 title=_('The proposition that is going to be voted'), required=True,
152 readonly=False)167 readonly=False))
153168
154 type = Choice(169 type = exported(Choice(
155 title=_('The type of this poll'), required=True,170 title=_('The type of this poll'), required=True,
156 readonly=False, vocabulary=PollAlgorithm,171 readonly=False, vocabulary=PollAlgorithm,
157 default=PollAlgorithm.CONDORCET)172 default=PollAlgorithm.CONDORCET))
158173
159 allowspoilt = Bool(174 allowspoilt = exported(Bool(
160 title=_('Users can spoil their votes?'),175 title=_('Users can spoil their votes?'),
161 description=_(176 description=_(
162 'Allow users to leave the ballot blank (i.e. cast a vote for '177 'Allow users to leave the ballot blank (i.e. cast a vote for '
163 '"None of the above")'),178 '"None of the above")'),
164 required=True, readonly=False, default=True)179 required=True, readonly=False, default=True))
165180
166 secrecy = Choice(181 secrecy = exported(Choice(
167 title=_('The secrecy of the Poll'), required=True,182 title=_('The secrecy of the Poll'), required=True,
168 readonly=False, vocabulary=PollSecrecy,183 readonly=False, vocabulary=PollSecrecy,
169 default=PollSecrecy.SECRET)184 default=PollSecrecy.SECRET))
170185
171 @invariant186 @invariant
172 def saneDates(poll):187 def saneDates(poll):
@@ -297,6 +312,7 @@ class IPoll(Interface):
297 """312 """
298313
299314
315@exported_as_webservice_collection(IPoll)
300class IPollSet(Interface):316class IPollSet(Interface):
301 """The set of Poll objects."""317 """The set of Poll objects."""
302318
@@ -304,19 +320,44 @@ class IPollSet(Interface):
304 secrecy, allowspoilt, poll_type=PollAlgorithm.SIMPLE):320 secrecy, allowspoilt, poll_type=PollAlgorithm.SIMPLE):
305 """Create a new Poll for the given team."""321 """Create a new Poll for the given team."""
306322
307 def findByTeam(team, status=PollStatus.ALL, order_by=None, when=None):323 @operation_parameters(
308 """Return all Polls for the given team, filtered by status.324 team=Reference(ITeam, title=_("Team"), required=False),
309325 status=List(
310 :status: is a sequence containing as many values as you want from326 title=_("Poll statuses"),
311 PollStatus.327 description=_(
328 "A list of one or more of 'open', 'closed', or "
329 "'not-yet-opened'. Defaults to all statuses."),
330 value_type=Choice(values=PollStatus.ALL), min_length=1,
331 required=False),
332 order_by=Choice(
333 title=_("Sort order"), vocabulary=PollSort, required=False))
334 @operation_returns_collection_of(IPoll)
335 @export_read_operation()
336 @operation_for_version("devel")
337 def find(team=None, status=None, order_by=PollSort.NEWEST_FIRST,
338 when=None):
339 """Search for polls.
340
341 :param team: An optional `ITeam` to filter by.
342 :param status: A collection containing as many values as you want
343 from PollStatus. Defaults to `PollStatus.ALL`.
344 :param order_by: An optional `PollSort` item indicating how to sort
345 the results. Defaults to `PollSort.NEWEST_FIRST`.
346 :param when: Used only by tests, to filter for polls open at a
347 specific date.
348 """
312349
313 :order_by: can be either a string with the column name you want to350 def findByTeam(team, status=None, order_by=PollSort.NEWEST_FIRST,
314 sort or a list of column names as strings.351 when=None):
315 If no order_by is specified the results will be ordered using the352 """Return all Polls for the given team, filtered by status.
316 default ordering specified in Poll.sortingColumns.
317353
318 The optional :when argument is used only by our tests, to test if the354 :param team: A `ITeam` to filter by.
319 poll is/was/will-be open at a specific date.355 :param status: A collection containing as many values as you want
356 from PollStatus. Defaults to `PollStatus.ALL`.
357 :param order_by: An optional `PollSort` item indicating how to sort
358 the results. Defaults to `PollSort.NEWEST_FIRST`.
359 :param when: Used only by tests, to filter for polls open at a
360 specific date.
320 """361 """
321362
322 def getByTeamAndName(team, name, default=None):363 def getByTeamAndName(team, name, default=None):
@@ -325,6 +366,13 @@ class IPollSet(Interface):
325 Return :default if there's no Poll with this name for that team.366 Return :default if there's no Poll with this name for that team.
326 """367 """
327368
369 @collection_default_content()
370 def emptyList():
371 """Return an empty collection of polls.
372
373 This only exists to keep lazr.restful happy.
374 """
375
328376
329class IPollSubset(Interface):377class IPollSubset(Interface):
330 """The set of Poll objects for a given team."""378 """The set of Poll objects for a given team."""
diff --git a/lib/lp/registry/interfaces/webservice.py b/lib/lp/registry/interfaces/webservice.py
index aab6c21..c27edda 100644
--- a/lib/lp/registry/interfaces/webservice.py
+++ b/lib/lp/registry/interfaces/webservice.py
@@ -22,6 +22,8 @@ __all__ = [
22 'IPersonSet',22 'IPersonSet',
23 'IPillar',23 'IPillar',
24 'IPillarNameSet',24 'IPillarNameSet',
25 'IPoll',
26 'IPollSet',
25 'IProduct',27 'IProduct',
26 'IProductRelease',28 'IProductRelease',
27 'IProductReleaseFile',29 'IProductReleaseFile',
@@ -83,6 +85,10 @@ from lp.registry.interfaces.pillar import (
83 IPillar,85 IPillar,
84 IPillarNameSet,86 IPillarNameSet,
85 )87 )
88from lp.registry.interfaces.poll import (
89 IPoll,
90 IPollSet,
91 )
86from lp.registry.interfaces.product import (92from lp.registry.interfaces.product import (
87 IProduct,93 IProduct,
88 IProductSet,94 IProductSet,
diff --git a/lib/lp/registry/model/poll.py b/lib/lp/registry/model/poll.py
index 419a620..a4fe45d 100644
--- a/lib/lp/registry/model/poll.py
+++ b/lib/lp/registry/model/poll.py
@@ -22,6 +22,7 @@ from storm.locals import (
22 And,22 And,
23 Bool,23 Bool,
24 DateTime,24 DateTime,
25 Desc,
25 Int,26 Int,
26 Or,27 Or,
27 Reference,28 Reference,
@@ -31,6 +32,7 @@ from storm.locals import (
31from zope.component import getUtility32from zope.component import getUtility
32from zope.interface import implementer33from zope.interface import implementer
3334
35from lp.registry.enums import PollSort
34from lp.registry.interfaces.person import validate_public_person36from lp.registry.interfaces.person import validate_public_person
35from lp.registry.interfaces.poll import (37from lp.registry.interfaces.poll import (
36 CannotCreatePoll,38 CannotCreatePoll,
@@ -309,15 +311,27 @@ class PollSet:
309 IStore(Poll).add(poll)311 IStore(Poll).add(poll)
310 return poll312 return poll
311313
312 def findByTeam(self, team, status=PollStatus.ALL, order_by=None,314 @staticmethod
313 when=None):315 def _convertPollSortToOrderBy(sort_by):
316 """Compute a value to pass to `order_by` on a poll collection.
317
318 :param sort_by: An item from the `PollSort` enumeration.
319 """
320 return {
321 PollSort.OLDEST_FIRST: [Poll.id],
322 PollSort.NEWEST_FIRST: [Desc(Poll.id)],
323 PollSort.OPENING: [Poll.dateopens, Poll.id],
324 PollSort.CLOSING: [Poll.datecloses, Poll.id],
325 }[sort_by]
326
327 def find(self, team=None, status=None, order_by=PollSort.NEWEST_FIRST,
328 when=None):
314 """See IPollSet."""329 """See IPollSet."""
330 if status is None:
331 status = PollStatus.ALL
315 if when is None:332 if when is None:
316 when = datetime.now(pytz.timezone('UTC'))333 when = datetime.now(pytz.timezone('UTC'))
317334
318 if order_by is None:
319 order_by = Poll.sortingColumns
320
321 status = set(status)335 status = set(status)
322 status_clauses = []336 status_clauses = []
323 if PollStatus.OPEN in status:337 if PollStatus.OPEN in status:
@@ -330,16 +344,30 @@ class PollSet:
330344
331 assert len(status_clauses) > 0, "No poll statuses were selected"345 assert len(status_clauses) > 0, "No poll statuses were selected"
332346
333 results = IStore(Poll).find(347 clauses = []
334 Poll, Poll.team == team, Or(*status_clauses))348 if team is not None:
349 clauses.append(Poll.team == team)
350 clauses.append(Or(*status_clauses))
351
352 results = IStore(Poll).find(Poll, *clauses)
335353
336 return results.order_by(order_by)354 return results.order_by(self._convertPollSortToOrderBy(order_by))
355
356 def findByTeam(self, team, status=None, order_by=PollSort.NEWEST_FIRST,
357 when=None):
358 """See IPollSet."""
359 return self.find(
360 team=team, status=status, order_by=order_by, when=when)
337361
338 def getByTeamAndName(self, team, name, default=None):362 def getByTeamAndName(self, team, name, default=None):
339 """See IPollSet."""363 """See IPollSet."""
340 poll = IStore(Poll).find(Poll, team=team, name=name).one()364 poll = IStore(Poll).find(Poll, team=team, name=name).one()
341 return poll if poll is not None else default365 return poll if poll is not None else default
342366
367 def emptyList(self):
368 """See IPollSet."""
369 return []
370
343371
344@implementer(IPollOption)372@implementer(IPollOption)
345class PollOption(StormBase):373class PollOption(StormBase):
diff --git a/lib/lp/registry/tests/test_poll.py b/lib/lp/registry/tests/test_poll.py
index e2297fc..77591dc 100644
--- a/lib/lp/registry/tests/test_poll.py
+++ b/lib/lp/registry/tests/test_poll.py
@@ -7,14 +7,32 @@ from datetime import (
7 datetime,7 datetime,
8 timedelta,8 timedelta,
9 )9 )
10from operator import attrgetter
1011
11import pytz12import pytz
13from testtools.matchers import (
14 ContainsDict,
15 Equals,
16 MatchesListwise,
17 )
18from zope.component import getUtility
1219
20from lp.registry.interfaces.poll import (
21 IPollSet,
22 PollSecrecy,
23 )
24from lp.registry.model.poll import Poll
25from lp.services.database.interfaces import IStore
26from lp.services.webapp.interfaces import OAuthPermission
13from lp.testing import (27from lp.testing import (
28 api_url,
14 login,29 login,
30 login_person,
31 logout,
15 TestCaseWithFactory,32 TestCaseWithFactory,
16 )33 )
17from lp.testing.layers import LaunchpadFunctionalLayer34from lp.testing.layers import LaunchpadFunctionalLayer
35from lp.testing.pages import webservice_for_person
1836
1937
20class TestPoll(TestCaseWithFactory):38class TestPoll(TestCaseWithFactory):
@@ -31,3 +49,114 @@ class TestPoll(TestCaseWithFactory):
31 # Force closing of the poll so that we can call getWinners().49 # Force closing of the poll so that we can call getWinners().
32 poll.datecloses = datetime.now(pytz.UTC)50 poll.datecloses = datetime.now(pytz.UTC)
33 self.assertIsNone(poll.getWinners(), poll.getWinners())51 self.assertIsNone(poll.getWinners(), poll.getWinners())
52
53
54class MatchesPollAPI(ContainsDict):
55
56 def __init__(self, webservice, poll):
57 super(MatchesPollAPI, self).__init__({
58 "team_link": Equals(webservice.getAbsoluteUrl(api_url(poll.team))),
59 "name": Equals(poll.name),
60 "title": Equals(poll.title),
61 "dateopens": Equals(poll.dateopens.isoformat()),
62 "datecloses": Equals(poll.datecloses.isoformat()),
63 "proposition": Equals(poll.proposition),
64 "type": Equals(poll.type.title),
65 "allowspoilt": Equals(poll.allowspoilt),
66 "secrecy": Equals(poll.secrecy.title),
67 })
68
69
70class TestPollWebservice(TestCaseWithFactory):
71 layer = LaunchpadFunctionalLayer
72
73 def setUp(self):
74 super(TestPollWebservice, self).setUp()
75 self.person = self.factory.makePerson()
76 self.pushConfig("launchpad", default_batch_size=50)
77
78 def makePolls(self):
79 teams = [self.factory.makeTeam() for _ in range(3)]
80 polls = []
81 for team in teams:
82 for offset in (-8, -1, 1):
83 dateopens = datetime.now(pytz.UTC) + timedelta(days=offset)
84 datecloses = dateopens + timedelta(days=7)
85 polls.append(getUtility(IPollSet).new(
86 team=team, name=self.factory.getUniqueUnicode(),
87 title=self.factory.getUniqueUnicode(),
88 proposition=self.factory.getUniqueUnicode(),
89 dateopens=dateopens, datecloses=datecloses,
90 secrecy=PollSecrecy.SECRET, allowspoilt=True,
91 check_permissions=False))
92 return polls
93
94 def test_find_all(self):
95 polls = list(IStore(Poll).find(Poll)) + self.makePolls()
96 webservice = webservice_for_person(
97 self.person, permission=OAuthPermission.READ_PUBLIC)
98 webservice.default_api_version = "devel"
99 logout()
100 response = webservice.named_get("/+polls", "find")
101 login_person(self.person)
102 self.assertEqual(200, response.status)
103 self.assertThat(response.jsonBody()["entries"], MatchesListwise([
104 MatchesPollAPI(webservice, poll)
105 for poll in sorted(polls, key=attrgetter("id"), reverse=True)
106 ]))
107
108 def test_find_all_ordered(self):
109 polls = list(IStore(Poll).find(Poll)) + self.makePolls()
110 webservice = webservice_for_person(
111 self.person, permission=OAuthPermission.READ_PUBLIC)
112 webservice.default_api_version = "devel"
113 logout()
114 response = webservice.named_get(
115 "/+polls", "find", order_by="by opening date")
116 login_person(self.person)
117 self.assertEqual(200, response.status)
118 self.assertThat(response.jsonBody()["entries"], MatchesListwise([
119 MatchesPollAPI(webservice, poll)
120 for poll in sorted(polls, key=attrgetter("dateopens", "id"))
121 ]))
122
123 def test_find_by_team(self):
124 polls = self.makePolls()
125 team_url = api_url(polls[0].team)
126 webservice = webservice_for_person(
127 self.person, permission=OAuthPermission.READ_PUBLIC)
128 webservice.default_api_version = "devel"
129 logout()
130 response = webservice.named_get("/+polls", "find", team=team_url)
131 login_person(self.person)
132 self.assertEqual(200, response.status)
133 self.assertThat(response.jsonBody()["entries"], MatchesListwise([
134 MatchesPollAPI(webservice, poll)
135 for poll in sorted(polls[:3], key=attrgetter("id"), reverse=True)
136 ]))
137
138 def test_find_by_team_and_status(self):
139 polls = self.makePolls()
140 team_url = api_url(polls[0].team)
141 webservice = webservice_for_person(
142 self.person, permission=OAuthPermission.READ_PUBLIC)
143 webservice.default_api_version = "devel"
144 logout()
145 response = webservice.named_get(
146 "/+polls", "find", team=team_url,
147 status=["open", "not-yet-opened"])
148 login_person(self.person)
149 self.assertEqual(200, response.status)
150 self.assertThat(response.jsonBody()["entries"], MatchesListwise([
151 MatchesPollAPI(webservice, poll)
152 for poll in sorted(polls[1:3], key=attrgetter("id"), reverse=True)
153 ]))
154 logout()
155 response = webservice.named_get(
156 "/+polls", "find", team=team_url, status=["closed", "open"])
157 login_person(self.person)
158 self.assertEqual(200, response.status)
159 self.assertThat(response.jsonBody()["entries"], MatchesListwise([
160 MatchesPollAPI(webservice, poll)
161 for poll in sorted(polls[:2], key=attrgetter("id"), reverse=True)
162 ]))
diff --git a/lib/lp/services/webservice/wadl-to-refhtml.xsl b/lib/lp/services/webservice/wadl-to-refhtml.xsl
index 331f09b..ee5e2e6 100644
--- a/lib/lp/services/webservice/wadl-to-refhtml.xsl
+++ b/lib/lp/services/webservice/wadl-to-refhtml.xsl
@@ -475,6 +475,12 @@
475 <xsl:when test="@id = 'pillars'">475 <xsl:when test="@id = 'pillars'">
476 <xsl:text>/pillars</xsl:text>476 <xsl:text>/pillars</xsl:text>
477 </xsl:when>477 </xsl:when>
478 <xsl:when test="@id = 'poll'">
479 <xsl:text>/~</xsl:text>
480 <var>&lt;team.name&gt;</var>
481 <xsl:text>/+poll/</xsl:text>
482 <var>&lt;poll.name&gt;</var>
483 </xsl:when>
478 <xsl:when test="@id = 'processor'">484 <xsl:when test="@id = 'processor'">
479 <xsl:text>/+processors/</xsl:text>485 <xsl:text>/+processors/</xsl:text>
480 <var>&lt;processor.name&gt;</var>486 <var>&lt;processor.name&gt;</var>
@@ -732,6 +738,9 @@
732 <xsl:template name="find-root-object-uri">738 <xsl:template name="find-root-object-uri">
733 <xsl:value-of select="$base"/>739 <xsl:value-of select="$base"/>
734 <xsl:choose>740 <xsl:choose>
741 <xsl:when test="@id = 'polls'">
742 <xsl:text>/+polls</xsl:text>
743 </xsl:when>
735 <xsl:when test="@id = 'snap_bases'">744 <xsl:when test="@id = 'snap_bases'">
736 <xsl:text>/+snap-bases</xsl:text>745 <xsl:text>/+snap-bases</xsl:text>
737 </xsl:when>746 </xsl:when>

Subscribers

People subscribed via source and target branches

to status/vote changes: