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
1=== modified file 'lib/lp/app/javascript/choice.js'
2--- lib/lp/app/javascript/choice.js 2012-09-05 23:06:42 +0000
3+++ lib/lp/app/javascript/choice.js 2012-09-12 13:36:49 +0000
4@@ -160,6 +160,9 @@
5 * @param cfg
6 */
7 namespace.addPopupChoiceForRadioButtons = function(field_name, choices, cfg) {
8+ if (!choices) {
9+ throw 'No choices for the popup.'
10+ }
11 cfg = Y.merge(default_popup_choice_config, cfg);
12 var field_node = cfg.container.one('[name="field.' + field_name + '"]');
13 if (!Y.Lang.isValue(field_node)) {
14
15=== modified file 'lib/lp/registry/browser/product.py'
16--- lib/lp/registry/browser/product.py 2012-08-24 05:09:51 +0000
17+++ lib/lp/registry/browser/product.py 2012-09-12 13:36:49 +0000
18@@ -52,6 +52,8 @@
19
20 from lazr.delegates import delegates
21 from lazr.restful.interface import copy_field
22+from lazr.restful.interfaces import IJSONRequestCache
23+
24 import pytz
25 from z3c.ptcompat import ViewPageTemplateFile
26 from zope.app.form import CustomWidgetFactory
27@@ -114,6 +116,7 @@
28 from lp.app.widgets.itemswidgets import (
29 CheckBoxMatrixWidget,
30 LaunchpadRadioWidget,
31+ LaunchpadRadioWidgetWithDescription,
32 )
33 from lp.app.widgets.popup import PersonPickerWidget
34 from lp.app.widgets.product import (
35@@ -141,6 +144,7 @@
36 add_subscribe_link,
37 BaseRdfView,
38 )
39+from lp.services.features import getFeatureFlag
40 from lp.registry.browser.announcement import HasAnnouncementsView
41 from lp.registry.browser.branding import BrandingChangeView
42 from lp.registry.browser.menu import (
43@@ -154,6 +158,11 @@
44 PillarViewMixin,
45 )
46 from lp.registry.browser.productseries import get_series_branch_error
47+from lp.registry.enums import (
48+ InformationType,
49+ PRIVATE_INFORMATION_TYPES,
50+ PUBLIC_INFORMATION_TYPES,
51+ )
52 from lp.registry.interfaces.pillar import IPillarNameSet
53 from lp.registry.interfaces.product import (
54 IProduct,
55@@ -1986,9 +1995,9 @@
56 class ProjectAddStepTwo(StepView, ProductLicenseMixin, ReturnToReferrerMixin):
57 """Step 2 (of 2) in the +new project add wizard."""
58
59- _field_names = ['displayname', 'name', 'title', 'summary',
60- 'description', 'homepageurl', 'licenses', 'license_info',
61- 'owner',
62+ _field_names = ['displayname', 'name', 'title', 'summary', 'description',
63+ 'homepageurl', 'information_type', 'licenses',
64+ 'license_info', 'driver', 'bug_supervisor', 'owner',
65 ]
66 schema = IProduct
67 step_name = 'projectaddstep2'
68@@ -2002,12 +2011,39 @@
69 custom_widget('homepageurl', TextWidget, displayWidth=30)
70 custom_widget('licenses', LicenseWidget)
71 custom_widget('license_info', GhostWidget)
72+ custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
73+
74 custom_widget(
75 'owner', PersonPickerWidget, header="Select the maintainer",
76 show_create_team_link=True)
77 custom_widget(
78+ 'bug_supervisor', PersonPickerWidget, header="Set a bug supervisor",
79+ required=True, show_create_team_link=True)
80+ custom_widget(
81+ 'driver', PersonPickerWidget, header="Set a driver",
82+ required=True, show_create_team_link=True)
83+ custom_widget(
84 'disclaim_maintainer', CheckBoxWidget, cssClass="subordinate")
85
86+ def initialize(self):
87+ # The JSON cache must be populated before the super call, since
88+ # the form is rendered during LaunchpadFormView's initialize()
89+ # when an action is invokved.
90+ if IProductSet.providedBy(self.context):
91+ cache = IJSONRequestCache(self.request)
92+ cache.objects['private_types'] = [
93+ type.name for type in PRIVATE_INFORMATION_TYPES]
94+ cache.objects['public_types'] = [
95+ type.name for type in PUBLIC_INFORMATION_TYPES]
96+ cache.objects['information_type_data'] = [
97+ {'value': term.name, 'description': term.description,
98+ 'name': term.title,
99+ 'description_css_class': 'choice-description'}
100+ for term in
101+ self.context.getAllowedProductInformationTypes()]
102+
103+ super(ProjectAddStepTwo, self).initialize()
104+
105 @property
106 def main_action_label(self):
107 if self.source_package_name is None:
108@@ -2036,13 +2072,24 @@
109
110 @property
111 def initial_values(self):
112- return {'owner': self.user.name}
113+ return {
114+ 'driver': self.user.name,
115+ 'bug_supervisor': self.user.name,
116+ 'owner': self.user.name,
117+ }
118
119 def setUpFields(self):
120 """See `LaunchpadFormView`."""
121 super(ProjectAddStepTwo, self).setUpFields()
122- hidden_names = ('__visited_steps__', 'license_info')
123+ hidden_names = ['__visited_steps__', 'license_info']
124 hidden_fields = self.form_fields.select(*hidden_names)
125+
126+ private_projects_flag = 'disclosure.private_projects.enabled'
127+ private_projects = bool(getFeatureFlag(private_projects_flag))
128+ if not private_projects or not IProductSet.providedBy(self.context):
129+ hidden_names.extend([
130+ 'information_type', 'bug_supervisor', 'driver'])
131+
132 visible_fields = self.form_fields.omit(*hidden_names)
133 self.form_fields = (visible_fields +
134 self._createDisclaimMaintainerField() +
135@@ -2056,7 +2103,6 @@
136 this checkbox and the ownership will be transfered to the registry
137 admins team.
138 """
139-
140 return form.Fields(
141 Bool(__name__='disclaim_maintainer',
142 title=_("I do not want to maintain this project"),
143@@ -2079,10 +2125,15 @@
144 self.widgets['name'].hint = ('When published, '
145 "this will be the project's URL.")
146 self.widgets['displayname'].visible = False
147-
148 self.widgets['source_package_name'].visible = False
149 self.widgets['distroseries'].visible = False
150
151+ private_projects_flag = 'disclosure.private_projects.enabled'
152+ private_projects = bool(getFeatureFlag(private_projects_flag))
153+
154+ if private_projects and IProductSet.providedBy(self.context):
155+ self.widgets['information_type'].value = InformationType.PUBLIC
156+
157 # Set the source_package_release attribute on the licenses
158 # widget, so that the source package's copyright info can be
159 # displayed.
160@@ -2150,6 +2201,16 @@
161 for error in errors:
162 self.errors.remove(error)
163
164+ private_projects_flag = 'disclosure.private_projects.enabled'
165+ private_projects = bool(getFeatureFlag(private_projects_flag))
166+ if private_projects:
167+ if data.get('information_type') != InformationType.PUBLIC:
168+ for required_field in ('bug_supervisor', 'driver'):
169+ if data.get(required_field) is None:
170+ self.setFieldError(
171+ required_field,
172+ 'Select a user or team.')
173+
174 @property
175 def label(self):
176 """See `LaunchpadFormView`."""
177@@ -2169,6 +2230,8 @@
178 owner = data.get('owner')
179 return getUtility(IProductSet).createProduct(
180 registrant=self.user,
181+ bug_supervisor=data.get('bug_supervisor', None),
182+ driver=data.get('driver', None),
183 owner=owner,
184 name=data['name'],
185 displayname=data['displayname'],
186
187=== modified file 'lib/lp/registry/browser/tests/test_product.py'
188--- lib/lp/registry/browser/tests/test_product.py 2012-08-22 04:55:44 +0000
189+++ lib/lp/registry/browser/tests/test_product.py 2012-09-12 13:36:49 +0000
190@@ -148,7 +148,8 @@
191 self.assertEqual('subordinate', disclaim_widget.cssClass)
192 self.assertEqual(
193 ['displayname', 'name', 'title', 'summary', 'description',
194- 'homepageurl', 'licenses', 'license_info', 'owner',
195+ 'homepageurl', 'information_type', 'licenses', 'license_info',
196+ 'driver', 'bug_supervisor', 'owner',
197 '__visited_steps__'],
198 view.view.field_names)
199 self.assertEqual(
200
201=== modified file 'lib/lp/registry/interfaces/product.py'
202--- lib/lp/registry/interfaces/product.py 2012-09-03 02:18:05 +0000
203+++ lib/lp/registry/interfaces/product.py 2012-09-12 13:36:49 +0000
204@@ -108,6 +108,7 @@
205 from lp.registry.enums import (
206 BranchSharingPolicy,
207 BugSharingPolicy,
208+ InformationType,
209 )
210 from lp.registry.interfaces.announcement import IMakesAnnouncements
211 from lp.registry.interfaces.commercialsubscription import (
212@@ -448,6 +449,13 @@
213 'and security policy will apply to this project.')),
214 exported_as='project_group')
215
216+ information_type = exported(
217+ Choice(
218+ title=_('Information Type'), vocabulary=InformationType,
219+ required=True, readonly=True,
220+ description=_(
221+ 'The type of of data contained in this project.')))
222+
223 owner = exported(
224 PersonChoice(
225 title=_('Maintainer'),
226@@ -966,6 +974,12 @@
227 returned.
228 """
229
230+ def getAllowedProductInformationTypes():
231+ """Get the information types that a project can have.
232+
233+ :return: A sequence of `InformationType`s.
234+ """
235+
236 @call_with(owner=REQUEST_USER)
237 @rename_parameters_as(
238 displayname='display_name', project='project_group',
239@@ -980,7 +994,7 @@
240 'downloadurl', 'freshmeatproject', 'wikiurl',
241 'sourceforgeproject', 'programminglang',
242 'project_reviewed', 'licenses', 'license_info',
243- 'registrant'])
244+ 'registrant', 'bug_supervisor', 'driver'])
245 @export_operation_as('new_project')
246 def createProduct(owner, name, displayname, title, summary,
247 description=None, project=None, homepageurl=None,
248@@ -989,7 +1003,7 @@
249 sourceforgeproject=None, programminglang=None,
250 project_reviewed=False, mugshot=None, logo=None,
251 icon=None, licenses=None, license_info=None,
252- registrant=None):
253+ registrant=None, bug_supervisor=None, driver=None):
254 """Create and return a brand new Product.
255
256 See `IProduct` for a description of the parameters.
257
258=== modified file 'lib/lp/registry/model/product.py'
259--- lib/lp/registry/model/product.py 2012-09-06 00:01:38 +0000
260+++ lib/lp/registry/model/product.py 2012-09-12 13:36:49 +0000
261@@ -396,6 +396,16 @@
262 date_next_suggest_packaging = UtcDateTimeCol(default=None)
263
264 @property
265+ def information_type(self):
266+ """See `IProduct`
267+
268+ Place holder for a db column.
269+ XXX: rharding 2012-09-10 bug=1048720: Waiting on db patch to connect
270+ into place.
271+ """
272+ pass
273+
274+ @property
275 def pillar(self):
276 """See `IBugTarget`."""
277 return self
278@@ -1550,6 +1560,12 @@
279 results = results.limit(num_products)
280 return results
281
282+ def getAllowedProductInformationTypes(self):
283+ """See `IProductSet`."""
284+ return (InformationType.PUBLIC,
285+ InformationType.EMBARGOED,
286+ InformationType.PROPRIETARY)
287+
288 def createProduct(self, owner, name, displayname, title, summary,
289 description=None, project=None, homepageurl=None,
290 screenshotsurl=None, wikiurl=None,
291@@ -1557,7 +1573,7 @@
292 sourceforgeproject=None, programminglang=None,
293 project_reviewed=False, mugshot=None, logo=None,
294 icon=None, licenses=None, license_info=None,
295- registrant=None):
296+ registrant=None, bug_supervisor=None, driver=None):
297 """See `IProductSet`."""
298 if registrant is None:
299 registrant = owner
300@@ -1572,7 +1588,8 @@
301 sourceforgeproject=sourceforgeproject,
302 programminglang=programminglang,
303 project_reviewed=project_reviewed,
304- icon=icon, logo=logo, mugshot=mugshot, license_info=license_info)
305+ icon=icon, logo=logo, mugshot=mugshot, license_info=license_info,
306+ bug_supervisor=bug_supervisor, driver=driver)
307
308 # Set up the sharing policies and product licence.
309 bug_sharing_policy_to_use = BugSharingPolicy.PUBLIC
310
311=== modified file 'lib/lp/registry/model/projectgroup.py'
312--- lib/lp/registry/model/projectgroup.py 2012-08-03 01:42:13 +0000
313+++ lib/lp/registry/model/projectgroup.py 2012-09-12 13:36:49 +0000
314@@ -575,7 +575,7 @@
315
316 def new(self, name, displayname, title, homepageurl, summary,
317 description, owner, mugshot=None, logo=None, icon=None,
318- registrant=None):
319+ registrant=None, bug_supervisor=None, driver=None):
320 """See `lp.registry.interfaces.projectgroup.IProjectGroupSet`."""
321 if registrant is None:
322 registrant = owner
323
324=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
325--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2012-08-21 04:04:47 +0000
326+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2012-09-12 13:36:49 +0000
327@@ -168,6 +168,7 @@
328 freshmeat_project: None
329 homepage_url: None
330 icon_link: u'http://.../firefox/icon'
331+ information_type: None
332 is_permitted: True
333 license_approved: False
334 license_info: None
335
336=== modified file 'lib/lp/registry/templates/product-new.pt'
337--- lib/lp/registry/templates/product-new.pt 2012-09-10 20:52:27 +0000
338+++ lib/lp/registry/templates/product-new.pt 2012-09-12 13:36:49 +0000
339@@ -14,8 +14,14 @@
340 * details widgets until the user states that the project they are
341 * registering is not a duplicate.
342 */
343-LPJS.use('node', 'lp.ui.effects', function(Y) {
344+LPJS.use('node', 'lp.ui.effects', 'lp.app.choice', function(Y) {
345 Y.on('domready', function() {
346+ // Setup the information choice widget.
347+ if (Y.one('input[name="field.information_type"]')) {
348+ Y.lp.app.choice.addPopupChoiceForRadioButtons(
349+ 'information_type', LP.cache.information_type_data, true);
350+ }
351+
352 /* These two regexps serve slightly different purposes. The first
353 * finds the leftmost run of valid url characters for the autofill
354 * operation. The second validates the entire string, used for
355
356=== modified file 'lib/lp/registry/tests/test_pillaraffiliation.py'
357--- lib/lp/registry/tests/test_pillaraffiliation.py 2012-08-21 04:04:47 +0000
358+++ lib/lp/registry/tests/test_pillaraffiliation.py 2012-09-12 13:36:49 +0000
359@@ -150,7 +150,7 @@
360 Store.of(product).invalidate()
361 with StormStatementRecorder() as recorder:
362 IHasAffiliation(product).getAffiliationBadges([person])
363- self.assertThat(recorder, HasQueryCount(Equals(2)))
364+ self.assertThat(recorder, HasQueryCount(Equals(5)))
365
366 def test_distro_affiliation_query_count(self):
367 # Only 2 business queries are expected, selects from: