Merge lp:~barry/launchpad/394133-urlwrap into lp:launchpad

Proposed by Barry Warsaw
Status: Merged
Approved by: Barry Warsaw
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~barry/launchpad/394133-urlwrap
Merge into: lp:launchpad
Diff against target: 147 lines (+98/-5)
3 files modified
lib/canonical/launchpad/doc/textformatting.txt (+76/-0)
lib/canonical/launchpad/mailnotification.py (+6/-1)
lib/lp/services/mail/mailwrapper.py (+16/-4)
To merge this branch: bzr merge lp:~barry/launchpad/394133-urlwrap
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+14504@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Barry Warsaw (barry) wrote :

= Summary =

Bug 394133 describes an annoying problem when messages are held for moderator
approval, with a team name that is fairly long and contains hyphens. The
message that gets sent to the team administrators has the url wrapped so that
it's generally not clickable in most mail readers.

== Proposed fix ==

Add a hack to MailWrapper.format() so that you can pass in a callable. This
callable gets the paragraph to wrap as an argument and may return False if the
paragraph should not get wrapped. Otherwise (return True), normal processing
will continue.

== Pre-implementation notes ==

The bug report describes a few alternative implementations. Once we're on
Python 2.6 we can just use textwrap's new argument to suppress breaking on
hyphens. We decided not to try to pull in Python 2.6's textwrap just for
this.

== Implementation details ==

None in particular.

== Tests ==

% bin/test -vv -t textformatting

== Demo and Q/A ==

 * make run_all
 * Create a team with a long name
 * Put the following message text in a file:

{{{
From: <email address hidden>
To: <email address hidden>
Subject: Test
Message-ID: <first>

Testing
}}}

 * Tweak the team name to match the long one you created above.
 * lib/mailman/bin/inject -l frankly-my-dear-i-dont-give-a-damn message.txt
 * Watch your email for the non-wrapped message

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/services/mail/mailwrapper.py
  lib/canonical/launchpad/mailnotification.py
  lib/canonical/launchpad/doc/textformatting.txt

Revision history for this message
Abel Deuring (adeuring) wrote :

Hi Barry,

Two remarks about this branch:

- Your changes avoid a broken URL in "moderation approval messages".
  But I am wondering, if this should perhaps be applied to other
  messages as well. After all, wrapped URLs for example in bug
  comments are bad too. (OK, in this case you can visit the related
  web page to see the "good" URL)
- Lines are only wrapped iff the paragraph starts with "http:" or
  "https:", so it won't work when the paragraph contains a few
  introductory words before the URL.

OTOH, you write that the wrapper in Python 2.6 will no longer need
this workaround, so I think this highly specialized fix is fine.

Abel

review: Approve (code)
Revision history for this message
Barry Warsaw (barry) wrote :

> Two remarks about this branch:
>
> - Your changes avoid a broken URL in "moderation approval messages".
> But I am wondering, if this should perhaps be applied to other
> messages as well. After all, wrapped URLs for example in bug
> comments are bad too. (OK, in this case you can visit the related
> web page to see the "good" URL)

I think we can handle this on a case-by-case basis. If we get additional bug reports we can implement a no-wrap filter function at that time.

> - Lines are only wrapped iff the paragraph starts with "http:" or
> "https:", so it won't work when the paragraph contains a few
> introductory words before the URL.

This is true, however if we run across such a situation, we can change the no-wrap filter function to suite the particular situation. The one implemented in this fix is only meant to cover the new-held-message email message.

>
> OTOH, you write that the wrapper in Python 2.6 will no longer need
> this workaround, so I think this highly specialized fix is fine.
>

