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