launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #13610
[Merge] lp:~abentley/launchpad/person-assigned-specs-in-progress into lp:launchpad
Aaron Bentley has proposed merging lp:~abentley/launchpad/person-assigned-specs-in-progress into lp:launchpad.
Commit message:
Fix +portlet-currentfocus with proprietary specs.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1068719 in Launchpad itself: "Person overview page breaks when assigned proprietary blueprints"
https://bugs.launchpad.net/launchpad/+bug/1068719
For more details, see:
https://code.launchpad.net/~abentley/launchpad/person-assigned-specs-in-progress/+merge/130888
= 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
--
https://code.launchpad.net/~abentley/launchpad/person-assigned-specs-in-progress/+merge/130888
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/person-assigned-specs-in-progress into lp:launchpad.
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2012-10-17 14:37:03 +0000
+++ lib/lp/blueprints/model/specification.py 2012-10-22 20:00:30 +0000
@@ -14,6 +14,7 @@
'SPECIFICATION_POLICY_ALLOWED_TYPES',
'SPECIFICATION_POLICY_DEFAULT_TYPES',
'SpecificationSet',
+ 'spec_started_clause',
]
from lazr.lifecycle.event import (
@@ -1334,3 +1335,16 @@
# A string in the filter is a text search filter.
clauses.append(fti_search(Specification, constraint))
return clauses
+
+
+spec_started_clause = Or(Not(Specification.implementation_status.is_in([
+ SpecificationImplementationStatus.UNKNOWN,
+ SpecificationImplementationStatus.NOTSTARTED,
+ SpecificationImplementationStatus.DEFERRED,
+ SpecificationImplementationStatus.INFORMATIONAL,
+ ])),
+ And(Specification.implementation_status ==
+ SpecificationImplementationStatus.INFORMATIONAL,
+ Specification.definition_status ==
+ SpecificationDefinitionStatus.APPROVED
+ ))
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2012-10-19 16:54:35 +0000
+++ lib/lp/registry/browser/person.py 2012-10-22 20:00:30 +0000
@@ -1705,7 +1705,8 @@
@cachedproperty
def assigned_specs_in_progress(self):
"""Return up to 5 assigned specs that are being worked on."""
- return list(self.context.assigned_specs_in_progress)
+ specs = self.context.findVisibleAssignedInProgressSpecs(self.user)
+ return list(specs)
@property
def has_assigned_bugs_or_specs_in_progress(self):
=== modified file 'lib/lp/registry/browser/tests/test_person.py'
--- lib/lp/registry/browser/tests/test_person.py 2012-09-06 00:01:38 +0000
+++ lib/lp/registry/browser/tests/test_person.py 2012-10-22 20:00:30 +0000
@@ -21,7 +21,9 @@
from lp.app.browser.lazrjs import TextAreaEditorWidget
from lp.app.errors import NotFoundError
+from lp.app.enums import InformationType
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.blueprints.enums import SpecificationImplementationStatus
from lp.buildmaster.enums import BuildStatus
from lp.registry.browser.person import PersonView
from lp.registry.browser.team import TeamInvitationView
@@ -115,7 +117,7 @@
'http://prod.launchpad.dev/~eris', self.view.openid_identity_url)
-class TestPersonIndexView(TestCaseWithFactory):
+class TestPersonIndexView(BrowserTestCase):
layer = DatabaseFunctionalLayer
@@ -203,6 +205,21 @@
view = create_initialized_view(person, '+index')
self.assertThat(view.page_description, Equals(person_description))
+ def test_assigned_blueprints(self):
+ person = self.factory.makePerson()
+
+ def make_started_spec(information_type):
+ enum = SpecificationImplementationStatus
+ return self.factory.makeSpecification(
+ implementation_status=enum.STARTED, assignee=person,
+ information_type=information_type)
+ public_spec = make_started_spec(InformationType.PUBLIC)
+ private_spec = make_started_spec(InformationType.PROPRIETARY)
+ with person_logged_in(None):
+ browser = self.getViewBrowser(person)
+ self.assertIn(public_spec.name, browser.contents)
+ self.assertNotIn(private_spec.name, browser.contents)
+
class TestPersonViewKarma(TestCaseWithFactory):
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2012-10-16 00:48:55 +0000
+++ lib/lp/registry/interfaces/person.py 2012-10-22 20:00:30 +0000
@@ -850,9 +850,16 @@
"Sorted newest-first.")
assigned_specs = Attribute(
"Specifications assigned to this person, sorted newest first.")
- assigned_specs_in_progress = Attribute(
- "Specifications assigned to this person whose implementation is "
- "started but not yet completed, sorted newest first.")
+
+ def findVisibleAssignedInProgressSpecs(user):
+ """List specifications in progress assigned to this person.
+
+ In progress means their implementation is started but not yet
+ completed. They are sorted newest first.
+
+ :param user: The use to use for determining visibility.
+ """
+
teamowner = exported(
PublicPersonChoice(
title=_('Team Owner'), required=False, readonly=False,
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-10-19 16:54:35 +0000
+++ lib/lp/registry/model/person.py 2012-10-22 20:00:30 +0000
@@ -130,6 +130,7 @@
get_specification_privacy_filter,
HasSpecificationsMixin,
Specification,
+ spec_started_clause,
)
from lp.blueprints.model.specificationworkitem import SpecificationWorkItem
from lp.bugs.interfaces.bugtarget import IBugTarget
@@ -798,17 +799,9 @@
return shortlist(Specification.selectBy(
assignee=self, orderBy=['-datecreated']))
- @property
- def assigned_specs_in_progress(self):
- replacements = sqlvalues(assignee=self)
- replacements['started_clause'] = Specification.started_clause
- replacements['completed_clause'] = Specification.completeness_clause
- query = """
- (assignee = %(assignee)s)
- AND (%(started_clause)s)
- AND NOT (%(completed_clause)s)
- """ % replacements
- return Specification.select(query, orderBy=['-date_started'], limit=5)
+ def findVisibleAssignedInProgressSpecs(self, user):
+ return self.specifications(user, in_progress=True, quantity=5,
+ sort=Desc(Specification.date_started))
@property
def unique_displayname(self):
@@ -816,7 +809,7 @@
return "%s (%s)" % (self.displayname, self.name)
def specifications(self, user, sort=None, quantity=None, filter=None,
- prejoin_people=True):
+ prejoin_people=True, in_progress=False):
"""See `IHasSpecifications`."""
from lp.blueprints.model.specificationsubscription import (
SpecificationSubscription,
@@ -830,11 +823,6 @@
# Now look at the filter and fill in the unsaid bits.
- # Defaults for completeness: if nothing is said about completeness
- # then we want to show INCOMPLETE.
- if SpecificationFilter.COMPLETE not in filter:
- filter.add(SpecificationFilter.INCOMPLETE)
-
# Defaults for acceptance: in this case we have nothing to do
# because specs are not accepted/declined against a person.
@@ -866,12 +854,24 @@
[SpecificationSubscription.person == self]
)))
clauses = [Or(*role_clauses), get_specification_privacy_filter(user)]
+ # Defaults for completeness: if nothing is said about completeness
+ # then we want to show INCOMPLETE.
+ if SpecificationFilter.COMPLETE not in filter:
+ if (in_progress and SpecificationFilter.INCOMPLETE not in filter
+ and SpecificationFilter.ALL not in filter):
+ clauses.append(spec_started_clause)
+ filter.add(SpecificationFilter.INCOMPLETE)
+
clauses.extend(get_specification_filters(filter))
results = Store.of(self).find(Specification, *clauses)
# The default sort is priority descending, so only explictly sort for
# DATE.
if sort == SpecificationSort.DATE:
- results = results.order_by(Desc(Specification.datecreated))
+ sort = Desc(Specification.datecreated)
+ elif getattr(sort, 'enum', None) is SpecificationSort:
+ sort = None
+ if sort is not None:
+ results = results.order_by(sort)
if quantity is not None:
results = results[:quantity]
return results
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2012-10-17 15:16:09 +0000
+++ lib/lp/registry/tests/test_person.py 2012-10-22 20:00:30 +0000
@@ -12,6 +12,7 @@
from lazr.restful.utils import smartquote
import pytz
from storm.store import Store
+from storm.locals import Desc
from testtools.matchers import (
Equals,
LessThan,
@@ -1815,3 +1816,59 @@
self.assertEqual(
[blueprint1],
list_result(blueprint1.owner, user=grant.grantee))
+
+ def test_storm_sort(self):
+ # A Storm expression can be used to sort specs.
+ owner = self.factory.makePerson()
+ spec = self.factory.makeSpecification(owner=owner, name='a')
+ spec2 = self.factory.makeSpecification(owner=owner, name='z')
+ spec3 = self.factory.makeSpecification(owner=owner, name='b')
+ self.assertEqual([spec2, spec3, spec],
+ list(owner.specifications(owner,
+ sort=Desc(Specification.name))))
+
+ def test_in_progress(self):
+ # In-progress filters to exclude not-started and completed.
+ enum = SpecificationImplementationStatus
+ notstarted = self.factory.makeSpecification(
+ implementation_status=enum.NOTSTARTED)
+ owner = notstarted.owner
+ started = self.factory.makeSpecification(
+ owner=owner, implementation_status=enum.STARTED)
+ self.factory.makeSpecification(
+ owner=owner, implementation_status=enum.IMPLEMENTED)
+ specs = list(owner.specifications(owner, in_progress=True))
+ self.assertEqual([started], specs)
+
+ def test_in_progress_all(self):
+ # SpecificationFilter.ALL overrides in_progress.
+ enum = SpecificationImplementationStatus
+ notstarted = self.factory.makeSpecification(
+ implementation_status=enum.NOTSTARTED)
+ owner = notstarted.owner
+ specs = list(owner.specifications(
+ owner, filter=[SpecificationFilter.ALL], in_progress=True))
+ self.assertEqual([notstarted], specs)
+
+ def test_complete_overrides_in_progress(self):
+ # SpecificationFilter.COMPLETE overrides in_progress.
+ enum = SpecificationImplementationStatus
+ started = self.factory.makeSpecification(
+ implementation_status=enum.STARTED)
+ owner = started.owner
+ implemented = self.factory.makeSpecification(
+ implementation_status=enum.IMPLEMENTED, owner=owner)
+ specs = list(owner.specifications(
+ owner, filter=[SpecificationFilter.COMPLETE], in_progress=True))
+ self.assertEqual([implemented], specs)
+
+ def test_incomplete_overrides_in_progress(self):
+ # SpecificationFilter.INCOMPLETE overrides in_progress.
+ enum = SpecificationImplementationStatus
+ notstarted = self.factory.makeSpecification(
+ implementation_status=enum.NOTSTARTED)
+ owner = notstarted.owner
+ specs = list(owner.specifications(
+ owner, filter=[SpecificationFilter.INCOMPLETE],
+ in_progress=True))
+ self.assertEqual([notstarted], specs)
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2012-10-19 16:54:35 +0000
+++ lib/lp/testing/factory.py 2012-10-22 20:00:30 +0000
@@ -2124,11 +2124,6 @@
distribution=distribution,
priority=priority)
naked_spec = removeSecurityProxy(spec)
- if information_type is not None:
- if proprietary:
- naked_spec.target._ensurePolicies([information_type])
- naked_spec.transitionToInformationType(
- information_type, removeSecurityProxy(spec.target).owner)
if status.name not in status_names:
# Set the closed status after the status has a sane initial state.
naked_spec.definition_status = status
@@ -2144,6 +2139,11 @@
if implementation_status is not None:
naked_spec.implementation_status = implementation_status
naked_spec.updateLifecycleStatus(owner)
+ if information_type is not None:
+ if proprietary:
+ naked_spec.target._ensurePolicies([information_type])
+ naked_spec.transitionToInformationType(
+ information_type, removeSecurityProxy(spec.target).owner)
return spec
makeBlueprint = makeSpecification
Follow ups