launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #18072
[Merge] lp:~cjwatson/launchpad/participation-query-count into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/participation-query-count into lp:launchpad.
Commit message:
Push the mailing list subscription checks on Person:+participation down to a bulk method on IMailingListSet.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1429806 in Launchpad itself: "Person:+participation times out when lots of mailing lists are involved"
https://bugs.launchpad.net/launchpad/+bug/1429806
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/participation-query-count/+merge/252267
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.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/participation-query-count into lp:launchpad.
=== 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 11:42:57 +0000
@@ -2028,7 +2028,8 @@
def label(self):
return 'Team participation for ' + self.context.displayname
- def _asParticipation(self, membership=None, team=None, via=None):
+ def _asParticipation(self, membership=None, team=None, via=None,
+ mailing_list=None, subscription=None):
"""Return a dict of participation information for the membership.
Method requires membership or team, not both.
@@ -2057,15 +2058,14 @@
# The member is a direct member; use the membership data.
team = membership.team
datejoined = membership.datejoined
- if membership.person == team.teamowner:
+ if membership.personID == team.teamownerID:
role = 'Owner'
elif membership.status == TeamMembershipStatus.ADMIN:
role = 'Admin'
else:
role = 'Member'
- if team.mailing_list is not None and team.mailing_list.is_usable:
- subscription = team.mailing_list.getSubscription(self.context)
+ if mailing_list is not None:
if subscription is None:
subscribed = 'Not subscribed'
else:
@@ -2081,28 +2081,37 @@
def active_participations(self):
"""Return the participation information for active memberships."""
paths, memberships = self.context.getPathsToTeams()
+ memberships = [
+ membership for membership in memberships
+ if check_permission('launchpad.View', membership.team)]
direct_teams = [membership.team for membership in memberships]
indirect_teams = [
team for team in paths.keys()
- if team not in direct_teams]
+ if team not in direct_teams and
+ check_permission('launchpad.View', team)]
participations = []
+ # Bulk-load mailing list subscriptions.
+ subscriptions = getUtility(IMailingListSet).getSubscriptionsForTeams(
+ self.context, direct_teams + indirect_teams)
+
# First, create a participation for all direct memberships.
for membership in memberships:
- # Add a participation record for the membership if allowed.
- if check_permission('launchpad.View', membership.team):
- participations.append(
- self._asParticipation(membership=membership))
+ mailing_list, subscription = subscriptions.get(
+ membership.team.id, (None, None))
+ participations.append(self._asParticipation(
+ membership=membership, mailing_list=mailing_list,
+ subscription=subscription))
# Second, create a participation for all indirect memberships,
# using the remaining paths.
for indirect_team in indirect_teams:
- if not check_permission('launchpad.View', indirect_team):
- continue
+ mailing_list, subscription = subscriptions.get(
+ indirect_team.id, (None, None))
participations.append(
self._asParticipation(
- via=paths[indirect_team],
- team=indirect_team))
+ via=paths[indirect_team], team=indirect_team,
+ mailing_list=mailing_list, subscription=subscription))
return sorted(participations, key=itemgetter('displayname'))
@cachedproperty
=== 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 11:42:57 +0000
@@ -47,6 +47,7 @@
from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
from lp.services.log.logger import FakeLogger
from lp.services.mail import stub
+from lp.services.propertycache import clear_property_cache
from lp.services.verification.interfaces.authtoken import LoginTokenType
from lp.services.verification.interfaces.logintoken import ILoginTokenSet
from lp.services.verification.tests.logintoken import get_token_url_from_email
@@ -68,6 +69,7 @@
login_person,
monkey_patch,
person_logged_in,
+ record_two_runs,
StormStatementRecorder,
TestCaseWithFactory,
)
@@ -878,6 +880,26 @@
self.assertEqual(1, len(participations))
self.assertEqual(True, self.view.has_participations)
+ def test_mailing_list_subscriptions_query_count(self):
+ # Additional mailing list subscriptions do not add additional queries.
+ def create_subscriptions():
+ direct_team = self.factory.makeTeam(members=[self.user])
+ direct_list = self.factory.makeMailingList(
+ direct_team, direct_team.teamowner)
+ direct_list.subscribe(self.user)
+ indirect_team = self.factory.makeTeam(members=[direct_team])
+ indirect_list = self.factory.makeMailingList(
+ indirect_team, indirect_team.teamowner)
+ indirect_list.subscribe(self.user)
+
+ def get_participations():
+ clear_property_cache(self.view)
+ return list(self.view.active_participations)
+
+ recorder1, recorder2 = record_two_runs(
+ get_participations, create_subscriptions, 5)
+ self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
+
class TestPersonRelatedPackagesView(TestCaseWithFactory):
"""Test the related software view."""
=== 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 11:42:57 +0000
@@ -37,6 +37,7 @@
id
team
person
+ personID
status
proposed_by
acknowledged_by
=== 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 11:42:57 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Mailing list interfaces."""
@@ -464,6 +464,17 @@
:raises AssertionError: When `team_name` is not a string.
"""
+ def getSubscriptionsForTeams(person, teams):
+ """Return a person's subscriptions to usable lists for a set of teams.
+
+ :param person: An `IPerson`.
+ :param teams: A list of `ITeam`s.
+ :return: A dictionary mapping team IDs to tuples of `IMailingList`
+ IDs and `IMailingListSubscription` IDs; the second element will
+ be None if a team has a list to which the person is not
+ subscribed.
+ """
+
def getSubscribedAddresses(team_names):
"""Return the set of subscribed email addresses for members.
=== 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 11:42:57 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Team membership interfaces."""
@@ -131,6 +131,7 @@
Reference(title=_("Member"), required=True, readonly=True,
schema=Interface), # Specified in interfaces/person.py.
exported_as='member')
+ personID = Int(title=_("Person ID"), required=True, readonly=True)
proposed_by = Attribute(_('Proponent'))
reviewed_by = Attribute(
_("The team admin who approved/rejected the member."))
=== 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 11:42:57 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -100,6 +100,13 @@
PostedMessageStatus.APPROVAL_PENDING)
+USABLE_STATUSES = (
+ MailingListStatus.ACTIVE,
+ MailingListStatus.MODIFIED,
+ MailingListStatus.UPDATING,
+ MailingListStatus.MOD_FAILED)
+
+
class MessageApproval(SQLBase):
"""A held message."""
@@ -344,10 +351,7 @@
@property
def is_usable(self):
"""See `IMailingList`."""
- return self.status in [MailingListStatus.ACTIVE,
- MailingListStatus.MODIFIED,
- MailingListStatus.UPDATING,
- MailingListStatus.MOD_FAILED]
+ return self.status in USABLE_STATUSES
def _get_welcome_message(self):
return self.welcome_message_
@@ -547,6 +551,24 @@
""" % sqlvalues(team_name),
clauseTables=['Person'])
+ def getSubscriptionsForTeams(self, person, teams):
+ """See `IMailingListSet`."""
+ store = IStore(MailingList)
+ team_ids = set(map(operator.attrgetter("id"), teams))
+ lists = dict(store.find(
+ (MailingList.teamID, MailingList.id),
+ MailingList.teamID.is_in(team_ids),
+ MailingList.status.is_in(USABLE_STATUSES)))
+ subscriptions = dict(store.find(
+ (MailingListSubscription.mailing_listID,
+ MailingListSubscription.id),
+ MailingListSubscription.person == person,
+ MailingListSubscription.mailing_listID.is_in(lists.values())))
+ by_team = {}
+ for team, mailing_list in lists.items():
+ by_team[team] = (mailing_list, subscriptions.get(mailing_list))
+ return by_team
+
def _getTeamIdsAndMailingListIds(self, team_names):
"""Return a tuple of team and mailing list Ids for the team names."""
store = IStore(MailingList)
Follow ups