Merge lp:~wallyworld/launchpad/person-index-timeout-931771 into lp:launchpad

Proposed by Ian Booth on 2012-11-01
Status: Merged
Approved by: j.c.sackett on 2012-11-01
Approved revision: no longer in the source branch.
Merged at revision: 16232
Proposed branch: lp:~wallyworld/launchpad/person-index-timeout-931771
Merge into: lp:launchpad
Diff against target: 324 lines (+121/-48)
11 files modified
lib/lp/registry/browser/person.py (+5/-15)
lib/lp/registry/interfaces/person.py (+3/-0)
lib/lp/registry/model/person.py (+14/-0)
lib/lp/security.py (+9/-12)
lib/lp/soyuz/browser/archive.py (+4/-0)
lib/lp/soyuz/browser/configure.zcml (+0/-3)
lib/lp/soyuz/model/archive.py (+63/-0)
lib/lp/soyuz/templates/archive-activate.pt (+3/-1)
lib/lp/soyuz/templates/archive-macros.pt (+17/-0)
lib/lp/soyuz/templates/person-portlet-ppas.pt (+3/-2)
lib/lp/soyuz/templates/person-ppas.pt (+0/-15)
To merge this branch: bzr merge lp:~wallyworld/launchpad/person-index-timeout-931771
Reviewer Review Type Date Requested Status
j.c.sackett (community) 2012-11-01 Approve on 2012-11-01
Review via email: mp+132446@code.launchpad.net

Commit Message

Improve archive permission checks so that visible ppas for a person can be found efficiently.

Description of the Change

== Implementation ==

The root cause of the bug is that the page iterates over all person ppas checking for launchpad.View permission. Each permission check does several queries. I refactored the implementation as follows:

I looked at the permission checking code in the security adaptor and gathered all the rules used to formulate the final database query done for each archive. I coded these rules in a method called get_enabled_archive_filter(). There is a new method on IPerson called getVisiblePPAs() which uses this filter to load in one query all the PPAs visible to the caller. This single call replaces the individual calls to the security adaptor.

The TAL and view have been refactored to use the new API. To ensure property caching works, I eliminated the subview and converted it to a macro, thus allowing the visible ppas to be loaded once and passed via a variable declaration to the macro.

Finally, I converted other permission checks IArchive in the security adaptors to use the new filter. This was simply to ensure that a common code base was used for the permission checks.

I got wgrant to run the queries on DF to ensure performance was acceptable and a few tweaks were need due to Postgres being "special".

== Tests ==

This is an internal change, so rely on existing tests.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/security.py
  lib/lp/registry/browser/person.py
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/model/person.py
  lib/lp/soyuz/browser/archive.py
  lib/lp/soyuz/browser/configure.zcml
  lib/lp/soyuz/model/archive.py
  lib/lp/soyuz/templates/archive-activate.pt
  lib/lp/soyuz/templates/archive-macros.pt
  lib/lp/soyuz/templates/person-portlet-ppas.pt

To post a comment you must log in.
j.c.sackett (jcsackett) wrote :

This looks good, Ian, just one minor quibble you can deal with before landing.

#248: If this is meant to get around circular imports, please comment it as
such. It's also cleaner to move imports to the top of the execution scope
they're brought into.

review: Approve

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 2012-10-26 07:15:49 +0000
3+++ lib/lp/registry/browser/person.py 2012-11-03 13:52:20 +0000
4@@ -138,12 +138,8 @@
5 LaunchpadRadioWidgetWithDescription,
6 )
7 from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
8-from lp.bugs.interfaces.bugtask import (
9- BugTaskStatus,
10- IBugTaskSet,
11- )
12+from lp.bugs.interfaces.bugtask import BugTaskStatus
13 from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
14-from lp.bugs.model.bugtask import BugTaskSet
15 from lp.buildmaster.enums import BuildStatus
16 from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
17 from lp.code.errors import InvalidNamespace
18@@ -1931,11 +1927,11 @@
19 return True
20
21 # If the current user can view any PPA, show the section.
22- for ppa in self.context.ppas:
23- if check_permission('launchpad.View', ppa):
24- return True
25+ return self.visible_ppas.count() > 0
26
27- return False
28+ @cachedproperty
29+ def visible_ppas(self):
30+ return self.context.getVisiblePPAs(self.user)
31
32 @property
33 def time_zone_offset(self):
34@@ -3470,14 +3466,8 @@
35 is_driver, is_bugsupervisor.
36 """
37 projects = []
38- user = getUtility(ILaunchBag).user
39 max_projects = self.max_results_to_display
40 pillarnames = self._related_projects[:max_projects]
41- products = [pillarname.pillar for pillarname in pillarnames
42- if IProduct.providedBy(pillarname.pillar)]
43- bugtask_set = getUtility(IBugTaskSet)
44- product_bugtask_counts = bugtask_set.getOpenBugTasksPerProduct(
45- user, products)
46 for pillarname in pillarnames:
47 pillar = pillarname.pillar
48 project = {}
49
50=== modified file 'lib/lp/registry/interfaces/person.py'
51--- lib/lp/registry/interfaces/person.py 2012-10-26 13:29:52 +0000
52+++ lib/lp/registry/interfaces/person.py 2012-11-03 13:52:20 +0000
53@@ -1051,6 +1051,9 @@
54 It will create a new IArchiveAuthToken if one doesn't already exist.
55 """
56
57+ def getVisiblePPAs(user):
58+ """Return the PPAs for which user has launchpad.View permission."""
59+
60 def getInvitedMemberships():
61 """Return all TeamMemberships of this team with the INVITED status.
62
63
64=== modified file 'lib/lp/registry/model/person.py'
65--- lib/lp/registry/model/person.py 2012-11-01 20:33:27 +0000
66+++ lib/lp/registry/model/person.py 2012-11-03 13:52:20 +0000
67@@ -3055,6 +3055,20 @@
68 return Archive.selectBy(
69 owner=self, purpose=ArchivePurpose.PPA, orderBy='name')
70
71+ def getVisiblePPAs(self, user):
72+ """See `IPerson`."""
73+
74+ # Avoid circular imports.
75+ from lp.soyuz.model.archive import get_enabled_archive_filter
76+
77+ filter = get_enabled_archive_filter(
78+ user, purpose=ArchivePurpose.PPA,
79+ include_public=True, include_subscribed=True)
80+ return Store.of(self).find(
81+ Archive,
82+ Archive.owner == self,
83+ filter).order_by(Archive.name)
84+
85 def getPPAByName(self, name):
86 """See `IPerson`."""
87 return getUtility(IArchiveSet).getPPAOwnedByPerson(self, name)
88
89=== modified file 'lib/lp/security.py'
90--- lib/lp/security.py 2012-10-30 16:59:58 +0000
91+++ lib/lp/security.py 2012-11-03 13:52:20 +0000
92@@ -14,6 +14,7 @@
93 from operator import methodcaller
94
95 from storm.expr import (
96+ And,
97 Select,
98 Union,
99 )
100@@ -219,6 +220,10 @@
101 IPackageUploadQueue,
102 )
103 from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
104+from lp.soyuz.model.archive import (
105+ Archive,
106+ get_enabled_archive_filter,
107+ )
108 from lp.translations.interfaces.customlanguagecode import ICustomLanguageCode
109 from lp.translations.interfaces.languagepack import ILanguagePack
110 from lp.translations.interfaces.pofile import IPOFile
111@@ -2493,18 +2498,10 @@
112 if user.inTeam(self.obj.owner):
113 return True
114
115- # Uploaders can view private PPAs.
116- if self.obj.is_ppa and self.obj.checkArchivePermission(user.person):
117- return True
118-
119- # Subscribers can view private PPAs.
120- if self.obj.is_ppa and self.obj.private:
121- archive_subs = getUtility(IArchiveSubscriberSet).getBySubscriber(
122- user.person, self.obj).any()
123- if archive_subs:
124- return True
125-
126- return False
127+ filter = get_enabled_archive_filter(
128+ user.person, include_subscribed=True)
129+ return not IStore(self.obj).find(
130+ Archive.id, And(Archive.id == self.obj.id, filter)).is_empty()
131
132 def checkUnauthenticated(self):
133 """Unauthenticated users can see the PPA if it's not private."""
134
135=== modified file 'lib/lp/soyuz/browser/archive.py'
136--- lib/lp/soyuz/browser/archive.py 2012-10-29 16:52:31 +0000
137+++ lib/lp/soyuz/browser/archive.py 2012-11-03 13:52:20 +0000
138@@ -1955,6 +1955,10 @@
139 def ubuntu(self):
140 return getUtility(ILaunchpadCelebrities).ubuntu
141
142+ @cachedproperty
143+ def visible_ppas(self):
144+ return self.context.getVisiblePPAs(self.user)
145+
146 @property
147 def initial_values(self):
148 """Set up default values for form fields."""
149
150=== modified file 'lib/lp/soyuz/browser/configure.zcml'
151--- lib/lp/soyuz/browser/configure.zcml 2012-07-09 12:32:23 +0000
152+++ lib/lp/soyuz/browser/configure.zcml 2012-11-03 13:52:20 +0000
153@@ -750,9 +750,6 @@
154 <browser:page
155 name="+portlet-ppas"
156 template="../templates/person-portlet-ppas.pt"/>
157- <browser:page
158- name="+ppas-list"
159- template="../templates/person-ppas.pt"/>
160 </browser:pages>
161 <browser:pages
162 for="lp.registry.interfaces.distribution.IDistribution"
163
164=== modified file 'lib/lp/soyuz/model/archive.py'
165--- lib/lp/soyuz/model/archive.py 2012-10-25 11:02:37 +0000
166+++ lib/lp/soyuz/model/archive.py 2012-11-03 13:52:20 +0000
167@@ -9,6 +9,7 @@
168 'Archive',
169 'ArchiveSet',
170 'get_archive_privacy_filter',
171+ 'get_enabled_archive_filter',
172 'validate_ppa',
173 ]
174
175@@ -176,6 +177,7 @@
176 )
177 from lp.soyuz.model.archiveauthtoken import ArchiveAuthToken
178 from lp.soyuz.model.archivedependency import ArchiveDependency
179+from lp.soyuz.model.archivepermission import ArchivePermission
180 from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
181 from lp.soyuz.model.binarypackagename import BinaryPackageName
182 from lp.soyuz.model.binarypackagerelease import (
183@@ -2597,3 +2599,64 @@
184 TeamParticipation.teamID,
185 where=(TeamParticipation.person == user))))
186 return privacy_filter
187+
188+
189+def get_enabled_archive_filter(user, purpose=None,
190+ include_public=False, include_subscribed=False):
191+ """ Return a filter that can be used with a Storm query to filter Archives.
192+
193+ The archive must be enabled, plus satisfy the other specified conditions.
194+ """
195+ purpose_term = True
196+ if purpose:
197+ purpose_term = Archive.purpose == purpose
198+ if user is None:
199+ if include_public:
200+ terms = [
201+ purpose_term, Archive._private == False,
202+ Archive._enabled == True]
203+ return And(*terms)
204+ else:
205+ return False
206+
207+ # Administrator are allowed to view private archives.
208+ roles = IPersonRoles(user)
209+ if roles.in_admin or roles.in_commercial_admin:
210+ return purpose_term
211+
212+ main = getUtility(IComponentSet)['main']
213+ user_teams = Select(
214+ TeamParticipation.teamID,
215+ where=TeamParticipation.person == user)
216+
217+ is_owner = Archive.ownerID.is_in(user_teams)
218+
219+ from lp.soyuz.model.archivesubscriber import ArchiveSubscriber
220+
221+ is_allowed = Select(
222+ ArchivePermission.archiveID, where=And(
223+ ArchivePermission.permission == ArchivePermissionType.UPLOAD,
224+ ArchivePermission.component == main,
225+ ArchivePermission.personID.is_in(user_teams)),
226+ tables=ArchivePermission, distinct=True)
227+
228+ is_subscribed = Select(
229+ ArchiveSubscriber.archive_id, where=And(
230+ ArchiveSubscriber.status == ArchiveSubscriberStatus.CURRENT,
231+ ArchiveSubscriber.subscriber_id.is_in(user_teams)),
232+ tables=ArchiveSubscriber, distinct=True)
233+
234+ filter_terms = [
235+ is_owner,
236+ And(
237+ Archive.purpose == ArchivePurpose.PPA,
238+ Archive.id.is_in(is_allowed))]
239+ if include_subscribed:
240+ filter_terms.append(And(
241+ Archive.purpose == ArchivePurpose.PPA,
242+ Archive.id.is_in(is_subscribed)))
243+
244+ if include_public:
245+ filter_terms.append(
246+ And(Archive._enabled == True, Archive._private == False))
247+ return And(purpose_term, Or(*filter_terms))
248
249=== modified file 'lib/lp/soyuz/templates/archive-activate.pt'
250--- lib/lp/soyuz/templates/archive-activate.pt 2011-01-17 18:55:38 +0000
251+++ lib/lp/soyuz/templates/archive-activate.pt 2012-11-03 13:52:20 +0000
252@@ -23,7 +23,9 @@
253 <div tal:condition="context/ppas" id="ppas"
254 style="padding-top: .3em; padding-bottom: .3em;">
255 <h2>Existing PPAs</h2>
256- <div tal:replace="structure context/@@+ppas-list"/>
257+ <tal:ppa tal:define="visible_ppas view/visible_ppas">
258+ <metal:ppas-list use-macro="context/@@+macros/ppas-list"/>
259+ </tal:ppa>
260 </div>
261 <p>
262 Read the current version of
263
264=== modified file 'lib/lp/soyuz/templates/archive-macros.pt'
265--- lib/lp/soyuz/templates/archive-macros.pt 2012-02-01 15:31:32 +0000
266+++ lib/lp/soyuz/templates/archive-macros.pt 2012-11-03 13:52:20 +0000
267@@ -350,4 +350,21 @@
268 </tal:latest_updates>
269 </metal:latest_updates_portlet>
270
271+<metal:ppas-list define-macro="ppas-list">
272+ <tal:comment condition="nothing">
273+ This macro requires the following defined variables:
274+ visible_ppas - the ppas for which the user has view permission.
275+ </tal:comment>
276+
277+ <div tal:define="ppas visible_ppas" tal:condition="ppas">
278+ <table>
279+ <tal:ppa_line tal:repeat="ppa ppas">
280+ <tr>
281+ <td tal:content="structure ppa/fmt:link"></td>
282+ </tr>
283+ </tal:ppa_line>
284+ </table>
285+ </div>
286+</metal:ppas-list>
287+
288 </tal:root>
289
290=== modified file 'lib/lp/soyuz/templates/person-portlet-ppas.pt'
291--- lib/lp/soyuz/templates/person-portlet-ppas.pt 2012-06-16 13:12:41 +0000
292+++ lib/lp/soyuz/templates/person-portlet-ppas.pt 2012-11-03 13:52:20 +0000
293@@ -6,8 +6,9 @@
294 <div id="ppas" class="portlet" tal:condition="view/should_show_ppa_section">
295 <h2>Personal package archives</h2>
296
297- <div tal:replace="structure context/@@+ppas-list"/>
298-
299+ <tal:ppa tal:define="visible_ppas view/visible_ppas">
300+ <metal:ppas-list use-macro="context/@@+macros/ppas-list"/>
301+ </tal:ppa>
302 <ul class="horizontal">
303 <tal:can-create-ppa condition="context/canCreatePPA">
304 <li tal:define="link context/menu:overview/activate_ppa"
305
306=== removed file 'lib/lp/soyuz/templates/person-ppas.pt'
307--- lib/lp/soyuz/templates/person-ppas.pt 2010-06-12 05:05:11 +0000
308+++ lib/lp/soyuz/templates/person-ppas.pt 1970-01-01 00:00:00 +0000
309@@ -1,15 +0,0 @@
310-<tal:root
311- xmlns:tal="http://xml.zope.org/namespaces/tal"
312- xmlns:metal="http://xml.zope.org/namespaces/metal"
313- xmlns:i18n="http://xml.zope.org/namespaces/i18n"
314- omit-tag="">
315- <div tal:define="ppas context/ppas" tal:condition="ppas">
316- <table>
317- <tal:ppa_line tal:repeat="ppa ppas">
318- <tr tal:condition="ppa/required:launchpad.View">
319- <td tal:content="structure ppa/fmt:link"></td>
320- </tr>
321- </tal:ppa_line>
322- </table>
323- </div>
324-</tal:root>