Merge lp:~gmb/launchpad/refactor-bugnotificationbuilder-bug-594211-part-2 into lp:launchpad

Proposed by Graham Binns
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 11201
Proposed branch: lp:~gmb/launchpad/refactor-bugnotificationbuilder-bug-594211-part-2
Merge into: lp:launchpad
Diff against target: 333 lines (+298/-3)
3 files modified
lib/lp/bugs/configure.zcml (+8/-0)
lib/lp/bugs/mail/bugnotificationrecipients.py (+101/-3)
lib/lp/bugs/mail/tests/test_bugnotificationrecipients.py (+189/-0)
To merge this branch: bzr merge lp:~gmb/launchpad/refactor-bugnotificationbuilder-bug-594211-part-2
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+30649@code.launchpad.net

Commit message

A BugNotificationRecipients class has been added as part of the work to refactor bug notifications towards using BaseMailer.

Description of the change

BugNotification Refactoring: The saga continues
===============================================

Ah, what fun. This branch is the eventual culmination of several weeks'
development work. It's actually about two days' worth of work - there's
a whole lot of crap that I've had to revert to get tests to pass; I
realise now that my strategy was too ambitious in the first place, so
I've pared this branch down to the simplest it could be whilst still
adding value (shame I couldn't have hear Curtis' Epic talk two weeks
earlier).

Anyway.

This branch adds a BugNotificationRecipientReason class; this is the
next step in refactoring bug notifications to use BaseMailer. The new
class isn't hooked up to anything yet; I'll do that in a subsequent
branch since it seems to break every test *evar* (there are a lot of
tests that rely on notification reasons behaving like strings for some
reason).

I've added unittests to cover the new class.

No lint.

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

nice work!

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

That looks ok, but I wonder if it would be better using a factory
approach rather than class method helpers : it smells to me like you
are setting yourself up for an if:elif:elif:else: scenario, which is
painful and generally leads to bugs.

-Rob

Revision history for this message
Graham Binns (gmb) wrote :

On 22 July 2010 20:46, Robert Collins <email address hidden> wrote:
> That looks ok, but I wonder if it would be better using a factory
> approach rather than class method helpers : it smells to me like you
> are setting yourself up for an if:elif:elif:else: scenario, which is
> painful and generally leads to bugs.

Possibly; I was following the pattern that's used elsewhere by
RecipientReason descendants, but there's no guarantee that that's the
Right Way to do things.

When I return from leave I'll be working on some pretty heavy
refactoring; I suspect I'll find out then whether this is the wrong
approach and will refactor accordingly.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/configure.zcml'
2--- lib/lp/bugs/configure.zcml 2010-07-22 12:17:41 +0000
3+++ lib/lp/bugs/configure.zcml 2010-07-22 19:14:44 +0000
4@@ -981,4 +981,12 @@
5 <class class="lp.bugs.model.bug.FileBugData">
6 <allow interface="lp.bugs.interfaces.bug.IFileBugData" />
7 </class>
8+
9+ <class class="lp.bugs.mail.bugnotificationrecipients.BugNotificationRecipientReason">
10+ <allow attributes="
11+ bug
12+ getReason
13+ mail_header
14+ recipient"/>
15+ </class>
16 </configure>
17
18=== modified file 'lib/lp/bugs/mail/bugnotificationrecipients.py'
19--- lib/lp/bugs/mail/bugnotificationrecipients.py 2010-07-09 19:41:05 +0000
20+++ lib/lp/bugs/mail/bugnotificationrecipients.py 2010-07-22 19:14:44 +0000
21@@ -12,10 +12,111 @@
22
23 from canonical.launchpad.interfaces import INotificationRecipientSet
24
25+from lp.services.mail.basemailer import RecipientReason
26 from lp.services.mail.notificationrecipientset import (
27 NotificationRecipientSet)
28
29
30+class BugNotificationRecipientReason(RecipientReason):
31+ """A `RecipientReason` subsclass specifically for `BugNotification`s."""
32+
33+ def _getTemplateValues(self):
34+ template_values = (
35+ super(BugNotificationRecipientReason, self)._getTemplateValues())
36+ if self.recipient != self.subscriber or self.subscriber.is_team:
37+ template_values['entity_is'] = (
38+ 'You are a member of %s, which is' %
39+ self.subscriber.displayname)
40+ template_values['lc_entity_is'] = (
41+ 'you are a member of %s, which is' %
42+ self.subscriber.displayname)
43+ return template_values
44+
45+ @classmethod
46+ def makeRationale(cls, rationale, person, duplicate_bug=None):
47+ """See `RecipientReason.makeRationale`."""
48+ rationale = RecipientReason.makeRationale(rationale, person)
49+ if duplicate_bug is not None:
50+ rationale = "%s via Bug %s" % (rationale, duplicate_bug.id)
51+
52+ return rationale
53+
54+ @classmethod
55+ def _getReasonTemplate(cls, reason_string, duplicate_bug=None):
56+ """Return a reason template to pass to __init__()."""
57+ if duplicate_bug is not None:
58+ reason_suffix = " (via bug %s)." % duplicate_bug.id
59+ else:
60+ reason_suffix = "."
61+
62+ reason_parts = {
63+ 'prefix':
64+ "You received this bug notification because %(lc_entity_is)s",
65+ 'reason': reason_string,
66+ 'suffix': reason_suffix,
67+ }
68+ return "%(prefix)s %(reason)s%(suffix)s" % reason_parts
69+
70+ @classmethod
71+ def forDupeSubscriber(cls, person, duplicate_bug):
72+ """Return a `BugNotificationRecipientReason` for a dupe subscriber.
73+ """
74+ header = cls.makeRationale(
75+ 'Subscriber of Duplicate', person, duplicate_bug)
76+
77+ reason = cls._getReasonTemplate(
78+ "a direct subscriber of a duplicate bug (via bug %s)" %
79+ duplicate_bug.id)
80+ return cls(person, person, header, reason)
81+
82+ @classmethod
83+ def forDirectSubscriber(cls, person, duplicate_bug=None):
84+ """Return a `BugNotificationRecipientReason` for a direct subscriber.
85+ """
86+ header = cls.makeRationale("Subscriber", person, duplicate_bug)
87+ reason = cls._getReasonTemplate(
88+ "a direct subscriber of the bug", duplicate_bug)
89+ return cls(person, person, header, reason)
90+
91+ @classmethod
92+ def forAssignee(cls, person, duplicate_bug=None):
93+ """Return a `BugNotificationRecipientReason` for a bug assignee."""
94+ header = cls.makeRationale("Assignee", person, duplicate_bug)
95+ reason = cls._getReasonTemplate("a bug assignee", duplicate_bug)
96+ return cls(person, person, header, reason)
97+
98+ @classmethod
99+ def forBugSupervisor(cls, person, target, duplicate_bug=None):
100+ """Return a `BugNotificationRecipientReason` for a bug supervisor."""
101+ # All displaynames in these reasons should be changed to bugtargetname
102+ # (as part of bug 113262) once bugtargetname is finalized for packages
103+ # (bug 113258). Changing it before then would be excessively
104+ # disruptive.
105+ header = cls.makeRationale(
106+ "Bug Supervisor (%s)" % target.displayname, person, duplicate_bug)
107+ reason = cls._getReasonTemplate(
108+ "the bug supervisor for %s" % target.displayname, duplicate_bug)
109+ return cls(person, person, header, reason)
110+
111+ @classmethod
112+ def forStructuralSubscriber(cls, person, target, duplicate_bug=None):
113+ """Return a recipient reason for a structural subscriber."""
114+ header = cls.makeRationale(
115+ "Subscriber (%s)" % target.displayname, person, duplicate_bug)
116+ reason = cls._getReasonTemplate(
117+ "subscribed to %s" % target.displayname, duplicate_bug)
118+ return cls(person, person, header, reason)
119+
120+ @classmethod
121+ def forRegistrant(cls, person, target, duplicate_bug=None):
122+ """Return a recipient reason for a registrant."""
123+ header = cls.makeRationale(
124+ "Registrant (%s)" % target.displayname, person, duplicate_bug)
125+ reason = cls._getReasonTemplate(
126+ "the registrant for %s" % target.displayname, duplicate_bug)
127+ return cls(person, person, header, reason)
128+
129+
130 class BugNotificationRecipients(NotificationRecipientSet):
131 """A set of emails and rationales notified for a bug change.
132
133@@ -155,6 +256,3 @@
134 else:
135 text = "are the registrant for %s" % upstream.displayname
136 self._addReason(person, text, reason)
137-
138-
139-
140
141=== added file 'lib/lp/bugs/mail/tests/test_bugnotificationrecipients.py'
142--- lib/lp/bugs/mail/tests/test_bugnotificationrecipients.py 1970-01-01 00:00:00 +0000
143+++ lib/lp/bugs/mail/tests/test_bugnotificationrecipients.py 2010-07-22 19:14:44 +0000
144@@ -0,0 +1,189 @@
145+# Copyright 2010 Canonical Ltd. This software is licensed under the
146+# GNU Affero General Public License version 3 (see the file LICENSE).
147+
148+"""Tests for the bugnotificationrecipients module."""
149+
150+__metaclass__ = type
151+
152+import unittest
153+
154+from canonical.testing import DatabaseFunctionalLayer
155+
156+from lp.bugs.mail.bugnotificationrecipients import (
157+ BugNotificationRecipientReason)
158+from lp.testing import TestCaseWithFactory
159+
160+
161+class BugNotificationRecipientReasonTestCase(TestCaseWithFactory):
162+ """A TestCase for the `BugNotificationRecipientReason` class."""
163+
164+ layer = DatabaseFunctionalLayer
165+
166+ def setUp(self):
167+ super(BugNotificationRecipientReasonTestCase, self).setUp()
168+ self.person = self.factory.makePerson()
169+ self.team = self.factory.makeTeam(owner=self.person)
170+
171+ def _assertReasonAndHeaderAreCorrect(self, recipient_reason,
172+ expected_reason, expected_header):
173+ self.assertEqual(expected_header, recipient_reason.mail_header)
174+ self.assertEqual(expected_reason, recipient_reason.getReason())
175+
176+ def test_forDupeSubscriber(self):
177+ duplicate_bug = self.factory.makeBug()
178+ reason = BugNotificationRecipientReason.forDupeSubscriber(
179+ self.person, duplicate_bug)
180+
181+ expected_header = (
182+ 'Subscriber of Duplicate via Bug %s' % duplicate_bug.id)
183+ expected_reason = (
184+ 'You received this bug notification because you are a direct '
185+ 'subscriber of a duplicate bug (via bug %s).' % duplicate_bug.id)
186+ self._assertReasonAndHeaderAreCorrect(
187+ reason, expected_reason, expected_header)
188+
189+ def test_forDupeSubscriber_for_team(self):
190+ duplicate_bug = self.factory.makeBug()
191+ reason = BugNotificationRecipientReason.forDupeSubscriber(
192+ self.team, duplicate_bug)
193+
194+ expected_header = 'Subscriber of Duplicate @%s via Bug %s' % (
195+ self.team.name, duplicate_bug.id)
196+ expected_reason = (
197+ 'You received this bug notification because you are a member of '
198+ '%s, which is a direct subscriber of a duplicate bug (via bug '
199+ '%s).' %
200+ (self.team.displayname, duplicate_bug.id))
201+ self._assertReasonAndHeaderAreCorrect(
202+ reason, expected_reason, expected_header)
203+
204+ def test_forDirectSubscriber(self):
205+ reason = BugNotificationRecipientReason.forDirectSubscriber(
206+ self.person)
207+
208+ expected_header = "Subscriber"
209+ expected_reason = (
210+ "You received this bug notification because you are a direct "
211+ "subscriber of the bug.")
212+ self._assertReasonAndHeaderAreCorrect(
213+ reason, expected_reason, expected_header)
214+
215+ def test_forDirectSubscriber_for_team(self):
216+ reason = BugNotificationRecipientReason.forDirectSubscriber(
217+ self.team)
218+
219+ expected_header = "Subscriber @%s" % self.team.name
220+ expected_reason = (
221+ "You received this bug notification because you are a member "
222+ "of %s, which is a direct subscriber of the bug." %
223+ self.team.displayname)
224+ self._assertReasonAndHeaderAreCorrect(
225+ reason, expected_reason, expected_header)
226+
227+ def test_forAssignee(self):
228+ reason = BugNotificationRecipientReason.forAssignee(self.person)
229+
230+ expected_header = "Assignee"
231+ expected_reason = (
232+ "You received this bug notification because you are a bug "
233+ "assignee.")
234+
235+ self._assertReasonAndHeaderAreCorrect(
236+ reason, expected_reason, expected_header)
237+
238+ def test_forAssignee_for_team(self):
239+ reason = BugNotificationRecipientReason.forAssignee(self.team)
240+
241+ expected_header = "Assignee @%s" % self.team.name
242+ expected_reason = (
243+ "You received this bug notification because you are a member "
244+ "of %s, which is a bug assignee." % self.team.displayname)
245+
246+ self._assertReasonAndHeaderAreCorrect(
247+ reason, expected_reason, expected_header)
248+
249+ def test_forBugSupervisor(self):
250+ distro = self.factory.makeDistribution()
251+ reason = BugNotificationRecipientReason.forBugSupervisor(
252+ self.person, distro)
253+
254+ expected_header = "Bug Supervisor (%s)" % distro.displayname
255+ expected_reason = (
256+ "You received this bug notification because you are the bug "
257+ "supervisor for %s." % distro.displayname)
258+ self._assertReasonAndHeaderAreCorrect(
259+ reason, expected_reason, expected_header)
260+
261+ def test_forBugSupervisor_for_team(self):
262+ distro = self.factory.makeDistribution()
263+ reason = BugNotificationRecipientReason.forBugSupervisor(
264+ self.team, distro)
265+
266+ expected_header = "Bug Supervisor (%s) @%s" % (
267+ distro.displayname, self.team.name)
268+ expected_reason = (
269+ "You received this bug notification because you are a member "
270+ "of %s, which is the bug supervisor for %s." %
271+ (self.team.displayname, distro.displayname))
272+ self._assertReasonAndHeaderAreCorrect(
273+ reason, expected_reason, expected_header)
274+
275+ def test_forStructuralSubscriber(self):
276+ target = self.factory.makeProduct()
277+ reason = BugNotificationRecipientReason.forStructuralSubscriber(
278+ self.person, target)
279+
280+ expected_header = "Subscriber (%s)" % target.displayname
281+ expected_reason = (
282+ "You received this bug notification because you are subscribed "
283+ "to %s." % target.displayname)
284+
285+ self._assertReasonAndHeaderAreCorrect(
286+ reason, expected_reason, expected_header)
287+
288+ def test_forStructuralSubscriber_for_team(self):
289+ target = self.factory.makeProduct()
290+ reason = BugNotificationRecipientReason.forStructuralSubscriber(
291+ self.team, target)
292+
293+ expected_header = "Subscriber (%s) @%s" % (
294+ target.displayname, self.team.name)
295+ expected_reason = (
296+ "You received this bug notification because you are a member "
297+ "of %s, which is subscribed to %s." %
298+ (self.team.displayname, target.displayname))
299+
300+ self._assertReasonAndHeaderAreCorrect(
301+ reason, expected_reason, expected_header)
302+
303+ def test_forRegistrant(self):
304+ target = self.factory.makeProduct()
305+ reason = BugNotificationRecipientReason.forRegistrant(
306+ self.person, target)
307+
308+ expected_header = "Registrant (%s)" % target.displayname
309+ expected_reason = (
310+ "You received this bug notification because you are the "
311+ "registrant for %s." % target.displayname)
312+
313+ self._assertReasonAndHeaderAreCorrect(
314+ reason, expected_reason, expected_header)
315+
316+ def test_forRegistrant_for_team(self):
317+ target = self.factory.makeProduct()
318+ reason = BugNotificationRecipientReason.forRegistrant(
319+ self.team, target)
320+
321+ expected_header = "Registrant (%s) @%s" % (
322+ target.displayname, self.team.name)
323+ expected_reason = (
324+ "You received this bug notification because you are a member "
325+ "of %s, which is the registrant for %s." % (
326+ self.team.displayname, target.displayname))
327+
328+ self._assertReasonAndHeaderAreCorrect(
329+ reason, expected_reason, expected_header)
330+
331+
332+def test_suite():
333+ return unittest.TestLoader().loadTestsFromName(__name__)