Merge lp:~abentley/launchpad/spec-creation-info-type into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 15949
Proposed branch: lp:~abentley/launchpad/spec-creation-info-type
Merge into: lp:launchpad
Diff against target: 512 lines (+204/-37)
9 files modified
lib/lp/blueprints/browser/specification.py (+64/-19)
lib/lp/blueprints/browser/tests/test_specification.py (+117/-4)
lib/lp/blueprints/interfaces/specification.py (+1/-4)
lib/lp/blueprints/interfaces/specificationtarget.py (+3/-0)
lib/lp/blueprints/model/specification.py (+2/-2)
lib/lp/registry/model/distribution.py (+4/-0)
lib/lp/registry/model/distroseries.py (+4/-4)
lib/lp/registry/model/product.py (+5/-0)
lib/lp/registry/model/productseries.py (+4/-4)
To merge this branch: bzr merge lp:~abentley/launchpad/spec-creation-info-type
Reviewer Review Type Date Requested Status
Deryck Hodge (community) Approve
Review via email: mp+123821@code.launchpad.net

Commit message

Allow specifying information_type when creating Blueprint.

Description of the change

= Summary =
Allow selecting information type when creating Specification

== Proposed fix ==
When creating a specification for an unknown target, allow any value. (Validation will catch places where an inappropriate value is used.)

When creating a specification for a known target, use the getAlloweedSpecificationInformationTypes for that target.

If only one value is allowed, do not show the choice.

== Pre-implementation notes ==
Discussed hiding when no choice is available with rick_h

== LOC Rationale ==
Part of Private Projects

== Implementation details ==
No changes are visible until blueprints.information_type.enabled is true.

DistroSeries and ProductSeries now explicitly delegate ISpecificationTarget to
distro and product, to simplify implementation.

Distribution always returns PUBLIC as the sole allowed InformationType.

Product always returns the PUBLIC_PROPRIETARY set as the allowed InformationTypes, but this is a stub until Product policies for information types have landed.

As a driveby, removed ProductEditView.private, since Product now provides this directly.

== Tests ==
bin/test specification

== Demo and Q/A ==
Create a specification
- From the root page
- From a sprint (meeting) page
- From a product page
- From a productseries page
- From a distribution page
- From a distroseries pagea

You should not be prompted for an information_type.

Enable blueprints.information_type.enabled

Create a specification
- From the root page
- From a sprint (meeting) page
- From a product page
- From a productseries page

You should be prompted for an information type. The values provided should be "Public", "Proprietary", "Embargoed".

Create a specification
- From a distribution page
- From a distroseries pagea
You should not be prompted for an information_type.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/model/productseries.py
  lib/lp/blueprints/interfaces/specificationtarget.py
  lib/lp/blueprints/model/specification.py
  lib/lp/registry/model/product.py
  lib/lp/blueprints/browser/specification.py
  lib/lp/registry/model/distroseries.py
  lib/lp/blueprints/browser/tests/test_specification.py
  lib/lp/registry/model/distribution.py
  lib/lp/blueprints/interfaces/specification.py

To post a comment you must log in.
Revision history for this message
Deryck Hodge (deryck) wrote :

Looks great. Good test coverage. Thanks!

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/browser/specification.py'
2--- lib/lp/blueprints/browser/specification.py 2012-09-04 20:24:29 +0000
3+++ lib/lp/blueprints/browser/specification.py 2012-09-11 19:43:18 +0000
4@@ -116,11 +116,14 @@
5 from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
6 from lp.blueprints.interfaces.sprintspecification import ISprintSpecification
7 from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
8-from lp.registry.enums import PRIVATE_INFORMATION_TYPES
9+from lp.registry.enums import (
10+ PUBLIC_PROPRIETARY_INFORMATION_TYPES,
11+ )
12 from lp.registry.interfaces.distribution import IDistribution
13 from lp.registry.interfaces.product import IProduct
14 from lp.registry.vocabularies import InformationTypeVocabulary
15 from lp.services.config import config
16+from lp.services.features import getFeatureFlag
17 from lp.services.fields import WorkItemsText
18 from lp.services.propertycache import cachedproperty
19 from lp.services.webapp import (
20@@ -139,6 +142,9 @@
21 )
22
23
24+INFORMATION_TYPE_FLAG = 'blueprints.information_type.enabled'
25+
26+
27 class INewSpecification(Interface):
28 """A schema for a new specification."""
29
30@@ -207,8 +213,11 @@
31
32 page_title = 'Register a blueprint in Launchpad'
33 label = "Register a new blueprint"
34+
35 custom_widget('specurl', TextWidget, displayWidth=60)
36
37+ custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
38+
39 @action(_('Register Blueprint'), name='register')
40 def register(self, action, data):
41 """Registers a new specification."""
42@@ -224,7 +233,8 @@
43 assignee=data.get('assignee'),
44 approver=data.get('approver'),
45 distribution=data.get('distribution'),
46- definition_status=data.get('definition_status'))
47+ definition_status=data.get('definition_status'),
48+ information_type=data.get('information_type'))
49 # Propose the specification as a series goal, if specified.
50 series = data.get('series')
51 if series is not None:
52@@ -274,8 +284,27 @@
53 The context must correspond to a unique specification target.
54 """
55
56- schema = Fields(INewSpecification,
57- INewSpecificationSprint)
58+ @property
59+ def info_type_field(self):
60+ """An info_type_field for creating a Specification.
61+
62+ None if the user cannot select different information types or the
63+ feature flag is not enabled.
64+ """
65+ if not getFeatureFlag(INFORMATION_TYPE_FLAG):
66+ return None
67+ info_types = self.context.getAllowedSpecificationInformationTypes()
68+ if len(info_types) < 2:
69+ return None
70+ return copy_field(ISpecification['information_type'], readonly=False,
71+ vocabulary=InformationTypeVocabulary(types=info_types))
72+
73+ @property
74+ def schema(self):
75+ fields = Fields(INewSpecification, INewSpecificationSprint)
76+ if self.info_type_field is not None:
77+ fields = fields + Fields(self.info_type_field)
78+ return fields
79
80
81 class NewSpecificationFromDistributionView(NewSpecificationFromTargetView):
82@@ -295,9 +324,14 @@
83 class NewSpecificationFromSeriesView(NewSpecificationFromTargetView):
84 """An abstract view for creating a specification from a series."""
85
86- schema = Fields(INewSpecification,
87- INewSpecificationSprint,
88- INewSpecificationSeriesGoal)
89+ @property
90+ def schema(self):
91+ fields = Fields(INewSpecification,
92+ INewSpecificationSprint,
93+ INewSpecificationSeriesGoal)
94+ if self.info_type_field is not None:
95+ fields = fields + Fields(self.info_type_field)
96+ return fields
97
98 def transform(self, data):
99 if data['goal']:
100@@ -320,13 +354,17 @@
101 data['product'] = self.context.product
102
103
104+all_info_type_field = copy_field(ISpecification['information_type'],
105+ readonly=False, vocabulary=InformationTypeVocabulary(
106+ types=PUBLIC_PROPRIETARY_INFORMATION_TYPES))
107+
108+
109 class NewSpecificationFromNonTargetView(NewSpecificationView):
110 """An abstract view for creating a specification outside a target context.
111
112 The context may not correspond to a unique specification target. Hence
113 sub-classes must define a schema requiring the user to specify a target.
114 """
115-
116 def transform(self, data):
117 data['distribution'] = IDistribution(data['target'], None)
118 data['product'] = IProduct(data['target'], None)
119@@ -349,22 +387,32 @@
120
121 schema = Fields(INewSpecificationProjectTarget,
122 INewSpecification,
123- INewSpecificationSprint)
124+ INewSpecificationSprint,
125+ all_info_type_field,)
126
127
128 class NewSpecificationFromRootView(NewSpecificationFromNonTargetView):
129 """A view for creating a specification from the root of Launchpad."""
130
131- schema = Fields(INewSpecificationTarget,
132- INewSpecification,
133- INewSpecificationSprint)
134+ @property
135+ def schema(self):
136+ fields = Fields(INewSpecificationTarget,
137+ INewSpecification,
138+ INewSpecificationSprint)
139+ if getFeatureFlag(INFORMATION_TYPE_FLAG):
140+ fields = fields + Fields(all_info_type_field)
141+ return fields
142
143
144 class NewSpecificationFromSprintView(NewSpecificationFromNonTargetView):
145 """A view for creating a specification from a sprint."""
146
147- schema = Fields(INewSpecificationTarget,
148- INewSpecification)
149+ @property
150+ def schema(self):
151+ fields = Fields(INewSpecificationTarget, INewSpecification)
152+ if getFeatureFlag(INFORMATION_TYPE_FLAG):
153+ fields = fields + Fields(all_info_type_field)
154+ return fields
155
156 def transform(self, data):
157 super(NewSpecificationFromSprintView, self).transform(data)
158@@ -535,6 +583,7 @@
159 text = 'Link a related branch'
160 return Link('+linkbranch', text, icon='add')
161
162+ @enabled_with_permission('launchpad.Edit')
163 def information_type(self):
164 """Return the 'Set privacy/security' Link."""
165 text = 'Change privacy/security'
166@@ -559,15 +608,11 @@
167
168 @cachedproperty
169 def privacy_portlet_css(self):
170- if self.private:
171+ if self.context.private:
172 return 'portlet private'
173 else:
174 return 'portlet public'
175
176- @cachedproperty
177- def private(self):
178- return self.context.information_type in PRIVATE_INFORMATION_TYPES
179-
180
181 class SpecificationView(SpecificationSimpleView):
182 """Used to render the main view of a specification."""
183
184=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
185--- lib/lp/blueprints/browser/tests/test_specification.py 2012-09-07 08:07:48 +0000
186+++ lib/lp/blueprints/browser/tests/test_specification.py 2012-09-11 19:43:18 +0000
187@@ -23,6 +23,7 @@
188
189 from lp.app.browser.tales import format_link
190 from lp.blueprints.browser import specification
191+from lp.blueprints.browser.specification import INFORMATION_TYPE_FLAG
192 from lp.blueprints.enums import SpecificationImplementationStatus
193 from lp.blueprints.interfaces.specification import (
194 ISpecification,
195@@ -175,6 +176,12 @@
196 "... Registered by Some Person ... ago ..."))
197
198
199+def set_blueprint_information_type(test_case, enabled):
200+ value = 'true' if enabled else ''
201+ fixture = FeatureFixture({INFORMATION_TYPE_FLAG: value})
202+ test_case.useFixture(fixture)
203+
204+
205 class TestSpecificationInformationType(BrowserTestCase):
206
207 layer = DatabaseFunctionalLayer
208@@ -184,8 +191,7 @@
209
210 def setUp(self):
211 super(TestSpecificationInformationType, self).setUp()
212- self.useFixture(FeatureFixture({'blueprints.information_type.enabled':
213- 'true'}))
214+ set_blueprint_information_type(self, True)
215
216 def assertBrowserMatches(self, matcher):
217 browser = self.getViewBrowser(self.factory.makeSpecification())
218@@ -195,8 +201,7 @@
219 self.assertBrowserMatches(soupmatchers.HTMLContains(self.portlet_tag))
220
221 def test_privacy_portlet_requires_flag(self):
222- self.useFixture(FeatureFixture({'blueprints.information_type.enabled':
223- ''}))
224+ set_blueprint_information_type(self, False)
225 self.assertBrowserMatches(
226 Not(soupmatchers.HTMLContains(self.portlet_tag)))
227
228@@ -256,6 +261,114 @@
229 self.assertEqual(InformationType.PUBLIC, spec.information_type)
230
231
232+# canonical_url erroneously returns http://blueprints.launchpad.dev/+new
233+NEW_SPEC_FROM_ROOT_URL = 'http://blueprints.launchpad.dev/specs/+new'
234+
235+
236+class TestNewSpecificationInformationType(BrowserTestCase):
237+
238+ layer = DatabaseFunctionalLayer
239+
240+ def setUp(self):
241+ super(TestNewSpecificationInformationType, self).setUp()
242+ set_blueprint_information_type(self, True)
243+ it_field = soupmatchers.Tag('it-field', True,
244+ attrs=dict(name='field.information_type'))
245+ self.match_it = soupmatchers.HTMLContains(it_field)
246+
247+ def test_from_root(self):
248+ """Information_type is included creating from root."""
249+ browser = self.getUserBrowser(NEW_SPEC_FROM_ROOT_URL)
250+ self.assertThat(browser.contents, self.match_it)
251+
252+ def test_from_root_no_flag(self):
253+ """Information_type is excluded with no flag."""
254+ set_blueprint_information_type(self, False)
255+ browser = self.getUserBrowser(NEW_SPEC_FROM_ROOT_URL)
256+ self.assertThat(browser.contents, Not(self.match_it))
257+
258+ def test_from_sprint(self):
259+ """Information_type is included creating from a sprint."""
260+ sprint = self.factory.makeSprint()
261+ browser = self.getViewBrowser(sprint, view_name='+addspec')
262+ self.assertThat(browser.contents, self.match_it)
263+
264+ def test_from_sprint_no_flag(self):
265+ """Information_type is excluded with no flag."""
266+ set_blueprint_information_type(self, False)
267+ sprint = self.factory.makeSprint()
268+ browser = self.getViewBrowser(sprint, view_name='+addspec')
269+ self.assertThat(browser.contents, Not(self.match_it))
270+
271+ def submitSpec(self, browser):
272+ """Submit a Specification via a browser."""
273+ name = self.factory.getUniqueString()
274+ browser.getControl('Name').value = name
275+ browser.getControl('Title').value = self.factory.getUniqueString()
276+ browser.getControl('Summary').value = self.factory.getUniqueString()
277+ browser.getControl('Register Blueprint').click()
278+ return name
279+
280+ def createSpec(self, information_type):
281+ """Create a specification via a browser."""
282+ with person_logged_in(self.user):
283+ product = self.factory.makeProduct()
284+ browser = self.getViewBrowser(product, view_name='+addspec')
285+ control = browser.getControl(information_type.title)
286+ if not control.selected:
287+ control.click()
288+ return product.getSpecification(self.submitSpec(browser))
289+
290+ def test_from_product(self):
291+ """Creating from a product defaults to PUBLIC."""
292+ product = self.factory.makeProduct()
293+ browser = self.getViewBrowser(product, view_name='+addspec')
294+ self.assertThat(browser.contents, self.match_it)
295+ spec = product.getSpecification(self.submitSpec(browser))
296+ self.assertEqual(spec.information_type, InformationType.PUBLIC)
297+
298+ def test_supplied_information_types(self):
299+ """Creating honours information types."""
300+ spec = self.createSpec(InformationType.PUBLIC)
301+ self.assertEqual(InformationType.PUBLIC, spec.information_type)
302+ spec = self.createSpec(InformationType.PROPRIETARY)
303+ self.assertEqual(InformationType.PROPRIETARY, spec.information_type)
304+ spec = self.createSpec(InformationType.EMBARGOED)
305+ self.assertEqual(InformationType.EMBARGOED, spec.information_type)
306+
307+ def test_from_product_no_flag(self):
308+ """information_type is excluded with no flag."""
309+ set_blueprint_information_type(self, False)
310+ product = self.factory.makeProduct()
311+ browser = self.getViewBrowser(product, view_name='+addspec')
312+ self.assertThat(browser.contents, Not(self.match_it))
313+
314+ def test_from_productseries(self):
315+ """Information_type is included creating from productseries."""
316+ series = self.factory.makeProductSeries()
317+ browser = self.getViewBrowser(series, view_name='+addspec')
318+ self.assertThat(browser.contents, self.match_it)
319+
320+ def test_from_productseries_no_flag(self):
321+ """information_type is excluded with no flag."""
322+ set_blueprint_information_type(self, False)
323+ series = self.factory.makeProductSeries()
324+ browser = self.getViewBrowser(series, view_name='+addspec')
325+ self.assertThat(browser.contents, Not(self.match_it))
326+
327+ def test_from_distribution(self):
328+ """information_type is excluded creating from distro."""
329+ distro = self.factory.makeDistribution()
330+ browser = self.getViewBrowser(distro, view_name='+addspec')
331+ self.assertThat(browser.contents, Not(self.match_it))
332+
333+ def test_from_distroseries(self):
334+ """information_type is excluded creating from distroseries."""
335+ series = self.factory.makeDistroSeries()
336+ browser = self.getViewBrowser(series, view_name='+addspec')
337+ self.assertThat(browser.contents, Not(self.match_it))
338+
339+
340 class TestSpecificationViewPrivateArtifacts(BrowserTestCase):
341 """ Tests that specifications with private team artifacts can be viewed.
342
343
344=== modified file 'lib/lp/blueprints/interfaces/specification.py'
345--- lib/lp/blueprints/interfaces/specification.py 2012-09-06 10:59:56 +0000
346+++ lib/lp/blueprints/interfaces/specification.py 2012-09-11 19:43:18 +0000
347@@ -566,10 +566,7 @@
348 """
349
350 def getAllowedInformationTypes(who):
351- """Get a list of acceptable `InformationType`s for this spec.
352-
353- The intersection of the affected pillars' allowed types is permitted.
354- """
355+ """Get a list of acceptable `InformationType`s for this spec."""
356
357
358 class ISpecificationEditRestricted(Interface):
359
360=== modified file 'lib/lp/blueprints/interfaces/specificationtarget.py'
361--- lib/lp/blueprints/interfaces/specificationtarget.py 2011-12-24 16:54:44 +0000
362+++ lib/lp/blueprints/interfaces/specificationtarget.py 2012-09-11 19:43:18 +0000
363@@ -110,6 +110,9 @@
364 or None.
365 """
366
367+ def getAllowedSpecificationInformationTypes():
368+ """Get the InformationTypes for this targets' specifications."""
369+
370
371 class ISpecificationGoal(ISpecificationTarget):
372 """An interface for those things which can have specifications proposed
373
374=== modified file 'lib/lp/blueprints/model/specification.py'
375--- lib/lp/blueprints/model/specification.py 2012-09-10 08:30:42 +0000
376+++ lib/lp/blueprints/model/specification.py 2012-09-11 19:43:18 +0000
377@@ -70,7 +70,6 @@
378 InformationType,
379 PRIVATE_INFORMATION_TYPES,
380 PUBLIC_INFORMATION_TYPES,
381- PUBLIC_PROPRIETARY_INFORMATION_TYPES,
382 )
383 from lp.registry.errors import CannotChangeInformationType
384 from lp.registry.interfaces.distribution import IDistribution
385@@ -818,7 +817,8 @@
386 self.id, self.name, self.target.name)
387
388 def getAllowedInformationTypes(self, who):
389- return set(PUBLIC_PROPRIETARY_INFORMATION_TYPES)
390+ """See `ISpecification`."""
391+ return self.target.getAllowedSpecificationInformationTypes()
392
393 def transitionToInformationType(self, information_type, who):
394 """See ISpecification."""
395
396=== modified file 'lib/lp/registry/model/distribution.py'
397--- lib/lp/registry/model/distribution.py 2012-09-05 07:16:09 +0000
398+++ lib/lp/registry/model/distribution.py 2012-09-11 19:43:18 +0000
399@@ -975,6 +975,10 @@
400 """See `ISpecificationTarget`."""
401 return Specification.selectOneBy(distribution=self, name=name)
402
403+ def getAllowedSpecificationInformationTypes(self):
404+ """See `ISpecificationTarget`."""
405+ return (InformationType.PUBLIC,)
406+
407 def searchQuestions(self, search_text=None,
408 status=QUESTION_STATUS_DEFAULT_SEARCH,
409 language=None, sort=None, owner=None,
410
411=== modified file 'lib/lp/registry/model/distroseries.py'
412--- lib/lp/registry/model/distroseries.py 2012-09-07 05:13:57 +0000
413+++ lib/lp/registry/model/distroseries.py 2012-09-11 19:43:18 +0000
414@@ -17,6 +17,7 @@
415 import logging
416
417 import apt_pkg
418+from lazr.delegates import delegates
419 from sqlobject import (
420 BoolCol,
421 ForeignKey,
422@@ -48,6 +49,7 @@
423 SpecificationImplementationStatus,
424 SpecificationSort,
425 )
426+from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
427 from lp.blueprints.model.specification import (
428 HasSpecificationsMixin,
429 Specification,
430@@ -201,6 +203,8 @@
431 IHasBuildRecords, IHasQueueItems, IServiceUsage,
432 ISeriesBugTarget)
433
434+ delegates(ISpecificationTarget, 'distribution')
435+
436 _table = 'DistroSeries'
437 _defaultOrder = ['distribution', 'version']
438
439@@ -908,10 +912,6 @@
440 results = results.prejoin(['assignee', 'approver', 'drafter'])
441 return results
442
443- def getSpecification(self, name):
444- """See ISpecificationTarget."""
445- return self.distribution.getSpecification(name)
446-
447 def getDistroSeriesLanguage(self, language):
448 """See `IDistroSeries`."""
449 return DistroSeriesLanguage.selectOneBy(
450
451=== modified file 'lib/lp/registry/model/product.py'
452--- lib/lp/registry/model/product.py 2012-09-06 00:01:38 +0000
453+++ lib/lp/registry/model/product.py 2012-09-11 19:43:18 +0000
454@@ -122,6 +122,7 @@
455 FREE_INFORMATION_TYPES,
456 InformationType,
457 PRIVATE_INFORMATION_TYPES,
458+ PUBLIC_PROPRIETARY_INFORMATION_TYPES,
459 )
460 from lp.registry.errors import CommercialSubscribersOnly
461 from lp.registry.interfaces.accesspolicy import (
462@@ -604,6 +605,10 @@
463 else:
464 return InformationType.PUBLIC
465
466+ def getAllowedSpecificationInformationTypes(self):
467+ """See `ISpecificationTarget`."""
468+ return PUBLIC_PROPRIETARY_INFORMATION_TYPES
469+
470 def _ensurePolicies(self, information_types):
471 # Ensure that the product has access policies for the specified
472 # information types.
473
474=== modified file 'lib/lp/registry/model/productseries.py'
475--- lib/lp/registry/model/productseries.py 2012-08-08 05:36:44 +0000
476+++ lib/lp/registry/model/productseries.py 2012-09-11 19:43:18 +0000
477@@ -14,6 +14,7 @@
478
479 import datetime
480
481+from lazr.delegates import delegates
482 from sqlobject import (
483 ForeignKey,
484 SQLMultipleJoin,
485@@ -45,6 +46,7 @@
486 SpecificationImplementationStatus,
487 SpecificationSort,
488 )
489+from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
490 from lp.blueprints.model.specification import (
491 HasSpecificationsMixin,
492 Specification,
493@@ -123,6 +125,8 @@
494 IBugSummaryDimension, IProductSeries, IServiceUsage,
495 ISeriesBugTarget)
496
497+ delegates(ISpecificationTarget, 'product')
498+
499 _table = 'ProductSeries'
500
501 product = ForeignKey(dbName='product', foreignKey='Product', notNull=True)
502@@ -451,10 +455,6 @@
503 from lp.bugs.model.bugsummary import BugSummary
504 return BugSummary.productseries_id == self.id
505
506- def getSpecification(self, name):
507- """See ISpecificationTarget."""
508- return self.product.getSpecification(name)
509-
510 def getLatestRelease(self):
511 """See `IProductRelease.`"""
512 try: