Merge lp:~abentley/launchpad/user-blueprints into lp:launchpad

Proposed by Aaron Bentley on 2012-10-16
Status: Merged
Approved by: Aaron Bentley on 2012-10-16
Approved revision: no longer in the source branch.
Merged at revision: 16157
Proposed branch: lp:~abentley/launchpad/user-blueprints
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/user-blueprints-tests
Diff against target: 389 lines (+96/-141)
5 files modified
lib/lp/blueprints/model/specification.py (+43/-0)
lib/lp/blueprints/model/sprint.py (+3/-23)
lib/lp/blueprints/templates/hasspecifications-specs.pt (+2/-2)
lib/lp/blueprints/templates/person-specworkload.pt (+2/-2)
lib/lp/registry/model/person.py (+46/-114)
To merge this branch: bzr merge lp:~abentley/launchpad/user-blueprints
Reviewer Review Type Date Requested Status
Richard Harding (community) 2012-10-16 Approve on 2012-10-16
Review via email: mp+129945@code.launchpad.net

Commit Message

Re-implement Person.specifications using Storm expressions.

Description of the Change

= Summary =
Implement Person.specifications using Storm expressions, to improve composability and refactoring.

== Pre-implementation notes ==
None

== LOC Rationale ==
Part of private projects

== Implementation details ==
Extract common portions of Sprint.specifications to get_specification_filters, extend with support for SpecificationFilter.ALL and SpecificationFilter.VALID.

== Tests ==
bin/test -t TestSpecifications -m '(sprint|person)'

== Demo and Q/A ==
None

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/testing/factory.py
  lib/lp/blueprints/model/specification.py
  lib/lp/blueprints/model/sprint.py
  lib/lp/registry/model/person.py
  lib/lp/registry/tests/test_person.py

To post a comment you must log in.
Richard Harding (rharding) wrote :

#34 The comments in here should be complete sentences ending in . per PEP8
http://www.python.org/dev/peps/pep-0008/#comments

#204 extra blank line?

review: Approve
Aaron Bentley (abentley) wrote :

> #34 The comments in here should be complete sentences ending in . per PEP8
> http://www.python.org/dev/peps/pep-0008/#comments

Okay. I was copying them verbatim to make it easier to see how the new code matched the old behaviour. That way, the diff matches up on the comments, since it can't match on the code.

> #204 extra blank line?

