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
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/browser/team.py 2010-08-22 19:16:42 +0000
@@ -150,7 +150,7 @@
150 "name", "visibility", "displayname", "contactemail",150 "name", "visibility", "displayname", "contactemail",
151 "teamdescription", "subscriptionpolicy",151 "teamdescription", "subscriptionpolicy",
152 "defaultmembershipperiod", "renewal_policy",152 "defaultmembershipperiod", "renewal_policy",
153 "defaultrenewalperiod", "teamowner",153 "defaultrenewalperiod", "teamowner",
154 ]154 ]
155 private_prefix = PRIVATE_TEAM_PREFIX155 private_prefix = PRIVATE_TEAM_PREFIX
156156
@@ -767,7 +767,7 @@
767767
768 def renderTable(self):768 def renderTable(self):
769 html = ['<table style="max-width: 80em">']769 html = ['<table style="max-width: 80em">']
770 items = self.subscribers.currentBatch()770 items = list(self.subscribers.currentBatch())
771 assert len(items) > 0, (771 assert len(items) > 0, (
772 "Don't call this method if there are no subscribers to show.")772 "Don't call this method if there are no subscribers to show.")
773 # When there are more than 10 items, we use multiple columns, but773 # When there are more than 10 items, we use multiple columns, but
774774
=== modified file 'lib/lp/registry/model/mailinglist.py'
--- lib/lp/registry/model/mailinglist.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/mailinglist.py 2010-08-22 19:16:42 +0000
@@ -389,7 +389,7 @@
389 TeamParticipation.team == self.team,389 TeamParticipation.team == self.team,
390 MailingListSubscription.person == Person.id,390 MailingListSubscription.person == Person.id,
391 MailingListSubscription.mailing_list == self)391 MailingListSubscription.mailing_list == self)
392 return results.order_by(Person.displayname)392 return results.order_by(Person.displayname, Person.name)
393393
394 def subscribe(self, person, address=None):394 def subscribe(self, person, address=None):
395 """See `IMailingList`."""395 """See `IMailingList`."""
@@ -451,8 +451,9 @@
451 MailingListSubscription.personID451 MailingListSubscription.personID
452 == EmailAddress.personID),452 == EmailAddress.personID),
453 # pylint: disable-msg=C0301453 # pylint: disable-msg=C0301
454 LeftJoin(MailingList,454 LeftJoin(
455 MailingList.id == MailingListSubscription.mailing_listID),455 MailingList,
456 MailingList.id == MailingListSubscription.mailing_listID),
456 LeftJoin(TeamParticipation,457 LeftJoin(TeamParticipation,
457 TeamParticipation.personID458 TeamParticipation.personID
458 == MailingListSubscription.personID),459 == MailingListSubscription.personID),
@@ -472,8 +473,9 @@
472 MailingListSubscription.email_addressID473 MailingListSubscription.email_addressID
473 == EmailAddress.id),474 == EmailAddress.id),
474 # pylint: disable-msg=C0301475 # pylint: disable-msg=C0301
475 LeftJoin(MailingList,476 LeftJoin(
476 MailingList.id == MailingListSubscription.mailing_listID),477 MailingList,
478 MailingList.id == MailingListSubscription.mailing_listID),
477 LeftJoin(TeamParticipation,479 LeftJoin(TeamParticipation,
478 TeamParticipation.personID480 TeamParticipation.personID
479 == MailingListSubscription.personID),481 == MailingListSubscription.personID),
@@ -664,8 +666,9 @@
664 MailingListSubscription.personID666 MailingListSubscription.personID
665 == EmailAddress.personID),667 == EmailAddress.personID),
666 # pylint: disable-msg=C0301668 # pylint: disable-msg=C0301
667 LeftJoin(MailingList,669 LeftJoin(
668 MailingList.id == MailingListSubscription.mailing_listID),670 MailingList,
671 MailingList.id == MailingListSubscription.mailing_listID),
669 LeftJoin(TeamParticipation,672 LeftJoin(TeamParticipation,
670 TeamParticipation.personID673 TeamParticipation.personID
671 == MailingListSubscription.personID),674 == MailingListSubscription.personID),
@@ -678,8 +681,7 @@
678 team.id for team in store.find(681 team.id for team in store.find(
679 Person,682 Person,
680 And(Person.name.is_in(team_names),683 And(Person.name.is_in(team_names),
681 Person.teamowner != None))684 Person.teamowner != None)))
682 )
683 list_ids = set(685 list_ids = set(
684 mailing_list.id for mailing_list in store.find(686 mailing_list.id for mailing_list in store.find(
685 MailingList,687 MailingList,
@@ -709,8 +711,9 @@
709 MailingListSubscription.email_addressID711 MailingListSubscription.email_addressID
710 == EmailAddress.id),712 == EmailAddress.id),
711 # pylint: disable-msg=C0301713 # pylint: disable-msg=C0301
712 LeftJoin(MailingList,714 LeftJoin(
713 MailingList.id == MailingListSubscription.mailing_listID),715 MailingList,
716 MailingList.id == MailingListSubscription.mailing_listID),
714 LeftJoin(TeamParticipation,717 LeftJoin(TeamParticipation,
715 TeamParticipation.personID718 TeamParticipation.personID
716 == MailingListSubscription.personID),719 == MailingListSubscription.personID),
@@ -756,8 +759,7 @@
756 team.id for team in store.find(759 team.id for team in store.find(
757 Person,760 Person,
758 And(Person.name.is_in(team_names),761 And(Person.name.is_in(team_names),
759 Person.teamowner != None))762 Person.teamowner != None)))
760 )
761 team_members = store.using(*tables).find(763 team_members = store.using(*tables).find(
762 (Team.name, Person.displayname, EmailAddress.email),764 (Team.name, Person.displayname, EmailAddress.email),
763 And(TeamParticipation.teamID.is_in(team_ids),765 And(TeamParticipation.teamID.is_in(team_ids),
764766
=== modified file 'lib/lp/registry/tests/test_mailinglist.py'
--- lib/lp/registry/tests/test_mailinglist.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/tests/test_mailinglist.py 2010-08-22 19:16:42 +0000
@@ -1,64 +1,64 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4from __future__ import with_statement
5
4__metaclass__ = type6__metaclass__ = type
5__all__ = []7__all__ = []
68
79from canonical.testing import DatabaseFunctionalLayer
8import unittest
9
10from canonical.launchpad.ftests import login
11from canonical.testing import LaunchpadFunctionalLayer
12from lp.registry.interfaces.mailinglistsubscription import (10from lp.registry.interfaces.mailinglistsubscription import (
13 MailingListAutoSubscribePolicy,11 MailingListAutoSubscribePolicy,
14 )12 )
15from lp.registry.interfaces.person import TeamSubscriptionPolicy13from lp.registry.interfaces.person import TeamSubscriptionPolicy
16from lp.testing import TestCaseWithFactory14from lp.testing import login_celebrity, person_logged_in, TestCaseWithFactory
1715
1816
19class MailingList_getSubscribers_TestCase(TestCaseWithFactory):17class MailingList_getSubscribers_TestCase(TestCaseWithFactory):
20 """Tests for `IMailingList`.getSubscribers()."""18 """Tests for `IMailingList`.getSubscribers()."""
2119
22 layer = LaunchpadFunctionalLayer20 layer = DatabaseFunctionalLayer
2321
24 def setUp(self):22 def setUp(self):
25 # Create a team (tied to a mailing list) with one former member, one
26 # pending member and one active member.
27 TestCaseWithFactory.setUp(self)23 TestCaseWithFactory.setUp(self)
28 login('foo.bar@canonical.com')24 self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
25 'test-mailinglist', 'team-owner')
26
27 def test_only_active_members_can_be_subscribers(self):
29 former_member = self.factory.makePerson()28 former_member = self.factory.makePerson()
30 pending_member = self.factory.makePerson()29 pending_member = self.factory.makePerson()
31 active_member = self.active_member = self.factory.makePerson()30 active_member = self.active_member = self.factory.makePerson()
32 self.team, self.mailing_list = self.factory.makeTeamAndMailingList(
33 'test-mailinglist', 'team-owner')
34 self.team.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
35
36 # Each of our members want to be subscribed to a team's mailing list31 # Each of our members want to be subscribed to a team's mailing list
37 # whenever they join the team.32 # whenever they join the team.
33 login_celebrity('admin')
38 former_member.mailing_list_auto_subscribe_policy = (34 former_member.mailing_list_auto_subscribe_policy = (
39 MailingListAutoSubscribePolicy.ALWAYS)35 MailingListAutoSubscribePolicy.ALWAYS)
40 active_member.mailing_list_auto_subscribe_policy = (36 active_member.mailing_list_auto_subscribe_policy = (
41 MailingListAutoSubscribePolicy.ALWAYS)37 MailingListAutoSubscribePolicy.ALWAYS)
42 pending_member.mailing_list_auto_subscribe_policy = (38 pending_member.mailing_list_auto_subscribe_policy = (
43 MailingListAutoSubscribePolicy.ALWAYS)39 MailingListAutoSubscribePolicy.ALWAYS)
4440 self.team.subscriptionpolicy = TeamSubscriptionPolicy.MODERATED
45 pending_member.join(self.team)41 pending_member.join(self.team)
46 self.assertEqual(False, pending_member.inTeam(self.team))
47
48 self.team.addMember(former_member, reviewer=self.team.teamowner)42 self.team.addMember(former_member, reviewer=self.team.teamowner)
49 former_member.leave(self.team)43 former_member.leave(self.team)
50 self.assertEqual(False, former_member.inTeam(self.team))
51
52 self.team.addMember(active_member, reviewer=self.team.teamowner)44 self.team.addMember(active_member, reviewer=self.team.teamowner)
53 self.assertEqual(True, active_member.inTeam(self.team))
54
55 def test_only_active_members_can_be_subscribers(self):
56 # Even though our 3 members want to subscribe to the team's mailing45 # Even though our 3 members want to subscribe to the team's mailing
57 # list, only the active member is considered a subscriber.46 # list, only the active member is considered a subscriber.
58 subscribers = [self.active_member]47 self.assertEqual(
59 self.assertEqual(48 [active_member], list(self.mailing_list.getSubscribers()))
60 subscribers, list(self.mailing_list.getSubscribers()))49
6150 def test_getSubscribers_order(self):
6251 person_1 = self.factory.makePerson(name="pb1", displayname="Me")
63def test_suite():52 with person_logged_in(person_1):
64 return unittest.TestLoader().loadTestsFromName(__name__)53 person_1.mailing_list_auto_subscribe_policy = (
54 MailingListAutoSubscribePolicy.ALWAYS)
55 person_1.join(self.team)
56 person_2 = self.factory.makePerson(name="pa2", displayname="Me")
57 with person_logged_in(person_2):
58 person_2.mailing_list_auto_subscribe_policy = (
59 MailingListAutoSubscribePolicy.ALWAYS)
60 person_2.join(self.team)
61 subscribers = self.mailing_list.getSubscribers()
62 self.assertEqual(2, subscribers.count())
63 self.assertEqual(
64 ['pa2', 'pb1'], [person.name for person in subscribers])