← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/poll-oops into lp:launchpad/devel

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/poll-oops into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #46581 Change a poll type URL manually crashes
  https://bugs.launchpad.net/bugs/46581


This is my branch to simplify the poll vote behaviour to prevent an oops.

    lp:~sinzui/launchpad/poll-oops
    Diff size: 324
    Launchpad bug: https://bugs.launchpad.net/bugs/46581
    Test command: ./bin/test -vv -t test_poll -t poll-view -t team-polls
    Pre-implementation: no one
    Target release: 10.11

     I. 
    II. Simplify the poll vote behaviour to prevent an oops
---------------------------------------------------

Users who read the Launchpad code can discover that condorcet polls are
partially implemented but disabled. The user can create an oops by hacking
the URL to attempt to change the way voting works.

The crux of the problem is that there are separate names for the vote pages
which tempts users to try switching the vote mechanism. One name for voting
could be registered and the view can select the template for the poll type.

The original suggestion I made in the bug supposed that the poll_types were
implemented as interfaces--they are enums. The view knows the PollAlgorithm
of the poll and makes decisions based on it. The view can do the same for
the template.


Rules
-----

    * Update PollVoteView to select the template based on the PollAlgorithm.
    * Change the view name to +vote in configure.zcml and remove the templates
      attrs.
    * Find and replace +vote-simple and +vote-condorcet with +vote.


QA
--

    * Visit an open poll you can vote on.
    * Verify the URL to vote ends with +vote


Lint
----

Linting changed files:
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/browser/poll.py
  lib/lp/registry/browser/tests/poll-views_0.txt
  lib/lp/registry/browser/tests/test_poll.py
  lib/lp/registry/stories/team-polls/edit-poll.txt
  lib/lp/registry/stories/team-polls/vote-poll.txt
  lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt
  lib/lp/testing/factory.py

^ Lint reports indentation and heading issues in the doctests. I will fix
these after this branch is approved to land.


Test
----

Added tests to verify PollVoteView selects the correct template for +vote.
    * lib/lp/registry/browser/tests/test_poll.py

Used find and replace to update existing tests to use _+vote.
    * lib/lp/registry/browser/tests/poll-views_0.txt
    * lib/lp/registry/stories/team-polls/edit-poll.txt
    * lib/lp/registry/stories/team-polls/vote-poll.txt
    * lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt


Implementation
--------------

Move the template selection rules into the view.
    * lib/lp/registry/browser/configure.zcml
    * lib/lp/registry/browser/poll.py
      * Also simplified the redirect rule to redirect eligible users to vote.

Updated the factor to make testing easier.
    * lib/lp/testing/factory.py
-- 
https://code.launchpad.net/~sinzui/launchpad/poll-oops/+merge/38142
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/poll-oops into lp:launchpad/devel.
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml	2010-10-04 20:46:55 +0000
+++ lib/lp/registry/browser/configure.zcml	2010-10-11 17:56:01 +0000
@@ -625,17 +625,11 @@
                 name="+portlet-options"
                 template="../templates/poll-portlet-options.pt"/>
         </browser:pages>
-        <browser:pages
+        <browser:page
+            name="+vote"
             for="lp.registry.interfaces.poll.IPoll"
             permission="launchpad.AnyPerson"
-            class="lp.registry.browser.poll.PollVoteView">
-            <browser:page
-                name="+vote-simple"
-                template="../templates/poll-vote-simple.pt"/>
-            <browser:page
-                name="+vote-condorcet"
-                template="../templates/poll-vote-condorcet.pt"/>
-        </browser:pages>
+            class="lp.registry.browser.poll.PollVoteView"/>
         <browser:page
             name="+edit"
             for="lp.registry.interfaces.poll.IPoll"

=== modified file 'lib/lp/registry/browser/poll.py'
--- lib/lp/registry/browser/poll.py	2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/poll.py	2010-10-11 17:56:01 +0000
@@ -17,6 +17,7 @@
     'PollBreadcrumb',
     ]
 