Fixed.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/model/specification.py'
2--- lib/lp/blueprints/model/specification.py 2012-10-10 14:35:49 +0000
3+++ lib/lp/blueprints/model/specification.py 2012-10-16 23:52:16 +0000
4@@ -5,6 +5,7 @@
5
6 __metaclass__ = type
7 __all__ = [
8+ 'get_specification_filters',
9 'get_specification_privacy_filter',
10 'HasSpecificationsMixin',
11 'recursive_blocked_query',
12@@ -34,6 +35,7 @@
13 Join,
14 LeftJoin,
15 Or,
16+ Not,
17 Select,
18 )
19 from storm.locals import (
20@@ -104,6 +106,7 @@
21 SQLBase,
22 sqlvalues,
23 )
24+from lp.services.database.stormexpr import fti_search
25 from lp.services.mail.helpers import get_contact_email_addresses
26 from lp.services.propertycache import (
27 cachedproperty,
28@@ -1289,3 +1292,43 @@
29 where=Or(
30 AccessPolicyGrantFlat.abstract_artifact_id == None,
31 AccessArtifact.specification_id == Specification.id))))
32+
33+
34+def get_specification_filters(filter):
35+ """Return a list of Storm expressions for filtering Specifications.
36+
37+ :param filters: A collection of SpecificationFilter and/or strings.
38+ Strings are used for text searches.
39+ """
40+ from lp.registry.model.product import Product
41+ # If Product is used, it must be active.
42+ clauses = [Or(Specification.product == None,
43+ Not(Specification.productID.is_in(Select(Product.id,
44+ Product.active == False))))]
45+ # ALL is the trump card.
46+ if SpecificationFilter.ALL in filter:
47+ return clauses
48+ # Look for informational specs.
49+ if SpecificationFilter.INFORMATIONAL in filter:
50+ clauses.append(Specification.implementation_status ==
51+ SpecificationImplementationStatus.INFORMATIONAL)
52+ # Filter based on completion. See the implementation of
53+ # Specification.is_complete() for more details.
54+ if SpecificationFilter.COMPLETE in filter:
55+ clauses.append(Specification.storm_completeness())
56+ if SpecificationFilter.INCOMPLETE in filter:
57+ clauses.append(Not(Specification.storm_completeness()))
58+
59+ # Filter for validity. If we want valid specs only, then we should exclude
60+ # all OBSOLETE or SUPERSEDED specs.
61+ if SpecificationFilter.VALID in filter:
62+ clauses.append(Not(Specification.definition_status.is_in([
63+ SpecificationDefinitionStatus.OBSOLETE,
64+ SpecificationDefinitionStatus.SUPERSEDED,
65+ ])))
66+ # Filter for specification text.
67+ for constraint in filter:
68+ if isinstance(constraint, basestring):
69+ # A string in the filter is a text search filter.
70+ clauses.append(fti_search(Specification, constraint))
71+ return clauses
72
73=== modified file 'lib/lp/blueprints/model/sprint.py'
74--- lib/lp/blueprints/model/sprint.py 2012-10-12 01:56:34 +0000
75+++ lib/lp/blueprints/model/sprint.py 2012-10-16 23:52:16 +0000
76@@ -17,9 +17,7 @@
77 )
78 from storm.locals import (
79 Desc,
80- Not,
81 Or,
82- Select,
83 Store,
84 )
85 from zope.component import getUtility
86@@ -33,7 +31,6 @@
87 )
88 from lp.blueprints.enums import (
89 SpecificationFilter,
90- SpecificationImplementationStatus,
91 SpecificationSort,
92 SprintSpecificationStatus,
93 )
94@@ -42,6 +39,7 @@
95 ISprintSet,
96 )
97 from lp.blueprints.model.specification import (
98+ get_specification_filters,
99 get_specification_privacy_filter,
100 HasSpecificationsMixin,
101 )
102@@ -59,7 +57,6 @@
103 quote,
104 SQLBase,
105 )
106-from lp.services.database.stormexpr import fti_search
107 from lp.services.propertycache import cachedproperty
108
109
110@@ -124,12 +121,8 @@
111 """
112 # import here to avoid circular deps
113 from lp.blueprints.model.specification import Specification
114- from lp.registry.model.product import Product
115 query = [SprintSpecification.sprintID == self.id,
116- SprintSpecification.specificationID == Specification.id,
117- Or(Specification.product == None,
118- Not(Specification.productID.is_in(Select(Product.id,
119- Product.active == False))))]
120+ SprintSpecification.specificationID == Specification.id]
121 query.append(get_specification_privacy_filter(user))
122 if not filter:
123 # filter could be None or [] then we decide the default
124@@ -144,16 +137,6 @@
125 # - informational.
126 #
127
128- # look for informational specs
129- if SpecificationFilter.INFORMATIONAL in filter:
130- query.append(Specification.implementation_status ==
131- SpecificationImplementationStatus.INFORMATIONAL)
132- # filter based on completion. see the implementation of
133- # Specification.is_complete() for more details
134- if SpecificationFilter.COMPLETE in filter:
135- query.append(Specification.storm_completeness())
136- if SpecificationFilter.INCOMPLETE in filter:
137- query.append(Not(Specification.storm_completeness()))
138 sprint_status = []
139 # look for specs that have a particular SprintSpecification
140 # status (proposed, accepted or declined)
141@@ -168,10 +151,7 @@
142 if len(statuses) > 0:
143 query.append(Or(*statuses))
144 # Filter for specification text
145- for constraint in filter:
146- if isinstance(constraint, basestring):
147- # a string in the filter is a text search filter
148- query.append(fti_search(Specification, constraint))
149+ query.extend(get_specification_filters(filter))
150 return query
151
152 def all_specifications(self, user):
153
154=== modified file 'lib/lp/blueprints/templates/hasspecifications-specs.pt'
155--- lib/lp/blueprints/templates/hasspecifications-specs.pt 2009-11-10 17:35:17 +0000
156+++ lib/lp/blueprints/templates/hasspecifications-specs.pt 2012-10-16 23:52:16 +0000
157@@ -155,7 +155,7 @@
158 </div>
159 <div class="lesser" tal:content="structure view/batchnav/@@+navigation-links-upper" />
160 <table class="listing sortable" id="speclisting"
161- tal:condition="view/specs">
162+ tal:condition="not: view/specs/is_empty">
163 <thead>
164 <tr>
165 <th tal:condition="view/show_priority">Priority</th>
166@@ -229,7 +229,7 @@
167 </table>
168 <div class="lesser" tal:content="structure view/batchnav/@@+navigation-links-lower" />
169
170- <tal:nomatches condition="not: view/specs"
171+ <tal:nomatches condition="view/specs/is_empty"
172 replace="structure context/@@+nomatches" />
173
174 <p tal:condition="view/is_series">
175
176=== modified file 'lib/lp/blueprints/templates/person-specworkload.pt'
177--- lib/lp/blueprints/templates/person-specworkload.pt 2012-09-27 15:28:38 +0000
178+++ lib/lp/blueprints/templates/person-specworkload.pt 2012-10-16 23:52:16 +0000
179@@ -46,7 +46,7 @@
180
181 <tal:specs define="specifications member/@@+specworkload/specifications">
182
183- <div style="margin-bottom: 1em;" tal:condition="specifications">
184+ <div style="margin-bottom: 1em;" tal:condition="not: specifications/is_empty">
185 <p>
186 <a tal:replace="structure member/fmt:link">Foo Bar</a>'s
187 specifications:
188@@ -55,7 +55,7 @@
189 <div tal:replace="structure member/@@+table-specworkload" />
190 </div>
191
192- <p tal:condition="not: specifications">
193+ <p tal:condition="specifications/is_empty">
194 <a tal:replace="structure member/fmt:link">Foo Bar</a>
195 has no outstanding specifications.<br />
196 </p>
197
198=== modified file 'lib/lp/registry/model/person.py'
199--- lib/lp/registry/model/person.py 2012-10-15 18:30:48 +0000
200+++ lib/lp/registry/model/person.py 2012-10-16 23:52:16 +0000
201@@ -122,12 +122,11 @@
202 valid_name,
203 )
204 from lp.blueprints.enums import (
205- SpecificationDefinitionStatus,
206 SpecificationFilter,
207- SpecificationImplementationStatus,
208 SpecificationSort,
209 )
210 from lp.blueprints.model.specification import (
211+ get_specification_filters,
212 get_specification_privacy_filter,
213 HasSpecificationsMixin,
214 Specification,
215@@ -819,129 +818,62 @@
216 def specifications(self, user, sort=None, quantity=None, filter=None,
217 prejoin_people=True):
218 """See `IHasSpecifications`."""
219-
220- # Make a new list of the filter, so that we do not mutate what we
221- # were passed as a filter
222- if not filter:
223- # if no filter was passed (None or []) then we must decide the
224- # default filtering, and for a person we want related incomplete
225- # specs
226- filter = [SpecificationFilter.INCOMPLETE]
227-
228- # now look at the filter and fill in the unsaid bits
229-
230- # defaults for completeness: if nothing is said about completeness
231- # then we want to show INCOMPLETE
232- completeness = False
233- for option in [
234- SpecificationFilter.COMPLETE,
235- SpecificationFilter.INCOMPLETE]:
236- if option in filter:
237- completeness = True
238- if completeness is False:
239- filter.append(SpecificationFilter.INCOMPLETE)
240-
241- # defaults for acceptance: in this case we have nothing to do
242- # because specs are not accepted/declined against a person
243-
244- # defaults for informationalness: we don't have to do anything
245- # because the default if nothing is said is ANY
246-
247- # if no roles are given then we want everything
248- linked = False
249+ from lp.blueprints.model.specificationsubscription import (
250+ SpecificationSubscription,
251+ )
252+ # Make a new copy of the filter, so that we do not mutate what we
253+ # were passed as a filter.
254+ if filter is None:
255+ filter = set()
256+ else:
257+ filter = set(filter)
258+
259+ # Now look at the filter and fill in the unsaid bits.
260+
261+ # Defaults for completeness: if nothing is said about completeness
262+ # then we want to show INCOMPLETE.
263+ if SpecificationFilter.COMPLETE not in filter:
264+ filter.add(SpecificationFilter.INCOMPLETE)
265+
266+ # Defaults for acceptance: in this case we have nothing to do
267+ # because specs are not accepted/declined against a person.
268+
269+ # Defaults for informationalness: we don't have to do anything
270+ # because the default if nothing is said is ANY.
271+
272 roles = set([
273 SpecificationFilter.CREATOR,
274 SpecificationFilter.ASSIGNEE,
275 SpecificationFilter.DRAFTER,
276 SpecificationFilter.APPROVER,
277 SpecificationFilter.SUBSCRIBER])
278- for role in roles:
279- if role in filter:
280- linked = True
281- if not linked:
282- for role in roles:
283- filter.append(role)
284-
285- # sort by priority descending, by default
286- if sort is None or sort == SpecificationSort.PRIORITY:
287- order = ['-priority', 'Specification.definition_status',
288- 'Specification.name']
289- elif sort == SpecificationSort.DATE:
290- order = ['-Specification.datecreated', 'Specification.id']
291-
292- # figure out what set of specifications we are interested in. for
293- # products, we need to be able to filter on the basis of:
294- #
295- # - role (owner, drafter, approver, subscriber, assignee etc)
296- # - completeness.
297- # - informational.
298- #
299-
300- # in this case the "base" is quite complicated because it is
301- # determined by the roles so lets do that first
302-
303- base = '(1=0' # we want to start with a FALSE and OR them
304+ # If no roles are given, then we want everything.
305+ if filter.intersection(roles) == set():
306+ filter.update(roles)
307+ role_clauses = []
308 if SpecificationFilter.CREATOR in filter:
309- base += ' OR Specification.owner = %(my_id)d'
310+ role_clauses.append(Specification.owner == self)
311 if SpecificationFilter.ASSIGNEE in filter:
312- base += ' OR Specification.assignee = %(my_id)d'
313+ role_clauses.append(Specification.assignee == self)
314 if SpecificationFilter.DRAFTER in filter:
315- base += ' OR Specification.drafter = %(my_id)d'
316+ role_clauses.append(Specification.drafter == self)
317 if SpecificationFilter.APPROVER in filter:
318- base += ' OR Specification.approver = %(my_id)d'
319+ role_clauses.append(Specification.approver == self)
320 if SpecificationFilter.SUBSCRIBER in filter:
321- base += """ OR Specification.id in
322- (SELECT specification FROM SpecificationSubscription
323- WHERE person = %(my_id)d)"""
324- base += ') '
325-
326- # filter out specs on inactive products
327- base += """AND (Specification.product IS NULL OR
328- Specification.product NOT IN
329- (SELECT Product.id FROM Product
330- WHERE Product.active IS FALSE))
331- """
332-
333- base = base % {'my_id': self.id}
334-
335- query = base
336- # look for informational specs
337- if SpecificationFilter.INFORMATIONAL in filter:
338- query += (' AND Specification.implementation_status = %s' %
339- quote(SpecificationImplementationStatus.INFORMATIONAL))
340-
341- # filter based on completion. see the implementation of
342- # Specification.is_complete() for more details
343- completeness = Specification.completeness_clause
344-
345- if SpecificationFilter.COMPLETE in filter:
346- query += ' AND ( %s ) ' % completeness
347- elif SpecificationFilter.INCOMPLETE in filter:
348- query += ' AND NOT ( %s ) ' % completeness
349-
350- # Filter for validity. If we want valid specs only then we should
351- # exclude all OBSOLETE or SUPERSEDED specs
352- if SpecificationFilter.VALID in filter:
353- query += (
354- ' AND Specification.definition_status NOT IN ( %s, ''%s ) ' %
355- sqlvalues(SpecificationDefinitionStatus.OBSOLETE,
356- SpecificationDefinitionStatus.SUPERSEDED))
357-
358- # ALL is the trump card
359- if SpecificationFilter.ALL in filter:
360- query = base
361-
362- # Filter for specification text
363- for constraint in filter:
364- if isinstance(constraint, basestring):
365- # a string in the filter is a text search filter
366- query += ' AND Specification.fti @@ ftq(%s) ' % quote(
367- constraint)
368-
369- results = Specification.select(query, orderBy=order,
370- limit=quantity)
371- if prejoin_people:
372- results = results.prejoin(['assignee', 'approver', 'drafter'])
373+ role_clauses.append(
374+ Specification.id.is_in(
375+ Select(SpecificationSubscription.specificationID,
376+ [SpecificationSubscription.person == self]
377+ )))
378+ clauses = [Or(*role_clauses)]
379+ clauses.extend(get_specification_filters(filter))
380+ results = Store.of(self).find(Specification, *clauses)
381+ # The default sort is priority descending, so only explictly sort for
382+ # DATE.
383+ if sort == SpecificationSort.DATE:
384+ results = results.order_by(Desc(Specification.datecreated))
385+ if quantity is not None:
386+ results = results[:quantity]
387 return results
388
389 # XXX: Tom Berger 2008-04-14 bug=191799: