Merge lp:~abentley/launchpad/storm-sprint-queries into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 16057
Proposed branch: lp:~abentley/launchpad/storm-sprint-queries
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/new-tests
Diff against target: 306 lines (+91/-88)
5 files modified
lib/lp/blueprints/browser/tests/test_sprint.py (+1/-1)
lib/lp/blueprints/model/specification.py (+18/-0)
lib/lp/blueprints/model/sprint.py (+53/-81)
lib/lp/blueprints/templates/specificationtarget-assignments.pt (+4/-4)
lib/lp/services/database/stormexpr.py (+15/-2)
To merge this branch: bzr merge lp:~abentley/launchpad/storm-sprint-queries
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+126770@code.launchpad.net

Commit message

Convert Sprint.specifications to use storm expressions.

Description of the change

= Summary =
Update Sprint.specifications to use storm expressions

== Pre-implementation notes ==
None

== LOC Rationale ==
Part of private projects

== Implementation details ==
Convert Sprint.specifications to use storm expressions, so that we can add privacy checks. Also, remove support for prejoin_people. This is justifiable because with the new code, the Sprint index page actually emits fewer queries. Since sorting by priority is the default, it is not explicitly implemented.

Reduce the signature of specificationLinks to those values that are actually used.

Tweak specificationtarget-assignments.pt, because it relied on the old ResultSet evaluating False when it was empty.

Implement stormexpr.fti_search because it was needed to emit a Storm expression.

== Tests ==
bin/test test_sprint

== Demo and Q/A ==
The sprint pages should work, and should list relevant blueprints.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/services/database/stormexpr.py
  lib/lp/blueprints/templates/specificationtarget-assignments.pt
  lib/lp/blueprints/model/tests/test_sprint.py
  lib/lp/blueprints/model/specification.py
  lib/lp/blueprints/model/sprint.py
  lib/lp/testing/_webservice.py
  lib/lp/blueprints/browser/tests/test_sprint.py

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

This looks alright, but I'm not sure I see the value in the completeness expression being a class method on the Specification class--it seems like something that could just be defined as a clause separately in that code rather than a class method, and then used as needed. Is there some other reason for this implementation?

Also: I concur with the lack of a need for an explicit implementation for Priority, but that should probably be added in a comment in the relevant block of code.

review: Needs Information
Revision history for this message
Aaron Bentley (abentley) wrote :

> This looks alright, but I'm not sure I see the value in the completeness
> expression being a class method on the Specification class--it seems like
> something that could just be defined as a clause separately in that code

Which code? Sprint.specification_filter? It should not be defined there, because it is more generally useful. We will need it when we convert other implementations of IHasSpecifications.specifications to Storm.

> rather than a class method, and then used as needed. Is there some other
> reason for this implementation?

I thought it was valuable to have it directly underneath the non-Storm version, Specification.completeness.

> Also: I concur with the lack of a need for an explicit implementation for
> Priority, but that should probably be added in a comment in the relevant block
> of code.

There is a comment:
212 + # fall back to default, which is priority, descending.

Does it need improvement?

Revision history for this message
j.c.sackett (jcsackett) wrote :

Ah, I missed that there was a precedent for the storm_completeness class method. Given that, I concur with your reasoning.

The comment is fine--I must have just missed it when reading through.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/browser/tests/test_sprint.py'
2--- lib/lp/blueprints/browser/tests/test_sprint.py 2012-09-27 19:29:23 +0000
3+++ lib/lp/blueprints/browser/tests/test_sprint.py 2012-09-27 19:29:23 +0000
4@@ -39,4 +39,4 @@
5 link.acceptBy(sprint.owner)
6 with QueryCollector() as recorder:
7 self.getViewBrowser(sprint)
8- self.assertThat(recorder, HasQueryCount(Equals(33)))
9+ self.assertThat(recorder, HasQueryCount(Equals(30)))
10
11=== modified file 'lib/lp/blueprints/model/specification.py'
12--- lib/lp/blueprints/model/specification.py 2012-09-24 13:43:00 +0000
13+++ lib/lp/blueprints/model/specification.py 2012-09-27 19:29:23 +0000
14@@ -532,6 +532,24 @@
15 SpecificationImplementationStatus.INFORMATIONAL.value,
16 SpecificationDefinitionStatus.APPROVED.value))
17
18+ @classmethod
19+ def storm_completeness(cls):
20+ """Storm version of the above."""
21+ return Or(
22+ cls.implementation_status ==
23+ SpecificationImplementationStatus.IMPLEMENTED,
24+ cls.definition_status.is_in([
25+ SpecificationDefinitionStatus.OBSOLETE,
26+ SpecificationDefinitionStatus.SUPERSEDED,
27+ ]),
28+ And(
29+ cls.implementation_status ==
30+ SpecificationImplementationStatus.INFORMATIONAL,
31+ cls.definition_status ==
32+ SpecificationDefinitionStatus.APPROVED
33+ ),
34+ )
35+
36 @property
37 def is_complete(self):
38 """See `ISpecification`."""
39
40=== modified file 'lib/lp/blueprints/model/sprint.py'
41--- lib/lp/blueprints/model/sprint.py 2011-12-30 06:14:56 +0000
42+++ lib/lp/blueprints/model/sprint.py 2012-09-27 19:29:23 +0000
43@@ -15,7 +15,13 @@
44 ForeignKey,
45 StringCol,
46 )
47-from storm.locals import Store
48+from storm.locals import (
49+ Desc,
50+ Not,
51+ Or,
52+ Select,
53+ Store,
54+ )
55 from zope.component import getUtility
56 from zope.interface import implements
57
58@@ -50,6 +56,7 @@
59 quote,
60 SQLBase,
61 )
62+from lp.services.database.stormexpr import fti_search
63
64
65 class Sprint(SQLBase, HasDriversMixin, HasSpecificationsMixin):
66@@ -111,9 +118,14 @@
67 specifications() method because we want to reuse this query in the
68 specificationLinks() method.
69 """
70-
71- # Make a new list of the filter, so that we do not mutate what we
72- # were passed as a filter
73+ # import here to avoid circular deps
74+ from lp.blueprints.model.specification import Specification
75+ from lp.registry.model.product import Product
76+ query = [SprintSpecification.sprintID == self.id,
77+ SprintSpecification.specificationID == Specification.id,
78+ Or(Specification.product == None,
79+ Not(Specification.productID.is_in(Select(Product.id,
80+ Product.active == False))))]
81 if not filter:
82 # filter could be None or [] then we decide the default
83 # which for a sprint is to show everything approved
84@@ -126,55 +138,35 @@
85 # - acceptance for sprint agenda.
86 # - informational.
87 #
88- base = """SprintSpecification.sprint = %s AND
89- SprintSpecification.specification = Specification.id AND
90- (Specification.product IS NULL OR
91- Specification.product NOT IN
92- (SELECT Product.id FROM Product
93- WHERE Product.active IS FALSE))
94- """ % quote(self)
95- query = base
96
97 # look for informational specs
98 if SpecificationFilter.INFORMATIONAL in filter:
99- query += (' AND Specification.implementation_status = %s' %
100- quote(SpecificationImplementationStatus.INFORMATIONAL))
101-
102- # import here to avoid circular deps
103- from lp.blueprints.model.specification import Specification
104-
105+ query.append(Specification.implementation_status ==
106+ SpecificationImplementationStatus.INFORMATIONAL)
107 # filter based on completion. see the implementation of
108 # Specification.is_complete() for more details
109- completeness = Specification.completeness_clause
110-
111 if SpecificationFilter.COMPLETE in filter:
112- query += ' AND ( %s ) ' % completeness
113- elif SpecificationFilter.INCOMPLETE in filter:
114- query += ' AND NOT ( %s ) ' % completeness
115-
116+ query.append(Specification.storm_completeness())
117+ if SpecificationFilter.INCOMPLETE in filter:
118+ query.append(Not(Specification.storm_completeness()))
119+ sprint_status = []
120 # look for specs that have a particular SprintSpecification
121 # status (proposed, accepted or declined)
122 if SpecificationFilter.ACCEPTED in filter:
123- query += ' AND SprintSpecification.status = %s' % (
124- quote(SprintSpecificationStatus.ACCEPTED))
125- elif SpecificationFilter.PROPOSED in filter:
126- query += ' AND SprintSpecification.status = %s' % (
127- quote(SprintSpecificationStatus.PROPOSED))
128- elif SpecificationFilter.DECLINED in filter:
129- query += ' AND SprintSpecification.status = %s' % (
130- quote(SprintSpecificationStatus.DECLINED))
131-
132- # ALL is the trump card
133- if SpecificationFilter.ALL in filter:
134- query = base
135-
136+ sprint_status.append(SprintSpecificationStatus.ACCEPTED)
137+ if SpecificationFilter.PROPOSED in filter:
138+ sprint_status.append(SprintSpecificationStatus.PROPOSED)
139+ if SpecificationFilter.DECLINED in filter:
140+ sprint_status.append(SprintSpecificationStatus.DECLINED)
141+ statuses = [SprintSpecification.status == status for status in
142+ sprint_status]
143+ if len(statuses) > 0:
144+ query.append(Or(*statuses))
145 # Filter for specification text
146 for constraint in filter:
147 if isinstance(constraint, basestring):
148 # a string in the filter is a text search filter
149- query += ' AND Specification.fti @@ ftq(%s) ' % quote(
150- constraint)
151-
152+ query.append(fti_search(Specification, constraint))
153 return query
154
155 @property
156@@ -187,59 +179,39 @@
157 return self.specifications(filter=[SpecificationFilter.ALL])
158
159 def specifications(self, sort=None, quantity=None, filter=None,
160- prejoin_people=True):
161+ prejoin_people=False):
162 """See IHasSpecifications."""
163-
164+ # prejoin_people is provided only for interface compatibility and
165+ # prejoin_people=True is not implemented.
166+ assert not prejoin_people
167+ if filter is None:
168+ filter = set([SpecificationFilter.ACCEPTED])
169 query = self.spec_filter_clause(filter=filter)
170- if filter == None:
171- filter = []
172-
173 # import here to avoid circular deps
174 from lp.blueprints.model.specification import Specification
175-
176- # sort by priority descending, by default
177- if sort is None or sort == SpecificationSort.PRIORITY:
178- order = ['-priority', 'Specification.definition_status',
179- 'Specification.name']
180- elif sort == SpecificationSort.DATE:
181+ results = Store.of(self).find(Specification, *query)
182+ if sort == SpecificationSort.DATE:
183+ order = (Desc(SprintSpecification.date_created), Specification.id)
184 # we need to establish if the listing will show specs that have
185 # been decided only, or will include proposed specs.
186- show_proposed = set([
187- SpecificationFilter.ALL,
188- SpecificationFilter.PROPOSED,
189- ])
190- if len(show_proposed.intersection(set(filter))) > 0:
191- # we are showing proposed specs so use the date proposed
192- # because not all specs will have a date decided.
193- order = ['-SprintSpecification.date_created',
194- 'Specification.id']
195- else:
196+ if (SpecificationFilter.ALL not in filter and
197+ SpecificationFilter.PROPOSED not in filter):
198 # this will show only decided specs so use the date the spec
199 # was accepted or declined for the sprint
200- order = ['-SprintSpecification.date_decided',
201- '-SprintSpecification.date_created',
202- 'Specification.id']
203-
204- results = Specification.select(query, orderBy=order, limit=quantity,
205- clauseTables=['SprintSpecification'])
206- if prejoin_people:
207- results = results.prejoin(['assignee', 'approver', 'drafter'])
208+ order = (Desc(SprintSpecification.date_decided),) + order
209+ results = results.order_by(*order)
210+ else:
211+ assert sort is None or sort == SpecificationSort.PRIORITY
212+ # fall back to default, which is priority, descending.
213+ if quantity is not None:
214+ results = results[:quantity]
215 return results
216
217- def specificationLinks(self, sort=None, quantity=None, filter=None):
218+ def specificationLinks(self, filter=None):
219 """See `ISprint`."""
220-
221 query = self.spec_filter_clause(filter=filter)
222-
223- # sort by priority descending, by default
224- if sort is None or sort == SpecificationSort.PRIORITY:
225- order = ['-priority', 'status', 'name']
226- elif sort == SpecificationSort.DATE:
227- order = ['-datecreated', 'id']
228-
229- results = SprintSpecification.select(query,
230- clauseTables=['Specification'], orderBy=order, limit=quantity)
231- return results.prejoin(['specification'])
232+ result = Store.of(self).find(SprintSpecification, *query)
233+ return result
234
235 def getSpecificationLink(self, speclink_id):
236 """See `ISprint`.
237
238=== modified file 'lib/lp/blueprints/templates/specificationtarget-assignments.pt'
239--- lib/lp/blueprints/templates/specificationtarget-assignments.pt 2010-09-15 00:07:54 +0000
240+++ lib/lp/blueprints/templates/specificationtarget-assignments.pt 2012-09-27 19:29:23 +0000
241@@ -16,14 +16,14 @@
242
243 <div metal:fill-slot="main">
244
245- <p class="portlet" tal:condition="not: view/specs" id="no-blueprints">
246+ <p class="portlet" tal:condition="view/specs/is_empty" id="no-blueprints">
247 There are no open blueprints.
248 </p>
249
250- <div tal:condition="view/specs" class="portlet">
251+ <div tal:condition="not: view/specs/is_empty" class="portlet">
252 <p>
253 This listing shows the assignment of work for
254- blueprints currently associated with
255+ blueprints currently associated with
256 <span tal:replace="context/displayname">Mozilla</span>.
257 The drafter is responsible for getting the specification correctly
258 written up and approved.
259@@ -84,7 +84,7 @@
260 </a>
261 </td>
262 <td>
263- <a tal:condition="spec/approver"
264+ <a tal:condition="spec/approver"
265 tal:replace="structure spec/approver/fmt:link">
266 Approver Foo
267 </a>
268
269=== modified file 'lib/lp/services/database/stormexpr.py'
270--- lib/lp/services/database/stormexpr.py 2012-07-24 07:24:58 +0000
271+++ lib/lp/services/database/stormexpr.py 2012-09-27 19:29:23 +0000
272@@ -11,6 +11,7 @@
273 'ColumnSelect',
274 'Concatenate',
275 'CountDistinct',
276+ 'fti_search',
277 'Greatest',
278 'get_where_for_reference',
279 'NullCount',
280@@ -29,8 +30,12 @@
281 In,
282 NamedFunc,
283 Or,
284- )
285-from storm.info import get_obj_info
286+ SQL,
287+ )
288+from storm.info import (
289+ get_cls_info,
290+ get_obj_info,
291+ )
292
293
294 class ColumnSelect(Expr):
295@@ -191,3 +196,11 @@
296 [_remote_variables(relation, value) for value in others])
297 else:
298 return Or(*[relation.get_where_for_local(value) for value in others])
299+
300+
301+def fti_search(table, text):
302+ """An expression ensuring that table rows match the specified text."""
303+ table = get_cls_info(table).table
304+ tables = (table,)
305+ text = unicode(text)
306+ return SQL('%s.fti @@ ftq(?)' % table.name, params=(text,), tables=tables)