Merge lp:~stevenk/launchpad/use-specification-aag into lp:launchpad

Proposed by Steve Kowalik
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 16438
Proposed branch: lp:~stevenk/launchpad/use-specification-aag
Merge into: lp:launchpad
Diff against target: 1713 lines (+423/-794)
16 files modified
lib/lp/blueprints/browser/specificationtarget.py (+2/-3)
lib/lp/blueprints/enums.py (+8/-1)
lib/lp/blueprints/model/specification.py (+5/-229)
lib/lp/blueprints/model/specificationsearch.py (+276/-0)
lib/lp/blueprints/model/sprint.py (+13/-10)
lib/lp/blueprints/tests/test_hasspecifications.py (+4/-7)
lib/lp/blueprints/tests/test_specification.py (+23/-27)
lib/lp/registry/doc/distroseries.txt (+3/-2)
lib/lp/registry/model/distribution.py (+5/-86)
lib/lp/registry/model/distroseries.py (+6/-111)
lib/lp/registry/model/milestone.py (+12/-10)
lib/lp/registry/model/person.py (+34/-54)
lib/lp/registry/model/product.py (+5/-49)
lib/lp/registry/model/productseries.py (+6/-119)
lib/lp/registry/model/projectgroup.py (+15/-72)
lib/lp/registry/model/sharingjob.py (+6/-14)
To merge this branch: bzr merge lp:~stevenk/launchpad/use-specification-aag
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+143630@code.launchpad.net

Commit message

Massively refactor IHasSpecifications.specifications methods to now mostly back
onto a new function, search_specifications. Destroy visible_specification_query, and write a new method get_specification_privacy_filter, which backs onto the denormalized columns Specification.access_{policy,grants}.

Description of the change

Massively refactor IHasSpecifications.specifications methods to now mostly back
onto a new function, search_specifications. The two exceptions are WorkItems in IPerson, and ISprint.specifications. As a bonus this stormifies all of them.

Destroy visible_specification_query, and write a new method get_specification_privacy_filter, which backs onto the denormalized columns Specification.access_{policy,grants}.

get_specification_filters and the new method get_specification_privacy_filter are now in specificationsearch, along with moving things like Specification.storm_completeness (now get_specification_completeness_clause() in specificationsearch) and spec_started_clause (now get_specification_started_clause() also in the same module), and completly killing Specification.completeness_clause.

_preload_specifications_people has also moved out of Specification to specificationsearch. Sadly, it's still a horrible mess.

There are no tests for search_specification itself, but it is called from enough places in the code base and the test suite that it's pretty well covered.

Some lint has been cleaned up.

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

185 + return tables, [
186 + active_products, Or(public_spec_filter, artifact_grant_query,
187 + policy_grant_query)]

Does the Or not fit on one line?

review: Approve (code)
Revision history for this message
William Grant (wgrant) wrote :

You can push show_proposed down down into the branch that uses it, otherwise fine now. Thanks.

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/blueprints/browser/specificationtarget.py'
2--- lib/lp/blueprints/browser/specificationtarget.py 2012-09-27 15:28:38 +0000
3+++ lib/lp/blueprints/browser/specificationtarget.py 2013-01-22 06:44:52 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """ISpecificationTarget browser views."""
10@@ -347,8 +347,7 @@
11 and self.context.private
12 and not check_permission('launchpad.View', self.context)):
13 return []
14- filter = self.spec_filter
15- return self.context.specifications(self.user, filter=filter)
16+ return self.context.specifications(self.user, filter=self.spec_filter)
17
18 @cachedproperty
19 def specs_batched(self):
20
21=== modified file 'lib/lp/blueprints/enums.py'
22--- lib/lp/blueprints/enums.py 2012-10-22 20:04:30 +0000
23+++ lib/lp/blueprints/enums.py 2013-01-22 06:44:52 +0000
24@@ -1,4 +1,4 @@
25-# Copyright 2010 Canonical Ltd. This software is licensed under the
26+# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
27 # GNU Affero General Public License version 3 (see the file LICENSE).
28
29 """Enumerations used in the lp/blueprints modules."""
30@@ -334,6 +334,13 @@
31 to which the person has subscribed.
32 """)
33
34+ STARTED = DBItem(110, """
35+ Started
36+
37+ This indicates that the list should include specifications that are
38+ marked as started.
39+ """)
40+
41
42 class SpecificationSort(EnumeratedType):
43 """The scheme to sort the results of a specifications query.
44
45=== modified file 'lib/lp/blueprints/model/specification.py'
46--- lib/lp/blueprints/model/specification.py 2012-12-26 01:04:05 +0000
47+++ lib/lp/blueprints/model/specification.py 2013-01-22 06:44:52 +0000
48@@ -1,9 +1,8 @@
49-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
50+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
51 # GNU Affero General Public License version 3 (see the file LICENSE).
52
53 __metaclass__ = type
54 __all__ = [
55- 'get_specification_filters',
56 'HasSpecificationsMixin',
57 'recursive_blocked_query',
58 'recursive_dependent_query',
59@@ -11,8 +10,6 @@
60 'SPECIFICATION_POLICY_ALLOWED_TYPES',
61 'SPECIFICATION_POLICY_DEFAULT_TYPES',
62 'SpecificationSet',
63- 'spec_started_clause',
64- 'visible_specification_query',
65 ]
66
67 from lazr.lifecycle.event import (
68@@ -32,8 +29,6 @@
69 And,
70 In,
71 Join,
72- LeftJoin,
73- Not,
74 Or,
75 Select,
76 )
77@@ -103,7 +98,6 @@
78 UTC_NOW,
79 )
80 from lp.services.database.datetimecol import UtcDateTimeCol
81-from lp.services.database.decoratedresultset import DecoratedResultSet
82 from lp.services.database.enumcol import EnumCol
83 from lp.services.database.lpstorm import IStore
84 from lp.services.database.sqlbase import (
85@@ -111,7 +105,6 @@
86 SQLBase,
87 sqlvalues,
88 )
89-from lp.services.database.stormexpr import fti_search
90 from lp.services.mail.helpers import get_contact_email_addresses
91 from lp.services.propertycache import (
92 cachedproperty,
93@@ -556,46 +549,6 @@
94 """See ISpecification."""
95 return not self.is_complete
96
97- # Several other classes need to generate lists of specifications, and
98- # one thing they often have to filter for is completeness. We maintain
99- # this single canonical query string here so that it does not have to be
100- # cargo culted into Product, Distribution, ProductSeries etc
101-
102- # Also note that there is a constraint in the database which ensures
103- # that date_completed is set if the spec is complete, and that db
104- # constraint parrots this definition exactly.
105-
106- # NB NB NB if you change this definition PLEASE update the db constraint
107- # Specification.specification_completion_recorded_chk !!!
108- completeness_clause = ("""
109- Specification.implementation_status = %s OR
110- Specification.definition_status IN ( %s, %s ) OR
111- (Specification.implementation_status = %s AND
112- Specification.definition_status = %s)
113- """ % sqlvalues(SpecificationImplementationStatus.IMPLEMENTED.value,
114- SpecificationDefinitionStatus.OBSOLETE.value,
115- SpecificationDefinitionStatus.SUPERSEDED.value,
116- SpecificationImplementationStatus.INFORMATIONAL.value,
117- SpecificationDefinitionStatus.APPROVED.value))
118-
119- @classmethod
120- def storm_completeness(cls):
121- """Storm version of the above."""
122- return Or(
123- cls.implementation_status ==
124- SpecificationImplementationStatus.IMPLEMENTED,
125- cls.definition_status.is_in([
126- SpecificationDefinitionStatus.OBSOLETE,
127- SpecificationDefinitionStatus.SUPERSEDED,
128- ]),
129- And(
130- cls.implementation_status ==
131- SpecificationImplementationStatus.INFORMATIONAL,
132- cls.definition_status ==
133- SpecificationDefinitionStatus.APPROVED
134- ),
135- )
136-
137 @property
138 def is_complete(self):
139 """See `ISpecification`."""
140@@ -1051,54 +1004,6 @@
141 elif sort == SpecificationSort.DATE:
142 return (Desc(Specification.datecreated), Specification.id)
143
144- def _preload_specifications_people(self, tables, clauses):
145- """Perform eager loading of people and their validity for query.
146-
147- :param query: a string query generated in the 'specifications'
148- method.
149- :return: A DecoratedResultSet with Person precaching setup.
150- """
151- # Circular import.
152- if isinstance(clauses, basestring):
153- clauses = [SQL(clauses)]
154-
155- def cache_people(rows):
156- """DecoratedResultSet pre_iter_hook to eager load Person
157- attributes.
158- """
159- from lp.registry.model.person import Person
160- # Find the people we need:
161- person_ids = set()
162- for spec in rows:
163- person_ids.add(spec._assigneeID)
164- person_ids.add(spec._approverID)
165- person_ids.add(spec._drafterID)
166- person_ids.discard(None)
167- if not person_ids:
168- return
169- # Query those people
170- origin = [Person]
171- columns = [Person]
172- validity_info = Person._validity_queries()
173- origin.extend(validity_info["joins"])
174- columns.extend(validity_info["tables"])
175- decorators = validity_info["decorators"]
176- personset = IStore(Specification).using(*origin).find(
177- tuple(columns),
178- Person.id.is_in(person_ids),
179- )
180- for row in personset:
181- person = row[0]
182- index = 1
183- for decorator in decorators:
184- column = row[index]
185- index += 1
186- decorator(person, column)
187-
188- results = IStore(Specification).using(*tables).find(
189- Specification, *clauses)
190- return DecoratedResultSet(results, pre_iter_hook=cache_people)
191-
192 @property
193 def _all_specifications(self):
194 """See IHasSpecifications."""
195@@ -1155,41 +1060,10 @@
196
197 def specifications(self, user, sort=None, quantity=None, filter=None,
198 prejoin_people=True):
199- store = IStore(Specification)
200-
201- # Take the visibility due to privacy into account.
202- privacy_tables, clauses = visible_specification_query(user)
203-
204- if not filter:
205- # Default to showing incomplete specs
206- filter = [SpecificationFilter.INCOMPLETE]
207-
208- spec_clauses = get_specification_filters(filter)
209- clauses.extend(spec_clauses)
210-
211- # sort by priority descending, by default
212- if sort is None or sort == SpecificationSort.PRIORITY:
213- order = [Desc(Specification.priority),
214- Specification.definition_status,
215- Specification.name]
216-
217- elif sort == SpecificationSort.DATE:
218- if SpecificationFilter.COMPLETE in filter:
219- # if we are showing completed, we care about date completed
220- order = [Desc(Specification.date_completed),
221- Specification.id]
222- else:
223- # if not specially looking for complete, we care about date
224- # registered
225- order = [Desc(Specification.datecreated), Specification.id]
226-
227- if prejoin_people:
228- results = self._preload_specifications_people(
229- privacy_tables, clauses)
230- else:
231- results = store.using(*privacy_tables).find(
232- Specification, *clauses)
233- return results.order_by(*order)[:quantity]
234+ from lp.blueprints.model.specificationsearch import (
235+ search_specifications)
236+ return search_specifications(
237+ self, [], user, sort, quantity, filter, prejoin_people)
238
239 def getByURL(self, url):
240 """See ISpecificationSet."""
241@@ -1264,101 +1138,3 @@
242 def get(self, spec_id):
243 """See lp.blueprints.interfaces.specification.ISpecificationSet."""
244 return Specification.get(spec_id)
245-
246-
247-def visible_specification_query(user):
248- """Return a Storm expression and list of tables for filtering
249- specifications by privacy.
250-
251- :param user: A Person ID or a column reference.
252- :return: A tuple of tables, clauses to filter out specifications that the
253- user cannot see.
254- """
255- from lp.registry.model.product import Product
256- from lp.registry.model.accesspolicy import (
257- AccessArtifact,
258- AccessPolicy,
259- AccessPolicyGrantFlat,
260- )
261- tables = [
262- Specification,
263- LeftJoin(Product, Specification.productID == Product.id),
264- LeftJoin(AccessPolicy, And(
265- Or(Specification.productID == AccessPolicy.product_id,
266- Specification.distributionID ==
267- AccessPolicy.distribution_id),
268- Specification.information_type == AccessPolicy.type)),
269- LeftJoin(AccessPolicyGrantFlat,
270- AccessPolicy.id == AccessPolicyGrantFlat.policy_id),
271- LeftJoin(
272- TeamParticipation,
273- And(AccessPolicyGrantFlat.grantee == TeamParticipation.teamID,
274- TeamParticipation.person == user)),
275- LeftJoin(AccessArtifact,
276- AccessPolicyGrantFlat.abstract_artifact_id ==
277- AccessArtifact.id)
278- ]
279- clauses = [
280- Or(Specification.information_type.is_in(PUBLIC_INFORMATION_TYPES),
281- And(AccessPolicyGrantFlat.id != None,
282- TeamParticipation.personID != None,
283- Or(AccessPolicyGrantFlat.abstract_artifact == None,
284- AccessArtifact.specification_id == Specification.id))),
285- Or(Specification.product == None, Product.active == True)]
286- return tables, clauses
287-
288-
289-def get_specification_filters(filter):
290- """Return a list of Storm expressions for filtering Specifications.
291-
292- :param filters: A collection of SpecificationFilter and/or strings.
293- Strings are used for text searches.
294- """
295- clauses = []
296- # ALL is the trump card.
297- if SpecificationFilter.ALL in filter:
298- return clauses
299- # Look for informational specs.
300- if SpecificationFilter.INFORMATIONAL in filter:
301- clauses.append(Specification.implementation_status ==
302- SpecificationImplementationStatus.INFORMATIONAL)
303- # Filter based on completion. See the implementation of
304- # Specification.is_complete() for more details.
305- if SpecificationFilter.COMPLETE in filter:
306- clauses.append(Specification.storm_completeness())
307- if SpecificationFilter.INCOMPLETE in filter:
308- clauses.append(Not(Specification.storm_completeness()))
309-
310- # Filter for validity. If we want valid specs only, then we should exclude
311- # all OBSOLETE or SUPERSEDED specs.
312- if SpecificationFilter.VALID in filter:
313- clauses.append(Not(Specification.definition_status.is_in([
314- SpecificationDefinitionStatus.OBSOLETE,
315- SpecificationDefinitionStatus.SUPERSEDED,
316- ])))
317- # Filter for specification text.
318- for constraint in filter:
319- if isinstance(constraint, basestring):
320- # A string in the filter is a text search filter.
321- clauses.append(fti_search(Specification, constraint))
322- return clauses
323-
324-
325-# NB NB If you change this definition, please update the equivalent
326-# DB constraint Specification.specification_start_recorded_chk
327-# We choose to define "started" as the set of delivery states NOT
328-# in the values we select. Another option would be to say "anything less
329-# than a threshold" and to comment the dbschema that "anything not
330-# started should be less than the threshold". We'll see how maintainable
331-# this is.
332-spec_started_clause = Or(Not(Specification.implementation_status.is_in([
333- SpecificationImplementationStatus.UNKNOWN,
334- SpecificationImplementationStatus.NOTSTARTED,
335- SpecificationImplementationStatus.DEFERRED,
336- SpecificationImplementationStatus.INFORMATIONAL,
337- ])),
338- And(Specification.implementation_status ==
339- SpecificationImplementationStatus.INFORMATIONAL,
340- Specification.definition_status ==
341- SpecificationDefinitionStatus.APPROVED
342- ))
343
344=== added file 'lib/lp/blueprints/model/specificationsearch.py'
345--- lib/lp/blueprints/model/specificationsearch.py 1970-01-01 00:00:00 +0000
346+++ lib/lp/blueprints/model/specificationsearch.py 2013-01-22 06:44:52 +0000
347@@ -0,0 +1,276 @@
348+# Copyright 2013 Canonical Ltd. This software is licensed under the
349+# GNU Affero General Public License version 3 (see the file LICENSE).
350+
351+"""Helper methods to search specifications."""
352+
353+__metaclass__ = type
354+__all__ = [
355+ 'get_specification_filters',
356+ 'get_specification_active_product_filter',
357+ 'get_specification_privacy_filter',
358+ 'search_specifications',
359+ ]
360+
361+from storm.expr import (
362+ And,
363+ Coalesce,
364+ Join,
365+ LeftJoin,
366+ Not,
367+ Or,
368+ Select,
369+ )
370+from storm.locals import (
371+ Desc,
372+ SQL,
373+ )
374+
375+from lp.app.enums import PUBLIC_INFORMATION_TYPES
376+from lp.blueprints.enums import (
377+ SpecificationDefinitionStatus,
378+ SpecificationFilter,
379+ SpecificationGoalStatus,
380+ SpecificationImplementationStatus,
381+ SpecificationSort,
382+ )
383+from lp.blueprints.model.specification import Specification
384+from lp.registry.interfaces.distribution import IDistribution
385+from lp.registry.interfaces.distroseries import IDistroSeries
386+from lp.registry.interfaces.product import IProduct
387+from lp.registry.interfaces.productseries import IProductSeries
388+from lp.registry.model.teammembership import TeamParticipation
389+from lp.services.database.decoratedresultset import DecoratedResultSet
390+from lp.services.database.lpstorm import IStore
391+from lp.services.database.stormexpr import (
392+ Array,
393+ ArrayAgg,
394+ ArrayIntersects,
395+ fti_search,
396+ )
397+
398+
399+def search_specifications(context, base_clauses, user, sort=None,
400+ quantity=None, spec_filter=None, prejoin_people=True,
401+ tables=[], default_acceptance=False):
402+ store = IStore(Specification)
403+ if not default_acceptance:
404+ default = SpecificationFilter.INCOMPLETE
405+ options = set([
406+ SpecificationFilter.COMPLETE, SpecificationFilter.INCOMPLETE])
407+ else:
408+ default = SpecificationFilter.ACCEPTED
409+ options = set([
410+ SpecificationFilter.ACCEPTED, SpecificationFilter.DECLINED,
411+ SpecificationFilter.PROPOSED])
412+ if not spec_filter:
413+ spec_filter = [default]
414+
415+ if not set(spec_filter) & options:
416+ spec_filter.append(default)
417+
418+ if not tables:
419+ tables = [Specification]
420+ clauses = base_clauses
421+ product_table, product_clauses = get_specification_active_product_filter(
422+ context)
423+ tables.extend(product_table)
424+ for extend in (get_specification_privacy_filter(user),
425+ get_specification_filters(spec_filter), product_clauses):
426+ clauses.extend(extend)
427+
428+ # Sort by priority descending, by default.
429+ if sort is None or sort == SpecificationSort.PRIORITY:
430+ order = [
431+ Desc(Specification.priority), Specification.definition_status,
432+ Specification.name]
433+ elif sort == SpecificationSort.DATE:
434+ if SpecificationFilter.COMPLETE in spec_filter:
435+ # If we are showing completed, we care about date completed.
436+ order = [Desc(Specification.date_completed), Specification.id]
437+ else:
438+ # If not specially looking for complete, we care about date
439+ # registered.
440+ order = []
441+ show_proposed = set(
442+ [SpecificationFilter.ALL, SpecificationFilter.PROPOSED])
443+ if default_acceptance and not (set(spec_filter) & show_proposed):
444+ order.append(Desc(Specification.date_goal_decided))
445+ order.extend([Desc(Specification.datecreated), Specification.id])
446+ else:
447+ order = [sort]
448+ if prejoin_people:
449+ results = _preload_specifications_people(tables, clauses)
450+ else:
451+ results = store.using(*tables).find(Specification, *clauses)
452+ return results.order_by(*order).config(limit=quantity)
453+
454+
455+def get_specification_active_product_filter(context):
456+ if (IDistribution.providedBy(context) or IDistroSeries.providedBy(context)
457+ or IProduct.providedBy(context) or IProductSeries.providedBy(context)):
458+ return [], []
459+ from lp.registry.model.product import Product
460+ tables = [
461+ LeftJoin(Product, Specification.productID == Product.id)]
462+ active_products = (
463+ Or(Specification.product == None, Product.active == True))
464+ return tables, [active_products]
465+
466+
467+def get_specification_privacy_filter(user):
468+ # Circular imports.
469+ from lp.registry.model.accesspolicy import AccessPolicyGrant
470+ public_spec_filter = (
471+ Specification.information_type.is_in(PUBLIC_INFORMATION_TYPES))
472+
473+ if user is None:
474+ return [public_spec_filter]
475+
476+ artifact_grant_query = Coalesce(
477+ ArrayIntersects(
478+ SQL('Specification.access_grants'),
479+ Select(
480+ ArrayAgg(TeamParticipation.teamID),
481+ tables=TeamParticipation,
482+ where=(TeamParticipation.person == user)
483+ )), False)
484+
485+ policy_grant_query = Coalesce(
486+ ArrayIntersects(
487+ Array(SQL('Specification.access_policy')),
488+ Select(
489+ ArrayAgg(AccessPolicyGrant.policy_id),
490+ tables=(AccessPolicyGrant,
491+ Join(TeamParticipation,
492+ TeamParticipation.teamID ==
493+ AccessPolicyGrant.grantee_id)),
494+ where=(TeamParticipation.person == user)
495+ )), False)
496+
497+ return [Or(public_spec_filter, artifact_grant_query, policy_grant_query)]
498+
499+
500+def get_specification_filters(filter, goalstatus=True):
501+ """Return a list of Storm expressions for filtering Specifications.
502+
503+ :param filters: A collection of SpecificationFilter and/or strings.
504+ Strings are used for text searches.
505+ """
506+ clauses = []
507+ # ALL is the trump card.
508+ if SpecificationFilter.ALL in filter:
509+ return clauses
510+ # Look for informational specs.
511+ if SpecificationFilter.INFORMATIONAL in filter:
512+ clauses.append(
513+ Specification.implementation_status ==
514+ SpecificationImplementationStatus.INFORMATIONAL)
515+ # Filter based on completion. See the implementation of
516+ # Specification.is_complete() for more details.
517+ if SpecificationFilter.COMPLETE in filter:
518+ clauses.append(get_specification_completeness_clause())
519+ if SpecificationFilter.INCOMPLETE in filter:
520+ clauses.append(Not(get_specification_completeness_clause()))
521+
522+ # Filter for goal status.
523+ if goalstatus:
524+ goalstatus = None
525+ if SpecificationFilter.ACCEPTED in filter:
526+ goalstatus = SpecificationGoalStatus.ACCEPTED
527+ elif SpecificationFilter.PROPOSED in filter:
528+ goalstatus = SpecificationGoalStatus.PROPOSED
529+ elif SpecificationFilter.DECLINED in filter:
530+ goalstatus = SpecificationGoalStatus.DECLINED
531+ if goalstatus:
532+ clauses.append(Specification.goalstatus == goalstatus)
533+
534+ if SpecificationFilter.STARTED in filter:
535+ clauses.append(get_specification_started_clause())
536+
537+ # Filter for validity. If we want valid specs only, then we should exclude
538+ # all OBSOLETE or SUPERSEDED specs.
539+ if SpecificationFilter.VALID in filter:
540+ clauses.append(Not(Specification.definition_status.is_in([
541+ SpecificationDefinitionStatus.OBSOLETE,
542+ SpecificationDefinitionStatus.SUPERSEDED])))
543+ # Filter for specification text.
544+ for constraint in filter:
545+ if isinstance(constraint, basestring):
546+ # A string in the filter is a text search filter.
547+ clauses.append(fti_search(Specification, constraint))
548+ return clauses
549+
550+
551+def _preload_specifications_people(tables, clauses):
552+ """Perform eager loading of people and their validity for query.
553+
554+ :param query: a string query generated in the 'specifications'
555+ method.
556+ :return: A DecoratedResultSet with Person precaching setup.
557+ """
558+ if isinstance(clauses, basestring):
559+ clauses = [SQL(clauses)]
560+
561+ def cache_people(rows):
562+ """DecoratedResultSet pre_iter_hook to eager load Person
563+ attributes.
564+ """
565+ from lp.registry.model.person import Person
566+ # Find the people we need:
567+ person_ids = set()
568+ for spec in rows:
569+ person_ids.add(spec._assigneeID)
570+ person_ids.add(spec._approverID)
571+ person_ids.add(spec._drafterID)
572+ person_ids.discard(None)
573+ if not person_ids:
574+ return
575+ # Query those people
576+ origin = [Person]
577+ columns = [Person]
578+ validity_info = Person._validity_queries()
579+ origin.extend(validity_info["joins"])
580+ columns.extend(validity_info["tables"])
581+ decorators = validity_info["decorators"]
582+ personset = IStore(Specification).using(*origin).find(
583+ tuple(columns),
584+ Person.id.is_in(person_ids),
585+ )
586+ for row in personset:
587+ person = row[0]
588+ index = 1
589+ for decorator in decorators:
590+ column = row[index]
591+ index += 1
592+ decorator(person, column)
593+
594+ results = IStore(Specification).using(*tables).find(
595+ Specification, *clauses)
596+ return DecoratedResultSet(results, pre_iter_hook=cache_people)
597+
598+
599+def get_specification_started_clause():
600+ return Or(Not(Specification.implementation_status.is_in([
601+ SpecificationImplementationStatus.UNKNOWN,
602+ SpecificationImplementationStatus.NOTSTARTED,
603+ SpecificationImplementationStatus.DEFERRED,
604+ SpecificationImplementationStatus.INFORMATIONAL])),
605+ And(Specification.implementation_status ==
606+ SpecificationImplementationStatus.INFORMATIONAL,
607+ Specification.definition_status ==
608+ SpecificationDefinitionStatus.APPROVED))
609+
610+
611+def get_specification_completeness_clause():
612+ return Or(
613+ Specification.implementation_status ==
614+ SpecificationImplementationStatus.IMPLEMENTED,
615+ Specification.definition_status.is_in([
616+ SpecificationDefinitionStatus.OBSOLETE,
617+ SpecificationDefinitionStatus.SUPERSEDED,
618+ ]),
619+ And(
620+ Specification.implementation_status ==
621+ SpecificationImplementationStatus.INFORMATIONAL,
622+ Specification.definition_status ==
623+ SpecificationDefinitionStatus.APPROVED))
624
625=== modified file 'lib/lp/blueprints/model/sprint.py'
626--- lib/lp/blueprints/model/sprint.py 2013-01-07 02:40:55 +0000
627+++ lib/lp/blueprints/model/sprint.py 2013-01-22 06:44:52 +0000
628@@ -1,4 +1,4 @@
629-# Copyright 2009 Canonical Ltd. This software is licensed under the
630+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
631 # GNU Affero General Public License version 3 (see the file LICENSE).
632
633 __metaclass__ = type
634@@ -37,10 +37,11 @@
635 ISprint,
636 ISprintSet,
637 )
638-from lp.blueprints.model.specification import (
639+from lp.blueprints.model.specification import HasSpecificationsMixin
640+from lp.blueprints.model.specificationsearch import (
641+ get_specification_active_product_filter,
642 get_specification_filters,
643- HasSpecificationsMixin,
644- visible_specification_query,
645+ get_specification_privacy_filter,
646 )
647 from lp.blueprints.model.sprintattendance import SprintAttendance
648 from lp.blueprints.model.sprintspecification import SprintSpecification
649@@ -118,14 +119,16 @@
650 specifications() method because we want to reuse this query in the
651 specificationLinks() method.
652 """
653- # import here to avoid circular deps
654+ # Avoid circular imports.
655 from lp.blueprints.model.specification import Specification
656- tables, query = visible_specification_query(user)
657+ tables, query = get_specification_active_product_filter(self)
658+ tables.insert(0, Specification)
659+ query.append(get_specification_privacy_filter(user))
660 tables.append(Join(
661 SprintSpecification,
662- SprintSpecification.specification == Specification.id
663- ))
664- query.extend([SprintSpecification.sprintID == self.id])
665+ SprintSpecification.specification == Specification.id))
666+ query.append(SprintSpecification.sprintID == self.id)
667+
668 if not filter:
669 # filter could be None or [] then we decide the default
670 # which for a sprint is to show everything approved
671@@ -153,7 +156,7 @@
672 if len(statuses) > 0:
673 query.append(Or(*statuses))
674 # Filter for specification text
675- query.extend(get_specification_filters(filter))
676+ query.extend(get_specification_filters(filter, goalstatus=False))
677 return tables, query
678
679 def all_specifications(self, user):
680
681=== modified file 'lib/lp/blueprints/tests/test_hasspecifications.py'
682--- lib/lp/blueprints/tests/test_hasspecifications.py 2012-09-26 19:10:28 +0000
683+++ lib/lp/blueprints/tests/test_hasspecifications.py 2013-01-22 06:44:52 +0000
684@@ -1,4 +1,4 @@
685-# Copyright 2010 Canonical Ltd. This software is licensed under the
686+# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
687 # GNU Affero General Public License version 3 (see the file LICENSE).
688
689 """Unit tests for objects implementing IHasSpecifications."""
690@@ -141,16 +141,13 @@
691 product1 = self.factory.makeProduct(project=projectgroup)
692 product2 = self.factory.makeProduct(project=projectgroup)
693 product3 = self.factory.makeProduct(project=other_projectgroup)
694- self.factory.makeSpecification(
695- product=product1, name="spec1")
696+ self.factory.makeSpecification(product=product1, name="spec1")
697 self.factory.makeSpecification(
698 product=product2, name="spec2",
699 status=SpecificationDefinitionStatus.OBSOLETE)
700- self.factory.makeSpecification(
701- product=product3, name="spec3")
702+ self.factory.makeSpecification(product=product3, name="spec3")
703 self.assertNamesOfSpecificationsAre(
704- ["spec1", "spec2"],
705- projectgroup._valid_specifications)
706+ ["spec1"], projectgroup._valid_specifications)
707
708 def test_person_all_specifications(self):
709 person = self.factory.makePerson(name="james-w")
710
711=== modified file 'lib/lp/blueprints/tests/test_specification.py'
712--- lib/lp/blueprints/tests/test_specification.py 2012-12-26 01:04:05 +0000
713+++ lib/lp/blueprints/tests/test_specification.py 2013-01-22 06:44:52 +0000
714@@ -1,4 +1,4 @@
715-# Copyright 2010-2012 Canonical Ltd. This software is licensed under the
716+# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
717 # GNU Affero General Public License version 3 (see the file LICENSE).
718
719 """Unit tests for Specification."""
720@@ -41,9 +41,9 @@
721 )
722 from lp.blueprints.errors import TargetAlreadyHasSpecification
723 from lp.blueprints.interfaces.specification import ISpecificationSet
724-from lp.blueprints.model.specification import (
725- Specification,
726- visible_specification_query,
727+from lp.blueprints.model.specification import Specification
728+from lp.blueprints.model.specificationsearch import (
729+ get_specification_privacy_filter,
730 )
731 from lp.registry.enums import (
732 SharingPermission,
733@@ -407,48 +407,44 @@
734 specification.target.owner, specification,
735 error_expected=False, attribute='name', value='foo')
736
737- def test_visible_specification_query(self):
738- # visible_specification_query returns a Storm expression
739- # that can be used to filter specifications by their visibility-
740+ def _fetch_specs_visible_for_user(self, user):
741+ return Store.of(self.product).find(
742+ Specification,
743+ Specification.productID == self.product.id,
744+ *get_specification_privacy_filter(user))
745+
746+ def test_get_specification_privacy_filter(self):
747+ # get_specification_privacy_filter returns a Storm expression
748+ # that can be used to filter specifications by their visibility.
749 owner = self.factory.makePerson()
750- product = self.factory.makeProduct(
751+ self.product = self.factory.makeProduct(
752 owner=owner,
753 specification_sharing_policy=(
754 SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
755- public_spec = self.factory.makeSpecification(product=product)
756+ public_spec = self.factory.makeSpecification(product=self.product)
757 proprietary_spec_1 = self.factory.makeSpecification(
758- product=product, information_type=InformationType.PROPRIETARY)
759+ product=self.product, information_type=InformationType.PROPRIETARY)
760 proprietary_spec_2 = self.factory.makeSpecification(
761- product=product, information_type=InformationType.PROPRIETARY)
762+ product=self.product, information_type=InformationType.PROPRIETARY)
763 all_specs = [
764 public_spec, proprietary_spec_1, proprietary_spec_2]
765- store = Store.of(product)
766- tables, query = visible_specification_query(None)
767- specs_for_anon = store.using(*tables).find(
768- Specification,
769- Specification.productID == product.id, *query)
770- self.assertContentEqual([public_spec],
771- specs_for_anon.config(distinct=True))
772+ specs_for_anon = self._fetch_specs_visible_for_user(None)
773+ self.assertContentEqual(
774+ [public_spec], specs_for_anon.config(distinct=True))
775 # Product owners havae grants on the product, the privacy
776 # filter returns thus all specifications for them.
777- tables, query = visible_specification_query(owner.id)
778- specs_for_owner = store.using(*tables).find(
779- Specification, Specification.productID == product.id, *query)
780+ specs_for_owner = self._fetch_specs_visible_for_user(owner)
781 self.assertContentEqual(all_specs, specs_for_owner)
782 # The filter returns only public specs for ordinary users.
783 user = self.factory.makePerson()
784- tables, query = visible_specification_query(user.id)
785- specs_for_other_user = store.using(*tables).find(
786- Specification, Specification.productID == product.id, *query)
787+ specs_for_other_user = self._fetch_specs_visible_for_user(user)
788 self.assertContentEqual([public_spec], specs_for_other_user)
789 # If the user has a grant for a specification, the filter returns
790 # this specification too.
791 with person_logged_in(owner):
792 getUtility(IService, 'sharing').ensureAccessGrants(
793 [user], owner, specifications=[proprietary_spec_1])
794- tables, query = visible_specification_query(user.id)
795- specs_for_other_user = store.using(*tables).find(
796- Specification, Specification.productID == product.id, *query)
797+ specs_for_other_user = self._fetch_specs_visible_for_user(user)
798 self.assertContentEqual(
799 [public_spec, proprietary_spec_1], specs_for_other_user)
800
801
802=== modified file 'lib/lp/registry/doc/distroseries.txt'
803--- lib/lp/registry/doc/distroseries.txt 2012-12-26 01:32:19 +0000
804+++ lib/lp/registry/doc/distroseries.txt 2013-01-22 06:44:52 +0000
805@@ -458,7 +458,8 @@
806
807 >>> for summary in hoary.getPrioritizedUnlinkedSourcePackages():
808 ... print summary['package'].name
809- ... print '%(bug_count)s %(total_messages)s' % summary
810+ ... naked_summary = removeSecurityProxy(summary)
811+ ... print '%(bug_count)s %(total_messages)s' % naked_summary
812 pmount 0 64
813 alsa-utils 0 0
814 cnews 0 0
815@@ -630,7 +631,7 @@
816 ... PackagePublishingPocket.RELEASE, component_main,
817 ... warty.main_archive)
818 >>> spphs.count()
819- 5
820+ 5
821 >>> for name in sorted(set(
822 ... pkgpub.sourcepackagerelease.sourcepackagename.name
823 ... for pkgpub in spphs)):
824
825=== modified file 'lib/lp/registry/model/distribution.py'
826--- lib/lp/registry/model/distribution.py 2012-11-15 20:54:45 +0000
827+++ lib/lp/registry/model/distribution.py 2013-01-22 06:44:52 +0000
828@@ -1,4 +1,4 @@
829-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
830+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
831 # GNU Affero General Public License version 3 (see the file LICENSE).
832
833 """Database classes for implementing distribution items."""
834@@ -69,15 +69,11 @@
835 valid_name,
836 )
837 from lp.archivepublisher.debversion import Version
838-from lp.blueprints.enums import (
839- SpecificationDefinitionStatus,
840- SpecificationFilter,
841- SpecificationImplementationStatus,
842- )
843 from lp.blueprints.model.specification import (
844 HasSpecificationsMixin,
845 Specification,
846 )
847+from lp.blueprints.model.specificationsearch import search_specifications
848 from lp.blueprints.model.sprint import HasSprintsMixin
849 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
850 from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
851@@ -882,86 +878,9 @@
852 - informationalness: we will show ANY if nothing is said
853
854 """
855-
856- # Make a new list of the filter, so that we do not mutate what we
857- # were passed as a filter
858- if not filter:
859- # it could be None or it could be []
860- filter = [SpecificationFilter.INCOMPLETE]
861-
862- # now look at the filter and fill in the unsaid bits
863-
864- # defaults for completeness: if nothing is said about completeness
865- # then we want to show INCOMPLETE
866- completeness = False
867- for option in [
868- SpecificationFilter.COMPLETE,
869- SpecificationFilter.INCOMPLETE]:
870- if option in filter:
871- completeness = True
872- if completeness is False:
873- filter.append(SpecificationFilter.INCOMPLETE)
874-
875- # defaults for acceptance: in this case we have nothing to do
876- # because specs are not accepted/declined against a distro
877-
878- # defaults for informationalness: we don't have to do anything
879- # because the default if nothing is said is ANY
880-
881- order = self._specification_sort(sort)
882-
883- # figure out what set of specifications we are interested in. for
884- # distributions, we need to be able to filter on the basis of:
885- #
886- # - completeness. by default, only incomplete specs shown
887- # - informational.
888- #
889- base = 'Specification.distribution = %s' % self.id
890- query = base
891- # look for informational specs
892- if SpecificationFilter.INFORMATIONAL in filter:
893- query += (' AND Specification.implementation_status = %s ' %
894- quote(SpecificationImplementationStatus.INFORMATIONAL))
895-
896- # filter based on completion. see the implementation of
897- # Specification.is_complete() for more details
898- completeness = Specification.completeness_clause
899-
900- if SpecificationFilter.COMPLETE in filter:
901- query += ' AND ( %s ) ' % completeness
902- elif SpecificationFilter.INCOMPLETE in filter:
903- query += ' AND NOT ( %s ) ' % completeness
904-
905- # Filter for validity. If we want valid specs only then we should
906- # exclude all OBSOLETE or SUPERSEDED specs
907- if SpecificationFilter.VALID in filter:
908- query += (' AND Specification.definition_status NOT IN '
909- '( %s, %s ) ' % sqlvalues(
910- SpecificationDefinitionStatus.OBSOLETE,
911- SpecificationDefinitionStatus.SUPERSEDED))
912-
913- # ALL is the trump card
914- if SpecificationFilter.ALL in filter:
915- query = base
916-
917- # Filter for specification text
918- for constraint in filter:
919- if isinstance(constraint, basestring):
920- # a string in the filter is a text search filter
921- query += ' AND Specification.fti @@ ftq(%s) ' % quote(
922- constraint)
923-
924- if prejoin_people:
925- results = self._preload_specifications_people([Specification],
926- query)
927- else:
928- results = Store.of(self).find(
929- Specification,
930- SQL(query))
931- results.order_by(order)
932- if quantity is not None:
933- results = results[:quantity]
934- return results
935+ base_clauses = [Specification.distributionID == self.id]
936+ return search_specifications(
937+ self, base_clauses, user, sort, quantity, filter, prejoin_people)
938
939 def getSpecification(self, name):
940 """See `ISpecificationTarget`."""
941
942=== modified file 'lib/lp/registry/model/distroseries.py'
943--- lib/lp/registry/model/distroseries.py 2012-12-14 00:36:37 +0000
944+++ lib/lp/registry/model/distroseries.py 2013-01-22 06:44:52 +0000
945@@ -1,4 +1,4 @@
946-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
947+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
948 # GNU Affero General Public License version 3 (see the file LICENSE).
949
950 """Database classes for a distribution series."""
951@@ -42,17 +42,12 @@
952 from lp.app.enums import service_uses_launchpad
953 from lp.app.errors import NotFoundError
954 from lp.app.interfaces.launchpad import IServiceUsage
955-from lp.blueprints.enums import (
956- SpecificationFilter,
957- SpecificationGoalStatus,
958- SpecificationImplementationStatus,
959- SpecificationSort,
960- )
961 from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
962 from lp.blueprints.model.specification import (
963 HasSpecificationsMixin,
964 Specification,
965 )
966+from lp.blueprints.model.specificationsearch import search_specifications
967 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
968 from lp.bugs.interfaces.bugtarget import ISeriesBugTarget
969 from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
970@@ -110,7 +105,6 @@
971 from lp.services.database.sqlbase import (
972 flush_database_caches,
973 flush_database_updates,
974- quote,
975 SQLBase,
976 sqlvalues,
977 )
978@@ -787,109 +781,10 @@
979 - informationalness: if nothing is said, ANY
980
981 """
982-
983- # Make a new list of the filter, so that we do not mutate what we
984- # were passed as a filter
985- if not filter:
986- # filter could be None or [] then we decide the default
987- # which for a distroseries is to show everything approved
988- filter = [SpecificationFilter.ACCEPTED]
989-
990- # defaults for completeness: in this case we don't actually need to
991- # do anything, because the default is ANY
992-
993- # defaults for acceptance: in this case, if nothing is said about
994- # acceptance, we want to show only accepted specs
995- acceptance = False
996- for option in [
997- SpecificationFilter.ACCEPTED,
998- SpecificationFilter.DECLINED,
999- SpecificationFilter.PROPOSED]:
1000- if option in filter:
1001- acceptance = True
1002- if acceptance is False:
1003- filter.append(SpecificationFilter.ACCEPTED)
1004-
1005- # defaults for informationalness: we don't have to do anything
1006- # because the default if nothing is said is ANY
1007-
1008- # sort by priority descending, by default
1009- if sort is None or sort == SpecificationSort.PRIORITY:
1010- order = ['-priority', 'Specification.definition_status',
1011- 'Specification.name']
1012- elif sort == SpecificationSort.DATE:
1013- # we are showing specs for a GOAL, so under some circumstances
1014- # we care about the order in which the specs were nominated for
1015- # the goal, and in others we care about the order in which the
1016- # decision was made.
1017-
1018- # we need to establish if the listing will show specs that have
1019- # been decided only, or will include proposed specs.
1020- show_proposed = set([
1021- SpecificationFilter.ALL,
1022- SpecificationFilter.PROPOSED,
1023- ])
1024- if len(show_proposed.intersection(set(filter))) > 0:
1025- # we are showing proposed specs so use the date proposed
1026- # because not all specs will have a date decided.
1027- order = ['-Specification.datecreated', 'Specification.id']
1028- else:
1029- # this will show only decided specs so use the date the spec
1030- # was accepted or declined for the sprint
1031- order = ['-Specification.date_goal_decided',
1032- '-Specification.datecreated',
1033- 'Specification.id']
1034-
1035- # figure out what set of specifications we are interested in. for
1036- # distroseries, we need to be able to filter on the basis of:
1037- #
1038- # - completeness.
1039- # - goal status.
1040- # - informational.
1041- #
1042- base = 'Specification.distroseries = %s' % self.id
1043- query = base
1044- # look for informational specs
1045- if SpecificationFilter.INFORMATIONAL in filter:
1046- query += (' AND Specification.implementation_status = %s' %
1047- quote(SpecificationImplementationStatus.INFORMATIONAL))
1048-
1049- # filter based on completion. see the implementation of
1050- # Specification.is_complete() for more details
1051- completeness = Specification.completeness_clause
1052-
1053- if SpecificationFilter.COMPLETE in filter:
1054- query += ' AND ( %s ) ' % completeness
1055- elif SpecificationFilter.INCOMPLETE in filter:
1056- query += ' AND NOT ( %s ) ' % completeness
1057-
1058- # look for specs that have a particular goalstatus (proposed,
1059- # accepted or declined)
1060- if SpecificationFilter.ACCEPTED in filter:
1061- query += ' AND Specification.goalstatus = %d' % (
1062- SpecificationGoalStatus.ACCEPTED.value)
1063- elif SpecificationFilter.PROPOSED in filter:
1064- query += ' AND Specification.goalstatus = %d' % (
1065- SpecificationGoalStatus.PROPOSED.value)
1066- elif SpecificationFilter.DECLINED in filter:
1067- query += ' AND Specification.goalstatus = %d' % (
1068- SpecificationGoalStatus.DECLINED.value)
1069-
1070- # ALL is the trump card
1071- if SpecificationFilter.ALL in filter:
1072- query = base
1073-
1074- # Filter for specification text
1075- for constraint in filter:
1076- if isinstance(constraint, basestring):
1077- # a string in the filter is a text search filter
1078- query += ' AND Specification.fti @@ ftq(%s) ' % quote(
1079- constraint)
1080-
1081- results = Specification.select(query, orderBy=order, limit=quantity)
1082- if prejoin_people:
1083- results = results.prejoin(['_assignee', '_approver', '_drafter'])
1084- return results
1085+ base_clauses = [Specification.distroseriesID == self.id]
1086+ return search_specifications(
1087+ self, base_clauses, user, sort, quantity, filter, prejoin_people,
1088+ default_acceptance=True)
1089
1090 def getDistroSeriesLanguage(self, language):
1091 """See `IDistroSeries`."""
1092
1093=== modified file 'lib/lp/registry/model/milestone.py'
1094--- lib/lp/registry/model/milestone.py 2013-01-07 02:40:55 +0000
1095+++ lib/lp/registry/model/milestone.py 2013-01-22 06:44:52 +0000
1096@@ -1,4 +1,4 @@
1097-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
1098+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
1099 # GNU Affero General Public License version 3 (see the file LICENSE).
1100
1101 """Milestone model classes."""
1102@@ -38,9 +38,10 @@
1103
1104 from lp.app.enums import InformationType
1105 from lp.app.errors import NotFoundError
1106-from lp.blueprints.model.specification import (
1107- Specification,
1108- visible_specification_query,
1109+from lp.blueprints.model.specification import Specification
1110+from lp.blueprints.model.specificationsearch import (
1111+ get_specification_active_product_filter,
1112+ get_specification_privacy_filter,
1113 )
1114 from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
1115 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
1116@@ -155,14 +156,15 @@
1117 def getSpecifications(self, user):
1118 """See `IMilestoneData`"""
1119 from lp.registry.model.person import Person
1120- store = Store.of(self.target)
1121- origin, clauses = visible_specification_query(user)
1122- origin.extend([
1123- LeftJoin(Person, Specification._assigneeID == Person.id),
1124- ])
1125+ origin = [Specification]
1126+ product_origin, clauses = get_specification_active_product_filter(
1127+ self)
1128+ origin.extend(product_origin)
1129+ clauses.extend(get_specification_privacy_filter(user))
1130+ origin.append(LeftJoin(Person, Specification._assigneeID == Person.id))
1131 milestones = self._milestone_ids_expr(user)
1132
1133- results = store.using(*origin).find(
1134+ results = Store.of(self.target).using(*origin).find(
1135 (Specification, Person),
1136 Specification.id.is_in(
1137 Union(
1138
1139=== modified file 'lib/lp/registry/model/person.py'
1140--- lib/lp/registry/model/person.py 2013-01-14 06:13:52 +0000
1141+++ lib/lp/registry/model/person.py 2013-01-22 06:44:52 +0000
1142@@ -125,16 +125,15 @@
1143 sanitize_name,
1144 valid_name,
1145 )
1146-from lp.blueprints.enums import (
1147- SpecificationFilter,
1148- SpecificationSort,
1149- )
1150+from lp.blueprints.enums import SpecificationFilter
1151 from lp.blueprints.model.specification import (
1152- get_specification_filters,
1153 HasSpecificationsMixin,
1154- spec_started_clause,
1155 Specification,
1156- visible_specification_query,
1157+ )
1158+from lp.blueprints.model.specificationsearch import (
1159+ get_specification_active_product_filter,
1160+ get_specification_privacy_filter,
1161+ search_specifications,
1162 )
1163 from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
1164 from lp.bugs.interfaces.bugtarget import IBugTarget
1165@@ -856,10 +855,8 @@
1166 # because the default if nothing is said is ANY.
1167
1168 roles = set([
1169- SpecificationFilter.CREATOR,
1170- SpecificationFilter.ASSIGNEE,
1171- SpecificationFilter.DRAFTER,
1172- SpecificationFilter.APPROVER,
1173+ SpecificationFilter.CREATOR, SpecificationFilter.ASSIGNEE,
1174+ SpecificationFilter.DRAFTER, SpecificationFilter.APPROVER,
1175 SpecificationFilter.SUBSCRIBER])
1176 # If no roles are given, then we want everything.
1177 if filter.intersection(roles) == set():
1178@@ -877,32 +874,18 @@
1179 role_clauses.append(
1180 Specification.id.is_in(
1181 Select(SpecificationSubscription.specificationID,
1182- [SpecificationSubscription.person == self]
1183- )))
1184- tables, clauses = visible_specification_query(user)
1185- clauses.append(Or(*role_clauses))
1186- # Defaults for completeness: if nothing is said about completeness
1187- # then we want to show INCOMPLETE.
1188+ [SpecificationSubscription.person == self])))
1189+
1190+ clauses = [Or(*role_clauses)]
1191 if SpecificationFilter.COMPLETE not in filter:
1192 if (in_progress and SpecificationFilter.INCOMPLETE not in filter
1193 and SpecificationFilter.ALL not in filter):
1194- clauses.append(spec_started_clause)
1195- filter.add(SpecificationFilter.INCOMPLETE)
1196+ filter.update(
1197+ [SpecificationFilter.INCOMPLETE,
1198+ SpecificationFilter.STARTED])
1199
1200- clauses.extend(get_specification_filters(filter))
1201- results = Store.of(self).using(*tables).find(Specification, *clauses)
1202- # The default sort is priority descending, so only explictly sort for
1203- # DATE.
1204- if sort == SpecificationSort.DATE:
1205- sort = Desc(Specification.datecreated)
1206- elif getattr(sort, 'enum', None) is SpecificationSort:
1207- sort = None
1208- if sort is not None:
1209- results = results.order_by(sort)
1210- results.config(distinct=True)
1211- if quantity is not None:
1212- results = results[:quantity]
1213- return results
1214+ return search_specifications(
1215+ self, clauses, user, sort, quantity, list(filter), prejoin_people)
1216
1217 # XXX: Tom Berger 2008-04-14 bug=191799:
1218 # The implementation of these functions
1219@@ -1482,20 +1465,22 @@
1220 from lp.registry.model.distribution import Distribution
1221 store = Store.of(self)
1222 WorkItem = SpecificationWorkItem
1223- origin, query = visible_specification_query(user)
1224+ origin = [Specification]
1225+ productjoin, query = get_specification_active_product_filter(self)
1226+ origin.extend(productjoin)
1227+ query.extend(get_specification_privacy_filter(user))
1228 origin.extend([
1229 Join(WorkItem, WorkItem.specification == Specification.id),
1230 # WorkItems may not have a milestone and in that case they inherit
1231 # the one from the spec.
1232 Join(Milestone,
1233 Coalesce(WorkItem.milestone_id,
1234- Specification.milestoneID) == Milestone.id),
1235- ])
1236+ Specification.milestoneID) == Milestone.id)])
1237 today = datetime.today().date()
1238 query.extend([
1239 Milestone.dateexpected <= date, Milestone.dateexpected >= today,
1240 WorkItem.deleted == False,
1241- OR(WorkItem.assignee_id.is_in(self.participant_ids),
1242+ Or(WorkItem.assignee_id.is_in(self.participant_ids),
1243 Specification._assigneeID.is_in(self.participant_ids))])
1244 result = store.using(*origin).find(WorkItem, *query)
1245 result.config(distinct=True)
1246@@ -1680,6 +1665,12 @@
1247 requester=reviewer)
1248 return (status_changed, tm.status)
1249
1250+ def _accept_or_decline_membership(self, team, status, comment):
1251+ tm = TeamMembership.selectOneBy(person=self, team=team)
1252+ assert tm is not None
1253+ assert tm.status == TeamMembershipStatus.INVITED
1254+ tm.setStatus(status, getUtility(ILaunchBag).user, comment=comment)
1255+
1256 # The three methods below are not in the IPerson interface because we want
1257 # to protect them with a launchpad.Edit permission. We could do that by
1258 # defining explicit permissions for all IPerson methods/attributes in
1259@@ -1691,12 +1682,8 @@
1260 the INVITED status. The status of this TeamMembership will be changed
1261 to APPROVED.
1262 """
1263- tm = TeamMembership.selectOneBy(person=self, team=team)
1264- assert tm is not None
1265- assert tm.status == TeamMembershipStatus.INVITED
1266- tm.setStatus(
1267- TeamMembershipStatus.APPROVED, getUtility(ILaunchBag).user,
1268- comment=comment)
1269+ self._accept_or_decline_membership(
1270+ team, TeamMembershipStatus.APPROVED, comment)
1271
1272 def declineInvitationToBeMemberOf(self, team, comment):
1273 """Decline an invitation to become a member of the given team.
1274@@ -1705,12 +1692,8 @@
1275 the INVITED status. The status of this TeamMembership will be changed
1276 to INVITATION_DECLINED.
1277 """
1278- tm = TeamMembership.selectOneBy(person=self, team=team)
1279- assert tm is not None
1280- assert tm.status == TeamMembershipStatus.INVITED
1281- tm.setStatus(
1282- TeamMembershipStatus.INVITATION_DECLINED,
1283- getUtility(ILaunchBag).user, comment=comment)
1284+ self._accept_or_decline_membership(
1285+ team, TeamMembershipStatus.INVITATION_DECLINED, comment)
1286
1287 def retractTeamMembership(self, team, user, comment=None):
1288 """See `IPerson`"""
1289@@ -1765,14 +1748,11 @@
1290 def getOwnedTeams(self, user=None):
1291 """See `IPerson`."""
1292 query = And(
1293- get_person_visibility_terms(user),
1294- Person.teamowner == self.id,
1295+ get_person_visibility_terms(user), Person.teamowner == self.id,
1296 Person.merged == None)
1297- store = IStore(Person)
1298- results = store.find(
1299+ return IStore(Person).find(
1300 Person, query).order_by(
1301 Upper(Person.displayname), Upper(Person.name))
1302- return results
1303
1304 @cachedproperty
1305 def administrated_teams(self):
1306
1307=== modified file 'lib/lp/registry/model/product.py'
1308--- lib/lp/registry/model/product.py 2013-01-03 05:00:59 +0000
1309+++ lib/lp/registry/model/product.py 2013-01-22 06:44:52 +0000
1310@@ -1,4 +1,4 @@
1311-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
1312+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
1313 # GNU Affero General Public License version 3 (see the file LICENSE).
1314
1315 """Database classes including and related to Product."""
1316@@ -91,13 +91,12 @@
1317 from lp.app.model.launchpad import InformationTypeMixin
1318 from lp.blueprints.enums import SpecificationFilter
1319 from lp.blueprints.model.specification import (
1320- get_specification_filters,
1321 HasSpecificationsMixin,
1322 Specification,
1323 SPECIFICATION_POLICY_ALLOWED_TYPES,
1324 SPECIFICATION_POLICY_DEFAULT_TYPES,
1325- visible_specification_query,
1326 )
1327+from lp.blueprints.model.specificationsearch import search_specifications
1328 from lp.blueprints.model.sprint import HasSprintsMixin
1329 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
1330 from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
1331@@ -1435,52 +1434,9 @@
1332 prejoin_people=True):
1333 """See `IHasSpecifications`."""
1334
1335- # Make a new list of the filter, so that we do not mutate what we
1336- # were passed as a filter
1337- if not filter:
1338- # filter could be None or [] then we decide the default
1339- # which for a product is to show incomplete specs
1340- filter = [SpecificationFilter.INCOMPLETE]
1341-
1342- # now look at the filter and fill in the unsaid bits
1343-
1344- # defaults for completeness: if nothing is said about completeness
1345- # then we want to show INCOMPLETE
1346- completeness = False
1347- for option in [
1348- SpecificationFilter.COMPLETE,
1349- SpecificationFilter.INCOMPLETE]:
1350- if option in filter:
1351- completeness = True
1352- if completeness is False:
1353- filter.append(SpecificationFilter.INCOMPLETE)
1354-
1355- # defaults for acceptance: in this case we have nothing to do
1356- # because specs are not accepted/declined against a distro
1357-
1358- # defaults for informationalness: we don't have to do anything
1359- # because the default if nothing is said is ANY
1360-
1361- order = self._specification_sort(sort)
1362-
1363- # figure out what set of specifications we are interested in. for
1364- # products, we need to be able to filter on the basis of:
1365- #
1366- # - completeness.
1367- # - informational.
1368- #
1369- tables, clauses = visible_specification_query(user)
1370- clauses.append(Specification.product == self)
1371- clauses.extend(get_specification_filters(filter))
1372- if prejoin_people:
1373- results = self._preload_specifications_people(tables, clauses)
1374- else:
1375- tableset = Store.of(self).using(*tables)
1376- results = tableset.find(Specification, *clauses)
1377- results.order_by(order).config(distinct=True)
1378- if quantity is not None:
1379- results = results[:quantity]
1380- return results
1381+ base_clauses = [Specification.productID == self.id]
1382+ return search_specifications(
1383+ self, base_clauses, user, sort, quantity, filter, prejoin_people)
1384
1385 def getSpecification(self, name):
1386 """See `ISpecificationTarget`."""
1387
1388=== modified file 'lib/lp/registry/model/productseries.py'
1389--- lib/lp/registry/model/productseries.py 2013-01-07 02:40:55 +0000
1390+++ lib/lp/registry/model/productseries.py 2013-01-22 06:44:52 +0000
1391@@ -1,4 +1,4 @@
1392-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
1393+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
1394 # GNU Affero General Public License version 3 (see the file LICENSE).
1395
1396 """Models for `IProductSeries`."""
1397@@ -38,18 +38,12 @@
1398 ILaunchpadCelebrities,
1399 IServiceUsage,
1400 )
1401-from lp.blueprints.enums import (
1402- SpecificationDefinitionStatus,
1403- SpecificationFilter,
1404- SpecificationGoalStatus,
1405- SpecificationImplementationStatus,
1406- SpecificationSort,
1407- )
1408 from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
1409 from lp.blueprints.model.specification import (
1410 HasSpecificationsMixin,
1411 Specification,
1412 )
1413+from lp.blueprints.model.specificationsearch import search_specifications
1414 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
1415 from lp.bugs.interfaces.bugtarget import ISeriesBugTarget
1416 from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
1417@@ -79,7 +73,6 @@
1418 from lp.services.database.decoratedresultset import DecoratedResultSet
1419 from lp.services.database.enumcol import EnumCol
1420 from lp.services.database.sqlbase import (
1421- quote,
1422 SQLBase,
1423 sqlvalues,
1424 )
1425@@ -334,116 +327,10 @@
1426 - informational, which defaults to showing BOTH if nothing is said
1427
1428 """
1429-
1430- # Make a new list of the filter, so that we do not mutate what we
1431- # were passed as a filter
1432- if not filter:
1433- # filter could be None or [] then we decide the default
1434- # which for a productseries is to show everything accepted
1435- filter = [SpecificationFilter.ACCEPTED]
1436-
1437- # defaults for completeness: in this case we don't actually need to
1438- # do anything, because the default is ANY
1439-
1440- # defaults for acceptance: in this case, if nothing is said about
1441- # acceptance, we want to show only accepted specs
1442- acceptance = False
1443- for option in [
1444- SpecificationFilter.ACCEPTED,
1445- SpecificationFilter.DECLINED,
1446- SpecificationFilter.PROPOSED]:
1447- if option in filter:
1448- acceptance = True
1449- if acceptance is False:
1450- filter.append(SpecificationFilter.ACCEPTED)
1451-
1452- # defaults for informationalness: we don't have to do anything
1453- # because the default if nothing is said is ANY
1454-
1455- # sort by priority descending, by default
1456- if sort is None or sort == SpecificationSort.PRIORITY:
1457- order = ['-priority', 'definition_status', 'name']
1458- elif sort == SpecificationSort.DATE:
1459- # we are showing specs for a GOAL, so under some circumstances
1460- # we care about the order in which the specs were nominated for
1461- # the goal, and in others we care about the order in which the
1462- # decision was made.
1463-
1464- # we need to establish if the listing will show specs that have
1465- # been decided only, or will include proposed specs.
1466- show_proposed = set([
1467- SpecificationFilter.ALL,
1468- SpecificationFilter.PROPOSED,
1469- ])
1470- if len(show_proposed.intersection(set(filter))) > 0:
1471- # we are showing proposed specs so use the date proposed
1472- # because not all specs will have a date decided.
1473- order = ['-Specification.datecreated', 'Specification.id']
1474- else:
1475- # this will show only decided specs so use the date the spec
1476- # was accepted or declined for the sprint
1477- order = ['-Specification.date_goal_decided',
1478- '-Specification.datecreated',
1479- 'Specification.id']
1480-
1481- # figure out what set of specifications we are interested in. for
1482- # productseries, we need to be able to filter on the basis of:
1483- #
1484- # - completeness. by default, only incomplete specs shown
1485- # - goal status. by default, only accepted specs shown
1486- # - informational.
1487- #
1488- base = 'Specification.productseries = %s' % self.id
1489- query = base
1490- # look for informational specs
1491- if SpecificationFilter.INFORMATIONAL in filter:
1492- query += (' AND Specification.implementation_status = %s' %
1493- quote(SpecificationImplementationStatus.INFORMATIONAL))
1494-
1495- # filter based on completion. see the implementation of
1496- # Specification.is_complete() for more details
1497- completeness = Specification.completeness_clause
1498-
1499- if SpecificationFilter.COMPLETE in filter:
1500- query += ' AND ( %s ) ' % completeness
1501- elif SpecificationFilter.INCOMPLETE in filter:
1502- query += ' AND NOT ( %s ) ' % completeness
1503-
1504- # look for specs that have a particular goalstatus (proposed,
1505- # accepted or declined)
1506- if SpecificationFilter.ACCEPTED in filter:
1507- query += ' AND Specification.goalstatus = %d' % (
1508- SpecificationGoalStatus.ACCEPTED.value)
1509- elif SpecificationFilter.PROPOSED in filter:
1510- query += ' AND Specification.goalstatus = %d' % (
1511- SpecificationGoalStatus.PROPOSED.value)
1512- elif SpecificationFilter.DECLINED in filter:
1513- query += ' AND Specification.goalstatus = %d' % (
1514- SpecificationGoalStatus.DECLINED.value)
1515-
1516- # Filter for validity. If we want valid specs only then we should
1517- # exclude all OBSOLETE or SUPERSEDED specs
1518- if SpecificationFilter.VALID in filter:
1519- query += (
1520- ' AND Specification.definition_status NOT IN ( %s, %s ) '
1521- % sqlvalues(SpecificationDefinitionStatus.OBSOLETE,
1522- SpecificationDefinitionStatus.SUPERSEDED))
1523-
1524- # ALL is the trump card
1525- if SpecificationFilter.ALL in filter:
1526- query = base
1527-
1528- # Filter for specification text
1529- for constraint in filter:
1530- if isinstance(constraint, basestring):
1531- # a string in the filter is a text search filter
1532- query += ' AND Specification.fti @@ ftq(%s) ' % quote(
1533- constraint)
1534-
1535- results = Specification.select(query, orderBy=order, limit=quantity)
1536- if prejoin_people:
1537- results = results.prejoin(['_assignee', '_approver', '_drafter'])
1538- return results
1539+ base_clauses = [Specification.productseriesID == self.id]
1540+ return search_specifications(
1541+ self, base_clauses, user, sort, quantity, filter, prejoin_people,
1542+ default_acceptance=True)
1543
1544 def _customizeSearchParams(self, search_params):
1545 """Customize `search_params` for this product series."""
1546
1547=== modified file 'lib/lp/registry/model/projectgroup.py'
1548--- lib/lp/registry/model/projectgroup.py 2013-01-07 02:40:55 +0000
1549+++ lib/lp/registry/model/projectgroup.py 2013-01-22 06:44:52 +0000
1550@@ -1,4 +1,4 @@
1551-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
1552+# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
1553 # GNU Affero General Public License version 3 (see the file LICENSE).
1554
1555 """Launchpad ProjectGroup-related Database Table Objects."""
1556@@ -44,16 +44,12 @@
1557 IHasLogo,
1558 IHasMugshot,
1559 )
1560-from lp.blueprints.enums import (
1561- SpecificationFilter,
1562- SpecificationImplementationStatus,
1563- SpecificationSort,
1564- SprintSpecificationStatus,
1565- )
1566+from lp.blueprints.enums import SprintSpecificationStatus
1567 from lp.blueprints.model.specification import (
1568 HasSpecificationsMixin,
1569 Specification,
1570 )
1571+from lp.blueprints.model.specificationsearch import search_specifications
1572 from lp.blueprints.model.sprint import HasSprintsMixin
1573 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
1574 from lp.bugs.model.bugtarget import (
1575@@ -96,7 +92,6 @@
1576 from lp.services.database.datetimecol import UtcDateTimeCol
1577 from lp.services.database.enumcol import EnumCol
1578 from lp.services.database.sqlbase import (
1579- quote,
1580 SQLBase,
1581 sqlvalues,
1582 )
1583@@ -251,70 +246,18 @@
1584 def specifications(self, user, sort=None, quantity=None, filter=None,
1585 series=None, prejoin_people=True):
1586 """See `IHasSpecifications`."""
1587-
1588- # Make a new list of the filter, so that we do not mutate what we
1589- # were passed as a filter
1590- if not filter:
1591- # filter could be None or [] then we decide the default
1592- # which for a project group is to show incomplete specs
1593- filter = [SpecificationFilter.INCOMPLETE]
1594-
1595- # sort by priority descending, by default
1596- if sort is None or sort == SpecificationSort.PRIORITY:
1597- order = ['-priority', 'Specification.definition_status',
1598- 'Specification.name']
1599- elif sort == SpecificationSort.DATE:
1600- order = ['-Specification.datecreated', 'Specification.id']
1601-
1602- # figure out what set of specifications we are interested in. for
1603- # project groups, we need to be able to filter on the basis of:
1604- #
1605- # - completeness. by default, only incomplete specs shown
1606- # - informational.
1607- #
1608- base = """
1609- Specification.product = Product.id AND
1610- Product.active IS TRUE AND
1611- Product.project = %s
1612- """ % self.id
1613- query = base
1614- # look for informational specs
1615- if SpecificationFilter.INFORMATIONAL in filter:
1616- query += (' AND Specification.implementation_status = %s' %
1617- quote(SpecificationImplementationStatus.INFORMATIONAL))
1618-
1619- # filter based on completion. see the implementation of
1620- # Specification.is_complete() for more details
1621- completeness = Specification.completeness_clause
1622-
1623- if SpecificationFilter.COMPLETE in filter:
1624- query += ' AND ( %s ) ' % completeness
1625- elif SpecificationFilter.INCOMPLETE in filter:
1626- query += ' AND NOT ( %s ) ' % completeness
1627-
1628- # ALL is the trump card
1629- if SpecificationFilter.ALL in filter:
1630- query = base
1631-
1632- # Filter for specification text
1633- for constraint in filter:
1634- if isinstance(constraint, basestring):
1635- # a string in the filter is a text search filter
1636- query += ' AND Specification.fti @@ ftq(%s) ' % quote(
1637- constraint)
1638-
1639- clause_tables = ['Product']
1640- if series is not None:
1641- query += ('AND Specification.productseries = ProductSeries.id'
1642- ' AND ProductSeries.name = %s'
1643- % sqlvalues(series))
1644- clause_tables.append('ProductSeries')
1645-
1646- results = Specification.select(query, orderBy=order, limit=quantity,
1647- clauseTables=clause_tables)
1648- if prejoin_people:
1649- results = results.prejoin(['_assignee', '_approver', '_drafter'])
1650- return results
1651+ base_clauses = [
1652+ Specification.productID == Product.id,
1653+ Product.projectID == self.id]
1654+ tables = [Specification]
1655+ if series:
1656+ base_clauses.append(ProductSeries.name == series)
1657+ tables.append(
1658+ Join(ProductSeries,
1659+ Specification.productseriesID == ProductSeries.id))
1660+ return search_specifications(
1661+ self, base_clauses, user, sort, quantity, filter, prejoin_people,
1662+ tables=tables)
1663
1664 def _customizeSearchParams(self, search_params):
1665 """Customize `search_params` for this milestone."""
1666
1667=== modified file 'lib/lp/registry/model/sharingjob.py'
1668--- lib/lp/registry/model/sharingjob.py 2012-11-16 20:30:12 +0000
1669+++ lib/lp/registry/model/sharingjob.py 2013-01-22 06:44:52 +0000
1670@@ -1,7 +1,6 @@
1671-# Copyright 2012 Canonical Ltd. This software is licensed under the
1672+# Copyright 2012-2013 Canonical Ltd. This software is licensed under the
1673 # GNU Affero General Public License version 3 (see the file LICENSE).
1674
1675-
1676 """Job classes related to the sharing feature are in here."""
1677
1678 __metaclass__ = type
1679@@ -43,9 +42,9 @@
1680
1681 from lp.app.enums import InformationType
1682 from lp.blueprints.interfaces.specification import ISpecification
1683-from lp.blueprints.model.specification import (
1684- Specification,
1685- visible_specification_query,
1686+from lp.blueprints.model.specification import Specification
1687+from lp.blueprints.model.specificationsearch import (
1688+ get_specification_privacy_filter,
1689 )
1690 from lp.blueprints.model.specificationsubscription import (
1691 SpecificationSubscription,
1692@@ -439,8 +438,8 @@
1693 sub.branch.unsubscribe(
1694 sub.person, self.requestor, ignore_permissions=True)
1695 if specification_filters:
1696- specification_filters.append(
1697- spec_not_visible(SpecificationSubscription.personID))
1698+ specification_filters.append(Not(*get_specification_privacy_filter(
1699+ SpecificationSubscription.personID)))
1700 tables = (
1701 SpecificationSubscription,
1702 Join(
1703@@ -454,10 +453,3 @@
1704 for sub in specifications_subscriptions:
1705 sub.specification.unsubscribe(
1706 sub.person, self.requestor, ignore_permissions=True)
1707-
1708-
1709-def spec_not_visible(person_id):
1710- """Return an expression for finding specs not visible to the person."""
1711- tables, clauses = visible_specification_query(person_id)
1712- subselect = Select(Specification.id, tables=tables, where=And(clauses))
1713- return Not(Specification.id.is_in(subselect))