Merge lp:~sinzui/launchpad/sane-definition-status-0 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 12514
Proposed branch: lp:~sinzui/launchpad/sane-definition-status-0
Merge into: lp:launchpad
Diff against target: 296 lines (+104/-13)
8 files modified
lib/lp/blueprints/enums.py (+18/-0)
lib/lp/blueprints/interfaces/specification.py (+15/-2)
lib/lp/blueprints/model/specification.py (+12/-0)
lib/lp/blueprints/tests/test_hasspecifications.py (+1/-3)
lib/lp/blueprints/tests/test_specification.py (+43/-1)
lib/lp/blueprints/tests/test_webservice.py (+1/-3)
lib/lp/code/model/tests/test_branch.py (+2/-2)
lib/lp/testing/factory.py (+12/-2)
To merge this branch: bzr merge lp:~sinzui/launchpad/sane-definition-status-0
Reviewer Review Type Date Requested Status
Robert Collins (community) Approve
Review via email: mp+51932@code.launchpad.net

Description of the change

Do not allow closed definition status when creating a specification.

    Launchpad bug: https://bugs.launchpad.net/bugs/121608
    Pre-implementation: thumper
    Test command: ./bin/test -vv -m lp.blueprints

DB constraint triggered adding a new blueprint with status obsolete or
superseded: IntegrityError ERROR: new row for relation "specification"
violates check constraint "specification_completion_recorded_chk".

You cannot create a obsolete or superseded specification because the
form does not capture the all the status and people to actually set the
specification to a closed statues. Users should not be creating closed
specifications. This issue can also happen over API.

--------------------------------------------------------------------

RULES

    * Create an enum that does not contain obsolete or superseded
      definition status.
    * Update INewSpecification to use it.
    * ADDENDUM: copy the original definition_status to ISpecification to
      preserve the rules for life cycle events.
    * Add an adaption block to new() to convert the new enum to the life
      cycle enum.
      * This is because The two enum items are not equivalent, not through
        inheritance or via use_template.
      * As with branches, use the name of the item to adapt.
      * I discussed this design issue with Tim. We may solve this issue in
        lazr.enum and Lp's storm enumcol, but that is a separate issue.

QA

    * Visit https://blueprints.qastaging.launchpad.net/gdp/+addspec
    * Verify that obsolete or superseded are not in the definition
      status field.
    * Create a blueprint with any status.
    * Change it's definition status to obsolete

LINT

    lib/lp/blueprints/enums.py
    lib/lp/blueprints/interfaces/specification.py
    lib/lp/blueprints/model/specification.py
    lib/lp/blueprints/tests/test_hasspecifications.py
    lib/lp/blueprints/tests/test_specification.py
    lib/lp/blueprints/tests/test_webservice.py
    lib/lp/code/model/tests/test_branch.py
    lib/lp/testing/factory.py

IMPLEMENTATION

Added a new enum using the original as a template. Updated the interface
to use the enum. Updated ISpecificationSet.new() to adapt the new enum to
the enum used by lifecycle events.
    lib/lp/blueprints/enums.py
    lib/lp/blueprints/interfaces/specification.py
    lib/lp/blueprints/model/specification.py
    lib/lp/blueprints/tests/test_hasspecifications.py
    lib/lp/blueprints/tests/test_specification.py

Updated tests to import the enums form the true location.
    lib/lp/blueprints/tests/test_webservice.py
    lib/lp/code/model/tests/test_branch.py

Updated the factory to work with the life cycle rules.
    lib/lp/testing/factory.py

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/blueprints/enums.py'
2--- lib/lp/blueprints/enums.py 2010-11-01 03:57:52 +0000
3+++ lib/lp/blueprints/enums.py 2011-03-02 18:01:19 +0000
4@@ -5,6 +5,7 @@
5
6 __metaclass__ = type
7 __all__ = [
8+ 'NewSpecificationDefinitionStatus',
9 'SpecificationDefinitionStatus',
10 'SpecificationFilter',
11 'SpecificationGoalStatus',
12@@ -21,6 +22,7 @@
13 DBItem,
14 EnumeratedType,
15 Item,
16+ use_template,
17 )
18
19
20@@ -433,6 +435,22 @@
21 """)
22
23
24+class NewSpecificationDefinitionStatus(DBEnumeratedType):
25+ """The Initial status of a Specification.
26+
27+ The initial status to define the feature and get approval for the
28+ implementation plan.
29+ """
30+ use_template(SpecificationDefinitionStatus, include=(
31+ 'NEW',
32+ 'DISCUSSION',
33+ 'DRAFT',
34+ 'PENDINGREVIEW',
35+ 'PENDINGAPPROVAL',
36+ 'APPROVED',
37+ ))
38+
39+
40 class SpecificationGoalStatus(DBEnumeratedType):
41 """The target status for this specification.
42
43
44=== modified file 'lib/lp/blueprints/interfaces/specification.py'
45--- lib/lp/blueprints/interfaces/specification.py 2011-02-24 15:30:54 +0000
46+++ lib/lp/blueprints/interfaces/specification.py 2011-03-02 18:01:19 +0000
47@@ -47,6 +47,7 @@
48 from canonical.launchpad.interfaces.validation import valid_webref
49 from lp.app.validators import LaunchpadValidationError
50 from lp.blueprints.enums import (
51+ NewSpecificationDefinitionStatus,
52 SpecificationDefinitionStatus,
53 SpecificationGoalStatus,
54 SpecificationImplementationStatus,
55@@ -168,8 +169,8 @@
56 definition_status = exported(
57 Choice(
58 title=_('Definition Status'),
59- vocabulary=SpecificationDefinitionStatus,
60- default=SpecificationDefinitionStatus.NEW,
61+ vocabulary=NewSpecificationDefinitionStatus,
62+ default=NewSpecificationDefinitionStatus.NEW,
63 description=_(
64 "The current status of the process to define the "
65 "feature and get approval for the implementation plan.")),
66@@ -275,6 +276,18 @@
67 # referencing it.
68 id = Int(title=_("Database ID"), required=True, readonly=True)
69
70+ # Redefine definition_status to support all definition statuses for
71+ # life cycle events.
72+ definition_status = exported(
73+ Choice(
74+ title=_('Definition Status'),
75+ vocabulary=SpecificationDefinitionStatus,
76+ default=SpecificationDefinitionStatus.NEW,
77+ description=_(
78+ "The current status of the process to define the "
79+ "feature and get approval for the implementation plan.")),
80+ ('devel', dict(exported=True, readonly=True)), exported=False)
81+
82 priority = exported(
83 Choice(
84 title=_('Priority'), vocabulary=SpecificationPriority,
85
86=== modified file 'lib/lp/blueprints/model/specification.py'
87--- lib/lp/blueprints/model/specification.py 2010-12-02 16:13:51 +0000
88+++ lib/lp/blueprints/model/specification.py 2011-03-02 18:01:19 +0000
89@@ -52,6 +52,7 @@
90 )
91 from lp.blueprints.adapters import SpecificationDelta
92 from lp.blueprints.enums import (
93+ NewSpecificationDefinitionStatus,
94 SpecificationDefinitionStatus,
95 SpecificationFilter,
96 SpecificationGoalStatus,
97@@ -689,6 +690,7 @@
98 """
99 # Circular import.
100 from lp.registry.model.person import Person
101+
102 def cache_people(rows):
103 # Find the people we need:
104 person_ids = set()
105@@ -717,6 +719,7 @@
106 column = row[index]
107 index += 1
108 decorator(person, column)
109+
110 results = Store.of(self).find(
111 Specification,
112 SQL(query),
113@@ -900,6 +903,15 @@
114 drafter=None, whiteboard=None,
115 priority=SpecificationPriority.UNDEFINED):
116 """See ISpecificationSet."""
117+ # Adapt the NewSpecificationDefinitionStatus item to a
118+ # SpecificationDefinitionStatus item.
119+ status_name = definition_status.name
120+ status_names = NewSpecificationDefinitionStatus.items.mapping.keys()
121+ if status_name not in status_names:
122+ raise AssertionError(
123+ "definition_status must an item found in "
124+ "NewSpecificationDefinitionStatus.")
125+ definition_status = SpecificationDefinitionStatus.items[status_name]
126 return Specification(name=name, title=title, specurl=specurl,
127 summary=summary, priority=priority,
128 definition_status=definition_status, owner=owner,
129
130=== modified file 'lib/lp/blueprints/tests/test_hasspecifications.py'
131--- lib/lp/blueprints/tests/test_hasspecifications.py 2011-01-13 14:26:14 +0000
132+++ lib/lp/blueprints/tests/test_hasspecifications.py 2011-03-02 18:01:19 +0000
133@@ -6,9 +6,7 @@
134 __metaclass__ = type
135
136 from canonical.testing.layers import DatabaseFunctionalLayer
137-from lp.blueprints.interfaces.specification import (
138- SpecificationDefinitionStatus,
139- )
140+from lp.blueprints.enums import SpecificationDefinitionStatus
141 from lp.blueprints.interfaces.specificationtarget import (
142 IHasSpecifications,
143 )
144
145=== modified file 'lib/lp/blueprints/tests/test_specification.py'
146--- lib/lp/blueprints/tests/test_specification.py 2010-12-17 13:29:40 +0000
147+++ lib/lp/blueprints/tests/test_specification.py 2011-03-02 18:01:19 +0000
148@@ -6,13 +6,19 @@
149 __metaclass__ = type
150
151
152+from zope.component import getUtility
153 from zope.security.interfaces import Unauthorized
154 from zope.security.proxy import removeSecurityProxy
155
156 from canonical.launchpad.webapp.authorization import check_permission
157 from canonical.testing.layers import DatabaseFunctionalLayer
158 from lp.blueprints.errors import TargetAlreadyHasSpecification
159-from lp.blueprints.interfaces.specification import SpecificationGoalStatus
160+from lp.blueprints.enums import (
161+ NewSpecificationDefinitionStatus,
162+ SpecificationDefinitionStatus,
163+ SpecificationGoalStatus,
164+ )
165+from lp.blueprints.interfaces.specification import ISpecificationSet
166 from lp.testing import (
167 login_person,
168 TestCaseWithFactory,
169@@ -101,3 +107,39 @@
170 product=self.factory.makeProduct())
171 self.assertRaises(
172 Unauthorized, getattr, specification, 'setTarget')
173+
174+
175+class TestSpecificationSet(TestCaseWithFactory):
176+
177+ layer = DatabaseFunctionalLayer
178+
179+ def setUp(self):
180+ super(TestSpecificationSet, self).setUp()
181+ self.specification_set = getUtility(ISpecificationSet)
182+ self.new_names = NewSpecificationDefinitionStatus.items.mapping.keys()
183+
184+ def test_new_with_open_definition_status_creates_specification(self):
185+ # Calling new() with an open definition status will will create
186+ # a specification.
187+ self.assertTrue(
188+ SpecificationDefinitionStatus.NEW.name in self.new_names)
189+ product = self.factory.makeProduct()
190+ spec = self.specification_set.new(
191+ name='plane', title='Place', specurl='http://eg.org/plane',
192+ summary='summary', owner=product.owner, product=product,
193+ definition_status=SpecificationDefinitionStatus.NEW)
194+ self.assertEqual(
195+ SpecificationDefinitionStatus.NEW, spec.definition_status)
196+
197+ def test_new_with_closed_definition_status_raises_error(self):
198+ # Calling new() with a obsolete or superseded definition status
199+ # raises an error.
200+ self.assertTrue(
201+ SpecificationDefinitionStatus.OBSOLETE.name not in self.new_names)
202+ product = self.factory.makeProduct()
203+ args = dict(
204+ name='plane', title='Place', specurl='http://eg.org/plane',
205+ summary='summary', owner=product.owner, product=product,
206+ definition_status=SpecificationDefinitionStatus.OBSOLETE)
207+ self.assertRaises(
208+ AssertionError, self.specification_set.new, **args)
209
210=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
211--- lib/lp/blueprints/tests/test_webservice.py 2011-02-03 19:01:10 +0000
212+++ lib/lp/blueprints/tests/test_webservice.py 2011-03-02 18:01:19 +0000
213@@ -9,9 +9,7 @@
214
215 from canonical.testing import DatabaseFunctionalLayer
216 from canonical.launchpad.testing.pages import webservice_for_person
217-from lp.blueprints.interfaces.specification import (
218- SpecificationDefinitionStatus,
219- )
220+from lp.blueprints.enums import SpecificationDefinitionStatus
221 from lp.testing import (
222 launchpadlib_for,
223 person_logged_in,
224
225=== modified file 'lib/lp/code/model/tests/test_branch.py'
226--- lib/lp/code/model/tests/test_branch.py 2010-12-02 16:13:51 +0000
227+++ lib/lp/code/model/tests/test_branch.py 2011-03-02 18:01:19 +0000
228@@ -35,7 +35,7 @@
229 DatabaseFunctionalLayer,
230 LaunchpadZopelessLayer,
231 )
232-from lp.blueprints.enums import SpecificationDefinitionStatus
233+from lp.blueprints.enums import NewSpecificationDefinitionStatus
234 from lp.blueprints.interfaces.specification import ISpecificationSet
235 from lp.blueprints.model.specificationbranch import SpecificationBranch
236 from lp.bugs.interfaces.bug import (
237@@ -1100,7 +1100,7 @@
238 spec = getUtility(ISpecificationSet).new(
239 name='some-spec', title='Some spec', product=self.product,
240 owner=self.user, summary='', specurl=None,
241- definition_status=SpecificationDefinitionStatus.NEW)
242+ definition_status=NewSpecificationDefinitionStatus.NEW)
243 spec.linkBranch(self.branch, self.user)
244 self.assertEqual(self.branch.canBeDeleted(), False,
245 "A branch linked to a spec is not deletable.")
246
247=== modified file 'lib/lp/testing/factory.py'
248--- lib/lp/testing/factory.py 2011-03-01 09:43:35 +0000
249+++ lib/lp/testing/factory.py 2011-03-02 18:01:19 +0000
250@@ -104,6 +104,7 @@
251 from lp.archiveuploader.dscfile import DSCFile
252 from lp.archiveuploader.uploadpolicy import BuildDaemonUploadPolicy
253 from lp.blueprints.enums import (
254+ NewSpecificationDefinitionStatus,
255 SpecificationDefinitionStatus,
256 SpecificationPriority,
257 )
258@@ -1882,7 +1883,7 @@
259
260 def makeSpecification(self, product=None, title=None, distribution=None,
261 name=None, summary=None, owner=None,
262- status=SpecificationDefinitionStatus.NEW,
263+ status=NewSpecificationDefinitionStatus.NEW,
264 implementation_status=None, goal=None, specurl=None,
265 assignee=None, drafter=None, approver=None,
266 priority=None, whiteboard=None, milestone=None):
267@@ -1903,12 +1904,18 @@
268 owner = self.makePerson()
269 if priority is None:
270 priority = SpecificationPriority.UNDEFINED
271+ status_names = NewSpecificationDefinitionStatus.items.mapping.keys()
272+ if status.name in status_names:
273+ definition_status = status
274+ else:
275+ # This is to satisfy life cycle requirements.
276+ definition_status = NewSpecificationDefinitionStatus.NEW
277 spec = getUtility(ISpecificationSet).new(
278 name=name,
279 title=title,
280 specurl=None,
281 summary=summary,
282- definition_status=status,
283+ definition_status=definition_status,
284 whiteboard=whiteboard,
285 owner=owner,
286 assignee=assignee,
287@@ -1918,6 +1925,9 @@
288 distribution=distribution,
289 priority=priority)
290 naked_spec = removeSecurityProxy(spec)
291+ if status.name not in status_names:
292+ # Set the closed status after the status has a sane initial state.
293+ naked_spec.definition_status = status
294 if status == SpecificationDefinitionStatus.OBSOLETE:
295 # This is to satisfy a DB constraint of obsolete specs.
296 naked_spec.completer = owner