Merge lp:~sinzui/launchpad/mailing-list-subscribers-0 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 11424
Proposed branch: lp:~sinzui/launchpad/mailing-list-subscribers-0
Merge into: lp:launchpad
Diff against target: 200 lines (+46/-44)
3 files modified
lib/lp/registry/browser/team.py (+2/-2)
lib/lp/registry/model/mailinglist.py (+15/-13)
lib/lp/registry/tests/test_mailinglist.py (+29/-29)
To merge this branch: bzr merge lp:~sinzui/launchpad/mailing-list-subscribers-0
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+33279@code.launchpad.net

Description of the change

This is my branch to walk the list of mailing list subscribers properly.

    lp:~sinzui/launchpad/mailing-list-subscribers-0
    Diff size: 201 (lint demanded a lot of fixes)
    Launchpad bug:
          https://bugs.launchpad.net/bugs/568114
    Test command: ./bin/test -vv -t test_mailinglist
    Pre-implementation: no one
    Target release: 10.09

Walk the list of mailing list subscribers properly
-------------------------------------------------

The table layout is abusing the batch navigator, asking for items by index,
causing a query for each user using the getSubscribers() query as the base,
adding an offset and a limit. Since the getSubscribers intends to return a all
users, sorted by display, asking for OFFSET 1, LIMIT 1 and OFFSET 2, LIMIT 2
are working with a pair of results, not one, and the table builder, expecting
only one, always uses the first one!

Adding the proposed Person.id fixes the issue. Using Person.name is clearer.
But the queries on the page remain outrageous. The table could create a list
of the users in the batch before it starts getting items by index so that a
single query for the user is needed.

Rules
-----

    * Add displayname to the getSubscribers() order by clause to ensure
      only 1 person can match repeated calls to OFFSET and Limit.
    * Listify the current batch before the table starts using indexes to
      get items so that one query is called to get all the subscribers
      for the page.

QA
--

    * Visit https://launchpad.net/~kicad-developers/+mailing-list-subscribers
    * Verify that Alain Portals is listed twice and each links to a different
      user profile.
    * View the source of the page and verify less than 129 queries were
      issues (~79 queries)

Lint
----

Linting changed files:
  lib/lp/registry/browser/team.py
  lib/lp/registry/model/mailinglist.py
  lib/lp/registry/tests/test_mailinglist.py

Test
----

    * lib/lp/registry/tests/test_mailinglist.py
      * Updated an older test for getSubscribers to use current rules
        for unittests.
      * Refactored the test so that another test could share the setup.
      * Added a new test to verify that items are sorted by displayname
        and name.

Implementation
--------------

    * lib/lp/registry/browser/team.py
      * Listified the batch of persons. This is an internal optimisation
        that had not test changes.
    * lib/lp/registry/model/mailinglist.py
      * Added Person.name to the order by clause to ensure a query with
        LIMIT 1 OFFSET X will return a distinct row.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) :
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/team.py'
2--- lib/lp/registry/browser/team.py 2010-08-20 20:31:18 +0000
3+++ lib/lp/registry/browser/team.py 2010-08-22 19:16:42 +0000
4@@ -150,7 +150,7 @@
5 "name", "visibility", "displayname", "contactemail",
6 "teamdescription", "subscriptionpolicy",
7 "defaultmembershipperiod", "renewal_policy",
8- "defaultrenewalperiod", "teamowner",
9+ "defaultrenewalperiod", "teamowner",
10 ]
11 private_prefix = PRIVATE_TEAM_PREFIX
12
13@@ -767,7 +767,7 @@
14
15 def renderTable(self):
16 html = ['<table style="max-width: 80em">']
17- items = self.subscribers.currentBatch()
18+ items = list(self.subscribers.currentBatch())
19 assert len(items) > 0, (
20 "Don't call this method if there are no subscribers to show.")
21 # When there are more than 10 items, we use multiple columns, but
22
23=== modified file 'lib/lp/registry/model/mailinglist.py'
24--- lib/lp/registry/model/mailinglist.py 2010-08-20 20:31:18 +0000
25+++ lib/lp/registry/model/mailinglist.py 2010-08-22 19:16:42 +0000
26@@ -389,7 +389,7 @@
27 TeamParticipation.team == self.team,
28 MailingListSubscription.person == Person.id,
29 MailingListSubscription.mailing_list == self)
30- return results.order_by(Person.displayname)
31+ return results.order_by(Person.displayname, Person.name)
32
33 def subscribe(self, person, address=None):
34 """See `IMailingList`."""
35@@ -451,8 +451,9 @@
36 MailingListSubscription.personID
37 == EmailAddress.personID),
38 # pylint: disable-msg=C0301
39- LeftJoin(MailingList,
40- MailingList.id == MailingListSubscription.mailing_listID),
41+ LeftJoin(
42+ MailingList,
43+ MailingList.id == MailingListSubscription.mailing_listID),
44 LeftJoin(TeamParticipation,
45 TeamParticipation.personID
46 == MailingListSubscription.personID),
47@@ -472,8 +473,9 @@
48 MailingListSubscription.email_addressID
49 == EmailAddress.id),
50 # pylint: disable-msg=C0301
51- LeftJoin(MailingList,
52- MailingList.id == MailingListSubscription.mailing_listID),
53+ LeftJoin(
54+ MailingList,
55+ MailingList.id == MailingListSubscription.mailing_listID),
56 LeftJoin(TeamParticipation,
57 TeamParticipation.personID
58 == MailingListSubscription.personID),
59@@ -664,8 +666,9 @@
60 MailingListSubscription.personID
61 == EmailAddress.personID),
62 # pylint: disable-msg=C0301
63- LeftJoin(MailingList,
64- MailingList.id == MailingListSubscription.mailing_listID),
65+ LeftJoin(
66+ MailingList,
67+ MailingList.id == MailingListSubscription.mailing_listID),
68 LeftJoin(TeamParticipation,
69 TeamParticipation.personID
70 == MailingListSubscription.personID),
71@@ -678,8 +681,7 @@
72 team.id for team in store.find(
73 Person,
74 And(Person.name.is_in(team_names),
75- Person.teamowner != None))
76- )
77+ Person.teamowner != None)))
78 list_ids = set(
79 mailing_list.id for mailing_list in store.find(
80 MailingList,
81@@ -709,8 +711,9 @@
82 MailingListSubscription.email_addressID
83 == EmailAddress.id),
84 # pylint: disable-msg=C0301
85- LeftJoin(MailingList,
86- MailingList.id == MailingListSubscription.mailing_listID),
87+ LeftJoin(
88+ MailingList,
89+ MailingList.id == MailingListSubscription.mailing_listID),
90 LeftJoin(TeamParticipation,
91 TeamParticipation.personID
92 == MailingListSubscription.personID),
93@@ -756,8 +759,7 @@
94 team.id for team in store.find(
95 Person,
96 And(Person.name.is_in(team_names),
97- Person.teamowner != None))
98- )
99+ Person.teamowner != None)))
100 team_members = store.using(*tables).find(
101 (Team.name, Person.displayname, EmailAddress.email),
102 And(TeamParticipation.teamID.is_in(team_ids),
103
104=== modified file 'lib/lp/registry/tests/test_mailinglist.py'
105--- lib/lp/registry/tests/test_mailinglist.py 2010-08-20 20:31:18 +0000
106+++ lib/lp/registry/tests/test_mailinglist.py 2010-08-22 19:16:42 +0000
107@@ -1,64 +1,64 @@
108 # Copyright 2009 Canonical Ltd. This software is licensed under the
109 # GNU Affero General Public License version 3 (see the file LICENSE).
110
111+from __future__ import with_statement
112+
113 __metaclass__ = type
114 __all__ = []
115
116-
117-import unittest
118-
119-from canonical.launchpad.ftests import login
120-from canonical.testing import LaunchpadFunctionalLayer
121+from canonical.testing import DatabaseFunctionalLayer
122 from lp.registry.interfaces.mailinglistsubscription import (
123 MailingListAutoSubscribePolicy,
124 )
125 from lp.registry.interfaces.person import TeamSubscriptionPolicy
126-from lp.testing import TestCaseWithFactory
127+from lp.testing import login_celebrity, person_logged_in, TestCaseWithFactory
128
129
130 class MailingList_getSubscribers_TestCase(TestCaseWithFactory):
131 """Tests for `IMailingList`.getSubscribers()."""
132
133- layer = LaunchpadFunctionalLayer
134+ layer = DatabaseFunctionalLayer
135
136 def setUp(self):
137- # Create a team (tied to a mailing list) with one former member, one
138- # pending member and one active member.
139 TestCaseWithFactory.setUp(self)
140- login('foo.bar@canonical.com')
141+ self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
142+ 'test-mailinglist', 'team-owner')
143+
144+ def test_only_active_members_can_be_subscribers(self):
145 former_member = self.factory.makePerson()
146 pending_member = self.factory.makePerson()
147 active_member = self.active_member = self.factory.makePerson()
148- self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
149- 'test-mailinglist', 'team-owner')
150- self.team.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
151-
152 # Each of our members want to be subscribed to a team's mailing list
153 # whenever they join the team.
154+ login_celebrity('admin')
155 former_member.mailing_list_auto_subscribe_policy = (
156 MailingListAutoSubscribePolicy.ALWAYS)
157 active_member.mailing_list_auto_subscribe_policy = (
158 MailingListAutoSubscribePolicy.ALWAYS)
159 pending_member.mailing_list_auto_subscribe_policy = (
160 MailingListAutoSubscribePolicy.ALWAYS)
161-
162+ self.team.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
163 pending_member.join(self.team)
164- self.assertEqual(False, pending_member.inTeam(self.team))
165-
166 self.team.addMember(former_member, reviewer=self.team.teamowner)
167 former_member.leave(self.team)
168- self.assertEqual(False, former_member.inTeam(self.team))
169-
170 self.team.addMember(active_member, reviewer=self.team.teamowner)
171- self.assertEqual(True, active_member.inTeam(self.team))
172-
173- def test_only_active_members_can_be_subscribers(self):
174 # Even though our 3 members want to subscribe to the team's mailing
175 # list, only the active member is considered a subscriber.
176- subscribers = [self.active_member]
177- self.assertEqual(
178- subscribers, list(self.mailing_list.getSubscribers()))
179-
180-
181-def test_suite():
182- return unittest.TestLoader().loadTestsFromName(__name__)
183+ self.assertEqual(
184+ [active_member], list(self.mailing_list.getSubscribers()))
185+
186+ def test_getSubscribers_order(self):
187+ person_1 = self.factory.makePerson(name="pb1", displayname="Me")
188+ with person_logged_in(person_1):
189+ person_1.mailing_list_auto_subscribe_policy = (
190+ MailingListAutoSubscribePolicy.ALWAYS)
191+ person_1.join(self.team)
192+ person_2 = self.factory.makePerson(name="pa2", displayname="Me")
193+ with person_logged_in(person_2):
194+ person_2.mailing_list_auto_subscribe_policy = (
195+ MailingListAutoSubscribePolicy.ALWAYS)
196+ person_2.join(self.team)
197+ subscribers = self.mailing_list.getSubscribers()
198+ self.assertEqual(2, subscribers.count())
199+ self.assertEqual(
200+ ['pa2', 'pb1'], [person.name for person in subscribers])