Merge lp:~bac/launchpad/bug-689719 into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 12091
Proposed branch: lp:~bac/launchpad/bug-689719
Merge into: lp:launchpad
Diff against target: 4702 lines (+51/-4129)
43 files modified
lib/canonical/launchpad/browser/__init__.py (+0/-3)
lib/canonical/launchpad/pagetests/basics/notfound-traversals.txt (+0/-1)
lib/canonical/launchpad/pagetitles.py (+0/-14)
lib/canonical/launchpad/security.py (+0/-25)
lib/canonical/launchpad/tests/test_poll.py (+0/-34)
lib/lp/registry/adapters.py (+2/-65)
lib/lp/registry/browser/configure.zcml (+0/-81)
lib/lp/registry/browser/person.py (+1/-50)
lib/lp/registry/browser/poll.py (+0/-469)
lib/lp/registry/browser/tests/poll-views.txt (+0/-134)
lib/lp/registry/browser/tests/poll-views_0.txt (+0/-92)
lib/lp/registry/browser/tests/test_breadcrumbs.py (+0/-20)
lib/lp/registry/browser/tests/test_poll.py (+0/-38)
lib/lp/registry/configure.zcml (+0/-103)
lib/lp/registry/doc/person-merge.txt (+26/-32)
lib/lp/registry/doc/poll-preconditions.txt (+0/-74)
lib/lp/registry/doc/poll.txt (+0/-140)
lib/lp/registry/doc/team-nav-menus.txt (+0/-3)
lib/lp/registry/interfaces/poll.py (+0/-500)
lib/lp/registry/model/person.py (+7/-3)
lib/lp/registry/model/poll.py (+0/-440)
lib/lp/registry/stories/team-polls/create-poll-options.txt (+0/-85)
lib/lp/registry/stories/team-polls/create-polls.txt (+0/-163)
lib/lp/registry/stories/team-polls/edit-options.txt (+0/-59)
lib/lp/registry/stories/team-polls/edit-poll.txt (+0/-97)
lib/lp/registry/stories/team-polls/vote-poll.txt (+0/-167)
lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt (+0/-229)
lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt (+0/-89)
lib/lp/registry/stories/team-polls/xx-poll-results.txt (+0/-69)
lib/lp/registry/stories/team/xx-team-home.txt (+8/-15)
lib/lp/registry/templates/poll-edit.pt (+0/-34)
lib/lp/registry/templates/poll-index.pt (+0/-207)
lib/lp/registry/templates/poll-newoption.pt (+0/-36)
lib/lp/registry/templates/poll-portlet-details.pt (+0/-38)
lib/lp/registry/templates/poll-portlet-options.pt (+0/-46)
lib/lp/registry/templates/poll-vote-condorcet.pt (+0/-130)
lib/lp/registry/templates/poll-vote-simple.pt (+0/-141)
lib/lp/registry/templates/polloption-edit.pt (+0/-37)
lib/lp/registry/templates/team-index.pt (+0/-1)
lib/lp/registry/templates/team-newpoll.pt (+0/-25)
lib/lp/registry/templates/team-polls.pt (+7/-69)
lib/lp/registry/templates/team-portlet-polls.pt (+0/-56)
lib/lp/testing/factory.py (+0/-15)
To merge this branch: bzr merge lp:~bac/launchpad/bug-689719
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+43840@code.launchpad.net

Commit message

[r=sinzui][ui=none][bug=689719] Remove the Polls feature from Launchpad

Description of the change

= Summary =

Remove the polls feature from Launchpad.

== Proposed fix ==

Rip, rip, rip.

== Pre-implementation notes ==

Talks and querying with Curtis regarding existing data.

== Implementation details ==

N/A

== Tests ==

bin/test -vvm lp.registry

== Demo and Q/A ==

http://dev.launchpad.net/~ubuntu-team/+polls

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/adapters.py
  lib/lp/registry/templates/team-index.pt
  lib/lp/registry/browser/tests/test_breadcrumbs.py
  lib/canonical/launchpad/security.py
  lib/canonical/launchpad/pagetitles.py
  lib/lp/registry/templates/team-polls.pt
  lib/lp/registry/configure.zcml
  lib/lp/testing/factory.py
  lib/lp/registry/doc/person-merge.txt
  lib/lp/registry/stories/team/xx-team-home.txt
  lib/lp/registry/browser/person.py
  lib/lp/registry/model/person.py

These are all bogus.

./lib/canonical/launchpad/security.py
     742: E302 expected 2 blank lines, found 1
./lib/canonical/launchpad/pagetitles.py
      58: E302 expected 2 blank lines, found 3
      60: E301 expected 1 blank line, found 0
      69: E301 expected 1 blank line, found 0
      75: E301 expected 1 blank line, found 0
      81: E301 expected 1 blank line, found 0
      87: E301 expected 1 blank line, found 0
     100: E301 expected 1 blank line, found 0
     121: E302 expected 2 blank lines, found 1
     147: E302 expected 2 blank lines, found 1
     151: E302 expected 2 blank lines, found 1
     158: E302 expected 2 blank lines, found 1
     193: E302 expected 2 blank lines, found 1
     202: E302 expected 2 blank lines, found 1
     239: E302 expected 2 blank lines, found 1
     256: E302 expected 2 blank lines, found 1

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

