Merge lp:~stevenk/launchpad/distroseries-spec-preload into lp:launchpad

Proposed by Steve Kowalik
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 16732
Proposed branch: lp:~stevenk/launchpad/distroseries-spec-preload
Merge into: lp:launchpad
Diff against target: 1014 lines (+203/-159)
22 files modified
lib/lp/_schema_circular_imports.py (+1/-1)
lib/lp/blueprints/browser/specificationtarget.py (+1/-1)
lib/lp/blueprints/interfaces/specificationtarget.py (+12/-5)
lib/lp/blueprints/mail/notifications.py (+0/-1)
lib/lp/blueprints/model/specification.py (+28/-19)
lib/lp/blueprints/model/specificationsearch.py (+36/-20)
lib/lp/blueprints/model/sprint.py (+5/-6)
lib/lp/blueprints/model/tests/test_specification.py (+21/-20)
lib/lp/blueprints/tests/test_hasspecifications.py (+25/-45)
lib/lp/registry/configure.zcml (+2/-1)
lib/lp/registry/doc/distribution.txt (+1/-1)
lib/lp/registry/doc/projectgroup.txt (+4/-4)
lib/lp/registry/model/distribution.py (+5/-2)
lib/lp/registry/model/distroseries.py (+5/-3)
lib/lp/registry/model/person.py (+5/-2)
lib/lp/registry/model/product.py (+5/-3)
lib/lp/registry/model/productseries.py (+5/-3)
lib/lp/registry/model/projectgroup.py (+9/-6)
lib/lp/registry/tests/test_distroseries.py (+22/-1)
lib/lp/registry/tests/test_product.py (+2/-2)
lib/lp/registry/tests/test_productseries.py (+7/-7)
lib/lp/soyuz/browser/tests/test_archive_webservice.py (+2/-6)
To merge this branch: bzr merge lp:~stevenk/launchpad/distroseries-spec-preload
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+180046@code.launchpad.net

Commit message

Rewrite preloading for searching specifications, allowing callsites to specify which of people, branches and workitems they need preloaded; rename IHasSpecification._valid_specifications has been renamed to valid_specifications, and create a api_valid_specifications property to ensure API compatibility.

Description of the change

Rewrite preloading for searching specifications, allowing callsites to specify which of people, branches and workitems they need preloaded, modeled off IPersonSet.getPrecachedPersonsFromIDs().

IHasSpecification._valid_specifications has been renamed to valid_specifications and is no longer a property, and to ensure API compatability, api_valid_specifications is a property which calls into valid_specifications.

I've also re-enabled a disabled test that now works due to the refactoring work that went into search_specifications().

I have more than enough LoC credit to take the +50 hit for this branch.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

