Merge lp:~stevenk/launchpad/branch-information_type-ui into lp:launchpad

Proposed by Steve Kowalik on 2012-05-30
Status: Merged
Approved by: Steve Kowalik on 2012-05-30
Approved revision: no longer in the source branch.
Merged at revision: 15327
Proposed branch: lp:~stevenk/launchpad/branch-information_type-ui
Merge into: lp:launchpad
Diff against target: 505 lines (+224/-79)
7 files modified
lib/lp/app/browser/informationtype.py (+61/-0)
lib/lp/bugs/browser/bug.py (+2/-42)
lib/lp/code/browser/branch.py (+65/-32)
lib/lp/code/browser/configure.zcml (+3/-5)
lib/lp/code/browser/tests/test_branch.py (+81/-0)
lib/lp/code/templates/branch-portlet-privacy.pt (+6/-0)
lib/lp/services/features/flags.py (+6/-0)
To merge this branch: bzr merge lp:~stevenk/launchpad/branch-information_type-ui
Reviewer Review Type Date Requested Status
William Grant code 2012-05-30 Approve on 2012-05-30
Review via email: mp+107909@code.launchpad.net

Commit Message

Show the information_type in the branch UI if a feature flag is set.

Description of the Change

In moving towards the death march of having branch use information_type, show it in the branch UI.

Add a new feature flag, show_information_type_in_branch_ui, because I was short-sighted enough to not put bugs_ in the last feature flag. Refactor the information_type portlet methods out from BugView into a separate module and make use of it in both places. I've added new tests for the UI with the feature flag enabled, and have left existing tests alone.

To post a comment you must log in.
William Grant (wgrant) wrote :

39 + @property
40 + def show_information_type_in_ui(self):
41 + feature_flag_base = 'disclosure.show_information_type_in_ui.enabled'
42 + if IBug.providedBy(self.context):
43 + pass
44 + elif IBranch.providedBy(self.context):
45 + feature_flag_base = feature_flag_base.replace('ui', 'branch_ui')
46 + else:
47 + raise NotImplemented()
48 + return bool(getFeatureFlag(feature_flag_base))

Since InformationTypePortlet isn't used as its own view (always as a mixin for an edit form class), this horror can probably die or be replaced with "raise NotImplementedError()". It should also possibly be renamed to InformationTypePortletMixin, and the module name should be informationtype, not information_type.

60 + if (
61 + self.context.information_type == InformationType.USERDATA and
62 + self.show_userdata_as_private):

Why is that extra newline there?

if (self.context.information_type == InformationType.USERDATA
    and self.show_userdata_as_private):

71 + if (
72 + self.context.information_type == InformationType.USERDATA and
73 + self.show_userdata_as_private):

Likewise.

107 def initialize(self):
108 super(BugView, self).initialize()

These can be deleted, as the method looks empty apart from that super() call.

433 +class TestBranchPrivacyPortlet(TestCaseWithFactory):

That's a bit odd. Is there really nowhere else that tests the portlet?

485 + This branch contains <strong id="information-type" tal:content="view/information_type"></strong> information&nbsp;
486 + <div id='information-type-description' style='padding-top: 5px' tal:content="view/information_type_description"></div>

