Merge lp:~gary/launchpad/bug164196-2 into lp:launchpad/db-devel

Proposed by Gary Poster
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 10215
Proposed branch: lp:~gary/launchpad/bug164196-2
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~gary/launchpad/bug164196-1
Diff against target: 1212 lines (+691/-80)
8 files modified
lib/lp/bugs/adapters/bugchange.py (+38/-12)
lib/lp/bugs/browser/bugtask.py (+13/-6)
lib/lp/bugs/doc/bugactivity.txt (+8/-3)
lib/lp/bugs/doc/bugnotification-sending.txt (+23/-21)
lib/lp/bugs/model/bugactivity.py (+35/-3)
lib/lp/bugs/scripts/bugnotification.py (+64/-18)
lib/lp/bugs/scripts/tests/test_bugnotification.py (+426/-16)
lib/lp/bugs/tests/test_bugchanges.py (+84/-1)
To merge this branch: bzr merge lp:~gary/launchpad/bug164196-2
Reviewer Review Type Date Requested Status
Данило Шеган (community) Approve
Review via email: mp+49977@code.launchpad.net

Commit message

use the activity attributes on bug notifications to implement the logic to silence emails for actions that are done and then undone in rapid succession.

Description of the change

This is the second of three branches that address bug 164196. The other two are lp:~gary/launchpad/bug164196-1 and lp:~gary/launchpad/bug164196-3. My pre-implementation call for these changes was with Graham Binns.

This branch uses the activity attributes on bug notifications introduced in the first branch to implement the logic to silence notifications for actions that are done and then undone in rapid succession (that is, to do what the bug requests). Undone actions are omitted from emails, and if the emails then have no content, they are not sent.

The only thing left to do is to make sure that the omitted notification objects are marked as processed so that they are not perpetually considered for subsequent notification cronscript runs. This is tackled in the last branch of the series.

The implementation is all in lib/lp/bugs/scripts/bugnotification.py. Within construct_email_notifications, there were already two loops through the notification objects, so I rearranged them so I could hook into one of them to determine what could be omitted. The changes are a bit smaller than what a quick scan of the diff suggests, because I reordered the two loops.

`get_key` probably holds the subtlest code here: we normalize the bug activity's data to give us a key that will work for items that are linked/added and unlinked/removed. For instance, if a change adds one branch but then removes another, both of these should be reported. We can only squelch the notification if the same branch is added and removed. A less subtle but important part of get_key is that it normalizes the "duplicate" names. Unlike other changes, the activity text changes depending on what happens, so we need to normalize that here.

I renamed the local variable "person_causing_change" to "actor" as an easy solution to some long lines.

The bulk of the branch is the tests for these relatively small changes. I changed the doctest enough for it to pass (and mention the behavior in passing), and updated some test infrastructure in lib/lp/bugs/scripts/tests/test_bugnotification.py (by adding the "activity" attribute to the mock bug notifications), but the main changes are brand new tests at the bottom of lib/lp/bugs/scripts/tests/test_bugnotification.py . I don't test all change objects, just the ones that were unusual. In this case, it means I omitted tests for non-collection bug attributes other than title (representative) and duplicate (exceptional), expecting the rest to work out fine; and I omitted tests for non-collection bugtask attributes other than status. I can add them relatively easily--with fun test subclasses--if you like.

Speaking of subclasses, I don't love what I've done to assemble these tests from subclasses and mixins, but it certainly has precedence in our codebase, and the alternative, with lots of repetition, is even less attractive.

I removed the "def test_suite():" boilerplate because, AIUI, we have now 'fessed up to the fact that it didn't accomplish anything practical, and are actively encouraging removal.

Thank you

To post a comment you must log in.
Revision history for this message
Данило Шеган (danilo) wrote :

Conclusions from IRC.

To increase trust in what's going on in parsing whatchanged, we should:
 1) add docstring and unittest for get_key
 2) move duplicateof code to target computed attribute code, and add test
 3) change get_key to use target/attribute

As for the rest, I really like the test coverage: great job there!

And a side note for s/person_causing_change/actor/: wordy name was introduced instead of 'person' to help make code easier to understand/read. It's up to you to decide which you want to keep as long as you take code readability into account :)

Considering we have agreed on a few changes, I am marking this as 'approved', though I'd be happy to take a look at incremental diff if it's a significant change.

review: Approve
Revision history for this message
Gary Poster (gary) wrote :

I'll wait for Данило's review of my changes, because they ended up being fairly extensive.

Revision history for this message
Данило Шеган (danilo) wrote :

Gary, thanks for the improvements.

This looks much, much, nicer — sorry for putting you through the
trouble, but I think it was worth it. :)

  review approve
  merge approve

Just one minor tidbit below:

>=== modified file 'lib/lp/bugs/scripts/tests/test_bugnotification.py'
...
>+
>+class MockBugActivity:
>+ """A mock BugActivity user for testing."""

A typo most likely: s/user/used/.

Cheers,
Danilo

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

