Merge lp:~salgado/launchpad/safe-blueprints-model into lp:launchpad

Proposed by Guilherme Salgado
Status: Merged
Merged at revision: 11976
Proposed branch: lp:~salgado/launchpad/safe-blueprints-model
Merge into: lp:launchpad
Diff against target: 613 lines (+231/-80)
15 files modified
lib/canonical/launchpad/interfaces/launchpad.py (+7/-0)
lib/canonical/launchpad/security.py (+1/-3)
lib/lp/blueprints/browser/specification.py (+11/-32)
lib/lp/blueprints/configure.zcml (+2/-1)
lib/lp/blueprints/doc/specification.txt (+3/-2)
lib/lp/blueprints/errors.py (+19/-0)
lib/lp/blueprints/interfaces/specification.py (+29/-9)
lib/lp/blueprints/model/specification.py (+32/-23)
lib/lp/blueprints/model/sprint.py (+4/-3)
lib/lp/blueprints/tests/test_specification.py (+90/-0)
lib/lp/registry/model/distribution.py (+2/-1)
lib/lp/registry/model/hasdrivers.py (+22/-0)
lib/lp/registry/model/product.py (+2/-1)
lib/lp/registry/model/projectgroup.py (+2/-1)
lib/lp/registry/model/series.py (+5/-4)
To merge this branch: bzr merge lp:~salgado/launchpad/safe-blueprints-model
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Jelmer Vernooij (community) Approve
Review via email: mp+41722@code.launchpad.net

Commit message

[r=jelmer][ui=none][bug=680875] Move some Blueprint code from views to models in preparation for exposing Blueprints on the API.

 - Get rid of propose_goal_with_automatic_approval by merging it with proposeGoal(). This required refactoring the code of the security adapter into a model method that is then used in proposeGoal() and the security adapter

 - Refactor retarget() to take just a target instead of a product or a distribution.

 - Start splitting ISpecification into several interfaces where each will be protected with a different permission.

 - Consolidate validation of blueprint retargeting into a validateMove() method and use that all around.

Description of the change

This branch moves some Blueprint logic from views to models.

We want that before we expose Blueprints over the API so that we don't have to
duplicate the logic into multiple places.

Below is a more detailed list of all the changes:

 - Get rid of propose_goal_with_automatic_approval by merging it with
   proposeGoal(). This required refactoring the code of the security adapter
   into a model method that is then used in proposeGoal() and the security
   adapter

 - Refactor retarget() to take just a target instead of a product or a
   distribution.

 - Start splitting ISpecification into several interfaces where each will be
   protected with a different permission.

 - Consolidate validation of blueprint retargeting into a validateMove()
   method and use that all around.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Great to see this is being cleaned up!

review: Approve
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

One really minor note: it'd be nice to have docstrings for the module and testcase in lib/lp/blueprints/tests/test_specification.py.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for addressing the retarget and permission issues. In this branch. This branch address most of my concerns about https://code.launchpad.net/~james-w/launchpad/expose-blueprints/+merge/30026

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/interfaces/launchpad.py'
--- lib/canonical/launchpad/interfaces/launchpad.py 2010-10-03 15:30:06 +0000
+++ lib/canonical/launchpad/interfaces/launchpad.py 2010-11-24 18:10:40 +0000
@@ -414,6 +414,13 @@
414 """414 """
415 drivers = Attribute("A list of drivers")415 drivers = Attribute("A list of drivers")
416416
417 def personHasDriverRights(person):
418 """Does the given person have launchpad.Driver rights on this object?
419
420 True if the person is one of this object's drivers, its owner or a
421 Launchpad admin.
422 """
423
417424
418class IHasAppointedDriver(Interface):425class IHasAppointedDriver(Interface):
419 """An object that has an appointed driver."""426 """An object that has an appointed driver."""
420427
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2010-11-12 23:30:57 +0000
+++ lib/canonical/launchpad/security.py 2010-11-24 18:10:40 +0000
@@ -998,9 +998,7 @@
998 usedfor = IHasDrivers998 usedfor = IHasDrivers
999999
1000 def checkAuthenticated(self, user):1000 def checkAuthenticated(self, user):
1001 return (user.isOneOfDrivers(self.obj) or1001 return self.obj.personHasDriverRights(user)
1002 user.isOwner(self.obj) or
1003 user.in_admin)
10041002
10051003
1006class ViewProductSeries(AnonymousAuthorization):1004class ViewProductSeries(AnonymousAuthorization):
10071005
=== modified file 'lib/lp/blueprints/browser/specification.py'
--- lib/lp/blueprints/browser/specification.py 2010-11-23 23:22:27 +0000
+++ lib/lp/blueprints/browser/specification.py 2010-11-24 18:10:40 +0000
@@ -96,6 +96,7 @@
96 ISpecification,96 ISpecification,
97 ISpecificationSet,97 ISpecificationSet,
98 )98 )
99from lp.blueprints.errors import TargetAlreadyHasSpecification
99from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch100from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
100from lp.blueprints.interfaces.sprintspecification import ISprintSpecification101from lp.blueprints.interfaces.sprintspecification import ISprintSpecification
101from lp.code.interfaces.branchnamespace import IBranchNamespaceSet102from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
@@ -129,7 +130,7 @@
129 # Propose the specification as a series goal, if specified.130 # Propose the specification as a series goal, if specified.
130 series = data.get('series')131 series = data.get('series')
131 if series is not None:132 if series is not None:
132 propose_goal_with_automatic_approval(spec, series, self.user)133 spec.proposeGoal(series, self.user)
133 # Propose the specification as a sprint topic, if specified.134 # Propose the specification as a sprint topic, if specified.
134 sprint = data.get('sprint')135 sprint = data.get('sprint')
135 if sprint is not None:136 if sprint is not None:
@@ -603,8 +604,7 @@
603 @action('Continue', name='continue')604 @action('Continue', name='continue')
604 def continue_action(self, action, data):605 def continue_action(self, action, data):
605 self.context.whiteboard = data['whiteboard']606 self.context.whiteboard = data['whiteboard']
606 propose_goal_with_automatic_approval(607 self.context.proposeGoal(data['distroseries'], self.user)
607 self.context, data['distroseries'], self.user)
608 self.next_url = canonical_url(self.context)608 self.next_url = canonical_url(self.context)
609609
610 @property610 @property
@@ -619,8 +619,7 @@
619 @action('Continue', name='continue')619 @action('Continue', name='continue')
620 def continue_action(self, action, data):620 def continue_action(self, action, data):
621 self.context.whiteboard = data['whiteboard']621 self.context.whiteboard = data['whiteboard']
622 propose_goal_with_automatic_approval(622 self.context.proposeGoal(data['productseries'], self.user)
623 self.context, data['productseries'], self.user)
624 self.next_url = canonical_url(self.context)623 self.next_url = canonical_url(self.context)
625624
626 @property625 @property
@@ -628,16 +627,6 @@
628 return canonical_url(self.context)627 return canonical_url(self.context)
629628
630629
631def propose_goal_with_automatic_approval(specification, series, user):
632 """Proposes the given specification as a goal for the given series. If
633 the given user has permission, the proposal is approved automatically.
634 """
635 specification.proposeGoal(series, user)
636 # If the proposer has permission, approve the goal automatically.
637 if series is not None and check_permission('launchpad.Driver', series):
638 specification.acceptBy(user)
639
640
641class SpecificationGoalDecideView(LaunchpadFormView):630class SpecificationGoalDecideView(LaunchpadFormView):
642 """View used to allow the drivers of a series to accept631 """View used to allow the drivers of a series to accept
643 or decline the spec as a goal for that series. Typically they would use632 or decline the spec as a goal for that series. Typically they would use
@@ -688,29 +677,17 @@
688 self.request.form.get("field.target"))677 self.request.form.get("field.target"))
689 return678 return
690679
691 if target.getSpecification(self.context.name) is not None:680 try:
681 self.context.validateMove(target)
682 except TargetAlreadyHasSpecification:
692 self.setFieldError('target',683 self.setFieldError('target',
693 'There is already a blueprint with this name for %s. '684 'There is already a blueprint with this name for %s. '
694 'Please change the name of this blueprint and try again.' %685 'Please change the name of this blueprint and try again.' %
695 target.displayname)686 target.displayname)
696 return
697687
698 @action(_('Retarget Blueprint'), name='retarget')688 @action(_('Retarget Blueprint'), name='retarget')
699 def register_action(self, action, data):689 def retarget_action(self, action, data):
700 # we need to ensure that there is not already a spec with this name690 self.context.retarget(data['target'])
701 # for this new target
702 target = data['target']
703 if target.getSpecification(self.context.name) is not None:
704 return '%s already has a blueprint called %s' % (
705 target.displayname, self.context.name)
706 product = distribution = None
707 if IProduct.providedBy(target):
708 product = target
709 elif IDistribution.providedBy(target):
710 distribution = target
711 else:
712 raise AssertionError('Unknown target.')
713 self.context.retarget(product=product, distribution=distribution)
714 self._nextURL = canonical_url(self.context)691 self._nextURL = canonical_url(self.context)
715692
716 @property693 @property
@@ -775,6 +752,8 @@
775 SUPERSEDED = SpecificationDefinitionStatus.SUPERSEDED752 SUPERSEDED = SpecificationDefinitionStatus.SUPERSEDED
776 NEW = SpecificationDefinitionStatus.NEW753 NEW = SpecificationDefinitionStatus.NEW
777 self.context.superseded_by = data['superseded_by']754 self.context.superseded_by = data['superseded_by']
755 # XXX: salgado, 2010-11-24, bug=680880: This logic should be in model
756 # code.
778 if data['superseded_by'] is not None:757 if data['superseded_by'] is not None:
779 # set the state to superseded758 # set the state to superseded
780 self.context.definition_status = SUPERSEDED759 self.context.definition_status = SUPERSEDED
781760
=== modified file 'lib/lp/blueprints/configure.zcml'
--- lib/lp/blueprints/configure.zcml 2010-11-09 14:35:44 +0000
+++ lib/lp/blueprints/configure.zcml 2010-11-24 18:10:40 +0000
@@ -152,7 +152,7 @@
152 <!-- Specification -->152 <!-- Specification -->
153153
154 <class class="lp.blueprints.model.specification.Specification">154 <class class="lp.blueprints.model.specification.Specification">
155 <allow interface="lp.blueprints.interfaces.specification.ISpecification"/>155 <allow interface="lp.blueprints.interfaces.specification.ISpecificationPublic"/>
156 <!-- We allow any authenticated person to update the whiteboard -->156 <!-- We allow any authenticated person to update the whiteboard -->
157 <require157 <require
158 permission="launchpad.AnyPerson"158 permission="launchpad.AnyPerson"
@@ -162,6 +162,7 @@
162 methods -->162 methods -->
163 <require163 <require
164 permission="launchpad.Edit"164 permission="launchpad.Edit"
165 interface="lp.blueprints.interfaces.specification.ISpecificationEditRestricted"
165 set_attributes="name title summary definition_status specurl166 set_attributes="name title summary definition_status specurl
166 superseded_by milestone product distribution167 superseded_by milestone product distribution
167 approver assignee drafter man_days168 approver assignee drafter man_days
168169
=== modified file 'lib/lp/blueprints/doc/specification.txt'
--- lib/lp/blueprints/doc/specification.txt 2010-11-01 03:32:29 +0000
+++ lib/lp/blueprints/doc/specification.txt 2010-11-24 18:10:40 +0000
@@ -435,10 +435,11 @@
435 'Declined'435 'Declined'
436436
437And finally, if we propose a new goal, then the decision status is437And finally, if we propose a new goal, then the decision status is
438invalidated.438invalidated. (Notice that we propose the goal as jdub as goals proposed by one
439of their drivers [e.g. mark] would be automatically accepted)
439440
440 >>> trunk = upstream_firefox.getSeries('trunk')441 >>> trunk = upstream_firefox.getSeries('trunk')
441 >>> e4x.proposeGoal(trunk, mark)442 >>> e4x.proposeGoal(trunk, jdub)
442 >>> e4x.goalstatus.title443 >>> e4x.goalstatus.title
443 'Proposed'444 'Proposed'
444445
445446
=== added file 'lib/lp/blueprints/errors.py'
--- lib/lp/blueprints/errors.py 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/errors.py 2010-11-24 18:10:40 +0000
@@ -0,0 +1,19 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Specification views."""
5
6__metaclass__ = type
7
8__all__ = [
9 'TargetAlreadyHasSpecification',
10 ]
11
12
13class TargetAlreadyHasSpecification(Exception):
14 """The ISpecificationTarget already has a specification of that name."""
15
16 def __init__(self, target, name):
17 msg = "The target %s already has a specification named %s" % (
18 target, name)
19 super(TargetAlreadyHasSpecification, self).__init__(msg)
020
=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py 2010-11-04 03:34:54 +0000
+++ lib/lp/blueprints/interfaces/specification.py 2010-11-24 18:10:40 +0000
@@ -207,11 +207,27 @@
207 required=True, vocabulary='DistributionOrProduct')207 required=True, vocabulary='DistributionOrProduct')
208208
209209
210class ISpecification(INewSpecification, INewSpecificationTarget, IHasOwner,210class ISpecificationEditRestricted(Interface):
211 IHasLinkedBranches):211 """Specification's attributes and methods protected with launchpad.Edit.
212 """A Specification."""212 """
213213
214 export_as_webservice_entry()214 def setTarget(target):
215 """Set this specification's target.
216
217 :param target: an IProduct or IDistribution.
218 """
219
220 def retarget(target):
221 """Move the spec to the given target.
222
223 The new target must be an IProduct or IDistribution.
224 """
225
226
227class ISpecificationPublic(
228 INewSpecification, INewSpecificationTarget, IHasOwner,
229 IHasLinkedBranches):
230 """Specification's public attributes and methods."""
215231
216 # TomBerger 2007-06-20: 'id' is required for232 # TomBerger 2007-06-20: 'id' is required for
217 # SQLObject to be able to assign a security-proxied233 # SQLObject to be able to assign a security-proxied
@@ -344,10 +360,8 @@
344 default=SpecificationLifecycleStatus.NOTSTARTED,360 default=SpecificationLifecycleStatus.NOTSTARTED,
345 readonly=True)361 readonly=True)
346362
347 def retarget(product=None, distribution=None):363 def validateMove(target):
348 """Retarget the spec to a new product or distribution. One of364 """Check that the specification can be moved to the target."""
349 product or distribution must be None (but not both).
350 """
351365
352 def getSprintSpecification(sprintname):366 def getSprintSpecification(sprintname):
353 """Get the record that links this spec to the named sprint."""367 """Get the record that links this spec to the named sprint."""
@@ -450,6 +464,12 @@
450 """Return the SpecificationBranch link for the branch, or None."""464 """Return the SpecificationBranch link for the branch, or None."""
451465
452466
467class ISpecification(ISpecificationPublic, ISpecificationEditRestricted):
468 """A Specification."""
469
470 export_as_webservice_entry()
471
472
453class ISpecificationSet(IHasSpecifications):473class ISpecificationSet(IHasSpecifications):
454 """A container for specifications."""474 """A container for specifications."""
455475
456476
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2010-11-04 03:58:05 +0000
+++ lib/lp/blueprints/model/specification.py 2010-11-24 18:10:40 +0000
@@ -60,6 +60,7 @@
60 SpecificationPriority,60 SpecificationPriority,
61 SpecificationSort,61 SpecificationSort,
62 )62 )
63from lp.blueprints.errors import TargetAlreadyHasSpecification
63from lp.blueprints.interfaces.specification import (64from lp.blueprints.interfaces.specification import (
64 ISpecification,65 ISpecification,
65 ISpecificationSet,66 ISpecificationSet,
@@ -77,9 +78,11 @@
77from lp.blueprints.model.sprintspecification import SprintSpecification78from lp.blueprints.model.sprintspecification import SprintSpecification
78from lp.bugs.interfaces.buglink import IBugLinkTarget79from lp.bugs.interfaces.buglink import IBugLinkTarget
79from lp.bugs.model.buglinktarget import BugLinkTargetMixin80from lp.bugs.model.buglinktarget import BugLinkTargetMixin
81from lp.registry.interfaces.distribution import IDistribution
80from lp.registry.interfaces.distroseries import IDistroSeries82from lp.registry.interfaces.distroseries import IDistroSeries
81from lp.registry.interfaces.person import validate_public_person83from lp.registry.interfaces.person import validate_public_person
82from lp.registry.interfaces.productseries import IProductSeries84from lp.registry.interfaces.productseries import IProductSeries
85from lp.registry.interfaces.product import IProduct
8386
8487
85class Specification(SQLBase, BugLinkTargetMixin):88class Specification(SQLBase, BugLinkTargetMixin):
@@ -182,7 +185,6 @@
182 otherColumn='specification', orderBy='title',185 otherColumn='specification', orderBy='title',
183 intermediateTable='SpecificationDependency')186 intermediateTable='SpecificationDependency')
184187
185 # attributes
186 @property188 @property
187 def target(self):189 def target(self):
188 """See ISpecification."""190 """See ISpecification."""
@@ -190,25 +192,27 @@
190 return self.product192 return self.product
191 return self.distribution193 return self.distribution
192194
193 def retarget(self, product=None, distribution=None):195 def setTarget(self, target):
194 """See ISpecification."""196 """See ISpecification."""
195 assert not (product and distribution)197 if IProduct.providedBy(target):
196 assert (product or distribution)198 self.product = target
197199 self.distribution = None
198 # we need to ensure that there is not already a spec with this name200 elif IDistribution.providedBy(target):
199 # for this new target201 self.product = None
200 if product:202 self.distribution = target
201 assert product.getSpecification(self.name) is None203 else:
202 elif distribution:204 raise AssertionError("Unknown target: %s" % target)
203 assert distribution.getSpecification(self.name) is None205
204206 def retarget(self, target):
205 # if we are not changing anything, then return207 """See ISpecification."""
206 if self.product == product and self.distribution == distribution:208 if self.target == target:
207 return209 return
208210
209 # we must lose any goal we have set and approved/declined because we211 self.validateMove(target)
210 # are moving to a different product that will have different212
211 # policies and drivers213 # We must lose any goal we have set and approved/declined because we
214 # are moving to a different target that will have different
215 # policies and drivers.
212 self.productseries = None216 self.productseries = None
213 self.distroseries = None217 self.distroseries = None
214 self.goalstatus = SpecificationGoalStatus.PROPOSED218 self.goalstatus = SpecificationGoalStatus.PROPOSED
@@ -216,12 +220,15 @@
216 self.date_goal_proposed = None220 self.date_goal_proposed = None
217 self.milestone = None221 self.milestone = None
218222
219 # set the new values223 self.setTarget(target)
220 self.product = product
221 self.distribution = distribution
222 self.priority = SpecificationPriority.UNDEFINED224 self.priority = SpecificationPriority.UNDEFINED
223 self.direction_approved = False225 self.direction_approved = False
224226
227 def validateMove(self, target):
228 """See ISpecification."""
229 if target.getSpecification(self.name) is not None:
230 raise TargetAlreadyHasSpecification(target, self.name)
231
225 @property232 @property
226 def goal(self):233 def goal(self):
227 """See ISpecification."""234 """See ISpecification."""
@@ -259,6 +266,8 @@
259 # the goal should now also not have a decider266 # the goal should now also not have a decider
260 self.goal_decider = None267 self.goal_decider = None
261 self.date_goal_decided = None268 self.date_goal_decided = None
269 if goal is not None and goal.personHasDriverRights(proposer):
270 self.acceptBy(proposer)
262271
263 def acceptBy(self, decider):272 def acceptBy(self, decider):
264 """See ISpecification."""273 """See ISpecification."""
@@ -658,7 +667,7 @@
658667
659 def _specification_sort(self, sort):668 def _specification_sort(self, sort):
660 """Return the storm sort order for 'specifications'.669 """Return the storm sort order for 'specifications'.
661 670
662 :param sort: As per HasSpecificationsMixin.specifications.671 :param sort: As per HasSpecificationsMixin.specifications.
663 """672 """
664 # sort by priority descending, by default673 # sort by priority descending, by default
@@ -671,7 +680,7 @@
671680
672 def _preload_specifications_people(self, query):681 def _preload_specifications_people(self, query):
673 """Perform eager loading of people and their validity for query.682 """Perform eager loading of people and their validity for query.
674 683
675 :param query: a string query generated in the 'specifications'684 :param query: a string query generated in the 'specifications'
676 method.685 method.
677 :return: A DecoratedResultSet with Person precaching setup.686 :return: A DecoratedResultSet with Person precaching setup.
678687
=== modified file 'lib/lp/blueprints/model/sprint.py'
--- lib/lp/blueprints/model/sprint.py 2010-11-01 03:57:52 +0000
+++ lib/lp/blueprints/model/sprint.py 2010-11-24 18:10:40 +0000
@@ -45,9 +45,10 @@
45from lp.blueprints.model.sprintattendance import SprintAttendance45from lp.blueprints.model.sprintattendance import SprintAttendance
46from lp.blueprints.model.sprintspecification import SprintSpecification46from lp.blueprints.model.sprintspecification import SprintSpecification
47from lp.registry.interfaces.person import validate_public_person47from lp.registry.interfaces.person import validate_public_person
4848from lp.registry.model.hasdrivers import HasDriversMixin
4949
50class Sprint(SQLBase):50
51class Sprint(SQLBase, HasDriversMixin):
51 """See `ISprint`."""52 """See `ISprint`."""
5253
53 implements(ISprint, IHasLogo, IHasMugshot, IHasIcon)54 implements(ISprint, IHasLogo, IHasMugshot, IHasIcon)
5455
=== added file 'lib/lp/blueprints/tests/test_specification.py'
--- lib/lp/blueprints/tests/test_specification.py 1970-01-01 00:00:00 +0000
+++ lib/lp/blueprints/tests/test_specification.py 2010-11-24 18:10:40 +0000
@@ -0,0 +1,90 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Unit tests for Specification."""
5
6__metaclass__ = type
7
8
9from zope.security.interfaces import Unauthorized
10from zope.security.proxy import removeSecurityProxy
11
12from canonical.testing.layers import DatabaseFunctionalLayer
13from lp.blueprints.errors import TargetAlreadyHasSpecification
14from lp.blueprints.interfaces.specification import SpecificationGoalStatus
15from lp.testing import TestCaseWithFactory
16
17
18class SpecificationTests(TestCaseWithFactory):
19
20 layer = DatabaseFunctionalLayer
21
22 def test_auto_accept_of_goal_for_drivers(self):
23 """Drivers of a series accept the goal when they propose."""
24 product = self.factory.makeProduct()
25 proposer = self.factory.makePerson()
26 productseries = self.factory.makeProductSeries(product=product)
27 removeSecurityProxy(productseries).driver = proposer
28 specification = self.factory.makeSpecification(product=product)
29 specification.proposeGoal(productseries, proposer)
30 self.assertEqual(
31 SpecificationGoalStatus.ACCEPTED, specification.goalstatus)
32
33 def test_goal_not_accepted_for_non_drivers(self):
34 """People who aren't drivers don't have their proposals approved."""
35 product = self.factory.makeProduct()
36 proposer = self.factory.makePerson()
37 productseries = self.factory.makeProductSeries(product=product)
38 specification = self.factory.makeSpecification(product=product)
39 specification.proposeGoal(productseries, proposer)
40 self.assertEqual(
41 SpecificationGoalStatus.PROPOSED, specification.goalstatus)
42
43 def test_retarget_existing_specification(self):
44 """An error is raised if the name is already taken."""
45 product1 = self.factory.makeProduct()
46 product2 = self.factory.makeProduct()
47 specification1 = self.factory.makeSpecification(
48 product=product1, name="foo")
49 specification2 = self.factory.makeSpecification(
50 product=product2, name="foo")
51 self.assertRaises(
52 TargetAlreadyHasSpecification,
53 removeSecurityProxy(specification1).retarget, product2)
54
55 def test_retarget_is_protected(self):
56 specification = self.factory.makeSpecification(
57 product=self.factory.makeProduct())
58 self.assertRaises(
59 Unauthorized, getattr, specification, 'retarget')
60
61 def test_validate_move_existing_specification(self):
62 """An error is raised by validateMove if the name is already taken."""
63 product1 = self.factory.makeProduct()
64 product2 = self.factory.makeProduct()
65 specification1 = self.factory.makeSpecification(
66 product=product1, name="foo")
67 specification2 = self.factory.makeSpecification(
68 product=product2, name="foo")
69 self.assertRaises(
70 TargetAlreadyHasSpecification, specification1.validateMove,
71 product2)
72
73 def test_setTarget(self):
74 product = self.factory.makeProduct()
75 specification = self.factory.makeSpecification(product=product)
76 self.assertEqual(product, specification.target)
77 self.assertIs(None, specification.distribution)
78
79 distribution = self.factory.makeDistribution()
80 removeSecurityProxy(specification).setTarget(distribution)
81
82 self.assertEqual(distribution, specification.target)
83 self.assertEqual(distribution, specification.distribution)
84 self.assertIs(None, specification.product)
85
86 def test_setTarget_is_protected(self):
87 specification = self.factory.makeSpecification(
88 product=self.factory.makeProduct())
89 self.assertRaises(
90 Unauthorized, getattr, specification, 'setTarget')
091
=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py 2010-11-12 23:30:57 +0000
+++ lib/lp/registry/model/distribution.py 2010-11-24 18:10:40 +0000
@@ -146,6 +146,7 @@
146 Milestone,146 Milestone,
147 )147 )
148from lp.registry.model.pillar import HasAliasMixin148from lp.registry.model.pillar import HasAliasMixin
149from lp.registry.model.hasdrivers import HasDriversMixin
149from lp.registry.model.sourcepackagename import SourcePackageName150from lp.registry.model.sourcepackagename import SourcePackageName
150from lp.registry.model.structuralsubscription import (151from lp.registry.model.structuralsubscription import (
151 StructuralSubscriptionTargetMixin,152 StructuralSubscriptionTargetMixin,
@@ -201,7 +202,7 @@
201 HasTranslationImportsMixin, KarmaContextMixin,202 HasTranslationImportsMixin, KarmaContextMixin,
202 OfficialBugTagTargetMixin, QuestionTargetMixin,203 OfficialBugTagTargetMixin, QuestionTargetMixin,
203 StructuralSubscriptionTargetMixin, HasMilestonesMixin,204 StructuralSubscriptionTargetMixin, HasMilestonesMixin,
204 HasBugHeatMixin):205 HasBugHeatMixin, HasDriversMixin):
205 """A distribution of an operating system, e.g. Debian GNU/Linux."""206 """A distribution of an operating system, e.g. Debian GNU/Linux."""
206 implements(207 implements(
207 IDistribution, IFAQTarget, IHasBugHeat, IHasBugSupervisor,208 IDistribution, IFAQTarget, IHasBugHeat, IHasBugSupervisor,
208209
=== added file 'lib/lp/registry/model/hasdrivers.py'
--- lib/lp/registry/model/hasdrivers.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/model/hasdrivers.py 2010-11-24 18:10:40 +0000
@@ -0,0 +1,22 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Common implementations for IHasDrivers."""
5
6__metaclass__ = type
7
8__all__ = [
9 'HasDriversMixin',
10 ]
11
12from canonical.launchpad.interfaces.launchpad import IPersonRoles
13
14
15class HasDriversMixin:
16
17 def personHasDriverRights(self, person):
18 """See `IHasDrivers`."""
19 person_roles = IPersonRoles(person)
20 return (person_roles.isOneOfDrivers(self) or
21 person_roles.isOwner(self) or
22 person_roles.in_admin)
023
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2010-11-19 17:27:35 +0000
+++ lib/lp/registry/model/product.py 2010-11-24 18:10:40 +0000
@@ -156,6 +156,7 @@
156from lp.registry.model.productlicense import ProductLicense156from lp.registry.model.productlicense import ProductLicense
157from lp.registry.model.productrelease import ProductRelease157from lp.registry.model.productrelease import ProductRelease
158from lp.registry.model.productseries import ProductSeries158from lp.registry.model.productseries import ProductSeries
159from lp.registry.model.hasdrivers import HasDriversMixin
159from lp.registry.model.sourcepackagename import SourcePackageName160from lp.registry.model.sourcepackagename import SourcePackageName
160from lp.registry.model.structuralsubscription import (161from lp.registry.model.structuralsubscription import (
161 StructuralSubscriptionTargetMixin,162 StructuralSubscriptionTargetMixin,
@@ -286,7 +287,7 @@
286287
287288
288class Product(SQLBase, BugTargetBase, MakesAnnouncements,289class Product(SQLBase, BugTargetBase, MakesAnnouncements,
289 HasSpecificationsMixin, HasSprintsMixin,290 HasDriversMixin, HasSpecificationsMixin, HasSprintsMixin,
290 KarmaContextMixin, BranchVisibilityPolicyMixin,291 KarmaContextMixin, BranchVisibilityPolicyMixin,
291 QuestionTargetMixin, HasTranslationImportsMixin,292 QuestionTargetMixin, HasTranslationImportsMixin,
292 HasAliasMixin, StructuralSubscriptionTargetMixin,293 HasAliasMixin, StructuralSubscriptionTargetMixin,
293294
=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py 2010-11-09 08:43:34 +0000
+++ lib/lp/registry/model/projectgroup.py 2010-11-24 18:10:40 +0000
@@ -99,6 +99,7 @@
99from lp.registry.model.pillar import HasAliasMixin99from lp.registry.model.pillar import HasAliasMixin
100from lp.registry.model.product import Product100from lp.registry.model.product import Product
101from lp.registry.model.productseries import ProductSeries101from lp.registry.model.productseries import ProductSeries
102from lp.registry.model.hasdrivers import HasDriversMixin
102from lp.registry.model.structuralsubscription import (103from lp.registry.model.structuralsubscription import (
103 StructuralSubscriptionTargetMixin,104 StructuralSubscriptionTargetMixin,
104 )105 )
@@ -111,7 +112,7 @@
111 KarmaContextMixin, BranchVisibilityPolicyMixin,112 KarmaContextMixin, BranchVisibilityPolicyMixin,
112 StructuralSubscriptionTargetMixin,113 StructuralSubscriptionTargetMixin,
113 HasBranchesMixin, HasMergeProposalsMixin, HasBugHeatMixin,114 HasBranchesMixin, HasMergeProposalsMixin, HasBugHeatMixin,
114 HasMilestonesMixin):115 HasMilestonesMixin, HasDriversMixin):
115 """A ProjectGroup"""116 """A ProjectGroup"""
116117
117 implements(IProjectGroup, IFAQCollection, IHasBugHeat, IHasIcon, IHasLogo,118 implements(IProjectGroup, IFAQCollection, IHasBugHeat, IHasIcon, IHasLogo,
118119
=== modified file 'lib/lp/registry/model/series.py'
--- lib/lp/registry/model/series.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/series.py 2010-11-24 18:10:40 +0000
@@ -18,9 +18,10 @@
18 ISeriesMixin,18 ISeriesMixin,
19 SeriesStatus,19 SeriesStatus,
20 )20 )
2121from lp.registry.model.hasdrivers import HasDriversMixin
2222
23class SeriesMixin:23
24class SeriesMixin(HasDriversMixin):
24 """See `ISeriesMixin`."""25 """See `ISeriesMixin`."""
2526
26 implements(ISeriesMixin)27 implements(ISeriesMixin)
@@ -48,7 +49,7 @@
4849
49 @property50 @property
50 def drivers(self):51 def drivers(self):
51 """See `ISeriesMixin`."""52 """See `IHasDrivers`."""
52 drivers = set()53 drivers = set()
53 drivers.add(self.driver)54 drivers.add(self.driver)
54 drivers = drivers.union(self.parent.drivers)55 drivers = drivers.union(self.parent.drivers)