launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00772
[Merge] lp:~gmb/launchpad/lp-devs-can-reset-watches into lp:launchpad/devel
Graham Binns has proposed merging lp:~gmb/launchpad/lp-devs-can-reset-watches into lp:launchpad/devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers): code
Related bugs:
#624721 Launchpad developers should be able to completely reset individual bug watches
https://bugs.launchpad.net/bugs/624721
This branch adds a button to the BugWatch +edit page, visible to LP devs and admins. The button allows the user to completely reset the bug watch so that checkwatches treats it as a brand new watch. This is distinct from the "reschedule this watch" button, which merely re-queues the watch for updating.
I've added a new AuthorizationBase for IBugWatch, defining the people who have launchpad.Admin permission on a BugWatch as Admins and Launchpad developers.
I've added a reset() method to BugWatch and a related action to BugWatchEditView. I've added unittests for BugWatch.reset() to lp.bugs.tests.test_bugwatch and a story to cover the use of the 'Reset this watch' button.
--
https://code.launchpad.net/~gmb/launchpad/lp-devs-can-reset-watches/+merge/33796
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~gmb/launchpad/lp-devs-can-reset-watches into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2010-08-26 12:33:03 +0000
+++ lib/canonical/launchpad/security.py 2010-08-26 15:46:37 +0000
@@ -56,6 +56,7 @@
from lp.bugs.interfaces.bugnomination import IBugNomination
from lp.bugs.interfaces.bugsubscription import IBugSubscription
from lp.bugs.interfaces.bugtracker import IBugTracker
+from lp.bugs.interfaces.bugwatch import IBugWatch
from lp.buildmaster.interfaces.builder import (
IBuilder,
IBuilderSet,
@@ -2572,3 +2573,13 @@
user.in_janitor or
user.in_admin or
user.in_launchpad_developers)
+
+
+class AdminBugWatch(AuthorizationBase):
+ permission = 'launchpad.Admin'
+ usedfor = IBugWatch
+
+ def checkAuthenticated(self, user):
+ return (
+ user.in_admin or
+ user.in_launchpad_developers)
=== modified file 'lib/lp/bugs/browser/bugwatch.py'
--- lib/lp/bugs/browser/bugwatch.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/browser/bugwatch.py 2010-08-26 15:46:37 +0000
@@ -25,6 +25,7 @@
LaunchpadFormView,
LaunchpadView,
)
+from canonical.launchpad.webapp.authorization import check_permission
from canonical.launchpad.webapp.interfaces import ILaunchBag
from canonical.launchpad.webapp.menu import structured
from canonical.widgets.textwidgets import URIWidget
@@ -148,6 +149,22 @@
remote_bug=bugwatch.remotebug))
bugwatch.bug.removeWatch(bugwatch, self.user)
+ def resetBugWatchCondition(self, action):
+ """Return True if the reset action can be shown to this user."""
+ return check_permission('launchpad.Admin', self.context)
+
+ @action('Reset this watch', name='reset',
+ condition=resetBugWatchCondition)
+ def reset_action(self, action, data):
+ bug_watch = self.context
+ bug_watch.reset()
+ self.request.response.addInfoNotification(
+ structured(
+ 'The <a href="%(url)s">%(bugtracker)s #%(remote_bug)s</a>'
+ ' bug watch has been reset.',
+ url=bug_watch.url, bugtracker=bug_watch.bugtracker.name,
+ remote_bug=bug_watch.remotebug))
+
@property
def next_url(self):
return canonical_url(getUtility(ILaunchBag).bug)
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml 2010-08-26 12:33:03 +0000
+++ lib/lp/bugs/configure.zcml 2010-08-26 15:46:37 +0000
@@ -882,7 +882,9 @@
attributes="
addComment
updateImportance
- updateStatus"
+ updateStatus
+ reset
+ "
set_attributes="
last_error_type
lastchanged
@@ -890,7 +892,8 @@
needscheck
next_check
remote_importance
- remotestatus"/>
+ remotestatus
+ "/>
</class>
<!-- https://launchpad.net/bugs/98639 -->
=== modified file 'lib/lp/bugs/interfaces/bugwatch.py'
--- lib/lp/bugs/interfaces/bugwatch.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/interfaces/bugwatch.py 2010-08-26 15:46:37 +0000
@@ -275,6 +275,17 @@
:raises: `BugWatchCannotBeRescheduled` if
`IBugWatch.can_be_rescheduled` is False.
"""
+ def reset():
+ """Completely reset the watch.
+
+ When called, the following attributes are reset:
+ * last_error_type -> None
+ * lastchanged -> None
+ * lastchecked -> None
+ * nextcheck -> now
+ * remoteimportance -> None
+ * remotestatus -> None
+ """
# Defined here because of circular imports.
=== modified file 'lib/lp/bugs/model/bugwatch.py'
--- lib/lp/bugs/model/bugwatch.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/model/bugwatch.py 2010-08-26 15:46:37 +0000
@@ -364,6 +364,15 @@
self.next_check = next_check
+ def reset(self):
+ """See `IBugWatch`."""
+ self.last_error_type = None
+ self.lastchanged = None
+ self.lastchecked = None
+ self.next_check = UTC_NOW
+ self.remote_importance = None
+ self.remotestatus = None
+
class BugWatchSet(BugSetBase):
"""A set for BugWatch"""
=== modified file 'lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt'
--- lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt 2010-05-17 15:10:01 +0000
+++ lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt 2010-08-26 15:46:37 +0000
@@ -261,3 +261,70 @@
Traceback (most recent call last):
...
LookupError: label 'Update Now'
+
+
+Resetting a watch
+-----------------
+
+It's possible to reset a watch at any time by clicking the "Reset this
+watch" button on the watch's page.
+
+ >>> from lp.testing.sampledata import ADMIN_EMAIL
+ >>> login(ADMIN_EMAIL)
+ >>> bug_watch = factory.makeBugWatch()
+ >>> bug_watch.lastchecked = datetime.now(utc)
+ >>> watch_url = (
+ ... 'http://bugs.launchpad.dev/bugs/%s/+watch/%s' %
+ ... (bug_watch.bug.id, bug_watch.id))
+ >>> logout()
+
+The "Reset this watch" button will appear for administrators.
+
+ >>> admin_browser.open(watch_url)
+ >>> admin_browser.getControl('Reset this watch')
+ <SubmitControl...>
+
+It also appears for Launchpad Developers.
+
+ >>> from zope.component import getUtility
+ >>> from canonical.launchpad.interfaces.launchpad import (
+ ... ILaunchpadCelebrities)
+ >>> from lp.registry.interfaces.person import IPersonSet
+
+ >>> login(ADMIN_EMAIL)
+ >>> admin_user = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+ >>> new_lp_developer = factory.makePerson(password='newpassword')
+ >>> launchpad_developers = getUtility(
+ ... ILaunchpadCelebrities).launchpad_developers
+ >>> dev_added = launchpad_developers.addMember(
+ ... new_lp_developer, admin_user)
+
+ >>> lp_dev_browser = setupBrowser(
+ ... auth='Basic %s:newpassword' %
+ ... new_lp_developer.preferredemail.email)
+ >>> logout()
+
+ >>> lp_dev_browser.open(watch_url)
+ >>> reset_button = lp_dev_browser.getControl('Reset this watch')
+
+Clicking the button will reset the watch completely.
+
+ >>> reset_button.click()
+ >>> for message in find_tags_by_class(
+ ... lp_dev_browser.contents, 'informational message'):
+ ... print extract_text(message)
+ The ... bug watch has been reset.
+
+ >>> data_tag = find_tag_by_id(
+ ... user_browser.contents, 'bugwatch-lastchecked')
+ >>> print extract_text(data_tag.renderContents())
+ Checked:
+
+Should a non-admin, non-Launchpad-developer user visit the page, the
+button will not appear.
+
+ >>> user_browser.open(watch_url)
+ >>> user_browser.getControl('Reset this watch')
+ Traceback (most recent call last):
+ ...
+ LookupError: label 'Reset this watch'
=== modified file 'lib/lp/bugs/templates/bugwatch-portlet-details.pt'
--- lib/lp/bugs/templates/bugwatch-portlet-details.pt 2010-04-15 15:14:21 +0000
+++ lib/lp/bugs/templates/bugwatch-portlet-details.pt 2010-08-26 15:46:37 +0000
@@ -38,7 +38,7 @@
</span>
</dd>
</dl>
- <dl class="bugwatch-data">
+ <dl class="bugwatch-data" id="bugwatch-lastchecked">
<dt>Checked:</dt>
<dd>
<span
=== modified file 'lib/lp/bugs/tests/test_bugwatch.py'
--- lib/lp/bugs/tests/test_bugwatch.py 2010-08-20 20:31:18 +0000
+++ lib/lp/bugs/tests/test_bugwatch.py 2010-08-26 15:46:37 +0000
@@ -6,17 +6,26 @@
__metaclass__ = type
import unittest
+from datetime import (
+ datetime,
+ timedelta,
+ )
+from pytz import utc
from urlparse import urlunsplit
import transaction
from zope.component import getUtility
+from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
+from lazr.lifecycle.snapshot import Snapshot
+
from canonical.database.constants import UTC_NOW
from canonical.launchpad.ftests import (
ANONYMOUS,
login,
)
+from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
from canonical.launchpad.scripts.garbo import BugWatchActivityPruner
from canonical.launchpad.scripts.logger import QuietFakeLogger
from canonical.launchpad.webapp import urlsplit
@@ -46,6 +55,7 @@
login_person,
TestCaseWithFactory,
)
+from lp.testing.sampledata import ADMIN_EMAIL
class ExtractBugTrackerAndBugTestBase:
@@ -672,3 +682,84 @@
messages = [activity.message for activity in self.bug_watch.activity]
for i in range(MAX_SAMPLE_SIZE):
self.failUnless("Activity %s" % i in messages)
+
+
+class TestBugWatchResetting(TestCaseWithFactory):
+
+ layer = LaunchpadFunctionalLayer
+
+ def setUp(self):
+ super(TestBugWatchResetting, self).setUp(user=ADMIN_EMAIL)
+ self.bug_watch = self.factory.makeBugWatch()
+ self.bug_watch.last_error_type = BugWatchActivityStatus.BUG_NOT_FOUND
+ self.bug_watch.lastchanged = datetime.now(utc) - timedelta(days=1)
+ self.bug_watch.lastchecked = datetime.now(utc) - timedelta(days=1)
+ self.bug_watch.next_check = datetime.now(utc) + timedelta(days=7)
+ self.bug_watch.remote_importance = 'IMPORTANT'
+ self.bug_watch.remotestatus = 'FIXED'
+ self.default_bug_watch_fields = [
+ 'last_error_type',
+ 'lastchanged',
+ 'lastchecked',
+ 'next_check',
+ 'remote_importance',
+ 'remotestatus',
+ ]
+ self.original_bug_watch = Snapshot(
+ self.bug_watch, self.default_bug_watch_fields)
+
+ def _assertBugWatchHasBeenChanged(self, expected_changes=None):
+ """Assert that a bug watch has been changed.
+
+ :param expected_changes: A list of the attribute names that are
+ expected to have changed. If supplied, an assertion error
+ will be raised if one of the expected_changes members has
+ not changed *or* an attribute not in expected_changes has
+ changed. If not supplied, *all* attributes are expected to
+ have changed.
+ """
+ if expected_changes is None:
+ expected_changes = self.default_bug_watch_fields
+
+ actual_changes = []
+ has_changed = True
+ for expected_change in expected_changes:
+ original_value = getattr(self.original_bug_watch, expected_change)
+ current_value = getattr(self.bug_watch, expected_change)
+ if original_value != current_value:
+ has_changed = has_changed and True
+ actual_changes.append(expected_change)
+ else:
+ has_changed = False
+
+ self.assertTrue(
+ has_changed,
+ "Bug watch did not change as expected.\n"
+ "Expected changes: %s\n"
+ "Actual changes: %s" % (expected_changes, actual_changes))
+
+ def test_reset_resets(self):
+ # Calling reset() on a watch resets all of the attributes of the
+ # bug watch.
+ admin_user = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+ login_person(admin_user)
+ self.bug_watch.reset()
+ self._assertBugWatchHasBeenChanged()
+
+ def test_unprivileged_user_cant_reset_watches(self):
+ # An unprivileged user can't call the reset() method on a bug
+ # watch.
+ unprivileged_user = self.factory.makePerson()
+ login_person(unprivileged_user)
+ self.assertRaises(Unauthorized, getattr, self.bug_watch, 'reset')
+
+ def test_lp_developer_can_reset_watches(self):
+ # A Launchpad developer can call the reset() method on a bug
+ # watch.
+ admin_user = getUtility(IPersonSet).getByEmail(ADMIN_EMAIL)
+ lp_developers = getUtility(ILaunchpadCelebrities).launchpad_developers
+ lp_dev = self.factory.makePerson()
+ lp_developers.addMember(lp_dev, admin_user)
+ login_person(lp_dev)
+ self.bug_watch.reset()
+ self._assertBugWatchHasBeenChanged()