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
=== modified file 'quickstart/cli/forms.py'
--- quickstart/cli/forms.py 2014-01-02 17:04:25 +0000
+++ quickstart/cli/forms.py 2014-01-08 12:01:09 +0000
@@ -48,7 +48,7 @@
48 """48 """
49 generate_callback = ui.thunk(_generate, generate_callable, edit_widget)49 generate_callback = ui.thunk(_generate, generate_callable, edit_widget)
50 generate_button = ui.MenuButton(50 generate_button = ui.MenuButton(
51 ('highlight', 'Click here to automatically generate'),51 ('field button', 'Click here to automatically generate'),
52 generate_callback)52 generate_callback)
53 return urwid.Columns([53 return urwid.Columns([
54 ('pack', generate_button),54 ('pack', generate_button),
@@ -56,6 +56,32 @@
56 ])56 ])
5757
5858
59def _create_choices_widget(choices, required, edit_widget):
60 """Create and return a choices widget.
61
62 The widget displays the given choices for a specific form field. Clicking a
63 choice updates the corresponding edit widget.
64 """
65 widgets = [urwid.Text('possible values are:')]
66 callback = edit_widget.set_edit_text
67 buttons = [
68 ui.MenuButton(('field button', choice), ui.thunk(callback, choice))
69 for choice in choices
70 ]
71 cell_width = max(button.base_widget.pack()[0] for button in buttons)
72 widgets.append(urwid.GridFlow(buttons, cell_width, 1, 0, 'left'))
73 if not required:
74 button = ui.MenuButton(
75 ('field button', 'left empty'), ui.thunk(callback, ''))
76 widgets.append(urwid.Columns([
77 ('pack', urwid.Text('but this field can also be ')), button,
78 ]))
79 help = urwid.Text(
80 'click the choices to auto-fill the field with the standard options')
81 widgets.append(help)
82 return urwid.Pile(widgets)
83
84
59def create_string_widget(field, value, error):85def create_string_widget(field, value, error):
60 """Create a string widget and return a tuple (widget, value_getter).86 """Create a string widget and return a tuple (widget, value_getter).
6187
@@ -101,12 +127,9 @@
101 # as part of the help message.127 # as part of the help message.
102 choices = getattr(field, 'choices', None)128 choices = getattr(field, 'choices', None)
103 if choices is not None:129 if choices is not None:
104 # XXX frankban 2014-01-02: make the choices clickable and update the130 choices_widget = _create_choices_widget(
105 # edit widget when a choice is clicked.131 tuple(choices), field.required, edit_widget)
106 text = 'possible values are {}'.format(', '.join(choices))132 widgets.append(choices_widget)
107 if not field.required:
108 text = '{}, but this field can also be left empty'.format(text)
109 widgets.append(urwid.Text(text))
110 if field.default is not None:133 if field.default is not None:
111 widgets.append(134 widgets.append(
112 urwid.Text('default if not set: {}'.format(field.default)))135 urwid.Text('default if not set: {}'.format(field.default)))
113136
=== modified file 'quickstart/cli/ui.py'
--- quickstart/cli/ui.py 2014-01-03 13:32:25 +0000
+++ quickstart/cli/ui.py 2014-01-08 12:01:09 +0000
@@ -39,6 +39,8 @@
39 ('error', 'light red', 'black'),39 ('error', 'light red', 'black'),
40 ('error status', 'light red', 'light gray'),40 ('error status', 'light red', 'light gray'),
41 ('error selected', 'light red', 'dark blue'),41 ('error selected', 'light red', 'dark blue'),
42 ('field button', 'white,underline', 'black'),
43 ('field button selected', 'white,underline', 'dark blue'),
42 ('footer', 'black', 'light gray'),44 ('footer', 'black', 'light gray'),
43 ('message', 'white', 'dark green'),45 ('message', 'white', 'dark green'),
44 ('header', 'white', 'dark magenta'),46 ('header', 'white', 'dark magenta'),
@@ -54,6 +56,7 @@
54 None: 'selected',56 None: 'selected',
55 'control alert': 'error selected',57 'control alert': 'error selected',
56 'error': 'error selected',58 'error': 'error selected',
59 'field button': 'field button selected',
57 'highlight': 'selected',60 'highlight': 'selected',
58}61}
59# Define a default padding for the Urwid application.62# Define a default padding for the Urwid application.
6063
=== modified file 'quickstart/cli/views.py'
--- quickstart/cli/views.py 2014-01-03 13:55:57 +0000
+++ quickstart/cli/views.py 2014-01-08 12:01:09 +0000
@@ -102,6 +102,7 @@
102102
103import urwid103import urwid
104104
105from quickstart import settings
105from quickstart.cli import (106from quickstart.cli import (
106 base,107 base,
107 forms,108 forms,
@@ -188,10 +189,14 @@
188 'highlight', 'Use the links below to create new environments')),189 'highlight', 'Use the links below to create new environments')),
189 urwid.Divider(),190 urwid.Divider(),
190 ])191 ])
192 # The Juju GUI can be safely installed in the bootstrap node only if its
193 # series is "precise". Suggest this setting by pre-filling the value.
194 preferred_series = settings.JUJU_GUI_PREFERRED_SERIES
191 widgets.extend([195 widgets.extend([
192 ui.MenuButton(196 ui.MenuButton(
193 ['\N{BULLET} new ', ('highlight', env_type), ' environment'],197 ['\N{BULLET} new ', ('highlight', env_type), ' environment'],
194 ui.thunk(edit_view, {'type': env_type})198 ui.thunk(edit_view, {
199 'type': env_type, 'default-series': preferred_series})
195 )200 )
196 for env_type in envs.get_supported_env_types(env_type_db)201 for env_type in envs.get_supported_env_types(env_type_db)
197 ])202 ])
@@ -317,7 +322,7 @@
317322
318 The last value (env_data) indicates whether this view is used to create a323 The last value (env_data) indicates whether this view is used to create a
319 new environment or to change an existing one. In the former case, env_data324 new environment or to change an existing one. In the former case, env_data
320 only includes the "type" key. If instead the environment already exists,325 does not include the "name" key. If instead the environment already exists,
321 env_data includes the "name" key and all the other environment info.326 env_data includes the "name" key and all the other environment info.
322 """327 """
323 env_db = copy.deepcopy(env_db)328 env_db = copy.deepcopy(env_db)
324329
=== modified file 'quickstart/models/envs.py'
--- quickstart/models/envs.py 2014-01-03 11:38:43 +0000
+++ quickstart/models/envs.py 2014-01-08 12:01:09 +0000
@@ -334,6 +334,7 @@
334 default_series_field = fields.ChoiceField(334 default_series_field = fields.ChoiceField(
335 'default-series', choices=settings.JUJU_DEFAULT_SERIES,335 'default-series', choices=settings.JUJU_DEFAULT_SERIES,
336 label='default series', required=False,336 label='default series', required=False,
337 default=settings.JUJU_GUI_PREFERRED_SERIES,
337 help='the default Ubuntu series to use for the bootstrap node')338 help='the default Ubuntu series to use for the bootstrap node')
338 is_default_field = fields.BoolField(339 is_default_field = fields.BoolField(
339 'is-default', label='default', allow_mixed=False, required=True,340 'is-default', label='default', allow_mixed=False, required=True,
340341
=== modified file 'quickstart/tests/cli/test_forms.py'
--- quickstart/tests/cli/test_forms.py 2014-01-02 17:04:25 +0000
+++ quickstart/tests/cli/test_forms.py 2014-01-08 12:01:09 +0000
@@ -56,7 +56,89 @@
56 mock_edit_widget.set_edit_text.assert_called_once_with('generated')56 mock_edit_widget.set_edit_text.assert_called_once_with('generated')
5757
5858
59class TestCreateStringWidget(unittest.TestCase):59class ChoicesTestsMixin(object):
60 """Helpers for inspecting the choices widget."""
61
62 def get_buttons(self, choices_widget):
63 """Return the MenuButton instances included in the choices_widget."""
64 # The grid widget including choices is the second widget in the pile.
65 grid = choices_widget.contents[1][0]
66 buttons = [
67 widget for widget, _ in grid.contents
68 if isinstance(widget, ui.MenuButton)
69 ]
70 # Check if the unset button is present.
71 columns_or_text = choices_widget.contents[2][0]
72 if isinstance(columns_or_text, urwid.Columns):
73 buttons.append(columns_or_text.contents[1][0])
74 return buttons
75
76 def assert_choices(self, expected_choices, choices_widget):
77 """Ensure the given choices widget is well formed.
78
79 The message displayed should equal the given expected_message.
80 """
81 obtained_choices = [
82 cli_helpers.get_button_caption(button) for
83 button in self.get_buttons(choices_widget)
84 ]
85 self.assertEqual(expected_choices, obtained_choices)
86
87
88class TestCreateChoicesWidget(ChoicesTestsMixin, unittest.TestCase):
89
90 def setUp(self):
91 # Set up a mock edit widget and choices.
92 self.mock_edit_widget = mock.Mock()
93 self.choices = ['Kirk', 'Picard', 'Sisko']
94
95 def test_widget_message(self):
96 # The resulting widget includes all the choices.
97 widget = forms._create_choices_widget(
98 self.choices, True, self.mock_edit_widget)
99 # The resulting pile widget is composed by choices and help message.
100 self.assert_choices(self.choices, widget)
101 help_widget = widget.contents[2][0]
102 self.assertEqual(
103 'click the choices to auto-fill the field with the '
104 'standard options',
105 help_widget.text)
106
107 def test_widget_buttons(self):
108 # The widget includes a button for each choice.
109 widget = forms._create_choices_widget(
110 self.choices, True, self.mock_edit_widget)
111 buttons = self.get_buttons(widget)
112 self.assertEqual(3, len(buttons))
113 self.assertEqual(
114 self.choices, map(cli_helpers.get_button_caption, buttons))
115
116 def test_choice_click(self):
117 # Clicking a choice updates the edit widget.
118 widget = forms._create_choices_widget(
119 self.choices, True, self.mock_edit_widget)
120 buttons = self.get_buttons(widget)
121 for button in buttons:
122 choice = cli_helpers.get_button_caption(button)
123 # Click the button.
124 cli_helpers.emit(button)
125 # Ensure the edit widget has been updated accordingly.
126 self.mock_edit_widget.set_edit_text.assert_called_with(choice)
127
128 def test_not_required(self):
129 # If the field is not required, an option to unset the field is
130 # displayed.
131 widget = forms._create_choices_widget(
132 self.choices, False, self.mock_edit_widget)
133 self.assert_choices(self.choices + ['left empty'], widget)
134 buttons = self.get_buttons(widget)
135 # The last button is used to unset the edit widget.
136 self.assertEqual(4, len(buttons))
137 cli_helpers.emit(buttons[-1])
138 self.mock_edit_widget.set_edit_text.assert_called_once_with('')
139
140
141class TestCreateStringWidget(ChoicesTestsMixin, unittest.TestCase):
60142
61 def inspect_widget(self, widget, field):143 def inspect_widget(self, widget, field):
62 """Return a list of sub-widgets composing the given string widget.144 """Return a list of sub-widgets composing the given string widget.
@@ -165,10 +247,7 @@
165 field = fields.ChoiceField('captain', choices=('Kirk', 'Picard'))247 field = fields.ChoiceField('captain', choices=('Kirk', 'Picard'))
166 widget, _ = forms.create_string_widget(field, 'Luc', 'invalid name')248 widget, _ = forms.create_string_widget(field, 'Luc', 'invalid name')
167 choices = self.inspect_widget(widget, field)[6]249 choices = self.inspect_widget(widget, field)[6]
168 self.assertEqual(250 self.assert_choices(['Kirk', 'Picard', 'left empty'], choices)
169 'possible values are Kirk, Picard, '
170 'but this field can also be left empty',
171 choices.text)
172251
173 def test_choices_required_field(self):252 def test_choices_required_field(self):
174 # Possible choices are properly displayed below the edit widget for253 # Possible choices are properly displayed below the edit widget for
@@ -177,7 +256,7 @@
177 'captain', required=True, choices=('Janeway', 'Sisko'))256 'captain', required=True, choices=('Janeway', 'Sisko'))
178 widget, _ = forms.create_string_widget(field, 'Luc', 'invalid name')257 widget, _ = forms.create_string_widget(field, 'Luc', 'invalid name')
179 choices = self.inspect_widget(widget, field)[6]258 choices = self.inspect_widget(widget, field)[6]
180 self.assertEqual('possible values are Janeway, Sisko', choices.text)259 self.assert_choices(['Janeway', 'Sisko'], choices)
181260
182261
183class TestCreateBoolWidget(unittest.TestCase):262class TestCreateBoolWidget(unittest.TestCase):
184263
=== modified file 'quickstart/tests/cli/test_views.py'
--- quickstart/tests/cli/test_views.py 2014-01-03 14:13:34 +0000
+++ quickstart/tests/cli/test_views.py 2014-01-08 12:01:09 +0000
@@ -24,6 +24,7 @@
24import mock24import mock
25import urwid25import urwid
2626
27from quickstart import settings
27from quickstart.cli import (28from quickstart.cli import (
28 base,29 base,
29 forms,30 forms,
@@ -209,7 +210,8 @@
209 cli_helpers.emit(button)210 cli_helpers.emit(button)
210 mock_env_edit.assert_called_once_with(211 mock_env_edit.assert_called_once_with(
211 self.app, self.env_type_db, env_db, self.save_callable,212 self.app, self.env_type_db, env_db, self.save_callable,
212 {'type': env_type})213 {'type': env_type,
214 'default-series': settings.JUJU_GUI_PREFERRED_SERIES})
213 # Reset the mock so that it does not include any calls on the next215 # Reset the mock so that it does not include any calls on the next
214 # loop cycle.216 # loop cycle.
215 mock_env_edit.reset_mock()217 mock_env_edit.reset_mock()
@@ -457,8 +459,8 @@
457 def call_view(self, env_name='lxc', env_type=None):459 def call_view(self, env_name='lxc', env_type=None):
458 """Call the view passing the env_data corresponding to env_name.460 """Call the view passing the env_data corresponding to env_name.
459461
460 If env_type provider, the view is a creation form, env_name is ignored462 If env_type is provided, the view is a creation form, env_name is
461 and a new env_data is passed to the view.463 ignored and a new env_data is passed to the view.
462 """464 """
463 if env_type is None:465 if env_type is None:
464 self.env_data = envs.get_env_data(self.env_db, env_name)466 self.env_data = envs.get_env_data(self.env_db, env_name)
465467
=== modified file 'quickstart/tests/models/test_fields.py'
--- quickstart/tests/models/test_fields.py 2013-12-17 11:39:55 +0000
+++ quickstart/tests/models/test_fields.py 2014-01-08 12:01:09 +0000
@@ -440,7 +440,7 @@
440440
441 def test_validation_error_not_in_choices(self):441 def test_validation_error_not_in_choices(self):
442 # A ValueError is raised by choice fields if the value is not included442 # A ValueError is raised by choice fields if the value is not included
443 # in the specified choices/443 # in the specified choices.
444 field = self.field_class(444 field = self.field_class(
445 'word', choices=self.choices, label='selected word')445 'word', choices=self.choices, label='selected word')
446 expected = ('the selected word requires the value to be one of the '446 expected = ('the selected word requires the value to be one of the '

Subscribers

People subscribed via source and target branches