+from z3c.ptcompat import ViewPageTemplateFile
 from zope.app.form.browser import TextWidget
 from zope.component import getUtility
 from zope.event import notify
@@ -219,11 +220,8 @@
         request = self.request
         if (self.userCanVote() and self.context.isOpen() and
             self.context.getActiveOptions()):
-            context_url = canonical_url(self.context)
-            if self.isSimple():
-                request.response.redirect("%s/+vote-simple" % context_url)
-            elif self.isCondorcet():
-                request.response.redirect("%s/+vote-condorcet" % context_url)
+            vote_url = canonical_url(self.context, view_name='+vote')
+            request.response.redirect(vote_url)
 
     def getVotesByOption(self, option):
         """Return the number of votes the given option received."""
@@ -256,6 +254,18 @@
     change it. Otherwise he can register his vote.
     """
 
+    default_template = ViewPageTemplateFile(
+        '../templates/poll-vote-simple.pt')
+    condorcet_template = ViewPageTemplateFile(
+        '../templates/poll-vote-condorcet.pt')
+
+    @property
+    def template(self):
+        if self.isCondorcet():
+            return self.condorcet_template
+        else:
+            return self.default_template
+
     def initialize(self):
         """Process the form, if it was submitted."""
         super(PollVoteView, self).initialize()

=== modified file 'lib/lp/registry/browser/tests/poll-views_0.txt'
--- lib/lp/registry/browser/tests/poll-views_0.txt	2010-10-03 15:30:06 +0000
+++ lib/lp/registry/browser/tests/poll-views_0.txt	2010-10-11 17:56:01 +0000
@@ -78,7 +78,7 @@
 
     >>> request = TestRequest(form={'changevote': 'Change Vote'})
     >>> request.method = 'POST'
-    >>> voting_page = getMultiAdapter((poll, request), name="+vote-condorcet")
+    >>> voting_page = getMultiAdapter((poll, request), name="+vote")
     >>> form_processed = False
     >>> def form_processing():
     ...     form_processed = True

=== added file 'lib/lp/registry/browser/tests/test_poll.py'
--- lib/lp/registry/browser/tests/test_poll.py	1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_poll.py	2010-10-11 17:56:01 +0000
@@ -0,0 +1,38 @@
+# Copyright 2010 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for IPoll views."""
+
+__metaclass__ = type
+
+import os
+from canonical.testing.layers import DatabaseFunctionalLayer
+from lp.registry.interfaces.poll import PollAlgorithm
+from lp.testing import TestCaseWithFactory
+from lp.testing.views import create_view
+
+
+class TestPollVoteView(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestPollVoteView, self).setUp()
+        self.team = self.factory.makeTeam()
+
+    def test_simple_poll_template(self):
+        poll = self.factory.makePoll(
+            self.team, 'name', 'title', 'proposition',
+            poll_type=PollAlgorithm.SIMPLE)
+        view = create_view(poll, name='+vote')
+        self.assertEqual(
+            'poll-vote-simple.pt', os.path.basename(view.template.filename))
+
+    def test_condorcet_poll_template(self):
+        poll = self.factory.makePoll(
+            self.team, 'name', 'title', 'proposition',
+            poll_type=PollAlgorithm.CONDORCET)
+        view = create_view(poll, name='+vote')
+        self.assertEqual(
+            'poll-vote-condorcet.pt',
+            os.path.basename(view.template.filename))

=== modified file 'lib/lp/registry/stories/team-polls/edit-poll.txt'
--- lib/lp/registry/stories/team-polls/edit-poll.txt	2009-08-20 17:56:48 +0000
+++ lib/lp/registry/stories/team-polls/edit-poll.txt	2010-10-11 17:56:01 +0000
@@ -69,7 +69,7 @@
 
     >>> team_admin_browser.getLink('A random poll that never closes').click()
     >>> team_admin_browser.url
-    'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote-simple'
+    'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote'
 
     >>> team_admin_browser.open(
     ...     'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+edit')

=== modified file 'lib/lp/registry/stories/team-polls/vote-poll.txt'
--- lib/lp/registry/stories/team-polls/vote-poll.txt	2008-10-08 14:56:56 +0000
+++ lib/lp/registry/stories/team-polls/vote-poll.txt	2010-10-11 17:56:01 +0000
@@ -8,7 +8,7 @@
     >>> browser.open('http://launchpad.dev/~ubuntu-team/+polls')
     >>> browser.getLink('A random poll that never closes').click()
     >>> browser.url
-    'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote-simple'
+    'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote'
 
     >>> print find_tag_by_id(browser.contents, 'your-vote').renderContents()
     <BLANKLINE>
@@ -22,7 +22,7 @@
     >>> browser.getControl('Continue').click()
 
     >>> browser.url
-    'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote-simple'
+    'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote'
 
     >>> tags = find_tags_by_class(browser.contents, "informational message")
     >>> for tag in tags:
@@ -42,7 +42,7 @@
     >>> browser.open('http://launchpad.dev/~ubuntu-team/+polls')
     >>> browser.getLink('A public poll that never closes').click()
     >>> browser.url
-    'http://launchpad.dev/~ubuntu-team/+poll/never-closes4/+vote-simple'
+    'http://launchpad.dev/~ubuntu-team/+poll/never-closes4/+vote'
 
     >>> print find_tag_by_id(browser.contents, 'your-vote').renderContents()
     <BLANKLINE>
@@ -56,7 +56,7 @@
     >>> browser.getControl('Continue').click()
 
     >>> browser.url
-    'http://launchpad.dev/~ubuntu-team/+poll/never-closes4/+vote-simple'
+    'http://launchpad.dev/~ubuntu-team/+poll/never-closes4/+vote'
 
     >>> tags = find_tags_by_class(browser.contents, "informational message")
     >>> for tag in tags:
@@ -78,7 +78,7 @@
     >>> team_admin_browser = setupBrowser(
     ...     auth='Basic jeff.waugh@xxxxxxxxxxxxxxx:jdub')
     >>> team_admin_browser.open(
-    ...   'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote-simple')
+    ...   'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote')
     >>> not_yet_voted_message = 'You have not yet voted in this poll.'
     >>> not_yet_voted_message in team_admin_browser.contents
     True
@@ -107,7 +107,7 @@
     >>> non_member_browser = setupBrowser(
     ...     auth='Basic test@xxxxxxxxxxxxx:test')
     >>> non_member_browser.open(
-    ...     'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote-simple')
+    ...     'http://launchpad.dev/~ubuntu-team/+poll/never-closes/+vote')
     >>> for tag in find_tags_by_class(
     ...     non_member_browser.contents, "informational message"):
     ...     print tag.renderContents()
@@ -129,7 +129,7 @@
     ...
 
     >>> team_admin_browser.open(
-    ...     'http://launchpad.dev/~ubuntu-team/+poll/leader-2004/+vote-simple')
+    ...     'http://launchpad.dev/~ubuntu-team/+poll/leader-2004/+vote')
     >>> print find_tag_by_id(
     ...     team_admin_browser.contents, 'maincontent').renderContents()
     <BLANKLINE>
@@ -161,7 +161,7 @@
     LookupError: name 'continue'
 
     >>> team_admin_browser.open(
-    ...     'http://launchpad.dev/~ubuntu-team/+poll/director-2004/+vote-condorcet')
+    ...     'http://launchpad.dev/~ubuntu-team/+poll/director-2004/+vote')
     >>> for message in get_feedback_messages(team_admin_browser.contents):
     ...     print message
     This poll is already closed.

