Merge lp:~cjwatson/launchpad/mail-footer-pref into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17638
Proposed branch: lp:~cjwatson/launchpad/mail-footer-pref
Merge into: lp:launchpad
Diff against target: 358 lines (+152/-11)
10 files modified
database/schema/security.cfg (+1/-0)
lib/lp/registry/browser/person.py (+2/-1)
lib/lp/registry/interfaces/person.py (+8/-0)
lib/lp/registry/model/person.py (+4/-4)
lib/lp/registry/stories/person/xx-person-edit.txt (+12/-2)
lib/lp/registry/tests/test_person.py (+5/-0)
lib/lp/scripts/garbo.py (+28/-1)
lib/lp/scripts/tests/test_garbo.py (+45/-0)
lib/lp/services/mail/basemailer.py (+17/-2)
lib/lp/services/mail/tests/test_basemailer.py (+30/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/mail-footer-pref
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+264720@code.launchpad.net

Commit message

Include filtering information in e-mail footers if PersonSettings.expanded_notification_footers is set.

Description of the change

Include filtering information in e-mail footers if PersonSettings.expanded_notification_footers is set.

Following https://code.launchpad.net/~cjwatson/launchpad/db-mail-footer-pref/+merge/264622, this models the new column, backfills it with False in a garbo job, and adds the relevant code to BaseMailer.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2015-07-08 15:13:48 +0000
3+++ database/schema/security.cfg 2015-07-14 14:44:02 +0000
4@@ -2279,6 +2279,7 @@
5 public.openidconsumerassociation = SELECT, DELETE
6 public.openidconsumernonce = SELECT, DELETE
7 public.person = SELECT, DELETE
8+public.personsettings = SELECT, UPDATE
9 public.product = SELECT, UPDATE
10 public.pofiletranslator = SELECT, INSERT, UPDATE, DELETE
11 public.potranslation = SELECT, DELETE
12
13=== modified file 'lib/lp/registry/browser/person.py'
14--- lib/lp/registry/browser/person.py 2015-07-09 20:06:17 +0000
15+++ lib/lp/registry/browser/person.py 2015-07-14 14:44:02 +0000
16@@ -2702,7 +2702,8 @@
17
18 field_names = ['displayname', 'name', 'mugshot', 'description',
19 'hide_email_addresses', 'verbose_bugnotifications',
20- 'selfgenerated_bugnotifications']
21+ 'selfgenerated_bugnotifications',
22+ 'expanded_notification_footers']
23 custom_widget('mugshot', ImageChangeWidget, ImageChangeWidget.EDIT_STYLE)
24
25 label = 'Change your personal details'
26
27=== modified file 'lib/lp/registry/interfaces/person.py'
28--- lib/lp/registry/interfaces/person.py 2015-05-11 13:17:41 +0000
29+++ lib/lp/registry/interfaces/person.py 2015-07-14 14:44:02 +0000
30@@ -552,6 +552,14 @@
31 title=_("Send me bug notifications for changes I make"),
32 required=False, default=False)
33
34+ expanded_notification_footers = Bool(
35+ title=_("Include filtering information in e-mail footers"),
36+ description=_(
37+ "Some e-mail clients do not allow filtering on arbitrary message "
38+ "headers. If you use one of these, you can set this option to "
39+ "add more information to the end of message bodies."),
40+ required=False, default=False)
41+
42
43 class IPersonPublic(IPrivacy):
44 """Public attributes for a Person.
45
46=== modified file 'lib/lp/registry/model/person.py'
47--- lib/lp/registry/model/person.py 2015-07-09 20:06:17 +0000
48+++ lib/lp/registry/model/person.py 2015-07-14 14:44:02 +0000
49@@ -20,6 +20,7 @@
50 'person_sort_key',
51 'PersonLanguage',
52 'PersonSet',
53+ 'PersonSettings',
54 'SSHKey',
55 'SSHKeySet',
56 'TeamInvitationEvent',
57@@ -105,10 +106,7 @@
58
59 from lp import _
60 from lp.answers.model.questionsperson import QuestionsPersonMixin
61-from lp.app.enums import (
62- InformationType,
63- PRIVATE_INFORMATION_TYPES,
64- )
65+from lp.app.enums import PRIVATE_INFORMATION_TYPES
66 from lp.app.interfaces.launchpad import (
67 IHasIcon,
68 IHasLogo,
69@@ -423,6 +421,8 @@
70
71 selfgenerated_bugnotifications = BoolCol(notNull=True, default=False)
72
73+ expanded_notification_footers = BoolCol(notNull=False, default=False)
74+
75
76 def readonly_settings(message, interface):
77 """Make an object that disallows writes to values on the interface.
78
79=== modified file 'lib/lp/registry/stories/person/xx-person-edit.txt'
80--- lib/lp/registry/stories/person/xx-person-edit.txt 2012-01-15 13:32:27 +0000
81+++ lib/lp/registry/stories/person/xx-person-edit.txt 2015-07-14 14:44:02 +0000
82@@ -78,12 +78,19 @@
83 False
84 >>> self_generated_control.click()
85
86+He will enable expanded mail notification footers.
87+
88+ >>> expanded_footer_control = browser.getControl(
89+ ... "Include filtering information in e-mail footers")
90+ >>> expanded_footer_control.selected
91+ False
92+ >>> expanded_footer_control.click()
93+
94 >>> browser.getControl('Save').click()
95 >>> browser.url
96 'http://launchpad.dev/~rayjay'
97
98-We now check to ensure that the verbose bug notifications option and
99-self-generated bug notification option were changed:
100+We now check to ensure that the various notifications options were changed:
101
102 >>> browser.open('http://launchpad.dev/~rayjay/+edit')
103 >>> browser.getControl("Include bug descriptions when sending me "
104@@ -92,3 +99,6 @@
105 >>> browser.getControl(
106 ... "Send me bug notifications for changes I make").selected
107 True
108+ >>> browser.getControl(
109+ ... "Include filtering information in e-mail footers").selected
110+ True
111
112=== modified file 'lib/lp/registry/tests/test_person.py'
113--- lib/lp/registry/tests/test_person.py 2015-06-26 09:59:30 +0000
114+++ lib/lp/registry/tests/test_person.py 2015-07-14 14:44:02 +0000
115@@ -515,6 +515,11 @@
116 user = self.factory.makePerson()
117 self.assertFalse(user.selfgenerated_bugnotifications)
118
119+ def test_expanded_notification_footers_false_by_default(self):
120+ # Default for new accounts is to disable expanded notification footers.
121+ user = self.factory.makePerson()
122+ self.assertFalse(user.expanded_notification_footers)
123+
124 def test_canAccess__anonymous(self):
125 # Anonymous users cannot call Person.canAccess()
126 person = self.factory.makePerson()
127
128=== modified file 'lib/lp/scripts/garbo.py'
129--- lib/lp/scripts/garbo.py 2015-07-13 00:52:36 +0000
130+++ lib/lp/scripts/garbo.py 2015-07-14 14:44:02 +0000
131@@ -70,7 +70,10 @@
132 )
133 from lp.hardwaredb.model.hwdb import HWSubmission
134 from lp.registry.model.commercialsubscription import CommercialSubscription
135-from lp.registry.model.person import Person
136+from lp.registry.model.person import (
137+ Person,
138+ PersonSettings,
139+ )
140 from lp.registry.model.product import Product
141 from lp.registry.model.teammembership import TeamMembership
142 from lp.services.config import config
143@@ -1411,6 +1414,29 @@
144 """
145
146
147+class PersonSettingsENFPopulator(BulkPruner):
148+ """Populates PersonSettings.expanded_notification_footers."""
149+
150+ target_table_class = PersonSettings
151+ ids_to_prune_query = """
152+ SELECT person
153+ FROM PersonSettings
154+ WHERE expanded_notification_footers IS NULL
155+ """
156+
157+ def __call__(self, chunk_size):
158+ """See `ITunableLoop`."""
159+ result = self.store.execute("""
160+ UPDATE PersonSettings
161+ SET expanded_notification_footers = FALSE
162+ WHERE person IN (
163+ SELECT * FROM
164+ cursor_fetch('%s', %d) AS f(person integer))
165+ """ % (self.cursor_name, chunk_size))
166+ self._num_removed = result.rowcount
167+ transaction.commit()
168+
169+
170 class BaseDatabaseGarbageCollector(LaunchpadCronScript):
171 """Abstract base class to run a collection of TunableLoops."""
172 script_name = None # Script name for locking and database user. Override.
173@@ -1694,6 +1720,7 @@
174 LoginTokenPruner,
175 ObsoleteBugAttachmentPruner,
176 OldTimeLimitedTokenDeleter,
177+ PersonSettingsENFPopulator,
178 RevisionAuthorEmailLinker,
179 ScrubPOFileTranslator,
180 SuggestiveTemplatesCacheUpdater,
181
182=== modified file 'lib/lp/scripts/tests/test_garbo.py'
183--- lib/lp/scripts/tests/test_garbo.py 2015-07-13 00:55:56 +0000
184+++ lib/lp/scripts/tests/test_garbo.py 2015-07-14 14:44:02 +0000
185@@ -16,6 +16,7 @@
186 import time
187
188 from pytz import UTC
189+from storm.exceptions import NoneError
190 from storm.expr import (
191 In,
192 Like,
193@@ -70,6 +71,7 @@
194 from lp.registry.interfaces.person import IPersonSet
195 from lp.registry.interfaces.teammembership import TeamMembershipStatus
196 from lp.registry.model.commercialsubscription import CommercialSubscription
197+from lp.registry.model.person import PersonSettings
198 from lp.registry.model.teammembership import TeamMembership
199 from lp.scripts.garbo import (
200 AntiqueSessionPruner,
201@@ -1369,6 +1371,49 @@
202 self._test_LiveFSFilePruner(
203 'application/octet-stream', 0, expected_count=1)
204
205+ def test_PersonSettingsENFPopulator(self):
206+ switch_dbuser('testadmin')
207+ store = IMasterStore(PersonSettings)
208+ people_enf_none = []
209+ people_enf_false = []
210+ people_enf_true = []
211+ for _ in range(2):
212+ person = self.factory.makePerson()
213+ try:
214+ person.expanded_notification_footers = None
215+ except NoneError:
216+ # Now enforced by DB NOT NULL constraint; backfilling is no
217+ # longer necessary.
218+ return
219+ people_enf_none.append(person)
220+ person = self.factory.makePerson()
221+ person.expanded_notification_footers = False
222+ people_enf_false.append(person)
223+ person = self.factory.makePerson()
224+ person.expanded_notification_footers = True
225+ people_enf_true.append(person)
226+ settings_count = store.find(PersonSettings).count()
227+ self.runDaily()
228+ switch_dbuser('testadmin')
229+
230+ # No rows have been deleted.
231+ self.assertEqual(settings_count, store.find(PersonSettings).count())
232+
233+ def _assert_enf_by_person(person, expected):
234+ record = store.find(
235+ PersonSettings, PersonSettings.person == person.id).one()
236+ self.assertEqual(expected, record.expanded_notification_footers)
237+
238+ # Rows with expanded_notification_footers=None have been backfilled.
239+ for person in people_enf_none:
240+ _assert_enf_by_person(person, False)
241+
242+ # Other rows have been left alone.
243+ for person in people_enf_false:
244+ _assert_enf_by_person(person, False)
245+ for person in people_enf_true:
246+ _assert_enf_by_person(person, True)
247+
248
249 class TestGarboTasks(TestCaseWithFactory):
250 layer = LaunchpadZopelessLayer
251
252=== modified file 'lib/lp/services/mail/basemailer.py'
253--- lib/lp/services/mail/basemailer.py 2012-06-29 08:40:05 +0000
254+++ lib/lp/services/mail/basemailer.py 2015-07-14 14:44:02 +0000
255@@ -1,4 +1,4 @@
256-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
257+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
258 # GNU Affero General Public License version 3 (see the file LICENSE).
259
260 """Base class for sending out emails."""
261@@ -7,6 +7,7 @@
262
263 __all__ = ['BaseMailer', 'RecipientReason']
264
265+from collections import OrderedDict
266 import logging
267 from smtplib import SMTPException
268
269@@ -77,6 +78,9 @@
270 headers = self._getHeaders(email)
271 subject = self._getSubject(email, recipient)
272 body = self._getBody(email, recipient)
273+ expanded_footer = self._getExpandedFooter(headers, recipient)
274+ if expanded_footer:
275+ body = append_footer(body, expanded_footer)
276 ctrl = self._mail_controller_class(
277 self.from_address, to_addresses, subject, body, headers,
278 envelope_to=[email])
279@@ -100,7 +104,8 @@
280 def _getHeaders(self, email):
281 """Return the mail headers to use."""
282 reason, rationale = self._recipients.getReason(email)
283- headers = {'X-Launchpad-Message-Rationale': reason.mail_header}
284+ headers = OrderedDict()
285+ headers['X-Launchpad-Message-Rationale'] = reason.mail_header
286 if self.notification_type is not None:
287 headers['X-Launchpad-Notification-Type'] = self.notification_type
288 reply_to = self._getReplyToAddress()
289@@ -146,6 +151,16 @@
290 """Provide a footer to attach to the body, or None."""
291 return None
292
293+ def _getExpandedFooter(self, headers, recipient):
294+ """Provide an expanded footer for recipients who have requested it."""
295+ if not recipient.expanded_notification_footers:
296+ return None
297+ lines = []
298+ for key, value in headers.items():
299+ if key.startswith('X-Launchpad-'):
300+ lines.append('%s: %s\n' % (key[2:], value))
301+ return ''.join(lines)
302+
303 def sendAll(self):
304 """Send notifications to all recipients."""
305 # We never want SMTP errors to propagate from this function.
306
307=== modified file 'lib/lp/services/mail/tests/test_basemailer.py'
308--- lib/lp/services/mail/tests/test_basemailer.py 2012-01-01 02:58:52 +0000
309+++ lib/lp/services/mail/tests/test_basemailer.py 2015-07-14 14:44:02 +0000
310@@ -1,4 +1,4 @@
311-# Copyright 2009 Canonical Ltd. This software is licensed under the
312+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
313 # GNU Affero General Public License version 3 (see the file LICENSE).
314 """Tests for the BaseMailer class."""
315
316@@ -7,6 +7,8 @@
317
318 from smtplib import SMTPException
319
320+from testtools.matchers import EndsWith
321+
322 from lp.services.mail.basemailer import BaseMailer
323 from lp.services.mail.sendmail import MailController
324 from lp.testing import TestCaseWithFactory
325@@ -132,6 +134,33 @@
326 self.assertEqual('text/plain', attachment['Content-Type'])
327 self.assertEqual('inline', attachment['Content-Disposition'])
328
329+ def test_generateEmail_append_no_expanded_footer(self):
330+ # Recipients without expanded_notification_footers do not receive an
331+ # expanded footer on messages.
332+ fake_to = self.factory.makePerson(email='to@example.com')
333+ recipients = {fake_to: FakeSubscription()}
334+ mailer = BaseMailerSubclass(
335+ 'subject', None, recipients, 'from@example.com',
336+ notification_type='test')
337+ ctrl = mailer.generateEmail('to@example.com', fake_to)
338+ self.assertNotIn('Launchpad-Message-Rationale', ctrl.body)
339+
340+ def test_generateEmail_append_expanded_footer(self):
341+ # Recipients with expanded_notification_footers receive an expanded
342+ # footer on messages.
343+ fake_to = self.factory.makePerson(email='to@example.com')
344+ fake_to.expanded_notification_footers = True
345+ recipients = {fake_to: FakeSubscription()}
346+ mailer = BaseMailerSubclass(
347+ 'subject', None, recipients, 'from@example.com',
348+ notification_type='test')
349+ ctrl = mailer.generateEmail('to@example.com', fake_to)
350+ self.assertThat(
351+ ctrl.body, EndsWith(
352+ '\n-- \n'
353+ 'Launchpad-Message-Rationale: pete\n'
354+ 'Launchpad-Notification-Type: test\n'))
355+
356 def test_sendall_single_failure_doesnt_kill_all(self):
357 # A failure to send to a particular email address doesn't stop sending
358 # to others.