launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #11818
[Merge] lp:~abentley/launchpad/spec-creation-info-type into lp:launchpad
Aaron Bentley has proposed merging lp:~abentley/launchpad/spec-creation-info-type into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~abentley/launchpad/spec-creation-info-type/+merge/123821
= 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
--
https://code.launchpad.net/~abentley/launchpad/spec-creation-info-type/+merge/123821
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/spec-creation-info-type into lp:launchpad.
=== modified file 'lib/lp/blueprints/browser/specification.py'
--- lib/lp/blueprints/browser/specification.py 2012-09-04 20:24:29 +0000
+++ lib/lp/blueprints/browser/specification.py 2012-09-11 19:43:18 +0000
@@ -116,11 +116,14 @@
from lp.blueprints.interfaces.specificationbranch import ISpecificationBranch
from lp.blueprints.interfaces.sprintspecification import ISprintSpecification
from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
-from lp.registry.enums import PRIVATE_INFORMATION_TYPES
+from lp.registry.enums import (
+ PUBLIC_PROPRIETARY_INFORMATION_TYPES,
+ )
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.product import IProduct
from lp.registry.vocabularies import InformationTypeVocabulary
from lp.services.config import config
+from lp.services.features import getFeatureFlag
from lp.services.fields import WorkItemsText
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
@@ -139,6 +142,9 @@
)
+INFORMATION_TYPE_FLAG = 'blueprints.information_type.enabled'
+
+
class INewSpecification(Interface):
"""A schema for a new specification."""
@@ -207,8 +213,11 @@
page_title = 'Register a blueprint in Launchpad'
label = "Register a new blueprint"
+
custom_widget('specurl', TextWidget, displayWidth=60)
+ custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
+
@action(_('Register Blueprint'), name='register')
def register(self, action, data):
"""Registers a new specification."""
@@ -224,7 +233,8 @@
assignee=data.get('assignee'),
approver=data.get('approver'),
distribution=data.get('distribution'),
- definition_status=data.get('definition_status'))
+ definition_status=data.get('definition_status'),
+ information_type=data.get('information_type'))
# Propose the specification as a series goal, if specified.
series = data.get('series')
if series is not None:
@@ -274,8 +284,27 @@
The context must correspond to a unique specification target.
"""
- schema = Fields(INewSpecification,
- INewSpecificationSprint)
+ @property
+ def info_type_field(self):
+ """An info_type_field for creating a Specification.
+
+ None if the user cannot select different information types or the
+ feature flag is not enabled.
+ """
+ if not getFeatureFlag(INFORMATION_TYPE_FLAG):
+ return None
+ info_types = self.context.getAllowedSpecificationInformationTypes()
+ if len(info_types) < 2:
+ return None
+ return copy_field(ISpecification['information_type'], readonly=False,
+ vocabulary=InformationTypeVocabulary(types=info_types))
+
+ @property
+ def schema(self):
+ fields = Fields(INewSpecification, INewSpecificationSprint)
+ if self.info_type_field is not None:
+ fields = fields + Fields(self.info_type_field)
+ return fields
class NewSpecificationFromDistributionView(NewSpecificationFromTargetView):
@@ -295,9 +324,14 @@
class NewSpecificationFromSeriesView(NewSpecificationFromTargetView):
"""An abstract view for creating a specification from a series."""
- schema = Fields(INewSpecification,
- INewSpecificationSprint,
- INewSpecificationSeriesGoal)
+ @property
+ def schema(self):
+ fields = Fields(INewSpecification,
+ INewSpecificationSprint,
+ INewSpecificationSeriesGoal)
+ if self.info_type_field is not None:
+ fields = fields + Fields(self.info_type_field)
+ return fields
def transform(self, data):
if data['goal']:
@@ -320,13 +354,17 @@
data['product'] = self.context.product
+all_info_type_field = copy_field(ISpecification['information_type'],
+ readonly=False, vocabulary=InformationTypeVocabulary(
+ types=PUBLIC_PROPRIETARY_INFORMATION_TYPES))
+
+
class NewSpecificationFromNonTargetView(NewSpecificationView):
"""An abstract view for creating a specification outside a target context.
The context may not correspond to a unique specification target. Hence
sub-classes must define a schema requiring the user to specify a target.
"""
-
def transform(self, data):
data['distribution'] = IDistribution(data['target'], None)
data['product'] = IProduct(data['target'], None)
@@ -349,22 +387,32 @@
schema = Fields(INewSpecificationProjectTarget,
INewSpecification,
- INewSpecificationSprint)
+ INewSpecificationSprint,
+ all_info_type_field,)
class NewSpecificationFromRootView(NewSpecificationFromNonTargetView):
"""A view for creating a specification from the root of Launchpad."""
- schema = Fields(INewSpecificationTarget,
- INewSpecification,
- INewSpecificationSprint)
+ @property
+ def schema(self):
+ fields = Fields(INewSpecificationTarget,
+ INewSpecification,
+ INewSpecificationSprint)
+ if getFeatureFlag(INFORMATION_TYPE_FLAG):
+ fields = fields + Fields(all_info_type_field)
+ return fields
class NewSpecificationFromSprintView(NewSpecificationFromNonTargetView):
"""A view for creating a specification from a sprint."""
- schema = Fields(INewSpecificationTarget,
- INewSpecification)
+ @property
+ def schema(self):
+ fields = Fields(INewSpecificationTarget, INewSpecification)
+ if getFeatureFlag(INFORMATION_TYPE_FLAG):
+ fields = fields + Fields(all_info_type_field)
+ return fields
def transform(self, data):
super(NewSpecificationFromSprintView, self).transform(data)
@@ -535,6 +583,7 @@
text = 'Link a related branch'
return Link('+linkbranch', text, icon='add')
+ @enabled_with_permission('launchpad.Edit')
def information_type(self):
"""Return the 'Set privacy/security' Link."""
text = 'Change privacy/security'
@@ -559,15 +608,11 @@
@cachedproperty
def privacy_portlet_css(self):
- if self.private:
+ if self.context.private:
return 'portlet private'
else:
return 'portlet public'
- @cachedproperty
- def private(self):
- return self.context.information_type in PRIVATE_INFORMATION_TYPES
-
class SpecificationView(SpecificationSimpleView):
"""Used to render the main view of a specification."""
=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
--- lib/lp/blueprints/browser/tests/test_specification.py 2012-09-07 08:07:48 +0000
+++ lib/lp/blueprints/browser/tests/test_specification.py 2012-09-11 19:43:18 +0000
@@ -23,6 +23,7 @@
from lp.app.browser.tales import format_link
from lp.blueprints.browser import specification
+from lp.blueprints.browser.specification import INFORMATION_TYPE_FLAG
from lp.blueprints.enums import SpecificationImplementationStatus
from lp.blueprints.interfaces.specification import (
ISpecification,
@@ -175,6 +176,12 @@
"... Registered by Some Person ... ago ..."))
+def set_blueprint_information_type(test_case, enabled):
+ value = 'true' if enabled else ''
+ fixture = FeatureFixture({INFORMATION_TYPE_FLAG: value})
+ test_case.useFixture(fixture)
+
+
class TestSpecificationInformationType(BrowserTestCase):
layer = DatabaseFunctionalLayer
@@ -184,8 +191,7 @@
def setUp(self):
super(TestSpecificationInformationType, self).setUp()
- self.useFixture(FeatureFixture({'blueprints.information_type.enabled':
- 'true'}))
+ set_blueprint_information_type(self, True)
def assertBrowserMatches(self, matcher):
browser = self.getViewBrowser(self.factory.makeSpecification())
@@ -195,8 +201,7 @@
self.assertBrowserMatches(soupmatchers.HTMLContains(self.portlet_tag))
def test_privacy_portlet_requires_flag(self):
- self.useFixture(FeatureFixture({'blueprints.information_type.enabled':
- ''}))
+ set_blueprint_information_type(self, False)
self.assertBrowserMatches(
Not(soupmatchers.HTMLContains(self.portlet_tag)))
@@ -256,6 +261,114 @@
self.assertEqual(InformationType.PUBLIC, spec.information_type)
+# canonical_url erroneously returns http://blueprints.launchpad.dev/+new
+NEW_SPEC_FROM_ROOT_URL = 'http://blueprints.launchpad.dev/specs/+new'
+
+
+class TestNewSpecificationInformationType(BrowserTestCase):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestNewSpecificationInformationType, self).setUp()
+ set_blueprint_information_type(self, True)
+ it_field = soupmatchers.Tag('it-field', True,
+ attrs=dict(name='field.information_type'))
+ self.match_it = soupmatchers.HTMLContains(it_field)
+
+ def test_from_root(self):
+ """Information_type is included creating from root."""
+ browser = self.getUserBrowser(NEW_SPEC_FROM_ROOT_URL)
+ self.assertThat(browser.contents, self.match_it)
+
+ def test_from_root_no_flag(self):
+ """Information_type is excluded with no flag."""
+ set_blueprint_information_type(self, False)
+ browser = self.getUserBrowser(NEW_SPEC_FROM_ROOT_URL)
+ self.assertThat(browser.contents, Not(self.match_it))
+
+ def test_from_sprint(self):
+ """Information_type is included creating from a sprint."""
+ sprint = self.factory.makeSprint()
+ browser = self.getViewBrowser(sprint, view_name='+addspec')
+ self.assertThat(browser.contents, self.match_it)
+
+ def test_from_sprint_no_flag(self):
+ """Information_type is excluded with no flag."""
+ set_blueprint_information_type(self, False)
+ sprint = self.factory.makeSprint()
+ browser = self.getViewBrowser(sprint, view_name='+addspec')
+ self.assertThat(browser.contents, Not(self.match_it))
+
+ def submitSpec(self, browser):
+ """Submit a Specification via a browser."""
+ name = self.factory.getUniqueString()
+ browser.getControl('Name').value = name
+ browser.getControl('Title').value = self.factory.getUniqueString()
+ browser.getControl('Summary').value = self.factory.getUniqueString()
+ browser.getControl('Register Blueprint').click()
+ return name
+
+ def createSpec(self, information_type):
+ """Create a specification via a browser."""
+ with person_logged_in(self.user):
+ product = self.factory.makeProduct()
+ browser = self.getViewBrowser(product, view_name='+addspec')
+ control = browser.getControl(information_type.title)
+ if not control.selected:
+ control.click()
+ return product.getSpecification(self.submitSpec(browser))
+
+ def test_from_product(self):
+ """Creating from a product defaults to PUBLIC."""
+ product = self.factory.makeProduct()
+ browser = self.getViewBrowser(product, view_name='+addspec')
+ self.assertThat(browser.contents, self.match_it)
+ spec = product.getSpecification(self.submitSpec(browser))
+ self.assertEqual(spec.information_type, InformationType.PUBLIC)
+
+ def test_supplied_information_types(self):
+ """Creating honours information types."""
+ spec = self.createSpec(InformationType.PUBLIC)
+ self.assertEqual(InformationType.PUBLIC, spec.information_type)
+ spec = self.createSpec(InformationType.PROPRIETARY)
+ self.assertEqual(InformationType.PROPRIETARY, spec.information_type)
+ spec = self.createSpec(InformationType.EMBARGOED)
+ self.assertEqual(InformationType.EMBARGOED, spec.information_type)
+
+ def test_from_product_no_flag(self):
+ """information_type is excluded with no flag."""
+ set_blueprint_information_type(self, False)
+ product = self.factory.makeProduct()
+ browser = self.getViewBrowser(product, view_name='+addspec')
+ self.assertThat(browser.contents, Not(self.match_it))
+
+ def test_from_productseries(self):
+ """Information_type is included creating from productseries."""
+ series = self.factory.makeProductSeries()
+ browser = self.getViewBrowser(series, view_name='+addspec')
+ self.assertThat(browser.contents, self.match_it)
+
+ def test_from_productseries_no_flag(self):
+ """information_type is excluded with no flag."""
+ set_blueprint_information_type(self, False)
+ series = self.factory.makeProductSeries()
+ browser = self.getViewBrowser(series, view_name='+addspec')
+ self.assertThat(browser.contents, Not(self.match_it))
+
+ def test_from_distribution(self):
+ """information_type is excluded creating from distro."""
+ distro = self.factory.makeDistribution()
+ browser = self.getViewBrowser(distro, view_name='+addspec')
+ self.assertThat(browser.contents, Not(self.match_it))
+
+ def test_from_distroseries(self):
+ """information_type is excluded creating from distroseries."""
+ series = self.factory.makeDistroSeries()
+ browser = self.getViewBrowser(series, view_name='+addspec')
+ self.assertThat(browser.contents, Not(self.match_it))
+
+
class TestSpecificationViewPrivateArtifacts(BrowserTestCase):
""" Tests that specifications with private team artifacts can be viewed.
=== modified file 'lib/lp/blueprints/interfaces/specification.py'
--- lib/lp/blueprints/interfaces/specification.py 2012-09-06 10:59:56 +0000
+++ lib/lp/blueprints/interfaces/specification.py 2012-09-11 19:43:18 +0000
@@ -566,10 +566,7 @@
"""
def getAllowedInformationTypes(who):
- """Get a list of acceptable `InformationType`s for this spec.
-
- The intersection of the affected pillars' allowed types is permitted.
- """
+ """Get a list of acceptable `InformationType`s for this spec."""
class ISpecificationEditRestricted(Interface):
=== modified file 'lib/lp/blueprints/interfaces/specificationtarget.py'
--- lib/lp/blueprints/interfaces/specificationtarget.py 2011-12-24 16:54:44 +0000
+++ lib/lp/blueprints/interfaces/specificationtarget.py 2012-09-11 19:43:18 +0000
@@ -110,6 +110,9 @@
or None.
"""
+ def getAllowedSpecificationInformationTypes():
+ """Get the InformationTypes for this targets' specifications."""
+
class ISpecificationGoal(ISpecificationTarget):
"""An interface for those things which can have specifications proposed
=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py 2012-09-10 08:30:42 +0000
+++ lib/lp/blueprints/model/specification.py 2012-09-11 19:43:18 +0000
@@ -70,7 +70,6 @@
InformationType,
PRIVATE_INFORMATION_TYPES,
PUBLIC_INFORMATION_TYPES,
- PUBLIC_PROPRIETARY_INFORMATION_TYPES,
)
from lp.registry.errors import CannotChangeInformationType
from lp.registry.interfaces.distribution import IDistribution
@@ -818,7 +817,8 @@
self.id, self.name, self.target.name)
def getAllowedInformationTypes(self, who):
- return set(PUBLIC_PROPRIETARY_INFORMATION_TYPES)
+ """See `ISpecification`."""
+ return self.target.getAllowedSpecificationInformationTypes()
def transitionToInformationType(self, information_type, who):
"""See ISpecification."""
=== modified file 'lib/lp/registry/model/distribution.py'
--- lib/lp/registry/model/distribution.py 2012-09-05 07:16:09 +0000
+++ lib/lp/registry/model/distribution.py 2012-09-11 19:43:18 +0000
@@ -975,6 +975,10 @@
"""See `ISpecificationTarget`."""
return Specification.selectOneBy(distribution=self, name=name)
+ def getAllowedSpecificationInformationTypes(self):
+ """See `ISpecificationTarget`."""
+ return (InformationType.PUBLIC,)
+
def searchQuestions(self, search_text=None,
status=QUESTION_STATUS_DEFAULT_SEARCH,
language=None, sort=None, owner=None,
=== modified file 'lib/lp/registry/model/distroseries.py'
--- lib/lp/registry/model/distroseries.py 2012-09-07 05:13:57 +0000
+++ lib/lp/registry/model/distroseries.py 2012-09-11 19:43:18 +0000
@@ -17,6 +17,7 @@
import logging
import apt_pkg
+from lazr.delegates import delegates
from sqlobject import (
BoolCol,
ForeignKey,
@@ -48,6 +49,7 @@
SpecificationImplementationStatus,
SpecificationSort,
)
+from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
from lp.blueprints.model.specification import (
HasSpecificationsMixin,
Specification,
@@ -201,6 +203,8 @@
IHasBuildRecords, IHasQueueItems, IServiceUsage,
ISeriesBugTarget)
+ delegates(ISpecificationTarget, 'distribution')
+
_table = 'DistroSeries'
_defaultOrder = ['distribution', 'version']
@@ -908,10 +912,6 @@
results = results.prejoin(['assignee', 'approver', 'drafter'])
return results
- def getSpecification(self, name):
- """See ISpecificationTarget."""
- return self.distribution.getSpecification(name)
-
def getDistroSeriesLanguage(self, language):
"""See `IDistroSeries`."""
return DistroSeriesLanguage.selectOneBy(
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-09-06 00:01:38 +0000
+++ lib/lp/registry/model/product.py 2012-09-11 19:43:18 +0000
@@ -122,6 +122,7 @@
FREE_INFORMATION_TYPES,
InformationType,
PRIVATE_INFORMATION_TYPES,
+ PUBLIC_PROPRIETARY_INFORMATION_TYPES,
)
from lp.registry.errors import CommercialSubscribersOnly
from lp.registry.interfaces.accesspolicy import (
@@ -604,6 +605,10 @@
else:
return InformationType.PUBLIC
+ def getAllowedSpecificationInformationTypes(self):
+ """See `ISpecificationTarget`."""
+ return PUBLIC_PROPRIETARY_INFORMATION_TYPES
+
def _ensurePolicies(self, information_types):
# Ensure that the product has access policies for the specified
# information types.
=== modified file 'lib/lp/registry/model/productseries.py'
--- lib/lp/registry/model/productseries.py 2012-08-08 05:36:44 +0000
+++ lib/lp/registry/model/productseries.py 2012-09-11 19:43:18 +0000
@@ -14,6 +14,7 @@
import datetime
+from lazr.delegates import delegates
from sqlobject import (
ForeignKey,
SQLMultipleJoin,
@@ -45,6 +46,7 @@
SpecificationImplementationStatus,
SpecificationSort,
)
+from lp.blueprints.interfaces.specificationtarget import ISpecificationTarget
from lp.blueprints.model.specification import (
HasSpecificationsMixin,
Specification,
@@ -123,6 +125,8 @@
IBugSummaryDimension, IProductSeries, IServiceUsage,
ISeriesBugTarget)
+ delegates(ISpecificationTarget, 'product')
+
_table = 'ProductSeries'
product = ForeignKey(dbName='product', foreignKey='Product', notNull=True)
@@ -451,10 +455,6 @@
from lp.bugs.model.bugsummary import BugSummary
return BugSummary.productseries_id == self.id
- def getSpecification(self, name):
- """See ISpecificationTarget."""
- return self.product.getSpecification(name)
-
def getLatestRelease(self):
"""See `IProductRelease.`"""
try:
Follow ups