=== modified file 'lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt'
--- lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt	2009-08-21 18:46:34 +0000
+++ lib/lp/registry/stories/team-polls/xx-poll-condorcet-voting.txt	2010-10-11 17:56:01 +0000
@@ -11,11 +11,11 @@
   ... """)
   HTTP/1.1 303 See Other
   ...
-  Location: http://localhost/~ubuntu-team/+poll/never-closes2/+vote-condorcet
+  Location: http://localhost/~ubuntu-team/+poll/never-closes2/+vote
   ...
 
   >>> print http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes2/+vote-condorcet HTTP/1.1
+  ... GET /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
   ... """)
   HTTP/1.1 200 Ok
@@ -32,7 +32,7 @@
   he won't be allowed.
 
   >>> print http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes2/+vote-condorcet HTTP/1.1
+  ... GET /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
   ... Authorization: Basic dGVzdEBjYW5vbmljYWwuY29tOnRlc3Q=
   ... """)
   HTTP/1.1 200 Ok
@@ -43,7 +43,7 @@
   By providing the token we will be able to see our current vote.
 
   >>> print http(r"""
-  ... POST /~ubuntu-team/+poll/never-closes2/+vote-condorcet HTTP/1.1
+  ... POST /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
   ... Content-Type: application/x-www-form-urlencoded
   ...
@@ -78,7 +78,7 @@
   It's also possible to change the vote, if wanted.
 
   >>> print http(r"""
-  ... POST /~ubuntu-team/+poll/never-closes2/+vote-condorcet HTTP/1.1
+  ... POST /~ubuntu-team/+poll/never-closes2/+vote HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
   ... Content-Type: application/x-www-form-urlencoded
   ...
@@ -124,11 +124,11 @@
   ... """)
   HTTP/1.1 303 See Other
   ...
-  Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote-condorcet
+  Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote
   ...
 
   >>> print http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes3/+vote-condorcet HTTP/1.1
+  ... GET /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
   ... """)
   HTTP/1.1 200 Ok
@@ -165,7 +165,7 @@
   vote.
 
   >>> print http(r"""
-  ... POST /~ubuntu-team/+poll/never-closes3/+vote-condorcet HTTP/1.1
+  ... POST /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
   ... Authorization: Basic foo.bar@xxxxxxxxxxxxx:test
   ... Content-Type: application/x-www-form-urlencoded
   ...
@@ -210,7 +210,7 @@
   ... """)
   HTTP/1.1 303 See Other
   ...
-  Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote-condorcet
+  Location: http://localhost/~ubuntu-team/+poll/never-closes3/+vote
   ...
 
 
@@ -218,7 +218,7 @@
   to vote.
 
   >>> print http(r"""
-  ... GET /~ubuntu-team/+poll/never-closes3/+vote-condorcet HTTP/1.1
+  ... GET /~ubuntu-team/+poll/never-closes3/+vote HTTP/1.1
   ... Authorization: Basic mark@xxxxxxxxxxx:test
   ... """)
   HTTP/1.1 200 Ok

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-10-03 15:30:06 +0000
+++ lib/lp/testing/factory.py	2010-10-11 17:56:01 +0000
@@ -717,14 +717,15 @@
                 naked_team.addMember(member, owner)
         return team
 
-    def makePoll(self, team, name, title, proposition):
+    def makePoll(self, team, name, title, proposition,
+                 poll_type=PollAlgorithm.SIMPLE):
         """Create a new poll which starts tomorrow and lasts for a week."""
         dateopens = datetime.now(pytz.UTC) + timedelta(days=1)
         datecloses = dateopens + timedelta(days=7)
         return getUtility(IPollSet).new(
             team, name, title, proposition, dateopens, datecloses,
             PollSecrecy.SECRET, allowspoilt=True,
-            poll_type=PollAlgorithm.SIMPLE)
+            poll_type=poll_type)
 
     def makeTranslationGroup(
         self, owner=None, name=None, title=None, summary=None, url=None):