← Back to team overview

launchpad-reviewers team mailing list archive

[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