Merge lp:~danilo/launchpad/bug-826692-take2 into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 13809
Proposed branch: lp:~danilo/launchpad/bug-826692-take2
Merge into: lp:launchpad
Diff against target: 424 lines (+141/-65)
6 files modified
lib/lp/code/browser/branchmergeproposallisting.py (+9/-4)
lib/lp/code/browser/tests/test_branchmergeproposallisting.py (+18/-0)
lib/lp/code/interfaces/branch.py (+1/-1)
lib/lp/code/interfaces/branchcollection.py (+3/-1)
lib/lp/code/model/branch.py (+3/-2)
lib/lp/code/model/branchcollection.py (+107/-57)
To merge this branch: bzr merge lp:~danilo/launchpad/bug-826692-take2
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+72996@code.launchpad.net

Commit message

[r=jtv][bug=826692] Re-land the fix for bug 826692 with the fix for private branches included: makes +activereviews page render faster by eager loading relevant data in getMergeProposals().

Description of the change

= Bug 826692: resubmit =

This has mostly been reviewed in https://code.launchpad.net/~danilo/launchpad/bug-826692/+merge/71836

However, that didn't work for private branches. The only change (other than a few more lint fixes) is the one below:

== Proposed fix ==

http://paste.ubuntu.com/675046/

== Tests ==

bin/test -cvvt ActiveReviewsWithPrivateBranches -t TestLandingCandidates

(TestLandingCandidates was introduced when the landing was reverted)

== Demo and Q/A ==

Go to a Landscape branch lp:landscape and look at merge proposals page for it (note, go to the branch, not the project page). The number of proposals there should match the number listed on the branch page.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/model/branchcollection.py
  lib/lp/code/interfaces/branch.py
  lib/lp/code/interfaces/branchcollection.py
  lib/lp/code/model/branch.py
  lib/lp/code/browser/branchmergeproposallisting.py
  lib/lp/code/browser/tests/test_branchmergeproposallisting.py

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

The incremental diff from the pastebin looks fine (apart from a missing "the" in the test comment ☺).

I wish visible_by_user were called visible_to_user, but that's irrelevant for this branch.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/branchmergeproposallisting.py'
2--- lib/lp/code/browser/branchmergeproposallisting.py 2011-08-25 13:49:26 +0000
3+++ lib/lp/code/browser/branchmergeproposallisting.py 2011-08-26 06:36:42 +0000
4@@ -47,6 +47,7 @@
5 IBranchCollection,
6 )
7 from lp.code.interfaces.branchmergeproposal import (
8+ BRANCH_MERGE_PROPOSAL_FINAL_STATES,
9 IBranchMergeProposal,
10 IBranchMergeProposalGetter,
11 IBranchMergeProposalListingBatchNavigator,
12@@ -282,7 +283,7 @@
13 collection = collection.visibleByUser(self.user)
14 proposals = collection.getMergeProposals(
15 [BranchMergeProposalStatus.CODE_APPROVED,
16- BranchMergeProposalStatus.NEEDS_REVIEW,])
17+ BranchMergeProposalStatus.NEEDS_REVIEW, ])
18 return proposals
19
20 def _getReviewGroup(self, proposal, votes, reviewer):
21@@ -330,8 +331,8 @@
22 else:
23 return self.ARE_DOING
24 # Since team reviews are always pending, and we've eliminated
25- # the case where the reviewer is ther person, then if the reviewer
26- # is in the reviewer team, it is a can do.
27+ # the case where the reviewer is ther person, then if
28+ # the reviewer is in the reviewer team, it is a can do.
29 if reviewer.inTeam(vote.reviewer):
30 result = self.CAN_DO
31 return result
32@@ -419,7 +420,11 @@
33
34 def getProposals(self):
35 """See `ActiveReviewsView`."""
36- candidates = self.context.landing_candidates
37+ non_final = tuple(
38+ set(BranchMergeProposalStatus.items) -
39+ set(BRANCH_MERGE_PROPOSAL_FINAL_STATES))
40+ candidates = self.context.getMergeProposals(
41+ status=non_final, eager_load=True, visible_by_user=self.user)
42 return [proposal for proposal in candidates
43 if check_permission('launchpad.View', proposal)]
44
45
46=== modified file 'lib/lp/code/browser/tests/test_branchmergeproposallisting.py'
47--- lib/lp/code/browser/tests/test_branchmergeproposallisting.py 2011-08-12 11:37:08 +0000
48+++ lib/lp/code/browser/tests/test_branchmergeproposallisting.py 2011-08-26 06:36:42 +0000
49@@ -28,6 +28,7 @@
50 BrowserTestCase,
51 login,
52 login_person,
53+ person_logged_in,
54 TestCaseWithFactory,
55 )
56 from lp.testing.views import create_initialized_view
57@@ -359,3 +360,20 @@
58 self.assertEqual(
59 [bmp3, bmp2, bmp1],
60 [item.context for item in view.review_groups[view.OTHER]])
61+
62+
63+class ActiveReviewsWithPrivateBranches(TestCaseWithFactory):
64+ """Test the sorting of the active review groups."""
65+
66+ layer = DatabaseFunctionalLayer
67+
68+ def test_private_branch_owner(self):
69+ # Merge proposals against private branches are visible to
70+ # the branch owner.
71+ product = self.factory.makeProduct()
72+ branch = self.factory.makeBranch(private=True, product=product)
73+ with person_logged_in(removeSecurityProxy(branch).owner):
74+ mp = self.factory.makeBranchMergeProposal(target_branch=branch)
75+ view = create_initialized_view(
76+ branch, name='+activereviews', rootsite='code')
77+ self.assertEqual([mp], list(view.getProposals()))
78
79=== modified file 'lib/lp/code/interfaces/branch.py'
80--- lib/lp/code/interfaces/branch.py 2011-08-25 13:49:26 +0000
81+++ lib/lp/code/interfaces/branch.py 2011-08-26 06:36:42 +0000
82@@ -621,7 +621,7 @@
83 @export_read_operation()
84 @operation_for_version('beta')
85 def getMergeProposals(status=None, visible_by_user=None,
86- merged_revnos=None):
87+ merged_revnos=None, eager_load=False):
88 """Return matching BranchMergeProposals."""
89
90 def scheduleDiffUpdates():
91
92=== modified file 'lib/lp/code/interfaces/branchcollection.py'
93--- lib/lp/code/interfaces/branchcollection.py 2011-08-25 13:49:26 +0000
94+++ lib/lp/code/interfaces/branchcollection.py 2011-08-26 06:36:42 +0000
95@@ -70,7 +70,7 @@
96 """
97
98 def getMergeProposals(statuses=None, for_branches=None,
99- target_branch=None):
100+ target_branch=None, eager_load=False):
101 """Return a result set of merge proposals for the branches in this
102 collection.
103
104@@ -81,6 +81,8 @@
105 branch is one of the branches specified.
106 :param target_branch: If specified, only return merge proposals
107 that target the specified branch.
108+ :param eager_load: If True, preloads all the related information for
109+ merge proposals like PreviewDiffs and Branches.
110 """
111
112 def getMergeProposalsForPerson(person, status=None):
113
114=== modified file 'lib/lp/code/model/branch.py'
115--- lib/lp/code/model/branch.py 2011-08-25 13:49:26 +0000
116+++ lib/lp/code/model/branch.py 2011-08-26 06:36:42 +0000
117@@ -376,7 +376,7 @@
118 """ % sqlvalues(self, BRANCH_MERGE_PROPOSAL_FINAL_STATES))
119
120 def getMergeProposals(self, status=None, visible_by_user=None,
121- merged_revnos=None):
122+ merged_revnos=None, eager_load=False):
123 """See `IBranch`."""
124 if not status:
125 status = (
126@@ -386,7 +386,8 @@
127
128 collection = getUtility(IAllBranches).visibleByUser(visible_by_user)
129 return collection.getMergeProposals(
130- status, target_branch=self, merged_revnos=merged_revnos)
131+ status, target_branch=self, merged_revnos=merged_revnos,
132+ eager_load=eager_load)
133
134 def isBranchMergeable(self, target_branch):
135 """See `IBranch`."""
136
137=== modified file 'lib/lp/code/model/branchcollection.py'
138--- lib/lp/code/model/branchcollection.py 2011-08-25 13:49:26 +0000
139+++ lib/lp/code/model/branchcollection.py 2011-08-26 06:36:42 +0000
140@@ -63,12 +63,17 @@
141 from lp.code.model.codeimport import CodeImport
142 from lp.code.model.codereviewcomment import CodeReviewComment
143 from lp.code.model.codereviewvote import CodeReviewVoteReference
144+from lp.code.model.diff import (
145+ Diff,
146+ PreviewDiff,
147+ )
148 from lp.code.model.seriessourcepackagebranch import SeriesSourcePackageBranch
149 from lp.registry.model.distribution import Distribution
150 from lp.registry.model.distroseries import DistroSeries
151 from lp.registry.model.person import (
152 Owner,
153 Person,
154+ ValidPersonCache,
155 )
156 from lp.registry.model.product import Product
157 from lp.registry.model.sourcepackagename import SourcePackageName
158@@ -152,9 +157,9 @@
159 exclude_from_search=None, symmetric=True):
160 """Return a subset of this collection, filtered by 'expressions'.
161
162- :param symmetric: If True this filter will apply to both sides of merge
163- proposal lookups and any other lookups that join Branch back onto
164- Branch.
165+ :param symmetric: If True this filter will apply to both sides
166+ of merge proposal lookups and any other lookups that join
167+ Branch back onto Branch.
168 """
169 # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
170 # for explicit 'tables' by harnessing Storm's table inference system.
171@@ -175,7 +180,8 @@
172 if table is not None:
173 asymmetric_tables[table] = join
174 symmetric_expr = list(self._branch_filter_expressions)
175- asymmetric_expr = self._asymmetric_filter_expressions + expressions
176+ asymmetric_expr = (
177+ self._asymmetric_filter_expressions + expressions)
178 if exclude_from_search is None:
179 exclude_from_search = []
180 return self.__class__(
181@@ -205,12 +211,48 @@
182 def _getCandidateBranchesWith(self):
183 """Return WITH clauses defining candidate branches.
184
185- These are defined in terms of scope_branches which should be separately
186- calculated.
187+ These are defined in terms of scope_branches which should be
188+ separately calculated.
189 """
190 return [
191 With("candidate_branches", SQL("SELECT id from scope_branches"))]
192
193+ def _preloadDataForBranches(self, branches):
194+ """Preload branches cached associated product series and
195+ suite source packages."""
196+ caches = dict((branch.id, get_property_cache(branch))
197+ for branch in branches)
198+ branch_ids = caches.keys()
199+ for cache in caches.values():
200+ if not safe_hasattr(cache, '_associatedProductSeries'):
201+ cache._associatedProductSeries = []
202+ if not safe_hasattr(cache, '_associatedSuiteSourcePackages'):
203+ cache._associatedSuiteSourcePackages = []
204+ if not safe_hasattr(cache, 'code_import'):
205+ cache.code_import = None
206+ # associatedProductSeries
207+ # Imported here to avoid circular import.
208+ from lp.registry.model.productseries import ProductSeries
209+ for productseries in self.store.find(
210+ ProductSeries,
211+ ProductSeries.branchID.is_in(branch_ids)):
212+ cache = caches[productseries.branchID]
213+ cache._associatedProductSeries.append(productseries)
214+ # associatedSuiteSourcePackages
215+ series_set = getUtility(IFindOfficialBranchLinks)
216+ # Order by the pocket to get the release one first. If changing
217+ # this be sure to also change BranchCollection.getBranches.
218+ links = series_set.findForBranches(branches).order_by(
219+ SeriesSourcePackageBranch.pocket)
220+ for link in links:
221+ cache = caches[link.branchID]
222+ cache._associatedSuiteSourcePackages.append(
223+ link.suite_sourcepackage)
224+ for code_import in IStore(CodeImport).find(
225+ CodeImport, CodeImport.branchID.is_in(branch_ids)):
226+ cache = caches[code_import.branchID]
227+ cache.code_import = code_import
228+
229 def getBranches(self, eager_load=False):
230 """See `IBranchCollection`."""
231 all_tables = set(
232@@ -225,53 +267,23 @@
233 branch_ids = set(branch.id for branch in rows)
234 if not branch_ids:
235 return
236- branches = dict((branch.id, branch) for branch in rows)
237- caches = dict((branch.id, get_property_cache(branch))
238- for branch in rows)
239- for cache in caches.values():
240- if not safe_hasattr(cache, '_associatedProductSeries'):
241- cache._associatedProductSeries = []
242- if not safe_hasattr(cache, '_associatedSuiteSourcePackages'):
243- cache._associatedSuiteSourcePackages = []
244- if not safe_hasattr(cache, 'code_import'):
245- cache.code_import = None
246- # associatedProductSeries
247- # Imported here to avoid circular import.
248- from lp.registry.model.productseries import ProductSeries
249- for productseries in self.store.find(
250- ProductSeries,
251- ProductSeries.branchID.is_in(branch_ids)):
252- cache = caches[productseries.branchID]
253- cache._associatedProductSeries.append(productseries)
254- # associatedSuiteSourcePackages
255- series_set = getUtility(IFindOfficialBranchLinks)
256- # Order by the pocket to get the release one first. If changing
257- # this be sure to also change BranchCollection.getBranches.
258- links = series_set.findForBranches(rows).order_by(
259- SeriesSourcePackageBranch.pocket)
260- for link in links:
261- cache = caches[link.branchID]
262- cache._associatedSuiteSourcePackages.append(
263- link.suite_sourcepackage)
264+ self._preloadDataForBranches(rows)
265 load_related(Product, rows, ['productID'])
266 # So far have only needed the persons for their canonical_url - no
267 # need for validity etc in the /branches API call.
268 load_related(Person, rows,
269 ['ownerID', 'registrantID', 'reviewerID'])
270- for code_import in IStore(CodeImport).find(
271- CodeImport, CodeImport.branchID.is_in(branch_ids)):
272- cache = caches[code_import.branchID]
273- cache.code_import = code_import
274 load_referencing(BugBranch, rows, ['branchID'])
275 return DecoratedResultSet(resultset, pre_iter_hook=do_eager_load)
276
277 def getMergeProposals(self, statuses=None, for_branches=None,
278- target_branch=None, merged_revnos=None):
279+ target_branch=None, merged_revnos=None,
280+ eager_load=False):
281 """See `IBranchCollection`."""
282 if (self._asymmetric_filter_expressions or for_branches or
283 target_branch or merged_revnos):
284 return self._naiveGetMergeProposals(statuses, for_branches,
285- target_branch, merged_revnos)
286+ target_branch, merged_revnos, eager_load)
287 else:
288 # When examining merge proposals in a scope, this is a moderately
289 # effective set of constrained queries. It is not effective when
290@@ -279,16 +291,47 @@
291 return self._scopedGetMergeProposals(statuses)
292
293 def _naiveGetMergeProposals(self, statuses=None, for_branches=None,
294- target_branch=None, merged_revnos=None):
295+ target_branch=None, merged_revnos=None, eager_load=False):
296+
297+ def do_eager_load(rows):
298+ branch_ids = set()
299+ person_ids = set()
300+ diff_ids = set()
301+ for mp in rows:
302+ branch_ids.add(mp.target_branchID)
303+ branch_ids.add(mp.source_branchID)
304+ person_ids.add(mp.registrantID)
305+ person_ids.add(mp.merge_reporterID)
306+ diff_ids.add(mp.preview_diff_id)
307+ if not branch_ids:
308+ return
309+
310+ # Pre-load Person and ValidPersonCache.
311+ list(self.store.find(
312+ (Person, ValidPersonCache),
313+ ValidPersonCache.id == Person.id,
314+ Person.id.is_in(person_ids),
315+ ))
316+
317+ # Pre-load PreviewDiffs and Diffs.
318+ list(self.store.find(
319+ (PreviewDiff, Diff),
320+ PreviewDiff.id.is_in(diff_ids),
321+ Diff.id == PreviewDiff.diff_id))
322+
323+ branches = set(
324+ self.store.find(Branch, Branch.id.is_in(branch_ids)))
325+ self._preloadDataForBranches(branches)
326+
327 Target = ClassAlias(Branch, "target")
328 extra_tables = list(set(
329 self._tables.values() + self._asymmetric_tables.values()))
330 tables = [Branch] + extra_tables + [
331 Join(BranchMergeProposal, And(
332- Branch.id==BranchMergeProposal.source_branchID,
333+ Branch.id == BranchMergeProposal.source_branchID,
334 *(self._branch_filter_expressions +
335 self._asymmetric_filter_expressions))),
336- Join(Target, Target.id==BranchMergeProposal.target_branchID)
337+ Join(Target, Target.id == BranchMergeProposal.target_branchID),
338 ]
339 expressions = self._getBranchVisibilityExpression()
340 expressions.extend(self._getBranchVisibilityExpression(Target))
341@@ -305,7 +348,12 @@
342 if statuses is not None:
343 expressions.append(
344 BranchMergeProposal.queue_status.is_in(statuses))
345- return self.store.using(*tables).find(BranchMergeProposal, *expressions)
346+ resultset = self.store.using(*tables).find(
347+ BranchMergeProposal, *expressions)
348+ if not eager_load:
349+ return resultset
350+ else:
351+ return DecoratedResultSet(resultset, pre_iter_hook=do_eager_load)
352
353 def _scopedGetMergeProposals(self, statuses):
354 scope_tables = [Branch] + self._tables.values()
355@@ -513,7 +561,7 @@
356 """See `IBranchCollection`."""
357 subquery = Select(
358 TeamParticipation.teamID,
359- where=TeamParticipation.personID==person.id)
360+ where=TeamParticipation.personID == person.id)
361 filter = [In(Branch.ownerID, subquery)]
362
363 return self._filterBy(filter, symmetric=False)
364@@ -666,8 +714,8 @@
365 def _getCandidateBranchesWith(self):
366 """Return WITH clauses defining candidate branches.
367
368- These are defined in terms of scope_branches which should be separately
369- calculated.
370+ These are defined in terms of scope_branches which should be
371+ separately calculated.
372 """
373 # Anonymous users get public branches only.
374 return [
375@@ -694,9 +742,9 @@
376 exclude_from_search=None, symmetric=True):
377 """Return a subset of this collection, filtered by 'expressions'.
378
379- :param symmetric: If True this filter will apply to both sides of merge
380- proposal lookups and any other lookups that join Branch back onto
381- Branch.
382+ :param symmetric: If True this filter will apply to both sides
383+ of merge proposal lookups and any other lookups that join
384+ Branch back onto Branch.
385 """
386 # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
387 # for explicit 'tables' by harnessing Storm's table inference system.
388@@ -717,7 +765,8 @@
389 if table is not None:
390 asymmetric_tables[table] = join
391 symmetric_expr = list(self._branch_filter_expressions)
392- asymmetric_expr = self._asymmetric_filter_expressions + expressions
393+ asymmetric_expr = (
394+ self._asymmetric_filter_expressions + expressions)
395 if exclude_from_search is None:
396 exclude_from_search = []
397 return self.__class__(
398@@ -781,8 +830,8 @@
399 def _getCandidateBranchesWith(self):
400 """Return WITH clauses defining candidate branches.
401
402- These are defined in terms of scope_branches which should be separately
403- calculated.
404+ These are defined in terms of scope_branches which should be
405+ separately calculated.
406 """
407 person = self._user
408 if person is None:
409@@ -796,10 +845,11 @@
410 TeamParticipation.personID == person.id)._get_select()),
411 With("private_branches", SQL("""
412 SELECT scope_branches.id FROM scope_branches WHERE
413- scope_branches.private AND ((scope_branches.owner in (select team from teams) OR
414- EXISTS(SELECT true from BranchSubscription, teams WHERE
415- branchsubscription.branch = scope_branches.id AND
416- branchsubscription.person = teams.team)))""")),
417+ scope_branches.private AND (
418+ (scope_branches.owner in (select team from teams) OR
419+ EXISTS(SELECT true from BranchSubscription, teams WHERE
420+ branchsubscription.branch = scope_branches.id AND
421+ branchsubscription.person = teams.team)))""")),
422 With("candidate_branches", SQL("""
423 (SELECT id FROM private_branches) UNION
424 (select id FROM scope_branches WHERE not private)"""))