← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/user-blueprints into lp:launchpad with lp:~abentley/launchpad/user-blueprints-tests as a prerequisite.

Commit message:
Re-implement Person.specifications using Storm expressions.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1063275 in Launchpad itself: "user blueprints page cannot be viewed"
  https://bugs.launchpad.net/launchpad/+bug/1063275

For more details, see:
https://code.launchpad.net/~abentley/launchpad/user-blueprints/+merge/129945

= 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
-- 
https://code.launchpad.net/~abentley/launchpad/user-blueprints/+merge/129945
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/user-blueprints into lp:launchpad.
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py	2012-10-10 14:35:49 +0000
+++ lib/lp/blueprints/model/specification.py	2012-10-16 16:57:28 +0000
@@ -5,6 +5,7 @@
 
 __metaclass__ = type
 __all__ = [
+    'get_specification_filters',
     'get_specification_privacy_filter',
     'HasSpecificationsMixin',
     'recursive_blocked_query',
@@ -34,6 +35,7 @@
     Join,
     LeftJoin,
     Or,
+    Not,
     Select,
     )
 from storm.locals import (
@@ -104,6 +106,7 @@
     SQLBase,
     sqlvalues,
     )
+from lp.services.database.stormexpr import fti_search
 from lp.services.mail.helpers import get_contact_email_addresses
 from lp.services.propertycache import (
     cachedproperty,
@@ -1289,3 +1292,38 @@
                 where=Or(
                     AccessPolicyGrantFlat.abstract_artifact_id == None,
                     AccessArtifact.specification_id == Specification.id))))
+
+
+def get_specification_filters(filter):
+    from lp.registry.model.product import Product
+    # If Product is used, it must be active.
+    clauses = [Or(Specification.product == None,
+                  Not(Specification.productID.is_in(Select(Product.id,
+                      Product.active == False))))]
+    # ALL is the trump card
+    if SpecificationFilter.ALL in filter:
+        return clauses
+    # look for informational specs
+    if SpecificationFilter.INFORMATIONAL in filter:
+        clauses.append(Specification.implementation_status ==
+                       SpecificationImplementationStatus.INFORMATIONAL)
+    # filter based on completion. see the implementation of
+    # Specification.is_complete() for more details
+    if SpecificationFilter.COMPLETE in filter:
+        clauses.append(Specification.storm_completeness())
+    if SpecificationFilter.INCOMPLETE in filter:
+        clauses.append(Not(Specification.storm_completeness()))
+
+    # Filter for validity. If we want valid specs only then we should exclude
+    # all OBSOLETE or SUPERSEDED specs
+    if SpecificationFilter.VALID in filter:
+        clauses.append(Not(Specification.definition_status.is_in([
+            SpecificationDefinitionStatus.OBSOLETE,
+            SpecificationDefinitionStatus.SUPERSEDED,
+        ])))
+    # Filter for specification text
+    for constraint in filter:
+        if isinstance(constraint, basestring):
+            # a string in the filter is a text search filter
+            clauses.append(fti_search(Specification, constraint))
+    return clauses

=== modified file 'lib/lp/blueprints/model/sprint.py'
--- lib/lp/blueprints/model/sprint.py	2012-10-12 01:56:34 +0000
+++ lib/lp/blueprints/model/sprint.py	2012-10-16 16:57:28 +0000
@@ -17,9 +17,7 @@
     )
 from storm.locals import (
     Desc,
-    Not,
     Or,
-    Select,
     Store,
     )
 from zope.component import getUtility
@@ -33,7 +31,6 @@
     )
 from lp.blueprints.enums import (
     SpecificationFilter,
-    SpecificationImplementationStatus,
     SpecificationSort,
     SprintSpecificationStatus,
     )
@@ -42,6 +39,7 @@
     ISprintSet,
     )
 from lp.blueprints.model.specification import (
+    get_specification_filters,
     get_specification_privacy_filter,
     HasSpecificationsMixin,
     )
@@ -59,7 +57,6 @@
     quote,
     SQLBase,
     )
-from lp.services.database.stormexpr import fti_search
 from lp.services.propertycache import cachedproperty
 
 
@@ -124,12 +121,8 @@
         """
         # import here to avoid circular deps
         from lp.blueprints.model.specification import Specification
-        from lp.registry.model.product import Product
         query = [SprintSpecification.sprintID == self.id,
-                 SprintSpecification.specificationID == Specification.id,
-                 Or(Specification.product == None,
-                    Not(Specification.productID.is_in(Select(Product.id,
-                        Product.active == False))))]
+                 SprintSpecification.specificationID == Specification.id]
         query.append(get_specification_privacy_filter(user))
         if not filter:
             # filter could be None or [] then we decide the default
@@ -144,16 +137,6 @@
         #  - informational.
         #
 
-        # look for informational specs
-        if SpecificationFilter.INFORMATIONAL in filter:
-            query.append(Specification.implementation_status ==
-                         SpecificationImplementationStatus.INFORMATIONAL)
-        # filter based on completion. see the implementation of
-        # Specification.is_complete() for more details
-        if SpecificationFilter.COMPLETE in filter:
-            query.append(Specification.storm_completeness())
-        if SpecificationFilter.INCOMPLETE in filter:
-            query.append(Not(Specification.storm_completeness()))
         sprint_status = []
         # look for specs that have a particular SprintSpecification
         # status (proposed, accepted or declined)
@@ -168,10 +151,7 @@
         if len(statuses) > 0:
             query.append(Or(*statuses))
         # Filter for specification text
-        for constraint in filter:
-            if isinstance(constraint, basestring):
-                # a string in the filter is a text search filter
-                query.append(fti_search(Specification, constraint))
+        query.extend(get_specification_filters(filter))
         return query
 
     def all_specifications(self, user):

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2012-10-03 07:25:14 +0000
+++ lib/lp/registry/model/person.py	2012-10-16 16:57:28 +0000
@@ -128,6 +128,7 @@
     SpecificationSort,
     )
 from lp.blueprints.model.specification import (
+    get_specification_filters,
     HasSpecificationsMixin,
     Specification,
     )
@@ -818,27 +819,22 @@
     def specifications(self, user, sort=None, quantity=None, filter=None,
                        prejoin_people=True):
         """See `IHasSpecifications`."""
-
-        # Make a new list of the filter, so that we do not mutate what we
+        from lp.blueprints.model.specificationsubscription import (
+            SpecificationSubscription,
+            )
+        # Make a new copy of the filter, so that we do not mutate what we
         # were passed as a filter
-        if not filter:
-            # if no filter was passed (None or []) then we must decide the
-            # default filtering, and for a person we want related incomplete
-            # specs
-            filter = [SpecificationFilter.INCOMPLETE]
+        if filter is None:
+            filter = set()
+        else:
+            filter = set(filter)
 
         # 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
-        completeness = False
-        for option in [
-            SpecificationFilter.COMPLETE,
-            SpecificationFilter.INCOMPLETE]:
-            if option in filter:
-                completeness = True
-        if completeness is False:
-            filter.append(SpecificationFilter.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
@@ -846,101 +842,40 @@
         # defaults for informationalness: we don't have to do anything
         # because the default if nothing is said is ANY
 
-        # if no roles are given then we want everything
-        linked = False
+
         roles = set([
             SpecificationFilter.CREATOR,
             SpecificationFilter.ASSIGNEE,
             SpecificationFilter.DRAFTER,
             SpecificationFilter.APPROVER,
             SpecificationFilter.SUBSCRIBER])
-        for role in roles:
-            if role in filter:
-                linked = True
-        if not linked:
-            for role in roles:
-                filter.append(role)
-
-        # sort by priority descending, by default
-        if sort is None or sort == SpecificationSort.PRIORITY:
-            order = ['-priority', 'Specification.definition_status',
-                     'Specification.name']
-        elif sort == SpecificationSort.DATE:
-            order = ['-Specification.datecreated', 'Specification.id']
-
-        # figure out what set of specifications we are interested in. for
-        # products, we need to be able to filter on the basis of:
-        #
-        #  - role (owner, drafter, approver, subscriber, assignee etc)
-        #  - completeness.
-        #  - informational.
-        #
-
-        # in this case the "base" is quite complicated because it is
-        # determined by the roles so lets do that first
-
-        base = '(1=0'  # we want to start with a FALSE and OR them
+        # if no roles are given then we want everything
+        if filter.intersection(roles) == set():
+            filter.update(roles)
+        role_clauses = []
         if SpecificationFilter.CREATOR in filter:
-            base += ' OR Specification.owner = %(my_id)d'
+            role_clauses.append(Specification.owner == self)
         if SpecificationFilter.ASSIGNEE in filter:
-            base += ' OR Specification.assignee = %(my_id)d'
+            role_clauses.append(Specification.assignee == self)
         if SpecificationFilter.DRAFTER in filter:
-            base += ' OR Specification.drafter = %(my_id)d'
+            role_clauses.append(Specification.drafter == self)
         if SpecificationFilter.APPROVER in filter:
-            base += ' OR Specification.approver = %(my_id)d'
+            role_clauses.append(Specification.approver == self)
         if SpecificationFilter.SUBSCRIBER in filter:
-            base += """ OR Specification.id in
-                (SELECT specification FROM SpecificationSubscription
-                 WHERE person = %(my_id)d)"""
-        base += ') '
-
-        # filter out specs on inactive products
-        base += """AND (Specification.product IS NULL OR
-                        Specification.product NOT IN
-                         (SELECT Product.id FROM Product
-                          WHERE Product.active IS FALSE))
-                """
-
-        base = base % {'my_id': self.id}
-
-        query = base
-        # look for informational specs
-        if SpecificationFilter.INFORMATIONAL in filter:
-            query += (' AND Specification.implementation_status = %s' %
-                quote(SpecificationImplementationStatus.INFORMATIONAL))
-
-        # filter based on completion. see the implementation of
-        # Specification.is_complete() for more details
-        completeness = Specification.completeness_clause
-
-        if SpecificationFilter.COMPLETE in filter:
-            query += ' AND ( %s ) ' % completeness
-        elif SpecificationFilter.INCOMPLETE in filter:
-            query += ' AND NOT ( %s ) ' % completeness
-
-        # Filter for validity. If we want valid specs only then we should
-        # exclude all OBSOLETE or SUPERSEDED specs
-        if SpecificationFilter.VALID in filter:
-            query += (
-                ' AND Specification.definition_status NOT IN ( %s, ''%s ) ' %
-                sqlvalues(SpecificationDefinitionStatus.OBSOLETE,
-                          SpecificationDefinitionStatus.SUPERSEDED))
-
-        # ALL is the trump card
-        if SpecificationFilter.ALL in filter:
-            query = base
-
-        # Filter for specification text
-        for constraint in filter:
-            if isinstance(constraint, basestring):
-                # a string in the filter is a text search filter
-                query += ' AND Specification.fti @@ ftq(%s) ' % quote(
-                    constraint)
-
-        results = Specification.select(query, orderBy=order,
-            limit=quantity)
-        if prejoin_people:
-            results = results.prejoin(['assignee', 'approver', 'drafter'])
+            role_clauses.append(
+                Specification.id.is_in(
+                    Select(SpecificationSubscription.specificationID,
+                        [SpecificationSubscription.person == self]
+                    )))
+        clauses = [Or(*role_clauses)]
+        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))
+        if quantity is not None:
+            results = results[:quantity]
         return results
 
     # XXX: Tom Berger 2008-04-14 bug=191799:


Follow ups