Merge lp:~sinzui/launchpad/moderated-messages-0 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Merged at revision: 16128
Proposed branch: lp:~sinzui/launchpad/moderated-messages-0
Merge into: lp:launchpad
Diff against target: 739 lines (+217/-375)
7 files modified
lib/lp/registry/browser/mailinglists.py (+2/-6)
lib/lp/registry/browser/tests/mailinglist-message-views.txt (+0/-74)
lib/lp/registry/browser/tests/test_mailinglists.py (+194/-1)
lib/lp/registry/interfaces/mailinglist.py (+0/-13)
lib/lp/registry/model/mailinglist.py (+4/-33)
lib/lp/registry/stories/mailinglists/moderation.txt (+0/-237)
lib/lp/registry/tests/test_mailinglist.py (+17/-11)
To merge this branch: bzr merge lp:~sinzui/launchpad/moderated-messages-0
Reviewer Review Type Date Requested Status
Richard Harding (community) Approve
Review via email: mp+129013@code.launchpad.net

Commit message

Fix message moderation timeouts by avoiding the librarian.

Description of the change

The mailing list moderation view can timeout showing a list of messages
even though it shows fewer messages than most listings.

--------------------------------------------------------------------

RULES

    Pre-implementation: jcsackett
    * Lp is calling the librarian for each message to learn which
      email address was used to send the message.
      * This is not relevant because message approval is based on user,
        not email addresses.
      * The view is making a link to the lp user, but does extra work
        to craft the link to look like an email address.
    * HeldMessageDetails.email_message implicitly calls the librarian.
      * Only one callsite knows about this cachedproperty,
        HeldMessageDetails.sender
      * Only one callsite knows about the HeldMessageDetails.sender
        property.
    * Change the view to just use standard links for the the message
      author.
    * Remove .sender and .email_message from the HeldMessageDetails
      class

    ADDENDUM
    * The query that gets the MessageApproval also precaches the Message,
      but not the Person that sent the message. The Person is always
      accessed when working with MessageApproval.

QA

    * Ask a webops to make you an admin of
      https://launchpad.net/~ubuntu-cyclists
    * Visit https://launchpad.net/~ubuntu-cyclists/+moderation
    * Verify the page loads
    * Discard the messages prior to 2012 (the spam)
    * verify the page loads.

LINT

    lib/lp/registry/browser/mailinglists.py
    lib/lp/registry/browser/tests/mailinglist-message-views.txt
    lib/lp/registry/interfaces/mailinglist.py
    lib/lp/registry/model/mailinglist.py
    lib/lp/registry/tests/test_mailinglist.py

LoC

    This branch removes code that is unneeded by this refactoring.

TEST

    ./bin/test -vvc -t HeldMessage -t getReviewableMessages \
        lp.registry.tests.test_mailinglist
    ./bin/test -vvc -t HeldMessage lp.registry.browser.tests.test_mailinglist

IMPLEMENTATION

I updated the view to make a standard Lp link to the sender, which is faster
to format and provides consistency.
    lib/lp/registry/browser/mailinglists.py
    lib/lp/registry/browser/tests/mailinglist-message-views.txt

