launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #11757
[Merge] lp:~rharding/launchpad/pp_register into lp:launchpad
Richard Harding has proposed merging lp:~rharding/launchpad/pp_register into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1048717 in Launchpad itself: "project registration doesn't include private project options."
https://bugs.launchpad.net/launchpad/+bug/1048717
For more details, see:
https://code.launchpad.net/~rharding/launchpad/pp_register/+merge/122666
= 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.
--
https://code.launchpad.net/~rharding/launchpad/pp_register/+merge/122666
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rharding/launchpad/pp_register into lp:launchpad.
=== modified file 'lib/lp/app/javascript/choice.js'
--- lib/lp/app/javascript/choice.js 2012-07-07 14:00:30 +0000
+++ lib/lp/app/javascript/choice.js 2012-09-10 16:44:22 +0000
@@ -160,6 +160,9 @@
* @param cfg
*/
namespace.addPopupChoiceForRadioButtons = function(field_name, choices, cfg) {
+ if (!choices) {
+ throw 'No choices for the popup.'
+ }
cfg = Y.merge(default_popup_choice_config, cfg);
var field_node = cfg.container.one('[name="field.' + field_name + '"]');
if (!Y.Lang.isValue(field_node)) {
=== 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-10 16:44:22 +0000
@@ -52,6 +52,8 @@
from lazr.delegates import delegates
from lazr.restful.interface import copy_field
+from lazr.restful.interfaces import IJSONRequestCache
+
import pytz
from z3c.ptcompat import ViewPageTemplateFile
from zope.app.form import CustomWidgetFactory
@@ -114,6 +116,7 @@
from lp.app.widgets.itemswidgets import (
CheckBoxMatrixWidget,
LaunchpadRadioWidget,
+ LaunchpadRadioWidgetWithDescription,
)
from lp.app.widgets.popup import PersonPickerWidget
from lp.app.widgets.product import (
@@ -141,6 +144,7 @@
add_subscribe_link,
BaseRdfView,
)
+from lp.services.features import getFeatureFlag
from lp.registry.browser.announcement import HasAnnouncementsView
from lp.registry.browser.branding import BrandingChangeView
from lp.registry.browser.menu import (
@@ -154,6 +158,11 @@
PillarViewMixin,
)
from lp.registry.browser.productseries import get_series_branch_error
+from lp.registry.enums import (
+ InformationType,
+ PRIVATE_INFORMATION_TYPES,
+ PUBLIC_INFORMATION_TYPES,
+ )
from lp.registry.interfaces.pillar import IPillarNameSet
from lp.registry.interfaces.product import (
IProduct,
@@ -1986,9 +1995,9 @@
class ProjectAddStepTwo(StepView, ProductLicenseMixin, ReturnToReferrerMixin):
"""Step 2 (of 2) in the +new project add wizard."""
- _field_names = ['displayname', 'name', 'title', 'summary',
- 'description', 'homepageurl', 'licenses', 'license_info',
- 'owner',
+ _field_names = ['displayname', 'name', 'title', 'summary', 'description',
+ 'homepageurl', 'information_type', 'licenses',
+ 'license_info', 'driver', 'bug_supervisor', 'owner',
]
schema = IProduct
step_name = 'projectaddstep2'
@@ -2002,12 +2011,38 @@
custom_widget('homepageurl', TextWidget, displayWidth=30)
custom_widget('licenses', LicenseWidget)
custom_widget('license_info', GhostWidget)
+ custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
+
custom_widget(
'owner', PersonPickerWidget, header="Select the maintainer",
show_create_team_link=True)
custom_widget(
+ 'bug_supervisor', PersonPickerWidget, header="Set a bug supervisor",
+ required=True, show_create_team_link=True)
+ custom_widget(
+ 'driver', PersonPickerWidget, header="Set a driver",
+ required=True, show_create_team_link=True)
+ custom_widget(
'disclaim_maintainer', CheckBoxWidget, cssClass="subordinate")
+ def initialize(self):
+ # The JSON cache must be populated before the super call, since
+ # the form is rendered during LaunchpadFormView's initialize()
+ # when an action is invokved.
+ cache = IJSONRequestCache(self.request)
+ cache.objects['private_types'] = [
+ type.name for type in PRIVATE_INFORMATION_TYPES]
+ cache.objects['public_types'] = [
+ type.name for type in PUBLIC_INFORMATION_TYPES]
+ cache.objects['information_type_data'] = [
+ {'value': term.name, 'description': term.description,
+ 'name': term.title,
+ 'description_css_class': 'choice-description'}
+ for term in
+ self.context.getAllowedProductInformationTypes()]
+
+ super(ProjectAddStepTwo, self).initialize()
+
@property
def main_action_label(self):
if self.source_package_name is None:
@@ -2036,13 +2071,24 @@
@property
def initial_values(self):
- return {'owner': self.user.name}
+ return {
+ 'driver': self.user.name,
+ 'bug_supervisor': self.user.name,
+ 'owner': self.user.name,
+ }
def setUpFields(self):
"""See `LaunchpadFormView`."""
super(ProjectAddStepTwo, self).setUpFields()
- hidden_names = ('__visited_steps__', 'license_info')
+ hidden_names = ['__visited_steps__', 'license_info']
hidden_fields = self.form_fields.select(*hidden_names)
+
+ private_projects_flag = 'disclosure.private_projects.enabled'
+ private_projects = bool(getFeatureFlag(private_projects_flag))
+ if not private_projects or not IProductSet.providedBy(self.context):
+ hidden_names.extend([
+ 'information_type', 'bug_supervisor', 'driver'])
+
visible_fields = self.form_fields.omit(*hidden_names)
self.form_fields = (visible_fields +
self._createDisclaimMaintainerField() +
@@ -2056,7 +2102,6 @@
this checkbox and the ownership will be transfered to the registry
admins team.
"""
-
return form.Fields(
Bool(__name__='disclaim_maintainer',
title=_("I do not want to maintain this project"),
@@ -2079,10 +2124,15 @@
self.widgets['name'].hint = ('When published, '
"this will be the project's URL.")
self.widgets['displayname'].visible = False
-
self.widgets['source_package_name'].visible = False
self.widgets['distroseries'].visible = False
+ private_projects_flag = 'disclosure.private_projects.enabled'
+ private_projects = bool(getFeatureFlag(private_projects_flag))
+
+ if private_projects and IProductSet.providedBy(self.context):
+ self.widgets['information_type'].value = InformationType.PUBLIC
+
# Set the source_package_release attribute on the licenses
# widget, so that the source package's copyright info can be
# displayed.
@@ -2150,6 +2200,16 @@
for error in errors:
self.errors.remove(error)
+ private_projects_flag = 'disclosure.private_projects.enabled'
+ private_projects = bool(getFeatureFlag(private_projects_flag))
+ if private_projects:
+ if data.get('information_type') != InformationType.PUBLIC:
+ for required_field in ('bug_supervisor', 'driver'):
+ if data.get(required_field) is None:
+ self.setFieldError(
+ required_field,
+ 'Select a user or team.')
+
@property
def label(self):
"""See `LaunchpadFormView`."""
@@ -2169,6 +2229,8 @@
owner = data.get('owner')
return getUtility(IProductSet).createProduct(
registrant=self.user,
+ bug_supervisor=data.get('bug_supervisor', None),
+ driver=data.get('driver', None),
owner=owner,
name=data['name'],
displayname=data['displayname'],
=== 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-10 16:44:22 +0000
@@ -148,7 +148,8 @@
self.assertEqual('subordinate', disclaim_widget.cssClass)
self.assertEqual(
['displayname', 'name', 'title', 'summary', 'description',
- 'homepageurl', 'licenses', 'license_info', 'owner',
+ 'homepageurl', 'information_type', 'licenses', 'license_info',
+ 'driver', 'bug_supervisor', 'owner',
'__visited_steps__'],
view.view.field_names)
self.assertEqual(
=== 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-10 16:44:22 +0000
@@ -108,6 +108,7 @@
from lp.registry.enums import (
BranchSharingPolicy,
BugSharingPolicy,
+ InformationType,
)
from lp.registry.interfaces.announcement import IMakesAnnouncements
from lp.registry.interfaces.commercialsubscription import (
@@ -448,6 +449,13 @@
'and security policy will apply to this project.')),
exported_as='project_group')
+ information_type = exported(
+ Choice(
+ title=_('Information Type'), vocabulary=InformationType,
+ required=True, readonly=True,
+ description=_(
+ 'The type of of data contained in this project.')))
+
owner = exported(
PersonChoice(
title=_('Maintainer'),
@@ -966,6 +974,12 @@
returned.
"""
+ def getAllowedProductInformationTypes():
+ """Get the information types that a project can have.
+
+ :return: A sequence of `InformationType`s.
+ """
+
@call_with(owner=REQUEST_USER)
@rename_parameters_as(
displayname='display_name', project='project_group',
@@ -980,7 +994,7 @@
'downloadurl', 'freshmeatproject', 'wikiurl',
'sourceforgeproject', 'programminglang',
'project_reviewed', 'licenses', 'license_info',
- 'registrant'])
+ 'registrant', 'bug_supervisor', 'driver'])
@export_operation_as('new_project')
def createProduct(owner, name, displayname, title, summary,
description=None, project=None, homepageurl=None,
@@ -989,7 +1003,7 @@
sourceforgeproject=None, programminglang=None,
project_reviewed=False, mugshot=None, logo=None,
icon=None, licenses=None, license_info=None,
- registrant=None):
+ registrant=None, bug_supervisor=None, driver=None):
"""Create and return a brand new Product.
See `IProduct` for a description of the parameters.
=== modified file 'lib/lp/registry/interfaces/projectgroup.py'
--- lib/lp/registry/interfaces/projectgroup.py 2012-01-01 02:58:52 +0000
+++ lib/lp/registry/interfaces/projectgroup.py 2012-09-10 16:44:22 +0000
@@ -346,6 +346,12 @@
def getSeries(series_name):
"""Return a ProjectGroupSeries object with name `series_name`."""
+ def getAllowedProductInformationTypes():
+ """Get the information types that a project can have.
+
+ :return: A sequence of `InformationType`s.
+ """
+
product_milestones = Attribute('all the milestones for all the products.')
=== 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-10 16:44:22 +0000
@@ -396,6 +396,16 @@
date_next_suggest_packaging = UtcDateTimeCol(default=None)
@property
+ def information_type(self):
+ """See `IProduct`
+
+ Place holder for a db column.
+ XXX: rharding 2012-09-10 bug=1048720: Waiting on db patch to connect
+ into place.
+ """
+ pass
+
+ @property
def pillar(self):
"""See `IBugTarget`."""
return self
@@ -1550,6 +1560,12 @@
results = results.limit(num_products)
return results
+ def getAllowedProductInformationTypes(self):
+ """See `IProductSet`."""
+ return (InformationType.PUBLIC,
+ InformationType.EMBARGOED,
+ InformationType.PROPRIETARY)
+
def createProduct(self, owner, name, displayname, title, summary,
description=None, project=None, homepageurl=None,
screenshotsurl=None, wikiurl=None,
@@ -1557,7 +1573,7 @@
sourceforgeproject=None, programminglang=None,
project_reviewed=False, mugshot=None, logo=None,
icon=None, licenses=None, license_info=None,
- registrant=None):
+ registrant=None, bug_supervisor=None, driver=None):
"""See `IProductSet`."""
if registrant is None:
registrant = owner
@@ -1572,7 +1588,8 @@
sourceforgeproject=sourceforgeproject,
programminglang=programminglang,
project_reviewed=project_reviewed,
- icon=icon, logo=logo, mugshot=mugshot, license_info=license_info)
+ icon=icon, logo=logo, mugshot=mugshot, license_info=license_info,
+ bug_supervisor=bug_supervisor, driver=driver)
# Set up the sharing policies and product licence.
bug_sharing_policy_to_use = BugSharingPolicy.PUBLIC
=== 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-10 16:44:22 +0000
@@ -575,7 +575,7 @@
def new(self, name, displayname, title, homepageurl, summary,
description, owner, mugshot=None, logo=None, icon=None,
- registrant=None):
+ registrant=None, bug_supervisor=None, driver=None):
"""See `lp.registry.interfaces.projectgroup.IProjectGroupSet`."""
if registrant is None:
registrant = owner
=== modified file 'lib/lp/registry/templates/product-new.pt'
--- lib/lp/registry/templates/product-new.pt 2012-07-07 14:00:30 +0000
+++ lib/lp/registry/templates/product-new.pt 2012-09-10 16:44:22 +0000
@@ -14,8 +14,14 @@
* details widgets until the user states that the project they are
* registering is not a duplicate.
*/
-LPJS.use('node', 'lazr.effects', function(Y) {
+LPJS.use('node', 'lazr.effects', 'lp.app.choice', function(Y) {
Y.on('domready', function() {
+ // Setup the information choice widget.
+ if (Y.one('input[name="field.information_type"]')) {
+ Y.lp.app.choice.addPopupChoiceForRadioButtons(
+ 'information_type', LP.cache.information_type_data, true);
+ }
+
/* These two regexps serve slightly different purposes. The first
* finds the leftmost run of valid url characters for the autofill
* operation. The second validates the entire string, used for
=== 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-10 16:44:22 +0000
@@ -150,7 +150,7 @@
Store.of(product).invalidate()
with StormStatementRecorder() as recorder:
IHasAffiliation(product).getAffiliationBadges([person])
- self.assertThat(recorder, HasQueryCount(Equals(2)))
+ self.assertThat(recorder, HasQueryCount(Equals(5)))
def test_distro_affiliation_query_count(self):
# Only 2 business queries are expected, selects from:
Follow ups