We talked on IRC and agreed that "Conducting polls is no longer a feature in Launchpad" is awkward. We will use "Launchpad no longer support team polls".

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/browser/__init__.py'
--- lib/canonical/launchpad/browser/__init__.py 2010-10-31 20:18:45 +0000
+++ lib/canonical/launchpad/browser/__init__.py 2010-12-16 14:48:27 +0000
@@ -48,7 +48,6 @@
48from lp.registry.browser.mailinglists import *48from lp.registry.browser.mailinglists import *
49from lp.registry.browser.objectreassignment import *49from lp.registry.browser.objectreassignment import *
50from lp.registry.browser.peoplemerge import *50from lp.registry.browser.peoplemerge import *
51from lp.registry.browser.poll import *
52from lp.registry.browser.team import *51from lp.registry.browser.team import *
53from lp.registry.browser.teammembership import *52from lp.registry.browser.teammembership import *
54# XXX flacoste 2009/03/18 We should use specific imports instead of53# XXX flacoste 2009/03/18 We should use specific imports instead of
@@ -63,5 +62,3 @@
63from lp.soyuz.browser.publishing import *62from lp.soyuz.browser.publishing import *
64from lp.soyuz.browser.queue import *63from lp.soyuz.browser.queue import *
65from lp.soyuz.browser.sourcepackagerelease import *64from lp.soyuz.browser.sourcepackagerelease import *
66
67
6865
=== modified file 'lib/canonical/launchpad/pagetests/basics/notfound-traversals.txt'
--- lib/canonical/launchpad/pagetests/basics/notfound-traversals.txt 2010-12-13 18:04:24 +0000
+++ lib/canonical/launchpad/pagetests/basics/notfound-traversals.txt 2010-12-16 14:48:27 +0000
@@ -369,7 +369,6 @@
369>>> check("/~name18/+imports", host='translations.launchpad.dev')369>>> check("/~name18/+imports", host='translations.launchpad.dev')
370>>> check("/~name18/+teamlist")370>>> check("/~name18/+teamlist")
371>>> check("/~name18/+polls")371>>> check("/~name18/+polls")
372>>> check("/~name18/+newpoll", auth=True)
373>>> check("/~name16/+rdf")372>>> check("/~name16/+rdf")
374>>> check("/~name18/+rdf")373>>> check("/~name18/+rdf")
375374
376375
=== modified file 'lib/canonical/launchpad/pagetitles.py'
--- lib/canonical/launchpad/pagetitles.py 2010-11-08 12:52:43 +0000
+++ lib/canonical/launchpad/pagetitles.py 2010-12-16 14:48:27 +0000
@@ -285,20 +285,6 @@
285person_translations_to_review = ContextDisplayName(285person_translations_to_review = ContextDisplayName(
286 'Translations for review by %s')286 'Translations for review by %s')
287287
288poll_edit = ContextTitle(smartquote('Edit poll "%s"'))
289
290poll_index = ContextTitle(smartquote('Poll: "%s"'))
291
292poll_newoption = ContextTitle(smartquote('New option for poll "%s"'))
293
294def polloption_edit(context, view):
295 """Return the page title to edit a poll's option."""
296 return 'Edit option: %s' % context.title
297
298poll_vote_condorcet = ContextTitle(smartquote('Vote in poll "%s"'))
299
300poll_vote_simple = ContextTitle(smartquote('Vote in poll "%s"'))
301
302product_cvereport = ContextTitle('CVE reports for %s')288product_cvereport = ContextTitle('CVE reports for %s')
303289
304product_index = ContextTitle('%s in Launchpad')290product_index = ContextTitle('%s in Launchpad')
305291
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2010-12-14 00:03:18 +0000
+++ lib/canonical/launchpad/security.py 2010-12-16 14:48:27 +0000
@@ -130,11 +130,6 @@
130 PersonVisibility,130 PersonVisibility,
131 )131 )
132from lp.registry.interfaces.pillar import IPillar132from lp.registry.interfaces.pillar import IPillar
133from lp.registry.interfaces.poll import (
134 IPoll,
135 IPollOption,
136 IPollSubset,
137 )
138from lp.registry.interfaces.product import (133from lp.registry.interfaces.product import (
139 IProduct,134 IProduct,
140 IProductSet,135 IProductSet,
@@ -858,26 +853,6 @@
858 return False853 return False
859854
860855
861class EditPollByTeamOwnerOrTeamAdminsOrAdmins(
862 EditTeamMembershipByTeamOwnerOrTeamAdminsOrAdmins):
863 permission = 'launchpad.Edit'
864 usedfor = IPoll
865
866
867class EditPollSubsetByTeamOwnerOrTeamAdminsOrAdmins(
868 EditPollByTeamOwnerOrTeamAdminsOrAdmins):
869 permission = 'launchpad.Edit'
870 usedfor = IPollSubset
871
872
873class EditPollOptionByTeamOwnerOrTeamAdminsOrAdmins(AuthorizationBase):
874 permission = 'launchpad.Edit'
875 usedfor = IPollOption
876
877 def checkAuthenticated(self, user):
878 return can_edit_team(self.obj.poll.team, user)
879
880
881class AdminDistribution(AdminByAdminsTeam):856class AdminDistribution(AdminByAdminsTeam):
882 """Soyuz involves huge chunks of data in the archive and librarian,857 """Soyuz involves huge chunks of data in the archive and librarian,
883 so for the moment we are locking down admin and edit on distributions858 so for the moment we are locking down admin and edit on distributions
884859
=== removed file 'lib/canonical/launchpad/tests/test_poll.py'
--- lib/canonical/launchpad/tests/test_poll.py 2010-10-04 19:50:45 +0000
+++ lib/canonical/launchpad/tests/test_poll.py 1970-01-01 00:00:00 +0000
@@ -1,34 +0,0 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4from datetime import (
5 datetime,
6 timedelta,
7 )
8import unittest
9
10import pytz
11
12from canonical.launchpad.ftests import login
13from canonical.testing.layers import LaunchpadFunctionalLayer
14from lp.testing import TestCaseWithFactory
15
16
17class TestPoll(TestCaseWithFactory):
18 layer = LaunchpadFunctionalLayer
19
20 def test_getWinners_handle_polls_with_only_spoilt_votes(self):
21 login('mark@example.com')
22 owner = self.factory.makePerson()
23 team = self.factory.makeTeam(owner)
24 poll = self.factory.makePoll(team, 'name', 'title', 'proposition')
25 # Force opening of poll so that we can vote.
26 poll.dateopens = datetime.now(pytz.UTC) - timedelta(minutes=2)
27 poll.storeSimpleVote(owner, None)
28 # Force closing of the poll so that we can call getWinners().
29 poll.datecloses = datetime.now(pytz.UTC)
30 self.failUnless(poll.getWinners() is None, poll.getWinners())
31
32
33def test_suite():
34 return unittest.TestLoader().loadTestsFromName(__name__)
350
=== modified file 'lib/lp/registry/adapters.py'
--- lib/lp/registry/adapters.py 2010-09-21 13:39:17 +0000
+++ lib/lp/registry/adapters.py 2010-12-16 14:48:27 +0000
@@ -6,24 +6,15 @@
6__metaclass__ = type6__metaclass__ = type
77
8__all__ = [8__all__ = [
9 'distroseries_to_launchpadusage',9 'distroseries_to_distribution',
10 'distroseries_to_serviceusage',10 'person_from_principal',
11 'PollSubset',
12 'productseries_to_product',11 'productseries_to_product',
13 ]12 ]
1413
1514
16from zope.component import getUtility
17from zope.component.interfaces import ComponentLookupError15from zope.component.interfaces import ComponentLookupError
18from zope.interface import implements
1916
20from canonical.launchpad.webapp.interfaces import ILaunchpadPrincipal17from canonical.launchpad.webapp.interfaces import ILaunchpadPrincipal
21from lp.registry.interfaces.poll import (
22 IPollSet,
23 IPollSubset,
24 PollAlgorithm,
25 PollStatus,
26 )
2718
2819
29def distroseries_to_distribution(distroseries):20def distroseries_to_distribution(distroseries):
@@ -51,60 +42,6 @@
51 raise ComponentLookupError42 raise ComponentLookupError
5243
5344
54class PollSubset:
55 """Adapt an `IPoll` to an `IPollSubset`."""
56 implements(IPollSubset)
57
58 title = 'Team polls'
59
60 def __init__(self, team=None):
61 self.team = team
62
63 def new(self, name, title, proposition, dateopens, datecloses,
64 secrecy, allowspoilt, poll_type=PollAlgorithm.SIMPLE):
65 """See IPollSubset."""
66 assert self.team is not None, (
67 'team cannot be None to call this method.')
68 return getUtility(IPollSet).new(
69 self.team, name, title, proposition, dateopens,
70 datecloses, secrecy, allowspoilt, poll_type)
71
72 def getByName(self, name, default=None):
73 """See IPollSubset."""
74 assert self.team is not None, (
75 'team cannot be None to call this method.')
76 pollset = getUtility(IPollSet)
77 return pollset.getByTeamAndName(self.team, name, default)
78
79 def getAll(self):
80 """See IPollSubset."""
81 assert self.team is not None, (
82 'team cannot be None to call this method.')
83 return getUtility(IPollSet).selectByTeam(self.team)
84
85 def getOpenPolls(self, when=None):
86 """See IPollSubset."""
87 assert self.team is not None, (
88 'team cannot be None to call this method.')
89 return getUtility(IPollSet).selectByTeam(
90 self.team, [PollStatus.OPEN], orderBy='datecloses', when=when)
91
92 def getClosedPolls(self, when=None):
93 """See IPollSubset."""
94 assert self.team is not None, (
95 'team cannot be None to call this method.')
96 return getUtility(IPollSet).selectByTeam(
97 self.team, [PollStatus.CLOSED], orderBy='datecloses', when=when)
98
99 def getNotYetOpenedPolls(self, when=None):
100 """See IPollSubset."""
101 assert self.team is not None, (
102 'team cannot be None to call this method.')
103 return getUtility(IPollSet).selectByTeam(
104 self.team, [PollStatus.NOT_YET_OPENED],
105 orderBy='dateopens', when=when)
106
107
108def productseries_to_product(productseries):45def productseries_to_product(productseries):
109 """Adapts `IProductSeries` object to `IProduct`.46 """Adapts `IProductSeries` object to `IProduct`.
11047
11148
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2010-12-10 23:36:06 +0000
+++ lib/lp/registry/browser/configure.zcml 2010-12-16 14:48:27 +0000
@@ -600,75 +600,6 @@
600 name="karmacontext-macros"600 name="karmacontext-macros"
601 template="../templates/karmacontext-macros.pt"/>601 template="../templates/karmacontext-macros.pt"/>
602 </facet>602 </facet>
603 <facet
604 facet="overview">
605 <browser:menus
606 module="lp.registry.browser.poll"
607 classes="
608 PollOverviewMenu
609 PollActionNavigationMenu
610 PollEditNavigationMenu"/>
611 <browser:defaultView
612 for="lp.registry.interfaces.poll.IPoll"
613 name="+index"/>
614 <browser:navigation
615 module="lp.registry.browser.poll"
616 classes="
617 PollNavigation"/>
618 <browser:url
619 for="lp.registry.interfaces.poll.IPoll"
620 path_expression="string:+poll/${name}"
621 attribute_to_parent="team"/>
622 <browser:pages
623 for="lp.registry.interfaces.poll.IPoll"
624 permission="zope.Public"
625 class="lp.registry.browser.poll.PollView">
626 <browser:page
627 name="+index"
628 template="../templates/poll-index.pt"/>
629 </browser:pages>
630 <browser:pages
631 for="lp.registry.interfaces.poll.IPoll"
632 permission="zope.Public"
633 class="lp.registry.browser.poll.BasePollView">
634 <browser:page
635 name="+portlet-details"
636 template="../templates/poll-portlet-details.pt"/>
637 <browser:page
638 name="+portlet-options"
639 template="../templates/poll-portlet-options.pt"/>
640 </browser:pages>
641 <browser:page
642 name="+vote"
643 for="lp.registry.interfaces.poll.IPoll"
644 permission="launchpad.AnyPerson"
645 class="lp.registry.browser.poll.PollVoteView"/>
646 <browser:page
647 name="+edit"
648 for="lp.registry.interfaces.poll.IPoll"
649 class="lp.registry.browser.poll.PollEditView"
650 permission="launchpad.Edit"
651 template="../templates/poll-edit.pt"/>
652 <browser:page
653 name="+newoption"
654 for="lp.registry.interfaces.poll.IPoll"
655 class="lp.registry.browser.poll.PollOptionAddView"
656 permission="launchpad.Edit"
657 template="../templates/poll-newoption.pt"/>
658 <browser:defaultView
659 for="lp.registry.interfaces.poll.IPollOption"
660 name="+edit"/>
661 <browser:url
662 for="lp.registry.interfaces.poll.IPollOption"
663 path_expression="string:+option/${id}"
664 attribute_to_parent="poll"/>
665 <browser:page
666 name="+edit"
667 for="lp.registry.interfaces.poll.IPollOption"
668 class="lp.registry.browser.poll.PollOptionEditView"
669 permission="launchpad.Edit"
670 template="../templates/polloption-edit.pt"/>
671 </facet>
672 <browser:url603 <browser:url
673 for="lp.registry.interfaces.announcement.IAnnouncement"604 for="lp.registry.interfaces.announcement.IAnnouncement"
674 path_expression="string:+announcement/${id}"605 path_expression="string:+announcement/${id}"
@@ -1080,12 +1011,6 @@
1080 template="../templates/team-index.pt"/>1011 template="../templates/team-index.pt"/>
1081 <browser:page1012 <browser:page
1082 for="lp.registry.interfaces.person.ITeam"1013 for="lp.registry.interfaces.person.ITeam"
1083 class="lp.registry.browser.person.TeamIndexView"
1084 permission="zope.Public"
1085 name="+portlet-polls"
1086 template="../templates/team-portlet-polls.pt"/>
1087 <browser:page
1088 for="lp.registry.interfaces.person.ITeam"
1089 permission="zope.Public"1014 permission="zope.Public"
1090 class="lp.registry.browser.team.TeamMapView"1015 class="lp.registry.browser.team.TeamMapView"
1091 name="+map"1016 name="+map"
@@ -1198,12 +1123,6 @@
1198 name="+polls"1123 name="+polls"
1199 template="../templates/team-polls.pt"/>1124 template="../templates/team-polls.pt"/>
1200 <browser:page1125 <browser:page
1201 name="+newpoll"
1202 for="lp.registry.interfaces.person.ITeam"
1203 class="canonical.launchpad.browser.PollAddView"
1204 permission="launchpad.Edit"
1205 template="../templates/team-newpoll.pt"/>
1206 <browser:page
1207 name="+members"1126 name="+members"
1208 for="lp.registry.interfaces.person.ITeam"1127 for="lp.registry.interfaces.person.ITeam"
1209 permission="zope.Public"1128 permission="zope.Public"
12101129
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-12-10 23:10:09 +0000
+++ lib/lp/registry/browser/person.py 2010-12-16 14:48:27 +0000
@@ -284,10 +284,6 @@
284 )284 )
285from lp.registry.interfaces.personproduct import IPersonProductFactory285from lp.registry.interfaces.personproduct import IPersonProductFactory
286from lp.registry.interfaces.pillar import IPillarNameSet286from lp.registry.interfaces.pillar import IPillarNameSet
287from lp.registry.interfaces.poll import (
288 IPollSet,
289 IPollSubset,
290 )
291from lp.registry.interfaces.product import IProduct287from lp.registry.interfaces.product import IProduct
292from lp.registry.interfaces.ssh import (288from lp.registry.interfaces.ssh import (
293 ISSHKeySet,289 ISSHKeySet,
@@ -562,10 +558,6 @@
562558
563 usedfor = ITeam559 usedfor = ITeam
564560
565 @stepthrough('+poll')
566 def traverse_poll(self, name):
567 return getUtility(IPollSet).getByTeamAndName(self.context, name)
568
569 @stepthrough('+invitation')561 @stepthrough('+invitation')
570 def traverse_invitation(self, name):562 def traverse_invitation(self, name):
571 # Return the found membership regardless of its status as we know563 # Return the found membership regardless of its status as we know
@@ -1334,17 +1326,6 @@
1334 text = 'Show member photos'1326 text = 'Show member photos'
1335 return Link(target, text, icon='team')1327 return Link(target, text, icon='team')
13361328
1337 def polls(self):
1338 target = '+polls'
1339 text = 'Show polls'
1340 return Link(target, text, icon='info')
1341
1342 @enabled_with_permission('launchpad.Edit')
1343 def add_poll(self):
1344 target = '+newpoll'
1345 text = 'Create a poll'
1346 return Link(target, text, icon='add')
1347
1348 @enabled_with_permission('launchpad.Edit')1329 @enabled_with_permission('launchpad.Edit')
1349 def editemail(self):1330 def editemail(self):
1350 target = '+contactaddress'1331 target = '+contactaddress'
@@ -1429,8 +1410,6 @@
1429 'moderate_mailing_list',1410 'moderate_mailing_list',
1430 'editlanguages',1411 'editlanguages',
1431 'map',1412 'map',
1432 'polls',
1433 'add_poll',
1434 'join',1413 'join',
1435 'leave',1414 'leave',
1436 'add_my_teams',1415 'add_my_teams',
@@ -1451,7 +1430,7 @@
14511430
1452 usedfor = ITeam1431 usedfor = ITeam
1453 facet = 'overview'1432 facet = 'overview'
1454 links = ['profile', 'polls', 'members', 'ppas']1433 links = ['profile', 'members', 'ppas']
14551434
14561435
1457class TeamMembershipView(LaunchpadView):1436class TeamMembershipView(LaunchpadView):
@@ -2941,21 +2920,6 @@
2941 return ''2920 return ''
29422921
2943 @cachedproperty2922 @cachedproperty
2944 def openpolls(self):
2945 assert self.context.isTeam()
2946 return IPollSubset(self.context).getOpenPolls()
2947
2948 @cachedproperty
2949 def closedpolls(self):
2950 assert self.context.isTeam()
2951 return IPollSubset(self.context).getClosedPolls()
2952
2953 @cachedproperty
2954 def notyetopenedpolls(self):
2955 assert self.context.isTeam()
2956 return IPollSubset(self.context).getNotYetOpenedPolls()
2957
2958 @cachedproperty
2959 def contributions(self):2923 def contributions(self):
2960 """Cache the results of getProjectsAndCategoriesContributedTo()."""2924 """Cache the results of getProjectsAndCategoriesContributedTo()."""
2961 return self.context.getProjectsAndCategoriesContributedTo(2925 return self.context.getProjectsAndCategoriesContributedTo(
@@ -3124,19 +3088,6 @@
3124 else:3088 else:
3125 raise AssertionError('Unknown group to contact.')3089 raise AssertionError('Unknown group to contact.')
31263090
3127 @property
3128 def should_show_polls_portlet(self):
3129 menu = TeamOverviewMenu(self.context)
3130 return (
3131 self.has_current_polls or self.closedpolls
3132 or menu.add_poll().enabled)
3133
3134 @property
3135 def has_current_polls(self):
3136 """Return True if this team has any non-closed polls."""
3137 assert self.context.isTeam()
3138 return bool(self.openpolls) or bool(self.notyetopenedpolls)
3139
3140 def userIsOwner(self):3091 def userIsOwner(self):
3141 """Return True if the user is the owner of this Team."""3092 """Return True if the user is the owner of this Team."""
3142 if self.user is None:3093 if self.user is None:
31433094
=== removed file 'lib/lp/registry/browser/poll.py'
--- lib/lp/registry/browser/poll.py 2010-11-23 23:22:27 +0000
+++ lib/lp/registry/browser/poll.py 1970-01-01 00:00:00 +0000
@@ -1,469 +0,0 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6__all__ = [
7 'BasePollView',
8 'PollAddView',
9 'PollEditNavigationMenu',
10 'PollEditView',
11 'PollNavigation',
12 'PollOptionAddView',
13 'PollOptionEditView',
14 'PollOverviewMenu',
15 'PollView',
16 'PollVoteView',
17 'PollBreadcrumb',
18 ]
19
20from z3c.ptcompat import ViewPageTemplateFile
21from zope.app.form.browser import TextWidget
22from zope.component import getUtility
23from zope.event import notify
24from zope.interface import (
25 implements,
26 Interface,
27 )
28from zope.lifecycleevent import ObjectCreatedEvent
29
30from canonical.launchpad.helpers import shortlist
31from canonical.launchpad.webapp import (
32 ApplicationMenu,
33 canonical_url,
34 enabled_with_permission,
35 LaunchpadView,
36 Link,
37 Navigation,
38 NavigationMenu,
39 stepthrough,
40 )
41from canonical.launchpad.webapp.breadcrumb import TitleBreadcrumb
42from lp.app.browser.launchpadform import (
43 action,
44 custom_widget,
45 LaunchpadEditFormView,
46 LaunchpadFormView,
47 )
48from lp.registry.interfaces.poll import (
49 IPoll,
50 IPollOption,
51 IPollOptionSet,
52 IPollSubset,
53 IVoteSet,
54 PollAlgorithm,
55 PollSecrecy,
56 )
57
58
59class PollEditLinksMixin:
60
61 @enabled_with_permission('launchpad.Edit')
62 def addnew(self):
63 text = 'Add new option'
64 return Link('+newoption', text, icon='add')
65
66 @enabled_with_permission('launchpad.Edit')
67 def edit(self):
68 text = 'Change details'
69 return Link('+edit', text, icon='edit')
70
71
72class PollOverviewMenu(ApplicationMenu, PollEditLinksMixin):
73 usedfor = IPoll
74 facet = 'overview'
75 links = ['addnew']
76
77
78class IPollEditMenu(Interface):
79 """A marker interface for the edit navigation menu."""
80
81
82class PollEditNavigationMenu(NavigationMenu, PollEditLinksMixin):
83 usedfor = IPollEditMenu
84 facet = 'overview'
85 links = ['addnew', 'edit']
86
87
88class IPollActionMenu(Interface):
89 """A marker interface for the action menu."""
90
91
92class PollActionNavigationMenu(PollEditNavigationMenu):
93 usedfor = IPollActionMenu
94 links = ['edit']
95
96
97class PollNavigation(Navigation):
98
99 usedfor = IPoll
100
101 @stepthrough('+option')
102 def traverse_option(self, name):
103 return getUtility(IPollOptionSet).getByPollAndId(
104 self.context, int(name))
105
106
107class BasePollView(LaunchpadView):
108 """A base view class to be used in other poll views."""
109
110 token = None
111 gotTokenAndVotes = False
112 feedback = ""
113
114 def setUpTokenAndVotes(self):
115 """Set up the token and votes to be displayed."""
116 if not self.userVoted():
117 return
118
119 # For secret polls we can only display the votes after the token
120 # is submitted.
121 if self.request.method == 'POST' and self.isSecret():
122 self.setUpTokenAndVotesForSecretPolls()
123 elif not self.isSecret():
124 self.setUpTokenAndVotesForNonSecretPolls()
125
126 def setUpTokenAndVotesForNonSecretPolls(self):
127 """Get the votes of the logged in user in this poll.
128
129 Set the votes in instance variables and also set self.gotTokenAndVotes
130 to True, so the templates know they can display the vote.
131
132 This method should be used only on non-secret polls and if the logged
133 in user has voted on this poll.
134 """
135 assert not self.isSecret() and self.userVoted()
136 votes = self.context.getVotesByPerson(self.user)
137 assert votes, (
138 "User %r hasn't voted on poll %r" % (self.user, self.context))
139 if self.isSimple():
140 # Here we have only one vote.
141 self.currentVote = votes[0]
142 self.token = self.currentVote.token
143 elif self.isCondorcet():
144 # Here we have multiple votes, and the token is the same in
145 # all of them.
146 self.currentVotes = sorted(votes, key=lambda v: v.preference)
147 self.token = self.currentVotes[0].token
148 self.gotTokenAndVotes = True
149
150 def setUpTokenAndVotesForSecretPolls(self):
151 """Get the votes with the token provided in the form.
152
153 Set the votes, together with the token in instance variables. Also
154 set self.gotTokenAndVotes to True, so the templates know they can
155 display the vote.
156
157 Return True if there's any vote with the given token and the votes
158 are on this poll.
159
160 This method should be used only on secret polls and if the logged
161 in user has voted on this poll.
162 """
163 assert self.isSecret() and self.userVoted()
164 token = self.request.form.get('token')
165 # Only overwrite self.token if the request contains a 'token'
166 # variable.
167 if token is not None:
168 self.token = token
169 votes = getUtility(IVoteSet).getByToken(self.token)
170 if not votes:
171 self.feedback = ("There's no vote associated with the token %s"
172 % self.token)
173 return False
174
175 # All votes with a given token must be on the same poll. That means
176 # checking the poll of the first vote is enough.
177 if votes[0].poll != self.context:
178 self.feedback = ("The vote associated with the token %s is not "
179 "a vote on this poll." % self.token)
180 return False
181
182 if self.isSimple():
183 # A simple poll has only one vote, because you can choose only one
184 # option.
185 self.currentVote = votes[0]
186 elif self.isCondorcet():
187 self.currentVotes = sorted(votes, key=lambda v: v.preference)
188 self.gotTokenAndVotes = True
189 return True
190
191 def userCanVote(self):
192 """Return True if the user is/was eligible to vote on this poll."""
193 return (self.user and self.user.inTeam(self.context.team))
194
195 def userVoted(self):
196 """Return True if the user voted on this poll."""
197 return (self.user and self.context.personVoted(self.user))
198
199 def isCondorcet(self):
200 """Return True if this poll's type is Condorcet."""
201 return self.context.type == PollAlgorithm.CONDORCET
202
203 def isSimple(self):
204 """Return True if this poll's type is Simple."""
205 return self.context.type == PollAlgorithm.SIMPLE
206
207 def isSecret(self):
208 """Return True if this is a secret poll."""
209 return self.context.secrecy == PollSecrecy.SECRET
210
211
212class PollBreadcrumb(TitleBreadcrumb):
213 """Breadcrumb for polls."""
214
215
216class PollView(BasePollView):
217 """A view class to display the results of a poll."""
218 implements(IPollActionMenu)
219
220 def initialize(self):
221 super(PollView, self).initialize()
222 request = self.request
223 if (self.userCanVote() and self.context.isOpen() and
224 self.context.getActiveOptions()):
225 vote_url = canonical_url(self.context, view_name='+vote')
226 request.response.redirect(vote_url)
227
228 def getVotesByOption(self, option):
229 """Return the number of votes the given option received."""
230 return getUtility(IVoteSet).getVotesByOption(option)
231
232 def getPairwiseMatrixWithHeaders(self):
233 """Return the pairwise matrix, with headers being the option's
234 names.
235 """
236 # XXX: kiko 2006-03-13:
237 # The list() call here is necessary because, lo and behold,
238 # it gives us a non-security-proxied list object! Someone come
239 # in and fix this!
240 pairwise_matrix = list(self.context.getPairwiseMatrix())
241 headers = [None]
242 for idx, option in enumerate(self.context.getAllOptions()):
243 headers.append(option.title)
244 # Get a mutable row.
245 row = list(pairwise_matrix[idx])
246 row.insert(0, option.title)
247 pairwise_matrix[idx] = row
248 pairwise_matrix.insert(0, headers)
249 return pairwise_matrix
250
251
252class PollVoteView(BasePollView):
253 """A view class to where the user can vote on a poll.
254
255 If the user already voted, the current vote is displayed and the user can
256 change it. Otherwise he can register his vote.
257 """
258
259 default_template = ViewPageTemplateFile(
260 '../templates/poll-vote-simple.pt')
261 condorcet_template = ViewPageTemplateFile(
262 '../templates/poll-vote-condorcet.pt')
263
264 @property
265 def template(self):
266 if self.isCondorcet():
267 return self.condorcet_template
268 else:
269 return self.default_template
270
271 def initialize(self):
272 """Process the form, if it was submitted."""
273 super(PollVoteView, self).initialize()
274 if not self.isSecret() and self.userVoted():
275 # For non-secret polls, the user's vote is always displayed
276 self.setUpTokenAndVotesForNonSecretPolls()
277
278 if self.request.method != 'POST':
279 return
280
281 if self.isSecret() and self.userVoted():
282 if not self.setUpTokenAndVotesForSecretPolls():
283 # Not possible to get the votes. Probably the token was wrong.
284 return
285
286 if 'showvote' in self.request.form:
287 # The user only wants to see the vote.
288 return
289
290 if not self.context.isOpen():
291 self.feedback = "This poll is not open."
292 return
293
294 if self.isSimple():
295 self.processSimpleVotingForm()
296 else:
297 self.processCondorcetVotingForm()
298
299 # User may have voted, so we need to setup the vote to display again.
300 self.setUpTokenAndVotes()
301
302 def processSimpleVotingForm(self):
303 """Process the simple-voting form to change a user's vote or register
304 a new one.
305
306 This method must not be called if the poll is not open.
307 """
308 assert self.context.isOpen()
309 context = self.context
310 newoption_id = self.request.form.get('newoption')
311 if newoption_id == 'donotchange':
312 self.feedback = "Your vote was not changed."
313 return
314 elif newoption_id == 'donotvote':
315 self.feedback = "You chose not to vote yet."
316 return
317 elif newoption_id == 'none':
318 newoption = None
319 else:
320 newoption = getUtility(IPollOptionSet).getByPollAndId(
321 context, int(newoption_id))
322
323 if self.userVoted():
324 self.currentVote.option = newoption
325 self.feedback = "Your vote was changed successfully."
326 else:
327 self.currentVote = context.storeSimpleVote(self.user, newoption)
328 self.token = self.currentVote.token
329 self.currentVote = self.currentVote
330 if self.isSecret():
331 self.feedback = (
332 "Your vote has been recorded. If you want to view or "
333 "change it later you must write down this key: %s"
334 % self.token)
335 else:
336 self.feedback = (
337 "Your vote was stored successfully. You can come back to "
338 "this page at any time before this poll closes to view "
339 "or change your vote, if you want.")
340
341 def processCondorcetVotingForm(self):
342 """Process the condorcet-voting form to change a user's vote or
343 register a new one.
344
345 This method must not be called if the poll is not open.
346 """
347 assert self.context.isOpen()
348 form = self.request.form
349 activeoptions = shortlist(self.context.getActiveOptions())
350 newvotes = {}
351 for option in activeoptions:
352 try:
353 preference = int(form.get('option_%d' % option.id))
354 except ValueError:
355 # XXX: Guilherme Salgado 2005-09-14:
356 # User tried to specify a value which we can't convert to
357 # an integer. Better thing to do would be to notify the user
358 # and ask him to fix it.
359 preference = None
360 newvotes[option] = preference
361
362 if self.userVoted():
363 # This is a vote change.
364 # For now it's not possible to have votes in an inactive option,
365 # but it'll be in the future as we'll allow people to make options
366 # inactive after a poll opens.
367 assert len(activeoptions) == len(self.currentVotes)
368 for vote in self.currentVotes:
369 vote.preference = newvotes.get(vote.option)
370 self.currentVotes.sort(key=lambda v: v.preference)
371 self.feedback = "Your vote was changed successfully."
372 else:
373 # This is a new vote.
374 votes = self.context.storeCondorcetVote(self.user, newvotes)
375 self.token = votes[0].token
376 self.currentVotes = sorted(votes, key=lambda v: v.preference)
377 if self.isSecret():
378 self.feedback = (
379 "Your vote has been recorded. If you want to view or "
380 "change it later you must write down this key: %s"
381 % self.token)
382 else:
383 self.feedback = (
384 "Your vote was stored successfully. You can come back to "
385 "this page at any time before this poll closes to view "
386 "or change your vote, if you want.")
387
388
389class PollAddView(LaunchpadFormView):
390 """The view class to create a new poll in a given team."""
391
392 schema = IPoll
393 field_names = ["name", "title", "proposition", "allowspoilt", "dateopens",
394 "datecloses"]
395
396 @property
397 def cancel_url(self):
398 """See `LaunchpadFormView`."""
399 return canonical_url(self.context)
400
401 @action("Continue", name="continue")
402 def continue_action(self, action, data):
403 # XXX: salgado, 2008-10-08: Only secret polls can be created until we
404 # fix https://launchpad.net/bugs/80596.
405 secrecy = PollSecrecy.SECRET
406 poll = IPollSubset(self.context).new(
407 data['name'], data['title'], data['proposition'],
408 data['dateopens'], data['datecloses'], secrecy,
409 data['allowspoilt'])
410 self.next_url = canonical_url(poll)
411 notify(ObjectCreatedEvent(poll))
412
413
414class PollEditView(LaunchpadEditFormView):
415
416 implements(IPollEditMenu)
417 schema = IPoll
418 label = "Edit poll details"
419 field_names = ["name", "title", "proposition", "allowspoilt", "dateopens",
420 "datecloses"]
421
422 @property
423 def cancel_url(self):
424 """See `LaunchpadFormView`."""
425 return canonical_url(self.context)
426
427 @action("Save", name="save")
428 def save_action(self, action, data):
429 self.updateContextFromData(data)
430 self.next_url = canonical_url(self.context)
431
432
433class PollOptionEditView(LaunchpadEditFormView):
434 """Edit one of a poll's options."""
435
436 schema = IPollOption
437 label = "Edit option details"
438 field_names = ["name", "title"]
439 custom_widget("title", TextWidget, width=30)
440
441 @property
442 def cancel_url(self):
443 """See `LaunchpadFormView`."""
444 return canonical_url(self.context.poll)
445
446 @action("Save", name="save")
447 def save_action(self, action, data):
448 self.updateContextFromData(data)
449 self.next_url = canonical_url(self.context.poll)
450
451
452class PollOptionAddView(LaunchpadFormView):
453 """Create a new option in a given poll."""
454
455 schema = IPollOption
456 label = "Create new poll option"
457 field_names = ["name", "title"]
458 custom_widget("title", TextWidget, width=30)
459
460 @property
461 def cancel_url(self):
462 """See `LaunchpadFormView`."""
463 return canonical_url(self.context)
464
465 @action("Create", name="create")
466 def create_action(self, action, data):
467 polloption = self.context.newOption(data['name'], data['title'])
468 self.next_url = canonical_url(self.context)
469 notify(ObjectCreatedEvent(polloption))
4700
=== removed file 'lib/lp/registry/browser/tests/poll-views.txt'
--- lib/lp/registry/browser/tests/poll-views.txt 2009-12-15 20:33:49 +0000
+++ lib/lp/registry/browser/tests/poll-views.txt 1970-01-01 00:00:00 +0000
@@ -1,134 +0,0 @@
1Poll views
2----------
3
4The polls portlet shows the state of current polls, and links to past
5polls.
6
7 >>> from canonical.launchpad.testing.pages import extract_text
8
9 >>> user = factory.makePerson()
10 >>> team = factory.makeTeam(name='team')
11 >>> owner = team.teamowner
12
13 >>> def create_team_view(team, name=None, principal=None):
14 ... # XRDS inheritance requires a lot of setup.
15 ... path_info = '/~%s' % team.name
16 ... server_url = 'http://launchpad.dev'
17 ... view = create_view(
18 ... team, name=name, principal=principal,
19 ... server_url=server_url, path_info=path_info)
20 ... view.initialize()
21 ... return view
22
23The portlet does not render any markup when there are no polls...
24
25 >>> login_person(user)
26 >>> view = create_team_view(team, name='+portlet-polls', principal=user)
27 >>> view.has_current_polls
28 False
29
30 >>> view.should_show_polls_portlet
31 False
32
33 >>> print extract_text(view.render())
34 <BLANKLINE>
35
36Unless the user is a team owner.
37
38 >>> login_person(owner)
39 >>> view = create_team_view(team, name='+portlet-polls', principal=owner)
40 >>> view.has_current_polls
41 False
42
43 >>> view.should_show_polls_portlet
44 True
45
46 >>> print extract_text(view.render())
47 Polls
48 No current polls.
49 Show polls
50 Create a poll
51
52The portlet shows a link to polls to all users when there is a poll, but it
53has not opened.
54
55 >>> import pytz
56 >>> from datetime import datetime, timedelta
57 >>> from lp.registry.interfaces.poll import IPollSubset, PollSecrecy
58
59 >>> open_date = datetime.now().replace(tzinfo=pytz.timezone('UTC'))
60 >>> close_date = open_date + timedelta(weeks=1)
61 >>> poll_subset = IPollSubset(team)
62 >>> poll = poll_subset.new(
63 ... 'name', 'title', 'proposition', open_date, close_date,
64 ... PollSecrecy.OPEN, False)
65
66 >>> login_person(user)
67 >>> view = create_team_view(team, name='+portlet-polls', principal=user)
68 >>> view.has_current_polls
69 True
70
71 >>> view.should_show_polls_portlet
72 True
73
74 >>> print extract_text(view.render())
75 Polls
76 Show polls
77
78The portlet shows more details to the poll owner.
79
80 >>> login_person(owner)
81 >>> view = create_team_view(team, name='+portlet-polls', principal=owner)
82 >>> view.has_current_polls
83 True
84
85 >>> view.should_show_polls_portlet
86 True
87
88 >>> print extract_text(view.render())
89 Polls
90 title - opens in 5 hours
91 Show polls
92 Create a poll
93
94Current polls are listed in the portlet, the only difference between a user
95and an owner is the owner has a link to create more polls.
96
97 >>> poll.dateopens = open_date - timedelta(weeks=2)
98
99 >>> login_person(user)
100 >>> view = create_team_view(team, name='+portlet-polls', principal=user)
101 >>> print extract_text(view.render())
102 Polls
103 title - closes on ...
104 You have seven days left to vote in this poll.
105 Show polls
106
107 >>> login_person(owner)
108 >>> view = create_team_view(team, name='+portlet-polls', principal=owner)
109 >>> print extract_text(view.render())
110 Polls
111 title - closes on ...
112 You have seven days left to vote in this poll.
113 Show polls
114 Create a poll
115
116When all the polls are closed, the portlet states the case and has a link to
117see the polls.
118
119 >>> poll.datecloses = close_date - timedelta(weeks=2)
120
121 >>> login_person(user)
122 >>> view = create_team_view(team, name='+portlet-polls', principal=user)
123 >>> print extract_text(view.render())
124 Polls
125 No current polls.
126 Show polls
127
128 >>> login_person(owner)
129 >>> view = create_team_view(team, name='+portlet-polls', principal=owner)
130 >>> print extract_text(view.render())
131 Polls
132 No current polls.
133 Show polls
134 Create a poll
1350
=== removed file 'lib/lp/registry/browser/tests/poll-views_0.txt'
--- lib/lp/registry/browser/tests/poll-views_0.txt 2010-10-18 22:24:59 +0000
+++ lib/lp/registry/browser/tests/poll-views_0.txt 1970-01-01 00:00:00 +0000
@@ -1,92 +0,0 @@
1= Poll Pages =
2
3First import some stuff and setup some things we'll use in this test.
4
5 >>> from zope.component import getUtility, getMultiAdapter
6 >>> from zope.publisher.browser import TestRequest
7 >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
8 >>> from lp.registry.interfaces.person import IPersonSet
9 >>> from lp.registry.interfaces.poll import IPollSet
10 >>> from datetime import datetime, timedelta
11 >>> login("test@canonical.com")
12 >>> ubuntu_team = getUtility(IPersonSet).getByName('ubuntu-team')
13
14
15== Creating new polls ==
16
17When creating a new poll, its start date must be at least 12h after it is
18created.
19
20First we attempt to create a poll which starts 11h from now. That will fail
21with a proper explanation of why it failed.
22
23 >>> eleven_hours_from_now = datetime.now() + timedelta(hours=11)
24 >>> eleven_hours_from_now = eleven_hours_from_now.strftime(
25 ... '%Y-%m-%d %H:%M:%S')
26 >>> form = {
27 ... 'field.name': 'test-poll',
28 ... 'field.title': 'test-poll',
29 ... 'field.proposition': 'test-poll',
30 ... 'field.allowspoilt': '1',
31 ... 'field.secrecy': 'SECRET',
32 ... 'field.dateopens': eleven_hours_from_now,
33 ... 'field.datecloses': '2025-06-04',
34 ... 'field.actions.continue': 'Continue'}
35 >>> request = LaunchpadTestRequest(method='POST', form=form)
36 >>> new_poll = getMultiAdapter((ubuntu_team, request), name="+newpoll")
37 >>> new_poll.initialize()
38 >>> print "\n".join(new_poll.errors)
39 A poll cannot open less than 12 hours after it's created.
40
41Now we successfully create a poll which starts 12h from now.
42
43 >>> twelve_hours_from_now = datetime.now() + timedelta(hours=12)
44 >>> twelve_hours_from_now = twelve_hours_from_now.strftime(
45 ... '%Y-%m-%d %H:%M:%S')
46 >>> form['field.dateopens'] = twelve_hours_from_now
47 >>> request = LaunchpadTestRequest(method='POST', form=form)
48 >>> new_poll = getMultiAdapter((ubuntu_team, request), name="+newpoll")
49 >>> new_poll.initialize()
50 >>> new_poll.errors
51 []
52
53
54== Displaying results of condorcet polls ==
55
56 >>> poll = getUtility(IPollSet).getByTeamAndName(ubuntu_team, 'director-2004')
57 >>> poll.type.title
58 'Condorcet Voting'
59
60Although condorcet polls are disabled now, everything is implemented and we're
61using a pairwise matrix to display the results. It's very trick to create this
62matrix on page templates, so the view provides a method wich return this
63matrix as a python list, with the necessary headers (the option's names).
64
65 >>> poll_results = getMultiAdapter((poll, TestRequest()), name="+index")
66 >>> for row in poll_results.getPairwiseMatrixWithHeaders():
67 ... print row
68 [None, u'A', u'B', u'C', u'D']
69 [u'A', None, 2L, 2L, 2L]
70 [u'B', 2L, None, 2L, 2L]
71 [u'C', 1L, 1L, None, 1L]
72 [u'D', 2L, 1L, 2L, None]
73
74== Voting on closed polls ==
75
76This is not allowed, and apart from not linking to the +vote page and not
77even displaying its content for a closed poll, we also have some lower
78level checks.
79
80 >>> request = TestRequest(form={'changevote': 'Change Vote'})
81 >>> request.method = 'POST'
82 >>> voting_page = getMultiAdapter((poll, request), name="+vote")
83 >>> form_processed = False
84 >>> def form_processing():
85 ... form_processed = True
86 >>> voting_page.processCondorcetVotingForm = form_processing
87 >>> voting_page.initialize()
88
89 >>> form_processed
90 False
91 >>> voting_page.feedback
92 'This poll is not open.'
930
=== modified file 'lib/lp/registry/browser/tests/test_breadcrumbs.py'
--- lib/lp/registry/browser/tests/test_breadcrumbs.py 2010-11-30 13:41:48 +0000
+++ lib/lp/registry/browser/tests/test_breadcrumbs.py 2010-12-16 14:48:27 +0000
@@ -105,26 +105,6 @@
105 self.assertEqual(self.milestone.name, last_crumb.text)105 self.assertEqual(self.milestone.name, last_crumb.text)
106106
107107
108class TestPollBreadcrumb(BaseBreadcrumbTestCase):
109 """Test breadcrumbs for an `IPoll`."""
110
111 def setUp(self):
112 super(TestPollBreadcrumb, self).setUp()
113 self.team = self.factory.makeTeam(displayname="Poll Team")
114 name = "pollo-poll"
115 title = "Marco Pollo"
116 proposition = "Be mine"
117 self.poll = self.factory.makePoll(
118 team=self.team,
119 name=name,
120 title=title,
121 proposition=proposition)
122
123 def test_poll(self):
124 crumbs = self.getBreadcrumbsForObject(self.poll)
125 last_crumb = crumbs[-1]
126 self.assertEqual(self.poll.title, last_crumb.text)
127
128from lp.registry.interfaces.nameblacklist import INameBlacklistSet108from lp.registry.interfaces.nameblacklist import INameBlacklistSet
129109
130110
131111
=== removed file 'lib/lp/registry/browser/tests/test_poll.py'
--- lib/lp/registry/browser/tests/test_poll.py 2010-10-11 17:44:05 +0000
+++ lib/lp/registry/browser/tests/test_poll.py 1970-01-01 00:00:00 +0000
@@ -1,38 +0,0 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for IPoll views."""
5
6__metaclass__ = type
7
8import os
9from canonical.testing.layers import DatabaseFunctionalLayer
10from lp.registry.interfaces.poll import PollAlgorithm
11from lp.testing import TestCaseWithFactory
12from lp.testing.views import create_view
13
14
15class TestPollVoteView(TestCaseWithFactory):
16
17 layer = DatabaseFunctionalLayer
18
19 def setUp(self):
20 super(TestPollVoteView, self).setUp()
21 self.team = self.factory.makeTeam()
22
23 def test_simple_poll_template(self):
24 poll = self.factory.makePoll(
25 self.team, 'name', 'title', 'proposition',
26 poll_type=PollAlgorithm.SIMPLE)
27 view = create_view(poll, name='+vote')
28 self.assertEqual(
29 'poll-vote-simple.pt', os.path.basename(view.template.filename))
30
31 def test_condorcet_poll_template(self):
32 poll = self.factory.makePoll(
33 self.team, 'name', 'title', 'proposition',
34 poll_type=PollAlgorithm.CONDORCET)
35 view = create_view(poll, name='+vote')
36 self.assertEqual(
37 'poll-vote-condorcet.pt',
38 os.path.basename(view.template.filename))
390
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2010-12-06 17:33:19 +0000
+++ lib/lp/registry/configure.zcml 2010-12-16 14:48:27 +0000
@@ -689,109 +689,6 @@
689 interface="lp.registry.interfaces.karma.IKarmaActionSet"/>689 interface="lp.registry.interfaces.karma.IKarmaActionSet"/>
690 </securedutility>690 </securedutility>
691 </facet>691 </facet>
692 <facet
693 facet="overview">
694
695 <!-- Poll -->
696
697 <class
698 class="lp.registry.model.poll.Poll">
699 <allow
700 interface="lp.registry.interfaces.poll.IPoll"/>
701 <require
702 permission="launchpad.Edit"
703 set_schema="lp.registry.interfaces.poll.IPoll"/>
704 </class>
705
706 <adapter
707 provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
708 for="lp.registry.interfaces.poll.IPoll"
709 factory="lp.registry.browser.poll.PollBreadcrumb"
710 permission="zope.Public"/>
711
712 <!-- PollOption -->
713
714 <class
715 class="lp.registry.model.poll.PollOption">
716 <allow
717 interface="lp.registry.interfaces.poll.IPollOption"/>
718 <require
719 permission="launchpad.Edit"
720 set_schema="lp.registry.interfaces.poll.IPollOption"/>
721 </class>
722
723 <!-- Vote -->
724
725
726 <!-- Can't require launchpad.Edit to set_attributes because in most cases
727 the vote won't be associated with a person, and thus we can't check it
728 against the logged in user. -->
729
730 <class
731 class="lp.registry.model.poll.Vote">
732 <allow
733 interface="lp.registry.interfaces.poll.IVote"/>
734 <require
735 permission="zope.Public"
736 set_attributes="option preference"/>
737 </class>
738
739 <!-- VoteCast -->
740
741 <class
742 class="lp.registry.model.poll.VoteCast">
743 <allow
744 interface="lp.registry.interfaces.poll.IVoteCast"/>
745 </class>
746
747 <!-- PollSet -->
748
749 <class
750 class="lp.registry.model.poll.PollSet">
751 <allow
752 interface="lp.registry.interfaces.poll.IPollSet"/>
753 </class>
754 <securedutility
755 class="lp.registry.model.poll.PollSet"
756 provides="lp.registry.interfaces.poll.IPollSet">
757 <allow
758 interface="lp.registry.interfaces.poll.IPollSet"/>
759 </securedutility>
760
761 <!-- PollSubset -->
762
763 <adapter
764 for="lp.registry.interfaces.person.ITeam"
765 provides="lp.registry.interfaces.poll.IPollSubset"
766 factory="lp.registry.adapters.PollSubset"
767 permission="zope.Public"/>
768
769 <!-- PollOptionSet -->
770
771 <class
772 class="lp.registry.model.poll.PollOptionSet">
773 <allow
774 interface="lp.registry.interfaces.poll.IPollOptionSet"/>
775 </class>
776 <securedutility
777 class="lp.registry.model.poll.PollOptionSet"
778 provides="lp.registry.interfaces.poll.IPollOptionSet">
779 <allow
780 interface="lp.registry.interfaces.poll.IPollOptionSet"/>
781 </securedutility>
782 <securedutility
783 class="lp.registry.model.poll.VoteSet"
784 provides="lp.registry.interfaces.poll.IVoteSet">
785 <allow
786 interface="lp.registry.interfaces.poll.IVoteSet"/>
787 </securedutility>
788 <securedutility
789 class="lp.registry.model.poll.VoteCastSet"
790 provides="lp.registry.interfaces.poll.IVoteCastSet">
791 <allow
792 interface="lp.registry.interfaces.poll.IVoteCastSet"/>
793 </securedutility>
794 </facet>
795692
796 <!-- Announcement -->693 <!-- Announcement -->
797694
798695
=== modified file 'lib/lp/registry/doc/person-merge.txt'
--- lib/lp/registry/doc/person-merge.txt 2010-12-01 23:39:05 +0000
+++ lib/lp/registry/doc/person-merge.txt 2010-12-16 14:48:27 +0000
@@ -1,4 +1,5 @@
1= Merging =1Merging
2=======
23
3For many reasons (i.e. a gina run) we could have duplicated accounts in4For many reasons (i.e. a gina run) we could have duplicated accounts in
4Launchpad. Once a duplicated account is identified, we need to allow the user5Launchpad. Once a duplicated account is identified, we need to allow the user
@@ -19,7 +20,8 @@
19 >>> marilize = personset.getByName('marilize')20 >>> marilize = personset.getByName('marilize')
2021
2122
22== Sanity checks ==23Sanity checks
24-------------
2325
24We can't merge an account that still has email addresses attached to it26We can't merge an account that still has email addresses attached to it
2527
@@ -29,7 +31,8 @@
29 AssertionError: ...31 AssertionError: ...
3032
3133
32== Preparing test person for the merge ==34Preparing test person for the merge
35-----------------------------------
3336
34Merging people involves updating the merged person relationships. Let's37Merging people involves updating the merged person relationships. Let's
35put the person we will merge into some of those.38put the person we will merge into some of those.
@@ -57,10 +60,11 @@
57 marilize60 marilize
58 >>> sampleperson_old_karma = sample.karma61 >>> sampleperson_old_karma = sample.karma
5962
60Branches whose owner is being merged are uniquified by appending '-N' where N63Branches whose owner is being merged are uniquified by appending '-N'
61is a unique integer. We create "peoplemerge" and "peoplemerge-1" branches owned64where N is a unique integer. We create "peoplemerge" and
62by marilize, and a "peoplemerge" and "peoplemerge-1" branches owned by 'Sample65"peoplemerge-1" branches owned by marilize, and a "peoplemerge" and
63Person' to test that branch name uniquifying works.66"peoplemerge-1" branches owned by 'Sample Person' to test that branch
67name uniquifying works.
6468
65Branches with smaller IDs will be processed first, so we create "peoplemerge"69Branches with smaller IDs will be processed first, so we create "peoplemerge"
66first, and it will be renamed "peoplemerge-2". The extant "peoplemerge-1"70first, and it will be renamed "peoplemerge-2". The extant "peoplemerge-1"
@@ -81,9 +85,9 @@
81 >>> peoplemerge11 = factory.makePersonalBranch(85 >>> peoplemerge11 = factory.makePersonalBranch(
82 ... name='peoplemerge-1', owner=marilize)86 ... name='peoplemerge-1', owner=marilize)
8387
84'Sample Person' is a deactivated member of the 'Ubuntu Translators' team,88'Sample Person' is a deactivated member of the 'Ubuntu Translators'
85while marilize is an active member. After the merge, 'Sample Person' will be an89team, while marilize is an active member. After the merge, 'Sample
86active member of that team.90Person' will be an active member of that team.
8791
88 >>> sample in ubuntu_translators.inactivemembers92 >>> sample in ubuntu_translators.inactivemembers
89 True93 True
@@ -102,7 +106,8 @@
102 u'Marilize Coetzee'106 u'Marilize Coetzee'
103107
104108
105== Do the merge! ==109Do the merge!
110-------------
106111
107 # Now we remove the only email address marilize had, so that we can merge112 # Now we remove the only email address marilize had, so that we can merge
108 # it. First we need to change its status, though, because we can't delete113 # it. First we need to change its status, though, because we can't delete
@@ -119,7 +124,8 @@
119 >>> personset.merge(marilize, sample)124 >>> personset.merge(marilize, sample)
120125
121126
122== Merge results ==127Merge results
128-------------
123129
124Check that 'Sample Person' has indeed become an active member of 'Ubuntu130Check that 'Sample Person' has indeed become an active member of 'Ubuntu
125Translators'131Translators'
@@ -336,37 +342,30 @@
336 loser, winner,342 loser, winner,
337343
338344
339== Merging teams ==345Merging teams
346-------------
340347
341Merging of teams is also possible and uses the same API used for merging348Merging of teams is also possible and uses the same API used for merging
342people. Note, though, that when merging teams, its polls will not be349people. Team memberships are carried over just like when merging people.
343carried over to the remaining team. Team memberships, on the other hand,
344are carried over just like when merging people.
345350
346 >>> from datetime import datetime, timedelta351 >>> from datetime import datetime, timedelta
347 >>> import pytz352 >>> import pytz
348 >>> from lp.registry.interfaces.poll import IPollSubset, PollSecrecy
349 >>> test_team = personset.newTeam(sample, 'test-team', 'Test team')353 >>> test_team = personset.newTeam(sample, 'test-team', 'Test team')
350 >>> launchpad_devs = personset.getByName('launchpad')354 >>> launchpad_devs = personset.getByName('launchpad')
351 >>> ignored = launchpad_devs.addMember(355 >>> ignored = launchpad_devs.addMember(
352 ... test_team, reviewer=launchpad_devs.teamowner, force_team_add=True)356 ... test_team, reviewer=launchpad_devs.teamowner, force_team_add=True)
353 >>> today = datetime.now(pytz.timezone('UTC'))357 >>> today = datetime.now(pytz.UTC)
354 >>> tomorrow = today + timedelta(days=1)358 >>> tomorrow = today + timedelta(days=1)
355 >>> poll = IPollSubset(test_team).new(
356 ... 'test-poll', 'Title', 'Proposition', today, tomorrow,
357 ... PollSecrecy.OPEN, allowspoilt=True)
358359
359 # test_team has a superteam, one active member and a poll.360 # test_team has a superteam and one active member.
360 >>> [team.name for team in test_team.super_teams]361 >>> [team.name for team in test_team.super_teams]
361 [u'launchpad']362 [u'launchpad']
362 >>> test_team.teamowner.name363 >>> test_team.teamowner.name
363 u'name12'364 u'name12'
364 >>> [member.name for member in test_team.allmembers]365 >>> [member.name for member in test_team.allmembers]
365 [u'name12']366 [u'name12']
366 >>> list(IPollSubset(test_team).getAll())
367 [<Poll at ...]
368367
369 # Landscape-developers has no super teams, two members and no polls.368 # Landscape-developers has no super teams and two members.
370 >>> landscape = personset.getByName('landscape-developers')369 >>> landscape = personset.getByName('landscape-developers')
371 >>> [team.name for team in landscape.super_teams]370 >>> [team.name for team in landscape.super_teams]
372 []371 []
@@ -374,8 +373,6 @@
374 u'name12'373 u'name12'
375 >>> [member.name for member in landscape.allmembers]374 >>> [member.name for member in landscape.allmembers]
376 [u'salgado', u'name12']375 [u'salgado', u'name12']
377 >>> list(IPollSubset(landscape).getAll())
378 []
379376
380Now we try to merge them, but since test_team has active members it can't be377Now we try to merge them, but since test_team has active members it can't be
381merged.378merged.
@@ -427,14 +424,11 @@
427 ... test_team.retractTeamMembership(team, test_team.teamowner)424 ... test_team.retractTeamMembership(team, test_team.teamowner)
428 >>> personset.merge(test_team, landscape)425 >>> personset.merge(test_team, landscape)
429426
430 # The resulting Landscape-developers no new super teams, has427 # The resulting Landscape-developers no new super teams and its
431 # no polls and its members are still the same two from before the428 # members are still the same two from before the merge.
432 # merge.
433 >>> landscape.teamowner.name429 >>> landscape.teamowner.name
434 u'name12'430 u'name12'
435 >>> [member.name for member in landscape.allmembers]431 >>> [member.name for member in landscape.allmembers]
436 [u'salgado', u'name12']432 [u'salgado', u'name12']
437 >>> [team.name for team in landscape.super_teams]433 >>> [team.name for team in landscape.super_teams]
438 []434 []
439 >>> list(IPollSubset(landscape).getAll())
440 []
441435
=== removed file 'lib/lp/registry/doc/poll-preconditions.txt'
--- lib/lp/registry/doc/poll-preconditions.txt 2010-10-18 22:24:59 +0000
+++ lib/lp/registry/doc/poll-preconditions.txt 1970-01-01 00:00:00 +0000
@@ -1,74 +0,0 @@
1Poll preconditions
2==================
3
4There's some preconditions that we need to meet to vote in polls and remove
5options from them, Not meeting these preconditions is a programming error and
6should be threated as so.
7
8 >>> from zope.component import getUtility
9 >>> from canonical.database.sqlbase import flush_database_updates
10 >>> from canonical.launchpad.ftests import login
11 >>> from datetime import timedelta
12 >>> from lp.registry.interfaces.person import IPersonSet
13 >>> from lp.registry.interfaces.poll import IPollSet
14
15 >>> ubuntu_team = getUtility(IPersonSet).get(17)
16 >>> ubuntu_team_member = getUtility(IPersonSet).get(1)
17 >>> ubuntu_team_nonmember = getUtility(IPersonSet).get(12)
18
19 >>> pollset = getUtility(IPollSet)
20 >>> director_election = pollset.getByTeamAndName(ubuntu_team,
21 ... 'director-2004')
22 >>> director_options = director_election.getActiveOptions()
23 >>> leader_election = pollset.getByTeamAndName(ubuntu_team, 'leader-2004')
24 >>> leader_options = leader_election.getActiveOptions()
25 >>> opendate = leader_election.dateopens
26 >>> onesec = timedelta(seconds=1)
27
28
29If the poll is already opened, it's impossible to remove an option.
30
31 >>> leader_election.removeOption(leader_options[0], when=opendate)
32 Traceback (most recent call last):
33 ...
34 AssertionError
35
36
37Trying to vote two times is a programming error.
38
39 >>> votes = leader_election.storeSimpleVote(
40 ... ubuntu_team_member, leader_options[0], when=opendate)
41
42 >>> votes = leader_election.storeSimpleVote(
43 ... ubuntu_team_member, leader_options[0], when=opendate)
44 Traceback (most recent call last):
45 ...
46 AssertionError: Can't vote twice in the same poll
47
48
49It's not possible for a non-member to vote, neither to vote when the poll is
50not open.
51
52 >>> votes = leader_election.storeSimpleVote(
53 ... ubuntu_team_nonmember, leader_options[0], when=opendate)
54 Traceback (most recent call last):
55 ...
56 AssertionError: Person ... is not a member of this poll's team.
57
58 >>> votes = leader_election.storeSimpleVote(
59 ... ubuntu_team_member, leader_options[0], when=opendate - onesec)
60 Traceback (most recent call last):
61 ...
62 AssertionError: This poll is not open
63
64
65It's not possible to vote on an option that doesn't belong to the poll you're
66voting in.
67
68 >>> options = {leader_options[0]: 1}
69 >>> votes = director_election.storeCondorcetVote(
70 ... ubuntu_team_member, options, when=opendate)
71 Traceback (most recent call last):
72 ...
73 AssertionError: The option ... doesn't belong to this poll
74
750
=== removed file 'lib/lp/registry/doc/poll.txt'
--- lib/lp/registry/doc/poll.txt 2010-10-19 18:44:31 +0000
+++ lib/lp/registry/doc/poll.txt 1970-01-01 00:00:00 +0000
@@ -1,140 +0,0 @@
1Polls
2=====
3
4In Launchpad, we have teams as a way to group free software
5developers/contributors usually based on the free software
6product/project/distribution they're involved in. This is the case with teams
7like the 'Gnome Team' and the 'Ubuntu Team'. These teams often have leaders
8whose ellection depends on the vote of all members, and this is one of the
9reasons why we teams can have polls attached to them.
10
11 >>> import pytz
12 >>> from datetime import datetime, timedelta
13 >>> from zope.component import getUtility
14 >>> from canonical.database.sqlbase import flush_database_updates
15 >>> from canonical.launchpad.ftests import login
16 >>> from lp.registry.interfaces.person import IPersonSet
17 >>> from lp.registry.interfaces.poll import (
18 ... IPollSubset,
19 ... PollAlgorithm,
20 ... PollSecrecy,
21 ... )
22
23 >>> team = getUtility(IPersonSet).getByName('ubuntu-team')
24 >>> member = getUtility(IPersonSet).getByName('stevea')
25 >>> member2 = getUtility(IPersonSet).getByName('jdub')
26 >>> member3 = getUtility(IPersonSet).getByName('kamion')
27 >>> member4 = getUtility(IPersonSet).getByName('name16')
28 >>> member5 = getUtility(IPersonSet).getByName('limi')
29 >>> nonmember = getUtility(IPersonSet).getByName('justdave')
30 >>> now = datetime.now(pytz.timezone('UTC'))
31 >>> onesec = timedelta(seconds=1)
32
33We need to login with one of the administrators of the team named
34'ubuntu-team' to be able to create/edit polls.
35 >>> login('colin.watson@ubuntulinux.com')
36
37First we get an object implementing IPollSubset, which is the set of polls for
38a given team (in our case, the 'Ubuntu Team')
39 >>> pollsubset = IPollSubset(team)
40
41Now we create a new poll on this team.
42 >>> opendate = datetime(2005, 01, 01, tzinfo=pytz.timezone('UTC'))
43 >>> closedate = opendate + timedelta(weeks=2)
44 >>> title = "2005 Leader's Elections"
45 >>> proposition = "Who's going to be the next leader?"
46 >>> type = PollAlgorithm.SIMPLE
47 >>> secrecy = PollSecrecy.SECRET
48 >>> allowspoilt = True
49 >>> poll = pollsubset.new("leader-election", title, proposition, opendate,
50 ... closedate, secrecy, allowspoilt, type)
51
52Now we test the if the poll is open or closed in some specific dates.
53 >>> poll.isOpen(when=opendate)
54 True
55 >>> poll.isOpen(when=opendate - onesec)
56 False
57 >>> poll.isOpen(when=closedate)
58 True
59 >>> poll.isOpen(when=closedate + onesec)
60 False
61
62To know what polls are open/closed/not-yet-opened in a team, you can use the
63methods of PollSubset.
64Here we'll query using three different dates:
65
66Query for open polls in the exact second the poll is opening.
67 >>> [p.name for p in pollsubset.getOpenPolls(when=opendate)]
68 [u'leader-election', u'never-closes', u'never-closes2', u'never-closes3',
69 u'never-closes4']
70
71Query for closed polls, one second after the poll closes.
72 >>> [p.name for p in pollsubset.getClosedPolls(when=closedate + onesec)]
73 [u'director-2004', u'leader-2004', u'leader-election']
74
75Query for not-yet-opened polls, one second before the poll opens.
76 >>> [p.name for p in pollsubset.getNotYetOpenedPolls(when=opendate - onesec)]
77 [u'leader-election', u'not-yet-opened']
78
79All polls must have a set of options for people to choose, and they'll always
80start with zero options. We're responsible for adding new ones.
81 >>> poll.getAllOptions().count()
82 0
83
84Let's add some options to this poll, so people can start voting. :)
85 >>> will = poll.newOption('wgraham', 'Will Graham')
86 >>> jack = poll.newOption('jcrawford', 'Jack Crawford')
87 >>> francis = poll.newOption('fd', 'Francis Dolarhyde')
88 >>> [o.title for o in poll.getActiveOptions()]
89 [u'Francis Dolarhyde', u'Jack Crawford', u'Will Graham']
90
91Now, what happens if the poll is already open and, let's say, Francis Dolarhyde
92is convicted and thus becomes ineligible? We'll have to mark that option as
93inactive, so people can't vote on it.
94 >>> francis.active = False
95 >>> flush_database_updates()
96 >>> [o.title for o in poll.getActiveOptions()]
97 [u'Jack Crawford', u'Will Graham']
98
99If the poll is not yet opened, it's possible to simply remove a given option.
100 >>> poll.removeOption(will, when=opendate - onesec)
101 >>> [o.title for o in poll.getAllOptions()]
102 [u'Francis Dolarhyde', u'Jack Crawford']
103
104Any member of the team this poll refers to is eligible to vote, if the poll is
105still open.
106
107 >>> vote1 = poll.storeSimpleVote(member, jack, when=opendate)
108 >>> vote2 = poll.storeSimpleVote(member2, None, when=opendate)
109
110
111Now we create a Condorcet poll on this team and add some options to it, so
112people can start voting.
113
114 >>> title = "2005 Director's Elections"
115 >>> proposition = "Who's going to be the next director?"
116 >>> type = PollAlgorithm.CONDORCET
117 >>> secrecy = PollSecrecy.SECRET
118 >>> allowspoilt = True
119 >>> poll2 = pollsubset.new("director-election", title, proposition, opendate,
120 ... closedate, secrecy, allowspoilt, type)
121 >>> a = poll2.newOption('A', 'Option A')
122 >>> b = poll2.newOption('B', 'Option B')
123 >>> c = poll2.newOption('C', 'Option C')
124 >>> d = poll2.newOption('D', 'Option D')
125
126 >>> options = {b: 1, d: 2, c: 3}
127 >>> votes = poll2.storeCondorcetVote(member, options, when=opendate)
128 >>> options = {d: 1, b: 2}
129 >>> votes = poll2.storeCondorcetVote(member2, options, when=opendate)
130 >>> options = {a: 1, c: 2, b: 3}
131 >>> votes = poll2.storeCondorcetVote(member3, options, when=opendate)
132 >>> options = {a: 1}
133 >>> votes = poll2.storeCondorcetVote(member4, options, when=opendate)
134 >>> for row in poll2.getPairwiseMatrix():
135 ... print row
136 [None, 2L, 2L, 2L]
137 [2L, None, 2L, 2L]
138 [1L, 1L, None, 1L]
139 [2L, 1L, 2L, None]
140
1410
=== modified file 'lib/lp/registry/doc/team-nav-menus.txt'
--- lib/lp/registry/doc/team-nav-menus.txt 2010-10-18 22:24:59 +0000
+++ lib/lp/registry/doc/team-nav-menus.txt 2010-12-16 14:48:27 +0000
@@ -31,9 +31,6 @@
31 link members31 link members
32 url: .../~name18/+members32 url: .../~name18/+members
33 ...33 ...
34 link polls
35 url: .../~name18/+polls
36 ...
37 link profile34 link profile
38 url: .../~name1835 url: .../~name18
39 ...36 ...
4037
=== removed file 'lib/lp/registry/interfaces/poll.py'
--- lib/lp/registry/interfaces/poll.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/interfaces/poll.py 1970-01-01 00:00:00 +0000
@@ -1,500 +0,0 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4# pylint: disable-msg=E0211,E0213
5
6__all__ = [
7 'IPoll',
8 'IPollSet',
9 'IPollSubset',
10 'IPollOption',
11 'IPollOptionSet',
12 'IVote',
13 'IVoteCast',
14 'PollAlgorithm',
15 'PollSecrecy',
16 'PollStatus',
17 'IVoteSet',
18 'IVoteCastSet',
19 'OptionIsNotFromSimplePoll'
20 ]
21
22from datetime import (
23 datetime,
24 timedelta,
25 )
26
27from lazr.enum import (
28 DBEnumeratedType,
29 DBItem,
30 )
31import pytz
32from zope.component import getUtility
33from zope.interface import (
34 Attribute,
35 Interface,
36 )
37from zope.interface.exceptions import Invalid
38from zope.interface.interface import invariant
39from zope.schema import (
40 Bool,
41 Choice,
42 Datetime,
43 Int,
44 Text,
45 TextLine,
46 )
47
48from canonical.launchpad import _
49from canonical.launchpad.validators.name import name_validator
50from lp.registry.interfaces.person import ITeam
51from lp.services.fields import ContentNameField
52
53
54class PollNameField(ContentNameField):
55
56 errormessage = _("%s is already in use by another poll in this team.")
57
58 @property
59 def _content_iface(self):
60 return IPoll
61
62 def _getByName(self, name):
63 team = ITeam(self.context, None)
64 if team is None:
65 team = self.context.team
66 return getUtility(IPollSet).getByTeamAndName(team, name)
67
68
69class PollAlgorithm(DBEnumeratedType):
70 """The algorithm used to accept and calculate the results."""
71
72 SIMPLE = DBItem(1, """
73 Simple Voting
74
75 The most simple method for voting; you just choose a single option.
76 """)
77
78 CONDORCET = DBItem(2, """
79 Condorcet Voting
80
81 One of various methods used for calculating preferential votes. See
82 http://www.electionmethods.org/CondorcetEx.htm for more information.
83 """)
84
85
86class PollSecrecy(DBEnumeratedType):
87 """The secrecy of a given Poll."""
88
89 OPEN = DBItem(1, """
90 Public Votes (Anyone can see a person's vote)
91
92 Everyone who wants will be able to see a person's vote.
93 """)
94
95 ADMIN = DBItem(2, """
96 Semi-secret Votes (Only team administrators can see a person's vote)
97
98 All team owners and administrators will be able to see a person's vote.
99 """)
100
101 SECRET = DBItem(3, """
102 Secret Votes (It's impossible to track a person's vote)
103
104 We don't store the option a person voted in our database,
105 """)
106
107
108class PollStatus:
109 """This class stores the constants used when searching for polls."""
110
111 OPEN = 'open'
112 CLOSED = 'closed'
113 NOT_YET_OPENED = 'not-yet-opened'
114 ALL = frozenset([OPEN, CLOSED, NOT_YET_OPENED])
115
116
117class IPoll(Interface):
118 """A poll for a given proposition in a team."""
119
120 id = Int(title=_('The unique ID'), required=True, readonly=True)
121
122 team = Int(
123 title=_('The team that this poll refers to.'), required=True,
124 readonly=True)
125
126 name = PollNameField(
127 title=_('The unique name of this poll'),
128 description=_('A short unique name, beginning with a lower-case '
129 'letter or number, and containing only letters, '
130 'numbers, dots, hyphens, or plus signs.'),
131 required=True, readonly=False, constraint=name_validator)
132
133 title = TextLine(
134 title=_('The title of this poll'), required=True, readonly=False)
135
136 dateopens = Datetime(
137 title=_('The date and time when this poll opens'), required=True,
138 readonly=False)
139
140 datecloses = Datetime(
141 title=_('The date and time when this poll closes'), required=True,
142 readonly=False)
143
144 proposition = Text(
145 title=_('The proposition that is going to be voted'), required=True,
146 readonly=False)
147
148 type = Choice(
149 title=_('The type of this poll'), required=True,
150 readonly=False, vocabulary=PollAlgorithm,
151 default=PollAlgorithm.CONDORCET)
152
153 allowspoilt = Bool(
154 title=_('Users can spoil their votes?'),
155 description=_(
156 'Allow users to leave the ballot blank (i.e. cast a vote for '
157 '"None of the above")'),
158 required=True, readonly=False, default=True)
159
160 secrecy = Choice(
161 title=_('The secrecy of the Poll'), required=True,
162 readonly=False, vocabulary=PollSecrecy,
163 default=PollSecrecy.SECRET)
164
165 @invariant
166 def saneDates(poll):
167 """Ensure the poll's dates are sane.
168
169 A poll's end date must be after its start date and its start date must
170 be at least 12h from now.
171 """
172 if poll.dateopens >= poll.datecloses:
173 raise Invalid(
174 "A poll cannot close at the time (or before) it opens.")
175 now = datetime.now(pytz.UTC)
176 twelve_hours_ahead = now + timedelta(hours=12)
177 start_date = poll.dateopens.astimezone(pytz.UTC)
178 if start_date < twelve_hours_ahead:
179 raise Invalid(
180 "A poll cannot open less than 12 hours after it's created.")
181
182 def isOpen(when=None):
183 """Return True if this Poll is still open.
184
185 The optional :when argument is used only by our tests, to test if the
186 poll is/was/will be open at a specific date.
187 """
188
189 def isClosed(when=None):
190 """Return True if this Poll is already closed.
191
192 The optional :when argument is used only by our tests, to test if the
193 poll is/was/will be closed at a specific date.
194 """
195
196 def isNotYetOpened(when=None):
197 """Return True if this Poll is not yet opened.
198
199 The optional :when argument is used only by our tests, to test if the
200 poll is/was/will be not-yet-opened at a specific date.
201 """
202
203 def closesIn():
204 """Return a timedelta object of the interval between now and the date
205 when this poll closes."""
206
207 def opensIn():
208 """Return a timedelta object of the interval between now and the date
209 when this poll opens."""
210
211 def newOption(name, title=None, active=True):
212 """Create a new PollOption for this poll.
213
214 If title is None it'll be the same as name.
215 """
216
217 def getActiveOptions():
218 """Return all PollOptions of this poll that are active."""
219
220 def getAllOptions():
221 """Return all Options of this poll."""
222
223 def personVoted(person):
224 """Return True if :person has already voted in this poll."""
225
226 def getVotesByPerson(person):
227 """Return the votes of the given person in this poll.
228
229 The return value will always be a list of Vote objects. That's for
230 consistency because on simple polls there'll be always a single vote,
231 but for condorcet poll, there'll always be a list.
232 """
233
234 def getTotalVotes():
235 """Return the total number of votes this poll had.
236
237 This must be used only on closed polls.
238 """
239
240 def getWinners():
241 """Return the options which won this poll.
242
243 This should be used only on closed polls.
244 """
245
246 def removeOption(option, when=None):
247 """Remove the given option from this poll.
248
249 A ValueError is raised if the given option doesn't belong to this poll.
250 This method can be used only on polls that are not yet opened.
251 The optional :when argument is used only by our tests, to test if the
252 poll is/was/will be not-yet-opened at a specific date.
253 """
254
255 def getOptionByName(name):
256 """Return the PollOption by the given name."""
257
258 def storeSimpleVote(person, option, when=None):
259 """Store and return the vote of a given person in a this poll.
260
261 This method can be used only if this poll is still open and if this is
262 a Simple-style poll.
263
264 :option: The choosen option.
265
266 :when: Optional argument used only by our tests, to test if the poll
267 is/was/will be open at a specific date.
268 """
269
270 def storeCondorcetVote(person, options, when=None):
271 """Store and return the votes of a given person in this poll.
272
273 This method can be used only if this poll is still open and if this is
274 a Condorcet-style poll.
275
276 :options: A dictionary, where the options are the keys and the
277 preferences of each option are the values.
278
279 :when: Optional argument used only by our tests, to test if the poll
280 is/was/will be open at a specific date.
281 """
282
283 def getPairwiseMatrix():
284 """Return the pairwise matrix for this poll.
285
286 This method is only available for condorcet-style polls.
287 See http://www.electionmethods.org/CondorcetEx.htm for an example of a
288 pairwise matrix.
289 """
290
291
292class IPollSet(Interface):
293 """The set of Poll objects."""
294
295 def new(team, name, title, proposition, dateopens, datecloses,
296 secrecy, allowspoilt, poll_type=PollAlgorithm.SIMPLE):
297 """Create a new Poll for the given team."""
298
299 def selectByTeam(team, status=PollStatus.ALL, orderBy=None, when=None):
300 """Return all Polls for the given team, filtered by status.
301
302 :status: is a sequence containing as many values as you want from
303 PollStatus.
304
305 :orderBy: can be either a string with the column name you want to sort
306 or a list of column names as strings.
307 If no orderBy is specified the results will be ordered using the
308 default ordering specified in Poll._defaultOrder.
309
310 The optional :when argument is used only by our tests, to test if the
311 poll is/was/will-be open at a specific date.
312 """
313
314 def getByTeamAndName(team, name, default=None):
315 """Return the Poll for the given team with the given name.
316
317 Return :default if there's no Poll with this name for that team.
318 """
319
320
321class IPollSubset(Interface):
322 """The set of Poll objects for a given team."""
323
324 team = Attribute(_("The team of these polls."))
325
326 title = Attribute('Polls Page Title')
327
328 def new(name, title, proposition, dateopens, datecloses, secrecy,
329 allowspoilt, poll_type=PollAlgorithm.SIMPLE):
330 """Create a new Poll for this team."""
331
332 def getAll():
333 """Return all Polls of this team."""
334
335 def getOpenPolls(when=None):
336 """Return all Open Polls for this team ordered by the date they'll
337 close.
338
339 The optional :when argument is used only by our tests, to test if the
340 poll is/was/will be open at a specific date.
341 """
342
343 def getNotYetOpenedPolls(when=None):
344 """Return all Not-Yet-Opened Polls for this team ordered by the date
345 they'll open.
346
347 The optional :when argument is used only by our tests, to test if the
348 poll is/was/will be open at a specific date.
349 """
350
351 def getClosedPolls(when=None):
352 """Return all Closed Polls for this team ordered by the date they
353 closed.
354
355 The optional :when argument is used only by our tests, to test if the
356 poll is/was/will be open at a specific date.
357 """
358
359
360class PollOptionNameField(ContentNameField):
361
362 errormessage = _("%s is already in use by another option in this poll.")
363
364 @property
365 def _content_iface(self):
366 return IPollOption
367
368 def _getByName(self, name):
369 if IPollOption.providedBy(self.context):
370 poll = self.context.poll
371 else:
372 poll = self.context
373 return poll.getOptionByName(name)
374
375
376class IPollOption(Interface):
377 """An option to be voted in a given Poll."""
378
379 id = Int(title=_('The unique ID'), required=True, readonly=True)
380
381 poll = Int(
382 title=_('The Poll to which this option refers to.'), required=True,
383 readonly=True)
384
385 name = PollOptionNameField(
386 title=_('Name'), required=True, readonly=False)
387
388 title = TextLine(
389 title=_('Title'),
390 description=_(
391 'The title of this option. A single brief sentence that '
392 'summarises the outcome for which people are voting if '
393 'they select this option.'),
394 required=True, readonly=False)
395
396 active = Bool(
397 title=_('Is this option active?'), required=True, readonly=False,
398 default=True)
399
400 def destroySelf():
401 """Remove this option from the database."""
402
403
404class IPollOptionSet(Interface):
405 """The set of PollOption objects."""
406
407 def new(poll, name, title, active=True):
408 """Create a new PollOption."""
409
410 def selectByPoll(poll, only_active=False):
411 """Return all PollOptions of the given poll.
412
413 If :only_active is True, then return only the active polls.
414 """
415
416 def getByPollAndId(poll, id, default=None):
417 """Return the PollOption with the given id.
418
419 Return :default if there's no PollOption with the given id or if that
420 PollOption is not in the given poll.
421 """
422
423
424class IVoteCast(Interface):
425 """Here we store who voted in a Poll, but not their votes."""
426
427 id = Int(title=_('The unique ID'), required=True, readonly=True)
428
429 person = Int(
430 title=_('The Person that voted.'), required=False, readonly=True)
431
432 poll = Int(
433 title=_('The Poll in which the person voted.'), required=True,
434 readonly=True)
435
436
437class IVoteCastSet(Interface):
438 """The set of all VoteCast objects."""
439
440 def new(poll, person):
441 """Create a new VoteCast."""
442
443
444class IVote(Interface):
445 """Here we store the vote itself, linked to a special token.
446
447 This token is given to the user when he votes, so he can change his vote
448 later.
449 """
450
451 id = Int(
452 title=_('The unique ID'), required=True, readonly=True)
453
454 person = Int(
455 title=_('The Person that voted.'), required=False, readonly=True)
456
457 poll = Int(
458 title=_('The Poll in which the person voted.'), required=True,
459 readonly=True)
460
461 option = Int(
462 title=_('The PollOption choosen.'), required=True, readonly=False)
463
464 preference = Int(
465 title=_('The preference of the choosen PollOption'), required=True,
466 readonly=False)
467
468 token = Text(
469 title=_('The token we give to the user.'),
470 required=True, readonly=True)
471
472
473class OptionIsNotFromSimplePoll(Exception):
474 """Someone tried use an option from a non-SIMPLE poll as if it was from a
475 SIMPLE one."""
476
477
478class IVoteSet(Interface):
479 """The set of all Vote objects."""
480
481 def newToken():
482 """Return a token that was never used in the Vote table."""
483
484 def new(poll, option, preference, token, person):
485 """Create a new Vote."""
486
487 def getByToken(token):
488 """Return the list of votes with the given token.
489
490 For polls whose type is SIMPLE, this list will contain a single vote,
491 because in SIMPLE poll only one option can be choosen.
492 """
493
494 def getVotesByOption(option):
495 """Return the number of votes the given option received.
496
497 Raises a TypeError if the given option doesn't belong to a
498 simple-style poll.
499 """
500
5010
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-12-10 14:58:31 +0000
+++ lib/lp/registry/model/person.py 2010-12-16 14:48:27 +0000
@@ -3871,12 +3871,17 @@
3871 ('personlanguage', 'person'),3871 ('personlanguage', 'person'),
3872 ('person', 'merged'),3872 ('person', 'merged'),
3873 ('emailaddress', 'person'),3873 ('emailaddress', 'person'),
3874 # Polls are not carried over when merging teams.
3875 ('poll', 'team'),
3876 # We can safely ignore the mailinglist table as there's a sanity3874 # We can safely ignore the mailinglist table as there's a sanity
3877 # check above which prevents teams with associated mailing lists3875 # check above which prevents teams with associated mailing lists
3878 # from being merged.3876 # from being merged.
3879 ('mailinglist', 'team'),3877 ('mailinglist', 'team'),
3878 ('translationrelicensingagreement', 'person'),
3879 # Polls are not carried over when merging teams.
3880 # XXX: BradCrittenden 2010-12-16 bug=691105:
3881 # Even though polls have been removed as a feature and from the
3882 # data model, they still exist in the database and must be skipped
3883 # here to avoid violating uniqueness constraints.
3884 ('poll', 'team'),
3880 # I don't think we need to worry about the votecast and vote3885 # I don't think we need to worry about the votecast and vote
3881 # tables, because a real human should never have two profiles3886 # tables, because a real human should never have two profiles
3882 # in Launchpad that are active members of a given team and voted3887 # in Launchpad that are active members of a given team and voted
@@ -3885,7 +3890,6 @@
3885 # closed -- StuartBishop 200606023890 # closed -- StuartBishop 20060602
3886 ('votecast', 'person'),3891 ('votecast', 'person'),
3887 ('vote', 'person'),3892 ('vote', 'person'),
3888 ('translationrelicensingagreement', 'person'),
3889 ]3893 ]
38903894
3891 references = list(postgresql.listReferences(cur, 'person', 'id'))3895 references = list(postgresql.listReferences(cur, 'person', 'id'))
38923896
=== removed file 'lib/lp/registry/model/poll.py'
--- lib/lp/registry/model/poll.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/poll.py 1970-01-01 00:00:00 +0000
@@ -1,440 +0,0 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4# pylint: disable-msg=E0611,W0212
5
6__metaclass__ = type
7__all__ = [
8 'Poll',
9 'PollOption',
10 'PollOptionSet',
11 'PollSet',
12 'VoteCast',
13 'Vote',
14 'VoteSet',
15 'VoteCastSet',
16 ]
17
18from datetime import datetime
19import random
20
21import pytz
22from sqlobject import (
23 AND,
24 BoolCol,
25 ForeignKey,
26 IntCol,
27 OR,
28 SQLObjectNotFound,
29 StringCol,
30 )
31from storm.store import Store
32from zope.component import getUtility
33from zope.interface import implements
34
35from canonical.database.datetimecol import UtcDateTimeCol
36from canonical.database.enumcol import EnumCol
37from canonical.database.sqlbase import (
38 SQLBase,
39 sqlvalues,
40 )
41from lp.registry.interfaces.person import validate_public_person
42from lp.registry.interfaces.poll import (
43 IPoll,
44 IPollOption,
45 IPollOptionSet,
46 IPollSet,
47 IVote,
48 IVoteCast,
49 IVoteCastSet,
50 IVoteSet,
51 OptionIsNotFromSimplePoll,
52 PollAlgorithm,
53 PollSecrecy,
54 PollStatus,
55 )
56
57
58class Poll(SQLBase):
59 """See IPoll."""
60
61 implements(IPoll)
62 _table = 'Poll'
63 sortingColumns = ['title', 'id']
64 _defaultOrder = sortingColumns
65
66 team = ForeignKey(
67 dbName='team', foreignKey='Person',
68 storm_validator=validate_public_person, notNull=True)
69
70 name = StringCol(dbName='name', notNull=True)
71
72 title = StringCol(dbName='title', notNull=True, unique=True)
73
74 dateopens = UtcDateTimeCol(dbName='dateopens', notNull=True)
75
76 datecloses = UtcDateTimeCol(dbName='datecloses', notNull=True)
77
78 proposition = StringCol(dbName='proposition', notNull=True)
79
80 type = EnumCol(dbName='type', enum=PollAlgorithm,
81 default=PollAlgorithm.SIMPLE)
82
83 allowspoilt = BoolCol(dbName='allowspoilt', default=True, notNull=True)
84
85 secrecy = EnumCol(dbName='secrecy', enum=PollSecrecy,
86 default=PollSecrecy.SECRET)
87
88 def newOption(self, name, title, active=True):
89 """See IPoll."""
90 return getUtility(IPollOptionSet).new(self, name, title, active)
91
92 def isOpen(self, when=None):
93 """See IPoll."""
94 if when is None:
95 when = datetime.now(pytz.timezone('UTC'))
96 return (self.datecloses >= when and self.dateopens <= when)
97
98 @property
99 def closesIn(self):
100 """See IPoll."""
101 return self.datecloses - datetime.now(pytz.timezone('UTC'))
102
103 @property
104 def opensIn(self):
105 """See IPoll."""
106 return self.dateopens - datetime.now(pytz.timezone('UTC'))
107
108 def isClosed(self, when=None):
109 """See IPoll."""
110 if when is None:
111 when = datetime.now(pytz.timezone('UTC'))
112 return self.datecloses <= when
113
114 def isNotYetOpened(self, when=None):
115 """See IPoll."""
116 if when is None:
117 when = datetime.now(pytz.timezone('UTC'))
118 return self.dateopens > when
119
120 def getAllOptions(self):
121 """See IPoll."""
122 return getUtility(IPollOptionSet).selectByPoll(self)
123
124 def getActiveOptions(self):
125 """See IPoll."""
126 return getUtility(IPollOptionSet).selectByPoll(self, only_active=True)
127
128 def getVotesByPerson(self, person):
129 """See IPoll."""
130 return Vote.selectBy(person=person, poll=self)
131
132 def personVoted(self, person):
133 """See IPoll."""
134 results = VoteCast.selectBy(person=person, poll=self)
135 return bool(results.count())
136
137 def removeOption(self, option, when=None):
138 """See IPoll."""
139 assert self.isNotYetOpened(when=when)
140 if option.poll != self:
141 raise ValueError(
142 "Can't remove an option that doesn't belong to this poll")
143 option.destroySelf()
144
145 def getOptionByName(self, name):
146 """See IPoll."""
147 return PollOption.selectOneBy(poll=self, name=name)
148
149 def _assertEverythingOkAndGetVoter(self, person, when=None):
150 """Use assertions to Make sure all pre-conditions for a person to vote
151 are met.
152
153 Return the person if this is not a secret poll or None if it's a
154 secret one.
155 """
156 assert self.isOpen(when=when), "This poll is not open"
157 assert not self.personVoted(person), "Can't vote twice in the same poll"
158 assert person.inTeam(self.team), (
159 "Person %r is not a member of this poll's team." % person)
160
161 # We only associate the option with the person if the poll is not a
162 # SECRET one.
163 if self.secrecy == PollSecrecy.SECRET:
164 voter = None
165 else:
166 voter = person
167 return voter
168
169 def storeCondorcetVote(self, person, options, when=None):
170 """See IPoll."""
171 voter = self._assertEverythingOkAndGetVoter(person, when=when)
172 assert self.type == PollAlgorithm.CONDORCET
173 voteset = getUtility(IVoteSet)
174
175 token = voteset.newToken()
176 votes = []
177 activeoptions = self.getActiveOptions()
178 for option, preference in options.items():
179 assert option.poll == self, (
180 "The option %r doesn't belong to this poll" % option)
181 assert option.active, "Option %r is not active" % option
182 votes.append(voteset.new(self, option, preference, token, voter))
183
184 # Store a vote with preference = None for each active option of this
185 # poll that wasn't in the options argument.
186 for option in activeoptions:
187 if option not in options:
188 votes.append(voteset.new(self, option, None, token, voter))
189
190 getUtility(IVoteCastSet).new(self, person)
191 return votes
192
193 def storeSimpleVote(self, person, option, when=None):
194 """See IPoll."""
195 voter = self._assertEverythingOkAndGetVoter(person, when=when)
196 assert self.type == PollAlgorithm.SIMPLE
197 voteset = getUtility(IVoteSet)
198
199 if option is None and not self.allowspoilt:
200 raise ValueError("This poll doesn't allow spoilt votes.")
201 elif option is not None:
202 assert option.poll == self, (
203 "The option %r doesn't belong to this poll" % option)
204 assert option.active, "Option %r is not active" % option
205 token = voteset.newToken()
206 # This is a simple-style poll, so you can vote only on a single option
207 # and this option's preference must be 1
208 preference = 1
209 vote = voteset.new(self, option, preference, token, voter)
210 getUtility(IVoteCastSet).new(self, person)
211 return vote
212
213 def getTotalVotes(self):
214 """See IPoll."""
215 assert self.isClosed()
216 return Vote.selectBy(poll=self).count()
217
218 def getWinners(self):
219 """See IPoll."""
220 assert self.isClosed()
221 # XXX: GuilhermeSalgado 2005-08-24:
222 # For now, this method works only for SIMPLE-style polls. This is
223 # not a problem as CONDORCET-style polls are disabled.
224 assert self.type == PollAlgorithm.SIMPLE
225 query = """
226 SELECT option
227 FROM Vote
228 WHERE poll = %d AND option IS NOT NULL
229 GROUP BY option
230 HAVING COUNT(*) = (
231 SELECT COUNT(*)
232 FROM Vote
233 WHERE poll = %d
234 GROUP BY option
235 ORDER BY COUNT(*) DESC LIMIT 1
236 )
237 """ % (self.id, self.id)
238 results = Store.of(self).execute(query).get_all()
239 if not results:
240 return None
241 return [PollOption.get(id) for (id,) in results]
242
243 def getPairwiseMatrix(self):
244 """See IPoll."""
245 assert self.type == PollAlgorithm.CONDORCET
246 options = list(self.getAllOptions())
247 pairwise_matrix = []
248 for option1 in options:
249 pairwise_row = []
250 for option2 in options:
251 points_query = """
252 SELECT COUNT(*) FROM Vote as v1, Vote as v2 WHERE
253 v1.token = v2.token AND
254 v1.option = %s AND v2.option = %s AND
255 (
256 (
257 v1.preference IS NOT NULL AND
258 v2.preference IS NOT NULL AND
259 v1.preference < v2.preference
260 )
261 OR
262 (
263 v1.preference IS NOT NULL AND
264 v2.preference IS NULL
265 )
266 )
267 """ % sqlvalues(option1.id, option2.id)
268 if option1 == option2:
269 pairwise_row.append(None)
270 else:
271 points = Store.of(self).execute(points_query).get_one()[0]
272 pairwise_row.append(points)
273 pairwise_matrix.append(pairwise_row)
274 return pairwise_matrix
275
276
277class PollSet:
278 """See IPollSet."""
279
280 implements(IPollSet)
281
282 def new(self, team, name, title, proposition, dateopens, datecloses,
283 secrecy, allowspoilt, poll_type=PollAlgorithm.SIMPLE):
284 """See IPollSet."""
285 return Poll(team=team, name=name, title=title,
286 proposition=proposition, dateopens=dateopens,
287 datecloses=datecloses, secrecy=secrecy,
288 allowspoilt=allowspoilt, type=poll_type)
289
290 def selectByTeam(self, team, status=PollStatus.ALL, orderBy=None, when=None):
291 """See IPollSet."""
292 if when is None:
293 when = datetime.now(pytz.timezone('UTC'))
294
295 if orderBy is None:
296 orderBy = Poll.sortingColumns
297
298
299 status = set(status)
300 status_clauses = []
301 if PollStatus.OPEN in status:
302 status_clauses.append(AND(Poll.q.dateopens <= when,
303 Poll.q.datecloses > when))
304 if PollStatus.CLOSED in status:
305 status_clauses.append(Poll.q.datecloses <= when)
306 if PollStatus.NOT_YET_OPENED in status:
307 status_clauses.append(Poll.q.dateopens > when)
308
309 assert len(status_clauses) > 0, "No poll statuses were selected"
310
311 results = Poll.select(AND(Poll.q.teamID == team.id,
312 OR(*status_clauses)))
313
314 return results.orderBy(orderBy)
315
316 def getByTeamAndName(self, team, name, default=None):
317 """See IPollSet."""
318 query = AND(Poll.q.teamID == team.id, Poll.q.name == name)
319 try:
320 return Poll.selectOne(query)
321 except SQLObjectNotFound:
322 return default
323
324
325class PollOption(SQLBase):
326 """See IPollOption."""
327
328 implements(IPollOption)
329 _table = 'PollOption'
330 _defaultOrder = ['title', 'id']
331
332 poll = ForeignKey(dbName='poll', foreignKey='Poll', notNull=True)
333
334 name = StringCol(notNull=True)
335
336 title = StringCol(notNull=True)
337
338 active = BoolCol(notNull=True, default=False)
339
340
341class PollOptionSet:
342 """See IPollOptionSet."""
343
344 implements(IPollOptionSet)
345
346 def new(self, poll, name, title, active=True):
347 """See IPollOptionSet."""
348 return PollOption(poll=poll, name=name, title=title, active=active)
349
350 def selectByPoll(self, poll, only_active=False):
351 """See IPollOptionSet."""
352 query = PollOption.q.pollID == poll.id
353 if only_active:
354 query = AND(query, PollOption.q.active == True)
355 return PollOption.select(query)
356
357 def getByPollAndId(self, poll, option_id, default=None):
358 """See IPollOptionSet."""
359 query = AND(PollOption.q.pollID == poll.id,
360 PollOption.q.id == option_id)
361 try:
362 return PollOption.selectOne(query)
363 except SQLObjectNotFound:
364 return default
365
366
367class VoteCast(SQLBase):
368 """See IVoteCast."""
369
370 implements(IVoteCast)
371 _table = 'VoteCast'
372 _defaultOrder = 'id'
373
374 person = ForeignKey(
375 dbName='person', foreignKey='Person',
376 storm_validator=validate_public_person, notNull=True)
377
378 poll = ForeignKey(dbName='poll', foreignKey='Poll', notNull=True)
379
380
381class VoteCastSet:
382 """See IVoteCastSet."""
383
384 implements(IVoteCastSet)
385
386 def new(self, poll, person):
387 """See IVoteCastSet."""
388 return VoteCast(poll=poll, person=person)
389
390
391class Vote(SQLBase):
392 """See IVote."""
393
394 implements(IVote)
395 _table = 'Vote'
396 _defaultOrder = ['preference', 'id']
397
398 person = ForeignKey(
399 dbName='person', foreignKey='Person',
400 storm_validator=validate_public_person)
401
402 poll = ForeignKey(dbName='poll', foreignKey='Poll', notNull=True)
403
404 option = ForeignKey(dbName='option', foreignKey='PollOption')
405
406 preference = IntCol(dbName='preference')
407
408 token = StringCol(dbName='token', notNull=True, unique=True)
409
410
411class VoteSet:
412 """See IVoteSet."""
413
414 implements(IVoteSet)
415
416 def newToken(self):
417 """See IVoteSet."""
418 chars = '23456789bcdfghjkmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ'
419 length = 10
420 token = ''.join([random.choice(chars) for c in range(length)])
421 while self.getByToken(token):
422 token = ''.join([random.choice(chars) for c in range(length)])
423 return token
424
425 def new(self, poll, option, preference, token, person):
426 """See IVoteSet."""
427 return Vote(poll=poll, option=option, preference=preference,
428 token=token, person=person)
429
430 def getByToken(self, token):
431 """See IVoteSet."""
432 return Vote.selectBy(token=token)
433
434 def getVotesByOption(self, option):
435 """See IVoteSet."""
436 if option.poll.type != PollAlgorithm.SIMPLE:
437 raise OptionIsNotFromSimplePoll(
438 '%r is not an option of a simple-style poll.' % option)
439 return Vote.selectBy(option=option).count()
440
4410
=== removed directory 'lib/lp/registry/stories/team-polls'
=== removed file 'lib/lp/registry/stories/team-polls/create-poll-options.txt'
--- lib/lp/registry/stories/team-polls/create-poll-options.txt 2009-08-19 19:48:09 +0000
+++ lib/lp/registry/stories/team-polls/create-poll-options.txt 1970-01-01 00:00:00 +0000
@@ -1,85 +0,0 @@
1= Poll options =
2
3A poll can have any number of options, but these must be created
4before the poll has opened.
5
6First we create a new poll to use throughout this test.
7
8 >>> login('jeff.waugh@ubuntulinux.com')
9 >>> from zope.component import getUtility
10 >>> from lp.registry.interfaces.person import IPersonSet
11 >>> factory.makePoll(getUtility(IPersonSet).getByName('ubuntu-team'),
12 ... 'dpl-2080', 'dpl-2080', 'dpl-2080')
13 <Poll...
14 >>> logout()
15
16Our poll is not yet open, so new options can be added to it.
17
18 >>> team_admin_browser = setupBrowser(
19 ... auth='Basic jeff.waugh@ubuntulinux.com:jdub')
20 >>> team_admin_browser.open(
21 ... 'http://launchpad.dev/~ubuntu-team/+poll/dpl-2080')
22 >>> team_admin_browser.getLink('Add new option').click()
23 >>> team_admin_browser.url
24 'http://launchpad.dev/~ubuntu-team/+poll/dpl-2080/+newoption'
25
26 >>> bill_name = (
27 ... 'bill-amazingly-huge-middle-name-almost-impossible-to-read-graham')
28 >>> team_admin_browser.getControl('Name').value = bill_name
29 >>> team_admin_browser.getControl('Title').value = 'Bill Graham'
30 >>> team_admin_browser.getControl('Create').click()
31
32After adding an options we're taken back to the poll's home page.
33
34 >>> team_admin_browser.url
35 'http://launchpad.dev/~ubuntu-team/+poll/dpl-2080'
36
37And here we see the option listed as one of the active options for this
38poll.
39
40 >>> print extract_text(
41 ... find_tag_by_id(team_admin_browser.contents, 'options'))
42 Name Title Active
43 bill... Bill Graham Yes
44
45If we try to add a new option without providing a title, we'll get an error
46message because the title is required.
47
48 >>> team_admin_browser.getLink('Add new option').click()
49 >>> team_admin_browser.url
50 'http://launchpad.dev/~ubuntu-team/+poll/dpl-2080/+newoption'
51
52 >>> will_name = (
53 ... 'will-amazingly-huge-middle-name-almost-impossible-to-read-graham')
54 >>> team_admin_browser.getControl('Name').value = will_name
55 >>> team_admin_browser.getControl('Title').value = ''
56 >>> team_admin_browser.getControl('Create').click()
57
58 >>> print "\n".join(get_feedback_messages(team_admin_browser.contents))
59 There is 1 error.
60 Required input is missing.
61
62If we try to add a new option with the same name of a existing option, we
63should get a nice error message
64
65 >>> team_admin_browser.getControl('Name').value = bill_name
66 >>> team_admin_browser.getControl('Title').value = 'Bill Again'
67 >>> team_admin_browser.getControl('Create').click()
68
69 >>> print "\n".join(get_feedback_messages(team_admin_browser.contents))
70 There is 1 error.
71 ...is already in use by another option in this poll.
72
73It's not possible to add/edit a poll option after a poll is open or closed.
74That's only possible when the poll is not yet open.
75
76 >>> team_admin_browser.open(
77 ... 'http://launchpad.dev/~ubuntu-team/+poll/director-2004/+newoption')
78
79 >>> "\n".join(get_feedback_messages(team_admin_browser.contents))
80 u'You can&#8217;t add new options because the poll is already closed.'
81
82 >>> team_admin_browser.open(
83 ... 'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+newoption')
84 >>> "\n".join(get_feedback_messages(team_admin_browser.contents))
85 u'You can&#8217;t add new options because the poll is already open.'
860
=== removed file 'lib/lp/registry/stories/team-polls/create-polls.txt'
--- lib/lp/registry/stories/team-polls/create-polls.txt 2010-05-19 05:47:50 +0000
+++ lib/lp/registry/stories/team-polls/create-polls.txt 1970-01-01 00:00:00 +0000
@@ -1,163 +0,0 @@
1Let's first setup some objects that we'll need.
2
3 >>> no_priv_browser = setupBrowser(
4 ... auth='Basic no-priv@canonical.com:test')
5 >>> team_admin_browser = setupBrowser(
6 ... 'Basic jeff.waugh@ubuntulinux.com:jdub')
7
8If you're not logged in and go to the +polls page of the "Ubuntu Team"
9you'll see a link to login as a team administrator.
10
11 >>> anon_browser.open('http://launchpad.dev/~ubuntu-team')
12 >>> anon_browser.getLink('Show polls').click()
13 >>> anon_browser.url
14 'http://launchpad.dev/~ubuntu-team/+polls'
15 >>> anon_browser.getLink('Log in as an admin to set up a new poll').url
16 'http://launchpad.dev/~ubuntu-team/+login'
17
18Try to create a new poll logged in as 'no-priv', which is not a team
19administrator. There's no link leading to the +newpoll page, but the user can
20easily guess it.
21
22 >>> no_priv_browser.open('http://launchpad.dev/~ubuntu-team/+newpoll')
23 Traceback (most recent call last):
24 ...
25 Unauthorized:...
26
27Now we're logged in as Jeff Waugh which is a team administrator and thus can
28create a new poll.
29
30 >>> team_admin_browser.open('http://launchpad.dev/~ubuntu-team')
31 >>> team_admin_browser.getLink('Show polls').click()
32 >>> team_admin_browser.getLink('Set up a new poll').click()
33 >>> team_admin_browser.url
34 'http://launchpad.dev/~ubuntu-team/+newpoll'
35
36 >>> team_admin_browser.title
37 'New poll for team Ubuntu Team...
38
39First we try to create a poll with a invalid name to
40test the name field validator.
41
42 >>> team_admin_browser.getControl(
43 ... 'The unique name of this poll').value = 'election_2100'
44 >>> team_admin_browser.getControl(
45 ... 'The title of this poll').value = 'Presidential Election 2100'
46 >>> proposition = 'Who is going to be the next president?'
47 >>> team_admin_browser.getControl(
48 ... 'The proposition that is going to be voted').value = proposition
49 >>> team_admin_browser.getControl(
50 ... 'Users can spoil their votes?').selected = True
51 >>> team_admin_browser.getControl(
52 ... name='field.dateopens').value = '2100-06-04 02:00:00+00:00'
53 >>> team_admin_browser.getControl(
54 ... name='field.datecloses').value = '2100-07-04 02:00:00+00:00'
55 >>> team_admin_browser.getControl('Continue').click()
56
57 >>> print "\n".join(get_feedback_messages(team_admin_browser.contents))
58 There is 1 error.
59 Invalid name 'election_2100'. Names must be at least two characters ...
60
61We fix the name, but swap the dates. Again a nice error message.
62
63 >>> team_admin_browser.getControl(
64 ... 'The unique name of this poll').value = 'election-2100'
65 >>> team_admin_browser.getControl(
66 ... name='field.dateopens').value = '2100-07-04 02:00:00+00:00'
67 >>> team_admin_browser.getControl(
68 ... name='field.datecloses').value = '2100-06-04 02:00:00+00:00'
69 >>> team_admin_browser.getControl('Continue').click()
70
71 >>> print "\n".join(get_feedback_messages(team_admin_browser.contents))
72 There is 1 error.
73 A poll cannot close at the time (or before) it opens.
74
75Now we get it right.
76
77 >>> team_admin_browser.getControl(
78 ... name='field.dateopens').value = '2100-06-04 02:00:00+00:00'
79 >>> team_admin_browser.getControl(
80 ... name='field.datecloses').value = '2100-07-04 02:00:00+00:00'
81 >>> team_admin_browser.getControl('Continue').click()
82
83We're redirected to the newly created poll page.
84
85 >>> team_admin_browser.url
86 'http://launchpad.dev/~ubuntu-team/+poll/election-2100'
87
88Create a new poll that starts in 2025-06-04 and will last until 2035.
89
90 >>> team_admin_browser.open(
91 ... 'http://launchpad.dev/~ubuntu-team/+newpoll')
92 >>> team_admin_browser.getControl(
93 ... 'The unique name of this poll').value = 'dpl-2080'
94 >>> team_admin_browser.getControl(
95 ... 'The title of this poll').value = 'Debian Project Leader Election 2080'
96 >>> proposition = 'The next debian project leader'
97 >>> team_admin_browser.getControl(
98 ... 'The proposition that is going to be voted').value = proposition
99 >>> team_admin_browser.getControl(
100 ... 'Users can spoil their votes?').selected = True
101 >>> team_admin_browser.getControl(
102 ... name='field.dateopens').value = '2025-06-04 02:00:00+00:00'
103 >>> team_admin_browser.getControl(
104 ... name='field.datecloses').value = '2035-06-04 02:00:00+00:00'
105 >>> team_admin_browser.getControl('Continue').click()
106
107We're redirected to the newly created poll
108
109 >>> team_admin_browser.url
110 'http://launchpad.dev/~ubuntu-team/+poll/dpl-2080'
111 >>> team_admin_browser.title
112 'Debian Project Leader Election 2080 : \xe2\x80\x9cUbuntu Team\xe2\x80\x9d team'
113 >>> print_location(team_admin_browser.contents)
114 Hierarchy: ?Ubuntu Team? team > Debian Project Leader Election 2080
115 Tabs:
116 * Overview (selected) - http://launchpad.dev/~ubuntu-team
117 * Code - http://code.launchpad.dev/~ubuntu-team
118 * Bugs - http://bugs.launchpad.dev/~ubuntu-team
119 * Blueprints - http://blueprints.launchpad.dev/~ubuntu-team
120 * Translations - http://translations.launchpad.dev/~ubuntu-team
121 * Answers - http://answers.launchpad.dev/~ubuntu-team
122 Main heading: Debian Project Leader Election 2080
123 >>> team_admin_browser.getLink('add an option').url
124 'http://launchpad.dev/%7Eubuntu-team/+poll/dpl-2080/+newoption'
125
126Now lets try to insert a poll with the name of a existing one.
127
128# XXX matsubara 2006-07-17 bug=53302:
129# There's no link to get back to +polls.
130
131 >>> team_admin_browser.open(
132 ... 'http://launchpad.dev/~ubuntu-team/+newpoll')
133 >>> team_admin_browser.getControl(
134 ... 'The unique name of this poll').value = 'dpl-2080'
135 >>> team_admin_browser.getControl(
136 ... 'The title of this poll').value = 'Debian Project Leader Election 2080'
137 >>> proposition = 'The next debian project leader'
138 >>> team_admin_browser.getControl(
139 ... 'The proposition that is going to be voted').value = proposition
140 >>> team_admin_browser.getControl(
141 ... 'Users can spoil their votes?').selected = True
142 >>> team_admin_browser.getControl(
143 ... name='field.dateopens').value = '2025-06-04 02:00:00+00:00'
144 >>> team_admin_browser.getControl(
145 ... name='field.datecloses').value = '2035-06-04 02:00:00+00:00'
146 >>> team_admin_browser.getControl('Continue').click()
147
148 >>> print "\n".join(get_feedback_messages(team_admin_browser.contents))
149 There is 1 error.
150 dpl-2080 is already in use by another poll in this team.
151
152When creating a new poll, its start date must be at least 12 hours from
153now, so that the user creating it has a chance to add some options before
154the poll opens -- at that point new options cannot be added.
155
156 >>> team_admin_browser.getControl('The unique name').value = 'today'
157 >>> from datetime import datetime
158 >>> today = datetime.today().strftime('%Y-%m-%d')
159 >>> team_admin_browser.getControl(name='field.dateopens').value = today
160 >>> team_admin_browser.getControl('Continue').click()
161 >>> print "\n".join(get_feedback_messages(team_admin_browser.contents))
162 There is 1 error.
163 A poll cannot open less than 12 hours after it's created.
1640
=== removed file 'lib/lp/registry/stories/team-polls/edit-options.txt'
--- lib/lp/registry/stories/team-polls/edit-options.txt 2009-08-19 19:48:09 +0000
+++ lib/lp/registry/stories/team-polls/edit-options.txt 1970-01-01 00:00:00 +0000
@@ -1,59 +0,0 @@
1= Editing poll options =
2
3Changing the poll options detail is not possible if you are not one of the
4team's administrators:
5
6 >>> user_browser.open('http://launchpad.dev/~ubuntu-team/+polls')
7 >>> user_browser.getLink('A public poll that never closes').click()
8 >>> user_browser.url
9 'http://launchpad.dev/~ubuntu-team/+poll/never-closes4'
10 >>> print extract_text(find_tag_by_id(user_browser.contents, 'options'))
11 Name Title Active
12 OptionA OptionA Yes
13 ...
14 >>> user_browser.getLink('[Edit]')
15 Traceback (most recent call last):
16 ...
17 LinkNotFoundError
18
19And when the poll already started, administrators cannot change the options
20either:
21
22 # Need to craft the URL manually because there's no link to it -- the
23 # option can't be changed, after all.
24 >>> browser = setupBrowser(
25 ... auth='Basic jeff.waugh@ubuntulinux.com:jdub')
26 >>> browser.open('http://launchpad.dev/~ubuntu-team/+poll/never-closes4/'
27 ... '+option/20')
28 >>> print "\n".join(get_feedback_messages(browser.contents))
29 You can&#8217;t edit any options because the poll is already open.
30
31Since Jeff is an administrator of ubuntu-team and we have a poll that hasn't
32been opened yet, he should be able to edit its options.
33
34 >>> browser.open('http://launchpad.dev/~ubuntu-team/+polls')
35 >>> browser.getLink('A public poll that has not opened yet').click()
36 >>> browser.url
37 'http://launchpad.dev/~ubuntu-team/+poll/not-yet-opened'
38
39 >>> browser.getLink('[Edit]').click()
40 >>> browser.url
41 'http://launchpad.dev/~ubuntu-team/+poll/not-yet-opened/+option/...'
42
43 >>> browser.getControl('Name').value
44 'OptionX'
45 >>> browser.getControl('Title').value
46 'OptionX'
47 >>> browser.getControl('Name').value = 'option-z'
48 >>> browser.getControl('Title').value = 'Option Z'
49 >>> browser.getControl('Save').click()
50
51 >>> browser.url
52 'http://launchpad.dev/~ubuntu-team/+poll/not-yet-opened'
53 >>> print find_portlet(browser.contents, 'Voting options').renderContents()
54 <BLANKLINE>
55 <h2>Voting options</h2>
56 ...
57 ...option-z...
58 ...
59
600
=== removed file 'lib/lp/registry/stories/team-polls/edit-poll.txt'
--- lib/lp/registry/stories/team-polls/edit-poll.txt 2010-10-11 17:36:14 +0000
+++ lib/lp/registry/stories/team-polls/edit-poll.txt 1970-01-01 00:00:00 +0000
@@ -1,97 +0,0 @@
1= Editing a poll =
2
3All attributes of a poll can be changed as long as the poll has not opened
4yet.
5
6First we create a new poll to use throughout this test.
7
8 >>> login('jeff.waugh@ubuntulinux.com')
9 >>> from zope.component import getUtility
10 >>> from lp.registry.interfaces.person import IPersonSet
11 >>> factory.makePoll(getUtility(IPersonSet).getByName('ubuntu-team'),
12 ... 'dpl-2080', 'dpl-2080', 'dpl-2080')
13 <Poll...
14 >>> logout()
15
16Now we'll try to change its name to something that is already in use.
17
18 >>> team_admin_browser = setupBrowser(
19 ... auth='Basic jeff.waugh@ubuntulinux.com:jdub')
20 >>> team_admin_browser.open(
21 ... 'http://launchpad.dev/~ubuntu-team/+poll/dpl-2080')
22 >>> team_admin_browser.getLink('Change details').click()
23
24 >>> team_admin_browser.url
25 'http://launchpad.dev/~ubuntu-team/+poll/dpl-2080/+edit'
26
27 >>> team_admin_browser.getControl(
28 ... 'The unique name of this poll').value = 'never-closes'
29 >>> team_admin_browser.getControl('Save').click()
30
31 >>> print "\n".join(get_feedback_messages(team_admin_browser.contents))
32 There is 1 error.
33 ...never-closes is already in use by another poll in this team.
34
35Entering an end date that precedes the start date returns a nice error
36message.
37
38 >>> team_admin_browser.getControl(
39 ... 'The unique name of this poll').value = 'dpl-2080'
40 >>> team_admin_browser.getControl(
41 ... name='field.dateopens').value = '3000-11-01 00:00:00+00:00'
42 >>> team_admin_browser.getControl(
43 ... name='field.datecloses').value = '3000-01-01 00:00:00+00:00'
44 >>> team_admin_browser.getControl('Save').click()
45
46 >>> print "\n".join(get_feedback_messages(team_admin_browser.contents))
47 There is 1 error.
48 A poll cannot close at the time (or before) it opens.
49
50We successfully change the polls name
51
52 >>> team_admin_browser.getControl(
53 ... 'The unique name of this poll').value = 'election-3000'
54 >>> team_admin_browser.getControl(
55 ... name='field.dateopens').value = '3000-01-01 00:00:00+00:00'
56 >>> team_admin_browser.getControl(
57 ... name='field.datecloses').value = '3000-11-01 00:00:00+00:00'
58 >>> team_admin_browser.getControl('Save').click()
59
60 >>> team_admin_browser.url
61 'http://launchpad.dev/~ubuntu-team/+poll/election-3000'
62
63Trying to edit a poll that's already open isn't possible.
64
65 >>> team_admin_browser.open('http://launchpad.dev/~ubuntu-team/')
66 >>> team_admin_browser.getLink('Show polls').click()
67 >>> team_admin_browser.url
68 'http://launchpad.dev/~ubuntu-team/+polls'
69
70 >>> team_admin_browser.getLink('A random poll that never closes').click()
71 >>> team_admin_browser.url
72 'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote'
73
74 >>> team_admin_browser.open(
75 ... 'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+edit')
76 >>> print extract_text(
77 ... find_tag_by_id(team_admin_browser.contents, 'not-editable'))
78 This poll can't be edited...
79
80It's also not possible to edit a poll that's already closed.
81
82 >>> team_admin_browser.open('http://launchpad.dev/~ubuntu-team/')
83 >>> team_admin_browser.getLink('Show polls').click()
84 >>> team_admin_browser.url
85 'http://launchpad.dev/~ubuntu-team/+polls'
86
87 >>> team_admin_browser.getLink("2004 Director's Elections").click()
88 >>> team_admin_browser.url
89 'http://launchpad.dev/~ubuntu-team/+poll/director-2004'
90
91 >>> 'Voting has closed' in team_admin_browser.contents
92 True
93
94 >>> team_admin_browser.getLink('Change details').click()
95 >>> print extract_text(
96 ... find_tag_by_id(team_admin_browser.contents, 'not-editable'))
97 This poll can't be edited...
980
=== removed file 'lib/lp/registry/stories/team-polls/vote-poll.txt'
--- lib/lp/registry/stories/team-polls/vote-poll.txt 2010-10-11 17:36:14 +0000
+++ lib/lp/registry/stories/team-polls/vote-poll.txt 1970-01-01 00:00:00 +0000
@@ -1,167 +0,0 @@
1= Voting on polls =
2
3Foo Bar (a member of the ubuntu-team) wants to vote on the 'never-closes'
4poll, which is a poll with secret votes, which means he'll get a token that he
5must use to see/change his vote afterwards.
6
7 >>> browser = setupBrowser(auth='Basic foo.bar@canonical.com:test')
8 >>> browser.open('http://launchpad.dev/~ubuntu-team/+polls')
9 >>> browser.getLink('A random poll that never closes').click()
10 >>> browser.url
11 'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote'
12
13 >>> print find_tag_by_id(browser.contents, 'your-vote').renderContents()
14 <BLANKLINE>
15 ...
16 <h2>Your current vote</h2>
17 ...You have not yet voted in this poll...
18 <h2>Vote now</h2>
19 ...
20
21 >>> browser.getControl('None of these options').selected = True
22 >>> browser.getControl('Continue').click()
23
24 >>> browser.url
25 'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote'
26
27 >>> tags = find_tags_by_class(browser.contents, "informational message")
28 >>> for tag in tags:
29 ... print tag.renderContents()
30 Your vote has been recorded. If you want to view or change it later you
31 must write down this key: ...
32
33 >>> print find_tag_by_id(browser.contents, 'your-vote').renderContents()
34 <BLANKLINE>
35 ...
36 <h2>Your current vote</h2>
37 ...Your current vote is for <b> none of the options. </b>...
38 ...
39
40Foo Bar will now vote on a poll with public votes.
41
42 >>> browser.open('http://launchpad.dev/~ubuntu-team/+polls')
43 >>> browser.getLink('A public poll that never closes').click()
44 >>> browser.url
45 'http://launchpad.dev/~ubuntu-team/+poll/never-closes4/+vote'
46
47 >>> print find_tag_by_id(browser.contents, 'your-vote').renderContents()
48 <BLANKLINE>
49 ...
50 <h2>Your current vote</h2>
51 ...You have not yet voted in this poll...
52 <h2>Vote now</h2>
53 ...
54
55 >>> browser.getControl('OptionB').selected = True
56 >>> browser.getControl('Continue').click()
57
58 >>> browser.url
59 'http://launchpad.dev/~ubuntu-team/+poll/never-closes4/+vote'
60
61 >>> tags = find_tags_by_class(browser.contents, "informational message")
62 >>> for tag in tags:
63 ... print tag.renderContents()
64 Your vote was stored successfully. You can come back to this page at any
65 time before this poll closes to view or change your vote, if you want.
66
67 >>> print find_tag_by_id(browser.contents, 'your-vote').renderContents()
68 <BLANKLINE>
69 ...
70 <h2>Your current vote</h2>
71 ...Your current vote is for <b>OptionB</b>...
72 ...
73
74
75For convenience we provide an option for when the user doesn't want to vote
76yet.
77
78 >>> team_admin_browser = setupBrowser(
79 ... auth='Basic jeff.waugh@ubuntulinux.com:jdub')
80 >>> team_admin_browser.open(
81 ... 'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote')
82 >>> not_yet_voted_message = 'You have not yet voted in this poll.'
83 >>> not_yet_voted_message in team_admin_browser.contents
84 True
85
86 >>> team_admin_browser.getControl(name='newoption').value = ["donotvote"]
87 >>> team_admin_browser.getControl(name='continue').click()
88
89 >>> contents = team_admin_browser.contents
90 >>> for tag in find_tags_by_class(contents, "informational message"):
91 ... print tag.renderContents()
92 You chose not to vote yet.
93
94 >>> print find_tag_by_id(contents, 'your-vote').renderContents()
95 <BLANKLINE>
96 ...
97 <h2>Your current vote</h2>
98 ...You have not yet voted in this poll...
99 ...
100
101
102== No permission to vote ==
103
104Only members of a given team can vote on that team's polls. Other users can't,
105even if they guess the URL for the voting page.
106
107 >>> non_member_browser = setupBrowser(
108 ... auth='Basic test@canonical.com:test')
109 >>> non_member_browser.open(
110 ... 'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote')
111 >>> for tag in find_tags_by_class(
112 ... non_member_browser.contents, "informational message"):
113 ... print tag.renderContents()
114 You can&#8217;t vote in this poll because you&#8217;re not a member
115 of Ubuntu Team.
116
117
118== Closed polls ==
119
120It's not possible to vote on closed polls, even if we manually craft the URL.
121
122 >>> team_admin_browser.open(
123 ... 'http://launchpad.dev/~ubuntu-team/+poll/leader-2004')
124 >>> print find_tag_by_id(
125 ... team_admin_browser.contents, 'maincontent').renderContents()
126 <BLANKLINE>
127 ...
128 <h2>Voting has closed</h2>
129 ...
130
131 >>> team_admin_browser.open(
132 ... 'http://launchpad.dev/~ubuntu-team/+poll/leader-2004/+vote')
133 >>> print find_tag_by_id(
134 ... team_admin_browser.contents, 'maincontent').renderContents()
135 <BLANKLINE>
136 ...
137 <p class="informational message">
138 This poll is already closed.
139 </p>
140 ...
141
142 >>> team_admin_browser.getControl(name='continue')
143 Traceback (most recent call last):
144 ...
145 LookupError: name 'continue'
146
147The same is true for condorcet polls too.
148
149 >>> team_admin_browser.open(
150 ... 'http://launchpad.dev/~ubuntu-team/+poll/director-2004')
151 >>> print find_tag_by_id(
152 ... team_admin_browser.contents, 'maincontent').renderContents()
153 <BLANKLINE>
154 ...
155 <h2>Voting has closed</h2>
156 ...
157
158 >>> team_admin_browser.getControl(name='continue')
159 Traceback (most recent call last):
160 ...
161 LookupError: name 'continue'
162
163 >>> team_admin_browser.open(
164 ... 'http://launchpad.dev/~ubuntu-team/+poll/director-2004/+vote')
165 >>> for message in get_feedback_messages(team_admin_browser.contents):
166 ... print message
167 This poll is already closed.
1680
=== removed file 'lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt'
--- lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt 2010-10-11 17:36:14 +0000
+++ lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt 1970-01-01 00:00:00 +0000
@@ -1,229 +0,0 @@
1# XXX Guilherme Salgado, 2006-01-19:
2# Merge this test with team-polls/xx-votepoll.txt
3
4 Go to a condorcet-style poll (which is still open) and check that apart
5 from seeing our vote we can also change it.
6
7 >>> print http(r"""
8 ... GET /~ubuntu-team/+poll/never-closes2 HTTP/1.1
9 ... Accept-Language: en-us,en;q=0.5
10 ... Authorization: Basic foo.bar@canonical.com:test
11 ... """)
12 HTTP/1.1 303 See Other
13 ...
14 Location: http://localhost/~ubuntu-team/+poll/never-closes2/+vote
15 ...
16
17 >>> print http(r"""
18 ... GET /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
19 ... Authorization: Basic foo.bar@canonical.com:test
20 ... """)
21 HTTP/1.1 200 Ok
22 ...
23 ...You must enter your vote key...
24 ...This is a secret poll...
25 ...your vote is identified only by the key you...
26 ...were given when you voted. To view or change your vote you must enter...
27 ...your key:...
28 ...
29
30
31 If a non-member (Sample Person) guesses the voting URL and tries to vote,
32 he won't be allowed.
33
34 >>> print http(r"""
35 ... GET /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
36 ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
37 ... """)
38 HTTP/1.1 200 Ok
39 ...You can&#8217;t vote in this poll because you&#8217;re not...
40 ...a member of Ubuntu Team...
41
42
43 By providing the token we will be able to see our current vote.
44
45 >>> print http(r"""
46 ... POST /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
47 ... Authorization: Basic foo.bar@canonical.com:test
48 ... Content-Type: application/x-www-form-urlencoded
49 ...
50 ... token=xn9FDCTp4m&showvote=Show+My+Vote&option_12=&option_13=&option_14=&option_15=""")
51 HTTP/1.1 200 Ok
52 ...
53 <p>Your current vote is as follows:</p>
54 <p>
55 <BLANKLINE>
56 </p>
57 <p>
58 <BLANKLINE>
59 <b>1</b>.
60 Option 1
61 <BLANKLINE>
62 </p>
63 <p>
64 <BLANKLINE>
65 <b>2</b>.
66 Option 2
67 <BLANKLINE>
68 </p>
69 <p>
70 <BLANKLINE>
71 <b>3</b>.
72 Option 4
73 <BLANKLINE>
74 </p>
75 ...
76
77
78 It's also possible to change the vote, if wanted.
79
80 >>> print http(r"""
81 ... POST /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
82 ... Authorization: Basic foo.bar@canonical.com:test
83 ... Content-Type: application/x-www-form-urlencoded
84 ...
85 ... token=xn9FDCTp4m&option_12=2&option_13=3&option_14=4&option_15=1&changevote=Change+Vote""")
86 HTTP/1.1 200 Ok
87 ...
88 ...Your vote was changed successfully.</p>
89 ...
90 <p>Your current vote is as follows:</p>
91 <p>
92 <BLANKLINE>
93 <b>1</b>.
94 Option 4
95 <BLANKLINE>
96 </p>
97 <p>
98 <BLANKLINE>
99 <b>2</b>.
100 Option 1
101 <BLANKLINE>
102 </p>
103 <p>
104 <BLANKLINE>
105 <b>3</b>.
106 Option 2
107 <BLANKLINE>
108 </p>
109 <p>
110 <BLANKLINE>
111 <b>4</b>.
112 Option 3
113 <BLANKLINE>
114 </p>
115 ...
116
117
118 Now we go to another poll in which name16 voted. But this time it's a public
119 one, so there's no need to provide the token to see the current vote.
120
121 >>> print http(r"""
122 ... GET /~ubuntu-team/+poll/never-closes3 HTTP/1.1
123 ... Authorization: Basic foo.bar@canonical.com:test
124 ... """)
125 HTTP/1.1 303 See Other
126 ...
127 Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote
128 ...
129
130 >>> print http(r"""
131 ... GET /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
132 ... Authorization: Basic foo.bar@canonical.com:test
133 ... """)
134 HTTP/1.1 200 Ok
135 ...
136 <p>Your current vote is as follows:</p>
137 <p>
138 <BLANKLINE>
139 <b>1</b>.
140 Option 1
141 <BLANKLINE>
142 </p>
143 <p>
144 <BLANKLINE>
145 <b>2</b>.
146 Option 2
147 <BLANKLINE>
148 </p>
149 <p>
150 <BLANKLINE>
151 <b>3</b>.
152 Option 3
153 <BLANKLINE>
154 </p>
155 <p>
156 <BLANKLINE>
157 <b>4</b>.
158 Option 4
159 <BLANKLINE>
160 </p>
161 ...
162
163
164 Now we change the vote and we see the new vote displayed as our current
165 vote.
166
167 >>> print http(r"""
168 ... POST /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
169 ... Authorization: Basic foo.bar@canonical.com:test
170 ... Content-Type: application/x-www-form-urlencoded
171 ...
172 ... option_16=4&option_17=2&option_18=1&option_19=3&changevote=Change+Vote""")
173 HTTP/1.1 200 Ok
174 ...
175 <p>Your current vote is as follows:</p>
176 <p>
177 <BLANKLINE>
178 <b>1</b>.
179 Option 3
180 <BLANKLINE>
181 </p>
182 <p>
183 <BLANKLINE>
184 <b>2</b>.
185 Option 2
186 <BLANKLINE>
187 </p>
188 <p>
189 <BLANKLINE>
190 <b>3</b>.
191 Option 4
192 <BLANKLINE>
193 </p>
194 <p>
195 <BLANKLINE>
196 <b>4</b>.
197 Option 1
198 <BLANKLINE>
199 </p>
200 ...
201
202
203 Logged in as mark@example.com (which is a member of ubuntu-team), go to a public
204 condorcet-style poll that's still open and get redirected to a page where
205 it's possible to vote (and see the current vote).
206
207 >>> print http(r"""
208 ... GET /~ubuntu-team/+poll/never-closes3 HTTP/1.1
209 ... Authorization: Basic mark@example.com:test
210 ... """)
211 HTTP/1.1 303 See Other
212 ...
213 Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote
214 ...
215
216
217 And here we'll see the form which says you haven't voted yet and allows you
218 to vote.
219
220 >>> print http(r"""
221 ... GET /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
222 ... Authorization: Basic mark@example.com:test
223 ... """)
224 HTTP/1.1 200 Ok
225 ...
226 ...Your current vote...
227 ...You have not yet voted in this poll...
228 ...Rank options in order...
229 ...
2300
=== removed file 'lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt'
--- lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt 2009-08-21 18:46:34 +0000
+++ lib/lp/registry/stories/team-polls/xx-poll-confirm-vote.txt 1970-01-01 00:00:00 +0000
@@ -1,89 +0,0 @@
1 Logged in as 'jdub' (which voted in the director-2004 poll), let's see the
2 results of the director-2004 poll.
3
4 >>> print http(r"""
5 ... GET /~ubuntu-team/+poll/director-2004 HTTP/1.1
6 ... Authorization: Basic amVmZi53YXVnaEB1YnVudHVsaW51eC5jb206amR1Yg==
7 ... """)
8 HTTP/1.1 200 Ok
9 ...
10 ...2004 Director's Elections...
11 ...
12 ...This was a secret poll: your vote is identified only by the key...
13 ...you were given when you voted. To view your vote you must enter...
14 ...your key:...
15 ...Results...
16 ...This is the pairwise matrix for this poll...
17 ...
18
19
20 Now let's see if jdub's vote was stored correctly, by entering the token he
21 got when voting.
22
23 >>> print http(r"""
24 ... POST /~ubuntu-team/+poll/director-2004 HTTP/1.1
25 ... Authorization: Basic amVmZi53YXVnaEB1YnVudHVsaW51eC5jb206amR1Yg==
26 ... Content-Type: application/x-www-form-urlencoded
27 ...
28 ... token=9WjxQq2V9p&showvote=Show+My+Vote""")
29 HTTP/1.1 200 Ok
30 ...
31 <p>Your vote was as follows:</p>
32 <p>
33 <BLANKLINE>
34 <b>1</b>.
35 D
36 <BLANKLINE>
37 </p>
38 <p>
39 <BLANKLINE>
40 <b>2</b>.
41 B
42 <BLANKLINE>
43 </p>
44 <p>
45 <BLANKLINE>
46 <b>3</b>.
47 A
48 <BLANKLINE>
49 </p>
50 <p>
51 <BLANKLINE>
52 <b>3</b>.
53 C
54 <BLANKLINE>
55 </p>
56 ...
57
58
59 Now we'll see the results of the leader-2004 poll, in which jdub also
60 voted.
61
62 >>> print http(r"""
63 ... GET /~ubuntu-team/+poll/leader-2004 HTTP/1.1
64 ... Authorization: Basic amVmZi53YXVnaEB1YnVudHVsaW51eC5jb206amR1Yg==
65 ... """)
66 HTTP/1.1 200 Ok
67 ...
68 ...2004 Leader's Elections...
69 ...
70 ...This was a secret poll: your vote is identified only by the key...
71 ...you were given when you voted. To view your vote you must enter...
72 ...your key:...
73 ...
74
75
76 And now we confirm his vote on this poll too.
77
78 >>> print http(r"""
79 ... POST /~ubuntu-team/+poll/leader-2004 HTTP/1.1
80 ... Authorization: Basic amVmZi53YXVnaEB1YnVudHVsaW51eC5jb206amR1Yg==
81 ... Content-Type: application/x-www-form-urlencoded
82 ...
83 ... token=W7gR5mjNrX&showvote=Show+My+Vote""")
84 HTTP/1.1 200 Ok
85 ...
86 <p>Your vote was for
87 <BLANKLINE>
88 <b>Jack Crawford</b></p>
89 ...
900
=== removed file 'lib/lp/registry/stories/team-polls/xx-poll-results.txt'
--- lib/lp/registry/stories/team-polls/xx-poll-results.txt 2009-11-15 18:21:10 +0000
+++ lib/lp/registry/stories/team-polls/xx-poll-results.txt 1970-01-01 00:00:00 +0000
@@ -1,69 +0,0 @@
1First we check all polls of 'ubuntu-team'.
2
3 >>> anon_browser.open("http://launchpad.dev/~ubuntu-team")
4 >>> anon_browser.getLink('Show polls').click()
5 >>> print find_main_content(anon_browser.contents)
6 <...
7 ...Current polls...
8 ...A random poll that never closes...
9 ...A second random poll that never closes...
10 ...A third random poll that never closes...
11 ...Closed polls...
12 ...2004 Director's Elections...
13 ...2004 Leader's Elections...
14
15
16 Check the results of a closed simple-style poll.
17
18 >>> anon_browser.open("http://launchpad.dev/~ubuntu-team/+poll/leader-2004")
19 >>> print find_main_content(anon_browser.contents)
20 <...
21 ...Who's going to be the next leader?...
22 ...Results...
23 ...
24 <td>
25 Francis Dolarhyde
26 <BLANKLINE>
27 </td>
28 <td>1</td>
29 ...
30 <td>
31 Jack Crawford
32 <BLANKLINE>
33 </td>
34 <td>1</td>
35 ...
36 <td>
37 Will Graham
38 <BLANKLINE>
39 </td>
40 <td>2</td>
41 ...
42
43
44 Check the results of a closed condorcet-style poll.
45
46 >>> anon_browser.open("http://launchpad.dev/~ubuntu-team/+poll/director-2004")
47 >>> print find_main_content(anon_browser.contents)
48 <...
49 ...Who's going to be the next director?...
50 ...Results...
51 ...
52 ...A...
53 ...2...
54 ...2...
55 ...2...
56 ...B...
57 ...2...
58 ...2...
59 ...2...
60 ...C...
61 ...1...
62 ...1...
63 ...1...
64 ...D...
65 ...2...
66 ...1...
67 ...2...
68 ...
69
700
=== modified file 'lib/lp/registry/stories/team/xx-team-home.txt'
--- lib/lp/registry/stories/team/xx-team-home.txt 2010-04-22 17:18:29 +0000
+++ lib/lp/registry/stories/team/xx-team-home.txt 2010-12-16 14:48:27 +0000
@@ -1,4 +1,5 @@
1= A team's home page =1A team's home page
2==================
23
3The home page of a public team is visible to everyone.4The home page of a public team is visible to everyone.
45
@@ -59,17 +60,6 @@
59 Languages:60 Languages:
60 English61 English
6162
62The polls portlet is only shown if current polls exist.
63
64 >>> print extract_text(find_tag_by_id(browser.contents, 'polls'))
65 Polls
66 A random poll that never closes...
67 Show polls
68
69 >>> browser.open('http://launchpad.dev/~launchpad')
70 >>> print find_tag_by_id(browser.contents, 'polls')
71 None
72
73The subteam-of portlet is not shown if the team is not a subteam.63The subteam-of portlet is not shown if the team is not a subteam.
7464
75 >>> browser.open('http://launchpad.dev/~ubuntu-team')65 >>> browser.open('http://launchpad.dev/~ubuntu-team')
@@ -190,7 +180,8 @@
190 ...180 ...
191181
192182
193== Team admins ==183Team admins
184-----------
194185
195Team owners and admins can see a link to approve and decline applicants.186Team owners and admins can see a link to approve and decline applicants.
196187
@@ -207,7 +198,8 @@
207 <Link text='Approve or decline members' url='.../+editproposedmembers'>198 <Link text='Approve or decline members' url='.../+editproposedmembers'>
208199
209200
210== Non members ==201Non members
202-----------
211203
212No Privileges Person is not a member of the Ubuntu team.204No Privileges Person is not a member of the Ubuntu team.
213205
@@ -220,7 +212,8 @@
220He can see the contact address, and the link explains the email212He can see the contact address, and the link explains the email
221will actually go to the team's administrators.213will actually go to the team's administrators.
222214
223 >>> print extract_text(find_tag_by_id(user_browser.contents, 'contact-email'))215 >>> print extract_text(
216 ... find_tag_by_id(user_browser.contents, 'contact-email'))
224 Email:217 Email:
225 support@ubuntu.com218 support@ubuntu.com
226 >>> content = find_tag_by_id(user_browser.contents, 'contact-user')219 >>> content = find_tag_by_id(user_browser.contents, 'contact-user')
227220
=== removed file 'lib/lp/registry/templates/poll-edit.pt'
--- lib/lp/registry/templates/poll-edit.pt 2009-08-18 20:24:56 +0000
+++ lib/lp/registry/templates/poll-edit.pt 1970-01-01 00:00:00 +0000
@@ -1,34 +0,0 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9
10<body>
11
12 <div metal:fill-slot="main">
13
14 <div tal:condition="context/isNotYetOpened">
15 <div metal:use-macro="context/@@launchpad_form/form">
16 <h1 metal:fill-slot="heading">
17 Edit poll &#8220;<span tal:replace="context/title" />&#8221;
18 </h1>
19 </div>
20 </div>
21
22 <div tal:condition="not: context/isNotYetOpened" id="not-editable">
23 <h1>This poll can't be edited</h1>
24
25 <p>Only polls that are not yet opened can be edited. As soon as a poll
26 opens it can't be edited anymore.</p>
27 </div>
28
29 <tal:menu replace="structure view/@@+related-pages" />
30
31 </div>
32
33</body>
34</html>
350
=== removed file 'lib/lp/registry/templates/poll-index.pt'
--- lib/lp/registry/templates/poll-index.pt 2009-08-19 19:48:09 +0000
+++ lib/lp/registry/templates/poll-index.pt 1970-01-01 00:00:00 +0000
@@ -1,207 +0,0 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_side"
7 i18n:domain="launchpad"
8>
9<body>
10
11<tal:heading metal:fill-slot="heading">
12 <h1 tal:content="context/title">Mozilla</h1>
13</tal:heading>
14
15<div metal:fill-slot="main">
16
17 <tal:do-this-first replace="view/setUpTokenAndVotes" />
18
19 <div
20 class="highlighted"
21 tal:content="structure context/proposition/fmt:text-to-html"
22 />
23 <br />
24
25 <p tal:condition="not: context/getActiveOptions">
26 This poll does not yet have any voting options. Please <a
27 href="+newoption">add an option</a>. Note, you need more than one option
28 for a real poll, of course :-)
29 </p>
30
31 <div class="two-column-list">
32 <dl>
33 <dt>Opens:</dt>
34 <dd
35 tal:attributes="title context/dateopens/fmt:datetime"
36 tal:content="context/dateopens/fmt:approximatedate" />
37 </dl>
38
39 <dl>
40 <dt>Type:</dt>
41 <dd tal:content="context/type/title" />
42 </dl>
43
44 <dl>
45 <dt>Closes:</dt>
46 <dd
47 tal:attributes="title context/datecloses/fmt:datetime"
48 tal:content="context/datecloses/fmt:approximatedate" />
49 </dl>
50
51 <dl>
52 <dt>Secrecy:</dt>
53 <dd tal:content="context/secrecy/title" />
54 </dl>
55 </div>
56 <br />
57
58 <tal:details replace="structure context/@@+portlet-options" />
59 <br />
60
61 <tal:is_open condition="context/isOpen">
62 <p tal:condition="not: request/lp:person">
63 You need to <a href="+login">login to vote</a>.
64 </p>
65 </tal:is_open>
66
67 <tal:block condition="context/isClosed">
68
69 <h2>Voting has closed</h2>
70
71 <p>Voting closed
72 <span
73 tal:attributes="title context/datecloses/fmt:datetime"
74 tal:content="context/datecloses/fmt:displaydate" />.
75 </p>
76
77 <tal:block condition="view/userVoted">
78 <tal:block condition="view/isSecret">
79 <tal:block condition="not: view/gotTokenAndVotes">
80 <p>
81 This was a secret poll: your vote is identified only by the key
82 you were given when you voted. To view your vote you must enter
83 your key:
84 </p>
85 <form action="" method="POST">
86 <input type="text" name="token" />
87 <input type="submit" value="Show My Vote" name="showvote" />
88 </form>
89 </tal:block>
90 </tal:block>
91
92 <tal:block condition="view/gotTokenAndVotes">
93 <tal:block condition="view/isSimple">
94 <p>Your vote was for
95 <b tal:condition="not: view/currentVote/option">
96 none of the options.
97 </b>
98 <b tal:condition="view/currentVote/option"
99 tal:content="view/currentVote/option/name" /></p>
100 </tal:block>
101
102 <tal:block condition="view/isCondorcet">
103 <tal:block condition="view/currentVotes">
104 <p>Your vote was as follows:</p>
105 <p tal:repeat="vote view/currentVotes">
106 <tal:block tal:condition="vote/preference">
107 <b tal:content="vote/preference" />.
108 <span tal:replace="vote/option/name" />
109 </tal:block>
110 </p>
111 </tal:block>
112
113 <tal:block condition="not: view/currentVotes">
114 <p>You haven't voted for any of the existing options.</p>
115 </tal:block>
116 </tal:block>
117
118 </tal:block>
119 </tal:block>
120
121 <h2>Results</h2>
122
123 <tal:block condition="view/isSimple">
124 <tal:block define="winners context/getWinners">
125 <p tal:condition="winners">The winner(s) of this poll is(are)
126 <tal:block repeat="winner winners">
127 <b tal:content="winner/title"
128 /><span tal:condition="not: repeat/winner/end">,</span>
129 </tal:block>
130 </p>
131
132 <p tal:condition="not: winners">This poll has no winner(s).</p>
133 </tal:block>
134
135 <p>Here are the number of votes each option received.</p>
136 <table class="listing">
137 <thead>
138 <tr>
139 <th>Option</th>
140 <th>Votes</th>
141 </tr>
142 </thead>
143
144 <tr tal:repeat="option context/getAllOptions">
145 <tal:block define="votes python: view.getVotesByOption(option)">
146 <td>
147 <span tal:replace="option/title" />
148 <tal:block tal:condition="not: option/active">
149 (Inactive)
150 </tal:block>
151 </td>
152 <td tal:content="votes">
153 </td>
154 </tal:block>
155 </tr>
156 </table>
157 </tal:block>
158
159 <tal:block condition="view/isCondorcet">
160 <p>This is the pairwise matrix for this poll.</p>
161
162 <table border="2"
163 tal:define="pairwise_matrix view/getPairwiseMatrixWithHeaders">
164 <tr tal:repeat="row pairwise_matrix">
165 <tal:block repeat="column pairwise_matrix">
166 <tal:block tal:define="x repeat/row/index; y repeat/column/index">
167 <td tal:condition="python: x == y"
168 style="background-color: black" />
169
170 <tal:block condition="python: x != y">
171 <td tal:condition="python: x != 0 and y != 0"
172 style="text-align: right">
173 <span tal:replace="python: pairwise_matrix[x][y]" />
174 </td>
175 <td tal:condition="python: x == 0 or y == 0">
176 <span tal:replace="python: pairwise_matrix[x][y]" />
177 </td>
178 </tal:block>
179 </tal:block>
180 </tal:block>
181 </tr>
182 </table>
183 </tal:block>
184
185 </tal:block>
186
187 <tal:block condition="context/isNotYetOpened">
188 <h2>Voting hasn't opened yet</h2>
189
190 <p>
191 The vote will commence
192 <span
193 tal:attributes="title context/dateopens/fmt:datetime"
194 tal:content="context/dateopens/fmt:displaydate" />.
195 </p>
196 </tal:block>
197
198</div>
199
200<div metal:fill-slot="side">
201 <div id="object-actions" class="top-portlet">
202 <tal:menu replace="structure view/@@+global-actions" />
203 </div>
204</div>
205
206</body>
207</html>
2080
=== removed file 'lib/lp/registry/templates/poll-newoption.pt'
--- lib/lp/registry/templates/poll-newoption.pt 2009-08-19 19:48:09 +0000
+++ lib/lp/registry/templates/poll-newoption.pt 1970-01-01 00:00:00 +0000
@@ -1,36 +0,0 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9
10<body>
11
12 <div metal:fill-slot="main">
13
14 <tal:block condition="context/isNotYetOpened">
15 <div metal:use-macro="context/@@launchpad_form/form">
16
17 <h1 metal:fill-slot="heading">
18 Add a poll option
19 </h1>
20
21 </div>
22 </tal:block>
23
24 <tal:block condition="not: context/isNotYetOpened">
25 <p class="error message" tal:condition="context/isClosed">
26 You can&#8217;t add new options because the poll is already closed.
27 </p>
28 <p class="error message" tal:condition="context/isOpen">
29 You can&#8217;t add new options because the poll is already open.
30 </p>
31 </tal:block>
32
33 </div>
34
35</body>
36</html>
370
=== removed file 'lib/lp/registry/templates/poll-portlet-details.pt'
--- lib/lp/registry/templates/poll-portlet-details.pt 2009-07-17 17:59:07 +0000
+++ lib/lp/registry/templates/poll-portlet-details.pt 1970-01-01 00:00:00 +0000
@@ -1,38 +0,0 @@
1<tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 omit-tag="">
6
7<div class="portlet" id="portlet-details">
8 <h2><span tal:replace="context/name" /></h2>
9
10 <div class="portletBody portletContent">
11
12 <b>Title:</b>
13 <span tal:replace="context/title" /><br />
14
15 <b>Voting team:</b>
16 <a tal:attributes="href context/team/fmt:url"
17 tal:content="context/team/displayname" /><br />
18
19 <b>Opens:</b>
20 <span
21 tal:attributes="title context/dateopens/fmt:datetime"
22 tal:content="context/dateopens/fmt:approximatedate" /><br />
23
24 <b>Closes:</b>
25 <span
26 tal:attributes="title context/datecloses/fmt:datetime"
27 tal:content="context/datecloses/fmt:approximatedate" /><br />
28
29 <b>Type:</b>
30 <span tal:replace="context/type/title" /><br />
31
32 <b>Secrecy:</b>
33 <span tal:replace="context/secrecy/title" /><br />
34
35 </div>
36
37</div>
38</tal:root>
390
=== removed file 'lib/lp/registry/templates/poll-portlet-options.pt'
--- lib/lp/registry/templates/poll-portlet-options.pt 2009-08-20 18:25:13 +0000
+++ lib/lp/registry/templates/poll-portlet-options.pt 1970-01-01 00:00:00 +0000
@@ -1,46 +0,0 @@
1<tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 omit-tag="">
6
7<div class="portlet" id="portlet-options">
8
9 <h2>Voting options</h2>
10 <tal:block condition="context/getAllOptions">
11 <table class="listing" id="options">
12 <thead>
13 <tr>
14 <th>Name</th>
15 <th>Title</th>
16 <th>Active</th>
17 <th tal:condition="context/required:launchpad.Edit"></th>
18 </tr>
19 </thead>
20 <tr tal:repeat="polloption context/getAllOptions">
21 <td tal:content="polloption/name">mjg59</td>
22 <td tal:content="polloption/title/fmt:break-long-words">
23 This guy rocks!
24 </td>
25 <td>
26 <tal:is_active condition="polloption/active">Yes</tal:is_active>
27 <tal:inactive condition="not: polloption/active">No</tal:inactive>
28 </td>
29 <td tal:condition="context/required:launchpad.Edit">
30 <a tal:attributes="href polloption/fmt:url"
31 ><img src="/@@/edit" alt="[Edit]"
32 title="Change this option details" /></a>
33 </td>
34 </tr>
35 </table>
36 </tal:block>
37
38 <p class="warning message" tal:condition="not: context/getAllOptions">
39 This poll doesn't have any options for people to vote on yet.
40 Make sure you add some options before the poll opens!
41 </p>
42
43 <tal:new-option replace="structure context/menu:overview/addnew/render" />
44
45</div>
46</tal:root>
470
=== removed file 'lib/lp/registry/templates/poll-vote-condorcet.pt'
--- lib/lp/registry/templates/poll-vote-condorcet.pt 2009-08-19 19:48:09 +0000
+++ lib/lp/registry/templates/poll-vote-condorcet.pt 1970-01-01 00:00:00 +0000
@@ -1,130 +0,0 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9<body>
10
11 <tal:heading metal:fill-slot="heading">
12 <h1 tal:content="context/title">Mozilla</h1>
13 </tal:heading>
14
15 <div metal:fill-slot="main">
16
17 <tal:open-poll condition="context/isOpen">
18 <tal:can-vote condition="view/userCanVote">
19 <p
20 tal:condition="view/feedback"
21 tal:content="view/feedback"
22 class="informational message"
23 />
24
25 <div class="highlighted" style="font-size: 80%;">
26 <tal:proposition replace="structure context/proposition/fmt:text-to-html">
27 The proposition goes here.
28 </tal:proposition>
29 </div>
30
31 <form action="" method="POST">
32
33 <tal:block condition="view/userVoted">
34 <tal:block condition="view/isSecret">
35 <h2>You must enter your vote key</h2>
36
37 <p>This is a secret poll &mdash;
38 your vote is identified only by the key you
39 were given when you voted. To view or change your vote you must enter
40 your key:</p>
41
42 <input type="text" name="token"
43 tal:attributes="value view/token|nothing" />
44 <br />
45 <br />
46 </tal:block>
47 </tal:block>
48
49 <table cols="2" id="your-vote">
50 <tr>
51 <td>
52 <h2>Your current vote</h2>
53 <tal:block condition="not: view/userVoted">
54 <p>You have not yet voted in this poll.</p>
55 </tal:block>
56
57 <tal:block condition="view/userVoted">
58 <tal:block condition="view/gotTokenAndVotes">
59 <tal:block condition="view/currentVotes">
60 <p>Your current vote is as follows:</p>
61 <p tal:repeat="vote view/currentVotes">
62 <tal:block tal:condition="vote/preference">
63 <b tal:content="vote/preference" />.
64 <span tal:replace="vote/option/name" />
65 </tal:block>
66 </p>
67 </tal:block>
68
69 <tal:block condition="not: view/currentVotes">
70 <p>You haven't manifested preference for any of the existing
71 options.</p>
72 </tal:block>
73 </tal:block>
74
75 <tal:block condition="not: view/gotTokenAndVotes">
76 <p>You have voted in this poll. Launchpad can display your vote
77 once you have entered your vote key.</p>
78
79 <input type="submit" value="Show My Vote" name="showvote" />
80 </tal:block>
81 </tal:block>
82 </td>
83 <td>
84 <tal:block condition="not: view/userVoted">
85 <h2>Rank options in order of preference</h2>
86 </tal:block>
87
88 <tal:block condition="view/userVoted">
89 <h2>Change your vote</h2>
90 </tal:block>
91
92 <p>Enter 1 next to your most preferred option, 2 next to your second
93 preference, and so on. You may mark two or more options equally, or
94 leave some options unmarked, if desired.</p>
95
96 <tal:block repeat="option context/getActiveOptions">
97 <input type="text" size="2"
98 tal:attributes="name string:option_${option/id}" />
99 <span tal:replace="option/name" />
100 <br />
101 </tal:block>
102 <br />
103
104 <tal:block condition="view/userVoted">
105 <input type="submit" value="Change Vote" name="changevote" />
106 </tal:block>
107
108 <tal:block condition="not: view/userVoted">
109 <input type="submit" value="Vote" name="vote" />
110 </tal:block>
111 or <a tal:attributes="href context/team/fmt:url/+polls">Cancel</a>
112 </td>
113 </tr>
114 </table>
115 </form>
116 </tal:can-vote>
117
118 <p tal:condition="not: view/userCanVote" class="informational message">
119 You can&#8217;t vote in this poll because you&#8217;re not
120 a member of <span tal:replace="context/team/displayname" />.
121 </p>
122 </tal:open-poll>
123
124 <p tal:condition="not: context/isOpen" class="informational message">
125 This poll is already closed.
126 </p>
127
128 </div>
129</body>
130</html>
1310
=== removed file 'lib/lp/registry/templates/poll-vote-simple.pt'
--- lib/lp/registry/templates/poll-vote-simple.pt 2009-08-19 19:48:09 +0000
+++ lib/lp/registry/templates/poll-vote-simple.pt 1970-01-01 00:00:00 +0000
@@ -1,141 +0,0 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9<body>
10
11 <tal:heading metal:fill-slot="heading">
12 <h1 tal:content="context/title">Mozilla</h1>
13 </tal:heading>
14
15 <div metal:fill-slot="main">
16
17 <tal:open-poll condition="context/isOpen">
18
19 <tal:can-vote condition="view/userCanVote">
20 <p
21 tal:condition="view/feedback"
22 tal:content="view/feedback"
23 class="informational message"
24 />
25
26 <div
27 class="highlighted"
28 tal:content="structure context/proposition/fmt:text-to-html"
29 />
30
31 <form action="" method="POST">
32
33 <tal:block condition="view/userVoted">
34 <tal:block condition="view/isSecret">
35 <h2>You must enter your vote key</h2>
36
37 <p>This is a secret poll &mdash;
38 your vote is identified only by the key you
39 were given when you voted. To view or change your vote you must enter
40 your key:</p>
41
42 <input type="text" name="token"
43 tal:attributes="value view/token|nothing" />
44 <br />
45 <br />
46 </tal:block>
47 </tal:block>
48
49 <table cols="2" id="your-vote">
50 <tr>
51 <td>
52 <h2>Your current vote</h2>
53 <tal:block condition="not: view/userVoted">
54 <p>You have not yet voted in this poll.</p>
55 </tal:block>
56
57 <tal:block condition="view/userVoted">
58 <tal:block condition="view/gotTokenAndVotes">
59 <p>Your current vote is for
60 <b tal:condition="not: view/currentVote/option">
61 none of the options.
62 </b>
63 <b tal:condition="view/currentVote/option"
64 tal:content="view/currentVote/option/name" />
65 </p>
66 </tal:block>
67
68 <tal:block condition="not: view/gotTokenAndVotes">
69 <p>You have voted in this poll. Launchpad can display your vote
70 once you have entered your vote key.</p>
71
72 <input type="submit" value="Show My Vote" name="showvote" />
73 </tal:block>
74 </tal:block>
75 </td>
76 <td>
77 <tal:block condition="not: view/userVoted">
78 <h2>Vote now</h2>
79 <p>Choose one option</p>
80
81 <label>
82 <input type="radio" name="newoption" value="donotvote"
83 checked="checked" />
84 I'm not voting yet
85 </label>
86 <br />
87 </tal:block>
88
89 <tal:block condition="view/userVoted">
90 <h2>Change your vote</h2>
91 <p>Choose one option</p>
92
93 <label>
94 <input type="radio" name="newoption" value="donotchange"
95 checked="checked" />
96 Don't change my vote
97 </label>
98 <br />
99 </tal:block>
100
101 <tal:block repeat="option context/getActiveOptions">
102 <label>
103 <input type="radio" name="newoption"
104 tal:attributes="value option/id" />
105 <span tal:replace="option/name" />
106 </label>
107 <br />
108 </tal:block>
109
110 <tal:block condition="context/allowspoilt">
111 <label>
112 <input type="radio" name="newoption" value="none" />
113 None of these options
114 </label>
115 <br />
116 </tal:block>
117 <br />
118
119 <input type="submit" value="Continue" name="continue" />
120 or <a tal:attributes="href context/team/fmt:url/+polls">Cancel</a>
121 </td>
122 </tr>
123 </table>
124 </form>
125 </tal:can-vote>
126
127 <p tal:condition="not: view/userCanVote" class="informational message">
128 You can&#8217;t vote in this poll because you&#8217;re not
129 a member of <span tal:replace="context/team/displayname" />.
130 </p>
131
132 </tal:open-poll>
133
134 <p tal:condition="not: context/isOpen" class="informational message">
135 This poll is already closed.
136 </p>
137
138 </div>
139
140</body>
141</html>
1420
=== removed file 'lib/lp/registry/templates/polloption-edit.pt'
--- lib/lp/registry/templates/polloption-edit.pt 2009-08-18 20:24:56 +0000
+++ lib/lp/registry/templates/polloption-edit.pt 1970-01-01 00:00:00 +0000
@@ -1,37 +0,0 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9
10<body>
11
12 <div metal:fill-slot="main">
13
14 <tal:block condition="context/poll/isNotYetOpened">
15 <div metal:use-macro="context/@@launchpad_form/form">
16
17 <h1 metal:fill-slot="heading">
18 Edit option
19 &#8220;<span tal:replace="context/name" />&#8221;
20 </h1>
21
22 </div>
23 </tal:block>
24
25 <tal:block condition="not: context/poll/isNotYetOpened">
26 <p class="error message" tal:condition="context/poll/isClosed">
27 You can&#8217;t edit any options because the poll is already closed.
28 </p>
29 <p class="error message" tal:condition="context/poll/isOpen">
30 You can&#8217;t edit any options because the poll is already open.
31 </p>
32 </tal:block>
33
34 </div>
35
36</body>
37</html>
380
=== modified file 'lib/lp/registry/templates/team-index.pt'
--- lib/lp/registry/templates/team-index.pt 2010-10-10 21:54:16 +0000
+++ lib/lp/registry/templates/team-index.pt 2010-12-16 14:48:27 +0000
@@ -35,7 +35,6 @@
35 </metal:contact>35 </metal:contact>
3636
37 <tal:menu replace="structure view/@@+global-actions" />37 <tal:menu replace="structure view/@@+global-actions" />
38 <tal:polls replace="structure context/@@+portlet-polls" />
3938
40</div>39</div>
4140
4241
=== removed file 'lib/lp/registry/templates/team-newpoll.pt'
--- lib/lp/registry/templates/team-newpoll.pt 2009-08-24 14:18:10 +0000
+++ lib/lp/registry/templates/team-newpoll.pt 1970-01-01 00:00:00 +0000
@@ -1,25 +0,0 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9
10<body>
11
12 <h1 metal:fill-slot="heading">
13 Create a new poll
14 </h1>
15
16 <div metal:fill-slot="main">
17
18 <div metal:use-macro="context/@@launchpad_form/form">
19
20 </div>
21
22 </div>
23
24</body>
25</html>
260
=== modified file 'lib/lp/registry/templates/team-polls.pt'
--- lib/lp/registry/templates/team-polls.pt 2009-11-20 05:40:25 +0000
+++ lib/lp/registry/templates/team-polls.pt 2010-12-16 14:48:27 +0000
@@ -8,81 +8,19 @@
8>8>
9<body>9<body>
1010
11 <metal:heading fill-slot="heading">
12 <h1>Polls for <span tal:replace="context/displayname" /></h1>
13 </metal:heading>
14
15 <div metal:fill-slot="main">11 <div metal:fill-slot="main">
1612
17 <h2>Current polls</h2>13 <h1>Polls no longer supported</h1>
1814
19 <p tal:condition="not: view/has_current_polls">15 <p>
20 This team has no open polls nor polls that are not yet opened.16 Launchpad no longer supports team polls. We have archived the data from
17 all previously conducted polls, which can be found as a comma-separated
18 values file at
19 <a href="http://dev.launchpad.net/PollFeatureRemoved">
20 http://dev.launchpad.net/PollFeatureRemoved</a>.
21 </p>21 </p>
2222
23 <ul tal:condition="view/has_current_polls">
24 <li tal:repeat="poll view/openpolls">
25 <a tal:attributes="href poll/fmt:url">
26 <span tal:replace="poll/title" />
27 </a> - closes
28 <span
29 tal:attributes="title poll/datecloses/fmt:datetime"
30 tal:content="poll/datecloses/fmt:displaydate" />.
31
32 <tal:block define="user request/lp:person" condition="user">
33 <tal:block condition="python: poll.personVoted(user)">
34 You have
35 <span tal:replace="poll/closesIn/fmt:approximateduration" />
36 to change your vote if you wish.
37 </tal:block>
38
39 <tal:block condition="python: not poll.personVoted(user)">
40 You have
41 <span tal:replace="poll/closesIn/fmt:approximateduration" />
42 left to vote in this poll.
43 </tal:block>
44 </tal:block>
45
46 </li>
47
48 <li tal:repeat="poll view/notyetopenedpolls">
49 <a tal:attributes="href poll/fmt:url">
50 <span tal:replace="poll/title" />
51 </a> - opens
52 <span
53 tal:attributes="title poll/dateopens/fmt:datetime"
54 tal:content="poll/dateopens/fmt:displaydate" />
55 </li>
56 </ul>
57
58 <tal:block condition="view/closedpolls" >
59 <h2>Closed polls</h2>
60
61 <ul>
62 <li tal:repeat="poll view/closedpolls">
63 <a tal:attributes="href poll/fmt:url">
64 <span tal:replace="poll/title" />
65 </a> - closed
66 <span
67 tal:attributes="title poll/datecloses/fmt:datetime"
68 tal:content="poll/datecloses/fmt:displaydate" />
69 </li>
70 </ul>
71 </tal:block>
72
73 <br />
74 <tal:block tal:condition="request/lp:person">
75 <ul tal:condition="context/required:launchpad.Edit">
76 <li><a class="sprite add" href="+newpoll">Set up a new poll</a></li>
77 </ul>
78 </tal:block>
79
80 <tal:block tal:condition="not: request/lp:person">
81 <a href="+login">Log in as an admin to set up a new poll</a>
82 </tal:block>
83
84 </div>23 </div>
8524
86</body>25</body>
87</html>26</html>
88
8927
=== removed file 'lib/lp/registry/templates/team-portlet-polls.pt'
--- lib/lp/registry/templates/team-portlet-polls.pt 2009-11-20 05:40:25 +0000
+++ lib/lp/registry/templates/team-portlet-polls.pt 1970-01-01 00:00:00 +0000
@@ -1,56 +0,0 @@
1<tal:root
2 xmlns:tal="http://xml.zope.org/namespaces/tal"
3 xmlns:metal="http://xml.zope.org/namespaces/metal"
4 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
5 omit-tag="">
6
7 <div id="polls" class="portlet"
8 tal:define="overview_menu context/menu:overview"
9 tal:condition="view/should_show_polls_portlet">
10 <h2>Polls</h2>
11 <p tal:condition="not: view/has_current_polls">
12 No current polls.
13 </p>
14
15 <ul tal:condition="view/has_current_polls">
16 <li tal:repeat="poll view/openpolls">
17 <a tal:attributes="href poll/fmt:url">
18 <span tal:replace="poll/title" />
19 </a> - closes
20 <span
21 tal:attributes="title poll/datecloses/fmt:datetime"
22 tal:content="poll/datecloses/fmt:displaydate" />.
23
24 <tal:block define="user request/lp:person" condition="user">
25 <tal:block condition="python: poll.personVoted(user)">
26 You have
27 <span tal:replace="poll/closesIn/fmt:approximateduration" />
28 to change your vote if you wish.
29 </tal:block>
30
31 <tal:block condition="python: not poll.personVoted(user)">
32 You have
33 <span tal:replace="poll/closesIn/fmt:approximateduration" />
34 left to vote in this poll.
35 </tal:block>
36 </tal:block>
37
38 </li>
39
40 <li tal:condition="view/userIsOwner"
41 tal:repeat="poll view/notyetopenedpolls">
42 <a tal:attributes="href poll/fmt:url">
43 <span tal:replace="poll/title" />
44 </a> - opens
45 <span
46 tal:attributes="title poll/dateopens/fmt:datetime"
47 tal:content="poll/dateopens/fmt:displaydate" />
48 </li>
49 </ul>
50
51 <a tal:condition="view/should_show_polls_portlet"
52 tal:replace="structure overview_menu/polls/fmt:link" />
53 <a tal:replace="structure overview_menu/add_poll/fmt:link" />
54
55 </div>
56</tal:root>
570
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-12-14 05:43:47 +0000
+++ lib/lp/testing/factory.py 2010-12-16 14:48:27 +0000
@@ -207,11 +207,6 @@
207 TeamSubscriptionPolicy,207 TeamSubscriptionPolicy,
208 )208 )
209from lp.registry.interfaces.pocket import PackagePublishingPocket209from lp.registry.interfaces.pocket import PackagePublishingPocket
210from lp.registry.interfaces.poll import (
211 IPollSet,
212 PollAlgorithm,
213 PollSecrecy,
214 )
215from lp.registry.interfaces.product import (210from lp.registry.interfaces.product import (
216 IProductSet,211 IProductSet,
217 License,212 License,
@@ -729,16 +724,6 @@
729 naked_team.addMember(member, owner)724 naked_team.addMember(member, owner)
730 return team725 return team
731726
732 def makePoll(self, team, name, title, proposition,
733 poll_type=PollAlgorithm.SIMPLE):
734 """Create a new poll which starts tomorrow and lasts for a week."""
735 dateopens = datetime.now(pytz.UTC) + timedelta(days=1)
736 datecloses = dateopens + timedelta(days=7)
737 return getUtility(IPollSet).new(
738 team, name, title, proposition, dateopens, datecloses,
739 PollSecrecy.SECRET, allowspoilt=True,
740 poll_type=poll_type)
741
742 def makeTranslationGroup(self, owner=None, name=None, title=None,727 def makeTranslationGroup(self, owner=None, name=None, title=None,
743 summary=None, url=None):728 summary=None, url=None):
744 """Create a new, arbitrary `TranslationGroup`."""729 """Create a new, arbitrary `TranslationGroup`."""