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
1=== modified file 'lib/lp/registry/browser/person.py'
2--- lib/lp/registry/browser/person.py 2015-03-05 11:39:06 +0000
3+++ lib/lp/registry/browser/person.py 2015-03-09 15:05:42 +0000
4@@ -284,9 +284,6 @@
5 from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
6
7
8-COMMASPACE = ', '
9-
10-
11 class PersonBreadcrumb(DisplaynameBreadcrumb):
12 implements(IHeadingBreadcrumb, IMultiFacetedBreadcrumb)
13
14@@ -2028,23 +2025,24 @@
15 def label(self):
16 return 'Team participation for ' + self.context.displayname
17
18- def _asParticipation(self, membership=None, team=None, via=None):
19+ def _asParticipation(self, team=None, membership=None, via=None,
20+ mailing_list=None, subscription=None):
21 """Return a dict of participation information for the membership.
22
23- Method requires membership or team, not both.
24+ Method requires membership or via, not both.
25 :param via: The team through which the membership in the indirect
26 team is established.
27 """
28- if ((membership is None and team is None) or
29- (membership is not None and team is not None)):
30+ if ((membership is None and via is None) or
31+ (membership is not None and via is not None)):
32 raise AssertionError(
33- "The membership or team argument must be provided, not both.")
34+ "The membership or via argument must be provided, not both.")
35
36 if via is not None:
37 # When showing the path, it's unnecessary to show the team in
38 # question at the beginning of the path, or the user at the
39 # end of the path.
40- via = COMMASPACE.join(
41+ via = ", ".join(
42 [via_team.displayname for via_team in via[1:-1]])
43
44 if membership is None:
45@@ -2055,17 +2053,15 @@
46 datejoined = None
47 else:
48 # The member is a direct member; use the membership data.
49- team = membership.team
50 datejoined = membership.datejoined
51- if membership.person == team.teamowner:
52+ if membership.personID == team.teamownerID:
53 role = 'Owner'
54 elif membership.status == TeamMembershipStatus.ADMIN:
55 role = 'Admin'
56 else:
57 role = 'Member'
58
59- if team.mailing_list is not None and team.mailing_list.is_usable:
60- subscription = team.mailing_list.getSubscription(self.context)
61+ if mailing_list is not None:
62 if subscription is None:
63 subscribed = 'Not subscribed'
64 else:
65@@ -2082,27 +2078,26 @@
66 """Return the participation information for active memberships."""
67 paths, memberships = self.context.getPathsToTeams()
68 direct_teams = [membership.team for membership in memberships]
69- indirect_teams = [
70- team for team in paths.keys()
71- if team not in direct_teams]
72+ items = []
73+ for membership in memberships:
74+ items.append(dict(team=membership.team, membership=membership))
75+ for team, via in paths.items():
76+ if team not in direct_teams:
77+ items.append(dict(team=team, via=via))
78+ items = [
79+ item for item in items
80+ if check_permission('launchpad.View', item["team"])]
81 participations = []
82
83- # First, create a participation for all direct memberships.
84- for membership in memberships:
85- # Add a participation record for the membership if allowed.
86- if check_permission('launchpad.View', membership.team):
87- participations.append(
88- self._asParticipation(membership=membership))
89+ # Bulk-load mailing list subscriptions.
90+ subscriptions = getUtility(IMailingListSet).getSubscriptionsForTeams(
91+ self.context, [item["team"] for item in items])
92
93- # Second, create a participation for all indirect memberships,
94- # using the remaining paths.
95- for indirect_team in indirect_teams:
96- if not check_permission('launchpad.View', indirect_team):
97- continue
98- participations.append(
99- self._asParticipation(
100- via=paths[indirect_team],
101- team=indirect_team))
102+ # Create all the participations.
103+ for item in items:
104+ item["mailing_list"], item["subscription"] = subscriptions.get(
105+ item["team"].id, (None, None))
106+ participations.append(self._asParticipation(**item))
107 return sorted(participations, key=itemgetter('displayname'))
108
109 @cachedproperty
110
111=== modified file 'lib/lp/registry/browser/tests/test_person.py'
112--- lib/lp/registry/browser/tests/test_person.py 2015-03-05 15:28:11 +0000
113+++ lib/lp/registry/browser/tests/test_person.py 2015-03-09 15:05:42 +0000
114@@ -47,6 +47,7 @@
115 from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
116 from lp.services.log.logger import FakeLogger
117 from lp.services.mail import stub
118+from lp.services.propertycache import clear_property_cache
119 from lp.services.verification.interfaces.authtoken import LoginTokenType
120 from lp.services.verification.interfaces.logintoken import ILoginTokenSet
121 from lp.services.verification.tests.logintoken import get_token_url_from_email
122@@ -68,6 +69,7 @@
123 login_person,
124 monkey_patch,
125 person_logged_in,
126+ record_two_runs,
127 StormStatementRecorder,
128 TestCaseWithFactory,
129 )
130@@ -878,6 +880,26 @@
131 self.assertEqual(1, len(participations))
132 self.assertEqual(True, self.view.has_participations)
133
134+ def test_mailing_list_subscriptions_query_count(self):
135+ # Additional mailing list subscriptions do not add additional queries.
136+ def create_subscriptions():
137+ direct_team = self.factory.makeTeam(members=[self.user])
138+ direct_list = self.factory.makeMailingList(
139+ direct_team, direct_team.teamowner)
140+ direct_list.subscribe(self.user)
141+ indirect_team = self.factory.makeTeam(members=[direct_team])
142+ indirect_list = self.factory.makeMailingList(
143+ indirect_team, indirect_team.teamowner)
144+ indirect_list.subscribe(self.user)
145+
146+ def get_participations():
147+ clear_property_cache(self.view)
148+ return list(self.view.active_participations)
149+
150+ recorder1, recorder2 = record_two_runs(
151+ get_participations, create_subscriptions, 5)
152+ self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
153+
154
155 class TestPersonRelatedPackagesView(TestCaseWithFactory):
156 """Test the related software view."""
157
158=== modified file 'lib/lp/registry/configure.zcml'
159--- lib/lp/registry/configure.zcml 2015-02-26 11:34:47 +0000
160+++ lib/lp/registry/configure.zcml 2015-03-09 15:05:42 +0000
161@@ -37,6 +37,7 @@
162 id
163 team
164 person
165+ personID
166 status
167 proposed_by
168 acknowledged_by
169
170=== modified file 'lib/lp/registry/interfaces/mailinglist.py'
171--- lib/lp/registry/interfaces/mailinglist.py 2013-01-07 02:40:55 +0000
172+++ lib/lp/registry/interfaces/mailinglist.py 2015-03-09 15:05:42 +0000
173@@ -1,4 +1,4 @@
174-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
175+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
176 # GNU Affero General Public License version 3 (see the file LICENSE).
177
178 """Mailing list interfaces."""
179@@ -464,6 +464,17 @@
180 :raises AssertionError: When `team_name` is not a string.
181 """
182
183+ def getSubscriptionsForTeams(person, teams):
184+ """Return a person's subscriptions to usable lists for a set of teams.
185+
186+ :param person: An `IPerson`.
187+ :param teams: A list of `ITeam`s.
188+ :return: A dictionary mapping team IDs to tuples of `IMailingList`
189+ IDs and `IMailingListSubscription` IDs; the second element will
190+ be None if a team has a list to which the person is not
191+ subscribed.
192+ """
193+
194 def getSubscribedAddresses(team_names):
195 """Return the set of subscribed email addresses for members.
196
197
198=== modified file 'lib/lp/registry/interfaces/teammembership.py'
199--- lib/lp/registry/interfaces/teammembership.py 2013-01-07 02:40:55 +0000
200+++ lib/lp/registry/interfaces/teammembership.py 2015-03-09 15:05:42 +0000
201@@ -1,4 +1,4 @@
202-# Copyright 2009 Canonical Ltd. This software is licensed under the
203+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
204 # GNU Affero General Public License version 3 (see the file LICENSE).
205
206 """Team membership interfaces."""
207@@ -131,6 +131,7 @@
208 Reference(title=_("Member"), required=True, readonly=True,
209 schema=Interface), # Specified in interfaces/person.py.
210 exported_as='member')
211+ personID = Int(title=_("Person ID"), required=True, readonly=True)
212 proposed_by = Attribute(_('Proponent'))
213 reviewed_by = Attribute(
214 _("The team admin who approved/rejected the member."))
215
216=== modified file 'lib/lp/registry/model/mailinglist.py'
217--- lib/lp/registry/model/mailinglist.py 2013-06-20 05:50:00 +0000
218+++ lib/lp/registry/model/mailinglist.py 2015-03-09 15:05:42 +0000
219@@ -1,4 +1,4 @@
220-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
221+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
222 # GNU Affero General Public License version 3 (see the file LICENSE).
223
224 __metaclass__ = type
225@@ -100,6 +100,13 @@
226 PostedMessageStatus.APPROVAL_PENDING)
227
228
229+USABLE_STATUSES = (
230+ MailingListStatus.ACTIVE,
231+ MailingListStatus.MODIFIED,
232+ MailingListStatus.UPDATING,
233+ MailingListStatus.MOD_FAILED)
234+
235+
236 class MessageApproval(SQLBase):
237 """A held message."""
238
239@@ -344,10 +351,7 @@
240 @property
241 def is_usable(self):
242 """See `IMailingList`."""
243- return self.status in [MailingListStatus.ACTIVE,
244- MailingListStatus.MODIFIED,
245- MailingListStatus.UPDATING,
246- MailingListStatus.MOD_FAILED]
247+ return self.status in USABLE_STATUSES
248
249 def _get_welcome_message(self):
250 return self.welcome_message_
251@@ -547,6 +551,24 @@
252 """ % sqlvalues(team_name),
253 clauseTables=['Person'])
254
255+ def getSubscriptionsForTeams(self, person, teams):
256+ """See `IMailingListSet`."""
257+ store = IStore(MailingList)
258+ team_ids = set(map(operator.attrgetter("id"), teams))
259+ lists = dict(store.find(
260+ (MailingList.teamID, MailingList.id),
261+ MailingList.teamID.is_in(team_ids),
262+ MailingList.status.is_in(USABLE_STATUSES)))
263+ subscriptions = dict(store.find(
264+ (MailingListSubscription.mailing_listID,
265+ MailingListSubscription.id),
266+ MailingListSubscription.person == person,
267+ MailingListSubscription.mailing_listID.is_in(lists.values())))
268+ by_team = {}
269+ for team, mailing_list in lists.items():
270+ by_team[team] = (mailing_list, subscriptions.get(mailing_list))
271+ return by_team
272+
273 def _getTeamIdsAndMailingListIds(self, team_names):
274 """Return a tuple of team and mailing list Ids for the team names."""
275 store = IStore(MailingList)
276
277=== modified file 'lib/lp/registry/tests/mailinglists_helper.py'
278--- lib/lp/registry/tests/mailinglists_helper.py 2012-12-19 21:02:33 +0000
279+++ lib/lp/registry/tests/mailinglists_helper.py 2015-03-09 15:05:42 +0000
280@@ -29,9 +29,6 @@
281 from lp.services.database.sqlbase import flush_database_updates
282
283
284-COMMASPACE = ', '
285-
286-
287 def fault_catcher(func):
288 """Decorator for displaying Faults in a cross-compatible way.
289