Thanks! Yes, it will be better to use Python 2.6's facilities when we land on that version. Thanks for the review!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/doc/textformatting.txt'
--- lib/canonical/launchpad/doc/textformatting.txt 2009-03-24 12:43:49 +0000
+++ lib/canonical/launchpad/doc/textformatting.txt 2009-11-05 21:40:42 +0000
@@ -237,3 +237,79 @@
237 'separated with dos-style line endings.',237 'separated with dos-style line endings.',
238 '',238 '',
239 "Here's the second paragraph."]239 "Here's the second paragraph."]
240
241Sometimes certain paragraphs should not be wrapped, e.g. a line containing a
242long hyphenated URL. Under normal circumstances, this will get wrapped.
243
244 >>> from canonical.launchpad.helpers import get_email_template
245 >>> template = get_email_template('new-held-message.txt')
246 >>> text = template % dict(
247 ... user="Scarlett O'Hara",
248 ... team='frankly-my-dear-i-dont-give-a-damn',
249 ... subject='Thing',
250 ... author_name='Rhett Butler',
251 ... author_url='http://whatever.example.com/rhett',
252 ... date='today',
253 ... message_id='<aardvark>',
254 ... # And this is the one we're really interested in.
255 ... review_url=('http://launchpad.dev/~frankly-my-dear-i-'
256 ... 'dont-give-a-damn/+review-moderation-messages'),
257 ... )
258
259 >>> wrapper = MailWrapper(72)
260 >>> body = wrapper.format(text, force_wrap=True)
261 >>> print body
262 Hello Scarlett O'Hara,
263 <BLANKLINE>
264 frankly-my-dear-i-dont-give-a-damn has a new message requiring your
265 approval.
266 <BLANKLINE>
267 Subject: Thing
268 Author name: Rhett Butler
269 Author url: http://whatever.example.com/rhett
270 Date: today
271 Message-ID: <aardvark>
272 <BLANKLINE>
273 A message has been posted to the mailing list for your team, but this
274 message requires your approval before it will be sent to the list
275 members. After reviewing the message, you may approve, discard or
276 reject it.
277 <BLANKLINE>
278 To review all messages pending approval, visit:
279 <BLANKLINE>
280 http://launchpad.dev/~frankly-my-dear-i-dont-give-a-damn/+review-
281 moderation-messages
282 <BLANKLINE>
283 Regards,
284 The Launchpad team
285
286But if we don't want the line with the url to be wrapped, we can pass in a
287callable to format(). This callable prevents wrapping when it returns False.
288The callable's argument is the pre-wrapped paragraph.
289
290 >>> def nowrap(paragraph):
291 ... return paragraph.startswith('http://')
292
293 >>> body = wrapper.format(text, force_wrap=True, wrap_func=nowrap)
294 >>> print body
295 Hello Scarlett O'Hara,
296 <BLANKLINE>
297 frankly-my-dear-i-dont-give-a-damn has a new message requiring your approval.
298 <BLANKLINE>
299 Subject: Thing
300 Author name: Rhett Butler
301 Author url: http://whatever.example.com/rhett
302 Date: today
303 Message-ID: <aardvark>
304 <BLANKLINE>
305 A message has been posted to the mailing list for your team, but this
306 message requires your approval before it will be sent to the list
307 members. After reviewing the message, you may approve, discard or
308 reject it.
309 <BLANKLINE>
310 To review all messages pending approval, visit:
311 <BLANKLINE>
312 http://launchpad.dev/~frankly-my-dear-i-dont-give-a-damn/+review-moderation-messages
313 <BLANKLINE>
314 Regards,
315 The Launchpad team
240316
=== modified file 'lib/canonical/launchpad/mailnotification.py'
--- lib/canonical/launchpad/mailnotification.py 2009-10-30 13:08:01 +0000
+++ lib/canonical/launchpad/mailnotification.py 2009-11-05 21:40:42 +0000
@@ -1180,13 +1180,18 @@
1180 'team': team.displayname,1180 'team': team.displayname,
1181 }1181 }
11821182
1183 # Don't wrap the paragraph with the url.
1184 def wrap_function(paragraph):
1185 return (paragraph.startswith('http:') or
1186 paragraph.startswith('https:'))
1187
1183 # Send one message to every team administrator.1188 # Send one message to every team administrator.
1184 person_set = getUtility(IPersonSet)1189 person_set = getUtility(IPersonSet)
1185 for address in team.getTeamAdminsEmailAddresses():1190 for address in team.getTeamAdminsEmailAddresses():
1186 user = person_set.getByEmail(address)1191 user = person_set.getByEmail(address)
1187 replacements['user'] = user.displayname1192 replacements['user'] = user.displayname
1188 body = MailWrapper(72).format(1193 body = MailWrapper(72).format(
1189 template % replacements, force_wrap=True)1194 template % replacements, force_wrap=True, wrap_func=wrap_function)
1190 simple_sendmail(from_address, address, subject, body)1195 simple_sendmail(from_address, address, subject, body)
11911196
11921197
11931198
=== modified file 'lib/lp/services/mail/mailwrapper.py'
--- lib/lp/services/mail/mailwrapper.py 2009-06-25 04:06:00 +0000
+++ lib/lp/services/mail/mailwrapper.py 2009-11-05 21:40:42 +0000
@@ -32,11 +32,17 @@
32 width=width, subsequent_indent=indent,32 width=width, subsequent_indent=indent,
33 replace_whitespace=False, break_long_words=False)33 replace_whitespace=False, break_long_words=False)
3434
35 def format(self, text, force_wrap=False):35 def format(self, text, force_wrap=False, wrap_func=None):
36 """Format the text to be included in an email.36 """Format the text to be included in an email.
3737
38 If force_wrap is False, only paragraphs containing a single line38 :param force_wrap: When False (the default), only paragraphs
39 will be wrapped.39 containing a single line will be wrapped. Otherwise paragraphs in
40 text will be re-wrapped.
41 :type force_wrap: bool
42 :param wrap_func: A function to call at the beginning of each
43 paragraph to be wrapped. If the function returns False, the
44 paragraph is not wrapped.
45 :type wrap_func: callable or None
40 """46 """
41 wrapped_lines = []47 wrapped_lines = []
4248
@@ -54,7 +60,13 @@
54 for paragraph in text.split('\n\n'):60 for paragraph in text.split('\n\n'):
55 lines = paragraph.split('\n')61 lines = paragraph.split('\n')
5662
57 if len(lines) == 1:63 if wrap_func is not None and not wrap_func(paragraph):
64 # The user's callback function has indicated that the
65 # paragraph should not be wrapped.
66 wrapped_lines += (
67 [indentation + lines[0]] +
68 [self.indent + line for line in lines[1:]])
69 elif len(lines) == 1:
58 # We use TextWrapper only if the paragraph consists of a70 # We use TextWrapper only if the paragraph consists of a
59 # single line, like in the case where a person enters a71 # single line, like in the case where a person enters a
60 # comment via the web ui, without breaking the lines72 # comment via the web ui, without breaking the lines