Merge lp:~adeuring/launchpad/bug-834303 into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 14731
Proposed branch: lp:~adeuring/launchpad/bug-834303
Merge into: lp:launchpad
Diff against target: 217 lines (+88/-13)
6 files modified
lib/lp/soyuz/browser/archivesubscription.py (+18/-7)
lib/lp/soyuz/doc/archivesubscriber.txt (+5/-2)
lib/lp/soyuz/model/archivesubscriber.py (+9/-3)
lib/lp/soyuz/stories/ppa/xx-private-ppa-subscription-stories.txt (+2/-0)
lib/lp/soyuz/templates/archive-subscribers.pt (+5/-1)
lib/lp/soyuz/tests/test_archivesubscriptionview.py (+49/-0)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-834303
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Richard Harding (community) code* Approve
Review via email: mp+90304@code.launchpad.net

Commit message

[r=jcsackett,rharding][bug=834303] use batching in the +subscription view of PPPAs; sort subscribers by Person.name

Description of the change

This branch fixes bug 834303: Archive:+subscriptions times out with
many subscribers.

The cause of the timeout is Storm needing really much time to load
15000 or 20000 ArchiveSubscriber records.

As lifeless noted in a bug comment, the fix is obvious: To show the
data in batches.

This leads to another problem, noted by Anthony Lenton in another comment
on the bug: The data was sorted by the creation time of a subscription,
while an important use case is to change the subscription status of
a subscriber. Looking them up by sifting through more than 100 pages
to look up a subscriber by name would be really painful.

The fix is obvious: Sort by name. Anthony suggested to use the display
name; after an IRC chat with him I switched instead to sorting by LP
user name. This allows us to use StormRangeFactory, which uses the
sort value of the first/last displayed result row as the "batch value"
in URLs. This allows to manually edit a URL to quickly look up the
subscription status of a given user. Not as nice as a decent search
form, but at least a convenient work-around, as long as we don't have
a search form and the related result page...

In theory, we could use Person.displayname for batching with
StormRangeFactory too, but the related URLs would look even more
insane than the URLs for sorting by LP user name: We would need
Person.id as an additional sort parameter, and the sort value
-- which appears in the URL -- may contain spaces, and when you
change a URL manually, you should be aware of the meaning of the
second sort parameter, Person.id. To compare the URLs:

sort by Person.name:

https://launchpad.dev/~cprov/+archive/p3a/+subscriptions?batch=5&memo=[%22stevea%22]&start=5

sort by Person.displayname, Person.id

https://launchpad.dev/~cprov/+archive/p3a/+subscriptions?batch=5&memo=[%22Steve+Alexander%22,1234]&start=5

tests:

./bin/test soyuz -vvt test_archivesubscriptionview
./bin/test soyuz -vvt archivesubscriber.txt

no lint

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) wrote :

Looks good, my only comment is that I don't think you need the set() in line 30 since I don't think the subscriber ids should have dupes in there.

review: Approve (code*)
Revision history for this message
j.c.sackett (jcsackett) wrote :

Abel--