The nbsp before a new div doesn't strike me as particularly useful. Do you know why it's there?

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/app/browser/informationtype.py'
2--- lib/lp/app/browser/informationtype.py 1970-01-01 00:00:00 +0000
3+++ lib/lp/app/browser/informationtype.py 2012-05-30 05:10:23 +0000
4@@ -0,0 +1,61 @@
5+# Copyright 2012 Canonical Ltd. This software is licensed under the
6+# GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+__metaclass__ = type
9+__all__ = [
10+ 'InformationTypePortletMixin',
11+ ]
12+
13+from lazr.restful.interfaces import IJSONRequestCache
14+
15+from lp.registry.enums import (
16+ InformationType,
17+ PRIVATE_INFORMATION_TYPES,
18+ )
19+from lp.registry.vocabularies import InformationTypeVocabulary
20+from lp.services.features import getFeatureFlag
21+
22+
23+class InformationTypePortletMixin:
24+
25+ def initialize(self):
26+ cache = IJSONRequestCache(self.request)
27+ cache.objects['information_types'] = [
28+ {'value': term.value, 'description': term.description,
29+ 'name': term.title,
30+ 'description_css_class': 'choice-description'}
31+ for term in InformationTypeVocabulary()]
32+ cache.objects['private_types'] = [
33+ type.title for type in PRIVATE_INFORMATION_TYPES]
34+ cache.objects['show_information_type_in_ui'] = (
35+ self.show_information_type_in_ui)
36+
37+ @property
38+ def show_information_type_in_ui(self):
39+ raise NotImplementedError()
40+
41+ @property
42+ def show_userdata_as_private(self):
43+ return bool(getFeatureFlag(
44+ 'disclosure.display_userdata_as_private.enabled'))
45+
46+ @property
47+ def information_type(self):
48+ # This can be replaced with just a return when the feature flag is
49+ # dropped.
50+ title = self.context.information_type.title
51+ if (self.context.information_type == InformationType.USERDATA and
52+ self.show_userdata_as_private):
53+ return 'Private'
54+ return title
55+
56+ @property
57+ def information_type_description(self):
58+ # This can be replaced with just a return when the feature flag is
59+ # dropped.
60+ description = self.context.information_type.description
61+ if (self.context.information_type == InformationType.USERDATA and
62+ self.show_userdata_as_private):
63+ description = (
64+ description.replace('user data', 'private information'))
65+ return description
66
67=== modified file 'lib/lp/bugs/browser/bug.py'
68--- lib/lp/bugs/browser/bug.py 2012-05-17 09:45:29 +0000
69+++ lib/lp/bugs/browser/bug.py 2012-05-30 05:10:23 +0000
70@@ -55,6 +55,7 @@
71 from zope.security.interfaces import Unauthorized
72
73 from lp import _
74+from lp.app.browser.informationtype import InformationTypePortletMixin
75 from lp.app.browser.launchpadform import (
76 action,
77 custom_widget,
78@@ -518,7 +519,7 @@
79 return getUtility(ILaunchBag).bugtask
80
81
82-class BugView(LaunchpadView, BugViewMixin):
83+class BugView(InformationTypePortletMixin, LaunchpadView, BugViewMixin):
84 """View class for presenting information about an `IBug`.
85
86 Since all bug pages are registered on IBugTask, the context will be
87@@ -535,19 +536,6 @@
88 return bool(getFeatureFlag(
89 'disclosure.show_information_type_in_ui.enabled'))
90
91- def initialize(self):
92- super(BugView, self).initialize()
93- cache = IJSONRequestCache(self.request)
94- cache.objects['information_types'] = [
95- {'value': term.value, 'description': term.description,
96- 'name': term.title,
97- 'description_css_class': 'choice-description'}
98- for term in InformationTypeVocabulary()]
99- cache.objects['private_types'] = [
100- type.title for type in PRIVATE_INFORMATION_TYPES]
101- cache.objects['show_information_type_in_ui'] = (
102- self.show_information_type_in_ui)
103-
104 @cachedproperty
105 def page_description(self):
106 return IBug(self.context).description
107@@ -596,34 +584,6 @@
108 return ProxiedLibraryFileAlias(
109 attachment.libraryfile, attachment).http_url
110
111- @property
112- def information_type(self):
113- # This can be replaced with just a return when the feature flag is
114- # dropped.
115- title = self.context.information_type.title
116- show_userdata_as_private = bool(getFeatureFlag(
117- 'disclosure.display_userdata_as_private.enabled'))
118- if (
119- self.context.information_type == InformationType.USERDATA and
120- show_userdata_as_private):
121- return 'Private'
122- return title
123-
124- @property
125- def information_type_description(self):
126- # This can be replaced with just a return when the feature flag is
127- # dropped.
128- description = self.context.information_type.description
129- show_userdata_as_private = bool(getFeatureFlag(
130- 'disclosure.display_userdata_as_private.enabled'))
131- if (
132- self.context.information_type == InformationType.USERDATA and
133- show_userdata_as_private):
134- description = (
135- description.replace('user data', 'private information'))
136-
137- return description
138-
139
140 class BugActivity(BugView):
141
142
143=== modified file 'lib/lp/code/browser/branch.py'
144--- lib/lp/code/browser/branch.py 2012-03-27 04:36:24 +0000
145+++ lib/lp/code/browser/branch.py 2012-05-30 05:10:23 +0000
146@@ -75,6 +75,7 @@
147 from zope.traversing.interfaces import IPathAdapter
148
149 from lp import _
150+from lp.app.browser.informationtype import InformationTypePortletMixin
151 from lp.app.browser.launchpad import Hierarchy
152 from lp.app.browser.launchpadform import (
153 action,
154@@ -123,10 +124,14 @@
155 from lp.code.interfaces.codereviewvote import ICodeReviewVoteReference
156 from lp.registry.interfaces.person import IPersonSet
157 from lp.registry.interfaces.productseries import IProductSeries
158-from lp.registry.vocabularies import UserTeamsParticipationPlusSelfVocabulary
159+from lp.registry.vocabularies import (
160+ InformationTypeVocabulary,
161+ UserTeamsParticipationPlusSelfVocabulary,
162+ )
163 from lp.services import searchbuilder
164 from lp.services.config import config
165 from lp.services.database.constants import UTC_NOW
166+from lp.services.features import getFeatureFlag
167 from lp.services.feeds.browser import (
168 BranchFeedLink,
169 FeedsMixin,
170@@ -425,7 +430,8 @@
171 return branch.url
172
173
174-class BranchView(LaunchpadView, FeedsMixin, BranchMirrorMixin):
175+class BranchView(LaunchpadView, FeedsMixin, BranchMirrorMixin,
176+ InformationTypePortletMixin):
177
178 feed_types = (
179 BranchFeedLink,
180@@ -437,7 +443,13 @@
181
182 label = page_title
183
184+ @property
185+ def show_information_type_in_ui(self):
186+ return bool(getFeatureFlag(
187+ 'disclosure.show_information_type_in_branch_ui.enabled'))
188+
189 def initialize(self):
190+ super(BranchView, self).initialize()
191 self.branch = self.context
192 self.notices = []
193 # Cache permission so private team owner can be rendered.
194@@ -706,33 +718,38 @@
195 self.setFieldError(field_name, structured(message))
196
197
198-class BranchEditSchema(Interface):
199- """Defines the fields for the edit form.
200-
201- This is necessary so as to make an editable field for the branch privacy.
202- Normally the field is not editable through the interface in order to stop
203- direct setting of the private attribute, but in this case we actually want
204- the user to be able to edit it.
205- """
206- use_template(IBranch, include=[
207- 'name',
208- 'url',
209- 'description',
210- 'lifecycle_status',
211- 'whiteboard',
212- ])
213- explicitly_private = copy_field(
214- IBranch['explicitly_private'], readonly=False)
215- reviewer = copy_field(IBranch['reviewer'], required=True)
216- owner = copy_field(IBranch['owner'], readonly=False)
217-
218-
219 class BranchEditFormView(LaunchpadEditFormView):
220 """Base class for forms that edit a branch."""
221
222- schema = BranchEditSchema
223 field_names = None
224
225+ @cachedproperty
226+ def schema(self):
227+ class BranchEditSchema(Interface):
228+ """Defines the fields for the edit form.
229+
230+ This is necessary so as to make an editable field for the
231+ branch privacy. Normally the field is not editable through
232+ the interface in order to stop direct setting of the private
233+ attribute, but in this case we actually want the user to be
234+ able to edit it.
235+ """
236+ use_template(IBranch, include=[
237+ 'name',
238+ 'url',
239+ 'description',
240+ 'lifecycle_status',
241+ 'whiteboard',
242+ ])
243+ information_type = copy_field(
244+ IBranch['information_type'], readonly=False,
245+ vocabulary=InformationTypeVocabulary())
246+ explicitly_private = copy_field(
247+ IBranch['explicitly_private'], readonly=False)
248+ reviewer = copy_field(IBranch['reviewer'], required=True)
249+ owner = copy_field(IBranch['owner'], readonly=False)
250+ return BranchEditSchema
251+
252 @property
253 def page_title(self):
254 return 'Edit %s' % self.context.displayname
255@@ -744,7 +761,7 @@
256 @property
257 def adapters(self):
258 """See `LaunchpadFormView`"""
259- return {BranchEditSchema: self.context}
260+ return {self.schema: self.context}
261
262 @action('Change Branch', name='change')
263 def change_action(self, action, data):
264@@ -767,6 +784,10 @@
265 if 'private' in data:
266 # Read only for display.
267 data.pop('private')
268+ if 'information_type' in data:
269+ information_type = data.pop('information_type')
270+ self.context.transitionToInformationType(
271+ information_type, self.user)
272 if 'explicitly_private' in data:
273 private = data.pop('explicitly_private')
274 if (private != self.context.private
275@@ -1014,16 +1035,27 @@
276
277
278 class BranchEditView(BranchEditFormView, BranchNameValidationMixin):
279- """The main branch view for editing the branch attributes."""
280-
281- field_names = [
282- 'owner', 'name', 'explicitly_private', 'url', 'description',
283- 'lifecycle_status']
284+ """The main branch for editing the branch attributes."""
285+
286+ @property
287+ def show_information_type_in_ui(self):
288+ return bool(getFeatureFlag(
289+ 'disclosure.show_information_type_in_branch_ui.enabled'))
290+
291+ @property
292+ def field_names(self):
293+ fields = [
294+ 'owner', 'name', 'explicitly_private', 'url', 'description',
295+ 'lifecycle_status']
296+ if self.show_information_type_in_ui:
297+ fields[2] = 'information_type'
298+ return fields
299
300 custom_widget('lifecycle_status', LaunchpadRadioWidgetWithDescription)
301+ custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
302
303 def setUpFields(self):
304- LaunchpadFormView.setUpFields(self)
305+ super(BranchEditView, self).setUpFields()
306 # This is to prevent users from converting push/import
307 # branches to pull branches.
308 branch = self.context
309@@ -1061,7 +1093,8 @@
310 user_has_special_branch_access(self.user))
311
312 if not show_private_field:
313- self.form_fields = self.form_fields.omit('explicitly_private')
314+ self.form_fields = self.form_fields.omit(
315+ 'explicitly_private', 'information_type')
316
317 # If the user can administer branches, then they should be able to
318 # assign the ownership of the branch to any valid person or team.
319
320=== modified file 'lib/lp/code/browser/configure.zcml'
321--- lib/lp/code/browser/configure.zcml 2012-03-07 00:58:37 +0000
322+++ lib/lp/code/browser/configure.zcml 2012-05-30 05:10:23 +0000
323@@ -390,9 +390,11 @@
324 name="+index"
325 template="../templates/branch-index.pt"/>
326 <browser:page
327+ name="+portlet-privacy"
328+ template="../templates/branch-portlet-privacy.pt"/>
329+ <browser:page
330 name="++bug-links"
331 template="../templates/branch-bug-links.pt"/>
332-
333 <browser:page
334 name="++branch-information"
335 template="../templates/branch-information.pt" />
336@@ -417,16 +419,12 @@
337 <browser:page
338 name="++branch-related-bugs-specs"
339 template="../templates/branch-related-bugs-specs.pt" />
340-
341 </browser:pages>
342 <browser:pages
343 for="lp.code.interfaces.branch.IBranch"
344 layer="lp.code.publisher.CodeLayer"
345 permission="zope.Public">
346 <browser:page
347- name="+portlet-privacy"
348- template="../templates/branch-portlet-privacy.pt"/>
349- <browser:page
350 name="+heading"
351 template="../templates/branch-heading.pt"/>
352 </browser:pages>
353
354=== modified file 'lib/lp/code/browser/tests/test_branch.py'
355--- lib/lp/code/browser/tests/test_branch.py 2012-05-24 02:16:59 +0000
356+++ lib/lp/code/browser/tests/test_branch.py 2012-05-30 05:10:23 +0000
357@@ -10,10 +10,12 @@
358
359 from BeautifulSoup import BeautifulSoup
360 import pytz
361+from zope.component import getUtility
362 from zope.publisher.interfaces import NotFound
363 from zope.security.proxy import removeSecurityProxy
364
365 from lp.app.interfaces.headings import IRootContext
366+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
367 from lp.bugs.interfaces.bugtask import (
368 BugTaskStatus,
369 UNRESOLVED_BUGTASK_STATUSES,
370@@ -37,6 +39,7 @@
371 from lp.registry.interfaces.person import PersonVisibility
372 from lp.services.config import config
373 from lp.services.database.constants import UTC_NOW
374+from lp.services.features.testing import FeatureFixture
375 from lp.services.helpers import truncate_text
376 from lp.services.webapp.publisher import canonical_url
377 from lp.services.webapp.servers import LaunchpadTestRequest
378@@ -897,6 +900,39 @@
379 with person_logged_in(person):
380 self.assertEquals(team, branch.owner)
381
382+ def test_information_type_in_ui(self):
383+ # The information_type of a branch can be changed via the UI when
384+ # the feature flag is enabled.
385+ person = self.factory.makePerson()
386+ branch = self.factory.makeProductBranch(owner=person)
387+ feature_flag = {
388+ 'disclosure.show_information_type_in_branch_ui.enabled': 'on'}
389+ admins = getUtility(ILaunchpadCelebrities).admin
390+ admin = admins.teamowner
391+ with FeatureFixture(feature_flag):
392+ browser = self.getUserBrowser(
393+ canonical_url(branch) + '/+edit', user=admin)
394+ browser.getControl("Embargoed Security").click()
395+ browser.getControl("Change Branch").click()
396+ with person_logged_in(person):
397+ self.assertEqual(
398+ InformationType.EMBARGOEDSECURITY, branch.information_type)
399+
400+ def test_information_type_in_ui_vocabulary(self):
401+ # The vocabulary that Branch:+edit uses for the information_type
402+ # has been correctly created.
403+ person = self.factory.makePerson()
404+ branch = self.factory.makeProductBranch(owner=person)
405+ feature_flags = {
406+ 'disclosure.show_information_type_in_branch_ui.enabled': 'on',
407+ 'disclosure.proprietary_information_type.disabled': 'on'}
408+ admins = getUtility(ILaunchpadCelebrities).admin
409+ admin = admins.teamowner
410+ with FeatureFixture(feature_flags):
411+ browser = self.getUserBrowser(
412+ canonical_url(branch) + '/+edit', user=admin)
413+ self.assertRaises(LookupError, browser.getControl, "Proprietary")
414+
415
416 class TestBranchUpgradeView(TestCaseWithFactory):
417
418@@ -916,3 +952,48 @@
419 self.assertEqual(
420 'An upgrade is already in progress for branch %s.' %
421 branch.bzr_identity, view.request.notifications[0].message)
422+
423+
424+class TestBranchPrivacyPortlet(TestCaseWithFactory):
425+
426+ layer = LaunchpadFunctionalLayer
427+
428+ def test_information_type_in_ui(self):
429+ # With the show_information_type_in_branch_ui feature flag on, the
430+ # privacy portlet shows the information_type.
431+ owner = self.factory.makePerson()
432+ branch = self.factory.makeBranch(private=True, owner=owner)
433+ feature_flag = {
434+ 'disclosure.show_information_type_in_branch_ui.enabled': 'on'}
435+ with FeatureFixture(feature_flag):
436+ with person_logged_in(owner):
437+ view = create_initialized_view(branch, '+portlet-privacy')
438+ soup = BeautifulSoup(view.render())
439+ information_type = soup.find('strong')
440+ description = soup.find('div', id='information-type-description')
441+ self.assertEqual('User Data', information_type.renderContents())
442+ self.assertEqual(
443+ 'Visible only to users with whom the project has shared '
444+ 'information\ncontaining user data.\n',
445+ description.renderContents())
446+
447+ def test_information_type_in_ui_with_display_as_private(self):
448+ # With both show_information_type_in_branch_ui and
449+ # display_userdata_as_private, the information_type is shown with
450+ # User Data masked as Private.
451+ owner = self.factory.makePerson()
452+ branch = self.factory.makeBranch(private=True, owner=owner)
453+ feature_flags = {
454+ 'disclosure.show_information_type_in_branch_ui.enabled': 'on',
455+ 'disclosure.display_userdata_as_private.enabled': 'on'}
456+ with FeatureFixture(feature_flags):
457+ with person_logged_in(owner):
458+ view = create_initialized_view(branch, '+portlet-privacy')
459+ soup = BeautifulSoup(view.render())
460+ information_type = soup.find('strong')
461+ description = soup.find('div', id='information-type-description')
462+ self.assertEqual('Private', information_type.renderContents())
463+ self.assertEqual(
464+ 'Visible only to users with whom the project has shared '
465+ 'information\ncontaining private information.\n',
466+ description.renderContents())
467
468=== modified file 'lib/lp/code/templates/branch-portlet-privacy.pt'
469--- lib/lp/code/templates/branch-portlet-privacy.pt 2011-08-24 08:57:25 +0000
470+++ lib/lp/code/templates/branch-portlet-privacy.pt 2012-05-30 05:10:23 +0000
471@@ -7,6 +7,11 @@
472 class python: path('context/private') and 'portlet private' or 'portlet public'
473 "
474 >
475+ <tal:information_type tal:condition="view/show_information_type_in_ui">
476+ This branch contains <strong id="information-type" tal:content="view/information_type"></strong> information
477+ <div id='information-type-description' style='padding-top: 5px' tal:content="view/information_type_description"></div>
478+ </tal:information_type>
479+ <tal:privacy tal:condition="not:view/show_information_type_in_ui">
480 <div tal:condition="not:context/private"
481 >This branch is public</div>
482 <div tal:condition="context/private">
483@@ -15,4 +20,5 @@
484 because it is stacked on a private branch.
485 </tal:msg>
486 </div>
487+ </tal:privacy>
488 </div>
489
490=== modified file 'lib/lp/services/features/flags.py'
491--- lib/lp/services/features/flags.py 2012-05-18 05:31:54 +0000
492+++ lib/lp/services/features/flags.py 2012-05-30 05:10:23 +0000
493@@ -320,6 +320,12 @@
494 '',
495 '',
496 ''),
497+ ('disclosure.show_information_type_in_branch_ui.enabled',
498+ 'boolean',
499+ 'If true, displays the information_type directly on branch pages.',
500+ '',
501+ '',
502+ ''),
503 ])
504
505 # The set of all flag names that are documented.