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