Merge lp:~rockstar/launchpad/sourcepackagerecipe-name-contstraint into lp:launchpad

Proposed by Paul Hummer
Status: Rejected
Rejected by: Paul Hummer
Proposed branch: lp:~rockstar/launchpad/sourcepackagerecipe-name-contstraint
Merge into: lp:launchpad
Diff against target: 176 lines (+79/-7)
4 files modified
lib/lp/code/errors.py (+5/-0)
lib/lp/code/model/sourcepackagerecipe.py (+36/-4)
lib/lp/code/model/tests/test_sourcepackagerecipe.py (+35/-0)
lib/lp/registry/model/person.py (+3/-3)
To merge this branch: bzr merge lp:~rockstar/launchpad/sourcepackagerecipe-name-contstraint
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Disapprove
Review via email: mp+26122@code.launchpad.net

Description of the change

This branch ensures that a person cannot own two recipes owned by the same person. There's a db patch for this as well, but Aaron and I have decided that we also need to enforce this in the model. The followup to this branch is making sure this is dealt with in the UI, but we need to get this branch landed soon to make sure edge is prevented from having this problem.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) wrote :

UI code should ensure that invalid values are never passed through, as that will generate an OOPS.

Database constraints exist to catch things that slip through and generate the OOPS.

Guards at this level are unnecessary and wasteful, just duplicating the functionality of the db constraint.

review: Disapprove
Revision history for this message
Robert Collins (lifeless) wrote :

On Thu, May 27, 2010 at 6:24 PM, Stuart Bishop
<email address hidden> wrote:
> Review: Disapprove
> UI code should ensure that invalid values are never passed through, as that will generate an OOPS.
>
> Database constraints exist to catch things that slip through and generate the OOPS.
>
> Guards at this level are unnecessary and wasteful, just duplicating the functionality of the db constraint.

What level do LP APIs operate at? I thought they spoke directly to the
model code, and we certainly don't want mistaken LP API calls to
generate a server side OOPS due to DB constraint violation.

Revision history for this message
Stuart Bishop (stub) wrote :

On Thu, May 27, 2010 at 2:54 PM, Robert Collins
<email address hidden> wrote:
> On Thu, May 27, 2010 at 6:24 PM, Stuart Bishop
> <email address hidden> wrote:
>> Review: Disapprove
>> UI code should ensure that invalid values are never passed through, as that will generate an OOPS.
>>
>> Database constraints exist to catch things that slip through and generate the OOPS.
>>
>> Guards at this level are unnecessary and wasteful, just duplicating the functionality of the db constraint.
>
> What level do LP APIs operate at? I thought they spoke directly to the
> model code, and we certainly don't want mistaken LP API calls to
> generate a server side OOPS due to DB constraint violation.

That might be a point. I'm not sure. How do we currently handle all
the other unique fields?

--
Stuart Bishop <email address hidden>
http://www.stuartbishop.net/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/errors.py'
--- lib/lp/code/errors.py 2010-04-06 20:38:34 +0000
+++ lib/lp/code/errors.py 2010-05-27 05:25:46 +0000
@@ -14,6 +14,7 @@
14 'ClaimReviewFailed',14 'ClaimReviewFailed',
15 'InvalidBranchMergeProposal',15 'InvalidBranchMergeProposal',
16 'ReviewNotPending',16 'ReviewNotPending',
17 'SourcePackageRecipeExists',
17 'UnknownBranchTypeError',18 'UnknownBranchTypeError',
18 'UserHasExistingReview',19 'UserHasExistingReview',
19 'UserNotBranchReviewer',20 'UserNotBranchReviewer',
@@ -91,3 +92,7 @@
91 """Raised when the user requests an import that is already running."""92 """Raised when the user requests an import that is already running."""
9293
93 webservice_error(400)94 webservice_error(400)
95
96
97class SourcePackageRecipeExists(Exception):
98 """Raised when a source package recipe is isn't unique on owner/name."""
9499
=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py 2010-05-26 19:23:20 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py 2010-05-27 05:25:46 +0000
@@ -22,6 +22,7 @@
22from canonical.database.datetimecol import UtcDateTimeCol22from canonical.database.datetimecol import UtcDateTimeCol
23from canonical.launchpad.interfaces.lpstorm import IMasterStore, IStore23from canonical.launchpad.interfaces.lpstorm import IMasterStore, IStore
2424
25from lp.code.errors import SourcePackageRecipeExists
25from lp.code.interfaces.sourcepackagerecipe import (26from lp.code.interfaces.sourcepackagerecipe import (
26 ISourcePackageRecipe, ISourcePackageRecipeSource,27 ISourcePackageRecipe, ISourcePackageRecipeSource,
27 ISourcePackageRecipeData)28 ISourcePackageRecipeData)
@@ -68,7 +69,13 @@
68 date_last_modified = UtcDateTimeCol(notNull=True)69 date_last_modified = UtcDateTimeCol(notNull=True)
6970
70 owner_id = Int(name='owner', allow_none=True)71 owner_id = Int(name='owner', allow_none=True)
71 owner = Reference(owner_id, 'Person.id')72 _owner = Reference(owner_id, 'Person.id')
73 def set_owner(self, value):
74 self.ensureUnique(owner=value)
75 self._owner = value
76 def get_owner(self):
77 return self._owner
78 owner = property(get_owner, set_owner)
7279
73 registrant_id = Int(name='registrant', allow_none=True)80 registrant_id = Int(name='registrant', allow_none=True)
74 registrant = Reference(registrant_id, 'Person.id')81 registrant = Reference(registrant_id, 'Person.id')
@@ -87,7 +94,14 @@
87 def _sourcepackagename_text(self):94 def _sourcepackagename_text(self):
88 return self.sourcepackagename.name95 return self.sourcepackagename.name
8996
90 name = Unicode(allow_none=True)97 _name = Unicode('name', allow_none=True)
98 def set_name(self, value):
99 self.ensureUnique(name=value)
100 self._name = value
101 def get_name(self):
102 return self._name
103 name = property(get_name, set_name)
104
91 description = Unicode(allow_none=False)105 description = Unicode(allow_none=False)
92106
93 @property107 @property
@@ -122,12 +136,15 @@
122 builder_recipe, description):136 builder_recipe, description):
123 """See `ISourcePackageRecipeSource.new`."""137 """See `ISourcePackageRecipeSource.new`."""
124 store = IMasterStore(SourcePackageRecipe)138 store = IMasterStore(SourcePackageRecipe)
139
125 sprecipe = SourcePackageRecipe()140 sprecipe = SourcePackageRecipe()
141 sprecipe.ensureUnique(owner=owner, name=name)
142
126 SourcePackageRecipeData(builder_recipe, sprecipe)143 SourcePackageRecipeData(builder_recipe, sprecipe)
127 sprecipe.registrant = registrant144 sprecipe.registrant = registrant
128 sprecipe.owner = owner145 sprecipe._owner = owner
129 sprecipe.sourcepackagename = sourcepackagename146 sprecipe.sourcepackagename = sourcepackagename
130 sprecipe.name = name147 sprecipe._name = name
131 for distroseries_item in distroseries:148 for distroseries_item in distroseries:
132 sprecipe.distroseries.add(distroseries_item)149 sprecipe.distroseries.add(distroseries_item)
133 sprecipe.description = description150 sprecipe.description = description
@@ -197,3 +214,18 @@
197 if count == 0:214 if count == 0:
198 return None215 return None
199 return result[count/2]216 return result[count/2]
217
218 def ensureUnique(self, owner=None, name=None):
219 """Raise an exception if the recipe will no longer be unique."""
220 if not owner:
221 owner = self._owner
222 if not name:
223 name = self._name
224
225 store = IMasterStore(SourcePackageRecipe)
226 recipe = store.find(
227 SourcePackageRecipe,
228 SourcePackageRecipe._owner == owner,
229 SourcePackageRecipe._name == name).one()
230 if recipe:
231 raise SourcePackageRecipeExists
200232
=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-05-18 19:14:16 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py 2010-05-27 05:25:46 +0000
@@ -29,6 +29,7 @@
29 InvalidPocketForPPA)29 InvalidPocketForPPA)
30from lp.buildmaster.interfaces.buildqueue import IBuildQueue30from lp.buildmaster.interfaces.buildqueue import IBuildQueue
31from lp.buildmaster.model.buildqueue import BuildQueue31from lp.buildmaster.model.buildqueue import BuildQueue
32from lp.code.errors import SourcePackageRecipeExists
32from lp.code.interfaces.sourcepackagerecipe import (33from lp.code.interfaces.sourcepackagerecipe import (
33 ForbiddenInstruction, ISourcePackageRecipe, ISourcePackageRecipeSource,34 ForbiddenInstruction, ISourcePackageRecipe, ISourcePackageRecipeSource,
34 TooNewRecipeFormat, MINIMAL_RECIPE_TEXT)35 TooNewRecipeFormat, MINIMAL_RECIPE_TEXT)
@@ -85,6 +86,40 @@
85 (recipe.registrant, recipe.owner, set(recipe.distroseries),86 (recipe.registrant, recipe.owner, set(recipe.distroseries),
86 recipe.sourcepackagename, recipe.name))87 recipe.sourcepackagename, recipe.name))
8788
89 def test_creation_dupe(self):
90 # The metadata supplied when a SourcePackageRecipe is created is
91 # present on the new object.
92 recipe = self.factory.makeSourcePackageRecipe()
93
94 self.assertRaises(SourcePackageRecipeExists,
95 getUtility(ISourcePackageRecipeSource).new,
96 recipe.registrant, recipe.owner, [], recipe.sourcepackagename,
97 recipe.name, self.factory.makeRecipe(), recipe.description)
98
99 def test_edit_name_dupe(self):
100 recipe = self.factory.makeSourcePackageRecipe()
101 recipe_dupe = self.factory.makeSourcePackageRecipe(owner=recipe.owner)
102
103 try:
104 with person_logged_in(recipe_dupe.owner):
105 recipe_dupe.name = recipe.name
106 self.fail(
107 'Duplicate name does not raise SourcePackageRecipeExists')
108 except SourcePackageRecipeExists:
109 pass
110
111 def test_edit_owner_dupe(self):
112 recipe = self.factory.makeSourcePackageRecipe()
113 recipe_dupe = self.factory.makeSourcePackageRecipe(name=recipe.name)
114
115 try:
116 with person_logged_in(recipe_dupe.owner):
117 recipe_dupe.owner = recipe.owner
118 self.fail(
119 'Duplicate owner does not raise SourcePackageRecipeExists')
120 except SourcePackageRecipeExists:
121 pass
122
88 def test_source_implements_interface(self):123 def test_source_implements_interface(self):
89 # The SourcePackageRecipe class implements ISourcePackageRecipeSource.124 # The SourcePackageRecipe class implements ISourcePackageRecipeSource.
90 self.assertProvides(125 self.assertProvides(
91126
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-05-10 13:52:14 +0000
+++ lib/lp/registry/model/person.py 2010-05-27 05:25:46 +0000
@@ -2276,8 +2276,8 @@
2276 def getRecipe(self, name):2276 def getRecipe(self, name):
2277 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe2277 from lp.code.model.sourcepackagerecipe import SourcePackageRecipe
2278 return Store.of(self).find(2278 return Store.of(self).find(
2279 SourcePackageRecipe, SourcePackageRecipe.owner == self,2279 SourcePackageRecipe, SourcePackageRecipe._owner == self,
2280 SourcePackageRecipe.name == name).one()2280 SourcePackageRecipe._name == name).one()
22812281
2282 def isUploader(self, distribution):2282 def isUploader(self, distribution):
2283 """See `IPerson`."""2283 """See `IPerson`."""
@@ -2395,7 +2395,7 @@
2395 store = Store.of(self)2395 store = Store.of(self)
2396 return store.find(2396 return store.find(
2397 SourcePackageRecipe,2397 SourcePackageRecipe,
2398 SourcePackageRecipe.owner == self)2398 SourcePackageRecipe._owner == self)
23992399
24002400
2401class PersonSet:2401class PersonSet: