Merge lp:~abentley/launchpad/person-assigned-specs-in-progress into lp:launchpad

Proposed by Aaron Bentley on 2012-10-22
Status: Merged
Approved by: Aaron Bentley on 2012-10-26
Approved revision: no longer in the source branch.
Merged at revision: 16198
Proposed branch: lp:~abentley/launchpad/person-assigned-specs-in-progress
Merge into: lp:launchpad
Diff against target: 362 lines (+139/-52)
9 files modified
lib/lp/blueprints/enums.py (+4/-4)
lib/lp/blueprints/model/specification.py (+22/-19)
lib/lp/registry/browser/person.py (+2/-1)
lib/lp/registry/browser/tests/test_person.py (+18/-1)
lib/lp/registry/doc/person.txt (+1/-1)
lib/lp/registry/interfaces/person.py (+11/-3)
lib/lp/registry/model/person.py (+19/-18)
lib/lp/registry/tests/test_person.py (+57/-0)
lib/lp/testing/factory.py (+5/-5)
To merge this branch: bzr merge lp:~abentley/launchpad/person-assigned-specs-in-progress
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code 2012-10-22 Approve on 2012-10-26
Deryck Hodge (community) Approve on 2012-10-26
Review via email: mp+130888@code.launchpad.net

Commit Message

Fix +portlet-currentfocus with proprietary specs.

Description of the Change

= Summary =
Fix bug #1068719: Person overview page breaks when assigned proprietary blueprints

== Proposed fix ==
Enhance Person.specifications, rewrite assigned_specs_in_progress as findVisibleAssignedInProgressSpecs, extract core functionality to Person.specifications.

== Pre-implementation notes ==
None

== LOC Rationale ==
Part of Private Projects.

== Implementation details ==
Since Person.specifications already handles privacy and is used for many searches, it makes sense to extend it to handle searches for in-progress specs.

The results should be sorted according to Specification.date_started, which is not included in SpecificationSort. Person.specification's sort parameter is extended to support Storm expressions, rather than adding a new sort to SpecificationSort, because this is more direct.

in_progress is implemented as a boolean rather than extending SpecificationFilter because this is more direct.

== Tests ==
bin/test -t in_progress -t test_assigned_blueprints.

== Demo and Q/A ==
Create a proprietary specification with status STARTED, and assign it to a Person. You should see it listed on their overview page.

Log in as an unprivileged user and look at the same person's overview page. You should not see it listed.

= 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/registry/interfaces/person.py
  lib/lp/registry/browser/tests/test_person.py
  lib/lp/registry/browser/person.py
  lib/lp/registry/model/person.py
  lib/lp/registry/tests/test_person.py

To post a comment you must log in.
Steve Kowalik (stevenk) wrote :

The docstring for IPerson.findVisibleAssignedInProgressSpecs() makes no mention that it will only return five specs. I'd prefer the model function also includes the usual "See `Interface'." docstring.

This also means this code will now rely on get_specification_privacy_filter rather than OOPSing. Due to the query that is run by that function, I suspect we are now trading OOPSes for timeouts. I'd *really* prefer the access mechanisms for Specifications are de-normalized like they are for bugs and branches. I'd like to see some investigation of the query that get_specification_privacy_filter runs before I approve this branch.

review: Needs Fixing (code)
Deryck Hodge (deryck) wrote :

There is no observable difference in the two queries.

SELECT * from Specification WHERE
    Specification.owner = 2
    OR Specification.assignee = 2
    OR Specification.drafter = 2
    OR Specification.approver = 2
    OR Specification.id IN
    (
        SELECT SpecificationSubscription.specification
            FROM SpecificationSubscription
            WHERE SpecificationSubscription.person = 2
    )
    AND (
        NOT (Specification.implementation_status IN (0, 5, 90, 95))
        OR Specification.implementation_status = 60
        AND Specification.definition_status = 10
    ) AND (
        Specification.product IS NULL
        OR NOT Specification.product IN (
            SELECT Product.id FROM Product WHERE Product.active = true)
    ) AND (
        NOT (Specification.implementation_status = 0
        OR Specification.definition_status IN (90, 95)
        OR Specification.implementation_status = 60
        AND Specification.definition_status = 10)
    );

Total runtime: 155.522 ms

SELECT * from Specification WHERE
    Specification.owner = 2
    OR Specification.assignee = 2
    OR Specification.drafter = 2
    OR Specification.approver = 2
    OR Specification.id IN
    (
        SELECT SpecificationSubscription.specification
            FROM SpecificationSubscription
            WHERE SpecificationSubscription.person = 2
    )
    AND (
        Specification.information_type IN (NULL, 1)
        OR Specification.id IN (
            SELECT Specification.id FROM Specification
                JOIN AccessPolicy ON (
                    Specification.product = AccessPolicy.product
                    OR Specification.distribution = AccessPolicy.distribution
                )
                AND Specification.information_type = AccessPolicy.type
                    JOIN AccessPolicyGrantFlat ON
                    AccessPolicy.id = AccessPolicyGrantFlat.policy
                    LEFT JOIN AccessArtifact
                        ON AccessPolicyGrantFlat.artifact = AccessArtifact.id
                    JOIN TeamParticipation
                        ON TeamParticipation.team = AccessPolicyGrantFlat.grantee
                    AND TeamParticipation.person = 2
                    WHERE AccessPolicyGrantFlat.artifact IS NULL
                    OR AccessArtifact.specification = Specification.id
        )
    )
    AND (
        NOT (Specification.implementation_status IN (0, 5, 90, 95))
        OR Specification.implementation_status = 60
        AND Specification.definition_status = 10
    ) AND (
        Specification.product IS NULL
        OR NOT Specification.product IN (
            SELECT Product.id FROM Product WHERE Product.active = true)
    ) AND (
        NOT (Specification.implementation_status = 0
        OR Specification.definition_status IN (90, 95)
        OR Specification.implementation_status = 60
        AND Specification.definition_status = 10)
    );

Total runtime: 155.263 ms

Deryck Hodge (deryck) wrote :

As for the larger question of denormalized tables, we looked at the work people did and see it's great work, and discussed specifically with lifeless that we might not need them in the case of blueprints because the data is so much smaller than everything else. Regardless of whether we need them or not, we'll keep a close eye on timeouts during beta and add any performance-related changes we need to stay in good shape, denormalized tables or otherwise.

Deryck Hodge (deryck) wrote :

I wrote "the work people did" above and meant "the work Purple did." Sorry. Was typing quickly during the sprint here.

Steve Kowalik (stevenk) wrote :

Firstly, the thing I'm arguing for is two denormalized *columns* on Specification which means you don't need to JOIN against APG and APGF. However, I'm not going to block this MP with this particular point.

    AND (
        Specification.information_type IN (NULL, 1)

This, however, I will block on. You can't use NULL inside an IN, it will compile as SQL, but you are likely getting results you don't expect.

Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 12-10-25 12:29 AM, Steve Kowalik wrote:
> AND ( Specification.information_type IN (NULL, 1)
>
> This, however, I will block on. You can't use NULL inside an IN, it
> will compile as SQL, but youI are likely getting results you don't
> expect.

That SQL is not the actual SQL. We called storm.expr.compile to get
the expression, but this left us with a bunch of ? to fill in, and we
filled that part in incorrectly. The actual values that Launchpad
uses are: (1,2) i.e. PUBLIC and PUBLICSECURITY.

If there is a bug in get_specification_privacy filters, that is not a
bug in my code, so please do not block my branch on that basis.
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://www.enigmail.net/

iEYEARECAAYFAlCI6AsACgkQ0F+nu1YWqI2DUgCfbbumkODtgiAf6NzVz5UtC43o
xbsAniDAaA78ZYXAlhpFT2vN6D9mKJwc
=fLt9
-----END PGP SIGNATURE-----

Deryck Hodge (deryck) wrote :

Steve, we really need to get this landed, so I'm going to mark this approved for Aaron, since I read your comments to say you don't have any other objections. I'm basing my reading of this on the fact that you wouldn't block on the queries, and the NULL thing was just our mistake in data substitution.

If there are other concerns, please reach out the either me or Aaron and we can sort them out in another branch.

review: Approve
Steve Kowalik (stevenk) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/enums.py'
2--- lib/lp/blueprints/enums.py 2012-05-17 07:46:56 +0000
3+++ lib/lp/blueprints/enums.py 2012-10-26 09:03:23 +0000
4@@ -36,10 +36,10 @@
5 feature.
6
7 Note that some of the states associated with this schema correlate
8- to a "not started" definition. See Specification.started_clause for
9- further information, and make sure that it is updated (together with
10- the relevant database checks) if additional states are added that
11- are also "not started".
12+ to a "not started" definition. See spec_started_clause for further
13+ information, and make sure that it is updated (together with the relevant
14+ database checks) if additional states are added that are also "not
15+ started".
16 """
17 # The `UNKNOWN` state is considered "not started"
18 UNKNOWN = DBItem(0, """
19
20=== modified file 'lib/lp/blueprints/model/specification.py'
21--- lib/lp/blueprints/model/specification.py 2012-10-17 14:37:03 +0000
22+++ lib/lp/blueprints/model/specification.py 2012-10-26 09:03:23 +0000
23@@ -14,6 +14,7 @@
24 'SPECIFICATION_POLICY_ALLOWED_TYPES',
25 'SPECIFICATION_POLICY_DEFAULT_TYPES',
26 'SpecificationSet',
27+ 'spec_started_clause',
28 ]
29
30 from lazr.lifecycle.event import (
31@@ -576,28 +577,10 @@
32 else:
33 return False
34
35- # NB NB If you change this definition, please update the equivalent
36- # DB constraint Specification.specification_start_recorded_chk
37- # We choose to define "started" as the set of delivery states NOT
38- # in the values we select. Another option would be to say "anything less
39- # than a threshold" and to comment the dbschema that "anything not
40- # started should be less than the threshold". We'll see how maintainable
41- # this is.
42- started_clause = """
43- Specification.implementation_status NOT IN (%s, %s, %s, %s) OR
44- (Specification.implementation_status = %s AND
45- Specification.definition_status = %s)
46- """ % sqlvalues(SpecificationImplementationStatus.UNKNOWN.value,
47- SpecificationImplementationStatus.NOTSTARTED.value,
48- SpecificationImplementationStatus.DEFERRED.value,
49- SpecificationImplementationStatus.INFORMATIONAL.value,
50- SpecificationImplementationStatus.INFORMATIONAL.value,
51- SpecificationDefinitionStatus.APPROVED.value)
52-
53 @property
54 def is_started(self):
55 """See ISpecification. This is a code implementation of the
56- SQL in self.started_clause
57+ SQL in spec_started_clause
58 """
59 return (self.implementation_status not in [
60 SpecificationImplementationStatus.UNKNOWN,
61@@ -1334,3 +1317,23 @@
62 # A string in the filter is a text search filter.
63 clauses.append(fti_search(Specification, constraint))
64 return clauses
65+
66+
67+# NB NB If you change this definition, please update the equivalent
68+# DB constraint Specification.specification_start_recorded_chk
69+# We choose to define "started" as the set of delivery states NOT
70+# in the values we select. Another option would be to say "anything less
71+# than a threshold" and to comment the dbschema that "anything not
72+# started should be less than the threshold". We'll see how maintainable
73+# this is.
74+spec_started_clause = Or(Not(Specification.implementation_status.is_in([
75+ SpecificationImplementationStatus.UNKNOWN,
76+ SpecificationImplementationStatus.NOTSTARTED,
77+ SpecificationImplementationStatus.DEFERRED,
78+ SpecificationImplementationStatus.INFORMATIONAL,
79+ ])),
80+ And(Specification.implementation_status ==
81+ SpecificationImplementationStatus.INFORMATIONAL,
82+ Specification.definition_status ==
83+ SpecificationDefinitionStatus.APPROVED
84+ ))
85
86=== modified file 'lib/lp/registry/browser/person.py'
87--- lib/lp/registry/browser/person.py 2012-10-24 23:45:42 +0000
88+++ lib/lp/registry/browser/person.py 2012-10-26 09:03:23 +0000
89@@ -1718,7 +1718,8 @@
90 @cachedproperty
91 def assigned_specs_in_progress(self):
92 """Return up to 5 assigned specs that are being worked on."""
93- return list(self.context.assigned_specs_in_progress)
94+ specs = self.context.findVisibleAssignedInProgressSpecs(self.user)
95+ return list(specs)
96
97 @property
98 def has_assigned_bugs_or_specs_in_progress(self):
99
100=== modified file 'lib/lp/registry/browser/tests/test_person.py'
101--- lib/lp/registry/browser/tests/test_person.py 2012-10-24 23:40:33 +0000
102+++ lib/lp/registry/browser/tests/test_person.py 2012-10-26 09:03:23 +0000
103@@ -21,7 +21,9 @@
104
105 from lp.app.browser.lazrjs import TextAreaEditorWidget
106 from lp.app.errors import NotFoundError
107+from lp.app.enums import InformationType
108 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
109+from lp.blueprints.enums import SpecificationImplementationStatus
110 from lp.buildmaster.enums import BuildStatus
111 from lp.registry.browser.person import PersonView
112 from lp.registry.browser.team import TeamInvitationView
113@@ -115,7 +117,7 @@
114 'http://prod.launchpad.dev/~eris', self.view.openid_identity_url)
115
116
117-class TestPersonIndexView(TestCaseWithFactory):
118+class TestPersonIndexView(BrowserTestCase):
119
120 layer = DatabaseFunctionalLayer
121
122@@ -203,6 +205,21 @@
123 view = create_initialized_view(person, '+index')
124 self.assertThat(view.page_description, Equals(person_description))
125
126+ def test_assigned_blueprints(self):
127+ person = self.factory.makePerson()
128+
129+ def make_started_spec(information_type):
130+ enum = SpecificationImplementationStatus
131+ return self.factory.makeSpecification(
132+ implementation_status=enum.STARTED, assignee=person,
133+ information_type=information_type)
134+ public_spec = make_started_spec(InformationType.PUBLIC)
135+ private_spec = make_started_spec(InformationType.PROPRIETARY)
136+ with person_logged_in(None):
137+ browser = self.getViewBrowser(person)
138+ self.assertIn(public_spec.name, browser.contents)
139+ self.assertNotIn(private_spec.name, browser.contents)
140+
141
142 class TestPersonViewKarma(TestCaseWithFactory):
143
144
145=== modified file 'lib/lp/registry/doc/person.txt'
146--- lib/lp/registry/doc/person.txt 2012-10-23 02:30:37 +0000
147+++ lib/lp/registry/doc/person.txt 2012-10-26 09:03:23 +0000
148@@ -1191,7 +1191,7 @@
149 But from these two, only one has started.
150
151 >>> [(spec.name, spec.is_started)
152- ... for spec in carlos.assigned_specs_in_progress]
153+ ... for spec in carlos.findVisibleAssignedInProgressSpecs(None)]
154 [(u'svg-support', True)]
155
156 Just for fun, lets check the SAB. He should have one spec for which he
157
158=== modified file 'lib/lp/registry/interfaces/person.py'
159--- lib/lp/registry/interfaces/person.py 2012-10-24 23:45:42 +0000
160+++ lib/lp/registry/interfaces/person.py 2012-10-26 09:03:23 +0000
161@@ -850,9 +850,17 @@
162 "Sorted newest-first.")
163 assigned_specs = Attribute(
164 "Specifications assigned to this person, sorted newest first.")
165- assigned_specs_in_progress = Attribute(
166- "Specifications assigned to this person whose implementation is "
167- "started but not yet completed, sorted newest first.")
168+
169+ def findVisibleAssignedInProgressSpecs(user):
170+ """List specifications in progress assigned to this person.
171+
172+ In progress means their implementation is started but not yet
173+ completed. They are sorted newest first. No more than 5
174+ specifications are returned.
175+
176+ :param user: The use to use for determining visibility.
177+ """
178+
179 teamowner = exported(
180 PublicPersonChoice(
181 title=_('Team Owner'), required=False, readonly=False,
182
183=== modified file 'lib/lp/registry/model/person.py'
184--- lib/lp/registry/model/person.py 2012-10-24 23:45:42 +0000
185+++ lib/lp/registry/model/person.py 2012-10-26 09:03:23 +0000
186@@ -133,6 +133,7 @@
187 get_specification_privacy_filter,
188 HasSpecificationsMixin,
189 Specification,
190+ spec_started_clause,
191 )
192 from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
193 from lp.bugs.interfaces.bugtarget import IBugTarget
194@@ -804,17 +805,10 @@
195 return shortlist(Specification.selectBy(
196 assignee=self, orderBy=['-datecreated']))
197
198- @property
199- def assigned_specs_in_progress(self):
200- replacements = sqlvalues(assignee=self)
201- replacements['started_clause'] = Specification.started_clause
202- replacements['completed_clause'] = Specification.completeness_clause
203- query = """
204- (assignee = %(assignee)s)
205- AND (%(started_clause)s)
206- AND NOT (%(completed_clause)s)
207- """ % replacements
208- return Specification.select(query, orderBy=['-date_started'], limit=5)
209+ def findVisibleAssignedInProgressSpecs(self, user):
210+ """See `IPerson`."""
211+ return self.specifications(user, in_progress=True, quantity=5,
212+ sort=Desc(Specification.date_started))
213
214 @property
215 def unique_displayname(self):
216@@ -822,7 +816,7 @@
217 return "%s (%s)" % (self.displayname, self.name)
218
219 def specifications(self, user, sort=None, quantity=None, filter=None,
220- prejoin_people=True):
221+ prejoin_people=True, in_progress=False):
222 """See `IHasSpecifications`."""
223 from lp.blueprints.model.specificationsubscription import (
224 SpecificationSubscription,
225@@ -836,11 +830,6 @@
226
227 # Now look at the filter and fill in the unsaid bits.
228
229- # Defaults for completeness: if nothing is said about completeness
230- # then we want to show INCOMPLETE.
231- if SpecificationFilter.COMPLETE not in filter:
232- filter.add(SpecificationFilter.INCOMPLETE)
233-
234 # Defaults for acceptance: in this case we have nothing to do
235 # because specs are not accepted/declined against a person.
236
237@@ -872,12 +861,24 @@
238 [SpecificationSubscription.person == self]
239 )))
240 clauses = [Or(*role_clauses), get_specification_privacy_filter(user)]
241+ # Defaults for completeness: if nothing is said about completeness
242+ # then we want to show INCOMPLETE.
243+ if SpecificationFilter.COMPLETE not in filter:
244+ if (in_progress and SpecificationFilter.INCOMPLETE not in filter
245+ and SpecificationFilter.ALL not in filter):
246+ clauses.append(spec_started_clause)
247+ filter.add(SpecificationFilter.INCOMPLETE)
248+
249 clauses.extend(get_specification_filters(filter))
250 results = Store.of(self).find(Specification, *clauses)
251 # The default sort is priority descending, so only explictly sort for
252 # DATE.
253 if sort == SpecificationSort.DATE:
254- results = results.order_by(Desc(Specification.datecreated))
255+ sort = Desc(Specification.datecreated)
256+ elif getattr(sort, 'enum', None) is SpecificationSort:
257+ sort = None
258+ if sort is not None:
259+ results = results.order_by(sort)
260 if quantity is not None:
261 results = results[:quantity]
262 return results
263
264=== modified file 'lib/lp/registry/tests/test_person.py'
265--- lib/lp/registry/tests/test_person.py 2012-10-19 18:26:01 +0000
266+++ lib/lp/registry/tests/test_person.py 2012-10-26 09:03:23 +0000
267@@ -12,6 +12,7 @@
268 from lazr.restful.utils import smartquote
269 import pytz
270 from storm.store import Store
271+from storm.locals import Desc
272 from testtools.matchers import (
273 Equals,
274 LessThan,
275@@ -1907,3 +1908,59 @@
276 self.assertEqual(
277 [blueprint1],
278 list_result(blueprint1.owner, user=grant.grantee))
279+
280+ def test_storm_sort(self):
281+ # A Storm expression can be used to sort specs.
282+ owner = self.factory.makePerson()
283+ spec = self.factory.makeSpecification(owner=owner, name='a')
284+ spec2 = self.factory.makeSpecification(owner=owner, name='z')
285+ spec3 = self.factory.makeSpecification(owner=owner, name='b')
286+ self.assertEqual([spec2, spec3, spec],
287+ list(owner.specifications(owner,
288+ sort=Desc(Specification.name))))
289+
290+ def test_in_progress(self):
291+ # In-progress filters to exclude not-started and completed.
292+ enum = SpecificationImplementationStatus
293+ notstarted = self.factory.makeSpecification(
294+ implementation_status=enum.NOTSTARTED)
295+ owner = notstarted.owner
296+ started = self.factory.makeSpecification(
297+ owner=owner, implementation_status=enum.STARTED)
298+ self.factory.makeSpecification(
299+ owner=owner, implementation_status=enum.IMPLEMENTED)
300+ specs = list(owner.specifications(owner, in_progress=True))
301+ self.assertEqual([started], specs)
302+
303+ def test_in_progress_all(self):
304+ # SpecificationFilter.ALL overrides in_progress.
305+ enum = SpecificationImplementationStatus
306+ notstarted = self.factory.makeSpecification(
307+ implementation_status=enum.NOTSTARTED)
308+ owner = notstarted.owner
309+ specs = list(owner.specifications(
310+ owner, filter=[SpecificationFilter.ALL], in_progress=True))
311+ self.assertEqual([notstarted], specs)
312+
313+ def test_complete_overrides_in_progress(self):
314+ # SpecificationFilter.COMPLETE overrides in_progress.
315+ enum = SpecificationImplementationStatus
316+ started = self.factory.makeSpecification(
317+ implementation_status=enum.STARTED)
318+ owner = started.owner
319+ implemented = self.factory.makeSpecification(
320+ implementation_status=enum.IMPLEMENTED, owner=owner)
321+ specs = list(owner.specifications(
322+ owner, filter=[SpecificationFilter.COMPLETE], in_progress=True))
323+ self.assertEqual([implemented], specs)
324+
325+ def test_incomplete_overrides_in_progress(self):
326+ # SpecificationFilter.INCOMPLETE overrides in_progress.
327+ enum = SpecificationImplementationStatus
328+ notstarted = self.factory.makeSpecification(
329+ implementation_status=enum.NOTSTARTED)
330+ owner = notstarted.owner
331+ specs = list(owner.specifications(
332+ owner, filter=[SpecificationFilter.INCOMPLETE],
333+ in_progress=True))
334+ self.assertEqual([notstarted], specs)
335
336=== modified file 'lib/lp/testing/factory.py'
337--- lib/lp/testing/factory.py 2012-10-22 02:30:44 +0000
338+++ lib/lp/testing/factory.py 2012-10-26 09:03:23 +0000
339@@ -2124,11 +2124,6 @@
340 distribution=distribution,
341 priority=priority)
342 naked_spec = removeSecurityProxy(spec)
343- if information_type is not None:
344- if proprietary:
345- naked_spec.target._ensurePolicies([information_type])
346- naked_spec.transitionToInformationType(
347- information_type, removeSecurityProxy(spec.target).owner)
348 if status.name not in status_names:
349 # Set the closed status after the status has a sane initial state.
350 naked_spec.definition_status = status
351@@ -2144,6 +2139,11 @@
352 if implementation_status is not None:
353 naked_spec.implementation_status = implementation_status
354 naked_spec.updateLifecycleStatus(owner)
355+ if information_type is not None:
356+ if proprietary:
357+ naked_spec.target._ensurePolicies([information_type])
358+ naked_spec.transitionToInformationType(
359+ information_type, removeSecurityProxy(spec.target).owner)
360 return spec
361
362 makeBlueprint = makeSpecification