125 - for work_item in self.work_items:
126 + for work_item in self._work_items:
127 if (work_item.title not in title_counts or

Can't this use the cached version?

267 + for specid in work_items_by_spec.keys():
268 + work_items_by_spec[specid].sort(key=lambda wi: wi.sequence)
269 + for workitem in work_items_by_spec[specid]:
270 + get_property_cache(workitem.specification).work_items.append(
271 + workitem)

This seems needlessly circuitous. get_property_cache(spec).work_items = list(sorted(work_items_by_spec[specid], key=lambda wi: wi.sequence))?

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/_schema_circular_imports.py'
2--- lib/lp/_schema_circular_imports.py 2013-05-01 18:14:22 +0000
3+++ lib/lp/_schema_circular_imports.py 2013-08-20 07:26:08 +0000
4@@ -737,7 +737,7 @@
5 patch_collection_property(
6 IHasSpecifications, 'visible_specifications', ISpecification)
7 patch_collection_property(
8- IHasSpecifications, '_valid_specifications', ISpecification)
9+ IHasSpecifications, 'api_valid_specifications', ISpecification)
10
11
12 ###
13
14=== modified file 'lib/lp/blueprints/browser/specificationtarget.py'
15--- lib/lp/blueprints/browser/specificationtarget.py 2013-04-10 09:36:25 +0000
16+++ lib/lp/blueprints/browser/specificationtarget.py 2013-08-20 07:26:08 +0000
17@@ -406,7 +406,7 @@
18 """
19 return self.context.specifications(self.user,
20 sort=SpecificationSort.DATE, quantity=quantity,
21- prejoin_people=False)
22+ need_people=False, need_branches=False, need_workitems=False)
23
24
25 class SpecificationAssignmentsView(HasSpecificationsView):
26
27=== modified file 'lib/lp/blueprints/interfaces/specificationtarget.py'
28--- lib/lp/blueprints/interfaces/specificationtarget.py 2013-04-03 03:09:04 +0000
29+++ lib/lp/blueprints/interfaces/specificationtarget.py 2013-08-20 07:26:08 +0000
30@@ -47,7 +47,7 @@
31 'approval or completion, for this object.'))),
32 exported_as="all_specifications", as_of="devel")
33
34- _valid_specifications = exported(doNotSnapshot(
35+ api_valid_specifications = exported(doNotSnapshot(
36 CollectionField(
37 title=_("Valid specifications"),
38 value_type=Reference(schema=Interface), # ISpecification, really.
39@@ -59,7 +59,8 @@
40 exported_as="valid_specifications", as_of="devel")
41
42 def specifications(user, quantity=None, sort=None, filter=None,
43- prejoin_people=True):
44+ need_people=True, need_branches=True,
45+ need_workitems=False):
46 """Specifications for this target.
47
48 The user specifies which user to use for calculation of visibility.
49@@ -73,9 +74,15 @@
50 it will show all specs, in others, all approved specs, and in
51 others, all incomplete specs.
52
53- If prejoin_people=False is specified, then the assignee, drafter
54- and approver will not be prejoined. This can be used in
55- situations in which these are not rendered.
56+ If need_people is True, then the assignee, drafter and approver will
57+ be preloaded, if need_branches is True, linked_branches will be
58+ preloaded, and if need_workitems is True, work_items will be preloaded.
59+ """
60+
61+ def valid_specifications(**kwargs):
62+ """Valid specifications for this target.
63+
64+ Any kwargs are passed to specifications.
65 """
66
67
68
69=== modified file 'lib/lp/blueprints/mail/notifications.py'
70--- lib/lp/blueprints/mail/notifications.py 2013-03-05 08:24:25 +0000
71+++ lib/lp/blueprints/mail/notifications.py 2013-08-20 07:26:08 +0000
72@@ -92,7 +92,6 @@
73 workitems_delta['old'], workitems_delta['new'], 72)
74 info_lines.append('Work items changed:')
75 info_lines.append(workitems_diff)
76-
77 if not info_lines:
78 # The specification was modified, but we don't yet support
79 # sending notification for the change.
80
81=== modified file 'lib/lp/blueprints/model/specification.py'
82--- lib/lp/blueprints/model/specification.py 2013-06-27 15:11:38 +0000
83+++ lib/lp/blueprints/model/specification.py 2013-08-20 07:26:08 +0000
84@@ -335,7 +335,7 @@
85 else:
86 return "Work items for %s:" % milestone.name
87
88- if self.work_items.count() == 0:
89+ if len(self.work_items) == 0:
90 return ''
91 milestone = self.work_items[0].milestone
92 # Start by appending a header for the milestone of the first work
93@@ -353,9 +353,9 @@
94 else:
95 assignee_part = ""
96 # work_items are ordered by sequence
97- workitems_lines.append("%s%s: %s" % (assignee_part,
98- work_item.title,
99- work_item.status.name))
100+ workitems_lines.append(
101+ "%s%s: %s" % (
102+ assignee_part, work_item.title, work_item.status.name))
103 return "\n".join(workitems_lines)
104
105 @property
106@@ -377,9 +377,13 @@
107 title=title, status=status, specification=self, assignee=assignee,
108 milestone=milestone, sequence=sequence)
109
110- @property
111+ @cachedproperty
112 def work_items(self):
113 """See ISpecification."""
114+ return list(self._work_items)
115+
116+ @property
117+ def _work_items(self):
118 return Store.of(self).find(
119 SpecificationWorkItem, specification=self,
120 deleted=False).order_by("sequence")
121@@ -395,7 +399,7 @@
122 """
123 title_counts = self._list_to_dict_of_frequency(titles)
124
125- for work_item in self.work_items:
126+ for work_item in self._work_items:
127 if (work_item.title not in title_counts or
128 title_counts[work_item.title] == 0):
129 work_item.deleted = True
130@@ -418,9 +422,7 @@
131 # deleted.
132 self._deleteWorkItemsNotMatching(
133 [wi['title'] for wi in new_work_items])
134- work_items = Store.of(self).find(
135- SpecificationWorkItem, specification=self, deleted=False)
136- work_items = list(work_items.order_by("sequence"))
137+ work_items = self._work_items
138 # At this point the list of new_work_items is necessarily the same
139 # size (or longer) than the list of existing ones, so we can just
140 # iterate over it updating the existing items and creating any new
141@@ -456,6 +458,7 @@
142 for sequence, item in to_insert:
143 self.newWorkItem(item['title'], sequence, item['status'],
144 item['assignee'], item['milestone'])
145+ del get_property_cache(self).work_items
146
147 def setTarget(self, target):
148 """See ISpecification."""
149@@ -683,9 +686,7 @@
150 delta.recordNewAndOld(("name", "priority", "definition_status",
151 "target", "approver", "assignee", "drafter",
152 "whiteboard", "workitems_text"))
153- delta.recordListAddedAndRemoved("bugs",
154- "bugs_linked",
155- "bugs_unlinked")
156+ delta.recordListAddedAndRemoved("bugs", "bugs_linked", "bugs_unlinked")
157
158 if delta.changes:
159 changes = delta.changes
160@@ -952,9 +953,10 @@
161 """
162
163 def specifications(self, user, sort=None, quantity=None, filter=None,
164- prejoin_people=True):
165+ need_people=True, need_branches=True,
166+ need_workitems=False):
167 """See IHasSpecifications."""
168- # this should be implemented by the actual context class
169+ # This should be implemented by the actual context class.
170 raise NotImplementedError
171
172 def _specification_sort(self, sort):
173@@ -976,11 +978,16 @@
174 user = getUtility(ILaunchBag).user
175 return self.specifications(user, filter=[SpecificationFilter.ALL])
176
177- @property
178- def _valid_specifications(self):
179+ def valid_specifications(self, **kwargs):
180 """See IHasSpecifications."""
181 user = getUtility(ILaunchBag).user
182- return self.specifications(user, filter=[SpecificationFilter.VALID])
183+ return self.specifications(
184+ user, filter=[SpecificationFilter.VALID], **kwargs)
185+
186+ @property
187+ def api_valid_specifications(self):
188+ return self.valid_specifications(
189+ need_people=True, need_branches=True, need_workitems=True)
190
191 def specificationCount(self, user):
192 """See IHasSpecifications."""
193@@ -1017,11 +1024,13 @@
194 return cur.fetchall()
195
196 def specifications(self, user, sort=None, quantity=None, filter=None,
197- prejoin_people=True):
198+ need_people=True, need_branches=True,
199+ need_workitems=False):
200 from lp.blueprints.model.specificationsearch import (
201 search_specifications)
202 return search_specifications(
203- self, [], user, sort, quantity, filter, prejoin_people)
204+ self, [], user, sort, quantity, filter, need_people=need_people,
205+ need_branches=need_branches, need_workitems=need_workitems)
206
207 def getByURL(self, url):
208 """See ISpecificationSet."""
209
210=== modified file 'lib/lp/blueprints/model/specificationsearch.py'
211--- lib/lp/blueprints/model/specificationsearch.py 2013-06-20 05:50:00 +0000
212+++ lib/lp/blueprints/model/specificationsearch.py 2013-08-20 07:26:08 +0000
213@@ -11,6 +11,8 @@
214 'search_specifications',
215 ]
216
217+from collections import defaultdict
218+
219 from storm.expr import (
220 And,
221 Coalesce,
222@@ -36,6 +38,7 @@
223 )
224 from lp.blueprints.model.specification import Specification
225 from lp.blueprints.model.specificationbranch import SpecificationBranch
226+from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
227 from lp.registry.interfaces.distribution import IDistribution
228 from lp.registry.interfaces.distroseries import IDistroSeries
229 from lp.registry.interfaces.person import IPersonSet
230@@ -56,8 +59,9 @@
231
232
233 def search_specifications(context, base_clauses, user, sort=None,
234- quantity=None, spec_filter=None, prejoin_people=True,
235- tables=[], default_acceptance=False):
236+ quantity=None, spec_filter=None, tables=[],
237+ default_acceptance=False, need_people=True,
238+ need_branches=True, need_workitems=False):
239 store = IStore(Specification)
240 if not default_acceptance:
241 default = SpecificationFilter.INCOMPLETE
242@@ -106,12 +110,39 @@
243 order = [sort]
244 # Set the _known_viewers property for each specification, as well as
245 # preloading the objects involved, if asked.
246+ def preload_hook(rows):
247+ person_ids = set()
248+ work_items_by_spec = defaultdict(list)
249+ for spec in rows:
250+ if need_people:
251+ person_ids |= set(
252+ [spec._assigneeID, spec._approverID, spec._drafterID])
253+ if need_branches:
254+ get_property_cache(spec).linked_branches = []
255+ if need_workitems:
256+ work_items = load_referencing(
257+ SpecificationWorkItem, rows, ['specification_id'])
258+ for workitem in work_items:
259+ person_ids.add(workitem.assignee_id)
260+ work_items_by_spec[workitem.specification_id].append(workitem)
261+ person_ids -= set([None])
262+ if need_people:
263+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
264+ person_ids, need_validity=True))
265+ if need_workitems:
266+ for spec in rows:
267+ get_property_cache(spec).work_items = sorted(
268+ work_items_by_spec[spec.id], key=lambda wi: wi.sequence)
269+ if need_branches:
270+ spec_branches = load_referencing(
271+ SpecificationBranch, rows, ['specificationID'])
272+ for sbranch in spec_branches:
273+ spec_cache = get_property_cache(sbranch.specification)
274+ spec_cache.linked_branches.append(sbranch)
275+
276 decorators = []
277- preload_hook = None
278 if user is not None and not IPersonRoles(user).in_admin:
279 decorators.append(_make_cache_user_can_view_spec(user))
280- if prejoin_people:
281- preload_hook = _preload_specifications_related_objects
282 results = store.using(*tables).find(
283 Specification, *clauses).order_by(*order).config(limit=quantity)
284 return DecoratedResultSet(
285@@ -227,21 +258,6 @@
286 return cache_user_can_view_spec
287
288
289-def _preload_specifications_related_objects(rows):
290- person_ids = set()
291- for spec in rows:
292- person_ids |= set(
293- [spec._assigneeID, spec._approverID, spec._drafterID])
294- get_property_cache(spec).linked_branches = []
295- person_ids -= set([None])
296- list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
297- person_ids, need_validity=True))
298- spec_branches = load_referencing(
299- SpecificationBranch, rows, ['specificationID'])
300- for sbranch in spec_branches:
301- get_property_cache(sbranch.specification).linked_branches.append(
302- sbranch)
303-
304
305 def get_specification_started_clause():
306 return Or(Not(Specification.implementation_status.is_in([
307
308=== modified file 'lib/lp/blueprints/model/sprint.py'
309--- lib/lp/blueprints/model/sprint.py 2013-01-22 05:07:31 +0000
310+++ lib/lp/blueprints/model/sprint.py 2013-08-20 07:26:08 +0000
311@@ -163,18 +163,17 @@
312 return self.specifications(user, filter=[SpecificationFilter.ALL])
313
314 def specifications(self, user, sort=None, quantity=None, filter=None,
315- prejoin_people=False):
316+ need_people=False, need_branches=False,
317+ need_workitems=False):
318 """See IHasSpecifications."""
319- # prejoin_people is provided only for interface compatibility and
320- # prejoin_people=True is not implemented.
321- assert not prejoin_people
322+ # need_* is provided only for interface compatibility and
323+ # need_*=True is not implemented.
324 if filter is None:
325 filter = set([SpecificationFilter.ACCEPTED])
326 tables, query = self.spec_filter_clause(user, filter)
327 # import here to avoid circular deps
328 from lp.blueprints.model.specification import Specification
329- store = Store.of(self)
330- results = store.using(*tables).find(Specification, *query)
331+ results = Store.of(self).using(*tables).find(Specification, *query)
332 if sort == SpecificationSort.DATE:
333 order = (Desc(SprintSpecification.date_created), Specification.id)
334 distinct = [SprintSpecification.date_created, Specification.id]
335
336=== modified file 'lib/lp/blueprints/model/tests/test_specification.py'
337--- lib/lp/blueprints/model/tests/test_specification.py 2013-01-25 03:30:08 +0000
338+++ lib/lp/blueprints/model/tests/test_specification.py 2013-08-20 07:26:08 +0000
339@@ -33,6 +33,7 @@
340 )
341 from lp.registry.errors import CannotChangeInformationType
342 from lp.registry.model.milestone import Milestone
343+from lp.services.propertycache import get_property_cache
344 from lp.services.mail import stub
345 from lp.services.webapp import canonical_url
346 from lp.testing import (
347@@ -222,10 +223,9 @@
348 stub.test_emails = []
349 spec = self.factory.makeSpecification()
350 old_spec = Snapshot(spec, providing=providedBy(spec))
351- status = SpecificationWorkItemStatus.TODO
352 new_work_item = {
353 'title': u'A work item',
354- 'status': status,
355+ 'status': SpecificationWorkItemStatus.TODO,
356 'assignee': None,
357 'milestone': None,
358 'sequence': 0
359@@ -233,10 +233,9 @@
360
361 login_person(spec.owner)
362 spec.updateWorkItems([new_work_item])
363- # In production this notification is fired by lazr.restful for changes
364- # in the specification form and notify(ObjectModifiedEvent(...)) for
365- # changes in the +workitems form. We need to do it ourselves in this
366- # test.
367+ # For API requests, lazr.restful does the notify() call, for this test
368+ # we need to call ourselves.
369+ transaction.commit()
370 notify(ObjectModifiedEvent(
371 spec, old_spec, edited_fields=['workitems_text']))
372 transaction.commit()
373@@ -500,7 +499,7 @@
374 spec.updateWorkItems([work_item1_data, work_item2_data])
375
376 # And after calling updateWorkItems() we have 2 work items.
377- self.assertEqual(2, spec.work_items.count())
378+ self.assertEqual(2, len(spec.work_items))
379
380 # The data dicts we pass to updateWorkItems() have no sequence because
381 # that's taken from their position on the list, so we update our data
382@@ -523,7 +522,7 @@
383 # Create two work-items in our database.
384 wi1_data = self._createWorkItemAndReturnDataDict(spec)
385 wi2_data = self._createWorkItemAndReturnDataDict(spec)
386- self.assertEqual(2, spec.work_items.count())
387+ self.assertEqual(2, len(spec.work_items))
388
389 # These are the work items we'll be inserting.
390 new_wi1_data = dict(
391@@ -546,7 +545,7 @@
392 new_wi2_data['sequence'] = 2
393 wi2_data['sequence'] = 3
394
395- self.assertEqual(4, spec.work_items.count())
396+ self.assertEqual(4, len(spec.work_items))
397 for data, obj in zip(work_items, list(spec.work_items)):
398 self.assertThat(obj, MatchesStructure.byEquality(**data))
399
400@@ -571,8 +570,8 @@
401 new_wi2_data['sequence'] = 2
402 wi2_data['sequence'] = 3
403
404- self.assertEqual(4, spec.work_items.count())
405- for data, obj in zip(work_items, list(spec.work_items)):
406+ self.assertEqual(4, len(spec.work_items))
407+ for data, obj in zip(work_items, spec.work_items):
408 self.assertThat(obj, MatchesStructure.byEquality(**data))
409
410 return spec, work_items
411@@ -586,7 +585,7 @@
412 work_items.append(new_wi3_data)
413 spec.updateWorkItems(work_items)
414
415- self.assertEqual(5, spec.work_items.count())
416+ self.assertEqual(5, len(spec.work_items))
417 for data, obj in zip(work_items, list(spec.work_items)):
418 self.assertThat(obj, MatchesStructure.byEquality(**data))
419
420@@ -597,7 +596,7 @@
421 work_items.pop()
422 spec.updateWorkItems(work_items)
423
424- self.assertEqual(3, spec.work_items.count())
425+ self.assertEqual(3, len(spec.work_items))
426 for data, obj in zip(work_items, list(spec.work_items)):
427 self.assertThat(obj, MatchesStructure.byEquality(**data))
428
429@@ -606,7 +605,7 @@
430 login_person(spec.owner)
431 # Create a work-item in our database.
432 wi_data = self._createWorkItemAndReturnDataDict(spec)
433- self.assertEqual(1, spec.work_items.count())
434+ self.assertEqual(1, len(spec.work_items))
435
436 # This time we're only changing the existing work item; we'll change
437 # its assignee and status.
438@@ -614,29 +613,29 @@
439 assignee=spec.owner))
440 spec.updateWorkItems([wi_data])
441
442- self.assertEqual(1, spec.work_items.count())
443+ self.assertEqual(1, len(spec.work_items))
444 self.assertThat(
445 spec.work_items[0], MatchesStructure.byEquality(**wi_data))
446
447 def test_updateWorkItems_deletes_all_if_given_empty_list(self):
448 work_item = self.factory.makeSpecificationWorkItem()
449 spec = work_item.specification
450- self.assertEqual(1, spec.work_items.count())
451+ self.assertEqual(1, len(spec.work_items))
452 spec.updateWorkItems([])
453- self.assertEqual(0, spec.work_items.count())
454+ self.assertEqual(0, len(spec.work_items))
455
456 def test_updateWorkItems_marks_removed_ones_as_deleted(self):
457 spec = self.factory.makeSpecification()
458 self._createWorkItemAndReturnDataDict(spec)
459 wi2_data = self._createWorkItemAndReturnDataDict(spec)
460- self.assertEqual(2, spec.work_items.count())
461+ self.assertEqual(2, len(spec.work_items))
462 login_person(spec.owner)
463
464 # We have two work items in the DB but now we want to update them to
465 # keep just the second one. The first will be deleted and the sequence
466 # of the second will be changed.
467 spec.updateWorkItems([wi2_data])
468- self.assertEqual(1, spec.work_items.count())
469+ self.assertEqual(1, len(spec.work_items))
470 wi2_data['sequence'] = 0
471 self.assertThat(
472 spec.work_items[0], MatchesStructure.byEquality(**wi2_data))
473@@ -648,12 +647,14 @@
474 Return a dict with the title, status, assignee, milestone and sequence
475 attributes of the spec.
476 """
477- if spec.work_items.count() == 0:
478+ del get_property_cache(spec).work_items
479+ if len(spec.work_items) == 0:
480 sequence = 0
481 else:
482 sequence = max(wi.sequence for wi in spec.work_items) + 1
483 wi = self.factory.makeSpecificationWorkItem(
484 specification=spec, sequence=sequence)
485+ del get_property_cache(spec).work_items
486 return dict(
487 title=wi.title, status=wi.status, assignee=wi.assignee,
488 milestone=wi.milestone, sequence=sequence)
489
490=== modified file 'lib/lp/blueprints/tests/test_hasspecifications.py'
491--- lib/lp/blueprints/tests/test_hasspecifications.py 2013-04-03 03:09:04 +0000
492+++ lib/lp/blueprints/tests/test_hasspecifications.py 2013-08-20 07:26:08 +0000
493@@ -34,61 +34,48 @@
494 product=product, name="spec2",
495 status=SpecificationDefinitionStatus.OBSOLETE)
496 self.assertNamesOfSpecificationsAre(
497- ["spec1"], product._valid_specifications)
498+ ["spec1"], product.valid_specifications())
499
500 def test_distribution_all_specifications(self):
501 distribution = self.factory.makeDistribution()
502- self.factory.makeSpecification(
503- distribution=distribution, name="spec1")
504- self.factory.makeSpecification(
505- distribution=distribution, name="spec2")
506+ self.factory.makeSpecification(distribution=distribution, name="spec1")
507+ self.factory.makeSpecification(distribution=distribution, name="spec2")
508 self.assertNamesOfSpecificationsAre(
509 ["spec1", "spec2"], distribution.visible_specifications)
510
511 def test_distribution_valid_specifications(self):
512 distribution = self.factory.makeDistribution()
513- self.factory.makeSpecification(
514- distribution=distribution, name="spec1")
515+ self.factory.makeSpecification(distribution=distribution, name="spec1")
516 self.factory.makeSpecification(
517 distribution=distribution, name="spec2",
518 status=SpecificationDefinitionStatus.OBSOLETE)
519 self.assertNamesOfSpecificationsAre(
520- ["spec1"], distribution._valid_specifications)
521+ ["spec1"], distribution.valid_specifications())
522
523 def test_distroseries_all_specifications(self):
524 distroseries = self.factory.makeDistroSeries(name='maudlin')
525 distribution = distroseries.distribution
526 self.factory.makeSpecification(
527- distribution=distribution, name="spec1",
528- goal=distroseries)
529- self.factory.makeSpecification(
530- distribution=distribution, name="spec2",
531- goal=distroseries)
532- self.factory.makeSpecification(
533- distribution=distribution, name="spec3")
534+ distribution=distribution, name="spec1", goal=distroseries)
535+ self.factory.makeSpecification(
536+ distribution=distribution, name="spec2", goal=distroseries)
537+ self.factory.makeSpecification(distribution=distribution, name="spec3")
538 self.assertNamesOfSpecificationsAre(
539 ["spec1", "spec2"], distroseries.visible_specifications)
540
541- # XXX: salgado, 2010-11-25, bug=681432: Test disabled because
542- # DistroSeries._valid_specifications is broken.
543- def disabled_test_distroseries_valid_specifications(self):
544+ def test_distroseries_valid_specifications(self):
545 distroseries = self.factory.makeDistroSeries(name='maudlin')
546 distribution = distroseries.distribution
547 self.factory.makeSpecification(
548- distribution=distribution, name="spec1",
549- goal=distroseries)
550- self.factory.makeSpecification(
551- distribution=distribution, name="spec2",
552- goal=distroseries)
553- self.factory.makeSpecification(
554- distribution=distribution, name="spec3",
555- goal=distroseries,
556+ distribution=distribution, name="spec1", goal=distroseries)
557+ self.factory.makeSpecification(
558+ distribution=distribution, name="spec2", goal=distroseries)
559+ self.factory.makeSpecification(
560+ distribution=distribution, name="spec3", goal=distroseries,
561 status=SpecificationDefinitionStatus.OBSOLETE)
562- self.factory.makeSpecification(
563- distribution=distribution, name="spec4")
564+ self.factory.makeSpecification(distribution=distribution, name="spec4")
565 self.assertNamesOfSpecificationsAre(
566- ["spec1", "spec2"],
567- distroseries._valid_specifications)
568+ ["spec1", "spec2"], distroseries.valid_specifications())
569
570 def test_productseries_all_specifications(self):
571 product = self.factory.makeProduct()
572@@ -115,7 +102,7 @@
573 status=SpecificationDefinitionStatus.OBSOLETE)
574 self.factory.makeSpecification(product=product, name="spec4")
575 self.assertNamesOfSpecificationsAre(
576- ["spec1", "spec2"], productseries._valid_specifications)
577+ ["spec1", "spec2"], productseries.valid_specifications())
578
579 def test_projectgroup_all_specifications(self):
580 projectgroup = self.factory.makeProject()
581@@ -123,13 +110,11 @@
582 product1 = self.factory.makeProduct(project=projectgroup)
583 product2 = self.factory.makeProduct(project=projectgroup)
584 product3 = self.factory.makeProduct(project=other_projectgroup)
585- self.factory.makeSpecification(
586- product=product1, name="spec1")
587+ self.factory.makeSpecification(product=product1, name="spec1")
588 self.factory.makeSpecification(
589 product=product2, name="spec2",
590 status=SpecificationDefinitionStatus.OBSOLETE)
591- self.factory.makeSpecification(
592- product=product3, name="spec3")
593+ self.factory.makeSpecification(product=product3, name="spec3")
594 self.assertNamesOfSpecificationsAre(
595 ["spec1", "spec2"], projectgroup.visible_specifications)
596
597@@ -145,7 +130,7 @@
598 status=SpecificationDefinitionStatus.OBSOLETE)
599 self.factory.makeSpecification(product=product3, name="spec3")
600 self.assertNamesOfSpecificationsAre(
601- ["spec1"], projectgroup._valid_specifications)
602+ ["spec1"], projectgroup.valid_specifications())
603
604 def test_person_all_specifications(self):
605 person = self.factory.makePerson(name="james-w")
606@@ -155,8 +140,7 @@
607 self.factory.makeSpecification(
608 product=product, name="spec2", approver=person,
609 status=SpecificationDefinitionStatus.OBSOLETE)
610- self.factory.makeSpecification(
611- product=product, name="spec3")
612+ self.factory.makeSpecification(product=product, name="spec3")
613 self.assertNamesOfSpecificationsAre(
614 ["spec1", "spec2"], person.visible_specifications)
615
616@@ -168,10 +152,9 @@
617 self.factory.makeSpecification(
618 product=product, name="spec2", approver=person,
619 status=SpecificationDefinitionStatus.OBSOLETE)
620- self.factory.makeSpecification(
621- product=product, name="spec3")
622+ self.factory.makeSpecification(product=product, name="spec3")
623 self.assertNamesOfSpecificationsAre(
624- ["spec1"], person._valid_specifications)
625+ ["spec1"], person.valid_specifications())
626
627
628 class HasSpecificationsSnapshotTestCase(TestCaseWithFactory):
629@@ -181,10 +164,7 @@
630
631 def check_skipped(self, target):
632 """Asserts that fields marked doNotSnapshot are skipped."""
633- skipped = [
634- 'all_specifications',
635- 'valid_specifications',
636- ]
637+ skipped = ['all_specifications', 'valid_specifications']
638 self.assertThat(target, DoesNotSnapshot(skipped, IHasSpecifications))
639
640 def test_product(self):
641
642=== modified file 'lib/lp/registry/configure.zcml'
643--- lib/lp/registry/configure.zcml 2013-04-03 03:09:04 +0000
644+++ lib/lp/registry/configure.zcml 2013-08-20 07:26:08 +0000
645@@ -1460,7 +1460,8 @@
646 permission="launchpad.View"
647 attributes="
648 visible_specifications
649- _valid_specifications
650+ valid_specifications
651+ api_valid_specifications
652 getAllowedSpecificationInformationTypes
653 getDefaultSpecificationInformationType
654 specifications" />
655
656=== modified file 'lib/lp/registry/doc/distribution.txt'
657--- lib/lp/registry/doc/distribution.txt 2013-05-01 21:23:16 +0000
658+++ lib/lp/registry/doc/distribution.txt 2013-08-20 07:26:08 +0000
659@@ -562,7 +562,7 @@
660 ... spec.definition_status = (
661 ... SpecificationDefinitionStatus.SUPERSEDED)
662 ... shim = spec.updateLifecycleStatus(owner)
663- >>> for spec in kubuntu._valid_specifications:
664+ >>> for spec in kubuntu.valid_specifications():
665 ... print spec.name
666 kde-desktopfile-langpacks
667
668
669=== modified file 'lib/lp/registry/doc/projectgroup.txt'
670--- lib/lp/registry/doc/projectgroup.txt 2013-05-01 21:23:16 +0000
671+++ lib/lp/registry/doc/projectgroup.txt 2013-08-20 07:26:08 +0000
672@@ -201,7 +201,7 @@
673 >>> flush_database_updates()
674
675 We can get all the specifications via the visible_specifications property,
676-and all valid specifications via the _valid_specifications property:
677+and all valid specifications via the valid_specifications method:
678
679 >>> for spec in mozilla.visible_specifications:
680 ... print spec.name
681@@ -211,7 +211,7 @@
682 mergewin
683 e4x
684
685- >>> for spec in mozilla._valid_specifications:
686+ >>> for spec in mozilla.valid_specifications():
687 ... print spec.name
688 svg-support
689 canvas
690@@ -313,7 +313,7 @@
691 >>> firefox.active = True
692
693 We can get all the specifications via the visible_specifications property,
694-and all valid specifications via the _valid_specifications property:
695+and all valid specifications via the valid_specifications method:
696
697 >>> for spec in mozilla_series_1_0.visible_specifications:
698 ... print spec.name
699@@ -323,7 +323,7 @@
700 mergewin
701 e4x
702
703- >>> for spec in mozilla_series_1_0._valid_specifications:
704+ >>> for spec in mozilla_series_1_0.valid_specifications():
705 ... print spec.name
706 svg-support
707 canvas
708
709=== modified file 'lib/lp/registry/model/distribution.py'
710--- lib/lp/registry/model/distribution.py 2013-07-11 06:12:20 +0000
711+++ lib/lp/registry/model/distribution.py 2013-08-20 07:26:08 +0000
712@@ -868,7 +868,8 @@
713 {self: source_package_names})
714
715 def specifications(self, user, sort=None, quantity=None, filter=None,
716- prejoin_people=True):
717+ need_people=True, need_branches=True,
718+ need_workitems=False):
719 """See `IHasSpecifications`.
720
721 In the case of distributions, there are two kinds of filtering,
722@@ -880,7 +881,9 @@
723 """
724 base_clauses = [Specification.distributionID == self.id]
725 return search_specifications(
726- self, base_clauses, user, sort, quantity, filter, prejoin_people)
727+ self, base_clauses, user, sort, quantity, filter,
728+ need_people=need_people, need_branches=need_branches,
729+ need_workitems=need_workitems)
730
731 def getSpecification(self, name):
732 """See `ISpecificationTarget`."""
733
734=== modified file 'lib/lp/registry/model/distroseries.py'
735--- lib/lp/registry/model/distroseries.py 2013-07-11 06:12:20 +0000
736+++ lib/lp/registry/model/distroseries.py 2013-08-20 07:26:08 +0000
737@@ -766,7 +766,8 @@
738 return self.distribution.official_bug_tags
739
740 def specifications(self, user, sort=None, quantity=None, filter=None,
741- prejoin_people=True):
742+ need_people=True, need_branches=True,
743+ need_workitems=False):
744 """See IHasSpecifications.
745
746 In this case the rules for the default behaviour cover three things:
747@@ -778,8 +779,9 @@
748 """
749 base_clauses = [Specification.distroseriesID == self.id]
750 return search_specifications(
751- self, base_clauses, user, sort, quantity, filter, prejoin_people,
752- default_acceptance=True)
753+ self, base_clauses, user, sort, quantity, filter,
754+ default_acceptance=True, need_people=need_people,
755+ need_branches=need_branches, need_workitems=need_workitems)
756
757 def getDistroSeriesLanguage(self, language):
758 """See `IDistroSeries`."""
759
760=== modified file 'lib/lp/registry/model/person.py'
761--- lib/lp/registry/model/person.py 2013-08-08 04:11:59 +0000
762+++ lib/lp/registry/model/person.py 2013-08-20 07:26:08 +0000
763@@ -823,7 +823,8 @@
764 return "%s (%s)" % (self.displayname, self.name)
765
766 def specifications(self, user, sort=None, quantity=None, filter=None,
767- prejoin_people=True, in_progress=False):
768+ in_progress=False, need_people=True, need_branches=True,
769+ need_workitems=False):
770 """See `IHasSpecifications`."""
771 from lp.blueprints.model.specificationsubscription import (
772 SpecificationSubscription,
773@@ -874,7 +875,9 @@
774 SpecificationFilter.STARTED])
775
776 return search_specifications(
777- self, clauses, user, sort, quantity, list(filter), prejoin_people)
778+ self, clauses, user, sort, quantity, list(filter),
779+ need_people=need_people, need_branches=need_branches,
780+ need_workitems=need_workitems)
781
782 # XXX: Tom Berger 2008-04-14 bug=191799:
783 # The implementation of these functions
784
785=== modified file 'lib/lp/registry/model/product.py'
786--- lib/lp/registry/model/product.py 2013-07-11 06:12:20 +0000
787+++ lib/lp/registry/model/product.py 2013-08-20 07:26:08 +0000
788@@ -1416,12 +1416,14 @@
789 return True
790
791 def specifications(self, user, sort=None, quantity=None, filter=None,
792- prejoin_people=True):
793+ need_people=True, need_branches=True,
794+ need_workitems=False):
795 """See `IHasSpecifications`."""
796-
797 base_clauses = [Specification.productID == self.id]
798 return search_specifications(
799- self, base_clauses, user, sort, quantity, filter, prejoin_people)
800+ self, base_clauses, user, sort, quantity, filter,
801+ need_people=need_people, need_branches=need_branches,
802+ need_workitems=need_workitems)
803
804 def getSpecification(self, name):
805 """See `ISpecificationTarget`."""
806
807=== modified file 'lib/lp/registry/model/productseries.py'
808--- lib/lp/registry/model/productseries.py 2013-04-03 03:09:04 +0000
809+++ lib/lp/registry/model/productseries.py 2013-08-20 07:26:08 +0000
810@@ -316,7 +316,8 @@
811 return self == self.product.development_focus
812
813 def specifications(self, user, sort=None, quantity=None, filter=None,
814- prejoin_people=True):
815+ need_people=True, need_branches=True,
816+ need_workitems=False):
817 """See IHasSpecifications.
818
819 The rules for filtering are that there are three areas where you can
820@@ -329,8 +330,9 @@
821 """
822 base_clauses = [Specification.productseriesID == self.id]
823 return search_specifications(
824- self, base_clauses, user, sort, quantity, filter, prejoin_people,
825- default_acceptance=True)
826+ self, base_clauses, user, sort, quantity, filter,
827+ default_acceptance=True, need_people=need_people,
828+ need_branches=need_branches, need_workitems=need_workitems)
829
830 @property
831 def all_specifications(self):
832
833=== modified file 'lib/lp/registry/model/projectgroup.py'
834--- lib/lp/registry/model/projectgroup.py 2013-07-12 06:14:22 +0000
835+++ lib/lp/registry/model/projectgroup.py 2013-08-20 07:26:08 +0000
836@@ -244,7 +244,8 @@
837 return query, ['Product', 'Specification', 'SprintSpecification']
838
839 def specifications(self, user, sort=None, quantity=None, filter=None,
840- series=None, prejoin_people=True):
841+ series=None, need_people=True, need_branches=True,
842+ need_workitems=False):
843 """See `IHasSpecifications`."""
844 base_clauses = [
845 Specification.productID == Product.id,
846@@ -256,8 +257,9 @@
847 Join(ProductSeries,
848 Specification.productseriesID == ProductSeries.id))
849 return search_specifications(
850- self, base_clauses, user, sort, quantity, filter, prejoin_people,
851- tables=tables)
852+ self, base_clauses, user, sort, quantity, filter, tables=tables,
853+ need_people=need_people, need_branches=need_branches,
854+ need_workitems=need_workitems)
855
856 def _customizeSearchParams(self, search_params):
857 """Customize `search_params` for this milestone."""
858@@ -617,10 +619,11 @@
859 self.name = name
860
861 def specifications(self, user, sort=None, quantity=None, filter=None,
862- prejoin_people=True):
863+ need_people=True, need_branches=True,
864+ need_workitems=False):
865 return self.project.specifications(
866- user, sort, quantity, filter, self.name,
867- prejoin_people=prejoin_people)
868+ user, sort, quantity, filter, self.name, need_people=need_people,
869+ need_branches=need_branches, need_workitems=need_workitems)
870
871 @property
872 def title(self):
873
874=== modified file 'lib/lp/registry/tests/test_distroseries.py'
875--- lib/lp/registry/tests/test_distroseries.py 2012-09-26 04:14:01 +0000
876+++ lib/lp/registry/tests/test_distroseries.py 2013-08-20 07:26:08 +0000
877@@ -1,4 +1,4 @@
878-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
879+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
880 # GNU Affero General Public License version 3 (see the file LICENSE).
881
882 """Tests for distroseries."""
883@@ -11,6 +11,7 @@
884
885 from logging import getLogger
886
887+from testtools.matchers import Equals
888 import transaction
889 from zope.component import getUtility
890 from zope.security.proxy import removeSecurityProxy
891@@ -19,6 +20,7 @@
892 from lp.registry.interfaces.distroseries import IDistroSeriesSet
893 from lp.registry.interfaces.pocket import PackagePublishingPocket
894 from lp.registry.interfaces.series import SeriesStatus
895+from lp.services.database.interfaces import IStore
896 from lp.soyuz.enums import (
897 ArchivePurpose,
898 PackagePublishingStatus,
899@@ -38,6 +40,7 @@
900 ANONYMOUS,
901 login,
902 person_logged_in,
903+ StormStatementRecorder,
904 TestCase,
905 TestCaseWithFactory,
906 )
907@@ -45,6 +48,7 @@
908 DatabaseFunctionalLayer,
909 LaunchpadFunctionalLayer,
910 )
911+from lp.testing.matchers import HasQueryCount
912 from lp.translations.interfaces.translations import (
913 TranslationsBranchImportMode,
914 )
915@@ -324,6 +328,23 @@
916 self.assertTrue(self.checkLegalPocket(
917 SeriesStatus.CURRENT, PackagePublishingPocket.UPDATES))
918
919+ def test_valid_specifications_query_count(self):
920+ distroseries = self.factory.makeDistroSeries()
921+ distribution = distroseries.distribution
922+ spec1 = self.factory.makeSpecification(
923+ distribution=distribution, goal=distroseries)
924+ spec2 = self.factory.makeSpecification(
925+ distribution=distribution, goal=distroseries)
926+ for i in range(5):
927+ self.factory.makeSpecificationWorkItem(specification=spec1)
928+ self.factory.makeSpecificationWorkItem(specification=spec2)
929+ IStore(spec1.__class__).flush()
930+ IStore(spec1.__class__).invalidate()
931+ with StormStatementRecorder() as recorder:
932+ for spec in distroseries.api_valid_specifications:
933+ spec.workitems_text
934+ self.assertThat(recorder, HasQueryCount(Equals(4)))
935+
936
937 class TestDistroSeriesPackaging(TestCaseWithFactory):
938
939
940=== modified file 'lib/lp/registry/tests/test_product.py'
941--- lib/lp/registry/tests/test_product.py 2013-06-20 05:50:00 +0000
942+++ lib/lp/registry/tests/test_product.py 2013-08-20 07:26:08 +0000
943@@ -813,8 +813,8 @@
944 'owner', 'parent_subscription_target', 'project', 'title', )),
945 'launchpad.View': set((
946 '_getOfficialTagClause', 'visible_specifications',
947- '_valid_specifications', 'active_or_packaged_series',
948- 'aliases', 'all_milestones',
949+ 'valid_specifications', 'api_valid_specifications',
950+ 'active_or_packaged_series', 'aliases', 'all_milestones',
951 'allowsTranslationEdits', 'allowsTranslationSuggestions',
952 'announce', 'answer_contacts', 'answers_usage', 'autoupdate',
953 'blueprints_usage', 'branch_sharing_policy',
954
955=== modified file 'lib/lp/registry/tests/test_productseries.py'
956--- lib/lp/registry/tests/test_productseries.py 2013-06-20 05:50:00 +0000
957+++ lib/lp/registry/tests/test_productseries.py 2013-08-20 07:26:08 +0000
958@@ -627,13 +627,13 @@
959 'parent_subscription_target', 'product', 'productID', 'series')),
960 'launchpad.View': set((
961 'visible_specifications', '_getOfficialTagClause',
962- '_valid_specifications', 'active', 'all_milestones',
963- 'answers_usage', 'blueprints_usage', 'branch',
964- 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
965- 'bug_subscriptions', 'bug_supervisor', 'bug_tracking_usage',
966- 'bugtargetname',
967- 'codehosting_usage', 'createBug', 'datecreated', 'displayname',
968- 'driver', 'drivers', 'enable_bugfiling_duplicate_search',
969+ 'valid_specifications', 'api_valid_specifications',
970+ 'active', 'all_milestones', 'answers_usage', 'blueprints_usage',
971+ 'branch', 'bug_reported_acknowledgement',
972+ 'bug_reporting_guidelines', 'bug_subscriptions', 'bug_supervisor',
973+ 'bug_tracking_usage', 'bugtargetname', 'codehosting_usage',
974+ 'createBug', 'datecreated', 'displayname', 'driver', 'drivers',
975+ 'enable_bugfiling_duplicate_search',
976 'getAllowedSpecificationInformationTypes',
977 'getBugSummaryContextWhereClause',
978 'getBugTaskWeightFunction', 'getCachedReleases',
979
980=== modified file 'lib/lp/soyuz/browser/tests/test_archive_webservice.py'
981--- lib/lp/soyuz/browser/tests/test_archive_webservice.py 2013-05-10 05:30:11 +0000
982+++ lib/lp/soyuz/browser/tests/test_archive_webservice.py 2013-08-20 07:26:08 +0000
983@@ -1,4 +1,4 @@
984-# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
985+# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
986 # GNU Affero General Public License version 3 (see the file LICENSE).
987
988 __metaclass__ = type
989@@ -37,14 +37,13 @@
990 )
991 from lp.testing.layers import DatabaseFunctionalLayer
992 from lp.testing.matchers import HasQueryCount
993-from lp.testing.pages import LaunchpadWebServiceCaller
994
995
996 class TestArchiveWebservice(TestCaseWithFactory):
997 layer = DatabaseFunctionalLayer
998
999 def setUp(self):
1000- TestCaseWithFactory.setUp(self)
1001+ super(TestArchiveWebservice, self).setUp()
1002 with celebrity_logged_in('admin'):
1003 self.archive = self.factory.makeArchive(
1004 purpose=ArchivePurpose.PRIMARY)
1005@@ -55,9 +54,6 @@
1006 distroseries_name = distroseries.name
1007 person_name = person.name
1008
1009- self.webservice = LaunchpadWebServiceCaller(
1010- 'launchpad-library', 'salgado-change-anything')
1011-
1012 self.launchpad = launchpadlib_for(
1013 "archive test", "salgado", "WRITE_PUBLIC")
1014 self.distribution = self.launchpad.distributions[distro_name]