Merge lp:~wallyworld/launchpad/subscription-policy-text-912159 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 14687
Proposed branch: lp:~wallyworld/launchpad/subscription-policy-text-912159
Merge into: lp:launchpad
Diff against target: 144 lines (+54/-4)
5 files modified
lib/lp/app/widgets/doc/launchpad-radio-widget.txt (+13/-0)
lib/lp/app/widgets/itemswidgets.py (+17/-2)
lib/lp/app/widgets/tests/test_itemswidgets.py (+9/-0)
lib/lp/registry/browser/team.py (+9/-2)
lib/lp/registry/browser/tests/test_team_view.py (+6/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/subscription-policy-text-912159
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+88808@code.launchpad.net

Commit message

[r=sinzui][bug=912159] Add informative text to team edit page explaining why subscription policy choices are limited.

Description of the change

== Implementation ==

The tal template uses a form macro to render all the widgets, including the subscription policy radio buttons, so I decided to extend the LaunchpadRadioWidgetWithDescription widget to allow an optional context specific hint to be rendered. This is done by setting optional attributes on the widget:
- extra_hint
- extra_hint_class

I could have simply added text to the widget's hint text. However, I feel the extra information required here is different to the standard (static) widget hint attribute which is derived from the form field definition. The extra hint value is designed to provide the user with more than just text describing what a field does. It needs to convey info about the available choices and will not be static.

The text for the extra hint comes from the message in the exception raised when checking what vocab to use for the widget.

== Demo and QA ==

See screenshot:
http://people.canonical.com/~ianb/team-subscription-policy-hint.png

I'm not sure if the extra hint text should be rendered the same lighter grey as the other text - opinions welcome.

== Tests ==

Tests were written for the new LaunchpadRadioWidgetWithDescription functionality.
Add to doc test:
- launchpad-radio-widget.txt

Add new test case to TestLaunchpadRadioWidgetWithDescription:
- test_renderExtraHint

Extend the TestTeamEditView tests to check the extra hint attributes have been set (or not) on the widget:
- _test_edit_team_view_expected_subscription_vocab
- test_edit_team_view_data

== Lint ==
Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/widgets/itemswidgets.py
  lib/lp/app/widgets/doc/launchpad-radio-widget.txt
  lib/lp/app/widgets/tests/test_itemswidgets.py
  lib/lp/registry/browser/team.py
  lib/lp/registry/browser/tests/test_team_view.py

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

I like the implementation (I really like the reuse of the exception). Why did you define the 'inline-informational' class? It looks like "sprite info"...sprite is inline and info is informational. Did you try that and was the layout broken?

review: Needs Information (code + ui)
Revision history for this message
Curtis Hovey (sinzui) wrote :

We talked about this and your can land it as is. You can try "sprite info" and if it is 100% compatible the CSS addition can be reverted.

review: Approve (code)
Revision history for this message
Ian Booth (wallyworld) wrote :

"sprite info" works great, thanks for pointing it out.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/app/widgets/doc/launchpad-radio-widget.txt'
--- lib/lp/app/widgets/doc/launchpad-radio-widget.txt 2011-12-24 17:49:30 +0000
+++ lib/lp/app/widgets/doc/launchpad-radio-widget.txt 2012-01-18 00:19:25 +0000
@@ -124,6 +124,19 @@
124 </table>124 </table>
125 <input name="field.branch_type-empty-marker" type="hidden" value="1" />125 <input name="field.branch_type-empty-marker" type="hidden" value="1" />
126126
127Sometimes, it is desirable to display to the user additional, context specific
128information to explain the choices available for selection. This can be done
129by setting the optional extra_hint and extra_hint_class attributes on the
130widget.
131 >>> radio_widget.extra_hint = 'Some additional information'
132 >>> radio_widget.extra_hint_class = 'inline-informational'
133 >>> print radio_widget()
134 <div class="inline-informational">Some additional information</div>
135 <table class="radio-button-widget"><tr>
136 ...
137 </table>
138 <input name="field.branch_type-empty-marker" type="hidden" value="1" />
139
127140
128LaunchpadBooleanRadioWidget141LaunchpadBooleanRadioWidget
129---------------------------142---------------------------
130143
=== modified file 'lib/lp/app/widgets/itemswidgets.py'
--- lib/lp/app/widgets/itemswidgets.py 2011-12-24 17:49:30 +0000
+++ lib/lp/app/widgets/itemswidgets.py 2012-01-18 00:19:25 +0000
@@ -152,6 +152,8 @@
152 'The vocabulary must implement IEnumeratedType')152 'The vocabulary must implement IEnumeratedType')
153 super(LaunchpadRadioWidgetWithDescription, self).__init__(153 super(LaunchpadRadioWidgetWithDescription, self).__init__(
154 field, vocabulary, request)154 field, vocabulary, request)
155 self.extra_hint = None
156 self.extra_hint_class = None
155157
156 def _renderRow(self, text, form_value, id, elem):158 def _renderRow(self, text, form_value, id, elem):
157 """Render the table row for the widget depending on description."""159 """Render the table row for the widget depending on description."""
@@ -192,12 +194,25 @@
192 type='radio')194 type='radio')
193 return self._renderRow(text, value, id, elem)195 return self._renderRow(text, value, id, elem)
194196
197 def renderExtraHint(self):
198 extra_hint_html = ''
199 extra_hint_class = ''
200 if self.extra_hint_class:
201 extra_hint_class = ' class="%s"' % self.extra_hint_class
202 if self.extra_hint:
203 extra_hint_html = ('<div%s>%s</div>'
204 % (extra_hint_class, escape(self.extra_hint)))
205 return extra_hint_html
206
195 def renderValue(self, value):207 def renderValue(self, value):
196 # Render the items in a table to align the descriptions.208 # Render the items in a table to align the descriptions.
197 rendered_items = self.renderItems(value)209 rendered_items = self.renderItems(value)
210 extra_hint = self.renderExtraHint()
198 return (211 return (
199 '<table class="radio-button-widget">%s</table>'212 '%(extra_hint)s\n'
200 % ''.join(rendered_items))213 '<table class="radio-button-widget">%(items)s</table>'
214 % {'extra_hint': extra_hint,
215 'items': ''.join(rendered_items)})
201216
202217
203class LaunchpadBooleanRadioWidget(LaunchpadRadioWidget):218class LaunchpadBooleanRadioWidget(LaunchpadRadioWidget):
204219
=== modified file 'lib/lp/app/widgets/tests/test_itemswidgets.py'
--- lib/lp/app/widgets/tests/test_itemswidgets.py 2012-01-01 02:58:52 +0000
+++ lib/lp/app/widgets/tests/test_itemswidgets.py 2012-01-18 00:19:25 +0000
@@ -203,3 +203,12 @@
203 '<...>item-&lt;2&gt;<...>&lt;unsafe&gt; &amp;nbsp; title<...>')203 '<...>item-&lt;2&gt;<...>&lt;unsafe&gt; &amp;nbsp; title<...>')
204 self.assertRenderItem(204 self.assertRenderItem(
205 expected, self.widget.renderItem, self.TestEnum.UNSAFE_TERM)205 expected, self.widget.renderItem, self.TestEnum.UNSAFE_TERM)
206
207 def test_renderExtraHint(self):
208 # If an extra hint is specified, it is rendered.
209 self.widget.extra_hint = "Hello World"
210 self.widget.extra_hint_class = 'hint_class'
211 expected = (
212 '<div class="hint_class">Hello World</div>')
213 hint_html = self.widget.renderExtraHint()
214 self.assertEqual(expected, hint_html)
206215
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2012-01-05 21:53:52 +0000
+++ lib/lp/registry/browser/team.py 2012-01-18 00:19:25 +0000
@@ -303,7 +303,7 @@
303 # Do we need to only show open subscription policy choices?303 # Do we need to only show open subscription policy choices?
304 try:304 try:
305 team.checkClosedSubscriptionPolicyAllowed()305 team.checkClosedSubscriptionPolicyAllowed()
306 except TeamSubscriptionPolicyError:306 except TeamSubscriptionPolicyError as e:
307 # Ideally SimpleVocabulary.fromItems() would accept 3-tuples but307 # Ideally SimpleVocabulary.fromItems() would accept 3-tuples but
308 # it doesn't so we need to be a bit more verbose.308 # it doesn't so we need to be a bit more verbose.
309 self.widgets['subscriptionpolicy'].vocabulary = (309 self.widgets['subscriptionpolicy'].vocabulary = (
@@ -311,10 +311,14 @@
311 policy, policy.name, policy.title)311 policy, policy.name, policy.title)
312 for policy in OPEN_TEAM_POLICY])312 for policy in OPEN_TEAM_POLICY])
313 )313 )
314 self.widgets['subscriptionpolicy'].extra_hint_class = (
315 'sprite info')
316 self.widgets['subscriptionpolicy'].extra_hint = e.message
317
314 # Do we need to only show closed subscription policy choices?318 # Do we need to only show closed subscription policy choices?
315 try:319 try:
316 team.checkOpenSubscriptionPolicyAllowed()320 team.checkOpenSubscriptionPolicyAllowed()
317 except TeamSubscriptionPolicyError:321 except TeamSubscriptionPolicyError as e:
318 # Ideally SimpleVocabulary.fromItems() would accept 3-tuples but322 # Ideally SimpleVocabulary.fromItems() would accept 3-tuples but
319 # it doesn't so we need to be a bit more verbose.323 # it doesn't so we need to be a bit more verbose.
320 self.widgets['subscriptionpolicy'].vocabulary = (324 self.widgets['subscriptionpolicy'].vocabulary = (
@@ -322,6 +326,9 @@
322 policy, policy.name, policy.title)326 policy, policy.name, policy.title)
323 for policy in CLOSED_TEAM_POLICY])327 for policy in CLOSED_TEAM_POLICY])
324 )328 )
329 self.widgets['subscriptionpolicy'].extra_hint_class = (
330 'sprite info')
331 self.widgets['subscriptionpolicy'].extra_hint = e.message
325332
326 @action('Save', name='save')333 @action('Save', name='save')
327 def action_save(self, action, data):334 def action_save(self, action, data):
328335
=== modified file 'lib/lp/registry/browser/tests/test_team_view.py'
--- lib/lp/registry/browser/tests/test_team_view.py 2012-01-01 02:58:52 +0000
+++ lib/lp/registry/browser/tests/test_team_view.py 2012-01-18 00:19:25 +0000
@@ -266,6 +266,7 @@
266 self.assertEqual(266 self.assertEqual(
267 TeamSubscriptionPolicy,267 TeamSubscriptionPolicy,
268 view.widgets['subscriptionpolicy'].vocabulary)268 view.widgets['subscriptionpolicy'].vocabulary)
269 self.assertIsNone(view.widgets['subscriptionpolicy'].extra_hint)
269 self.assertEqual(270 self.assertEqual(
270 TeamMembershipRenewalPolicy.NONE,271 TeamMembershipRenewalPolicy.NONE,
271 view.widgets['renewal_policy']._data)272 view.widgets['renewal_policy']._data)
@@ -286,6 +287,11 @@
286 expected_items,287 expected_items,
287 [term.value288 [term.value
288 for term in view.widgets['subscriptionpolicy'].vocabulary])289 for term in view.widgets['subscriptionpolicy'].vocabulary])
290 self.assertEqual(
291 'sprite info',
292 view.widgets['subscriptionpolicy'].extra_hint_class)
293 self.assertIsNotNone(
294 view.widgets['subscriptionpolicy'].extra_hint)
289295
290 def test_edit_team_view_pillar_owner(self):296 def test_edit_team_view_pillar_owner(self):
291 # The edit view renders only closed subscription policy choices when297 # The edit view renders only closed subscription policy choices when