← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~adeuring/launchpad/bug-1086876 into lp:launchpad

 

Abel Deuring has proposed merging lp:~adeuring/launchpad/bug-1086876 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1086876 in Launchpad itself: "The assignee, approver, drafter of a private blueprint should automatically be subscribed to the blueprint so that they have an access grant"
  https://bugs.launchpad.net/launchpad/+bug/1086876

For more details, see:
https://code.launchpad.net/~adeuring/launchpad/bug-1086876/+merge/138514

The fix is simple: I renamed the ForeignKey attributes assignee/drafter/
approver of the class Specification to _assignee/_drafter/_approver and
defined assignee/drafter/approver as computed properties. When the
attributes are set to a new value, the new "role owner" are subscribed, if
necessary.

Running "/bin/test blueprints -vv" revealed a number of locations where
assignee/drafter/approver are used to build SQL queries; I changed this
to _assignee etc there too.

The change in lib/lp/blueprints/browser/sprint.py does not result in
additional SQL queries to load the assignee/drafter because the specs
were loaded via self.context.specifications (line 466), and the methods
HasSpecificationsMixin.specifications() already pre-load the Person
objects.

"/bin/test blueprints -vv" also revealed a completely unrelated problem
in some tests; I filed bug 1087314 and added an XXX to
lib/lp/blueprints/browser/specification.py.

tests: ./bin/test blueprints -vvt  test_setting_.*_subscribes

no lint

-- 
https://code.launchpad.net/~adeuring/launchpad/bug-1086876/+merge/138514
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/bug-1086876 into lp:launchpad.
=== modified file 'lib/lp/blueprints/browser/specification.py'
--- lib/lp/blueprints/browser/specification.py	2012-11-30 21:26:50 +0000
+++ lib/lp/blueprints/browser/specification.py	2012-12-06 17:11:22 +0000
@@ -1440,6 +1440,9 @@
         process.stdin.close()
         output = process.stdout.read()
         err = process.stderr.read()
+        # XXX Abel Deuring 2012-12-06, bug 1087314
+        # err may just contain a warning, while the image might be rendered
+        # correctly. We should not raise an error in this case.
         if err:
             raise ProblemRenderingGraph(err, output)
         return output

=== modified file 'lib/lp/blueprints/browser/sprint.py'
--- lib/lp/blueprints/browser/sprint.py	2012-09-26 20:56:43 +0000
+++ lib/lp/blueprints/browser/sprint.py	2012-12-06 17:11:22 +0000
@@ -492,12 +492,12 @@
         for spec in model_specs:
             # Get the list of attendees that will attend the sprint.
             spec_people = people[spec.id]
-            if spec.assigneeID is not None:
-                spec_people[spec.assigneeID] = True
-                attendee_set.add(spec.assigneeID)
-            if spec.drafterID is not None:
-                spec_people[spec.drafterID] = True
-                attendee_set.add(spec.drafterID)
+            if spec.assignee is not None:
+                spec_people[spec.assignee.id] = True
+                attendee_set.add(spec.assignee.id)
+            if spec.drafter is not None:
+                spec_people[spec.drafter.id] = True
+                attendee_set.add(spec.drafter.id)
         people_by_id = dict((person.id, person) for person in
             getUtility(IPersonSet).getPrecachedPersonsFromIDs(attendee_set))
         self.specifications = [

=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py	2012-12-01 15:56:06 +0000
+++ lib/lp/blueprints/model/specification.py	2012-12-06 17:11:22 +0000
@@ -182,13 +182,13 @@
         default=SpecificationDefinitionStatus.NEW)
     priority = EnumCol(schema=SpecificationPriority, notNull=True,
         default=SpecificationPriority.UNDEFINED)
-    assignee = ForeignKey(dbName='assignee', notNull=False,
-        foreignKey='Person',
-        storm_validator=validate_public_person, default=None)
-    drafter = ForeignKey(dbName='drafter', notNull=False,
-        foreignKey='Person',
-        storm_validator=validate_public_person, default=None)
-    approver = ForeignKey(dbName='approver', notNull=False,
+    _assignee = ForeignKey(dbName='assignee', notNull=False,
+        foreignKey='Person',
+        storm_validator=validate_public_person, default=None)
+    _drafter = ForeignKey(dbName='drafter', notNull=False,
+        foreignKey='Person',
+        storm_validator=validate_public_person, default=None)
+    _approver = ForeignKey(dbName='approver', notNull=False,
         foreignKey='Person',
         storm_validator=validate_public_person, default=None)
     owner = ForeignKey(
@@ -265,6 +265,42 @@
     information_type = EnumCol(
         enum=InformationType, notNull=True, default=InformationType.PUBLIC)
 
+    def set_assignee(self, person):
+        self.subscribeIfAccessGrantNeeded(person)
+        self._assignee = person
+
+    def get_assignee(self):
+        return self._assignee
+
+    assignee = property(get_assignee, set_assignee)
+
+    def set_drafter(self, person):
+        self.subscribeIfAccessGrantNeeded(person)
+        self._drafter = person
+
+    def get_drafter(self):
+        return self._drafter
+
+    drafter = property(get_drafter, set_drafter)
+
+    def set_approver(self, person):
+        self.subscribeIfAccessGrantNeeded(person)
+        self._approver = person
+
+    def get_approver(self):
+        return self._approver
+
+    approver = property(get_approver, set_approver)
+
+    def subscribeIfAccessGrantNeeded(self, person):
+        """Subscribe person if this specification is not public and if
+        the person does not already have grants to access the specification.
+        """
+        if person is None or self.userCanView(person):
+            return
+        current_user = getUtility(ILaunchBag).user
+        self.subscribe(person, subscribed_by=current_user)
+
     @cachedproperty
     def subscriptions(self):
         """Sort the subscriptions"""
@@ -1027,14 +1063,16 @@
             clauses = [SQL(clauses)]
 
         def cache_people(rows):
-            """DecoratedResultSet pre_iter_hook to eager load Person attributes."""
+            """DecoratedResultSet pre_iter_hook to eager load Person
+             attributes.
+            """
             from lp.registry.model.person import Person
             # Find the people we need:
             person_ids = set()
             for spec in rows:
-                person_ids.add(spec.assigneeID)
-                person_ids.add(spec.approverID)
-                person_ids.add(spec.drafterID)
+                person_ids.add(spec._assigneeID)
+                person_ids.add(spec._approverID)
+                person_ids.add(spec._drafterID)
             person_ids.discard(None)
             if not person_ids:
                 return
@@ -1057,7 +1095,8 @@
                     index += 1
                     decorator(person, column)
 
-        results = IStore(Specification).using(*tables).find(Specification, *clauses)
+        results = IStore(Specification).using(*tables).find(
+            Specification, *clauses)
         return DecoratedResultSet(results, pre_iter_hook=cache_people)
 
     @property
@@ -1145,7 +1184,8 @@
                 order = [Desc(Specification.datecreated), Specification.id]
 
         if prejoin_people:
-            results = self._preload_specifications_people(privacy_tables, clauses)
+            results = self._preload_specifications_people(
+                privacy_tables, clauses)
         else:
             results = store.using(*privacy_tables).find(
                 Specification, *clauses)
@@ -1190,8 +1230,8 @@
         spec = Specification(name=name, title=title, specurl=specurl,
             summary=summary, priority=priority,
             definition_status=definition_status, owner=owner,
-            approver=approver, product=product, distribution=distribution,
-            assignee=assignee, drafter=drafter, whiteboard=whiteboard)
+            _approver=approver, product=product, distribution=distribution,
+            _assignee=assignee, _drafter=drafter, whiteboard=whiteboard)
         spec.transitionToInformationType(information_type, None)
         return spec
 

=== modified file 'lib/lp/blueprints/tests/test_specification.py'
--- lib/lp/blueprints/tests/test_specification.py	2012-11-29 15:34:18 +0000
+++ lib/lp/blueprints/tests/test_specification.py	2012-12-06 17:11:22 +0000
@@ -742,3 +742,52 @@
         self.assertIn(
             blueprint1,
             list(context.specifications(user=grant.grantee)))
+
+    def run_test_setting_special_role_subscribes(self, role_name):
+        # If a user becomes the assignee, drafter or approver of a
+        # proprietary specification, they are automatically subscribed,
+        # if they do not have yet been granted access to the specification.
+        specification_sharing_policy = (
+                SpecificationSharingPolicy.PROPRIETARY_OR_PUBLIC)
+        product = self.factory.makeProduct(
+            specification_sharing_policy=specification_sharing_policy)
+        blueprint = self.makeSpec(
+            product=product, information_type=InformationType.PROPRIETARY)
+        person_with_new_role = self.factory.makePerson()
+        with person_logged_in(product.owner):
+            setattr(blueprint, role_name, person_with_new_role)
+            self.assertIsNot(
+                None, blueprint.subscription(person_with_new_role))
+
+        # Assignees/drafters/approvers are not subscribed if they already
+        # have a policy grant for the specification's target.
+        blueprint_2 = self.makeSpec(
+            product=product, information_type=InformationType.PROPRIETARY)
+        person_with_new_role_2 = self.factory.makePerson()
+        with person_logged_in(product.owner):
+            permissions = {
+                InformationType.PROPRIETARY: SharingPermission.ALL,
+                }
+            getUtility(IService, 'sharing').sharePillarInformation(
+                product, person_with_new_role_2, product.owner, permissions)
+            setattr(blueprint_2, role_name, person_with_new_role_2)
+            self.assertIs(
+                None, blueprint.subscription(person_with_new_role_2))
+
+    def test_setting_assignee_subscribes(self):
+        # If a user becomes the assignee of a proprietary specification,
+        # they are automatically subscribed, if they do not have yet
+        # been granted access to the specification.
+        self.run_test_setting_special_role_subscribes('assignee')
+
+    def test_setting_drafter_subscribes(self):
+        # If a user becomes the drafter of a proprietary specification,
+        # they are automatically subscribed, if they do not have yet
+        # been granted access to the specification.
+        self.run_test_setting_special_role_subscribes('drafter')
+
+    def test_setting_approver_subscribes(self):
+        # If a user becomes the approver of a proprietary specification,
+        # they are automatically subscribed, if they do not have yet
+        # been granted access to the specification.
+        self.run_test_setting_special_role_subscribes('approver')

=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py	2012-10-03 08:41:47 +0000
+++ lib/lp/registry/model/distroseries.py	2012-12-06 17:11:22 +0000
@@ -888,7 +888,7 @@
 
         results = Specification.select(query, orderBy=order, limit=quantity)
         if prejoin_people:
-            results = results.prejoin(['assignee', 'approver', 'drafter'])
+            results = results.prejoin(['_assignee', '_approver', '_drafter'])
         return results
 
     def getDistroSeriesLanguage(self, language):

=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py	2012-11-26 08:33:03 +0000
+++ lib/lp/registry/model/milestone.py	2012-12-06 17:11:22 +0000
@@ -157,7 +157,7 @@
         store = Store.of(self.target)
         origin, clauses = visible_specification_query(user)
         origin.extend([
-            LeftJoin(Person, Specification.assigneeID == Person.id),
+            LeftJoin(Person, Specification._assigneeID == Person.id),
             ])
         milestones = self._milestone_ids_expr(user)
 

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2012-11-28 05:21:33 +0000
+++ lib/lp/registry/model/person.py	2012-12-06 17:11:22 +0000
@@ -871,11 +871,11 @@
         if SpecificationFilter.CREATOR in filter:
             role_clauses.append(Specification.owner == self)
         if SpecificationFilter.ASSIGNEE in filter:
-            role_clauses.append(Specification.assignee == self)
+            role_clauses.append(Specification._assignee == self)
         if SpecificationFilter.DRAFTER in filter:
-            role_clauses.append(Specification.drafter == self)
+            role_clauses.append(Specification._drafter == self)
         if SpecificationFilter.APPROVER in filter:
-            role_clauses.append(Specification.approver == self)
+            role_clauses.append(Specification._approver == self)
         if SpecificationFilter.SUBSCRIBER in filter:
             role_clauses.append(
                 Specification.id.is_in(
@@ -1499,7 +1499,7 @@
             Milestone.dateexpected <= date, Milestone.dateexpected >= today,
             WorkItem.deleted == False,
             OR(WorkItem.assignee_id.is_in(self.participant_ids),
-               Specification.assigneeID.is_in(self.participant_ids))])
+               Specification._assigneeID.is_in(self.participant_ids))])
         result = store.using(*origin).find(WorkItem, *query)
         result.config(distinct=True)
 
@@ -1510,7 +1510,7 @@
             bulk.load_related(Distribution, specs, ['distributionID'])
             assignee_ids = set(
                 [workitem.assignee_id for workitem in workitems]
-                + [spec.assigneeID for spec in specs])
+                + [spec._assigneeID for spec in specs])
             assignee_ids.discard(None)
             bulk.load(Person, assignee_ids, store)
             milestone_ids = set(

=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py	2012-11-30 20:52:15 +0000
+++ lib/lp/registry/model/productseries.py	2012-12-06 17:11:22 +0000
@@ -443,7 +443,7 @@
 
         results = Specification.select(query, orderBy=order, limit=quantity)
         if prejoin_people:
-            results = results.prejoin(['assignee', 'approver', 'drafter'])
+            results = results.prejoin(['_assignee', '_approver', '_drafter'])
         return results
 
     def _customizeSearchParams(self, search_params):

=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py	2012-11-20 20:52:40 +0000
+++ lib/lp/registry/model/projectgroup.py	2012-12-06 17:11:22 +0000
@@ -314,7 +314,7 @@
         results = Specification.select(query, orderBy=order, limit=quantity,
             clauseTables=clause_tables)
         if prejoin_people:
-            results = results.prejoin(['assignee', 'approver', 'drafter'])
+            results = results.prejoin(['_assignee', '_approver', '_drafter'])
         return results
 
     def _customizeSearchParams(self, search_params):


Follow ups