Merge lp:~frankban/juju-quickstart/improve-field-choices into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 43
Proposed branch: lp:~frankban/juju-quickstart/improve-field-choices
Merge into: lp:juju-quickstart
Diff against target: 293 lines (+132/-19)
7 files modified
quickstart/cli/forms.py (+30/-7)
quickstart/cli/ui.py (+3/-0)
quickstart/cli/views.py (+7/-2)
quickstart/models/envs.py (+1/-0)
quickstart/tests/cli/test_forms.py (+85/-6)
quickstart/tests/cli/test_views.py (+5/-3)
quickstart/tests/models/test_fields.py (+1/-1)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/improve-field-choices
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+200658@code.launchpad.net

Description of the change

Improve field choices handling.

Implement clickable choices: the user can update
the corresponding edit widget by clicking choices.

Also changed the default series field behavior so
that unset values default to "precise".

Tests: `make check`.

QA: start the demo app
(`make` and `./cli-app-demo.py).
Use it to edit existing environments and
to create new ones (ec2 and local).
Check that choice fields
(e.g. ec2's regions or default series)
can be updated by clicking the choices.
Also check that, if the default series
is not set, it defaults to "precise".

https://codereview.appspot.com/34630044/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

Reviewers: mp+200658_code.launchpad.net,

Message:
Please take a look.

Description:
Improve field choices handling.

Implement clickable choices: the user can update
the corresponding edit widget by clicking choices.

Also changed the default series field behavior so
that unset values default to "precise".

Tests: `make check`.

QA: start the demo app
(`make` and `./cli-app-demo.py).
Use it to edit existing environments and
to create new ones (ec2 and local).
Check that choice fields
(e.g. ec2's regions or default series)
can be updated by clicking the choices.
Also check that, if the default series
is not set, it defaults to "precise".

https://code.launchpad.net/~frankban/juju-quickstart/improve-field-choices/+merge/200658

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/34630044/

Affected files (+236, -22 lines):
   A [revision details]
   M quickstart/cli/forms.py
   M quickstart/models/envs.py
   M quickstart/models/fields.py
   M quickstart/tests/cli/test_forms.py
   M quickstart/tests/models/test_fields.py

Revision history for this message
Gary Poster (gary) wrote :

QA first:

- Very cool. Looks good generally.

- It looks like Urwid can support an "underline" setting. I think that
would make it easier to see the clickable help text (for both
autogeneration and these choices). WDYT?

- In my default terminal width (80x24) I see "possible values are:
precise, quantal, raring, saucy". If I use the arrow keys then I can
see there is a final option (", but this field can also be _left
empty_"). With the mouse I am out of luck. I'd like to improve that if
we could. Options include letting the line wrap, if Urwid supports
that; showing a clickable "MORE->" or "..." or something; or simply
trying to reduce the length of the clickable text.

In line with the last, simplest idea, what about replacing the four
lines with something like the following? I'm assuming we can make the
first text wrappable easily, but if not maybe this idea needs tweaking.

To select a value, type above, _clear the field_ to get the default
value (precise), or click one of the following choices:
_precise_, _quantal_, _raring_, _saucy_, _trusty_

Mm. Darn. I see in the region field that trying to reduce the length
of the clickable text is insufficient. Those options go on for days.
That will need line wrap or something else similar

- Relatedly, and probably for the future (and maybe never), it would be
nice if the fields that can be automatically generated are allowed to be
empty, which defaults to using the autogeneration.

- I see that we actually explicitly set precise in the file if it is
empty (as you describe in the cover letter). However, IIUC, Juju will
work fine if you do not specify a series. The current *Juju* default
behavior is to default to precise, but probably will change to
defaulting to trusty eventually. That makes me think for our
opinionated usage that we should begin rendering the field with
"precise" but if someone explicitly deletes the field then it is simply
empty (and the "empty" semantics are described in the fields help text).
  WDYT?

Thank you!

https://codereview.appspot.com/34630044/

48. By Francesco Banconi

Some changes as per review.

49. By Francesco Banconi

Checkpoint.

Revision history for this message
Francesco Banconi (frankban) wrote :
Revision history for this message
Gary Poster (gary) wrote :

LGTM. Thank you!

https://codereview.appspot.com/34630044/diff/20001/quickstart/tests/models/test_fields.py
File quickstart/tests/models/test_fields.py (left):

https://codereview.appspot.com/34630044/diff/20001/quickstart/tests/models/test_fields.py#oldcode443
quickstart/tests/models/test_fields.py:443: # in the specified choices/
Sorry for not catching this and other similar ones earlier.

https://codereview.appspot.com/34630044/

50. By Francesco Banconi

Checkpoint.

Revision history for this message
Francesco Banconi (frankban) wrote :
Revision history for this message
Francesco Banconi (frankban) wrote :

On 2014/01/07 13:50:48, gary.poster wrote:
> - It looks like Urwid can support an "underline" setting. I think
that would
> make it easier to see the clickable help text (for both autogeneration
and these
> choices). WDYT?

Sounds good, done.

> - In my default terminal width (80x24) I see "possible values are:
precise,
> quantal, raring, saucy". If I use the arrow keys then I can see there
is a
> final option (", but this field can also be _left empty_"). With the
mouse I am
> out of luck. I'd like to improve that if we could. Options include
letting the
> line wrap, if Urwid supports that; showing a clickable "MORE->" or
"..." or
> something; or simply trying to reduce the length of the clickable
text.

> In line with the last, simplest idea, what about replacing the four
lines with
> something like the following? I'm assuming we can make the first text
wrappable
> easily, but if not maybe this idea needs tweaking.

> To select a value, type above, _clear the field_ to get the default
value
> (precise), or click one of the following choices:
> _precise_, _quantal_, _raring_, _saucy_, _trusty_

> Mm. Darn. I see in the region field that trying to reduce the length
of the
> clickable text is insufficient. Those options go on for days. That
will need
> line wrap or something else similar

Fixed using a GridFlow in place of the Columns widget (which does not
support automatic line wrapping). This way the choices adapt better to
the screen size.

> - I see that we actually explicitly set precise in the file if it is
empty (as
> you describe in the cover letter). However, IIUC, Juju will work fine
if you do
> not specify a series. The current *Juju* default behavior is to
default to
> precise, but probably will change to defaulting to trusty eventually.
That
> makes me think for our opinionated usage that we should begin
rendering the
> field with "precise" but if someone explicitly deletes the field then
it is
> simply empty (and the "empty" semantics are described in the fields
help text).
> WDYT?

I think the current behavior is wrong and what you suggested is better.
I was wrong thinking the default series (if unset) is the host series.
Fixed, now we pre-fill precise when creating a new environment.

Thank you!

https://codereview.appspot.com/34630044/

Revision history for this message
Gary Poster (gary) wrote :
Revision history for this message
Francesco Banconi (frankban) wrote :

*** Submitted:

Improve field choices handling.

Implement clickable choices: the user can update
the corresponding edit widget by clicking choices.

Also changed the default series field behavior so
that unset values default to "precise".

Tests: `make check`.

QA: start the demo app
(`make` and `./cli-app-demo.py).
Use it to edit existing environments and
to create new ones (ec2 and local).
Check that choice fields
(e.g. ec2's regions or default series)
can be updated by clicking the choices.
Also check that, if the default series
is not set, it defaults to "precise".

R=gary.poster
CC=
https://codereview.appspot.com/34630044

https://codereview.appspot.com/34630044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'quickstart/cli/forms.py'
2--- quickstart/cli/forms.py 2014-01-02 17:04:25 +0000
3+++ quickstart/cli/forms.py 2014-01-08 12:01:09 +0000
4@@ -48,7 +48,7 @@
5 """
6 generate_callback = ui.thunk(_generate, generate_callable, edit_widget)
7 generate_button = ui.MenuButton(
8- ('highlight', 'Click here to automatically generate'),
9+ ('field button', 'Click here to automatically generate'),
10 generate_callback)
11 return urwid.Columns([
12 ('pack', generate_button),
13@@ -56,6 +56,32 @@
14 ])
15
16
17+def _create_choices_widget(choices, required, edit_widget):
18+ """Create and return a choices widget.
19+
20+ The widget displays the given choices for a specific form field. Clicking a
21+ choice updates the corresponding edit widget.
22+ """
23+ widgets = [urwid.Text('possible values are:')]
24+ callback = edit_widget.set_edit_text
25+ buttons = [
26+ ui.MenuButton(('field button', choice), ui.thunk(callback, choice))
27+ for choice in choices
28+ ]
29+ cell_width = max(button.base_widget.pack()[0] for button in buttons)
30+ widgets.append(urwid.GridFlow(buttons, cell_width, 1, 0, 'left'))
31+ if not required:
32+ button = ui.MenuButton(
33+ ('field button', 'left empty'), ui.thunk(callback, ''))
34+ widgets.append(urwid.Columns([
35+ ('pack', urwid.Text('but this field can also be ')), button,
36+ ]))
37+ help = urwid.Text(
38+ 'click the choices to auto-fill the field with the standard options')
39+ widgets.append(help)
40+ return urwid.Pile(widgets)
41+
42+
43 def create_string_widget(field, value, error):
44 """Create a string widget and return a tuple (widget, value_getter).
45
46@@ -101,12 +127,9 @@
47 # as part of the help message.
48 choices = getattr(field, 'choices', None)
49 if choices is not None:
50- # XXX frankban 2014-01-02: make the choices clickable and update the
51- # edit widget when a choice is clicked.
52- text = 'possible values are {}'.format(', '.join(choices))
53- if not field.required:
54- text = '{}, but this field can also be left empty'.format(text)
55- widgets.append(urwid.Text(text))
56+ choices_widget = _create_choices_widget(
57+ tuple(choices), field.required, edit_widget)
58+ widgets.append(choices_widget)
59 if field.default is not None:
60 widgets.append(
61 urwid.Text('default if not set: {}'.format(field.default)))
62
63=== modified file 'quickstart/cli/ui.py'
64--- quickstart/cli/ui.py 2014-01-03 13:32:25 +0000
65+++ quickstart/cli/ui.py 2014-01-08 12:01:09 +0000
66@@ -39,6 +39,8 @@
67 ('error', 'light red', 'black'),
68 ('error status', 'light red', 'light gray'),
69 ('error selected', 'light red', 'dark blue'),
70+ ('field button', 'white,underline', 'black'),
71+ ('field button selected', 'white,underline', 'dark blue'),
72 ('footer', 'black', 'light gray'),
73 ('message', 'white', 'dark green'),
74 ('header', 'white', 'dark magenta'),
75@@ -54,6 +56,7 @@
76 None: 'selected',
77 'control alert': 'error selected',
78 'error': 'error selected',
79+ 'field button': 'field button selected',
80 'highlight': 'selected',
81 }
82 # Define a default padding for the Urwid application.
83
84=== modified file 'quickstart/cli/views.py'
85--- quickstart/cli/views.py 2014-01-03 13:55:57 +0000
86+++ quickstart/cli/views.py 2014-01-08 12:01:09 +0000
87@@ -102,6 +102,7 @@
88
89 import urwid
90
91+from quickstart import settings
92 from quickstart.cli import (
93 base,
94 forms,
95@@ -188,10 +189,14 @@
96 'highlight', 'Use the links below to create new environments')),
97 urwid.Divider(),
98 ])
99+ # The Juju GUI can be safely installed in the bootstrap node only if its
100+ # series is "precise". Suggest this setting by pre-filling the value.
101+ preferred_series = settings.JUJU_GUI_PREFERRED_SERIES
102 widgets.extend([
103 ui.MenuButton(
104 ['\N{BULLET} new ', ('highlight', env_type), ' environment'],
105- ui.thunk(edit_view, {'type': env_type})
106+ ui.thunk(edit_view, {
107+ 'type': env_type, 'default-series': preferred_series})
108 )
109 for env_type in envs.get_supported_env_types(env_type_db)
110 ])
111@@ -317,7 +322,7 @@
112
113 The last value (env_data) indicates whether this view is used to create a
114 new environment or to change an existing one. In the former case, env_data
115- only includes the "type" key. If instead the environment already exists,
116+ does not include the "name" key. If instead the environment already exists,
117 env_data includes the "name" key and all the other environment info.
118 """
119 env_db = copy.deepcopy(env_db)
120
121=== modified file 'quickstart/models/envs.py'
122--- quickstart/models/envs.py 2014-01-03 11:38:43 +0000
123+++ quickstart/models/envs.py 2014-01-08 12:01:09 +0000
124@@ -334,6 +334,7 @@
125 default_series_field = fields.ChoiceField(
126 'default-series', choices=settings.JUJU_DEFAULT_SERIES,
127 label='default series', required=False,
128+ default=settings.JUJU_GUI_PREFERRED_SERIES,
129 help='the default Ubuntu series to use for the bootstrap node')
130 is_default_field = fields.BoolField(
131 'is-default', label='default', allow_mixed=False, required=True,
132
133=== modified file 'quickstart/tests/cli/test_forms.py'
134--- quickstart/tests/cli/test_forms.py 2014-01-02 17:04:25 +0000
135+++ quickstart/tests/cli/test_forms.py 2014-01-08 12:01:09 +0000
136@@ -56,7 +56,89 @@
137 mock_edit_widget.set_edit_text.assert_called_once_with('generated')
138
139
140-class TestCreateStringWidget(unittest.TestCase):
141+class ChoicesTestsMixin(object):
142+ """Helpers for inspecting the choices widget."""
143+
144+ def get_buttons(self, choices_widget):
145+ """Return the MenuButton instances included in the choices_widget."""
146+ # The grid widget including choices is the second widget in the pile.
147+ grid = choices_widget.contents[1][0]
148+ buttons = [
149+ widget for widget, _ in grid.contents
150+ if isinstance(widget, ui.MenuButton)
151+ ]
152+ # Check if the unset button is present.
153+ columns_or_text = choices_widget.contents[2][0]
154+ if isinstance(columns_or_text, urwid.Columns):
155+ buttons.append(columns_or_text.contents[1][0])
156+ return buttons
157+
158+ def assert_choices(self, expected_choices, choices_widget):
159+ """Ensure the given choices widget is well formed.
160+
161+ The message displayed should equal the given expected_message.
162+ """
163+ obtained_choices = [
164+ cli_helpers.get_button_caption(button) for
165+ button in self.get_buttons(choices_widget)
166+ ]
167+ self.assertEqual(expected_choices, obtained_choices)
168+
169+
170+class TestCreateChoicesWidget(ChoicesTestsMixin, unittest.TestCase):
171+
172+ def setUp(self):
173+ # Set up a mock edit widget and choices.
174+ self.mock_edit_widget = mock.Mock()
175+ self.choices = ['Kirk', 'Picard', 'Sisko']
176+
177+ def test_widget_message(self):
178+ # The resulting widget includes all the choices.
179+ widget = forms._create_choices_widget(
180+ self.choices, True, self.mock_edit_widget)
181+ # The resulting pile widget is composed by choices and help message.
182+ self.assert_choices(self.choices, widget)
183+ help_widget = widget.contents[2][0]
184+ self.assertEqual(
185+ 'click the choices to auto-fill the field with the '
186+ 'standard options',
187+ help_widget.text)
188+
189+ def test_widget_buttons(self):
190+ # The widget includes a button for each choice.
191+ widget = forms._create_choices_widget(
192+ self.choices, True, self.mock_edit_widget)
193+ buttons = self.get_buttons(widget)
194+ self.assertEqual(3, len(buttons))
195+ self.assertEqual(
196+ self.choices, map(cli_helpers.get_button_caption, buttons))
197+
198+ def test_choice_click(self):
199+ # Clicking a choice updates the edit widget.
200+ widget = forms._create_choices_widget(
201+ self.choices, True, self.mock_edit_widget)
202+ buttons = self.get_buttons(widget)
203+ for button in buttons:
204+ choice = cli_helpers.get_button_caption(button)
205+ # Click the button.
206+ cli_helpers.emit(button)
207+ # Ensure the edit widget has been updated accordingly.
208+ self.mock_edit_widget.set_edit_text.assert_called_with(choice)
209+
210+ def test_not_required(self):
211+ # If the field is not required, an option to unset the field is
212+ # displayed.
213+ widget = forms._create_choices_widget(
214+ self.choices, False, self.mock_edit_widget)
215+ self.assert_choices(self.choices + ['left empty'], widget)
216+ buttons = self.get_buttons(widget)
217+ # The last button is used to unset the edit widget.
218+ self.assertEqual(4, len(buttons))
219+ cli_helpers.emit(buttons[-1])
220+ self.mock_edit_widget.set_edit_text.assert_called_once_with('')
221+
222+
223+class TestCreateStringWidget(ChoicesTestsMixin, unittest.TestCase):
224
225 def inspect_widget(self, widget, field):
226 """Return a list of sub-widgets composing the given string widget.
227@@ -165,10 +247,7 @@
228 field = fields.ChoiceField('captain', choices=('Kirk', 'Picard'))
229 widget, _ = forms.create_string_widget(field, 'Luc', 'invalid name')
230 choices = self.inspect_widget(widget, field)[6]
231- self.assertEqual(
232- 'possible values are Kirk, Picard, '
233- 'but this field can also be left empty',
234- choices.text)
235+ self.assert_choices(['Kirk', 'Picard', 'left empty'], choices)
236
237 def test_choices_required_field(self):
238 # Possible choices are properly displayed below the edit widget for
239@@ -177,7 +256,7 @@
240 'captain', required=True, choices=('Janeway', 'Sisko'))
241 widget, _ = forms.create_string_widget(field, 'Luc', 'invalid name')
242 choices = self.inspect_widget(widget, field)[6]
243- self.assertEqual('possible values are Janeway, Sisko', choices.text)
244+ self.assert_choices(['Janeway', 'Sisko'], choices)
245
246
247 class TestCreateBoolWidget(unittest.TestCase):
248
249=== modified file 'quickstart/tests/cli/test_views.py'
250--- quickstart/tests/cli/test_views.py 2014-01-03 14:13:34 +0000
251+++ quickstart/tests/cli/test_views.py 2014-01-08 12:01:09 +0000
252@@ -24,6 +24,7 @@
253 import mock
254 import urwid
255
256+from quickstart import settings
257 from quickstart.cli import (
258 base,
259 forms,
260@@ -209,7 +210,8 @@
261 cli_helpers.emit(button)
262 mock_env_edit.assert_called_once_with(
263 self.app, self.env_type_db, env_db, self.save_callable,
264- {'type': env_type})
265+ {'type': env_type,
266+ 'default-series': settings.JUJU_GUI_PREFERRED_SERIES})
267 # Reset the mock so that it does not include any calls on the next
268 # loop cycle.
269 mock_env_edit.reset_mock()
270@@ -457,8 +459,8 @@
271 def call_view(self, env_name='lxc', env_type=None):
272 """Call the view passing the env_data corresponding to env_name.
273
274- If env_type provider, the view is a creation form, env_name is ignored
275- and a new env_data is passed to the view.
276+ If env_type is provided, the view is a creation form, env_name is
277+ ignored and a new env_data is passed to the view.
278 """
279 if env_type is None:
280 self.env_data = envs.get_env_data(self.env_db, env_name)
281
282=== modified file 'quickstart/tests/models/test_fields.py'
283--- quickstart/tests/models/test_fields.py 2013-12-17 11:39:55 +0000
284+++ quickstart/tests/models/test_fields.py 2014-01-08 12:01:09 +0000
285@@ -440,7 +440,7 @@
286
287 def test_validation_error_not_in_choices(self):
288 # A ValueError is raised by choice fields if the value is not included
289- # in the specified choices/
290+ # in the specified choices.
291 field = self.field_class(
292 'word', choices=self.choices, label='selected word')
293 expected = ('the selected word requires the value to be one of the '

Subscribers

People subscribed via source and target branches