BTW, there are mocks for bugactivity etc in the bugcomment tests
today; may want to snarf those and reused (if you haven't already, I
haven't read your diff).

Cheers,
Rob

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/adapters/bugchange.py'
--- lib/lp/bugs/adapters/bugchange.py 2011-02-04 01:15:13 +0000
+++ lib/lp/bugs/adapters/bugchange.py 2011-02-17 17:46:51 +0000
@@ -5,8 +5,20 @@
55
6__metaclass__ = type6__metaclass__ = type
7__all__ = [7__all__ = [
8 'ATTACHMENT_ADDED',
9 'ATTACHMENT_REMOVED',
10 'BRANCH_LINKED',
11 'BRANCH_UNLINKED',
12 'BUG_WATCH_ADDED',
13 'BUG_WATCH_REMOVED',
14 'CHANGED_DUPLICATE_MARKER',
15 'CVE_LINKED',
16 'CVE_UNLINKED',
17 'MARKED_AS_DUPLICATE',
18 'REMOVED_DUPLICATE_MARKER',
8 'BranchLinkedToBug',19 'BranchLinkedToBug',
9 'BranchUnlinkedFromBug',20 'BranchUnlinkedFromBug',
21 'BugAttachmentChange',
10 'BugConvertedToQuestion',22 'BugConvertedToQuestion',
11 'BugDescriptionChange',23 'BugDescriptionChange',
12 'BugDuplicateChange',24 'BugDuplicateChange',
@@ -40,13 +52,27 @@
40from lp.bugs.enum import BugNotificationLevel52from lp.bugs.enum import BugNotificationLevel
41from lp.bugs.interfaces.bugchange import IBugChange53from lp.bugs.interfaces.bugchange import IBugChange
42from lp.bugs.interfaces.bugtask import (54from lp.bugs.interfaces.bugtask import (
43 BugTaskStatus,
44 IBugTask,55 IBugTask,
45 RESOLVED_BUGTASK_STATUSES,56 RESOLVED_BUGTASK_STATUSES,
46 UNRESOLVED_BUGTASK_STATUSES,57 UNRESOLVED_BUGTASK_STATUSES,
47 )58 )
48from lp.registry.interfaces.product import IProduct59from lp.registry.interfaces.product import IProduct
4960
61# These are used lp.bugs.model.bugactivity.BugActivity.attribute to normalize
62# the output from these change objects into the attribute that actually
63# changed. It is fragile, but a reasonable incremental step.
64ATTACHMENT_ADDED = "attachment added"
65ATTACHMENT_REMOVED = "attachment removed"
66BRANCH_LINKED = 'branch linked'
67BRANCH_UNLINKED = 'branch unlinked'
68BUG_WATCH_ADDED = 'bug watch added'
69BUG_WATCH_REMOVED = 'bug watch removed'
70CHANGED_DUPLICATE_MARKER = 'changed duplicate marker'
71CVE_LINKED = 'cve linked'
72CVE_UNLINKED = 'cve unlinked'
73MARKED_AS_DUPLICATE = 'marked as duplicate'
74REMOVED_DUPLICATE_MARKER = 'removed duplicate marker'
75
5076
51class NoBugChangeFoundError(Exception):77class NoBugChangeFoundError(Exception):
52 """Raised when a BugChange class can't be found for an object."""78 """Raised when a BugChange class can't be found for an object."""
@@ -254,7 +280,7 @@
254 def getBugActivity(self):280 def getBugActivity(self):
255 """See `IBugChange`."""281 """See `IBugChange`."""
256 return dict(282 return dict(
257 whatchanged='bug watch added',283 whatchanged=BUG_WATCH_ADDED,
258 newvalue=self.bug_watch.url)284 newvalue=self.bug_watch.url)
259285
260 def getBugNotification(self):286 def getBugNotification(self):
@@ -278,7 +304,7 @@
278 def getBugActivity(self):304 def getBugActivity(self):
279 """See `IBugChange`."""305 """See `IBugChange`."""
280 return dict(306 return dict(
281 whatchanged='bug watch removed',307 whatchanged=BUG_WATCH_REMOVED,
282 oldvalue=self.bug_watch.url)308 oldvalue=self.bug_watch.url)
283309
284 def getBugNotification(self):310 def getBugNotification(self):
@@ -305,7 +331,7 @@
305 if self.branch.private:331 if self.branch.private:
306 return None332 return None
307 return dict(333 return dict(
308 whatchanged='branch linked',334 whatchanged=BRANCH_LINKED,
309 newvalue=self.branch.bzr_identity)335 newvalue=self.branch.bzr_identity)
310336
311 def getBugNotification(self):337 def getBugNotification(self):
@@ -328,7 +354,7 @@
328 if self.branch.private:354 if self.branch.private:
329 return None355 return None
330 return dict(356 return dict(
331 whatchanged='branch unlinked',357 whatchanged=BRANCH_UNLINKED,
332 oldvalue=self.branch.bzr_identity)358 oldvalue=self.branch.bzr_identity)
333359
334 def getBugNotification(self):360 def getBugNotification(self):
@@ -397,18 +423,18 @@
397 def getBugActivity(self):423 def getBugActivity(self):
398 if self.old_value is not None and self.new_value is not None:424 if self.old_value is not None and self.new_value is not None:
399 return {425 return {
400 'whatchanged': 'changed duplicate marker',426 'whatchanged': CHANGED_DUPLICATE_MARKER,
401 'oldvalue': str(self.old_value.id),427 'oldvalue': str(self.old_value.id),
402 'newvalue': str(self.new_value.id),428 'newvalue': str(self.new_value.id),
403 }429 }
404 elif self.old_value is None:430 elif self.old_value is None:
405 return {431 return {
406 'whatchanged': 'marked as duplicate',432 'whatchanged': MARKED_AS_DUPLICATE,
407 'newvalue': str(self.new_value.id),433 'newvalue': str(self.new_value.id),
408 }434 }
409 elif self.new_value is None:435 elif self.new_value is None:
410 return {436 return {
411 'whatchanged': 'removed duplicate marker',437 'whatchanged': REMOVED_DUPLICATE_MARKER,
412 'oldvalue': str(self.old_value.id),438 'oldvalue': str(self.old_value.id),
413 }439 }
414 else:440 else:
@@ -605,13 +631,13 @@
605631
606 def getBugActivity(self):632 def getBugActivity(self):
607 if self.old_value is None:633 if self.old_value is None:
608 what_changed = "attachment added"634 what_changed = ATTACHMENT_ADDED
609 old_value = None635 old_value = None
610 new_value = "%s %s" % (636 new_value = "%s %s" % (
611 self.new_value.title,637 self.new_value.title,
612 download_url_of_bugattachment(self.new_value))638 download_url_of_bugattachment(self.new_value))
613 else:639 else:
614 what_changed = "attachment removed"640 what_changed = ATTACHMENT_REMOVED
615 attachment = self.new_value641 attachment = self.new_value
616 old_value = "%s %s" % (642 old_value = "%s %s" % (
617 self.old_value.title,643 self.old_value.title,
@@ -656,7 +682,7 @@
656 """See `IBugChange`."""682 """See `IBugChange`."""
657 return dict(683 return dict(
658 newvalue=self.cve.sequence,684 newvalue=self.cve.sequence,
659 whatchanged='cve linked')685 whatchanged=CVE_LINKED)
660686
661 def getBugNotification(self):687 def getBugNotification(self):
662 """See `IBugChange`."""688 """See `IBugChange`."""
@@ -674,7 +700,7 @@
674 """See `IBugChange`."""700 """See `IBugChange`."""
675 return dict(701 return dict(
676 oldvalue=self.cve.sequence,702 oldvalue=self.cve.sequence,
677 whatchanged='cve unlinked')703 whatchanged=CVE_UNLINKED)
678704
679 def getBugNotification(self):705 def getBugNotification(self):
680 """See `IBugChange`."""706 """See `IBugChange`."""
681707
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-02-16 02:55:17 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-02-17 17:46:51 +0000
@@ -3814,7 +3814,13 @@
3814 @property3814 @property
3815 def change_summary(self):3815 def change_summary(self):
3816 """Return a formatted summary of the change."""3816 """Return a formatted summary of the change."""
3817 return self.attribute3817 if self.target is not None:
3818 # This is a bug task. We want the attribute, as filtered out.
3819 return self.attribute
3820 else:
3821 # Otherwise, the attribute is more normalized than what we want.
3822 # Use "whatchanged," which sometimes is more descriptive.
3823 return self.whatchanged
38183824
3819 @property3825 @property
3820 def _formatted_tags_change(self):3826 def _formatted_tags_change(self):
@@ -3858,30 +3864,31 @@
3858 'old_value': self.oldvalue,3864 'old_value': self.oldvalue,
3859 'new_value': self.newvalue,3865 'new_value': self.newvalue,
3860 }3866 }
3861 if self.attribute == 'summary':3867 attribute = self.attribute
3868 if attribute == 'title':
3862 # We display summary changes as a unified diff, replacing3869 # We display summary changes as a unified diff, replacing
3863 # \ns with <br />s so that the lines are separated properly.3870 # \ns with <br />s so that the lines are separated properly.
3864 diff = cgi.escape(3871 diff = cgi.escape(
3865 get_unified_diff(self.oldvalue, self.newvalue, 72), True)3872 get_unified_diff(self.oldvalue, self.newvalue, 72), True)
3866 return diff.replace("\n", "<br />")3873 return diff.replace("\n", "<br />")
38673874
3868 elif self.attribute == 'description':3875 elif attribute == 'description':
3869 # Description changes can be quite long, so we just return3876 # Description changes can be quite long, so we just return
3870 # 'updated' rather than returning the whole new description3877 # 'updated' rather than returning the whole new description
3871 # or a diff.3878 # or a diff.
3872 return 'updated'3879 return 'updated'
38733880
3874 elif self.attribute == 'tags':3881 elif attribute == 'tags':
3875 # We special-case tags because we can work out what's been3882 # We special-case tags because we can work out what's been
3876 # added and what's been removed.3883 # added and what's been removed.
3877 return self._formatted_tags_change.replace('\n', '<br />')3884 return self._formatted_tags_change.replace('\n', '<br />')
38783885
3879 elif self.attribute == 'assignee':3886 elif attribute == 'assignee':
3880 for key in return_dict:3887 for key in return_dict:
3881 if return_dict[key] is None:3888 if return_dict[key] is None:
3882 return_dict[key] = 'nobody'3889 return_dict[key] = 'nobody'
38833890
3884 elif self.attribute == 'milestone':3891 elif attribute == 'milestone':
3885 for key in return_dict:3892 for key in return_dict:
3886 if return_dict[key] is None:3893 if return_dict[key] is None:
3887 return_dict[key] = 'none'3894 return_dict[key] = 'none'
38883895
=== modified file 'lib/lp/bugs/doc/bugactivity.txt'
--- lib/lp/bugs/doc/bugactivity.txt 2011-02-11 18:33:04 +0000
+++ lib/lp/bugs/doc/bugactivity.txt 2011-02-17 17:46:51 +0000
@@ -113,13 +113,18 @@
113 u'status'113 u'status'
114114
115If the activity is not for a bug task, `target` is None, and `attribute` is115If the activity is not for a bug task, `target` is None, and `attribute` is
116the same as `whatchanged`. For instance, look at the attributes on the116typically the same as `whatchanged`. However, in some cases (ideally,
117previous activity.117whenever necessary), it is normalized to show the actual attribute name.
118For instance, look at the attributes on the previous activity.
118119
119 >>> print bug.activity[-2].target120 >>> print bug.activity[-2].target
120 None121 None
122 >>> bug.activity[-2].whatchanged
123 u'summary'
121 >>> bug.activity[-2].attribute124 >>> bug.activity[-2].attribute
122 u'summary'125 'title'
126
127(This is covered more comprehensively in tests/test_bugchanges.py).
123128
124Upstream product assignment edited129Upstream product assignment edited
125==================================130==================================
126131
=== modified file 'lib/lp/bugs/doc/bugnotification-sending.txt'
--- lib/lp/bugs/doc/bugnotification-sending.txt 2011-02-17 15:20:36 +0000
+++ lib/lp/bugs/doc/bugnotification-sending.txt 2011-02-17 17:46:51 +0000
@@ -293,14 +293,10 @@
293 - Old summary293 - Old summary
294 + New summary294 + New summary
295 <BLANKLINE>295 <BLANKLINE>
296 ** Visibility changed to: Private
297 <BLANKLINE>
298 ** Summary changed:296 ** Summary changed:
299 - New summary297 - New summary
300 + Another summary298 + Another summary
301 <BLANKLINE>299 <BLANKLINE>
302 ** Visibility changed to: Public
303 <BLANKLINE>
304 --300 --
305 You received this bug notification because you are a member of Ubuntu301 You received this bug notification because you are a member of Ubuntu
306 Team, which is the registrant for Ubuntu.302 Team, which is the registrant for Ubuntu.
@@ -312,6 +308,29 @@
312 To: test@canonical.com308 To: test@canonical.com
313 ...309 ...
314310
311If you look carefully, there's a surprise in that output: the visibility
312changes are not reported. This is because they are done and then undone
313within the same notification. Undone changes like that are omitted.
314moreover, if the email only would have reported done/undone changes, it
315is not sent at all. This is tested elsewhere (see
316lp/bugs/tests/test_bugnotification.py), and not demonstrated here.
317
318Another thing worth noting is that there's a blank line before the
319signature, and the signature marker has a trailing space.
320
321 >>> message.get_payload(decode=True).splitlines()
322 [...,
323 '',
324 '-- ',
325 'You received this bug notification because you are a direct subscriber',
326 'of the bug.',
327 'http://bugs.launchpad.dev/bugs/1',
328 '',
329 'Title:',
330 ' Firefox does not support SVG'...]
331
332 >>> flush_notifications()
333
315We send the notification only if the user hasn't done any other changes334We send the notification only if the user hasn't done any other changes
316for the last 5 minutes:335for the last 5 minutes:
317336
@@ -328,22 +347,6 @@
328347
329 >>> flush_notifications()348 >>> flush_notifications()
330349
331There's a blank line before the signature, and the signature marker has
332a trailing space.
333
334 >>> message.get_payload(decode=True).splitlines()
335 [...,
336 '',
337 '-- ',
338 'You received this bug notification because you are a direct subscriber',
339 'of the bug.',
340 'http://bugs.launchpad.dev/bugs/1',
341 '',
342 'Title:',
343 ' Firefox does not support SVG'...]
344
345 >>> flush_notifications()
346
347If a team without a contact address is subscribed to the bug, the350If a team without a contact address is subscribed to the bug, the
348notification will be sent to all members individually.351notification will be sent to all members individually.
349352
@@ -379,7 +382,6 @@
379382
380 >>> flush_notifications()383 >>> flush_notifications()
381384
382
383Duplicates385Duplicates
384----------386----------
385387
386388
=== modified file 'lib/lp/bugs/model/bugactivity.py'
--- lib/lp/bugs/model/bugactivity.py 2011-02-11 18:15:28 +0000
+++ lib/lp/bugs/model/bugactivity.py 2011-02-17 17:46:51 +0000
@@ -17,6 +17,19 @@
1717
18from canonical.database.datetimecol import UtcDateTimeCol18from canonical.database.datetimecol import UtcDateTimeCol
19from canonical.database.sqlbase import SQLBase19from canonical.database.sqlbase import SQLBase
20from lp.bugs.adapters.bugchange import (
21 ATTACHMENT_ADDED,
22 ATTACHMENT_REMOVED,
23 BRANCH_LINKED,
24 BRANCH_UNLINKED,
25 BUG_WATCH_ADDED,
26 BUG_WATCH_REMOVED,
27 CHANGED_DUPLICATE_MARKER,
28 CVE_LINKED,
29 CVE_UNLINKED,
30 MARKED_AS_DUPLICATE,
31 REMOVED_DUPLICATE_MARKER,
32 )
20from lp.bugs.interfaces.bugactivity import (33from lp.bugs.interfaces.bugactivity import (
21 IBugActivity,34 IBugActivity,
22 IBugActivitySet,35 IBugActivitySet,
@@ -68,12 +81,31 @@
68 `attribute` is determined based on the `whatchanged` string.81 `attribute` is determined based on the `whatchanged` string.
6982
70 :return: The attribute name of the item if `whatchanged` is of83 :return: The attribute name of the item if `whatchanged` is of
71 the form <target_name>: <attribute>. Otherwise, return the84 the form <target_name>: <attribute>. If we know how to determine
72 original `whatchanged` string.85 the attribute by normalizing whatchanged, we return that.
86 Otherwise, return the original `whatchanged` string.
73 """87 """
74 match = self.bugtask_change_re.match(self.whatchanged)88 match = self.bugtask_change_re.match(self.whatchanged)
75 if match is None:89 if match is None:
76 return self.whatchanged90 result = self.whatchanged
91 # Now we normalize names, as necessary. This is fragile, but
92 # a reasonable incremental step. These are consumed in
93 # lp.bugs.scripts.bugnotification.get_activity_key.
94 if result in (CHANGED_DUPLICATE_MARKER,
95 MARKED_AS_DUPLICATE,
96 REMOVED_DUPLICATE_MARKER):
97 result = 'duplicateof'
98 elif result in (ATTACHMENT_ADDED, ATTACHMENT_REMOVED):
99 result = 'attachments'
100 elif result in (BRANCH_LINKED, BRANCH_UNLINKED):
101 result = 'linked_branches'
102 elif result in (BUG_WATCH_ADDED, BUG_WATCH_REMOVED):
103 result = 'watches'
104 elif result in (CVE_LINKED, CVE_UNLINKED):
105 result = 'cves'
106 elif result == 'summary':
107 result = 'title'
108 return result
77 else:109 else:
78 return match.groupdict()['attribute']110 return match.groupdict()['attribute']
79111
80112
=== modified file 'lib/lp/bugs/scripts/bugnotification.py'
--- lib/lp/bugs/scripts/bugnotification.py 2011-02-16 14:11:46 +0000
+++ lib/lp/bugs/scripts/bugnotification.py 2011-02-17 17:46:51 +0000
@@ -31,6 +31,39 @@
31from lp.services.mail.mailwrapper import MailWrapper31from lp.services.mail.mailwrapper import MailWrapper
3232
3333
34def get_activity_key(notification):
35 """Given a notification, return a key for the activity if it exists.
36
37 The key will be used to determine whether changes for the activity are
38 undone within the same batch of notifications (which are supposed to
39 be all for the same bug when they get to this function). Therefore,
40 the activity's attribute is a good start for the key.
41
42 If the activity was on a bugtask, we will also want to distinguish
43 by bugtask, because, for instance, changing a status from INPROGRESS
44 to FIXCOMMITED on one bug task is not undone if the status changes
45 from FIXCOMMITTED to INPROGRESS on another bugtask.
46
47 Similarly, if the activity is about adding or removing something
48 that we can have multiple of, like a branch or an attachment, the
49 key should include information on that value, because adding one
50 attachment is not undone by removing another one.
51 """
52 activity = notification.activity
53 if activity is not None:
54 key = activity.attribute
55 if activity.target is not None:
56 key = ':'.join((activity.target, key))
57 if key in ('attachments', 'watches', 'cves', 'linked_branches'):
58 # We are intentionally leaving bug task bugwatches out of this
59 # list, so we use the key rather than the activity.attribute.
60 if activity.oldvalue is not None:
61 key = ':'.join((key, activity.oldvalue))
62 elif activity.newvalue is not None:
63 key = ':'.join((key, activity.newvalue))
64 return key
65
66
34def construct_email_notifications(bug_notifications):67def construct_email_notifications(bug_notifications):
35 """Construct an email from a list of related bug notifications.68 """Construct an email from a list of related bug notifications.
3669
@@ -39,31 +72,45 @@
39 """72 """
40 first_notification = bug_notifications[0]73 first_notification = bug_notifications[0]
41 bug = first_notification.bug74 bug = first_notification.bug
42 person_causing_change = first_notification.message.owner75 actor = first_notification.message.owner
43 subject = first_notification.message.subject76 subject = first_notification.message.subject
4477
45 comment = None78 comment = None
46 references = []79 references = []
47 text_notifications = []80 text_notifications = []
4881 old_values = {}
49 recipients = {}82 new_values = {}
50 for notification in bug_notifications:
51 for recipient in notification.recipients:
52 email_people = emailPeople(recipient.person)
53 if (not person_causing_change.selfgenerated_bugnotifications and
54 person_causing_change in email_people):
55 email_people.remove(person_causing_change)
56 for email_person in email_people:
57 recipients[email_person] = recipient
5883
59 for notification in bug_notifications:84 for notification in bug_notifications:
60 assert notification.bug == bug, bug.id85 assert notification.bug == bug, bug.id
61 assert notification.message.owner == person_causing_change, (86 assert notification.message.owner == actor, actor.id
62 person_causing_change.id)
63 if notification.is_comment:87 if notification.is_comment:
64 assert comment is None, (88 assert comment is None, (
65 "Only one of the notifications is allowed to be a comment.")89 "Only one of the notifications is allowed to be a comment.")
66 comment = notification.message90 comment = notification.message
91 else:
92 key = get_activity_key(notification)
93 if key is not None:
94 if key not in old_values:
95 old_values[key] = notification.activity.oldvalue
96 new_values[key] = notification.activity.newvalue
97
98 recipients = {}
99 filtered_notifications = []
100 for notification in bug_notifications:
101 key = get_activity_key(notification)
102 if (notification.is_comment or
103 key is None or
104 old_values[key] != new_values[key]):
105 # We will report this notification.
106 filtered_notifications.append(notification)
107 for recipient in notification.recipients:
108 email_people = emailPeople(recipient.person)
109 if (not actor.selfgenerated_bugnotifications and
110 actor in email_people):
111 email_people.remove(actor)
112 for email_person in email_people:
113 recipients[email_person] = recipient
67114
68 if bug.duplicateof is not None:115 if bug.duplicateof is not None:
69 text_notifications.append(116 text_notifications.append(
@@ -88,7 +135,7 @@
88 msgid = first_notification.message.rfc822msgid135 msgid = first_notification.message.rfc822msgid
89 email_date = first_notification.message.datecreated136 email_date = first_notification.message.datecreated
90137
91 for notification in bug_notifications:138 for notification in filtered_notifications:
92 if notification.message == comment:139 if notification.message == comment:
93 # Comments were just handled in the previous if block.140 # Comments were just handled in the previous if block.
94 continue141 continue
@@ -104,9 +151,8 @@
104 messages = []151 messages = []
105 mail_wrapper = MailWrapper(width=72)152 mail_wrapper = MailWrapper(width=72)
106 content = '\n\n'.join(text_notifications)153 content = '\n\n'.join(text_notifications)
107 from_address = get_bugmail_from_address(person_causing_change, bug)154 from_address = get_bugmail_from_address(actor, bug)
108 bug_notification_builder = BugNotificationBuilder(155 bug_notification_builder = BugNotificationBuilder(bug, actor)
109 bug, person_causing_change)
110 sorted_recipients = sorted(156 sorted_recipients = sorted(
111 recipients.items(), key=lambda t: t[0].preferredemail.email)157 recipients.items(), key=lambda t: t[0].preferredemail.email)
112 for email_person, recipient in sorted_recipients:158 for email_person, recipient in sorted_recipients:
@@ -157,7 +203,7 @@
157 rationale, references, msgid)203 rationale, references, msgid)
158 messages.append(msg)204 messages.append(msg)
159205
160 return bug_notifications, messages206 return filtered_notifications, messages
161207
162208
163def notification_comment_batches(notifications):209def notification_comment_batches(notifications):
164210
=== modified file 'lib/lp/bugs/scripts/tests/test_bugnotification.py'
--- lib/lp/bugs/scripts/tests/test_bugnotification.py 2011-02-17 09:17:00 +0000
+++ lib/lp/bugs/scripts/tests/test_bugnotification.py 2011-02-17 17:46:51 +0000
@@ -4,31 +4,57 @@
44
5__metaclass__ = type5__metaclass__ = type
66
7from datetime import datetime7from datetime import datetime, timedelta
8import unittest8import unittest
99
10import pytz10import pytz
11from testtools.matchers import Not
12from transaction import commit
11from zope.component import getUtility13from zope.component import getUtility
12from zope.interface import implements14from zope.interface import implements
1315
14from canonical.config import config16from canonical.config import config
15from canonical.database.sqlbase import commit17from canonical.database.sqlbase import flush_database_updates
16from lp.bugs.model.bugtask import BugTask18from canonical.launchpad.ftests import login
17from canonical.launchpad.helpers import get_contact_email_addresses19from canonical.launchpad.helpers import get_contact_email_addresses
18from canonical.launchpad.interfaces.message import IMessageSet20from canonical.launchpad.interfaces.message import IMessageSet
19from canonical.testing.layers import LaunchpadZopelessLayer21from canonical.testing.layers import LaunchpadZopelessLayer
22from lp.bugs.adapters.bugchange import (
23 BranchLinkedToBug,
24 BranchUnlinkedFromBug,
25 BugAttachmentChange,
26 BugDuplicateChange,
27 BugTagsChange,
28 BugTaskStatusChange,
29 BugTitleChange,
30 BugVisibilityChange,
31 BugWatchAdded,
32 BugWatchRemoved,
33 CveLinkedToBug,
34 CveUnlinkedFromBug,
35 )
20from lp.bugs.interfaces.bug import (36from lp.bugs.interfaces.bug import (
21 IBug,37 IBug,
22 IBugSet,38 IBugSet,
23 )39 )
40from lp.bugs.interfaces.bugnotification import IBugNotificationSet
41from lp.bugs.interfaces.bugtask import BugTaskStatus
24from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients42from lp.bugs.mail.bugnotificationrecipients import BugNotificationRecipients
43from lp.bugs.model.bugtask import BugTask
25from lp.bugs.scripts.bugnotification import (44from lp.bugs.scripts.bugnotification import (
26 get_email_notifications,45 get_email_notifications,
46 get_activity_key,
27 notification_batches,47 notification_batches,
28 notification_comment_batches,48 notification_comment_batches,
29 )49 )
30from lp.registry.interfaces.person import IPersonSet50from lp.registry.interfaces.person import IPersonSet
31from lp.registry.interfaces.product import IProductSet51from lp.registry.interfaces.product import IProductSet
52from lp.services.propertycache import cachedproperty
53from lp.testing import (
54 TestCase,
55 TestCaseWithFactory)
56from lp.testing.dbuser import lp_dbuser
57from lp.testing.matchers import Contains
3258
3359
34class MockBug:60class MockBug:
@@ -105,6 +131,67 @@
105 self.is_comment = is_comment131 self.is_comment = is_comment
106 self.date_emailed = date_emailed132 self.date_emailed = date_emailed
107 self.recipients = [MockBugNotificationRecipient()]133 self.recipients = [MockBugNotificationRecipient()]
134 self.activity = None
135
136
137class FakeNotification:
138 """An even simpler fake notification.
139
140 Used by TestGetActivityKey, TestNotificationCommentBatches and
141 TestNotificationBatches."""
142
143 class Message(object):
144 pass
145
146 def __init__(self, is_comment=False, bug=None, owner=None):
147 self.is_comment = is_comment
148 self.bug = bug
149 self.message = self.Message()
150 self.message.owner = owner
151 self.activity = None
152
153
154class MockBugActivity:
155 """A mock BugActivity used for testing."""
156 def __init__(self, target=None, attribute=None,
157 oldvalue=None, newvalue=None):
158 self.target = target
159 self.attribute = attribute
160 self.oldvalue = oldvalue
161 self.newvalue = newvalue
162
163
164class TestGetActivityKey(TestCase):
165 """Tests for get_activity_key()."""
166
167 def test_no_activity(self):
168 self.assertEqual(get_activity_key(FakeNotification()), None)
169
170 def test_normal_bug_attribute_activity(self):
171 notification = FakeNotification()
172 notification.activity = MockBugActivity(attribute='title')
173 self.assertEqual(get_activity_key(notification), 'title')
174
175 def test_collection_bug_attribute_added_activity(self):
176 notification = FakeNotification()
177 notification.activity = MockBugActivity(
178 attribute='cves', newvalue='some cve identifier')
179 self.assertEqual(get_activity_key(notification),
180 'cves:some cve identifier')
181
182 def test_collection_bug_attribute_removed_activity(self):
183 notification = FakeNotification()
184 notification.activity = MockBugActivity(
185 attribute='cves', oldvalue='some cve identifier')
186 self.assertEqual(get_activity_key(notification),
187 'cves:some cve identifier')
188
189 def test_bugtask_attribute_activity(self):
190 notification = FakeNotification()
191 notification.activity = MockBugActivity(
192 attribute='status', target='some bug task identifier')
193 self.assertEqual(get_activity_key(notification),
194 'some bug task identifier:status')
108195
109196
110class TestGetEmailNotifications(unittest.TestCase):197class TestGetEmailNotifications(unittest.TestCase):
@@ -240,19 +327,6 @@
240 self.assertEqual(bug_four.id, 4)327 self.assertEqual(bug_four.id, 4)
241328
242329
243class FakeNotification:
244 """Used by TestNotificationCommentBatches and TestNotificationBatches."""
245
246 class Message(object):
247 pass
248
249 def __init__(self, is_comment=False, bug=None, owner=None):
250 self.is_comment = is_comment
251 self.bug = bug
252 self.message = self.Message()
253 self.message.owner = owner
254
255
256class TestNotificationCommentBatches(unittest.TestCase):330class TestNotificationCommentBatches(unittest.TestCase):
257 """Tests of `notification_comment_batches`."""331 """Tests of `notification_comment_batches`."""
258332
@@ -456,3 +530,339 @@
456 expected = [notifications[0:3], notifications[3:4]]530 expected = [notifications[0:3], notifications[3:4]]
457 observed = list(notification_batches(notifications))531 observed = list(notification_batches(notifications))
458 self.assertEquals(expected, observed)532 self.assertEquals(expected, observed)
533
534
535class EmailNotificationTestBase(TestCaseWithFactory):
536
537 layer = LaunchpadZopelessLayer
538
539 def setUp(self):
540 super(EmailNotificationTestBase, self).setUp()
541 login('foo.bar@canonical.com')
542 self.product_owner = self.factory.makePerson(name="product-owner")
543 self.person = self.factory.makePerson(name="sample-person")
544 self.product = self.factory.makeProduct(owner=self.product_owner)
545 self.product_subscriber = self.factory.makePerson(
546 name="product-subscriber")
547 self.product.addBugSubscription(
548 self.product_subscriber, self.product_subscriber)
549 self.bug_subscriber = self.factory.makePerson(name="bug-subscriber")
550 self.bug_owner = self.factory.makePerson(name="bug-owner")
551 self.bug = self.factory.makeBug(
552 product=self.product, private=False, owner=self.bug_owner)
553 self.reporter = self.bug.owner
554 self.bug.subscribe(self.bug_subscriber, self.reporter)
555 [self.product_bugtask] = self.bug.bugtasks
556 commit()
557 login('test@canonical.com')
558 self.layer.switchDbUser(config.malone.bugnotification_dbuser)
559 self.now = datetime.now(pytz.timezone('UTC'))
560 self.ten_minutes_ago = self.now - timedelta(minutes=10)
561 self.notification_set = getUtility(IBugNotificationSet)
562 for notification in self.notification_set.getNotificationsToSend():
563 notification.date_emailed = self.now
564 flush_database_updates()
565
566 def tearDown(self):
567 for notification in self.notification_set.getNotificationsToSend():
568 notification.date_emailed = self.now
569 flush_database_updates()
570 super(EmailNotificationTestBase, self).tearDown()
571
572 def get_messages(self):
573 notifications = self.notification_set.getNotificationsToSend()
574 email_notifications = get_email_notifications(notifications)
575 for bug_notifications, messages in email_notifications:
576 for message in messages:
577 yield message, message.get_payload(decode=True)
578
579
580class EmailNotificationsBugMixin:
581
582 change_class = change_name = old = new = alt = unexpected_text = None
583
584 def change(self, old, new):
585 self.bug.addChange(
586 self.change_class(
587 self.ten_minutes_ago, self.person, self.change_name,
588 old, new))
589
590 def change_other(self):
591 self.bug.addChange(
592 BugVisibilityChange(
593 self.ten_minutes_ago, self.person, "private",
594 False, True))
595
596 def test_change_seen(self):
597 # A smoketest.
598 self.change(self.old, self.new)
599 message, body = self.get_messages().next()
600 self.assertThat(body, Contains(self.unexpected_text))
601
602 def test_undone_change_sends_no_emails(self):
603 self.change(self.old, self.new)
604 self.change(self.new, self.old)
605 self.assertEqual(list(self.get_messages()), [])
606
607 def test_undone_change_is_not_included(self):
608 self.change(self.old, self.new)
609 self.change(self.new, self.old)
610 self.change_other()
611 message, body = self.get_messages().next()
612 self.assertThat(body, Not(Contains(self.unexpected_text)))
613
614 def test_multiple_undone_changes_sends_no_emails(self):
615 self.change(self.old, self.new)
616 self.change(self.new, self.alt)
617 self.change(self.alt, self.old)
618 self.assertEqual(list(self.get_messages()), [])
619
620
621class EmailNotificationsBugNotRequiredMixin(EmailNotificationsBugMixin):
622 # This test collection is for attributes that can be None.
623 def test_added_removed_sends_no_emails(self):
624 self.change(None, self.old)
625 self.change(self.old, None)
626 self.assertEqual(list(self.get_messages()), [])
627
628 def test_removed_added_sends_no_emails(self):
629 self.change(self.old, None)
630 self.change(None, self.old)
631 self.assertEqual(list(self.get_messages()), [])
632
633 def test_duplicate_marked_changed_removed_sends_no_emails(self):
634 self.change(None, self.old)
635 self.change(self.old, self.new)
636 self.change(self.new, None)
637 self.assertEqual(list(self.get_messages()), [])
638
639
640class EmailNotificationsBugTaskMixin(EmailNotificationsBugMixin):
641
642 def change(self, old, new, index=0):
643 self.bug.addChange(
644 self.change_class(
645 self.bug.bugtasks[index], self.ten_minutes_ago,
646 self.person, self.change_name, old, new))
647
648 def test_changing_on_different_bugtasks_is_not_undoing(self):
649 with lp_dbuser():
650 product2 = self.factory.makeProduct(owner=self.product_owner)
651 self.bug.addTask(self.product_owner, product2)
652 self.change(self.old, self.new, index=0)
653 self.change(self.new, self.old, index=1)
654 message, body = self.get_messages().next()
655 self.assertThat(body, Contains(self.unexpected_text))
656
657
658class EmailNotificationsAddedRemovedMixin:
659
660 old = new = added_message = removed_message = None
661
662 def add(self, item):
663 raise NotImplementedError
664 remove = add
665
666 def test_added_seen(self):
667 self.add(self.old)
668 message, body = self.get_messages().next()
669 self.assertThat(body, Contains(self.added_message))
670
671 def test_added_removed_sends_no_emails(self):
672 self.add(self.old)
673 self.remove(self.old)
674 self.assertEqual(list(self.get_messages()), [])
675
676 def test_removed_added_sends_no_emails(self):
677 self.remove(self.old)
678 self.add(self.old)
679 self.assertEqual(list(self.get_messages()), [])
680
681 def test_added_another_removed_sends_emails(self):
682 self.add(self.old)
683 self.remove(self.new)
684 message, body = self.get_messages().next()
685 self.assertThat(body, Contains(self.added_message))
686 self.assertThat(body, Contains(self.removed_message))
687
688
689class TestEmailNotificationsBugTitle(
690 EmailNotificationsBugMixin, EmailNotificationTestBase):
691
692 change_class = BugTitleChange
693 change_name = "title"
694 old = "Old summary"
695 new = "New summary"
696 alt = "Another summary"
697 unexpected_text = '** Summary changed:'
698
699
700class TestEmailNotificationsBugTags(
701 EmailNotificationsBugMixin, EmailNotificationTestBase):
702
703 change_class = BugTagsChange
704 change_name = "tags"
705 old = ['foo', 'bar', 'baz']
706 new = ['foo', 'bar']
707 alt = ['bing', 'shazam']
708 unexpected_text = '** Tags'
709
710 def test_undone_ordered_set_sends_no_email(self):
711 # Tags use ordered sets to generate change descriptions, which we
712 # demonstrate here.
713 self.change(['foo', 'bar', 'baz'], ['foo', 'bar'])
714 self.change(['foo', 'bar'], ['baz', 'bar', 'foo', 'bar'])
715 self.assertEqual(list(self.get_messages()), [])
716
717
718class TestEmailNotificationsBugDuplicate(
719 EmailNotificationsBugNotRequiredMixin, EmailNotificationTestBase):
720
721 change_class = BugDuplicateChange
722 change_name = "duplicateof"
723 unexpected_text = 'duplicate'
724
725 def _bug(self):
726 with lp_dbuser():
727 return self.factory.makeBug()
728
729 old = cachedproperty('old')(_bug)
730 new = cachedproperty('new')(_bug)
731 alt = cachedproperty('alt')(_bug)
732
733
734class TestEmailNotificationsBugTaskStatus(
735 EmailNotificationsBugTaskMixin, EmailNotificationTestBase):
736
737 change_class = BugTaskStatusChange
738 change_name = "status"
739 old = BugTaskStatus.TRIAGED
740 new = BugTaskStatus.INPROGRESS
741 alt = BugTaskStatus.INVALID
742 unexpected_text = 'Status: '
743
744
745class TestEmailNotificationsBugWatch(
746 EmailNotificationsAddedRemovedMixin, EmailNotificationTestBase):
747
748 # Note that this is for bugwatches added to bugs. Bugwatches added
749 # to bugtasks are separate animals AIUI, and we don't try to combine
750 # them here for notifications. Bugtasks have only zero or one
751 # bugwatch, so they can be handled just as a simple bugtask attribute
752 # change, like status.
753
754 added_message = '** Bug watch added:'
755 removed_message = '** Bug watch removed:'
756
757 @cachedproperty
758 def tracker(self):
759 with lp_dbuser():
760 return self.factory.makeBugTracker()
761
762 def _watch(self, identifier='123'):
763 with lp_dbuser():
764 # This actually creates a notification all by itself. However,
765 # it won't be sent out for another five minutes. Therefore,
766 # we send out separate change notifications.
767 return self.bug.addWatch(
768 self.tracker, identifier, self.product_owner)
769
770 old = cachedproperty('old')(_watch)
771 new = cachedproperty('new')(lambda self: self._watch('456'))
772
773 def add(self, item):
774 with lp_dbuser():
775 self.bug.addChange(
776 BugWatchAdded(
777 self.ten_minutes_ago, self.product_owner, item))
778
779 def remove(self, item):
780 with lp_dbuser():
781 self.bug.addChange(
782 BugWatchRemoved(
783 self.ten_minutes_ago, self.product_owner, item))
784
785
786class TestEmailNotificationsBranch(
787 EmailNotificationsAddedRemovedMixin, EmailNotificationTestBase):
788
789 added_message = '** Branch linked:'
790 removed_message = '** Branch unlinked:'
791
792 def _branch(self):
793 with lp_dbuser():
794 return self.factory.makeBranch()
795
796 old = cachedproperty('old')(_branch)
797 new = cachedproperty('new')(_branch)
798
799 def add(self, item):
800 with lp_dbuser():
801 self.bug.addChange(
802 BranchLinkedToBug(
803 self.ten_minutes_ago, self.person, item, self.bug))
804
805 def remove(self, item):
806 with lp_dbuser():
807 self.bug.addChange(
808 BranchUnlinkedFromBug(
809 self.ten_minutes_ago, self.person, item, self.bug))
810
811
812class TestEmailNotificationsCVE(
813 EmailNotificationsAddedRemovedMixin, EmailNotificationTestBase):
814
815 added_message = '** CVE added:'
816 removed_message = '** CVE removed:'
817
818 def _cve(self, sequence):
819 with lp_dbuser():
820 return self.factory.makeCVE(sequence)
821
822 old = cachedproperty('old')(lambda self: self._cve('2020-1234'))
823 new = cachedproperty('new')(lambda self: self._cve('2020-5678'))
824
825 def add(self, item):
826 with lp_dbuser():
827 self.bug.addChange(
828 CveLinkedToBug(
829 self.ten_minutes_ago, self.person, item))
830
831 def remove(self, item):
832 with lp_dbuser():
833 self.bug.addChange(
834 CveUnlinkedFromBug(
835 self.ten_minutes_ago, self.person, item))
836
837
838class TestEmailNotificationsAttachments(
839 EmailNotificationsAddedRemovedMixin, EmailNotificationTestBase):
840
841 added_message = '** Attachment added:'
842 removed_message = '** Attachment removed:'
843
844 def _attachment(self):
845 with lp_dbuser():
846 # This actually creates a notification all by itself, via an
847 # event subscriber. However, it won't be sent out for
848 # another five minutes. Therefore, we send out separate
849 # change notifications.
850 return self.bug.addAttachment(
851 self.person, 'content', 'a comment', 'stuff.txt')
852
853 old = cachedproperty('old')(_attachment)
854 new = cachedproperty('new')(_attachment)
855
856 def add(self, item):
857 with lp_dbuser():
858 self.bug.addChange(
859 BugAttachmentChange(
860 self.ten_minutes_ago, self.person, 'attachment',
861 None, item))
862
863 def remove(self, item):
864 with lp_dbuser():
865 self.bug.addChange(
866 BugAttachmentChange(
867 self.ten_minutes_ago, self.person, 'attachment',
868 item, None))
459869
=== modified file 'lib/lp/bugs/tests/test_bugchanges.py'
--- lib/lp/bugs/tests/test_bugchanges.py 2011-02-16 12:05:44 +0000
+++ lib/lp/bugs/tests/test_bugchanges.py 2011-02-17 17:46:51 +0000
@@ -8,12 +8,12 @@
8 ObjectModifiedEvent,8 ObjectModifiedEvent,
9 )9 )
10from lazr.lifecycle.snapshot import Snapshot10from lazr.lifecycle.snapshot import Snapshot
11from testtools.matchers import StartsWith
11from zope.component import getUtility12from zope.component import getUtility
12from zope.event import notify13from zope.event import notify
13from zope.interface import providedBy14from zope.interface import providedBy
1415
15from canonical.launchpad.browser.librarian import ProxiedLibraryFileAlias16from canonical.launchpad.browser.librarian import ProxiedLibraryFileAlias
16from lp.bugs.model.bugnotification import BugNotification
17from canonical.launchpad.webapp.interfaces import ILaunchBag17from canonical.launchpad.webapp.interfaces import ILaunchBag
18from canonical.launchpad.webapp.publisher import canonical_url18from canonical.launchpad.webapp.publisher import canonical_url
19from canonical.testing.layers import LaunchpadFunctionalLayer19from canonical.testing.layers import LaunchpadFunctionalLayer
@@ -24,6 +24,7 @@
24 BugTaskStatus,24 BugTaskStatus,
25 )25 )
26from lp.bugs.interfaces.cve import ICveSet26from lp.bugs.interfaces.cve import ICveSet
27from lp.bugs.model.bugnotification import BugNotification
27from lp.bugs.scripts.bugnotification import construct_email_notifications28from lp.bugs.scripts.bugnotification import construct_email_notifications
28from lp.testing import (29from lp.testing import (
29 person_logged_in,30 person_logged_in,
@@ -220,6 +221,11 @@
220 # the Bug's notifications.221 # the Bug's notifications.
221 old_title = self.changeAttribute(self.bug, 'title', '42')222 old_title = self.changeAttribute(self.bug, 'title', '42')
222223
224 # This checks the activity's attribute and target attributes.
225 activity = self.bug.activity[-1]
226 self.assertEqual(activity.attribute, 'title')
227 self.assertEqual(activity.target, None)
228
223 title_change_activity = {229 title_change_activity = {
224 'whatchanged': 'summary',230 'whatchanged': 'summary',
225 'oldvalue': old_title,231 'oldvalue': old_title,
@@ -270,6 +276,11 @@
270 bugtracker = self.factory.makeBugTracker()276 bugtracker = self.factory.makeBugTracker()
271 bug_watch = self.bug.addWatch(bugtracker, '42', self.user)277 bug_watch = self.bug.addWatch(bugtracker, '42', self.user)
272278
279 # This checks the activity's attribute and target attributes.
280 activity = self.bug.activity[-1]
281 self.assertEqual(activity.attribute, 'watches')
282 self.assertEqual(activity.target, None)
283
273 bugwatch_activity = {284 bugwatch_activity = {
274 'person': self.user,285 'person': self.user,
275 'whatchanged': 'bug watch added',286 'whatchanged': 'bug watch added',
@@ -337,6 +348,11 @@
337 self.saveOldChanges()348 self.saveOldChanges()
338 self.bug.removeWatch(bug_watch, self.user)349 self.bug.removeWatch(bug_watch, self.user)
339350
351 # This checks the activity's attribute and target attributes.
352 activity = self.bug.activity[-1]
353 self.assertEqual(activity.attribute, 'watches')
354 self.assertEqual(activity.target, None)
355
340 bugwatch_activity = {356 bugwatch_activity = {
341 'person': self.user,357 'person': self.user,
342 'whatchanged': 'bug watch removed',358 'whatchanged': 'bug watch removed',
@@ -413,6 +429,12 @@
413 # sends an e-mail notification.429 # sends an e-mail notification.
414 branch = self.factory.makeBranch()430 branch = self.factory.makeBranch()
415 self.bug.linkBranch(branch, self.user)431 self.bug.linkBranch(branch, self.user)
432
433 # This checks the activity's attribute and target attributes.
434 activity = self.bug.activity[-1]
435 self.assertEqual(activity.attribute, 'linked_branches')
436 self.assertEqual(activity.target, None)
437
416 added_activity = {438 added_activity = {
417 'person': self.user,439 'person': self.user,
418 'whatchanged': 'branch linked',440 'whatchanged': 'branch linked',
@@ -459,6 +481,12 @@
459 self.bug.linkBranch(branch, self.user)481 self.bug.linkBranch(branch, self.user)
460 self.saveOldChanges()482 self.saveOldChanges()
461 self.bug.unlinkBranch(branch, self.user)483 self.bug.unlinkBranch(branch, self.user)
484
485 # This checks the activity's attribute and target attributes.
486 activity = self.bug.activity[-1]
487 self.assertEqual(activity.attribute, 'linked_branches')
488 self.assertEqual(activity.target, None)
489
462 added_activity = {490 added_activity = {
463 'person': self.user,491 'person': self.user,
464 'whatchanged': 'branch unlinked',492 'whatchanged': 'branch unlinked',
@@ -654,6 +682,11 @@
654 cve = getUtility(ICveSet)['1999-8979']682 cve = getUtility(ICveSet)['1999-8979']
655 self.bug.linkCVE(cve, self.user)683 self.bug.linkCVE(cve, self.user)
656684
685 # This checks the activity's attribute and target attributes.
686 activity = self.bug.activity[-1]
687 self.assertEqual(activity.attribute, 'cves')
688 self.assertEqual(activity.target, None)
689
657 cve_linked_activity = {690 cve_linked_activity = {
658 'person': self.user,691 'person': self.user,
659 'whatchanged': 'cve linked',692 'whatchanged': 'cve linked',
@@ -680,6 +713,11 @@
680 self.saveOldChanges()713 self.saveOldChanges()
681 self.bug.unlinkCVE(cve, self.user)714 self.bug.unlinkCVE(cve, self.user)
682715
716 # This checks the activity's attribute and target attributes.
717 activity = self.bug.activity[-1]
718 self.assertEqual(activity.attribute, 'cves')
719 self.assertEqual(activity.target, None)
720
683 cve_unlinked_activity = {721 cve_unlinked_activity = {
684 'person': self.user,722 'person': self.user,
685 'whatchanged': 'cve unlinked',723 'whatchanged': 'cve unlinked',
@@ -708,6 +746,11 @@
708 attachment = self.factory.makeBugAttachment(746 attachment = self.factory.makeBugAttachment(
709 bug=self.bug, owner=self.user, comment=message)747 bug=self.bug, owner=self.user, comment=message)
710748
749 # This checks the activity's attribute and target attributes.
750 activity = self.bug.activity[-1]
751 self.assertEqual(activity.attribute, 'attachments')
752 self.assertEqual(activity.target, None)
753
711 attachment_added_activity = {754 attachment_added_activity = {
712 'person': self.user,755 'person': self.user,
713 'whatchanged': 'attachment added',756 'whatchanged': 'attachment added',
@@ -741,6 +784,11 @@
741784
742 attachment.removeFromBug(user=self.user)785 attachment.removeFromBug(user=self.user)
743786
787 # This checks the activity's attribute and target attributes.
788 activity = self.bug.activity[-1]
789 self.assertEqual(activity.attribute, 'attachments')
790 self.assertEqual(activity.target, None)
791
744 attachment_removed_activity = {792 attachment_removed_activity = {
745 'person': self.user,793 'person': self.user,
746 'whatchanged': 'attachment removed',794 'whatchanged': 'attachment removed',
@@ -858,6 +906,11 @@
858 self.bug_task, bug_task_before_modification,906 self.bug_task, bug_task_before_modification,
859 ['importance'], user=self.user))907 ['importance'], user=self.user))
860908
909 # This checks the activity's attribute and target attributes.
910 activity = self.bug.activity[-1]
911 self.assertEqual(activity.attribute, 'importance')
912 self.assertThat(activity.target, StartsWith(u'product-name'))
913
861 expected_activity = {914 expected_activity = {
862 'person': self.user,915 'person': self.user,
863 'whatchanged': '%s: importance' % self.bug_task.bugtargetname,916 'whatchanged': '%s: importance' % self.bug_task.bugtargetname,
@@ -1304,6 +1357,11 @@
1304 level=BugNotificationLevel.METADATA).getRecipients()1357 level=BugNotificationLevel.METADATA).getRecipients()
1305 self.changeAttribute(duplicate_bug, 'duplicateof', self.bug)1358 self.changeAttribute(duplicate_bug, 'duplicateof', self.bug)
13061359
1360 # This checks the activity's attribute and target attributes.
1361 activity = duplicate_bug.activity[-1]
1362 self.assertEqual(activity.attribute, 'duplicateof')
1363 self.assertEqual(activity.target, None)
1364
1307 expected_activity = {1365 expected_activity = {
1308 'person': self.user,1366 'person': self.user,
1309 'whatchanged': 'marked as duplicate',1367 'whatchanged': 'marked as duplicate',
@@ -1351,6 +1409,11 @@
1351 self.saveOldChanges(duplicate_bug)1409 self.saveOldChanges(duplicate_bug)
1352 self.changeAttribute(duplicate_bug, 'duplicateof', None)1410 self.changeAttribute(duplicate_bug, 'duplicateof', None)
13531411
1412 # This checks the activity's attribute and target attributes.
1413 activity = duplicate_bug.activity[-1]
1414 self.assertEqual(activity.attribute, 'duplicateof')
1415 self.assertEqual(activity.target, None)
1416
1354 expected_activity = {1417 expected_activity = {
1355 'person': self.user,1418 'person': self.user,
1356 'whatchanged': 'removed duplicate marker',1419 'whatchanged': 'removed duplicate marker',
@@ -1382,6 +1445,11 @@
1382 self.saveOldChanges()1445 self.saveOldChanges()
1383 self.changeAttribute(self.bug, 'duplicateof', bug_two)1446 self.changeAttribute(self.bug, 'duplicateof', bug_two)
13841447
1448 # This checks the activity's attribute and target attributes.
1449 activity = self.bug.activity[-1]
1450 self.assertEqual(activity.attribute, 'duplicateof')
1451 self.assertEqual(activity.target, None)
1452
1385 expected_activity = {1453 expected_activity = {
1386 'person': self.user,1454 'person': self.user,
1387 'whatchanged': 'changed duplicate marker',1455 'whatchanged': 'changed duplicate marker',
@@ -1429,6 +1497,11 @@
1429 level=BugNotificationLevel.METADATA).getRecipients()1497 level=BugNotificationLevel.METADATA).getRecipients()
1430 self.changeAttribute(public_bug, 'duplicateof', private_bug)1498 self.changeAttribute(public_bug, 'duplicateof', private_bug)
14311499
1500 # This checks the activity's attribute and target attributes.
1501 activity = public_bug.activity[-1]
1502 self.assertEqual(activity.attribute, 'duplicateof')
1503 self.assertEqual(activity.target, None)
1504
1432 expected_activity = {1505 expected_activity = {
1433 'person': self.user,1506 'person': self.user,
1434 'whatchanged': 'marked as duplicate',1507 'whatchanged': 'marked as duplicate',
@@ -1468,6 +1541,11 @@
14681541
1469 self.changeAttribute(public_bug, 'duplicateof', None)1542 self.changeAttribute(public_bug, 'duplicateof', None)
14701543
1544 # This checks the activity's attribute and target attributes.
1545 activity = public_bug.activity[-1]
1546 self.assertEqual(activity.attribute, 'duplicateof')
1547 self.assertEqual(activity.target, None)
1548
1471 expected_activity = {1549 expected_activity = {
1472 'person': self.user,1550 'person': self.user,
1473 'whatchanged': 'removed duplicate marker',1551 'whatchanged': 'removed duplicate marker',
@@ -1504,6 +1582,11 @@
15041582
1505 self.changeAttribute(duplicate_bug, 'duplicateof', public_bug)1583 self.changeAttribute(duplicate_bug, 'duplicateof', public_bug)
15061584
1585 # This checks the activity's attribute and target attributes.
1586 activity = duplicate_bug.activity[-1]
1587 self.assertEqual(activity.attribute, 'duplicateof')
1588 self.assertEqual(activity.target, None)
1589
1507 expected_activity = {1590 expected_activity = {
1508 'person': self.user,1591 'person': self.user,
1509 'whatchanged': 'changed duplicate marker',1592 'whatchanged': 'changed duplicate marker',

Subscribers

People subscribed via source and target branches

to status/vote changes: