Merge lp:~rharding/launchpad/pp_register into lp:launchpad

Proposed by Richard Harding
Status: Merged
Approved by: Richard Harding
Approved revision: no longer in the source branch.
Merged at revision: 15948
Proposed branch: lp:~rharding/launchpad/pp_register
Merge into: lp:launchpad
Diff against target: 367 lines (+120/-15)
9 files modified
lib/lp/app/javascript/choice.js (+3/-0)
lib/lp/registry/browser/product.py (+70/-7)
lib/lp/registry/browser/tests/test_product.py (+2/-1)
lib/lp/registry/interfaces/product.py (+16/-2)
lib/lp/registry/model/product.py (+19/-2)
lib/lp/registry/model/projectgroup.py (+1/-1)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+1/-0)
lib/lp/registry/templates/product-new.pt (+7/-1)
lib/lp/registry/tests/test_pillaraffiliation.py (+1/-1)
To merge this branch: bzr merge lp:~rharding/launchpad/pp_register
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+122666@code.launchpad.net

Commit message

Add start of UI for selecting private projects during registration.

Description of the change

= Summary =

In order to allow creation or private projects we need to enable a drop down
to display information type.

Private projects will also get bugs, branches, and blueprints on by default so
selecting a driver and bug supervisor is moved up to project registration as
well.

== Pre Implementation ==

Lots of discussion about which fields to add and about choosing the three
valid information types for a 'private' project to be public, embargoed, and
proprietary.

== Implementation Notes ==

All work is hidden behind the private projects feature flag.

Deryck is working on the information type field in the database so this work
simply adds the property without storing. This merely adds the UI to select a
value during registration if the feature flag is enabled, but doesn't store
that value.

Bug supervisor and driver are intended to only be shown if the project is not
public, this will be done in a follow up branch since it's currently behind
the flag.

These changes are only applicable to +new project registration and so are
checked against the interface for IProductSet and not enabled for
ProjectGroup.

== Tests ==

All current tests pass. New tests will be forth coming as the db field is
properly stored and tests for the UI adjustments will be done in the
javascript tests in a follow up branch.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

Looks great.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/javascript/choice.js'
--- lib/lp/app/javascript/choice.js 2012-09-05 23:06:42 +0000
+++ lib/lp/app/javascript/choice.js 2012-09-12 13:36:49 +0000
@@ -160,6 +160,9 @@
160 * @param cfg160 * @param cfg
161 */161 */
162namespace.addPopupChoiceForRadioButtons = function(field_name, choices, cfg) {162namespace.addPopupChoiceForRadioButtons = function(field_name, choices, cfg) {
163 if (!choices) {
164 throw 'No choices for the popup.'
165 }
163 cfg = Y.merge(default_popup_choice_config, cfg);166 cfg = Y.merge(default_popup_choice_config, cfg);
164 var field_node = cfg.container.one('[name="field.' + field_name + '"]');167 var field_node = cfg.container.one('[name="field.' + field_name + '"]');
165 if (!Y.Lang.isValue(field_node)) {168 if (!Y.Lang.isValue(field_node)) {
166169
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2012-08-24 05:09:51 +0000
+++ lib/lp/registry/browser/product.py 2012-09-12 13:36:49 +0000
@@ -52,6 +52,8 @@
5252
53from lazr.delegates import delegates53from lazr.delegates import delegates
54from lazr.restful.interface import copy_field54from lazr.restful.interface import copy_field
55from lazr.restful.interfaces import IJSONRequestCache
56
55import pytz57import pytz
56from z3c.ptcompat import ViewPageTemplateFile58from z3c.ptcompat import ViewPageTemplateFile
57from zope.app.form import CustomWidgetFactory59from zope.app.form import CustomWidgetFactory
@@ -114,6 +116,7 @@
114from lp.app.widgets.itemswidgets import (116from lp.app.widgets.itemswidgets import (
115 CheckBoxMatrixWidget,117 CheckBoxMatrixWidget,
116 LaunchpadRadioWidget,118 LaunchpadRadioWidget,
119 LaunchpadRadioWidgetWithDescription,
117 )120 )
118from lp.app.widgets.popup import PersonPickerWidget121from lp.app.widgets.popup import PersonPickerWidget
119from lp.app.widgets.product import (122from lp.app.widgets.product import (
@@ -141,6 +144,7 @@
141 add_subscribe_link,144 add_subscribe_link,
142 BaseRdfView,145 BaseRdfView,
143 )146 )
147from lp.services.features import getFeatureFlag
144from lp.registry.browser.announcement import HasAnnouncementsView148from lp.registry.browser.announcement import HasAnnouncementsView
145from lp.registry.browser.branding import BrandingChangeView149from lp.registry.browser.branding import BrandingChangeView
146from lp.registry.browser.menu import (150from lp.registry.browser.menu import (
@@ -154,6 +158,11 @@
154 PillarViewMixin,158 PillarViewMixin,
155 )159 )
156from lp.registry.browser.productseries import get_series_branch_error160from lp.registry.browser.productseries import get_series_branch_error
161from lp.registry.enums import (
162 InformationType,
163 PRIVATE_INFORMATION_TYPES,
164 PUBLIC_INFORMATION_TYPES,
165 )
157from lp.registry.interfaces.pillar import IPillarNameSet166from lp.registry.interfaces.pillar import IPillarNameSet
158from lp.registry.interfaces.product import (167from lp.registry.interfaces.product import (
159 IProduct,168 IProduct,
@@ -1986,9 +1995,9 @@
1986class ProjectAddStepTwo(StepView, ProductLicenseMixin, ReturnToReferrerMixin):1995class ProjectAddStepTwo(StepView, ProductLicenseMixin, ReturnToReferrerMixin):
1987 """Step 2 (of 2) in the +new project add wizard."""1996 """Step 2 (of 2) in the +new project add wizard."""
19881997
1989 _field_names = ['displayname', 'name', 'title', 'summary',1998 _field_names = ['displayname', 'name', 'title', 'summary', 'description',
1990 'description', 'homepageurl', 'licenses', 'license_info',1999 'homepageurl', 'information_type', 'licenses',
1991 'owner',2000 'license_info', 'driver', 'bug_supervisor', 'owner',
1992 ]2001 ]
1993 schema = IProduct2002 schema = IProduct
1994 step_name = 'projectaddstep2'2003 step_name = 'projectaddstep2'
@@ -2002,12 +2011,39 @@
2002 custom_widget('homepageurl', TextWidget, displayWidth=30)2011 custom_widget('homepageurl', TextWidget, displayWidth=30)
2003 custom_widget('licenses', LicenseWidget)2012 custom_widget('licenses', LicenseWidget)
2004 custom_widget('license_info', GhostWidget)2013 custom_widget('license_info', GhostWidget)
2014 custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
2015
2005 custom_widget(2016 custom_widget(
2006 'owner', PersonPickerWidget, header="Select the maintainer",2017 'owner', PersonPickerWidget, header="Select the maintainer",
2007 show_create_team_link=True)2018 show_create_team_link=True)
2008 custom_widget(2019 custom_widget(
2020 'bug_supervisor', PersonPickerWidget, header="Set a bug supervisor",
2021 required=True, show_create_team_link=True)
2022 custom_widget(
2023 'driver', PersonPickerWidget, header="Set a driver",
2024 required=True, show_create_team_link=True)
2025 custom_widget(
2009 'disclaim_maintainer', CheckBoxWidget, cssClass="subordinate")2026 'disclaim_maintainer', CheckBoxWidget, cssClass="subordinate")
20102027
2028 def initialize(self):
2029 # The JSON cache must be populated before the super call, since
2030 # the form is rendered during LaunchpadFormView's initialize()
2031 # when an action is invokved.
2032 if IProductSet.providedBy(self.context):
2033 cache = IJSONRequestCache(self.request)
2034 cache.objects['private_types'] = [
2035 type.name for type in PRIVATE_INFORMATION_TYPES]
2036 cache.objects['public_types'] = [
2037 type.name for type in PUBLIC_INFORMATION_TYPES]
2038 cache.objects['information_type_data'] = [
2039 {'value': term.name, 'description': term.description,
2040 'name': term.title,
2041 'description_css_class': 'choice-description'}
2042 for term in
2043 self.context.getAllowedProductInformationTypes()]
2044
2045 super(ProjectAddStepTwo, self).initialize()
2046
2011 @property2047 @property
2012 def main_action_label(self):2048 def main_action_label(self):
2013 if self.source_package_name is None:2049 if self.source_package_name is None:
@@ -2036,13 +2072,24 @@
20362072
2037 @property2073 @property
2038 def initial_values(self):2074 def initial_values(self):
2039 return {'owner': self.user.name}2075 return {
2076 'driver': self.user.name,
2077 'bug_supervisor': self.user.name,
2078 'owner': self.user.name,
2079 }
20402080
2041 def setUpFields(self):2081 def setUpFields(self):
2042 """See `LaunchpadFormView`."""2082 """See `LaunchpadFormView`."""
2043 super(ProjectAddStepTwo, self).setUpFields()2083 super(ProjectAddStepTwo, self).setUpFields()
2044 hidden_names = ('__visited_steps__', 'license_info')2084 hidden_names = ['__visited_steps__', 'license_info']
2045 hidden_fields = self.form_fields.select(*hidden_names)2085 hidden_fields = self.form_fields.select(*hidden_names)
2086
2087 private_projects_flag = 'disclosure.private_projects.enabled'
2088 private_projects = bool(getFeatureFlag(private_projects_flag))
2089 if not private_projects or not IProductSet.providedBy(self.context):
2090 hidden_names.extend([
2091 'information_type', 'bug_supervisor', 'driver'])
2092
2046 visible_fields = self.form_fields.omit(*hidden_names)2093 visible_fields = self.form_fields.omit(*hidden_names)
2047 self.form_fields = (visible_fields +2094 self.form_fields = (visible_fields +
2048 self._createDisclaimMaintainerField() +2095 self._createDisclaimMaintainerField() +
@@ -2056,7 +2103,6 @@
2056 this checkbox and the ownership will be transfered to the registry2103 this checkbox and the ownership will be transfered to the registry
2057 admins team.2104 admins team.
2058 """2105 """
2059
2060 return form.Fields(2106 return form.Fields(
2061 Bool(__name__='disclaim_maintainer',2107 Bool(__name__='disclaim_maintainer',
2062 title=_("I do not want to maintain this project"),2108 title=_("I do not want to maintain this project"),
@@ -2079,10 +2125,15 @@
2079 self.widgets['name'].hint = ('When published, '2125 self.widgets['name'].hint = ('When published, '
2080 "this will be the project's URL.")2126 "this will be the project's URL.")
2081 self.widgets['displayname'].visible = False2127 self.widgets['displayname'].visible = False
2082
2083 self.widgets['source_package_name'].visible = False2128 self.widgets['source_package_name'].visible = False
2084 self.widgets['distroseries'].visible = False2129 self.widgets['distroseries'].visible = False
20852130
2131 private_projects_flag = 'disclosure.private_projects.enabled'
2132 private_projects = bool(getFeatureFlag(private_projects_flag))
2133
2134 if private_projects and IProductSet.providedBy(self.context):
2135 self.widgets['information_type'].value = InformationType.PUBLIC
2136
2086 # Set the source_package_release attribute on the licenses2137 # Set the source_package_release attribute on the licenses
2087 # widget, so that the source package's copyright info can be2138 # widget, so that the source package's copyright info can be
2088 # displayed.2139 # displayed.
@@ -2150,6 +2201,16 @@
2150 for error in errors:2201 for error in errors:
2151 self.errors.remove(error)2202 self.errors.remove(error)
21522203
2204 private_projects_flag = 'disclosure.private_projects.enabled'
2205 private_projects = bool(getFeatureFlag(private_projects_flag))
2206 if private_projects:
2207 if data.get('information_type') != InformationType.PUBLIC:
2208 for required_field in ('bug_supervisor', 'driver'):
2209 if data.get(required_field) is None:
2210 self.setFieldError(
2211 required_field,
2212 'Select a user or team.')
2213
2153 @property2214 @property
2154 def label(self):2215 def label(self):
2155 """See `LaunchpadFormView`."""2216 """See `LaunchpadFormView`."""
@@ -2169,6 +2230,8 @@
2169 owner = data.get('owner')2230 owner = data.get('owner')
2170 return getUtility(IProductSet).createProduct(2231 return getUtility(IProductSet).createProduct(
2171 registrant=self.user,2232 registrant=self.user,
2233 bug_supervisor=data.get('bug_supervisor', None),
2234 driver=data.get('driver', None),
2172 owner=owner,2235 owner=owner,
2173 name=data['name'],2236 name=data['name'],
2174 displayname=data['displayname'],2237 displayname=data['displayname'],
21752238
=== modified file 'lib/lp/registry/browser/tests/test_product.py'
--- lib/lp/registry/browser/tests/test_product.py 2012-08-22 04:55:44 +0000
+++ lib/lp/registry/browser/tests/test_product.py 2012-09-12 13:36:49 +0000
@@ -148,7 +148,8 @@
148 self.assertEqual('subordinate', disclaim_widget.cssClass)148 self.assertEqual('subordinate', disclaim_widget.cssClass)
149 self.assertEqual(149 self.assertEqual(
150 ['displayname', 'name', 'title', 'summary', 'description',150 ['displayname', 'name', 'title', 'summary', 'description',
151 'homepageurl', 'licenses', 'license_info', 'owner',151 'homepageurl', 'information_type', 'licenses', 'license_info',
152 'driver', 'bug_supervisor', 'owner',
152 '__visited_steps__'],153 '__visited_steps__'],
153 view.view.field_names)154 view.view.field_names)
154 self.assertEqual(155 self.assertEqual(
155156
=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2012-09-03 02:18:05 +0000
+++ lib/lp/registry/interfaces/product.py 2012-09-12 13:36:49 +0000
@@ -108,6 +108,7 @@
108from lp.registry.enums import (108from lp.registry.enums import (
109 BranchSharingPolicy,109 BranchSharingPolicy,
110 BugSharingPolicy,110 BugSharingPolicy,
111 InformationType,
111 )112 )
112from lp.registry.interfaces.announcement import IMakesAnnouncements113from lp.registry.interfaces.announcement import IMakesAnnouncements
113from lp.registry.interfaces.commercialsubscription import (114from lp.registry.interfaces.commercialsubscription import (
@@ -448,6 +449,13 @@
448 'and security policy will apply to this project.')),449 'and security policy will apply to this project.')),
449 exported_as='project_group')450 exported_as='project_group')
450451
452 information_type = exported(
453 Choice(
454 title=_('Information Type'), vocabulary=InformationType,
455 required=True, readonly=True,
456 description=_(
457 'The type of of data contained in this project.')))
458
451 owner = exported(459 owner = exported(
452 PersonChoice(460 PersonChoice(
453 title=_('Maintainer'),461 title=_('Maintainer'),
@@ -966,6 +974,12 @@
966 returned.974 returned.
967 """975 """
968976
977 def getAllowedProductInformationTypes():
978 """Get the information types that a project can have.
979
980 :return: A sequence of `InformationType`s.
981 """
982
969 @call_with(owner=REQUEST_USER)983 @call_with(owner=REQUEST_USER)
970 @rename_parameters_as(984 @rename_parameters_as(
971 displayname='display_name', project='project_group',985 displayname='display_name', project='project_group',
@@ -980,7 +994,7 @@
980 'downloadurl', 'freshmeatproject', 'wikiurl',994 'downloadurl', 'freshmeatproject', 'wikiurl',
981 'sourceforgeproject', 'programminglang',995 'sourceforgeproject', 'programminglang',
982 'project_reviewed', 'licenses', 'license_info',996 'project_reviewed', 'licenses', 'license_info',
983 'registrant'])997 'registrant', 'bug_supervisor', 'driver'])
984 @export_operation_as('new_project')998 @export_operation_as('new_project')
985 def createProduct(owner, name, displayname, title, summary,999 def createProduct(owner, name, displayname, title, summary,
986 description=None, project=None, homepageurl=None,1000 description=None, project=None, homepageurl=None,
@@ -989,7 +1003,7 @@
989 sourceforgeproject=None, programminglang=None,1003 sourceforgeproject=None, programminglang=None,
990 project_reviewed=False, mugshot=None, logo=None,1004 project_reviewed=False, mugshot=None, logo=None,
991 icon=None, licenses=None, license_info=None,1005 icon=None, licenses=None, license_info=None,
992 registrant=None):1006 registrant=None, bug_supervisor=None, driver=None):
993 """Create and return a brand new Product.1007 """Create and return a brand new Product.
9941008
995 See `IProduct` for a description of the parameters.1009 See `IProduct` for a description of the parameters.
9961010
=== 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-12 13:36:49 +0000
@@ -396,6 +396,16 @@
396 date_next_suggest_packaging = UtcDateTimeCol(default=None)396 date_next_suggest_packaging = UtcDateTimeCol(default=None)
397397
398 @property398 @property
399 def information_type(self):
400 """See `IProduct`
401
402 Place holder for a db column.
403 XXX: rharding 2012-09-10 bug=1048720: Waiting on db patch to connect
404 into place.
405 """
406 pass
407
408 @property
399 def pillar(self):409 def pillar(self):
400 """See `IBugTarget`."""410 """See `IBugTarget`."""
401 return self411 return self
@@ -1550,6 +1560,12 @@
1550 results = results.limit(num_products)1560 results = results.limit(num_products)
1551 return results1561 return results
15521562
1563 def getAllowedProductInformationTypes(self):
1564 """See `IProductSet`."""
1565 return (InformationType.PUBLIC,
1566 InformationType.EMBARGOED,
1567 InformationType.PROPRIETARY)
1568
1553 def createProduct(self, owner, name, displayname, title, summary,1569 def createProduct(self, owner, name, displayname, title, summary,
1554 description=None, project=None, homepageurl=None,1570 description=None, project=None, homepageurl=None,
1555 screenshotsurl=None, wikiurl=None,1571 screenshotsurl=None, wikiurl=None,
@@ -1557,7 +1573,7 @@
1557 sourceforgeproject=None, programminglang=None,1573 sourceforgeproject=None, programminglang=None,
1558 project_reviewed=False, mugshot=None, logo=None,1574 project_reviewed=False, mugshot=None, logo=None,
1559 icon=None, licenses=None, license_info=None,1575 icon=None, licenses=None, license_info=None,
1560 registrant=None):1576 registrant=None, bug_supervisor=None, driver=None):
1561 """See `IProductSet`."""1577 """See `IProductSet`."""
1562 if registrant is None:1578 if registrant is None:
1563 registrant = owner1579 registrant = owner
@@ -1572,7 +1588,8 @@
1572 sourceforgeproject=sourceforgeproject,1588 sourceforgeproject=sourceforgeproject,
1573 programminglang=programminglang,1589 programminglang=programminglang,
1574 project_reviewed=project_reviewed,1590 project_reviewed=project_reviewed,
1575 icon=icon, logo=logo, mugshot=mugshot, license_info=license_info)1591 icon=icon, logo=logo, mugshot=mugshot, license_info=license_info,
1592 bug_supervisor=bug_supervisor, driver=driver)
15761593
1577 # Set up the sharing policies and product licence.1594 # Set up the sharing policies and product licence.
1578 bug_sharing_policy_to_use = BugSharingPolicy.PUBLIC1595 bug_sharing_policy_to_use = BugSharingPolicy.PUBLIC
15791596
=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py 2012-08-03 01:42:13 +0000
+++ lib/lp/registry/model/projectgroup.py 2012-09-12 13:36:49 +0000
@@ -575,7 +575,7 @@
575575
576 def new(self, name, displayname, title, homepageurl, summary,576 def new(self, name, displayname, title, homepageurl, summary,
577 description, owner, mugshot=None, logo=None, icon=None,577 description, owner, mugshot=None, logo=None, icon=None,
578 registrant=None):578 registrant=None, bug_supervisor=None, driver=None):
579 """See `lp.registry.interfaces.projectgroup.IProjectGroupSet`."""579 """See `lp.registry.interfaces.projectgroup.IProjectGroupSet`."""
580 if registrant is None:580 if registrant is None:
581 registrant = owner581 registrant = owner
582582
=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2012-08-21 04:04:47 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2012-09-12 13:36:49 +0000
@@ -168,6 +168,7 @@
168 freshmeat_project: None168 freshmeat_project: None
169 homepage_url: None169 homepage_url: None
170 icon_link: u'http://.../firefox/icon'170 icon_link: u'http://.../firefox/icon'
171 information_type: None
171 is_permitted: True172 is_permitted: True
172 license_approved: False173 license_approved: False
173 license_info: None174 license_info: None
174175
=== modified file 'lib/lp/registry/templates/product-new.pt'
--- lib/lp/registry/templates/product-new.pt 2012-09-10 20:52:27 +0000
+++ lib/lp/registry/templates/product-new.pt 2012-09-12 13:36:49 +0000
@@ -14,8 +14,14 @@
14 * details widgets until the user states that the project they are14 * details widgets until the user states that the project they are
15 * registering is not a duplicate.15 * registering is not a duplicate.
16 */16 */
17LPJS.use('node', 'lp.ui.effects', function(Y) {17LPJS.use('node', 'lp.ui.effects', 'lp.app.choice', function(Y) {
18 Y.on('domready', function() {18 Y.on('domready', function() {
19 // Setup the information choice widget.
20 if (Y.one('input[name="field.information_type"]')) {
21 Y.lp.app.choice.addPopupChoiceForRadioButtons(
22 'information_type', LP.cache.information_type_data, true);
23 }
24
19 /* These two regexps serve slightly different purposes. The first25 /* These two regexps serve slightly different purposes. The first
20 * finds the leftmost run of valid url characters for the autofill26 * finds the leftmost run of valid url characters for the autofill
21 * operation. The second validates the entire string, used for27 * operation. The second validates the entire string, used for
2228
=== modified file 'lib/lp/registry/tests/test_pillaraffiliation.py'
--- lib/lp/registry/tests/test_pillaraffiliation.py 2012-08-21 04:04:47 +0000
+++ lib/lp/registry/tests/test_pillaraffiliation.py 2012-09-12 13:36:49 +0000
@@ -150,7 +150,7 @@
150 Store.of(product).invalidate()150 Store.of(product).invalidate()
151 with StormStatementRecorder() as recorder:151 with StormStatementRecorder() as recorder:
152 IHasAffiliation(product).getAffiliationBadges([person])152 IHasAffiliation(product).getAffiliationBadges([person])
153 self.assertThat(recorder, HasQueryCount(Equals(2)))153 self.assertThat(recorder, HasQueryCount(Equals(5)))
154154
155 def test_distro_affiliation_query_count(self):155 def test_distro_affiliation_query_count(self):
156 # Only 2 business queries are expected, selects from:156 # Only 2 business queries are expected, selects from: