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
1=== modified file 'lib/lp/app/widgets/doc/launchpad-radio-widget.txt'
2--- lib/lp/app/widgets/doc/launchpad-radio-widget.txt 2011-12-24 17:49:30 +0000
3+++ lib/lp/app/widgets/doc/launchpad-radio-widget.txt 2012-01-18 00:19:25 +0000
4@@ -124,6 +124,19 @@
5 </table>
6 <input name="field.branch_type-empty-marker" type="hidden" value="1" />
7
8+Sometimes, it is desirable to display to the user additional, context specific
9+information to explain the choices available for selection. This can be done
10+by setting the optional extra_hint and extra_hint_class attributes on the
11+widget.
12+ >>> radio_widget.extra_hint = 'Some additional information'
13+ >>> radio_widget.extra_hint_class = 'inline-informational'
14+ >>> print radio_widget()
15+ <div class="inline-informational">Some additional information</div>
16+ <table class="radio-button-widget"><tr>
17+ ...
18+ </table>
19+ <input name="field.branch_type-empty-marker" type="hidden" value="1" />
20+
21
22 LaunchpadBooleanRadioWidget
23 ---------------------------
24
25=== modified file 'lib/lp/app/widgets/itemswidgets.py'
26--- lib/lp/app/widgets/itemswidgets.py 2011-12-24 17:49:30 +0000
27+++ lib/lp/app/widgets/itemswidgets.py 2012-01-18 00:19:25 +0000
28@@ -152,6 +152,8 @@
29 'The vocabulary must implement IEnumeratedType')
30 super(LaunchpadRadioWidgetWithDescription, self).__init__(
31 field, vocabulary, request)
32+ self.extra_hint = None
33+ self.extra_hint_class = None
34
35 def _renderRow(self, text, form_value, id, elem):
36 """Render the table row for the widget depending on description."""
37@@ -192,12 +194,25 @@
38 type='radio')
39 return self._renderRow(text, value, id, elem)
40
41+ def renderExtraHint(self):
42+ extra_hint_html = ''
43+ extra_hint_class = ''
44+ if self.extra_hint_class:
45+ extra_hint_class = ' class="%s"' % self.extra_hint_class
46+ if self.extra_hint:
47+ extra_hint_html = ('<div%s>%s</div>'
48+ % (extra_hint_class, escape(self.extra_hint)))
49+ return extra_hint_html
50+
51 def renderValue(self, value):
52 # Render the items in a table to align the descriptions.
53 rendered_items = self.renderItems(value)
54+ extra_hint = self.renderExtraHint()
55 return (
56- '<table class="radio-button-widget">%s</table>'
57- % ''.join(rendered_items))
58+ '%(extra_hint)s\n'
59+ '<table class="radio-button-widget">%(items)s</table>'
60+ % {'extra_hint': extra_hint,
61+ 'items': ''.join(rendered_items)})
62
63
64 class LaunchpadBooleanRadioWidget(LaunchpadRadioWidget):
65
66=== modified file 'lib/lp/app/widgets/tests/test_itemswidgets.py'
67--- lib/lp/app/widgets/tests/test_itemswidgets.py 2012-01-01 02:58:52 +0000
68+++ lib/lp/app/widgets/tests/test_itemswidgets.py 2012-01-18 00:19:25 +0000
69@@ -203,3 +203,12 @@
70 '<...>item-&lt;2&gt;<...>&lt;unsafe&gt; &amp;nbsp; title<...>')
71 self.assertRenderItem(
72 expected, self.widget.renderItem, self.TestEnum.UNSAFE_TERM)
73+
74+ def test_renderExtraHint(self):
75+ # If an extra hint is specified, it is rendered.
76+ self.widget.extra_hint = "Hello World"
77+ self.widget.extra_hint_class = 'hint_class'
78+ expected = (
79+ '<div class="hint_class">Hello World</div>')
80+ hint_html = self.widget.renderExtraHint()
81+ self.assertEqual(expected, hint_html)
82
83=== modified file 'lib/lp/registry/browser/team.py'
84--- lib/lp/registry/browser/team.py 2012-01-05 21:53:52 +0000
85+++ lib/lp/registry/browser/team.py 2012-01-18 00:19:25 +0000
86@@ -303,7 +303,7 @@
87 # Do we need to only show open subscription policy choices?
88 try:
89 team.checkClosedSubscriptionPolicyAllowed()
90- except TeamSubscriptionPolicyError:
91+ except TeamSubscriptionPolicyError as e:
92 # Ideally SimpleVocabulary.fromItems() would accept 3-tuples but
93 # it doesn't so we need to be a bit more verbose.
94 self.widgets['subscriptionpolicy'].vocabulary = (
95@@ -311,10 +311,14 @@
96 policy, policy.name, policy.title)
97 for policy in OPEN_TEAM_POLICY])
98 )
99+ self.widgets['subscriptionpolicy'].extra_hint_class = (
100+ 'sprite info')
101+ self.widgets['subscriptionpolicy'].extra_hint = e.message
102+
103 # Do we need to only show closed subscription policy choices?
104 try:
105 team.checkOpenSubscriptionPolicyAllowed()
106- except TeamSubscriptionPolicyError:
107+ except TeamSubscriptionPolicyError as e:
108 # Ideally SimpleVocabulary.fromItems() would accept 3-tuples but
109 # it doesn't so we need to be a bit more verbose.
110 self.widgets['subscriptionpolicy'].vocabulary = (
111@@ -322,6 +326,9 @@
112 policy, policy.name, policy.title)
113 for policy in CLOSED_TEAM_POLICY])
114 )
115+ self.widgets['subscriptionpolicy'].extra_hint_class = (
116+ 'sprite info')
117+ self.widgets['subscriptionpolicy'].extra_hint = e.message
118
119 @action('Save', name='save')
120 def action_save(self, action, data):
121
122=== modified file 'lib/lp/registry/browser/tests/test_team_view.py'
123--- lib/lp/registry/browser/tests/test_team_view.py 2012-01-01 02:58:52 +0000
124+++ lib/lp/registry/browser/tests/test_team_view.py 2012-01-18 00:19:25 +0000
125@@ -266,6 +266,7 @@
126 self.assertEqual(
127 TeamSubscriptionPolicy,
128 view.widgets['subscriptionpolicy'].vocabulary)
129+ self.assertIsNone(view.widgets['subscriptionpolicy'].extra_hint)
130 self.assertEqual(
131 TeamMembershipRenewalPolicy.NONE,
132 view.widgets['renewal_policy']._data)
133@@ -286,6 +287,11 @@
134 expected_items,
135 [term.value
136 for term in view.widgets['subscriptionpolicy'].vocabulary])
137+ self.assertEqual(
138+ 'sprite info',
139+ view.widgets['subscriptionpolicy'].extra_hint_class)
140+ self.assertIsNotNone(
141+ view.widgets['subscriptionpolicy'].extra_hint)
142
143 def test_edit_team_view_pillar_owner(self):
144 # The edit view renders only closed subscription policy choices when