Merge lp:~cjwatson/launchpad/participation-query-count into lp:launchpad

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: no longer in the source branch.
Merged at revision: 17389
Proposed branch: lp:~cjwatson/launchpad/participation-query-count
Merge into: lp:launchpad
Diff against target: 288 lines (+90/-41)
7 files modified
lib/lp/registry/browser/person.py (+26/-31)
lib/lp/registry/browser/tests/test_person.py (+22/-0)
lib/lp/registry/configure.zcml (+1/-0)
lib/lp/registry/interfaces/mailinglist.py (+12/-1)
lib/lp/registry/interfaces/teammembership.py (+2/-1)
lib/lp/registry/model/mailinglist.py (+27/-5)
lib/lp/registry/tests/mailinglists_helper.py (+0/-3)
To merge this branch: bzr merge lp:~cjwatson/launchpad/participation-query-count
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+252267@code.launchpad.net

Commit message

Push the mailing list subscription checks on Person:+participation down to a bulk method on IMailingListSet.

Description of the change

Push the mailing list subscription checks on Person:+participation down to a bulk method on IMailingListSet. Also expose ITeamMembership.personID, allowing us to save an entirely unnecessary Person query for each direct team membership.

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2015-03-05 11:39:06 +0000
+++ lib/lp/registry/browser/person.py 2015-03-09 15:05:42 +0000
@@ -284,9 +284,6 @@
284from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease284from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
285285
286286
287COMMASPACE = ', '
288
289
290class PersonBreadcrumb(DisplaynameBreadcrumb):287class PersonBreadcrumb(DisplaynameBreadcrumb):
291 implements(IHeadingBreadcrumb, IMultiFacetedBreadcrumb)288 implements(IHeadingBreadcrumb, IMultiFacetedBreadcrumb)
292289
@@ -2028,23 +2025,24 @@
2028 def label(self):2025 def label(self):
2029 return 'Team participation for ' + self.context.displayname2026 return 'Team participation for ' + self.context.displayname
20302027
2031 def _asParticipation(self, membership=None, team=None, via=None):2028 def _asParticipation(self, team=None, membership=None, via=None,
2029 mailing_list=None, subscription=None):
2032 """Return a dict of participation information for the membership.2030 """Return a dict of participation information for the membership.
20332031
2034 Method requires membership or team, not both.2032 Method requires membership or via, not both.
2035 :param via: The team through which the membership in the indirect2033 :param via: The team through which the membership in the indirect
2036 team is established.2034 team is established.
2037 """2035 """
2038 if ((membership is None and team is None) or2036 if ((membership is None and via is None) or
2039 (membership is not None and team is not None)):2037 (membership is not None and via is not None)):
2040 raise AssertionError(2038 raise AssertionError(
2041 "The membership or team argument must be provided, not both.")2039 "The membership or via argument must be provided, not both.")
20422040
2043 if via is not None:2041 if via is not None:
2044 # When showing the path, it's unnecessary to show the team in2042 # When showing the path, it's unnecessary to show the team in
2045 # question at the beginning of the path, or the user at the2043 # question at the beginning of the path, or the user at the
2046 # end of the path.2044 # end of the path.
2047 via = COMMASPACE.join(2045 via = ", ".join(
2048 [via_team.displayname for via_team in via[1:-1]])2046 [via_team.displayname for via_team in via[1:-1]])
20492047
2050 if membership is None:2048 if membership is None:
@@ -2055,17 +2053,15 @@
2055 datejoined = None2053 datejoined = None
2056 else:2054 else:
2057 # The member is a direct member; use the membership data.2055 # The member is a direct member; use the membership data.
2058 team = membership.team
2059 datejoined = membership.datejoined2056 datejoined = membership.datejoined
2060 if membership.person == team.teamowner:2057 if membership.personID == team.teamownerID:
2061 role = 'Owner'2058 role = 'Owner'
2062 elif membership.status == TeamMembershipStatus.ADMIN:2059 elif membership.status == TeamMembershipStatus.ADMIN:
2063 role = 'Admin'2060 role = 'Admin'
2064 else:2061 else:
2065 role = 'Member'2062 role = 'Member'
20662063
2067 if team.mailing_list is not None and team.mailing_list.is_usable:2064 if mailing_list is not None:
2068 subscription = team.mailing_list.getSubscription(self.context)
2069 if subscription is None:2065 if subscription is None:
2070 subscribed = 'Not subscribed'2066 subscribed = 'Not subscribed'
2071 else:2067 else:
@@ -2082,27 +2078,26 @@
2082 """Return the participation information for active memberships."""2078 """Return the participation information for active memberships."""
2083 paths, memberships = self.context.getPathsToTeams()2079 paths, memberships = self.context.getPathsToTeams()
2084 direct_teams = [membership.team for membership in memberships]2080 direct_teams = [membership.team for membership in memberships]
2085 indirect_teams = [2081 items = []
2086 team for team in paths.keys()2082 for membership in memberships:
2087 if team not in direct_teams]2083 items.append(dict(team=membership.team, membership=membership))
2084 for team, via in paths.items():
2085 if team not in direct_teams:
2086 items.append(dict(team=team, via=via))
2087 items = [
2088 item for item in items
2089 if check_permission('launchpad.View', item["team"])]
2088 participations = []2090 participations = []
20892091
2090 # First, create a participation for all direct memberships.2092 # Bulk-load mailing list subscriptions.
2091 for membership in memberships:2093 subscriptions = getUtility(IMailingListSet).getSubscriptionsForTeams(
2092 # Add a participation record for the membership if allowed.2094 self.context, [item["team"] for item in items])
2093 if check_permission('launchpad.View', membership.team):
2094 participations.append(
2095 self._asParticipation(membership=membership))
20962095
2097 # Second, create a participation for all indirect memberships,2096 # Create all the participations.
2098 # using the remaining paths.2097 for item in items:
2099 for indirect_team in indirect_teams:2098 item["mailing_list"], item["subscription"] = subscriptions.get(
2100 if not check_permission('launchpad.View', indirect_team):2099 item["team"].id, (None, None))
2101 continue2100 participations.append(self._asParticipation(**item))
2102 participations.append(
2103 self._asParticipation(
2104 via=paths[indirect_team],
2105 team=indirect_team))
2106 return sorted(participations, key=itemgetter('displayname'))2101 return sorted(participations, key=itemgetter('displayname'))
21072102
2108 @cachedproperty2103 @cachedproperty
21092104
=== modified file 'lib/lp/registry/browser/tests/test_person.py'
--- lib/lp/registry/browser/tests/test_person.py 2015-03-05 15:28:11 +0000
+++ lib/lp/registry/browser/tests/test_person.py 2015-03-09 15:05:42 +0000
@@ -47,6 +47,7 @@
47from lp.services.identity.interfaces.emailaddress import IEmailAddressSet47from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
48from lp.services.log.logger import FakeLogger48from lp.services.log.logger import FakeLogger
49from lp.services.mail import stub49from lp.services.mail import stub
50from lp.services.propertycache import clear_property_cache
50from lp.services.verification.interfaces.authtoken import LoginTokenType51from lp.services.verification.interfaces.authtoken import LoginTokenType
51from lp.services.verification.interfaces.logintoken import ILoginTokenSet52from lp.services.verification.interfaces.logintoken import ILoginTokenSet
52from lp.services.verification.tests.logintoken import get_token_url_from_email53from lp.services.verification.tests.logintoken import get_token_url_from_email
@@ -68,6 +69,7 @@
68 login_person,69 login_person,
69 monkey_patch,70 monkey_patch,
70 person_logged_in,71 person_logged_in,
72 record_two_runs,
71 StormStatementRecorder,73 StormStatementRecorder,
72 TestCaseWithFactory,74 TestCaseWithFactory,
73 )75 )
@@ -878,6 +880,26 @@
878 self.assertEqual(1, len(participations))880 self.assertEqual(1, len(participations))
879 self.assertEqual(True, self.view.has_participations)881 self.assertEqual(True, self.view.has_participations)
880882
883 def test_mailing_list_subscriptions_query_count(self):
884 # Additional mailing list subscriptions do not add additional queries.
885 def create_subscriptions():
886 direct_team = self.factory.makeTeam(members=[self.user])
887 direct_list = self.factory.makeMailingList(
888 direct_team, direct_team.teamowner)
889 direct_list.subscribe(self.user)
890 indirect_team = self.factory.makeTeam(members=[direct_team])
891 indirect_list = self.factory.makeMailingList(
892 indirect_team, indirect_team.teamowner)
893 indirect_list.subscribe(self.user)
894
895 def get_participations():
896 clear_property_cache(self.view)
897 return list(self.view.active_participations)
898
899 recorder1, recorder2 = record_two_runs(
900 get_participations, create_subscriptions, 5)
901 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
902
881903
882class TestPersonRelatedPackagesView(TestCaseWithFactory):904class TestPersonRelatedPackagesView(TestCaseWithFactory):
883 """Test the related software view."""905 """Test the related software view."""
884906
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2015-02-26 11:34:47 +0000
+++ lib/lp/registry/configure.zcml 2015-03-09 15:05:42 +0000
@@ -37,6 +37,7 @@
37 id37 id
38 team38 team
39 person39 person
40 personID
40 status41 status
41 proposed_by42 proposed_by
42 acknowledged_by43 acknowledged_by
4344
=== modified file 'lib/lp/registry/interfaces/mailinglist.py'
--- lib/lp/registry/interfaces/mailinglist.py 2013-01-07 02:40:55 +0000
+++ lib/lp/registry/interfaces/mailinglist.py 2015-03-09 15:05:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2011 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 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
4"""Mailing list interfaces."""4"""Mailing list interfaces."""
@@ -464,6 +464,17 @@
464 :raises AssertionError: When `team_name` is not a string.464 :raises AssertionError: When `team_name` is not a string.
465 """465 """
466466
467 def getSubscriptionsForTeams(person, teams):
468 """Return a person's subscriptions to usable lists for a set of teams.
469
470 :param person: An `IPerson`.
471 :param teams: A list of `ITeam`s.
472 :return: A dictionary mapping team IDs to tuples of `IMailingList`
473 IDs and `IMailingListSubscription` IDs; the second element will
474 be None if a team has a list to which the person is not
475 subscribed.
476 """
477
467 def getSubscribedAddresses(team_names):478 def getSubscribedAddresses(team_names):
468 """Return the set of subscribed email addresses for members.479 """Return the set of subscribed email addresses for members.
469480
470481
=== modified file 'lib/lp/registry/interfaces/teammembership.py'
--- lib/lp/registry/interfaces/teammembership.py 2013-01-07 02:40:55 +0000
+++ lib/lp/registry/interfaces/teammembership.py 2015-03-09 15:05:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 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
4"""Team membership interfaces."""4"""Team membership interfaces."""
@@ -131,6 +131,7 @@
131 Reference(title=_("Member"), required=True, readonly=True,131 Reference(title=_("Member"), required=True, readonly=True,
132 schema=Interface), # Specified in interfaces/person.py.132 schema=Interface), # Specified in interfaces/person.py.
133 exported_as='member')133 exported_as='member')
134 personID = Int(title=_("Person ID"), required=True, readonly=True)
134 proposed_by = Attribute(_('Proponent'))135 proposed_by = Attribute(_('Proponent'))
135 reviewed_by = Attribute(136 reviewed_by = Attribute(
136 _("The team admin who approved/rejected the member."))137 _("The team admin who approved/rejected the member."))
137138
=== modified file 'lib/lp/registry/model/mailinglist.py'
--- lib/lp/registry/model/mailinglist.py 2013-06-20 05:50:00 +0000
+++ lib/lp/registry/model/mailinglist.py 2015-03-09 15:05:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 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
4__metaclass__ = type4__metaclass__ = type
@@ -100,6 +100,13 @@
100 PostedMessageStatus.APPROVAL_PENDING)100 PostedMessageStatus.APPROVAL_PENDING)
101101
102102
103USABLE_STATUSES = (
104 MailingListStatus.ACTIVE,
105 MailingListStatus.MODIFIED,
106 MailingListStatus.UPDATING,
107 MailingListStatus.MOD_FAILED)
108
109
103class MessageApproval(SQLBase):110class MessageApproval(SQLBase):
104 """A held message."""111 """A held message."""
105112
@@ -344,10 +351,7 @@
344 @property351 @property
345 def is_usable(self):352 def is_usable(self):
346 """See `IMailingList`."""353 """See `IMailingList`."""
347 return self.status in [MailingListStatus.ACTIVE,354 return self.status in USABLE_STATUSES
348 MailingListStatus.MODIFIED,
349 MailingListStatus.UPDATING,
350 MailingListStatus.MOD_FAILED]
351355
352 def _get_welcome_message(self):356 def _get_welcome_message(self):
353 return self.welcome_message_357 return self.welcome_message_
@@ -547,6 +551,24 @@
547 """ % sqlvalues(team_name),551 """ % sqlvalues(team_name),
548 clauseTables=['Person'])552 clauseTables=['Person'])
549553
554 def getSubscriptionsForTeams(self, person, teams):
555 """See `IMailingListSet`."""
556 store = IStore(MailingList)
557 team_ids = set(map(operator.attrgetter("id"), teams))
558 lists = dict(store.find(
559 (MailingList.teamID, MailingList.id),
560 MailingList.teamID.is_in(team_ids),
561 MailingList.status.is_in(USABLE_STATUSES)))
562 subscriptions = dict(store.find(
563 (MailingListSubscription.mailing_listID,
564 MailingListSubscription.id),
565 MailingListSubscription.person == person,
566 MailingListSubscription.mailing_listID.is_in(lists.values())))
567 by_team = {}
568 for team, mailing_list in lists.items():
569 by_team[team] = (mailing_list, subscriptions.get(mailing_list))
570 return by_team
571
550 def _getTeamIdsAndMailingListIds(self, team_names):572 def _getTeamIdsAndMailingListIds(self, team_names):
551 """Return a tuple of team and mailing list Ids for the team names."""573 """Return a tuple of team and mailing list Ids for the team names."""
552 store = IStore(MailingList)574 store = IStore(MailingList)
553575
=== modified file 'lib/lp/registry/tests/mailinglists_helper.py'
--- lib/lp/registry/tests/mailinglists_helper.py 2012-12-19 21:02:33 +0000
+++ lib/lp/registry/tests/mailinglists_helper.py 2015-03-09 15:05:42 +0000
@@ -29,9 +29,6 @@
29from lp.services.database.sqlbase import flush_database_updates29from lp.services.database.sqlbase import flush_database_updates
3030
3131
32COMMASPACE = ', '
33
34
35def fault_catcher(func):32def fault_catcher(func):
36 """Decorator for displaying Faults in a cross-compatible way.33 """Decorator for displaying Faults in a cross-compatible way.
3734