I concur with Rick's assessment, but set or list is a good cast, and there isn't really any reason to change it from set at this point.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/soyuz/browser/archivesubscription.py'
2--- lib/lp/soyuz/browser/archivesubscription.py 2012-01-01 02:58:52 +0000
3+++ lib/lp/soyuz/browser/archivesubscription.py 2012-01-27 14:28:24 +0000
4@@ -43,6 +43,10 @@
5 from lp.registry.interfaces.person import IPersonSet
6 from lp.services.fields import PersonChoice
7 from lp.services.propertycache import cachedproperty
8+from lp.services.webapp.batching import (
9+ BatchNavigator,
10+ StormRangeFactory,
11+ )
12 from lp.services.webapp.publisher import (
13 canonical_url,
14 LaunchpadView,
15@@ -147,21 +151,28 @@
16 return
17
18 super(ArchiveSubscribersView, self).initialize()
19+ subscription_set = getUtility(IArchiveSubscriberSet)
20+ self.subscriptions = subscription_set.getByArchive(self.context)
21+ self.batchnav = BatchNavigator(
22+ self.subscriptions, self.request,
23+ range_factory=StormRangeFactory(self.subscriptions))
24
25 @cachedproperty
26- def subscriptions(self):
27- """Return all the subscriptions for this archive."""
28- result = list(getUtility(IArchiveSubscriberSet
29- ).getByArchive(self.context))
30- ids = set(map(attrgetter('subscriber_id'), result))
31+ def current_subscriptions_batch(self):
32+ """Return the subscriptions of the current batch.
33+
34+ Bulk loads the related Person records.
35+ """
36+ batch = list(self.batchnav.currentBatch())
37+ ids = map(attrgetter('subscriber_id'), batch)
38 list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(ids,
39 need_validity=True))
40- return result
41+ return batch
42
43 @cachedproperty
44 def has_subscriptions(self):
45 """Return whether this archive has any subscribers."""
46- return bool(self.subscriptions)
47+ return self.subscriptions.any() is not None
48
49 def validate_new_subscription(self, action, data):
50 """Ensure the subscriber isn't already subscribed.
51
52=== modified file 'lib/lp/soyuz/doc/archivesubscriber.txt'
53--- lib/lp/soyuz/doc/archivesubscriber.txt 2012-01-15 13:32:27 +0000
54+++ lib/lp/soyuz/doc/archivesubscriber.txt 2012-01-27 14:28:24 +0000
55@@ -277,11 +277,14 @@
56 PPA named p3a for Celso Providelo 2009-02-26
57 PPA named p3a for Mark Shuttleworth 2009-02-22
58
59+getByArchive() sorts by subscriber name.
60+
61 >>> for subscription in sub_set.getByArchive(mark_private_ppa):
62+ ... print subscription.subscriber.name
63 ... print subscription.subscriber.displayname
64 ... print subscription.date_created.date()
65- Team Cprov 2009-02-24
66- Joe Smith 2009-02-22
67+ joesmith Joe Smith 2009-02-22
68+ team-name-... Team Cprov 2009-02-24
69
70 If we cancel one of the subscriptions:
71
72
73=== modified file 'lib/lp/soyuz/model/archivesubscriber.py'
74--- lib/lp/soyuz/model/archivesubscriber.py 2011-12-30 06:14:56 +0000
75+++ lib/lp/soyuz/model/archivesubscriber.py 2012-01-27 14:28:24 +0000
76@@ -9,6 +9,7 @@
77 'ArchiveSubscriber',
78 ]
79
80+from operator import itemgetter
81 import pytz
82 from storm.expr import (
83 And,
84@@ -31,6 +32,7 @@
85 from lp.registry.interfaces.person import validate_person
86 from lp.registry.model.teammembership import TeamParticipation
87 from lp.services.database.constants import UTC_NOW
88+from lp.services.database.decoratedresultset import DecoratedResultSet
89 from lp.services.database.enumcol import DBEnum
90 from lp.services.identity.interfaces.emailaddress import EmailAddressStatus
91 from lp.services.identity.model.emailaddress import EmailAddress
92@@ -209,13 +211,17 @@
93 """See `IArchiveSubscriberSet`."""
94 # Grab the extra Storm expressions, for this query,
95 # depending on the params:
96+ # avoid circular imports.
97+ from lp.registry.model.person import Person
98 extra_exprs = self._getExprsForSubscriptionQueries(
99 archive, current_only)
100
101 store = Store.of(archive)
102- return store.find(
103- ArchiveSubscriber,
104- *extra_exprs).order_by(Desc(ArchiveSubscriber.date_created))
105+ result = store.using(ArchiveSubscriber,
106+ Join(Person, ArchiveSubscriber.subscriber_id == Person.id)).find(
107+ (ArchiveSubscriber, Person),
108+ *extra_exprs).order_by(Person.name)
109+ return DecoratedResultSet(result, itemgetter(0))
110
111 def _getExprsForSubscriptionQueries(self, archive=None,
112 current_only=True):
113
114=== modified file 'lib/lp/soyuz/stories/ppa/xx-private-ppa-subscription-stories.txt'
115--- lib/lp/soyuz/stories/ppa/xx-private-ppa-subscription-stories.txt 2012-01-15 13:32:27 +0000
116+++ lib/lp/soyuz/stories/ppa/xx-private-ppa-subscription-stories.txt 2012-01-27 14:28:24 +0000
117@@ -108,6 +108,7 @@
118
119 >>> print_archive_subscriptions(cprov_browser.contents)
120 Name Expires Comment
121+ Joe Smith Joe is my friend ...
122 Launchpad Developers 2200-08-01 Launchpad developer access.
123 ...
124
125@@ -148,6 +149,7 @@
126 http://launchpad.dev/~cprov/+archive/p3a/+subscriptions
127 >>> print_archive_subscriptions(cprov_browser.contents)
128 Name Expires Comment
129+ Joe Smith Joe is my friend ...
130 Launchpad Developers 2200-08-01 a different description
131 ...
132 >>> for msg in get_feedback_messages(cprov_browser.contents):
133
134=== modified file 'lib/lp/soyuz/templates/archive-subscribers.pt'
135--- lib/lp/soyuz/templates/archive-subscribers.pt 2012-01-19 22:14:01 +0000
136+++ lib/lp/soyuz/templates/archive-subscribers.pt 2012-01-27 14:28:24 +0000
137@@ -37,6 +37,8 @@
138 enctype="multipart/form-data"
139 accept-charset="UTF-8">
140
141+ <tal:navigation_top
142+ replace="structure view/batchnav/@@+navigation-links-upper" />
143 <table tal:attributes="summary string:Subscribers for ${context/displayname};"
144 id="archive-subscribers" class="listing">
145 <thead>
146@@ -78,7 +80,7 @@
147 </td>
148 </tr>
149 <tr class="archive_subscriber_row"
150- tal:repeat="subscription view/subscriptions">
151+ tal:repeat="subscription view/current_subscriptions_batch">
152 <td tal:content="structure subscription/subscriber/fmt:link" />
153 <td tal:content="subscription/date_expires/fmt:date" />
154 <td tal:content="subscription/description" />
155@@ -90,6 +92,8 @@
156 </tr>
157 </tbody>
158 </table>
159+ <tal:navigation_bottom
160+ replace="structure view/batchnav/@@+navigation-links-lower" />
161 </form>
162 </div><!-- class="portlet" -->
163 <script type="text/javascript" id="setup-archivesubscribers-index">
164
165=== added file 'lib/lp/soyuz/tests/test_archivesubscriptionview.py'
166--- lib/lp/soyuz/tests/test_archivesubscriptionview.py 1970-01-01 00:00:00 +0000
167+++ lib/lp/soyuz/tests/test_archivesubscriptionview.py 2012-01-27 14:28:24 +0000
168@@ -0,0 +1,49 @@
169+# Copyright 2012 Canonical Ltd. This software is licensed under the
170+# GNU Affero General Public License version 3 (see the file LICENSE).
171+
172+"""Unit tests for ArchiveSubscribersView."""
173+
174+__metaclass__ = type
175+
176+from soupmatchers import (
177+ HTMLContains,
178+ Tag,
179+ )
180+from zope.component import getUtility
181+
182+from lp.registry.interfaces.person import IPersonSet
183+from lp.testing import (
184+ person_logged_in,
185+ TestCaseWithFactory,
186+ )
187+from lp.testing.layers import LaunchpadFunctionalLayer
188+from lp.testing.views import create_initialized_view
189+
190+
191+class TestArchiveSubscribersView(TestCaseWithFactory):
192+ """Tests for ArchiveSubscribersView."""
193+
194+ layer = LaunchpadFunctionalLayer
195+
196+ def setUp(self):
197+ super(TestArchiveSubscribersView, self).setUp()
198+ self.p3a_owner = self.factory.makePerson()
199+ admin = getUtility(IPersonSet).getByEmail('admin@canonical.com')
200+ with person_logged_in(admin):
201+ self.private_ppa = self.factory.makeArchive(
202+ owner=self.p3a_owner, private=True, name='p3a')
203+ with person_logged_in(self.p3a_owner):
204+ for count in range(3):
205+ subscriber = self.factory.makePerson()
206+ self.private_ppa.newSubscription(subscriber, self.p3a_owner)
207+
208+ def test_has_batch_navigation(self):
209+ # The page has the usual batch navigation links.
210+ with person_logged_in(self.p3a_owner):
211+ view = create_initialized_view(
212+ self.private_ppa, '+subscriptions', principal=self.p3a_owner)
213+ html = view.render()
214+ has_batch_navigation = HTMLContains(
215+ Tag('batch navigation links', 'td',
216+ attrs={'class': 'batch-navigation-links'}, count=2))
217+ self.assertThat(html, has_batch_navigation)