I updated MailingList.getReviewableMessages() to also cache the user in
MessageApproval.posted_by, then change HeldMessageDetails.posted_by to
use that same user. So the moderated view never asks the librarian
for the sender's email address, and the sender is cached along with the
Lp copy of the message. I removed .sender and .email_message because the
attributes are no longer unused.
    lib/lp/registry/interfaces/mailinglist.py
    lib/lp/registry/model/mailinglist.py
    lib/lp/registry/tests/test_mailinglist.py

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/mailinglists.py'
2--- lib/lp/registry/browser/mailinglists.py 2012-02-21 22:46:28 +0000
3+++ lib/lp/registry/browser/mailinglists.py 2012-10-11 01:57:24 +0000
4@@ -16,13 +16,13 @@
5
6 from zope.component import getUtility
7
8+from lp.app.browser.tales import PersonFormatterAPI
9 from lp.registry.interfaces.mailinglist import (
10 IHeldMessageDetails,
11 IMailingListSet,
12 )
13 from lp.registry.interfaces.person import ITeam
14 from lp.services.webapp import (
15- canonical_url,
16 LaunchpadView,
17 )
18
19@@ -44,11 +44,7 @@
20 self.subject = self.details.subject
21 self.date = self.details.date
22 self.widget_name = 'field.' + quote(self.message_id)
23- # The author field is very close to what the details has, except that
24- # the view wants to include a link to the person's overview page.
25- self.author = '<a href="%s">%s</a>' % (
26- canonical_url(self.details.author),
27- escape(self.details.sender))
28+ self.author = PersonFormatterAPI(self.details.author).link(None)
29
30 def initialize(self):
31 """See `LaunchpadView`."""
32
33=== removed file 'lib/lp/registry/browser/tests/mailinglist-message-views.txt'
34--- lib/lp/registry/browser/tests/mailinglist-message-views.txt 2012-04-10 14:01:17 +0000
35+++ lib/lp/registry/browser/tests/mailinglist-message-views.txt 1970-01-01 00:00:00 +0000
36@@ -1,74 +0,0 @@
37-Held message view
38------------------
39-
40-The HeldMessageView formats a held message so that it can be moderated by
41-the mailing list administrator.
42-
43- >>> from lp.services.messages.interfaces.message import IMessageSet
44-
45- >>> team, mlist = factory.makeTeamAndMailingList('parrot', 'owner')
46-
47- >>> login('admin@canonical.com')
48- >>> message_set = getUtility(IMessageSet)
49- >>> message = message_set.fromEmail("""\
50- ... From: Carlos <carlos@canonical.com>
51- ... To: parrot@lists.launchpad.dev
52- ... Subject: monkey
53- ... Message-ID: <monkey>
54- ... Date: Fri, 01 Aug 2000 01:09:00 -0000
55- ...
56- ... First paragraph.
57- ...
58- ... Second paragraph.
59- ...
60- ... Third paragraph.
61- ... """)
62- >>> held_message = mlist.holdMessage(message)
63- >>> transaction.commit()
64-
65- >>> ignored = login_person(team.teamowner)
66- >>> view = create_initialized_view(
67- ... held_message, name='+moderation', principal=team.teamowner)
68- >>> print view.widget_name
69- field.%3Cmonkey%3E
70- >>> print view.message_id
71- <monkey>
72- >>> print view.subject
73- monkey
74- >>> print view.date
75- 2000-08-01 01:09:00+00:00
76- >>> print view.author
77- <a href="http://launchpad.dev/~carlos">Carlos &lt;carlos@canonical.com&gt;</a>
78- >>> print view.body_summary
79- First paragraph.
80- >>> print view.body_details
81- <p>
82- Second paragraph.
83- </p>
84- <p>
85- Third paragraph.
86- </p>
87-
88-
89-View helpers
90-------------
91-
92-The view uses a helper method to format the paragraphs.
93-
94- >>> paragraphs = []
95- >>> current_paragraph = ['line 1', 'line 2']
96- >>> view._append_paragraph(paragraphs, current_paragraph)
97- >>> for line in paragraphs:
98- ... print line
99- <p>
100- line 1
101- line 2
102- </p>
103-
104-The _append_paragraph method ignores empty paragraphs.
105-
106- >>> paragraphs = []
107- >>> current_paragraph = []
108- >>> view._append_paragraph(paragraphs, current_paragraph)
109- >>> len(paragraphs)
110- 0
111
112=== modified file 'lib/lp/registry/browser/tests/test_mailinglists.py'
113--- lib/lp/registry/browser/tests/test_mailinglists.py 2012-01-01 02:58:52 +0000
114+++ lib/lp/registry/browser/tests/test_mailinglists.py 2012-10-11 01:57:24 +0000
115@@ -6,14 +6,22 @@
116
117 __metaclass__ = type
118
119+import transaction
120+from zope.component import getUtility
121
122+from lp.app.browser.tales import PersonFormatterAPI
123 from lp.registry.interfaces.person import PersonVisibility
124+from lp.services.messages.interfaces.message import IMessageSet
125+from lp.services.webapp.authorization import check_permission
126 from lp.testing import (
127 login_person,
128 person_logged_in,
129 TestCaseWithFactory,
130 )
131-from lp.testing.layers import DatabaseFunctionalLayer
132+from lp.testing.layers import (
133+ DatabaseFunctionalLayer,
134+ LaunchpadFunctionalLayer,
135+ )
136 from lp.testing.pages import (
137 extract_text,
138 find_tag_by_id,
139@@ -36,6 +44,26 @@
140 self.factory.makeMailingList(team=team, owner=owner)
141 return team
142
143+ def makeHeldMessage(self, team, sender=None):
144+ # Requires LaunchpadFunctionalLayer.
145+ if sender is None:
146+ sender = self.factory.makePerson(
147+ email='him@eg.dom', name='him', displayname='Him')
148+ raw = '\n'.join([
149+ 'From: Him <him@eg.dom>',
150+ 'To: %s' % str(team.mailing_list.address),
151+ 'Subject: monkey',
152+ 'Message-ID: <monkey>',
153+ 'Date: Fri, 01 Aug 2000 01:09:00 -0000',
154+ '',
155+ 'First paragraph.\n\nSecond paragraph.\n\nThird paragraph.'
156+ ])
157+ message_set = getUtility(IMessageSet)
158+ message = message_set.fromEmail(raw)
159+ transaction.commit()
160+ held_message = team.mailing_list.holdMessage(message)
161+ return sender, message, held_message
162+
163
164 class MailingListSubscriptionControlsTestCase(TestCaseWithFactory):
165 """Verify the team index subscribe/unsubscribe to mailing list content."""
166@@ -138,3 +166,168 @@
167 team, name='+mailinglist', principal=owner)
168 element = find_tag_by_id(view(), 'mailing-list-archive')
169 self.assertEqual('private', extract_text(element))
170+
171+
172+class HeldMessageViewTestCase(MailingListTestCase):
173+ """Verify the +moderation view."""
174+
175+ layer = LaunchpadFunctionalLayer
176+
177+ def test_view_properties(self):
178+ team = self.makeTeamWithMailingList()
179+ sender, message, held_message = self.makeHeldMessage(team)
180+ view = create_initialized_view(
181+ held_message, name='+moderation')
182+ self.assertEqual(message.subject, view.subject)
183+ self.assertEqual(message.rfc822msgid, view.message_id)
184+ self.assertEqual(message.datecreated, view.date)
185+ self.assertEqual(PersonFormatterAPI(sender).link(None), view.author)
186+ self.assertEqual("First paragraph.", view.body_summary)
187+ self.assertEqual(
188+ "\n<p>\nSecond paragraph.\n</p>\n\n<p>\nThird paragraph.\n</p>\n",
189+ view.body_details)
190+
191+ def test_view_append_paragraph(self):
192+ # Consecutive lines are wrapped in html <p> tags.
193+ team = self.makeTeamWithMailingList()
194+ sender, message, held_message = self.makeHeldMessage(team)
195+ view = create_initialized_view(
196+ held_message, name='+moderation')
197+ paragraphs = []
198+ view._append_paragraph(paragraphs, ['line 1', 'line 2'])
199+ self.assertEqual(
200+ ['\n<p>\n', 'line 1\nline 2', '\n</p>\n'], paragraphs)
201+ paragraphs = []
202+ view._append_paragraph(paragraphs, [])
203+ self.assertEqual([], paragraphs)
204+
205+ def test_render(self):
206+ team = self.makeTeamWithMailingList()
207+ sender, message, held_message = self.makeHeldMessage(team)
208+ view = create_initialized_view(
209+ held_message, name='+moderation', principal=team.teamowner)
210+ markup = view.render()
211+ self.assertTextMatchesExpressionIgnoreWhitespace(
212+ '.*Subject:.*monkey.*From:.*Him.*Date:.*2000-08-01.*Message-ID'
213+ '.*&lt;monkey&gt;.*class="foldable-quoted".*',
214+ markup)
215+ self.assertTextMatchesExpressionIgnoreWhitespace(
216+ '.*<input type="radio" value="approve"'
217+ '.*name="field.%3Cmonkey%3E" />'
218+ '.*<input type="radio" value="reject"'
219+ '.*name="field.%3Cmonkey%3E" />'
220+ '.*<input type="radio" value="discard"'
221+ '.*name="field.%3Cmonkey%3E" />'
222+ '.*<input type="radio" value="hold"'
223+ '.* name="field.%3Cmonkey%3E" checked="checked" />.*',
224+ markup)
225+
226+
227+class TeamMailingListModerationViewTestCase(MailingListTestCase):
228+ """Verify the +mailinglist-moderate view."""
229+
230+ layer = LaunchpadFunctionalLayer
231+
232+ def test_permissions(self):
233+ # Team admins and privileged users can see the view others cannot.
234+ team = self.makeTeamWithMailingList()
235+ member = self.factory.makePerson()
236+ with person_logged_in(team.teamowner):
237+ team.addMember(member, team.teamowner)
238+ view = create_initialized_view(team, name='+mailinglist-moderate')
239+ self.assertIs(True, check_permission('launchpad.Edit', view))
240+ with person_logged_in(member):
241+ self.assertIs(False, check_permission('launchpad.Edit', view))
242+
243+ def test_message_summary_text(self):
244+ team = self.makeTeamWithMailingList()
245+ # No messages.
246+ view = create_initialized_view(
247+ team, name='+mailinglist-moderate', principal=team.teamowner)
248+ self.assertTextMatchesExpressionIgnoreWhitespace(
249+ '.*There are no mailing list messages requiring your review.*',
250+ view.render())
251+ # One message.
252+ self.makeHeldMessage(team)
253+ view = create_initialized_view(
254+ team, name='+mailinglist-moderate', principal=team.teamowner)
255+ self.assertTextMatchesExpressionIgnoreWhitespace(
256+ '.*1.*message has.*been posted to your mailing list.*',
257+ view.render())
258+
259+ def test_batching(self):
260+ team = self.makeTeamWithMailingList()
261+ sender, message, held_message = self.makeHeldMessage(team)
262+ for i in range(5):
263+ self.makeHeldMessage(team, sender)
264+ view = create_initialized_view(
265+ team, name='+mailinglist-moderate', principal=team.teamowner)
266+ self.assertEqual(6, view.hold_count)
267+ self.assertEqual('messages', view.held_messages.heading)
268+ self.assertTextMatchesExpressionIgnoreWhitespace(
269+ '.*upper-batch-nav-batchnav-next.*lower-batch-nav-batchnav-next.*',
270+ view.render())
271+
272+ def test_widgets(self):
273+ team = self.makeTeamWithMailingList()
274+ sender, message, held_message = self.makeHeldMessage(team)
275+ view = create_initialized_view(
276+ team, name='+mailinglist-moderate', principal=team.teamowner)
277+ self.assertTextMatchesExpressionIgnoreWhitespace(
278+ '.*name="field.%3Cmonkey%3E.*', view.render())
279+
280+ def test_approve(self):
281+ team = self.makeTeamWithMailingList()
282+ sender, message, held_message = self.makeHeldMessage(team)
283+ form = {
284+ 'field.%3Cmonkey%3E': 'approve',
285+ 'field.actions.moderate': 'Moderate',
286+ }
287+ view = create_initialized_view(
288+ team, name='+mailinglist-moderate', form=form)
289+ self.assertEqual([], view.errors)
290+ self.assertEqual(
291+ 'Held message approved; Message-ID: &lt;monkey&gt;',
292+ view.request.notifications[0].message)
293+
294+ def test_discard(self):
295+ team = self.makeTeamWithMailingList()
296+ sender, message, held_message = self.makeHeldMessage(team)
297+ form = {
298+ 'field.%3Cmonkey%3E': 'discard',
299+ 'field.actions.moderate': 'Moderate',
300+ }
301+ view = create_initialized_view(
302+ team, name='+mailinglist-moderate', form=form)
303+ self.assertEqual([], view.errors)
304+ self.assertEqual(
305+ 'Held message discarded; Message-ID: &lt;monkey&gt;',
306+ view.request.notifications[0].message)
307+
308+ def test_reject(self):
309+ team = self.makeTeamWithMailingList()
310+ sender, message, held_message = self.makeHeldMessage(team)
311+ form = {
312+ 'field.%3Cmonkey%3E': 'reject',
313+ 'field.actions.moderate': 'Moderate',
314+ }
315+ view = create_initialized_view(
316+ team, name='+mailinglist-moderate', form=form)
317+ self.assertEqual([], view.errors)
318+ self.assertEqual(
319+ 'Held message rejected; Message-ID: &lt;monkey&gt;',
320+ view.request.notifications[0].message)
321+
322+ def test_held(self):
323+ team = self.makeTeamWithMailingList()
324+ sender, message, held_message = self.makeHeldMessage(team)
325+ form = {
326+ 'field.%3Cmonkey%3E': 'hold',
327+ 'field.actions.moderate': 'Moderate',
328+ }
329+ view = create_initialized_view(
330+ team, name='+mailinglist-moderate', form=form)
331+ self.assertEqual([], view.errors)
332+ self.assertEqual(
333+ 'Messages still held for review: 1 of 1',
334+ view.request.notifications[0].message)
335
336=== modified file 'lib/lp/registry/interfaces/mailinglist.py'
337--- lib/lp/registry/interfaces/mailinglist.py 2012-09-14 22:39:54 +0000
338+++ lib/lp/registry/interfaces/mailinglist.py 2012-10-11 01:57:24 +0000
339@@ -834,13 +834,6 @@
340 description=_('The RFC 2822 Subject header.'),
341 required=True, readonly=True)
342
343- sender = Text(
344- title=_('Message author'),
345- description=_('The message originator (i.e. author), formatted as '
346- 'per RFC 2822 and derived from the RFC 2822 originator '
347- 'fields From and Reply-To. This is a unicode string.'),
348- required=True, readonly=True)
349-
350 author = Object(
351 schema=IPerson,
352 title=_('Message author'),
353@@ -857,12 +850,6 @@
354 description=_('The message body as plain text.'),
355 required=True, readonly=True)
356
357- email_message = Text(
358- title=_('The email message object'),
359- description=_('The email.message.Message object created from the '
360- "original message's raw text."),
361- required=True, readonly=True)
362-
363
364 class BaseSubscriptionErrors(Exception):
365 """Base class for subscription exceptions."""
366
367=== modified file 'lib/lp/registry/model/mailinglist.py'
368--- lib/lp/registry/model/mailinglist.py 2012-09-15 03:39:21 +0000
369+++ lib/lp/registry/model/mailinglist.py 2012-10-11 01:57:24 +0000
370@@ -16,12 +16,6 @@
371
372
373 import collections
374-from email import message_from_string
375-from email.Header import (
376- decode_header,
377- make_header,
378- )
379-from itertools import repeat
380 import operator
381 from socket import getfqdn
382 from string import Template
383@@ -456,11 +450,12 @@
384 MessageApproval.mailing_listID == self.id,
385 MessageApproval.status == PostedMessageStatus.NEW,
386 MessageApproval.messageID == Message.id,
387+ MessageApproval.posted_byID == Person.id
388 ]
389 if message_id_filter is not None:
390 clauses.append(Message.rfc822msgid.is_in(message_id_filter))
391- results = store.find((MessageApproval, Message),
392- *clauses)
393+ results = store.find(
394+ (MessageApproval, Message, Person), *clauses)
395 results.order_by(MessageApproval.posted_date, Message.rfc822msgid)
396 return DecoratedResultSet(results, operator.itemgetter(0))
397
398@@ -797,31 +792,7 @@
399 self.message_id = message_approval.message_id
400 self.subject = self.message.subject
401 self.date = self.message.datecreated
402- self.author = self.message.owner
403-
404- @cachedproperty
405- def email_message(self):
406- self.message.raw.open()
407- try:
408- return message_from_string(self.message.raw.read())
409- finally:
410- self.message.raw.close()
411-
412- @cachedproperty
413- def sender(self):
414- """See `IHeldMessageDetails`."""
415- originators = self.email_message.get_all('from', [])
416- originators.extend(self.email_message.get_all('reply-to', []))
417- if len(originators) == 0:
418- return 'n/a'
419- unicode_parts = []
420- for bytes, charset in decode_header(originators[0]):
421- if charset is None:
422- charset = 'us-ascii'
423- unicode_parts.append(
424- bytes.decode(charset, 'replace').encode('utf-8'))
425- header = make_header(zip(unicode_parts, repeat('utf-8')))
426- return unicode(header)
427+ self.author = self.message_approval.posted_by
428
429 @cachedproperty
430 def body(self):
431
432=== removed file 'lib/lp/registry/stories/mailinglists/moderation.txt'
433--- lib/lp/registry/stories/mailinglists/moderation.txt 2011-12-24 15:18:32 +0000
434+++ lib/lp/registry/stories/mailinglists/moderation.txt 1970-01-01 00:00:00 +0000
435@@ -1,237 +0,0 @@
436-=======================
437-Mailing list moderation
438-=======================
439-
440-When a non-member posts a message to a team's mailing list, that message
441-will be held for moderator review, if the sender is not a Launchpad
442-member in good standing (as is the default).
443-
444- >>> admin_browser.open('http://launchpad.dev/~guadamen')
445- >>> admin_browser.getLink(url='+mailinglist').click()
446- >>> admin_browser.getControl('Create new Mailing List').click()
447-
448- >>> from lp.registry.tests import mailinglists_helper
449- >>> login('foo.bar@canonical.com')
450- >>> mailinglists_helper.mailman.act()
451- >>> transaction.commit()
452-
453- # Ignore any notifications up to this point.
454- >>> from lp.testing import mail_helpers
455- >>> ignore = mail_helpers.pop_notifications()
456-
457- >>> from lp.testing import login, logout
458- >>> from zope.component import getUtility
459- >>> from lp.registry.interfaces.mailinglist import IMailingListSet
460-
461- >>> guadamen = getUtility(IMailingListSet).get('guadamen')
462- >>> logout()
463-
464-Anonymous users cannot see the messages waiting for moderation.
465-
466- >>> anon_browser.open(
467- ... 'http://launchpad.dev/~guadamen/+mailinglist-moderate')
468- Traceback (most recent call last):
469- ...
470- Unauthorized: ...
471-
472-Non-administrators can also not review the messages.
473-
474- >>> user_browser.open(
475- ... 'http://launchpad.dev/~guadamen/+mailinglist-moderate')
476- Traceback (most recent call last):
477- ...
478- Unauthorized: ...
479-
480-Foo Bar though, is the moderator for the Guadamen team, so he can visit
481-the moderation page. But right now, there is nothing to moderate.
482-
483- >>> admin_browser.open(
484- ... 'http://launchpad.dev/~guadamen/+mailinglist-moderate')
485- >>> admin_browser.title
486- 'Mailing list moderation...
487- >>> print extract_text(find_tag_by_id(admin_browser.contents, 'legend'))
488- There are no mailing list messages requiring your review.
489-
490-Carlos is not a member of the Guadamen team, nor is he a Launchpad
491-member in good standing. He posts a message to the team's mailing list,
492-and it is held for approval.
493-
494- >>> login('foo.bar@canonical.com')
495- >>> from lp.services.messages.interfaces.message import IMessageSet
496- >>> message_set = getUtility(IMessageSet)
497- >>> message = message_set.fromEmail("""\
498- ... From: carlos@canonical.com
499- ... To: guadamen@lists.launchpad.dev
500- ... Subject: Aardvark
501- ... Message-ID: <aardvark>
502- ... Date: Fri, 01 Aug 2000 01:09:00 -0000
503- ...
504- ... This is my first post!
505- ... """)
506- >>> held_message = guadamen.holdMessage(message)
507- >>> logout()
508-
509-Foo Bar sees that there is one message waiting for approval.
510-
511- >>> admin_browser.reload()
512- >>> print extract_text(find_tag_by_id(admin_browser.contents, 'legend'))
513- 1 message has been posted to your mailing list...
514- ...
515-
516-Let's say Carlos posts another message to the list, which also gets held
517-for moderation. Now there are two messages waiting for review.
518-
519- >>> login('foo.bar@canonical.com')
520- >>> message = message_set.fromEmail("""\
521- ... From: carlos@canonical.com
522- ... To: guadamen@lists.launchpad.dev
523- ... Subject: Bobcat
524- ... Message-ID: <bobcat>
525- ... Date: Fri, 01 Aug 2000 01:09:01 -0000
526- ...
527- ... This is my second post!
528- ... """)
529- >>> held_message = guadamen.holdMessage(message)
530- >>> logout()
531-
532- >>> admin_browser.reload()
533- >>> print extract_text(find_tag_by_id(admin_browser.contents, 'legend'))
534- 2 messages have been posted to your mailing list...
535- ...
536-
537-If there are more messages than the batch size, they get batched.
538-
539- >>> admin_browser.open(
540- ... 'http://launchpad.dev/~guadamen/+mailinglist-moderate?batch=1')
541- >>> find_tag_by_id(admin_browser.contents, 'upper-batch-nav-batchnav-next')['class']
542- u'next'
543- >>> find_tag_by_id(admin_browser.contents, 'lower-batch-nav-batchnav-next')['class']
544- u'next'
545-
546-To test easily, we use the default batch size below.
547-
548- >>> admin_browser.open(
549- ... 'http://launchpad.dev/~guadamen/+mailinglist-moderate')
550-
551-
552-Each held message displays some details about what's being held.
553-
554- >>> print extract_text(find_tag_by_id(
555- ... admin_browser.contents, 'field.%3Caardvark%3E'))
556- Subject:
557- Aardvark
558- From:
559- carlos@canonical.com
560- Date:
561- 2000-08-01 01:09:00+00:00
562- Message-ID:
563- &lt;aardvark&gt;
564- This is my first post!
565-
566- >>> print extract_text(find_tag_by_id(
567- ... admin_browser.contents, 'field.%3Cbobcat%3E'))
568- Subject:
569- Bobcat
570- From:
571- carlos@canonical.com
572- Date:
573- 2000-08-01 01:09:01+00:00
574- Message-ID:
575- &lt;bobcat&gt;
576- This is my second post!
577-
578-By default, the action to take on all messages is to continue to hold
579-them. So if the messages are moderated now, they'll just show up the
580-next time Foo visits this page.
581-
582- >>> admin_browser.getControl('Moderate').click()
583- >>> admin_browser.title
584- 'GuadaMen in Launchpad'
585- >>> admin_browser.getLink('Moderate mailing list').click()
586- >>> print extract_text(find_tag_by_id(admin_browser.contents, 'legend'))
587- 2 messages have been posted to your mailing list...
588- ...
589-
590-Foo has three other options available to him for disposing of these
591-messages. Carlos sends one more to the list, and it is also held, which
592-gives Foo a perfect opportunity for exercises each of his options.
593-
594- >>> login('foo.bar@canonical.com')
595- >>> message = message_set.fromEmail("""\
596- ... From: carlos@canonical.com
597- ... To: guadamen@lists.launchpad.dev
598- ... Subject: Caribou
599- ... Message-ID: <caribou>
600- ... Date: Fri, 01 Aug 2000 01:09:02 -0000
601- ...
602- ... This is my third post!
603- ... """)
604- >>> held_message = guadamen.holdMessage(message)
605- >>> logout()
606-
607- >>> admin_browser.reload()
608- >>> print extract_text(find_tag_by_id(admin_browser.contents, 'legend'))
609- 3 messages have been posted to your mailing list...
610- ...
611-
612-Foo decides that Carlos's first message is spam and should just be
613-discarded.
614-
615- >>> admin_browser.getControl(name='field.%3Caardvark%3E').value = [
616- ... 'discard']
617- >>> admin_browser.getControl('Moderate').click()
618-
619- >>> login('foo.bar@canonical.com')
620- >>> from lp.registry.interfaces.mailinglist import IMessageApprovalSet
621- >>> message_approval_set = getUtility(IMessageApprovalSet)
622- >>> message_approval = message_approval_set.getMessageByMessageID(
623- ... '<aardvark>')
624- >>> message_approval.status
625- <DBItem PostedMessageStatus.DISCARD_PENDING, (60) Discard pending>
626- >>> logout()
627-
628- >>> admin_browser.getLink('Moderate mailing list').click()
629- >>> print extract_text(find_tag_by_id(admin_browser.contents, 'legend'))
630- 2 messages have been posted to your mailing list...
631- ...
632-
633-Foo decides that Carlos's second message isn't spam, but it isn't
634-on-topic, so he decides to decline this message.
635-
636- >>> admin_browser.getControl(name='field.%3Cbobcat%3E').value = [
637- ... 'reject']
638- >>> admin_browser.getControl('Moderate').click()
639-
640- >>> login('foo.bar@canonical.com')
641- >>> from lp.registry.interfaces.mailinglist import IMessageApprovalSet
642- >>> message_approval_set = getUtility(IMessageApprovalSet)
643- >>> message_approval = message_approval_set.getMessageByMessageID(
644- ... '<bobcat>')
645- >>> message_approval.status
646- <DBItem PostedMessageStatus.REJECTION_PENDING, (30) Decline pending>
647- >>> logout()
648-
649- >>> admin_browser.getLink('Moderate mailing list').click()
650- >>> print extract_text(find_tag_by_id(admin_browser.contents, 'legend'))
651- 1 message has been posted to your mailing list...
652- ...
653-
654-Foo decides that Carlos's third message is just right, and he decides to
655-accept the message.
656-
657- >>> admin_browser.getControl(name='field.%3Ccaribou%3E').value = [
658- ... 'approve']
659- >>> admin_browser.getControl('Moderate').click()
660-
661- >>> login('foo.bar@canonical.com')
662- >>> from lp.registry.interfaces.mailinglist import IMessageApprovalSet
663- >>> message_approval_set = getUtility(IMessageApprovalSet)
664- >>> message_approval = message_approval_set.getMessageByMessageID(
665- ... '<caribou>')
666- >>> message_approval.status
667- <DBItem PostedMessageStatus.APPROVAL_PENDING, (20) Approval pending>
668- >>> logout()
669-
670- >>> admin_browser.getLink('Moderate mailing list').click()
671- >>> print extract_text(find_tag_by_id(admin_browser.contents, 'legend'))
672- There are no mailing list messages requiring your review.
673
674=== modified file 'lib/lp/registry/tests/test_mailinglist.py'
675--- lib/lp/registry/tests/test_mailinglist.py 2012-09-28 06:15:58 +0000
676+++ lib/lp/registry/tests/test_mailinglist.py 2012-10-11 01:57:24 +0000
677@@ -8,6 +8,7 @@
678
679 import transaction
680 from zope.component import getUtility
681+from testtools.matchers import Equals
682
683 from lp.registry.interfaces.mailinglist import (
684 CannotChangeSubscription,
685@@ -34,6 +35,7 @@
686 from lp.testing import (
687 login_celebrity,
688 person_logged_in,
689+ StormStatementRecorder,
690 TestCaseWithFactory,
691 )
692 from lp.testing.layers import (
693@@ -41,6 +43,7 @@
694 LaunchpadFunctionalLayer,
695 )
696 from lp.testing.mail_helpers import pop_notifications
697+from lp.testing.matchers import HasQueryCount
698
699
700 class PersonMailingListTestCase(TestCaseWithFactory):
701@@ -745,6 +748,20 @@
702 self.assertEqual(1, held_messages.count())
703 self.assertEqual(held_message.message_id, held_messages[0].message_id)
704
705+ def test_getReviewableMessages_queries(self):
706+ # The Message and user that posted it are retrieved with the query
707+ # that get the MessageApproval.
708+ test_objects = self.makeMailingListAndHeldMessage()
709+ team, member, sender, held_message = test_objects
710+ held_messages = team.mailing_list.getReviewableMessages()
711+ with StormStatementRecorder() as recorder:
712+ held_message = held_messages[0]
713+ self.assertThat(recorder, HasQueryCount(Equals(1)))
714+ with StormStatementRecorder() as recorder:
715+ held_message.message
716+ held_message.posted_by
717+ self.assertThat(recorder, HasQueryCount(Equals(0)))
718+
719
720 class MessageApprovalTestCase(MailingListMessageTestCase):
721 """Test the MessageApproval data behaviour."""
722@@ -876,17 +893,6 @@
723 self.assertEqual(held_message.message.datecreated, details.date)
724 self.assertEqual(held_message.message.owner, details.author)
725
726- def test_email_message(self):
727- held_message = self.makeMailingListAndHeldMessage()[-1]
728- details = IHeldMessageDetails(held_message)
729- self.assertEqual('A question', details.email_message['subject'])
730-
731- def test_sender(self):
732- test_objects = self.makeMailingListAndHeldMessage()
733- team, member, sender, held_message = test_objects
734- details = IHeldMessageDetails(held_message)
735- self.assertEqual(sender.preferredemail.email, details.sender)
736-
737 def test_body(self):
738 held_message = self.makeMailingListAndHeldMessage()[-1]
739 details = IHeldMessageDetails(held_message)