Merge lp:~gmb/launchpad/fix-bugwatch-message-shenanigans into lp:launchpad

Proposed by Graham Binns on 2010-09-29
Status: Merged
Approved by: Graham Binns on 2010-09-30
Approved revision: no longer in the source branch.
Merged at revision: 11661
Proposed branch: lp:~gmb/launchpad/fix-bugwatch-message-shenanigans
Merge into: lp:launchpad
Diff against target: 195 lines (+98/-6)
6 files modified
lib/lp/bugs/browser/bugwatch.py (+4/-1)
lib/lp/bugs/browser/tests/test_bugwatch_views.py (+56/-0)
lib/lp/bugs/doc/bugmessage.txt (+10/-0)
lib/lp/bugs/model/bugwatch.py (+14/-1)
lib/lp/bugs/stories/bugwatches/xx-delete-bugwatch.txt (+2/-3)
lib/lp/bugs/tests/test_bugwatch.py (+12/-1)
To merge this branch: bzr merge lp:~gmb/launchpad/fix-bugwatch-message-shenanigans
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) code 2010-09-29 Approve on 2010-09-29
Review via email: mp+36972@code.launchpad.net

Commit Message

Users should no longer be able to attempt to delete a BugWatch that is linked to BugComments.

Description of the Change

This branch began as an attempt to fix bug 575911 (Trying to delete a
bug watch results in a non-OOPSing IntegrityError). The error was being
raised because we were allowing people to attempt to delete bug watches
which had comments linked to them. Since we don't do a cascading delete
on comments linked to watches, Postgres threw its toys out of the pram.

It turned out that the simplest way to stop the error from being raise
was to stop people from ever getting to the point where they could try
to delete a linked watch in the first place. The fact that the error
didn't OOPS was due to it occurring when the transaction was being
committed, which is (apparently) outside application code and so not
handled by the OOPS machinery.

Nevertheless I've followed Gavin's suggestion of adding a store.flush()
call to the end of BugWatch.destroySelf(). Whilst it should no longer be
possible for someone to try to delete a linked watch without getting a
sane error message the flush() should force any DB errors to be handled
by the OOPS machinery, thus making life easier should anything else go
wrong.

Here's a list of my changes:

== lib/lp/bugs/browser/bugwatch.py ==

 - I've updated the bugWatchIsUnlinked() method, which is used to
   determine whether ot display the delete action on the page. It now
   checks that the watch has no comments linked to it.

== lib/lp/bugs/browser/tests/test_bugwatch_views.py ==

 - I've added a couple of tests to cover the changes to
   bugWatchIsUnlinked() above.

== lib/lp/bugs/doc/bugmessage.txt ==

 - I've added tests to ensure that comments aren't linked to a watch
   when a watch is created from a URL in a comment. This was originally
   thought to be the source of (part of) the bug I was attempting to
   fix. That turned out not to be the case but it seemed sensible to
   leave the test in place.

== lib/lp/bugs/model/bugwatch.py ==

 - BugWatch.destroySelf() now raises a BugWatchDeletionError if the
   watch is linked to a task or comments. I've added a call to
   store.flush() as described above.

== lib/lp/bugs/stories/bugwatches/xx-delete-bugwatch.txt ==

 - I've updated the narrative for the deletion story.

== lib/lp/bugs/tests/test_bugwatch.py ==

 - I've added a test to check that BugWatch.destroySelf() raises a
   BugWatchDeletionError as described above.

To post a comment you must log in.
Edwin Grubbs (edwin-grubbs) wrote :

Hi Graham,

This branch looks good. Can you make sure there is a bug open about the oops machinery not reporting late integrity errors?

-Edwin

review: Approve (code)
Graham Binns (gmb) wrote :

On 29 September 2010 18:41, Edwin Grubbs <email address hidden> wrote:
> Review: Approve code
> Hi Graham,
>
> This branch looks good. Can you make sure there is a bug open about the oops machinery not reporting late integrity errors?

Gary filed bug 647103 about this problem. I've added an XXX referring
to it above the flush(), since we shouldn't have to do it.

Graham Binns (gmb) wrote :

On 29 September 2010 18:41, Edwin Grubbs <email address hidden> wrote:
> Review: Approve code
> Hi Graham,
>
> This branch looks good. Can you make sure there is a bug open about the oops machinery not reporting late integrity errors?

Gary filed bug 647103 about this problem. I've added an XXX referring
to it above the flush(), since we shouldn't have to do it.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/browser/bugwatch.py'
2--- lib/lp/bugs/browser/bugwatch.py 2010-09-24 14:24:06 +0000
3+++ lib/lp/bugs/browser/bugwatch.py 2010-09-29 19:17:49 +0000
4@@ -29,6 +29,7 @@
5 from canonical.launchpad.webapp.menu import structured
6 from canonical.widgets.textwidgets import URIWidget
7 from lp.bugs.browser.bugtask import get_comments_for_bugtask
8+from lp.bugs.interfaces.bugmessage import IBugMessageSet
9 from lp.bugs.interfaces.bugwatch import (
10 BUG_WATCH_ACTIVITY_SUCCESS_STATUSES,
11 IBugWatch,
12@@ -131,7 +132,9 @@
13
14 def bugWatchIsUnlinked(self, action):
15 """Return whether the bug watch is unlinked."""
16- return len(self.context.bugtasks) == 0
17+ return (
18+ len(self.context.bugtasks) == 0 and
19+ self.context.getImportedBugMessages().is_empty())
20
21 @action('Delete Bug Watch', name='delete', condition=bugWatchIsUnlinked)
22 def delete_action(self, action, data):
23
24=== added file 'lib/lp/bugs/browser/tests/test_bugwatch_views.py'
25--- lib/lp/bugs/browser/tests/test_bugwatch_views.py 1970-01-01 00:00:00 +0000
26+++ lib/lp/bugs/browser/tests/test_bugwatch_views.py 2010-09-29 19:17:49 +0000
27@@ -0,0 +1,56 @@
28+# Copyright 2010 Canonical Ltd. This software is licensed under the
29+# GNU Affero General Public License version 3 (see the file LICENSE).
30+
31+"""Tests for BugWatch views."""
32+
33+__metaclass__ = type
34+
35+from zope.component import getUtility
36+
37+from canonical.launchpad.interfaces.message import IMessageSet
38+from canonical.testing import LaunchpadFunctionalLayer
39+
40+from lp.testing import login, login_person, TestCaseWithFactory
41+from lp.testing.sampledata import ADMIN_EMAIL
42+from lp.testing.views import create_initialized_view
43+
44+
45+class TestBugWatchEditView(TestCaseWithFactory):
46+
47+ layer = LaunchpadFunctionalLayer
48+
49+ def setUp(self):
50+ super(TestBugWatchEditView, self).setUp()
51+ self.person = self.factory.makePerson()
52+
53+ login_person(self.person)
54+ self.bug_task = self.factory.makeBug(
55+ owner=self.person).default_bugtask
56+ self.bug_watch = self.factory.makeBugWatch(
57+ bug=self.bug_task.bug)
58+
59+ def test_cannot_delete_watch_if_linked_to_task(self):
60+ # It isn't possible to delete a bug watch that's linked to a bug
61+ # task.
62+ self.bug_task.bugwatch = self.bug_watch
63+ view = create_initialized_view(self.bug_watch, '+edit')
64+ self.assertFalse(
65+ view.bugWatchIsUnlinked(None),
66+ "bugWatchIsUnlinked() returned True though there is a task "
67+ "linked to the watch.")
68+
69+ def test_cannot_delete_watch_if_linked_to_coment(self):
70+ # It isn't possible to delete a bug watch that's linked to a bug
71+ # comment.
72+ message = getUtility(IMessageSet).fromText(
73+ "Example message", "With some example content to read.")
74+ # We need to log in as an admin here as only admins can link a
75+ # watch to a comment.
76+ login(ADMIN_EMAIL)
77+ bug_message = self.bug_watch.addComment('comment-id', message)
78+ login_person(self.person)
79+ view = create_initialized_view(self.bug_watch, '+edit')
80+ self.assertFalse(
81+ view.bugWatchIsUnlinked(None),
82+ "bugWatchIsUnlinked() returned True though there is a comment "
83+ "linked to the watch.")
84
85=== modified file 'lib/lp/bugs/doc/bugmessage.txt'
86--- lib/lp/bugs/doc/bugmessage.txt 2010-05-21 17:01:20 +0000
87+++ lib/lp/bugs/doc/bugmessage.txt 2010-09-29 19:17:49 +0000
88@@ -109,6 +109,16 @@
89 http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=304014
90 http://some.bugzilla/show_bug.cgi?id=9876
91
92+Note that although the watch was created when the Message was added to
93+the bug, the message and the watch are not linked because the message
94+was not imported by the bug watch.
95+
96+ >>> bug_message = bug_one.bug_messages[-1]
97+ >>> print bug_message.message == test_message
98+ True
99+ >>> print bug_message.bugwatch
100+ None
101+
102 CVE watches and bug watches are also created, when a message is imported from
103 an external bug tracker.
104
105
106=== modified file 'lib/lp/bugs/model/bugwatch.py'
107--- lib/lp/bugs/model/bugwatch.py 2010-08-26 12:12:00 +0000
108+++ lib/lp/bugs/model/bugwatch.py 2010-09-29 19:17:49 +0000
109@@ -7,6 +7,7 @@
110 __all__ = [
111 'BugWatch',
112 'BugWatchActivity',
113+ 'BugWatchDeletionError',
114 'BugWatchSet',
115 ]
116
117@@ -114,6 +115,10 @@
118 '%r is not a bug watch or an ID.' % (reference,))
119
120
121+class BugWatchDeletionError(Exception):
122+ """Raised when someone attempts to delete a linked watch."""
123+
124+
125 class BugWatch(SQLBase):
126 """See `IBugWatch`."""
127 implements(IBugWatch)
128@@ -223,8 +228,16 @@
129
130 def destroySelf(self):
131 """See `IBugWatch`."""
132- assert len(self.bugtasks) == 0, "Can't delete linked bug watches"
133+ if (len(self.bugtasks) > 0 or
134+ not self.getImportedBugMessages().is_empty()):
135+ raise BugWatchDeletionError(
136+ "Can't delete bug watches linked to tasks or comments.")
137+ # XXX 2010-09-29 gmb bug=647103
138+ # We flush the store to make sure that errors bubble up and
139+ # are caught by the OOPS machinery.
140 SQLBase.destroySelf(self)
141+ store = Store.of(self)
142+ store.flush()
143
144 @property
145 def unpushed_comments(self):
146
147=== modified file 'lib/lp/bugs/stories/bugwatches/xx-delete-bugwatch.txt'
148--- lib/lp/bugs/stories/bugwatches/xx-delete-bugwatch.txt 2009-09-04 00:37:37 +0000
149+++ lib/lp/bugs/stories/bugwatches/xx-delete-bugwatch.txt 2010-09-29 19:17:49 +0000
150@@ -1,7 +1,7 @@
151 = Delete a Bug Watch =
152
153-If a bug watch isn't linked to a bug task, it's possible to delete from
154-the bug watch edit page.
155+If a bug watch isn't linked to a bug task and no comments have been
156+imported for it, it's possible to delete from the bug watch edit page.
157
158 >>> user_browser.open('http://launchpad.dev/bugs/1')
159 >>> bug_watches = find_portlet(user_browser.contents, 'Remote bug watches')
160@@ -32,4 +32,3 @@
161 mozilla.org #2000
162 mozilla.org #42
163 debbugs #304014
164-
165
166=== modified file 'lib/lp/bugs/tests/test_bugwatch.py'
167--- lib/lp/bugs/tests/test_bugwatch.py 2010-08-26 13:10:24 +0000
168+++ lib/lp/bugs/tests/test_bugwatch.py 2010-09-29 19:17:49 +0000
169@@ -48,7 +48,10 @@
170 NoBugTrackerFound,
171 UnrecognizedBugTrackerURL,
172 )
173-from lp.bugs.model.bugwatch import get_bug_watch_ids
174+from lp.bugs.model.bugwatch import (
175+ BugWatchDeletionError,
176+ get_bug_watch_ids,
177+ )
178 from lp.bugs.scripts.checkwatches.scheduler import MAX_SAMPLE_SIZE
179 from lp.registry.interfaces.person import IPersonSet
180 from lp.testing import (
181@@ -465,6 +468,14 @@
182 self.assertRaises(
183 AssertionError, list, get_bug_watch_ids(['fred']))
184
185+ def test_destroySelf_raise_error_when_linked_to_a_task(self):
186+ # It's not possible to delete a bug watch that's linked to a
187+ # task. Trying will result in a BugWatchDeletionError.
188+ bug_watch = self.factory.makeBugWatch()
189+ bug = bug_watch.bug
190+ bug.default_bugtask.bugwatch = bug_watch
191+ self.assertRaises(BugWatchDeletionError, bug_watch.destroySelf)
192+
193
194 class TestBugWatchSet(TestCaseWithFactory):
195 """Tests for the bugwatch updating system."""