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
=== modified file 'lib/lp/blueprints/browser/specificationtarget.py'
--- lib/lp/blueprints/browser/specificationtarget.py 2012-09-27 15:28:38 +0000
+++ lib/lp/blueprints/browser/specificationtarget.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""ISpecificationTarget browser views."""4"""ISpecificationTarget browser views."""
@@ -347,8 +347,7 @@
347 and self.context.private347 and self.context.private
348 and not check_permission('launchpad.View', self.context)):348 and not check_permission('launchpad.View', self.context)):
349 return []349 return []
350 filter = self.spec_filter350 return self.context.specifications(self.user, filter=self.spec_filter)
351 return self.context.specifications(self.user, filter=filter)
352351
353 @cachedproperty352 @cachedproperty
354 def specs_batched(self):353 def specs_batched(self):
355354
=== modified file 'lib/lp/blueprints/enums.py'
--- lib/lp/blueprints/enums.py 2012-10-22 20:04:30 +0000
+++ lib/lp/blueprints/enums.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Enumerations used in the lp/blueprints modules."""4"""Enumerations used in the lp/blueprints modules."""
@@ -334,6 +334,13 @@
334 to which the person has subscribed.334 to which the person has subscribed.
335 """)335 """)
336336
337 STARTED = DBItem(110, """
338 Started
339
340 This indicates that the list should include specifications that are
341 marked as started.
342 """)
343
337344
338class SpecificationSort(EnumeratedType):345class SpecificationSort(EnumeratedType):
339 """The scheme to sort the results of a specifications query.346 """The scheme to sort the results of a specifications query.
340347
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2012-12-26 01:04:05 +0000
+++ lib/lp/blueprints/model/specification.py 2013-01-22 06:44:52 +0000
@@ -1,9 +1,8 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
5__all__ = [5__all__ = [
6 'get_specification_filters',
7 'HasSpecificationsMixin',6 'HasSpecificationsMixin',
8 'recursive_blocked_query',7 'recursive_blocked_query',
9 'recursive_dependent_query',8 'recursive_dependent_query',
@@ -11,8 +10,6 @@
11 'SPECIFICATION_POLICY_ALLOWED_TYPES',10 'SPECIFICATION_POLICY_ALLOWED_TYPES',
12 'SPECIFICATION_POLICY_DEFAULT_TYPES',11 'SPECIFICATION_POLICY_DEFAULT_TYPES',
13 'SpecificationSet',12 'SpecificationSet',
14 'spec_started_clause',
15 'visible_specification_query',
16 ]13 ]
1714
18from lazr.lifecycle.event import (15from lazr.lifecycle.event import (
@@ -32,8 +29,6 @@
32 And,29 And,
33 In,30 In,
34 Join,31 Join,
35 LeftJoin,
36 Not,
37 Or,32 Or,
38 Select,33 Select,
39 )34 )
@@ -103,7 +98,6 @@
103 UTC_NOW,98 UTC_NOW,
104 )99 )
105from lp.services.database.datetimecol import UtcDateTimeCol100from lp.services.database.datetimecol import UtcDateTimeCol
106from lp.services.database.decoratedresultset import DecoratedResultSet
107from lp.services.database.enumcol import EnumCol101from lp.services.database.enumcol import EnumCol
108from lp.services.database.lpstorm import IStore102from lp.services.database.lpstorm import IStore
109from lp.services.database.sqlbase import (103from lp.services.database.sqlbase import (
@@ -111,7 +105,6 @@
111 SQLBase,105 SQLBase,
112 sqlvalues,106 sqlvalues,
113 )107 )
114from lp.services.database.stormexpr import fti_search
115from lp.services.mail.helpers import get_contact_email_addresses108from lp.services.mail.helpers import get_contact_email_addresses
116from lp.services.propertycache import (109from lp.services.propertycache import (
117 cachedproperty,110 cachedproperty,
@@ -556,46 +549,6 @@
556 """See ISpecification."""549 """See ISpecification."""
557 return not self.is_complete550 return not self.is_complete
558551
559 # Several other classes need to generate lists of specifications, and
560 # one thing they often have to filter for is completeness. We maintain
561 # this single canonical query string here so that it does not have to be
562 # cargo culted into Product, Distribution, ProductSeries etc
563
564 # Also note that there is a constraint in the database which ensures
565 # that date_completed is set if the spec is complete, and that db
566 # constraint parrots this definition exactly.
567
568 # NB NB NB if you change this definition PLEASE update the db constraint
569 # Specification.specification_completion_recorded_chk !!!
570 completeness_clause = ("""
571 Specification.implementation_status = %s OR
572 Specification.definition_status IN ( %s, %s ) OR
573 (Specification.implementation_status = %s AND
574 Specification.definition_status = %s)
575 """ % sqlvalues(SpecificationImplementationStatus.IMPLEMENTED.value,
576 SpecificationDefinitionStatus.OBSOLETE.value,
577 SpecificationDefinitionStatus.SUPERSEDED.value,
578 SpecificationImplementationStatus.INFORMATIONAL.value,
579 SpecificationDefinitionStatus.APPROVED.value))
580
581 @classmethod
582 def storm_completeness(cls):
583 """Storm version of the above."""
584 return Or(
585 cls.implementation_status ==
586 SpecificationImplementationStatus.IMPLEMENTED,
587 cls.definition_status.is_in([
588 SpecificationDefinitionStatus.OBSOLETE,
589 SpecificationDefinitionStatus.SUPERSEDED,
590 ]),
591 And(
592 cls.implementation_status ==
593 SpecificationImplementationStatus.INFORMATIONAL,
594 cls.definition_status ==
595 SpecificationDefinitionStatus.APPROVED
596 ),
597 )
598
599 @property552 @property
600 def is_complete(self):553 def is_complete(self):
601 """See `ISpecification`."""554 """See `ISpecification`."""
@@ -1051,54 +1004,6 @@
1051 elif sort == SpecificationSort.DATE:1004 elif sort == SpecificationSort.DATE:
1052 return (Desc(Specification.datecreated), Specification.id)1005 return (Desc(Specification.datecreated), Specification.id)
10531006
1054 def _preload_specifications_people(self, tables, clauses):
1055 """Perform eager loading of people and their validity for query.
1056
1057 :param query: a string query generated in the 'specifications'
1058 method.
1059 :return: A DecoratedResultSet with Person precaching setup.
1060 """
1061 # Circular import.
1062 if isinstance(clauses, basestring):
1063 clauses = [SQL(clauses)]
1064
1065 def cache_people(rows):
1066 """DecoratedResultSet pre_iter_hook to eager load Person
1067 attributes.
1068 """
1069 from lp.registry.model.person import Person
1070 # Find the people we need:
1071 person_ids = set()
1072 for spec in rows:
1073 person_ids.add(spec._assigneeID)
1074 person_ids.add(spec._approverID)
1075 person_ids.add(spec._drafterID)
1076 person_ids.discard(None)
1077 if not person_ids:
1078 return
1079 # Query those people
1080 origin = [Person]
1081 columns = [Person]
1082 validity_info = Person._validity_queries()
1083 origin.extend(validity_info["joins"])
1084 columns.extend(validity_info["tables"])
1085 decorators = validity_info["decorators"]
1086 personset = IStore(Specification).using(*origin).find(
1087 tuple(columns),
1088 Person.id.is_in(person_ids),
1089 )
1090 for row in personset:
1091 person = row[0]
1092 index = 1
1093 for decorator in decorators:
1094 column = row[index]
1095 index += 1
1096 decorator(person, column)
1097
1098 results = IStore(Specification).using(*tables).find(
1099 Specification, *clauses)
1100 return DecoratedResultSet(results, pre_iter_hook=cache_people)
1101
1102 @property1007 @property
1103 def _all_specifications(self):1008 def _all_specifications(self):
1104 """See IHasSpecifications."""1009 """See IHasSpecifications."""
@@ -1155,41 +1060,10 @@
11551060
1156 def specifications(self, user, sort=None, quantity=None, filter=None,1061 def specifications(self, user, sort=None, quantity=None, filter=None,
1157 prejoin_people=True):1062 prejoin_people=True):
1158 store = IStore(Specification)1063 from lp.blueprints.model.specificationsearch import (
11591064 search_specifications)
1160 # Take the visibility due to privacy into account.1065 return search_specifications(
1161 privacy_tables, clauses = visible_specification_query(user)1066 self, [], user, sort, quantity, filter, prejoin_people)
1162
1163 if not filter:
1164 # Default to showing incomplete specs
1165 filter = [SpecificationFilter.INCOMPLETE]
1166
1167 spec_clauses = get_specification_filters(filter)
1168 clauses.extend(spec_clauses)
1169
1170 # sort by priority descending, by default
1171 if sort is None or sort == SpecificationSort.PRIORITY:
1172 order = [Desc(Specification.priority),
1173 Specification.definition_status,
1174 Specification.name]
1175
1176 elif sort == SpecificationSort.DATE:
1177 if SpecificationFilter.COMPLETE in filter:
1178 # if we are showing completed, we care about date completed
1179 order = [Desc(Specification.date_completed),
1180 Specification.id]
1181 else:
1182 # if not specially looking for complete, we care about date
1183 # registered
1184 order = [Desc(Specification.datecreated), Specification.id]
1185
1186 if prejoin_people:
1187 results = self._preload_specifications_people(
1188 privacy_tables, clauses)
1189 else:
1190 results = store.using(*privacy_tables).find(
1191 Specification, *clauses)
1192 return results.order_by(*order)[:quantity]
11931067
1194 def getByURL(self, url):1068 def getByURL(self, url):
1195 """See ISpecificationSet."""1069 """See ISpecificationSet."""
@@ -1264,101 +1138,3 @@
1264 def get(self, spec_id):1138 def get(self, spec_id):
1265 """See lp.blueprints.interfaces.specification.ISpecificationSet."""1139 """See lp.blueprints.interfaces.specification.ISpecificationSet."""
1266 return Specification.get(spec_id)1140 return Specification.get(spec_id)
1267
1268
1269def visible_specification_query(user):
1270 """Return a Storm expression and list of tables for filtering
1271 specifications by privacy.
1272
1273 :param user: A Person ID or a column reference.
1274 :return: A tuple of tables, clauses to filter out specifications that the
1275 user cannot see.
1276 """
1277 from lp.registry.model.product import Product
1278 from lp.registry.model.accesspolicy import (
1279 AccessArtifact,
1280 AccessPolicy,
1281 AccessPolicyGrantFlat,
1282 )
1283 tables = [
1284 Specification,
1285 LeftJoin(Product, Specification.productID == Product.id),
1286 LeftJoin(AccessPolicy, And(
1287 Or(Specification.productID == AccessPolicy.product_id,
1288 Specification.distributionID ==
1289 AccessPolicy.distribution_id),
1290 Specification.information_type == AccessPolicy.type)),
1291 LeftJoin(AccessPolicyGrantFlat,
1292 AccessPolicy.id == AccessPolicyGrantFlat.policy_id),
1293 LeftJoin(
1294 TeamParticipation,
1295 And(AccessPolicyGrantFlat.grantee == TeamParticipation.teamID,
1296 TeamParticipation.person == user)),
1297 LeftJoin(AccessArtifact,
1298 AccessPolicyGrantFlat.abstract_artifact_id ==
1299 AccessArtifact.id)
1300 ]
1301 clauses = [
1302 Or(Specification.information_type.is_in(PUBLIC_INFORMATION_TYPES),
1303 And(AccessPolicyGrantFlat.id != None,
1304 TeamParticipation.personID != None,
1305 Or(AccessPolicyGrantFlat.abstract_artifact == None,
1306 AccessArtifact.specification_id == Specification.id))),
1307 Or(Specification.product == None, Product.active == True)]
1308 return tables, clauses
1309
1310
1311def get_specification_filters(filter):
1312 """Return a list of Storm expressions for filtering Specifications.
1313
1314 :param filters: A collection of SpecificationFilter and/or strings.
1315 Strings are used for text searches.
1316 """
1317 clauses = []
1318 # ALL is the trump card.
1319 if SpecificationFilter.ALL in filter:
1320 return clauses
1321 # Look for informational specs.
1322 if SpecificationFilter.INFORMATIONAL in filter:
1323 clauses.append(Specification.implementation_status ==
1324 SpecificationImplementationStatus.INFORMATIONAL)
1325 # Filter based on completion. See the implementation of
1326 # Specification.is_complete() for more details.
1327 if SpecificationFilter.COMPLETE in filter:
1328 clauses.append(Specification.storm_completeness())
1329 if SpecificationFilter.INCOMPLETE in filter:
1330 clauses.append(Not(Specification.storm_completeness()))
1331
1332 # Filter for validity. If we want valid specs only, then we should exclude
1333 # all OBSOLETE or SUPERSEDED specs.
1334 if SpecificationFilter.VALID in filter:
1335 clauses.append(Not(Specification.definition_status.is_in([
1336 SpecificationDefinitionStatus.OBSOLETE,
1337 SpecificationDefinitionStatus.SUPERSEDED,
1338 ])))
1339 # Filter for specification text.
1340 for constraint in filter:
1341 if isinstance(constraint, basestring):
1342 # A string in the filter is a text search filter.
1343 clauses.append(fti_search(Specification, constraint))
1344 return clauses
1345
1346
1347# NB NB If you change this definition, please update the equivalent
1348# DB constraint Specification.specification_start_recorded_chk
1349# We choose to define "started" as the set of delivery states NOT
1350# in the values we select. Another option would be to say "anything less
1351# than a threshold" and to comment the dbschema that "anything not
1352# started should be less than the threshold". We'll see how maintainable
1353# this is.
1354spec_started_clause = Or(Not(Specification.implementation_status.is_in([
1355 SpecificationImplementationStatus.UNKNOWN,
1356 SpecificationImplementationStatus.NOTSTARTED,
1357 SpecificationImplementationStatus.DEFERRED,
1358 SpecificationImplementationStatus.INFORMATIONAL,
1359 ])),
1360 And(Specification.implementation_status ==
1361 SpecificationImplementationStatus.INFORMATIONAL,
1362 Specification.definition_status ==
1363 SpecificationDefinitionStatus.APPROVED
1364 ))
13651141
=== added file 'lib/lp/blueprints/model/specificationsearch.py'
--- lib/lp/blueprints/model/specificationsearch.py 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/model/specificationsearch.py 2013-01-22 06:44:52 +0000
@@ -0,0 +1,276 @@
1# Copyright 2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Helper methods to search specifications."""
5
6__metaclass__ = type
7__all__ = [
8 'get_specification_filters',
9 'get_specification_active_product_filter',
10 'get_specification_privacy_filter',
11 'search_specifications',
12 ]
13
14from storm.expr import (
15 And,
16 Coalesce,
17 Join,
18 LeftJoin,
19 Not,
20 Or,
21 Select,
22 )
23from storm.locals import (
24 Desc,
25 SQL,
26 )
27
28from lp.app.enums import PUBLIC_INFORMATION_TYPES
29from lp.blueprints.enums import (
30 SpecificationDefinitionStatus,
31 SpecificationFilter,
32 SpecificationGoalStatus,
33 SpecificationImplementationStatus,
34 SpecificationSort,
35 )
36from lp.blueprints.model.specification import Specification
37from lp.registry.interfaces.distribution import IDistribution
38from lp.registry.interfaces.distroseries import IDistroSeries
39from lp.registry.interfaces.product import IProduct
40from lp.registry.interfaces.productseries import IProductSeries
41from lp.registry.model.teammembership import TeamParticipation
42from lp.services.database.decoratedresultset import DecoratedResultSet
43from lp.services.database.lpstorm import IStore
44from lp.services.database.stormexpr import (
45 Array,
46 ArrayAgg,
47 ArrayIntersects,
48 fti_search,
49 )
50
51
52def search_specifications(context, base_clauses, user, sort=None,
53 quantity=None, spec_filter=None, prejoin_people=True,
54 tables=[], default_acceptance=False):
55 store = IStore(Specification)
56 if not default_acceptance:
57 default = SpecificationFilter.INCOMPLETE
58 options = set([
59 SpecificationFilter.COMPLETE, SpecificationFilter.INCOMPLETE])
60 else:
61 default = SpecificationFilter.ACCEPTED
62 options = set([
63 SpecificationFilter.ACCEPTED, SpecificationFilter.DECLINED,
64 SpecificationFilter.PROPOSED])
65 if not spec_filter:
66 spec_filter = [default]
67
68 if not set(spec_filter) & options:
69 spec_filter.append(default)
70
71 if not tables:
72 tables = [Specification]
73 clauses = base_clauses
74 product_table, product_clauses = get_specification_active_product_filter(
75 context)
76 tables.extend(product_table)
77 for extend in (get_specification_privacy_filter(user),
78 get_specification_filters(spec_filter), product_clauses):
79 clauses.extend(extend)
80
81 # Sort by priority descending, by default.
82 if sort is None or sort == SpecificationSort.PRIORITY:
83 order = [
84 Desc(Specification.priority), Specification.definition_status,
85 Specification.name]
86 elif sort == SpecificationSort.DATE:
87 if SpecificationFilter.COMPLETE in spec_filter:
88 # If we are showing completed, we care about date completed.
89 order = [Desc(Specification.date_completed), Specification.id]
90 else:
91 # If not specially looking for complete, we care about date
92 # registered.
93 order = []
94 show_proposed = set(
95 [SpecificationFilter.ALL, SpecificationFilter.PROPOSED])
96 if default_acceptance and not (set(spec_filter) & show_proposed):
97 order.append(Desc(Specification.date_goal_decided))
98 order.extend([Desc(Specification.datecreated), Specification.id])
99 else:
100 order = [sort]
101 if prejoin_people:
102 results = _preload_specifications_people(tables, clauses)
103 else:
104 results = store.using(*tables).find(Specification, *clauses)
105 return results.order_by(*order).config(limit=quantity)
106
107
108def get_specification_active_product_filter(context):
109 if (IDistribution.providedBy(context) or IDistroSeries.providedBy(context)
110 or IProduct.providedBy(context) or IProductSeries.providedBy(context)):
111 return [], []
112 from lp.registry.model.product import Product
113 tables = [
114 LeftJoin(Product, Specification.productID == Product.id)]
115 active_products = (
116 Or(Specification.product == None, Product.active == True))
117 return tables, [active_products]
118
119
120def get_specification_privacy_filter(user):
121 # Circular imports.
122 from lp.registry.model.accesspolicy import AccessPolicyGrant
123 public_spec_filter = (
124 Specification.information_type.is_in(PUBLIC_INFORMATION_TYPES))
125
126 if user is None:
127 return [public_spec_filter]
128
129 artifact_grant_query = Coalesce(
130 ArrayIntersects(
131 SQL('Specification.access_grants'),
132 Select(
133 ArrayAgg(TeamParticipation.teamID),
134 tables=TeamParticipation,
135 where=(TeamParticipation.person == user)
136 )), False)
137
138 policy_grant_query = Coalesce(
139 ArrayIntersects(
140 Array(SQL('Specification.access_policy')),
141 Select(
142 ArrayAgg(AccessPolicyGrant.policy_id),
143 tables=(AccessPolicyGrant,
144 Join(TeamParticipation,
145 TeamParticipation.teamID ==
146 AccessPolicyGrant.grantee_id)),
147 where=(TeamParticipation.person == user)
148 )), False)
149
150 return [Or(public_spec_filter, artifact_grant_query, policy_grant_query)]
151
152
153def get_specification_filters(filter, goalstatus=True):
154 """Return a list of Storm expressions for filtering Specifications.
155
156 :param filters: A collection of SpecificationFilter and/or strings.
157 Strings are used for text searches.
158 """
159 clauses = []
160 # ALL is the trump card.
161 if SpecificationFilter.ALL in filter:
162 return clauses
163 # Look for informational specs.
164 if SpecificationFilter.INFORMATIONAL in filter:
165 clauses.append(
166 Specification.implementation_status ==
167 SpecificationImplementationStatus.INFORMATIONAL)
168 # Filter based on completion. See the implementation of
169 # Specification.is_complete() for more details.
170 if SpecificationFilter.COMPLETE in filter:
171 clauses.append(get_specification_completeness_clause())
172 if SpecificationFilter.INCOMPLETE in filter:
173 clauses.append(Not(get_specification_completeness_clause()))
174
175 # Filter for goal status.
176 if goalstatus:
177 goalstatus = None
178 if SpecificationFilter.ACCEPTED in filter:
179 goalstatus = SpecificationGoalStatus.ACCEPTED
180 elif SpecificationFilter.PROPOSED in filter:
181 goalstatus = SpecificationGoalStatus.PROPOSED
182 elif SpecificationFilter.DECLINED in filter:
183 goalstatus = SpecificationGoalStatus.DECLINED
184 if goalstatus:
185 clauses.append(Specification.goalstatus == goalstatus)
186
187 if SpecificationFilter.STARTED in filter:
188 clauses.append(get_specification_started_clause())
189
190 # Filter for validity. If we want valid specs only, then we should exclude
191 # all OBSOLETE or SUPERSEDED specs.
192 if SpecificationFilter.VALID in filter:
193 clauses.append(Not(Specification.definition_status.is_in([
194 SpecificationDefinitionStatus.OBSOLETE,
195 SpecificationDefinitionStatus.SUPERSEDED])))
196 # Filter for specification text.
197 for constraint in filter:
198 if isinstance(constraint, basestring):
199 # A string in the filter is a text search filter.
200 clauses.append(fti_search(Specification, constraint))
201 return clauses
202
203
204def _preload_specifications_people(tables, clauses):
205 """Perform eager loading of people and their validity for query.
206
207 :param query: a string query generated in the 'specifications'
208 method.
209 :return: A DecoratedResultSet with Person precaching setup.
210 """
211 if isinstance(clauses, basestring):
212 clauses = [SQL(clauses)]
213
214 def cache_people(rows):
215 """DecoratedResultSet pre_iter_hook to eager load Person
216 attributes.
217 """
218 from lp.registry.model.person import Person
219 # Find the people we need:
220 person_ids = set()
221 for spec in rows:
222 person_ids.add(spec._assigneeID)
223 person_ids.add(spec._approverID)
224 person_ids.add(spec._drafterID)
225 person_ids.discard(None)
226 if not person_ids:
227 return
228 # Query those people
229 origin = [Person]
230 columns = [Person]
231 validity_info = Person._validity_queries()
232 origin.extend(validity_info["joins"])
233 columns.extend(validity_info["tables"])
234 decorators = validity_info["decorators"]
235 personset = IStore(Specification).using(*origin).find(
236 tuple(columns),
237 Person.id.is_in(person_ids),
238 )
239 for row in personset:
240 person = row[0]
241 index = 1
242 for decorator in decorators:
243 column = row[index]
244 index += 1
245 decorator(person, column)
246
247 results = IStore(Specification).using(*tables).find(
248 Specification, *clauses)
249 return DecoratedResultSet(results, pre_iter_hook=cache_people)
250
251
252def get_specification_started_clause():
253 return Or(Not(Specification.implementation_status.is_in([
254 SpecificationImplementationStatus.UNKNOWN,
255 SpecificationImplementationStatus.NOTSTARTED,
256 SpecificationImplementationStatus.DEFERRED,
257 SpecificationImplementationStatus.INFORMATIONAL])),
258 And(Specification.implementation_status ==
259 SpecificationImplementationStatus.INFORMATIONAL,
260 Specification.definition_status ==
261 SpecificationDefinitionStatus.APPROVED))
262
263
264def get_specification_completeness_clause():
265 return Or(
266 Specification.implementation_status ==
267 SpecificationImplementationStatus.IMPLEMENTED,
268 Specification.definition_status.is_in([
269 SpecificationDefinitionStatus.OBSOLETE,
270 SpecificationDefinitionStatus.SUPERSEDED,
271 ]),
272 And(
273 Specification.implementation_status ==
274 SpecificationImplementationStatus.INFORMATIONAL,
275 Specification.definition_status ==
276 SpecificationDefinitionStatus.APPROVED))
0277
=== modified file 'lib/lp/blueprints/model/sprint.py'
--- lib/lp/blueprints/model/sprint.py 2013-01-07 02:40:55 +0000
+++ lib/lp/blueprints/model/sprint.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -37,10 +37,11 @@
37 ISprint,37 ISprint,
38 ISprintSet,38 ISprintSet,
39 )39 )
40from lp.blueprints.model.specification import (40from lp.blueprints.model.specification import HasSpecificationsMixin
41from lp.blueprints.model.specificationsearch import (
42 get_specification_active_product_filter,
41 get_specification_filters,43 get_specification_filters,
42 HasSpecificationsMixin,44 get_specification_privacy_filter,
43 visible_specification_query,
44 )45 )
45from lp.blueprints.model.sprintattendance import SprintAttendance46from lp.blueprints.model.sprintattendance import SprintAttendance
46from lp.blueprints.model.sprintspecification import SprintSpecification47from lp.blueprints.model.sprintspecification import SprintSpecification
@@ -118,14 +119,16 @@
118 specifications() method because we want to reuse this query in the119 specifications() method because we want to reuse this query in the
119 specificationLinks() method.120 specificationLinks() method.
120 """121 """
121 # import here to avoid circular deps122 # Avoid circular imports.
122 from lp.blueprints.model.specification import Specification123 from lp.blueprints.model.specification import Specification
123 tables, query = visible_specification_query(user)124 tables, query = get_specification_active_product_filter(self)
125 tables.insert(0, Specification)
126 query.append(get_specification_privacy_filter(user))
124 tables.append(Join(127 tables.append(Join(
125 SprintSpecification,128 SprintSpecification,
126 SprintSpecification.specification == Specification.id129 SprintSpecification.specification == Specification.id))
127 ))130 query.append(SprintSpecification.sprintID == self.id)
128 query.extend([SprintSpecification.sprintID == self.id])131
129 if not filter:132 if not filter:
130 # filter could be None or [] then we decide the default133 # filter could be None or [] then we decide the default
131 # which for a sprint is to show everything approved134 # which for a sprint is to show everything approved
@@ -153,7 +156,7 @@
153 if len(statuses) > 0:156 if len(statuses) > 0:
154 query.append(Or(*statuses))157 query.append(Or(*statuses))
155 # Filter for specification text158 # Filter for specification text
156 query.extend(get_specification_filters(filter))159 query.extend(get_specification_filters(filter, goalstatus=False))
157 return tables, query160 return tables, query
158161
159 def all_specifications(self, user):162 def all_specifications(self, user):
160163
=== modified file 'lib/lp/blueprints/tests/test_hasspecifications.py'
--- lib/lp/blueprints/tests/test_hasspecifications.py 2012-09-26 19:10:28 +0000
+++ lib/lp/blueprints/tests/test_hasspecifications.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the1# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Unit tests for objects implementing IHasSpecifications."""4"""Unit tests for objects implementing IHasSpecifications."""
@@ -141,16 +141,13 @@
141 product1 = self.factory.makeProduct(project=projectgroup)141 product1 = self.factory.makeProduct(project=projectgroup)
142 product2 = self.factory.makeProduct(project=projectgroup)142 product2 = self.factory.makeProduct(project=projectgroup)
143 product3 = self.factory.makeProduct(project=other_projectgroup)143 product3 = self.factory.makeProduct(project=other_projectgroup)
144 self.factory.makeSpecification(144 self.factory.makeSpecification(product=product1, name="spec1")
145 product=product1, name="spec1")
146 self.factory.makeSpecification(145 self.factory.makeSpecification(
147 product=product2, name="spec2",146 product=product2, name="spec2",
148 status=SpecificationDefinitionStatus.OBSOLETE)147 status=SpecificationDefinitionStatus.OBSOLETE)
149 self.factory.makeSpecification(148 self.factory.makeSpecification(product=product3, name="spec3")
150 product=product3, name="spec3")
151 self.assertNamesOfSpecificationsAre(149 self.assertNamesOfSpecificationsAre(
152 ["spec1", "spec2"],150 ["spec1"], projectgroup._valid_specifications)
153 projectgroup._valid_specifications)
154151
155 def test_person_all_specifications(self):152 def test_person_all_specifications(self):
156 person = self.factory.makePerson(name="james-w")153 person = self.factory.makePerson(name="james-w")
157154
=== modified file 'lib/lp/blueprints/tests/test_specification.py'
--- lib/lp/blueprints/tests/test_specification.py 2012-12-26 01:04:05 +0000
+++ lib/lp/blueprints/tests/test_specification.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2010-2012 Canonical Ltd. This software is licensed under the1# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Unit tests for Specification."""4"""Unit tests for Specification."""
@@ -41,9 +41,9 @@
41 )41 )
42from lp.blueprints.errors import TargetAlreadyHasSpecification42from lp.blueprints.errors import TargetAlreadyHasSpecification
43from lp.blueprints.interfaces.specification import ISpecificationSet43from lp.blueprints.interfaces.specification import ISpecificationSet
44from lp.blueprints.model.specification import (44from lp.blueprints.model.specification import Specification
45 Specification,45from lp.blueprints.model.specificationsearch import (
46 visible_specification_query,46 get_specification_privacy_filter,
47 )47 )
48from lp.registry.enums import (48from lp.registry.enums import (
49 SharingPermission,49 SharingPermission,
@@ -407,48 +407,44 @@
407 specification.target.owner, specification,407 specification.target.owner, specification,
408 error_expected=False, attribute='name', value='foo')408 error_expected=False, attribute='name', value='foo')
409409
410 def test_visible_specification_query(self):410 def _fetch_specs_visible_for_user(self, user):
411 # visible_specification_query returns a Storm expression411 return Store.of(self.product).find(
412 # that can be used to filter specifications by their visibility-412 Specification,
413 Specification.productID == self.product.id,
414 *get_specification_privacy_filter(user))
415
416 def test_get_specification_privacy_filter(self):
417 # get_specification_privacy_filter returns a Storm expression
418 # that can be used to filter specifications by their visibility.
413 owner = self.factory.makePerson()419 owner = self.factory.makePerson()
414 product = self.factory.makeProduct(420 self.product = self.factory.makeProduct(
415 owner=owner,421 owner=owner,
416 specification_sharing_policy=(422 specification_sharing_policy=(
417 SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))423 SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
418 public_spec = self.factory.makeSpecification(product=product)424 public_spec = self.factory.makeSpecification(product=self.product)
419 proprietary_spec_1 = self.factory.makeSpecification(425 proprietary_spec_1 = self.factory.makeSpecification(
420 product=product, information_type=InformationType.PROPRIETARY)426 product=self.product, information_type=InformationType.PROPRIETARY)
421 proprietary_spec_2 = self.factory.makeSpecification(427 proprietary_spec_2 = self.factory.makeSpecification(
422 product=product, information_type=InformationType.PROPRIETARY)428 product=self.product, information_type=InformationType.PROPRIETARY)
423 all_specs = [429 all_specs = [
424 public_spec, proprietary_spec_1, proprietary_spec_2]430 public_spec, proprietary_spec_1, proprietary_spec_2]
425 store = Store.of(product)431 specs_for_anon = self._fetch_specs_visible_for_user(None)
426 tables, query = visible_specification_query(None)432 self.assertContentEqual(
427 specs_for_anon = store.using(*tables).find(433 [public_spec], specs_for_anon.config(distinct=True))
428 Specification,
429 Specification.productID == product.id, *query)
430 self.assertContentEqual([public_spec],
431 specs_for_anon.config(distinct=True))
432 # Product owners havae grants on the product, the privacy434 # Product owners havae grants on the product, the privacy
433 # filter returns thus all specifications for them.435 # filter returns thus all specifications for them.
434 tables, query = visible_specification_query(owner.id)436 specs_for_owner = self._fetch_specs_visible_for_user(owner)
435 specs_for_owner = store.using(*tables).find(
436 Specification, Specification.productID == product.id, *query)
437 self.assertContentEqual(all_specs, specs_for_owner)437 self.assertContentEqual(all_specs, specs_for_owner)
438 # The filter returns only public specs for ordinary users.438 # The filter returns only public specs for ordinary users.
439 user = self.factory.makePerson()439 user = self.factory.makePerson()
440 tables, query = visible_specification_query(user.id)440 specs_for_other_user = self._fetch_specs_visible_for_user(user)
441 specs_for_other_user = store.using(*tables).find(
442 Specification, Specification.productID == product.id, *query)
443 self.assertContentEqual([public_spec], specs_for_other_user)441 self.assertContentEqual([public_spec], specs_for_other_user)
444 # If the user has a grant for a specification, the filter returns442 # If the user has a grant for a specification, the filter returns
445 # this specification too.443 # this specification too.
446 with person_logged_in(owner):444 with person_logged_in(owner):
447 getUtility(IService, 'sharing').ensureAccessGrants(445 getUtility(IService, 'sharing').ensureAccessGrants(
448 [user], owner, specifications=[proprietary_spec_1])446 [user], owner, specifications=[proprietary_spec_1])
449 tables, query = visible_specification_query(user.id)447 specs_for_other_user = self._fetch_specs_visible_for_user(user)
450 specs_for_other_user = store.using(*tables).find(
451 Specification, Specification.productID == product.id, *query)
452 self.assertContentEqual(448 self.assertContentEqual(
453 [public_spec, proprietary_spec_1], specs_for_other_user)449 [public_spec, proprietary_spec_1], specs_for_other_user)
454450
455451
=== modified file 'lib/lp/registry/doc/distroseries.txt'
--- lib/lp/registry/doc/distroseries.txt 2012-12-26 01:32:19 +0000
+++ lib/lp/registry/doc/distroseries.txt 2013-01-22 06:44:52 +0000
@@ -458,7 +458,8 @@
458458
459 >>> for summary in hoary.getPrioritizedUnlinkedSourcePackages():459 >>> for summary in hoary.getPrioritizedUnlinkedSourcePackages():
460 ... print summary['package'].name460 ... print summary['package'].name
461 ... print '%(bug_count)s %(total_messages)s' % summary461 ... naked_summary = removeSecurityProxy(summary)
462 ... print '%(bug_count)s %(total_messages)s' % naked_summary
462 pmount 0 64463 pmount 0 64
463 alsa-utils 0 0464 alsa-utils 0 0
464 cnews 0 0465 cnews 0 0
@@ -630,7 +631,7 @@
630 ... PackagePublishingPocket.RELEASE, component_main,631 ... PackagePublishingPocket.RELEASE, component_main,
631 ... warty.main_archive)632 ... warty.main_archive)
632 >>> spphs.count()633 >>> spphs.count()
633 5 634 5
634 >>> for name in sorted(set(635 >>> for name in sorted(set(
635 ... pkgpub.sourcepackagerelease.sourcepackagename.name636 ... pkgpub.sourcepackagerelease.sourcepackagename.name
636 ... for pkgpub in spphs)):637 ... for pkgpub in spphs)):
637638
=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py 2012-11-15 20:54:45 +0000
+++ lib/lp/registry/model/distribution.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Database classes for implementing distribution items."""4"""Database classes for implementing distribution items."""
@@ -69,15 +69,11 @@
69 valid_name,69 valid_name,
70 )70 )
71from lp.archivepublisher.debversion import Version71from lp.archivepublisher.debversion import Version
72from lp.blueprints.enums import (
73 SpecificationDefinitionStatus,
74 SpecificationFilter,
75 SpecificationImplementationStatus,
76 )
77from lp.blueprints.model.specification import (72from lp.blueprints.model.specification import (
78 HasSpecificationsMixin,73 HasSpecificationsMixin,
79 Specification,74 Specification,
80 )75 )
76from lp.blueprints.model.specificationsearch import search_specifications
81from lp.blueprints.model.sprint import HasSprintsMixin77from lp.blueprints.model.sprint import HasSprintsMixin
82from lp.bugs.interfaces.bugsummary import IBugSummaryDimension78from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
83from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor79from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
@@ -882,86 +878,9 @@
882 - informationalness: we will show ANY if nothing is said878 - informationalness: we will show ANY if nothing is said
883879
884 """880 """
885881 base_clauses = [Specification.distributionID == self.id]
886 # Make a new list of the filter, so that we do not mutate what we882 return search_specifications(
887 # were passed as a filter883 self, base_clauses, user, sort, quantity, filter, prejoin_people)
888 if not filter:
889 # it could be None or it could be []
890 filter = [SpecificationFilter.INCOMPLETE]
891
892 # now look at the filter and fill in the unsaid bits
893
894 # defaults for completeness: if nothing is said about completeness
895 # then we want to show INCOMPLETE
896 completeness = False
897 for option in [
898 SpecificationFilter.COMPLETE,
899 SpecificationFilter.INCOMPLETE]:
900 if option in filter:
901 completeness = True
902 if completeness is False:
903 filter.append(SpecificationFilter.INCOMPLETE)
904
905 # defaults for acceptance: in this case we have nothing to do
906 # because specs are not accepted/declined against a distro
907
908 # defaults for informationalness: we don't have to do anything
909 # because the default if nothing is said is ANY
910
911 order = self._specification_sort(sort)
912
913 # figure out what set of specifications we are interested in. for
914 # distributions, we need to be able to filter on the basis of:
915 #
916 # - completeness. by default, only incomplete specs shown
917 # - informational.
918 #
919 base = 'Specification.distribution = %s' % self.id
920 query = base
921 # look for informational specs
922 if SpecificationFilter.INFORMATIONAL in filter:
923 query += (' AND Specification.implementation_status = %s ' %
924 quote(SpecificationImplementationStatus.INFORMATIONAL))
925
926 # filter based on completion. see the implementation of
927 # Specification.is_complete() for more details
928 completeness = Specification.completeness_clause
929
930 if SpecificationFilter.COMPLETE in filter:
931 query += ' AND ( %s ) ' % completeness
932 elif SpecificationFilter.INCOMPLETE in filter:
933 query += ' AND NOT ( %s ) ' % completeness
934
935 # Filter for validity. If we want valid specs only then we should
936 # exclude all OBSOLETE or SUPERSEDED specs
937 if SpecificationFilter.VALID in filter:
938 query += (' AND Specification.definition_status NOT IN '
939 '( %s, %s ) ' % sqlvalues(
940 SpecificationDefinitionStatus.OBSOLETE,
941 SpecificationDefinitionStatus.SUPERSEDED))
942
943 # ALL is the trump card
944 if SpecificationFilter.ALL in filter:
945 query = base
946
947 # Filter for specification text
948 for constraint in filter:
949 if isinstance(constraint, basestring):
950 # a string in the filter is a text search filter
951 query += ' AND Specification.fti @@ ftq(%s) ' % quote(
952 constraint)
953
954 if prejoin_people:
955 results = self._preload_specifications_people([Specification],
956 query)
957 else:
958 results = Store.of(self).find(
959 Specification,
960 SQL(query))
961 results.order_by(order)
962 if quantity is not None:
963 results = results[:quantity]
964 return results
965884
966 def getSpecification(self, name):885 def getSpecification(self, name):
967 """See `ISpecificationTarget`."""886 """See `ISpecificationTarget`."""
968887
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2012-12-14 00:36:37 +0000
+++ lib/lp/registry/model/distroseries.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Database classes for a distribution series."""4"""Database classes for a distribution series."""
@@ -42,17 +42,12 @@
42from lp.app.enums import service_uses_launchpad42from lp.app.enums import service_uses_launchpad
43from lp.app.errors import NotFoundError43from lp.app.errors import NotFoundError
44from lp.app.interfaces.launchpad import IServiceUsage44from lp.app.interfaces.launchpad import IServiceUsage
45from lp.blueprints.enums import (
46 SpecificationFilter,
47 SpecificationGoalStatus,
48 SpecificationImplementationStatus,
49 SpecificationSort,
50 )
51from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget45from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
52from lp.blueprints.model.specification import (46from lp.blueprints.model.specification import (
53 HasSpecificationsMixin,47 HasSpecificationsMixin,
54 Specification,48 Specification,
55 )49 )
50from lp.blueprints.model.specificationsearch import search_specifications
56from lp.bugs.interfaces.bugsummary import IBugSummaryDimension51from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
57from lp.bugs.interfaces.bugtarget import ISeriesBugTarget52from lp.bugs.interfaces.bugtarget import ISeriesBugTarget
58from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask53from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
@@ -110,7 +105,6 @@
110from lp.services.database.sqlbase import (105from lp.services.database.sqlbase import (
111 flush_database_caches,106 flush_database_caches,
112 flush_database_updates,107 flush_database_updates,
113 quote,
114 SQLBase,108 SQLBase,
115 sqlvalues,109 sqlvalues,
116 )110 )
@@ -787,109 +781,10 @@
787 - informationalness: if nothing is said, ANY781 - informationalness: if nothing is said, ANY
788782
789 """783 """
790784 base_clauses = [Specification.distroseriesID == self.id]
791 # Make a new list of the filter, so that we do not mutate what we785 return search_specifications(
792 # were passed as a filter786 self, base_clauses, user, sort, quantity, filter, prejoin_people,
793 if not filter:787 default_acceptance=True)
794 # filter could be None or [] then we decide the default
795 # which for a distroseries is to show everything approved
796 filter = [SpecificationFilter.ACCEPTED]
797
798 # defaults for completeness: in this case we don't actually need to
799 # do anything, because the default is ANY
800
801 # defaults for acceptance: in this case, if nothing is said about
802 # acceptance, we want to show only accepted specs
803 acceptance = False
804 for option in [
805 SpecificationFilter.ACCEPTED,
806 SpecificationFilter.DECLINED,
807 SpecificationFilter.PROPOSED]:
808 if option in filter:
809 acceptance = True
810 if acceptance is False:
811 filter.append(SpecificationFilter.ACCEPTED)
812
813 # defaults for informationalness: we don't have to do anything
814 # because the default if nothing is said is ANY
815
816 # sort by priority descending, by default
817 if sort is None or sort == SpecificationSort.PRIORITY:
818 order = ['-priority', 'Specification.definition_status',
819 'Specification.name']
820 elif sort == SpecificationSort.DATE:
821 # we are showing specs for a GOAL, so under some circumstances
822 # we care about the order in which the specs were nominated for
823 # the goal, and in others we care about the order in which the
824 # decision was made.
825
826 # we need to establish if the listing will show specs that have
827 # been decided only, or will include proposed specs.
828 show_proposed = set([
829 SpecificationFilter.ALL,
830 SpecificationFilter.PROPOSED,
831 ])
832 if len(show_proposed.intersection(set(filter))) > 0:
833 # we are showing proposed specs so use the date proposed
834 # because not all specs will have a date decided.
835 order = ['-Specification.datecreated', 'Specification.id']
836 else:
837 # this will show only decided specs so use the date the spec
838 # was accepted or declined for the sprint
839 order = ['-Specification.date_goal_decided',
840 '-Specification.datecreated',
841 'Specification.id']
842
843 # figure out what set of specifications we are interested in. for
844 # distroseries, we need to be able to filter on the basis of:
845 #
846 # - completeness.
847 # - goal status.
848 # - informational.
849 #
850 base = 'Specification.distroseries = %s' % self.id
851 query = base
852 # look for informational specs
853 if SpecificationFilter.INFORMATIONAL in filter:
854 query += (' AND Specification.implementation_status = %s' %
855 quote(SpecificationImplementationStatus.INFORMATIONAL))
856
857 # filter based on completion. see the implementation of
858 # Specification.is_complete() for more details
859 completeness = Specification.completeness_clause
860
861 if SpecificationFilter.COMPLETE in filter:
862 query += ' AND ( %s ) ' % completeness
863 elif SpecificationFilter.INCOMPLETE in filter:
864 query += ' AND NOT ( %s ) ' % completeness
865
866 # look for specs that have a particular goalstatus (proposed,
867 # accepted or declined)
868 if SpecificationFilter.ACCEPTED in filter:
869 query += ' AND Specification.goalstatus = %d' % (
870 SpecificationGoalStatus.ACCEPTED.value)
871 elif SpecificationFilter.PROPOSED in filter:
872 query += ' AND Specification.goalstatus = %d' % (
873 SpecificationGoalStatus.PROPOSED.value)
874 elif SpecificationFilter.DECLINED in filter:
875 query += ' AND Specification.goalstatus = %d' % (
876 SpecificationGoalStatus.DECLINED.value)
877
878 # ALL is the trump card
879 if SpecificationFilter.ALL in filter:
880 query = base
881
882 # Filter for specification text
883 for constraint in filter:
884 if isinstance(constraint, basestring):
885 # a string in the filter is a text search filter
886 query += ' AND Specification.fti @@ ftq(%s) ' % quote(
887 constraint)
888
889 results = Specification.select(query, orderBy=order, limit=quantity)
890 if prejoin_people:
891 results = results.prejoin(['_assignee', '_approver', '_drafter'])
892 return results
893788
894 def getDistroSeriesLanguage(self, language):789 def getDistroSeriesLanguage(self, language):
895 """See `IDistroSeries`."""790 """See `IDistroSeries`."""
896791
=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py 2013-01-07 02:40:55 +0000
+++ lib/lp/registry/model/milestone.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Milestone model classes."""4"""Milestone model classes."""
@@ -38,9 +38,10 @@
3838
39from lp.app.enums import InformationType39from lp.app.enums import InformationType
40from lp.app.errors import NotFoundError40from lp.app.errors import NotFoundError
41from lp.blueprints.model.specification import (41from lp.blueprints.model.specification import Specification
42 Specification,42from lp.blueprints.model.specificationsearch import (
43 visible_specification_query,43 get_specification_active_product_filter,
44 get_specification_privacy_filter,
44 )45 )
45from lp.blueprints.model.specificationworkitem import SpecificationWorkItem46from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
46from lp.bugs.interfaces.bugsummary import IBugSummaryDimension47from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
@@ -155,14 +156,15 @@
155 def getSpecifications(self, user):156 def getSpecifications(self, user):
156 """See `IMilestoneData`"""157 """See `IMilestoneData`"""
157 from lp.registry.model.person import Person158 from lp.registry.model.person import Person
158 store = Store.of(self.target)159 origin = [Specification]
159 origin, clauses = visible_specification_query(user)160 product_origin, clauses = get_specification_active_product_filter(
160 origin.extend([161 self)
161 LeftJoin(Person, Specification._assigneeID == Person.id),162 origin.extend(product_origin)
162 ])163 clauses.extend(get_specification_privacy_filter(user))
164 origin.append(LeftJoin(Person, Specification._assigneeID == Person.id))
163 milestones = self._milestone_ids_expr(user)165 milestones = self._milestone_ids_expr(user)
164166
165 results = store.using(*origin).find(167 results = Store.of(self.target).using(*origin).find(
166 (Specification, Person),168 (Specification, Person),
167 Specification.id.is_in(169 Specification.id.is_in(
168 Union(170 Union(
169171
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2013-01-14 06:13:52 +0000
+++ lib/lp/registry/model/person.py 2013-01-22 06:44:52 +0000
@@ -125,16 +125,15 @@
125 sanitize_name,125 sanitize_name,
126 valid_name,126 valid_name,
127 )127 )
128from lp.blueprints.enums import (128from lp.blueprints.enums import SpecificationFilter
129 SpecificationFilter,
130 SpecificationSort,
131 )
132from lp.blueprints.model.specification import (129from lp.blueprints.model.specification import (
133 get_specification_filters,
134 HasSpecificationsMixin,130 HasSpecificationsMixin,
135 spec_started_clause,
136 Specification,131 Specification,
137 visible_specification_query,132 )
133from lp.blueprints.model.specificationsearch import (
134 get_specification_active_product_filter,
135 get_specification_privacy_filter,
136 search_specifications,
138 )137 )
139from lp.blueprints.model.specificationworkitem import SpecificationWorkItem138from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
140from lp.bugs.interfaces.bugtarget import IBugTarget139from lp.bugs.interfaces.bugtarget import IBugTarget
@@ -856,10 +855,8 @@
856 # because the default if nothing is said is ANY.855 # because the default if nothing is said is ANY.
857856
858 roles = set([857 roles = set([
859 SpecificationFilter.CREATOR,858 SpecificationFilter.CREATOR, SpecificationFilter.ASSIGNEE,
860 SpecificationFilter.ASSIGNEE,859 SpecificationFilter.DRAFTER, SpecificationFilter.APPROVER,
861 SpecificationFilter.DRAFTER,
862 SpecificationFilter.APPROVER,
863 SpecificationFilter.SUBSCRIBER])860 SpecificationFilter.SUBSCRIBER])
864 # If no roles are given, then we want everything.861 # If no roles are given, then we want everything.
865 if filter.intersection(roles) == set():862 if filter.intersection(roles) == set():
@@ -877,32 +874,18 @@
877 role_clauses.append(874 role_clauses.append(
878 Specification.id.is_in(875 Specification.id.is_in(
879 Select(SpecificationSubscription.specificationID,876 Select(SpecificationSubscription.specificationID,
880 [SpecificationSubscription.person == self]877 [SpecificationSubscription.person == self])))
881 )))878
882 tables, clauses = visible_specification_query(user)879 clauses = [Or(*role_clauses)]
883 clauses.append(Or(*role_clauses))
884 # Defaults for completeness: if nothing is said about completeness
885 # then we want to show INCOMPLETE.
886 if SpecificationFilter.COMPLETE not in filter:880 if SpecificationFilter.COMPLETE not in filter:
887 if (in_progress and SpecificationFilter.INCOMPLETE not in filter881 if (in_progress and SpecificationFilter.INCOMPLETE not in filter
888 and SpecificationFilter.ALL not in filter):882 and SpecificationFilter.ALL not in filter):
889 clauses.append(spec_started_clause)883 filter.update(
890 filter.add(SpecificationFilter.INCOMPLETE)884 [SpecificationFilter.INCOMPLETE,
885 SpecificationFilter.STARTED])
891886
892 clauses.extend(get_specification_filters(filter))887 return search_specifications(
893 results = Store.of(self).using(*tables).find(Specification, *clauses)888 self, clauses, user, sort, quantity, list(filter), prejoin_people)
894 # The default sort is priority descending, so only explictly sort for
895 # DATE.
896 if sort == SpecificationSort.DATE:
897 sort = Desc(Specification.datecreated)
898 elif getattr(sort, 'enum', None) is SpecificationSort:
899 sort = None
900 if sort is not None:
901 results = results.order_by(sort)
902 results.config(distinct=True)
903 if quantity is not None:
904 results = results[:quantity]
905 return results
906889
907 # XXX: Tom Berger 2008-04-14 bug=191799:890 # XXX: Tom Berger 2008-04-14 bug=191799:
908 # The implementation of these functions891 # The implementation of these functions
@@ -1482,20 +1465,22 @@
1482 from lp.registry.model.distribution import Distribution1465 from lp.registry.model.distribution import Distribution
1483 store = Store.of(self)1466 store = Store.of(self)
1484 WorkItem = SpecificationWorkItem1467 WorkItem = SpecificationWorkItem
1485 origin, query = visible_specification_query(user)1468 origin = [Specification]
1469 productjoin, query = get_specification_active_product_filter(self)
1470 origin.extend(productjoin)
1471 query.extend(get_specification_privacy_filter(user))
1486 origin.extend([1472 origin.extend([
1487 Join(WorkItem, WorkItem.specification == Specification.id),1473 Join(WorkItem, WorkItem.specification == Specification.id),
1488 # WorkItems may not have a milestone and in that case they inherit1474 # WorkItems may not have a milestone and in that case they inherit
1489 # the one from the spec.1475 # the one from the spec.
1490 Join(Milestone,1476 Join(Milestone,
1491 Coalesce(WorkItem.milestone_id,1477 Coalesce(WorkItem.milestone_id,
1492 Specification.milestoneID) == Milestone.id),1478 Specification.milestoneID) == Milestone.id)])
1493 ])
1494 today = datetime.today().date()1479 today = datetime.today().date()
1495 query.extend([1480 query.extend([
1496 Milestone.dateexpected <= date, Milestone.dateexpected >= today,1481 Milestone.dateexpected <= date, Milestone.dateexpected >= today,
1497 WorkItem.deleted == False,1482 WorkItem.deleted == False,
1498 OR(WorkItem.assignee_id.is_in(self.participant_ids),1483 Or(WorkItem.assignee_id.is_in(self.participant_ids),
1499 Specification._assigneeID.is_in(self.participant_ids))])1484 Specification._assigneeID.is_in(self.participant_ids))])
1500 result = store.using(*origin).find(WorkItem, *query)1485 result = store.using(*origin).find(WorkItem, *query)
1501 result.config(distinct=True)1486 result.config(distinct=True)
@@ -1680,6 +1665,12 @@
1680 requester=reviewer)1665 requester=reviewer)
1681 return (status_changed, tm.status)1666 return (status_changed, tm.status)
16821667
1668 def _accept_or_decline_membership(self, team, status, comment):
1669 tm = TeamMembership.selectOneBy(person=self, team=team)
1670 assert tm is not None
1671 assert tm.status == TeamMembershipStatus.INVITED
1672 tm.setStatus(status, getUtility(ILaunchBag).user, comment=comment)
1673
1683 # The three methods below are not in the IPerson interface because we want1674 # The three methods below are not in the IPerson interface because we want
1684 # to protect them with a launchpad.Edit permission. We could do that by1675 # to protect them with a launchpad.Edit permission. We could do that by
1685 # defining explicit permissions for all IPerson methods/attributes in1676 # defining explicit permissions for all IPerson methods/attributes in
@@ -1691,12 +1682,8 @@
1691 the INVITED status. The status of this TeamMembership will be changed1682 the INVITED status. The status of this TeamMembership will be changed
1692 to APPROVED.1683 to APPROVED.
1693 """1684 """
1694 tm = TeamMembership.selectOneBy(person=self, team=team)1685 self._accept_or_decline_membership(
1695 assert tm is not None1686 team, TeamMembershipStatus.APPROVED, comment)
1696 assert tm.status == TeamMembershipStatus.INVITED
1697 tm.setStatus(
1698 TeamMembershipStatus.APPROVED, getUtility(ILaunchBag).user,
1699 comment=comment)
17001687
1701 def declineInvitationToBeMemberOf(self, team, comment):1688 def declineInvitationToBeMemberOf(self, team, comment):
1702 """Decline an invitation to become a member of the given team.1689 """Decline an invitation to become a member of the given team.
@@ -1705,12 +1692,8 @@
1705 the INVITED status. The status of this TeamMembership will be changed1692 the INVITED status. The status of this TeamMembership will be changed
1706 to INVITATION_DECLINED.1693 to INVITATION_DECLINED.
1707 """1694 """
1708 tm = TeamMembership.selectOneBy(person=self, team=team)1695 self._accept_or_decline_membership(
1709 assert tm is not None1696 team, TeamMembershipStatus.INVITATION_DECLINED, comment)
1710 assert tm.status == TeamMembershipStatus.INVITED
1711 tm.setStatus(
1712 TeamMembershipStatus.INVITATION_DECLINED,
1713 getUtility(ILaunchBag).user, comment=comment)
17141697
1715 def retractTeamMembership(self, team, user, comment=None):1698 def retractTeamMembership(self, team, user, comment=None):
1716 """See `IPerson`"""1699 """See `IPerson`"""
@@ -1765,14 +1748,11 @@
1765 def getOwnedTeams(self, user=None):1748 def getOwnedTeams(self, user=None):
1766 """See `IPerson`."""1749 """See `IPerson`."""
1767 query = And(1750 query = And(
1768 get_person_visibility_terms(user),1751 get_person_visibility_terms(user), Person.teamowner == self.id,
1769 Person.teamowner == self.id,
1770 Person.merged == None)1752 Person.merged == None)
1771 store = IStore(Person)1753 return IStore(Person).find(
1772 results = store.find(
1773 Person, query).order_by(1754 Person, query).order_by(
1774 Upper(Person.displayname), Upper(Person.name))1755 Upper(Person.displayname), Upper(Person.name))
1775 return results
17761756
1777 @cachedproperty1757 @cachedproperty
1778 def administrated_teams(self):1758 def administrated_teams(self):
17791759
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2013-01-03 05:00:59 +0000
+++ lib/lp/registry/model/product.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Database classes including and related to Product."""4"""Database classes including and related to Product."""
@@ -91,13 +91,12 @@
91from lp.app.model.launchpad import InformationTypeMixin91from lp.app.model.launchpad import InformationTypeMixin
92from lp.blueprints.enums import SpecificationFilter92from lp.blueprints.enums import SpecificationFilter
93from lp.blueprints.model.specification import (93from lp.blueprints.model.specification import (
94 get_specification_filters,
95 HasSpecificationsMixin,94 HasSpecificationsMixin,
96 Specification,95 Specification,
97 SPECIFICATION_POLICY_ALLOWED_TYPES,96 SPECIFICATION_POLICY_ALLOWED_TYPES,
98 SPECIFICATION_POLICY_DEFAULT_TYPES,97 SPECIFICATION_POLICY_DEFAULT_TYPES,
99 visible_specification_query,
100 )98 )
99from lp.blueprints.model.specificationsearch import search_specifications
101from lp.blueprints.model.sprint import HasSprintsMixin100from lp.blueprints.model.sprint import HasSprintsMixin
102from lp.bugs.interfaces.bugsummary import IBugSummaryDimension101from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
103from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor102from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
@@ -1435,52 +1434,9 @@
1435 prejoin_people=True):1434 prejoin_people=True):
1436 """See `IHasSpecifications`."""1435 """See `IHasSpecifications`."""
14371436
1438 # Make a new list of the filter, so that we do not mutate what we1437 base_clauses = [Specification.productID == self.id]
1439 # were passed as a filter1438 return search_specifications(
1440 if not filter:1439 self, base_clauses, user, sort, quantity, filter, prejoin_people)
1441 # filter could be None or [] then we decide the default
1442 # which for a product is to show incomplete specs
1443 filter = [SpecificationFilter.INCOMPLETE]
1444
1445 # now look at the filter and fill in the unsaid bits
1446
1447 # defaults for completeness: if nothing is said about completeness
1448 # then we want to show INCOMPLETE
1449 completeness = False
1450 for option in [
1451 SpecificationFilter.COMPLETE,
1452 SpecificationFilter.INCOMPLETE]:
1453 if option in filter:
1454 completeness = True
1455 if completeness is False:
1456 filter.append(SpecificationFilter.INCOMPLETE)
1457
1458 # defaults for acceptance: in this case we have nothing to do
1459 # because specs are not accepted/declined against a distro
1460
1461 # defaults for informationalness: we don't have to do anything
1462 # because the default if nothing is said is ANY
1463
1464 order = self._specification_sort(sort)
1465
1466 # figure out what set of specifications we are interested in. for
1467 # products, we need to be able to filter on the basis of:
1468 #
1469 # - completeness.
1470 # - informational.
1471 #
1472 tables, clauses = visible_specification_query(user)
1473 clauses.append(Specification.product == self)
1474 clauses.extend(get_specification_filters(filter))
1475 if prejoin_people:
1476 results = self._preload_specifications_people(tables, clauses)
1477 else:
1478 tableset = Store.of(self).using(*tables)
1479 results = tableset.find(Specification, *clauses)
1480 results.order_by(order).config(distinct=True)
1481 if quantity is not None:
1482 results = results[:quantity]
1483 return results
14841440
1485 def getSpecification(self, name):1441 def getSpecification(self, name):
1486 """See `ISpecificationTarget`."""1442 """See `ISpecificationTarget`."""
14871443
=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py 2013-01-07 02:40:55 +0000
+++ lib/lp/registry/model/productseries.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Models for `IProductSeries`."""4"""Models for `IProductSeries`."""
@@ -38,18 +38,12 @@
38 ILaunchpadCelebrities,38 ILaunchpadCelebrities,
39 IServiceUsage,39 IServiceUsage,
40 )40 )
41from lp.blueprints.enums import (
42 SpecificationDefinitionStatus,
43 SpecificationFilter,
44 SpecificationGoalStatus,
45 SpecificationImplementationStatus,
46 SpecificationSort,
47 )
48from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget41from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
49from lp.blueprints.model.specification import (42from lp.blueprints.model.specification import (
50 HasSpecificationsMixin,43 HasSpecificationsMixin,
51 Specification,44 Specification,
52 )45 )
46from lp.blueprints.model.specificationsearch import search_specifications
53from lp.bugs.interfaces.bugsummary import IBugSummaryDimension47from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
54from lp.bugs.interfaces.bugtarget import ISeriesBugTarget48from lp.bugs.interfaces.bugtarget import ISeriesBugTarget
55from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask49from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
@@ -79,7 +73,6 @@
79from lp.services.database.decoratedresultset import DecoratedResultSet73from lp.services.database.decoratedresultset import DecoratedResultSet
80from lp.services.database.enumcol import EnumCol74from lp.services.database.enumcol import EnumCol
81from lp.services.database.sqlbase import (75from lp.services.database.sqlbase import (
82 quote,
83 SQLBase,76 SQLBase,
84 sqlvalues,77 sqlvalues,
85 )78 )
@@ -334,116 +327,10 @@
334 - informational, which defaults to showing BOTH if nothing is said327 - informational, which defaults to showing BOTH if nothing is said
335328
336 """329 """
337330 base_clauses = [Specification.productseriesID == self.id]
338 # Make a new list of the filter, so that we do not mutate what we331 return search_specifications(
339 # were passed as a filter332 self, base_clauses, user, sort, quantity, filter, prejoin_people,
340 if not filter:333 default_acceptance=True)
341 # filter could be None or [] then we decide the default
342 # which for a productseries is to show everything accepted
343 filter = [SpecificationFilter.ACCEPTED]
344
345 # defaults for completeness: in this case we don't actually need to
346 # do anything, because the default is ANY
347
348 # defaults for acceptance: in this case, if nothing is said about
349 # acceptance, we want to show only accepted specs
350 acceptance = False
351 for option in [
352 SpecificationFilter.ACCEPTED,
353 SpecificationFilter.DECLINED,
354 SpecificationFilter.PROPOSED]:
355 if option in filter:
356 acceptance = True
357 if acceptance is False:
358 filter.append(SpecificationFilter.ACCEPTED)
359
360 # defaults for informationalness: we don't have to do anything
361 # because the default if nothing is said is ANY
362
363 # sort by priority descending, by default
364 if sort is None or sort == SpecificationSort.PRIORITY:
365 order = ['-priority', 'definition_status', 'name']
366 elif sort == SpecificationSort.DATE:
367 # we are showing specs for a GOAL, so under some circumstances
368 # we care about the order in which the specs were nominated for
369 # the goal, and in others we care about the order in which the
370 # decision was made.
371
372 # we need to establish if the listing will show specs that have
373 # been decided only, or will include proposed specs.
374 show_proposed = set([
375 SpecificationFilter.ALL,
376 SpecificationFilter.PROPOSED,
377 ])
378 if len(show_proposed.intersection(set(filter))) > 0:
379 # we are showing proposed specs so use the date proposed
380 # because not all specs will have a date decided.
381 order = ['-Specification.datecreated', 'Specification.id']
382 else:
383 # this will show only decided specs so use the date the spec
384 # was accepted or declined for the sprint
385 order = ['-Specification.date_goal_decided',
386 '-Specification.datecreated',
387 'Specification.id']
388
389 # figure out what set of specifications we are interested in. for
390 # productseries, we need to be able to filter on the basis of:
391 #
392 # - completeness. by default, only incomplete specs shown
393 # - goal status. by default, only accepted specs shown
394 # - informational.
395 #
396 base = 'Specification.productseries = %s' % self.id
397 query = base
398 # look for informational specs
399 if SpecificationFilter.INFORMATIONAL in filter:
400 query += (' AND Specification.implementation_status = %s' %
401 quote(SpecificationImplementationStatus.INFORMATIONAL))
402
403 # filter based on completion. see the implementation of
404 # Specification.is_complete() for more details
405 completeness = Specification.completeness_clause
406
407 if SpecificationFilter.COMPLETE in filter:
408 query += ' AND ( %s ) ' % completeness
409 elif SpecificationFilter.INCOMPLETE in filter:
410 query += ' AND NOT ( %s ) ' % completeness
411
412 # look for specs that have a particular goalstatus (proposed,
413 # accepted or declined)
414 if SpecificationFilter.ACCEPTED in filter:
415 query += ' AND Specification.goalstatus = %d' % (
416 SpecificationGoalStatus.ACCEPTED.value)
417 elif SpecificationFilter.PROPOSED in filter:
418 query += ' AND Specification.goalstatus = %d' % (
419 SpecificationGoalStatus.PROPOSED.value)
420 elif SpecificationFilter.DECLINED in filter:
421 query += ' AND Specification.goalstatus = %d' % (
422 SpecificationGoalStatus.DECLINED.value)
423
424 # Filter for validity. If we want valid specs only then we should
425 # exclude all OBSOLETE or SUPERSEDED specs
426 if SpecificationFilter.VALID in filter:
427 query += (
428 ' AND Specification.definition_status NOT IN ( %s, %s ) '
429 % sqlvalues(SpecificationDefinitionStatus.OBSOLETE,
430 SpecificationDefinitionStatus.SUPERSEDED))
431
432 # ALL is the trump card
433 if SpecificationFilter.ALL in filter:
434 query = base
435
436 # Filter for specification text
437 for constraint in filter:
438 if isinstance(constraint, basestring):
439 # a string in the filter is a text search filter
440 query += ' AND Specification.fti @@ ftq(%s) ' % quote(
441 constraint)
442
443 results = Specification.select(query, orderBy=order, limit=quantity)
444 if prejoin_people:
445 results = results.prejoin(['_assignee', '_approver', '_drafter'])
446 return results
447334
448 def _customizeSearchParams(self, search_params):335 def _customizeSearchParams(self, search_params):
449 """Customize `search_params` for this product series."""336 """Customize `search_params` for this product series."""
450337
=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py 2013-01-07 02:40:55 +0000
+++ lib/lp/registry/model/projectgroup.py 2013-01-22 06:44:52 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Launchpad ProjectGroup-related Database Table Objects."""4"""Launchpad ProjectGroup-related Database Table Objects."""
@@ -44,16 +44,12 @@
44 IHasLogo,44 IHasLogo,
45 IHasMugshot,45 IHasMugshot,
46 )46 )
47from lp.blueprints.enums import (47from lp.blueprints.enums import SprintSpecificationStatus
48 SpecificationFilter,
49 SpecificationImplementationStatus,
50 SpecificationSort,
51 SprintSpecificationStatus,
52 )
53from lp.blueprints.model.specification import (48from lp.blueprints.model.specification import (
54 HasSpecificationsMixin,49 HasSpecificationsMixin,
55 Specification,50 Specification,
56 )51 )
52from lp.blueprints.model.specificationsearch import search_specifications
57from lp.blueprints.model.sprint import HasSprintsMixin53from lp.blueprints.model.sprint import HasSprintsMixin
58from lp.bugs.interfaces.bugsummary import IBugSummaryDimension54from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
59from lp.bugs.model.bugtarget import (55from lp.bugs.model.bugtarget import (
@@ -96,7 +92,6 @@
96from lp.services.database.datetimecol import UtcDateTimeCol92from lp.services.database.datetimecol import UtcDateTimeCol
97from lp.services.database.enumcol import EnumCol93from lp.services.database.enumcol import EnumCol
98from lp.services.database.sqlbase import (94from lp.services.database.sqlbase import (
99 quote,
100 SQLBase,95 SQLBase,
101 sqlvalues,96 sqlvalues,
102 )97 )
@@ -251,70 +246,18 @@
251 def specifications(self, user, sort=None, quantity=None, filter=None,246 def specifications(self, user, sort=None, quantity=None, filter=None,
252 series=None, prejoin_people=True):247 series=None, prejoin_people=True):
253 """See `IHasSpecifications`."""248 """See `IHasSpecifications`."""
254249 base_clauses = [
255 # Make a new list of the filter, so that we do not mutate what we250 Specification.productID == Product.id,
256 # were passed as a filter251 Product.projectID == self.id]
257 if not filter:252 tables = [Specification]
258 # filter could be None or [] then we decide the default253 if series:
259 # which for a project group is to show incomplete specs254 base_clauses.append(ProductSeries.name == series)
260 filter = [SpecificationFilter.INCOMPLETE]255 tables.append(
261256 Join(ProductSeries,
262 # sort by priority descending, by default257 Specification.productseriesID == ProductSeries.id))
263 if sort is None or sort == SpecificationSort.PRIORITY:258 return search_specifications(
264 order = ['-priority', 'Specification.definition_status',259 self, base_clauses, user, sort, quantity, filter, prejoin_people,
265 'Specification.name']260 tables=tables)
266 elif sort == SpecificationSort.DATE:
267 order = ['-Specification.datecreated', 'Specification.id']
268
269 # figure out what set of specifications we are interested in. for
270 # project groups, we need to be able to filter on the basis of:
271 #
272 # - completeness. by default, only incomplete specs shown
273 # - informational.
274 #
275 base = """
276 Specification.product = Product.id AND
277 Product.active IS TRUE AND
278 Product.project = %s
279 """ % self.id
280 query = base
281 # look for informational specs
282 if SpecificationFilter.INFORMATIONAL in filter:
283 query += (' AND Specification.implementation_status = %s' %
284 quote(SpecificationImplementationStatus.INFORMATIONAL))
285
286 # filter based on completion. see the implementation of
287 # Specification.is_complete() for more details
288 completeness = Specification.completeness_clause
289
290 if SpecificationFilter.COMPLETE in filter:
291 query += ' AND ( %s ) ' % completeness
292 elif SpecificationFilter.INCOMPLETE in filter:
293 query += ' AND NOT ( %s ) ' % completeness
294
295 # ALL is the trump card
296 if SpecificationFilter.ALL in filter:
297 query = base
298
299 # Filter for specification text
300 for constraint in filter:
301 if isinstance(constraint, basestring):
302 # a string in the filter is a text search filter
303 query += ' AND Specification.fti @@ ftq(%s) ' % quote(
304 constraint)
305
306 clause_tables = ['Product']
307 if series is not None:
308 query += ('AND Specification.productseries = ProductSeries.id'
309 ' AND ProductSeries.name = %s'
310 % sqlvalues(series))
311 clause_tables.append('ProductSeries')
312
313 results = Specification.select(query, orderBy=order, limit=quantity,
314 clauseTables=clause_tables)
315 if prejoin_people:
316 results = results.prejoin(['_assignee', '_approver', '_drafter'])
317 return results
318261
319 def _customizeSearchParams(self, search_params):262 def _customizeSearchParams(self, search_params):
320 """Customize `search_params` for this milestone."""263 """Customize `search_params` for this milestone."""
321264
=== modified file 'lib/lp/registry/model/sharingjob.py'
--- lib/lp/registry/model/sharingjob.py 2012-11-16 20:30:12 +0000
+++ lib/lp/registry/model/sharingjob.py 2013-01-22 06:44:52 +0000
@@ -1,7 +1,6 @@
1# Copyright 2012 Canonical Ltd. This software is licensed under the1# Copyright 2012-2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4
5"""Job classes related to the sharing feature are in here."""4"""Job classes related to the sharing feature are in here."""
65
7__metaclass__ = type6__metaclass__ = type
@@ -43,9 +42,9 @@
4342
44from lp.app.enums import InformationType43from lp.app.enums import InformationType
45from lp.blueprints.interfaces.specification import ISpecification44from lp.blueprints.interfaces.specification import ISpecification
46from lp.blueprints.model.specification import (45from lp.blueprints.model.specification import Specification
47 Specification,46from lp.blueprints.model.specificationsearch import (
48 visible_specification_query,47 get_specification_privacy_filter,
49 )48 )
50from lp.blueprints.model.specificationsubscription import (49from lp.blueprints.model.specificationsubscription import (
51 SpecificationSubscription,50 SpecificationSubscription,
@@ -439,8 +438,8 @@
439 sub.branch.unsubscribe(438 sub.branch.unsubscribe(
440 sub.person, self.requestor, ignore_permissions=True)439 sub.person, self.requestor, ignore_permissions=True)
441 if specification_filters:440 if specification_filters:
442 specification_filters.append(441 specification_filters.append(Not(*get_specification_privacy_filter(
443 spec_not_visible(SpecificationSubscription.personID))442 SpecificationSubscription.personID)))
444 tables = (443 tables = (
445 SpecificationSubscription,444 SpecificationSubscription,
446 Join(445 Join(
@@ -454,10 +453,3 @@
454 for sub in specifications_subscriptions:453 for sub in specifications_subscriptions:
455 sub.specification.unsubscribe(454 sub.specification.unsubscribe(
456 sub.person, self.requestor, ignore_permissions=True)455 sub.person, self.requestor, ignore_permissions=True)
457
458
459def spec_not_visible(person_id):
460 """Return an expression for finding specs not visible to the person."""
461 tables, clauses = visible_specification_query(person_id)
462 subselect = Select(Specification.id, tables=tables, where=And(clauses))
463 return Not(Specification.id.is_in(subselect))