Merge lp:~makyo/juju-quickstart/env-manage-edit into lp:juju-quickstart

Proposed by Madison Scott-Clary
Status: Work in progress
Proposed branch: lp:~makyo/juju-quickstart/env-manage-edit
Merge into: lp:juju-quickstart
Diff against target: 739 lines (+547/-18)
7 files modified
quickstart/cli/forms.py (+199/-0)
quickstart/cli/ui.py (+3/-0)
quickstart/cli/views.py (+123/-6)
quickstart/models/envs.py (+1/-1)
quickstart/tests/cli/test_forms.py (+148/-0)
quickstart/tests/cli/test_views.py (+59/-11)
quickstart/tests/models/test_envs.py (+14/-0)
To merge this branch: bzr merge lp:~makyo/juju-quickstart/env-manage-edit
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+200024@code.launchpad.net

Description of the change

Editable env. management

Check: make check

QA: -todo-

https://codereview.appspot.com/45390044/

To post a comment you must log in.

Unmerged revisions

51. By Madison Scott-Clary

Lint for wip checkpoint

50. By Madison Scott-Clary

Checkpoint

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'quickstart/cli/forms.py'
2--- quickstart/cli/forms.py 1970-01-01 00:00:00 +0000
3+++ quickstart/cli/forms.py 2013-12-24 19:19:29 +0000
4@@ -0,0 +1,199 @@
5+# This file is part of the Juju Quickstart Plugin, which lets users set up a
6+# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
7+# Copyright (C) 2013 Canonical Ltd.
8+#
9+# This program is free software: you can redistribute it and/or modify it under
10+# the terms of the GNU Affero General Public License version 3, as published by
11+# the Free Software Foundation.
12+#
13+# This program is distributed in the hope that it will be useful, but WITHOUT
14+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
15+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+# Affero General Public License for more details.
17+#
18+# You should have received a copy of the GNU Affero General Public License
19+# along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
21+"""Juju Quickstart CLI forms management.
22+
23+This module contains a collection of functions which help creating and
24+manipulating forms in Urwid.
25+"""
26+
27+from __future__ import unicode_literals
28+
29+import functools
30+
31+import urwid
32+
33+from quickstart.cli import ui
34+
35+
36+# Define the value used in boolean widget to specify they allow mixed state.
37+MIXED = 'mixed'
38+
39+
40+def _generate(generate_callable, edit_widget):
41+ """Update the widget contents using generate_callable.
42+
43+ The given callable function takes no arguments and returns a string.
44+ """
45+ edit_widget.set_edit_text(generate_callable())
46+
47+
48+def create_string_widget(field, value, error):
49+ """Create a string widget and return a tuple (widget, value_getter).
50+
51+ Receives a Field instance (see quickstart.models.fields), the field value,
52+ and an error string (or None if the field has no errors).
53+
54+ In the tuple, widget is a Urwid widget suitable for editing string values,
55+ and value_getter is a callable returning the value currently stored in the
56+ widget. The value_getter callable must be called without arguments.
57+ """
58+ if value is None:
59+ # Unset values are converted to empty strings.
60+ value = ''
61+ elif not isinstance(value, unicode):
62+ # We do not expect byte strings, and all other values are converted to
63+ # unicode strings.
64+ value = unicode(value)
65+ caption_class = 'highlight' if field.required else 'optional'
66+ caption = []
67+ widgets = []
68+ if error:
69+ caption.append(('error', '\N{BULLET} '))
70+ # Display the error message above the edit widget.
71+ widgets.append(urwid.Text(('error', error)))
72+ caption.append((caption_class, '{}: '.format(field.label)))
73+ edit_widget = urwid.Edit(edit_text=value)
74+ widget = urwid.Columns([('pack', urwid.Text(caption)), edit_widget])
75+ if field.readonly:
76+ widgets.append(urwid.WidgetDisable(widget))
77+ else:
78+ widgets.append(urwid.AttrMap(widget, 'edit'))
79+ if field.help:
80+ # Display the field help message below the edit widget.
81+ widgets.append(urwid.Text(field.help))
82+ if not field.readonly:
83+ # Can the value be automatically generated?
84+ generate_method = getattr(field, 'generate', None)
85+ if generate_method is not None:
86+ generate_callable = ui.thunk(
87+ _generate, generate_method, edit_widget)
88+ generate_button = ui.MenuButton(
89+ ('highlight', 'automatically generate'), generate_callable)
90+ generate_columns = urwid.Columns([
91+ ('pack', urwid.Text('it is possible to ')),
92+ ('pack', generate_button),
93+ urwid.Text(' this value'),
94+ ])
95+ widgets.append(generate_columns)
96+ # If the value must be in a range of choices, display the possible choices
97+ # as part of the help message.
98+ choices = getattr(field, 'choices', None)
99+ if choices is not None:
100+ # TODO: the choices are clickable and update the edit widget.
101+ text = 'possible values are {}'.format(', '.join(choices))
102+ if not field.required:
103+ text = '{}, but this field can also be left empty'.format(text)
104+ widgets.append(urwid.Text(text))
105+ if field.default is not None:
106+ widgets.append(
107+ urwid.Text('default if not set: {}'.format(field.default)))
108+ widgets.append(urwid.Divider())
109+ return urwid.Pile(widgets), edit_widget.get_edit_text
110+
111+
112+def create_bool_widget(field, value, error):
113+ """Create a boolean widget and return a tuple (widget, value_getter).
114+
115+ Receives a Field instance (see quickstart.models.fields), the field value,
116+ and an error string (or None if the field has no errors).
117+
118+ In the tuple, widget is a Urwid widget suitable for editing boolean values
119+ (a checkbox), and value_getter is a callable returning the value currently
120+ stored in the widget. The value_getter callable receives no arguments.
121+ """
122+ if value is None:
123+ # Unset values are converted to a more convenient value for the
124+ # checkbox (a boolean or a mixed state if allowed by the field).
125+ value = MIXED if field.allow_mixed else bool(field.default)
126+ label = ('highlight', field.label)
127+ widget = urwid.CheckBox(label, state=value, has_mixed=field.allow_mixed)
128+ widgets = []
129+ if error:
130+ # Display the error message above the checkbox.
131+ widgets.append(urwid.Text(('error', error)))
132+ widgets.append(widget)
133+ if field.help:
134+ # Display the field help message below the checkbox.
135+ widgets.append(urwid.Text(field.help))
136+ widgets.append(urwid.Divider())
137+
138+ def get_state():
139+ # Reconvert mixed value to None value.
140+ state = widget.get_state()
141+ return None if state == MIXED else state
142+
143+ return urwid.Pile(widgets), get_state
144+
145+
146+def create_form(field_value_pairs, errors):
147+ """Create and return the form widgets for each field/value pair.
148+
149+ Return a tuple (widgets, values_getter) in which:
150+ - widgets is a list if Urwid objects that can be used to build view
151+ contents (e.g. by wrapping them in a urwid.ListBox);
152+ - data_getter is a function returning a dictionary mapping field names
153+ to values. Using the data_getter function it is always possible to
154+ retrieve the actual new field values as changed by the user.
155+
156+ The field_value_pairs argument is a list of (field, value) tuples where
157+ field is a Field instance (see quickstart.models.fields) and value is
158+ the corresponding field value.
159+
160+ The errors argument is a dictionary mapping field names to error messages.
161+ Passing an empty dictionary means the form has no errors.
162+ """
163+ form = {}
164+ widgets = []
165+ if errors:
166+ # Inform the user that the form has errors that need to be fixed.
167+ widgets.extend([
168+ urwid.Text(('error', 'please correct the errors below')),
169+ urwid.Divider(),
170+ ])
171+ # Build a widget and populate the form for each field/value pair.
172+ for field, value in field_value_pairs:
173+ error = errors.get(field.name)
174+ if field.field_type == 'bool':
175+ # Boolean values as represented as a checkbox.
176+ widget, value_getter = create_bool_widget(field, value, error)
177+ else:
178+ # All the other field types are displayed as strings.
179+ widget, value_getter = create_string_widget(field, value, error)
180+ widgets.append(widget)
181+ form[field.name] = value_getter
182+ return widgets, functools.partial(_get_data, form)
183+
184+
185+def _get_data(form):
186+ """Return a dictionary mapping the given form field names to values.
187+
188+ This is done just calling all the value getters in the form.
189+ """
190+ return dict((key, value_getter()) for key, value_getter in form.items())
191+
192+
193+def create_actions(actions):
194+ """Return the control widgets based on the given actions.
195+
196+ The actions arguments is as a sequence of (caption, callback) tuples. Those
197+ pairs are used to generate the clickable controls (MenuButton instances)
198+ used to manipulate the form.
199+ """
200+ controls = [
201+ ui.MenuButton(caption, callback) for caption, callback in actions
202+ ]
203+ return ui.create_controls(*controls)
204
205=== modified file 'quickstart/cli/ui.py'
206--- quickstart/cli/ui.py 2013-12-20 10:30:37 +0000
207+++ quickstart/cli/ui.py 2013-12-24 19:19:29 +0000
208@@ -45,6 +45,8 @@
209 ('highlight', 'white', 'black'),
210 ('line header', 'dark gray', 'dark magenta'),
211 ('line footer', 'light gray', 'light gray'),
212+ ('optional', 'light magenta', 'black'),
213+ ('optional status', 'light magenta', 'light gray'),
214 ('selected', 'white', 'dark blue'),
215 ]
216 # Map attributes to new attributes to apply when the widget is selected.
217@@ -52,6 +54,7 @@
218 None: 'selected',
219 'control alert': 'error selected',
220 'error': 'error selected',
221+ 'highlight': 'selected',
222 }
223 # Define a default padding for the Urwid application.
224 padding = functools.partial(urwid.Padding, left=2, right=2)
225
226=== modified file 'quickstart/cli/views.py'
227--- quickstart/cli/views.py 2013-12-19 17:40:36 +0000
228+++ quickstart/cli/views.py 2013-12-24 19:19:29 +0000
229@@ -104,6 +104,7 @@
230
231 from quickstart.cli import (
232 base,
233+ forms,
234 ui,
235 )
236 from quickstart.models import envs
237@@ -132,6 +133,8 @@
238 """Show the Juju environments list.
239
240 The env_detail view is displayed when the user clicks on an environment.
241+ From here it is also possible to switch to the edit view in order to create
242+ a new environment.
243
244 Receives:
245 - env_type_db: the environments meta information;
246@@ -145,6 +148,8 @@
247 app.set_return_value_on_exit((env_db, None))
248 detail_view = functools.partial(
249 env_detail, app, env_type_db, env_db, save_callable)
250+ edit_view = functools.partial(
251+ env_edit, app, env_type_db, env_db, save_callable)
252 # Alphabetically sort the existing environments.
253 environments = sorted([
254 envs.get_env_data(env_db, env_name)
255@@ -153,7 +158,6 @@
256 if environments:
257 title = 'Select the Juju environment you want to use'
258 else:
259- # XXX frankban 2013-12-18: implement the env creation view.
260 title = 'No Juju environments already set up: please create one'
261 app.set_title(title)
262 app.set_status('')
263@@ -179,9 +183,13 @@
264 text = [bullet, ' {}'.format(env_short_description)]
265 widgets.append(ui.MenuButton(text, ui.thunk(detail_view, env_data)))
266 widgets.append(urwid.Divider())
267- contents = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))
268- if focus_position is not None:
269- contents.set_focus(focus_position)
270+ # Add the buttons used to create new environments.
271+ widgets.extend([
272+ ui.MenuButton(
273+ '\N{BULLET} create new {} environment'.format(env_type),
274+ ui.thunk(edit_view, {'type': env_type}))
275+ for env_type in envs.get_supported_env_types(env_type_db)
276+ ])
277 # Set up the application status messages.
278 status = []
279 if default_found:
280@@ -190,6 +198,10 @@
281 status.extend([('error status', ' \N{BULLET}'), ' has errors '])
282 if status:
283 app.set_status(status)
284+ # Set up the application contents.
285+ contents = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))
286+ if focus_position is not None:
287+ contents.set_focus(focus_position)
288 app.set_contents(contents)
289
290
291@@ -212,6 +224,8 @@
292 app.set_return_value_on_exit((env_db, None))
293 index_view = functools.partial(
294 env_index, app, env_type_db, env_db, save_callable)
295+ edit_view = functools.partial(
296+ env_edit, app, env_type_db, env_db, save_callable, env_data)
297
298 def use(env_data):
299 # Quit the interactive session returning the (possibly modified)
300@@ -231,8 +245,10 @@
301 def remove(env_data):
302 # The environment deletion is confirmed: remove the environment from
303 # the database, save the new env_db and return to the index view.
304- envs.remove_env(env_db, env_data['name'])
305+ env_name = env_data['name']
306+ envs.remove_env(env_db, env_name)
307 save_callable(env_db)
308+ app.set_message('{} successfully removed'.format(env_name))
309 index_view()
310
311 def confirm_removal(env_data):
312@@ -272,10 +288,111 @@
313 controls.append(
314 ui.MenuButton('set default', ui.thunk(set_default, env_data)))
315 controls.extend([
316- # XXX frankban 2013-12-18: implement the env modification view.
317+ ui.MenuButton('edit', ui.thunk(edit_view)),
318 ui.MenuButton(
319 ('control alert', 'remove'), ui.thunk(confirm_removal, env_data)),
320 ])
321 widgets.append(ui.create_controls(*controls))
322 listbox = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))
323 app.set_contents(listbox)
324+
325+
326+def env_edit(app, env_type_db, env_db, save_callable, env_data):
327+ """Create or modify a Juju environment.
328+
329+ This view displays an edit form allowing for environment
330+ creation/modification. Saving the form redirects to the environment detail
331+ view if the values are valid.
332+
333+ Receives:
334+ - env_type_db: the environments meta information;
335+ - env_db: the environments database;
336+ - save_callable: a function called to save a new environment database.
337+ - env_data: the environment data.
338+
339+ The last value (env_data) indicates whether this view is used to create a
340+ new environment or to change an existing one. In the former case, env_data
341+ only includes the "type" key. If instead the environment already exists,
342+ env_data includes the "name" key and all the other environment info.
343+ """
344+ env_db = copy.deepcopy(env_db)
345+ # All the environment views return a tuple (new_env_db, env_data).
346+ # Set the env_data to None in the case the user quits the application
347+ # without selecting an environment to use.
348+ app.set_return_value_on_exit((env_db, None))
349+ env_metadata = envs.get_env_metadata(env_type_db, env_data)
350+ index_view = functools.partial(
351+ env_index, app, env_type_db, env_db, save_callable)
352+ detail_view = functools.partial(
353+ env_detail, app, env_type_db, env_db, save_callable)
354+ if 'name' in env_data:
355+ exists = True
356+ title = 'Edit the {} environment'
357+ # Retrieve all the errors for the existing environment.
358+ initial_errors = envs.validate(env_metadata, env_data)
359+ else:
360+ exists = False
361+ title = 'Create a new {} environment'
362+ # The environment does not exist: avoid bothering the user with errors
363+ # before the form is submitted.
364+ initial_errors = {}
365+ app.set_title(title.format(env_data['type']))
366+ app.set_status([
367+ ('optional status', ' \N{LOWER SEVEN EIGHTHS BLOCK}'),
368+ ' optional field ',
369+ ('error status', ' \N{BULLET}'),
370+ ' field errors ',
371+ ])
372+
373+ def save(env_data, get_new_env_data):
374+ # Create a new environment or save changes for an existing one.
375+ # The new values are saved only if the new env_data is valid, in which
376+ # case also redirect to the environment detail view.
377+ new_env_data = get_new_env_data()
378+ # Validate the new env_data.
379+ errors = envs.validate(env_metadata, new_env_data)
380+ new_name = new_env_data['name']
381+ initial_name = env_data.get('name')
382+ if (new_name != initial_name) and new_name in env_db['environments']:
383+ errors['name'] = 'an environment with this name already exists'
384+ # If errors are found, re-render the form passing the errors. This way
385+ # the errors are displayed as part of the form and the user is given
386+ # the opportunity to fix the invalid values.
387+ if errors:
388+ return render_form(new_env_data, errors)
389+ # Without errors, normalize the new values, update the env_db and save
390+ # the resulting environments database.
391+ env_data = envs.normalize(env_metadata, new_env_data)
392+ envs.set_env_data(env_db, initial_name, env_data)
393+ save_callable(env_db)
394+ verb = 'modified' if exists else 'created'
395+ app.set_message('{} successfully {}'.format(new_name, verb))
396+ return detail_view(env_data)
397+
398+ def cancel(env_data):
399+ # Dismiss any changes and return to the index or detail view.
400+ return detail_view(env_data) if exists else index_view()
401+
402+ def render_form(data, errors):
403+ widgets = [
404+ urwid.Text(env_metadata['description']),
405+ urwid.Divider(),
406+ ]
407+ field_value_pairs = envs.map_fields_to_env_data(env_metadata, data)
408+ # Retrieve the form widgets and the data getter function. The latter
409+ # can be used to retrieve the field name/value pairs included in the
410+ # displayed form, including user's changes (see quickstart.cli.forms).
411+ form_widgets, get_new_env_data = forms.create_form(
412+ field_value_pairs, errors)
413+ widgets.extend(form_widgets)
414+ actions = (
415+ ('save', ui.thunk(save, env_data, get_new_env_data)),
416+ ('cancel', ui.thunk(cancel, env_data)),
417+ ('restore', ui.thunk(render_form, env_data, initial_errors)),
418+ )
419+ widgets.append(forms.create_actions(actions))
420+ contents = urwid.ListBox(urwid.SimpleFocusListWalker(widgets))
421+ app.set_contents(contents)
422+
423+ # Render the initial form.
424+ render_form(env_data, initial_errors)
425
426=== modified file 'quickstart/models/envs.py'
427--- quickstart/models/envs.py 2013-12-19 16:48:38 +0000
428+++ quickstart/models/envs.py 2013-12-24 19:19:29 +0000
429@@ -292,7 +292,7 @@
430 environments[new_name] = new_env_data
431 if is_default:
432 env_db['default'] = new_name
433- elif env_db.get('default') == env_name:
434+ elif (env_db.get('default') == env_name) and (env_name is not None):
435 del env_db['default']
436
437
438
439=== added file 'quickstart/tests/cli/test_forms.py'
440--- quickstart/tests/cli/test_forms.py 1970-01-01 00:00:00 +0000
441+++ quickstart/tests/cli/test_forms.py 2013-12-24 19:19:29 +0000
442@@ -0,0 +1,148 @@
443+# This file is part of the Juju Quickstart Plugin, which lets users set up a
444+# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
445+# Copyright (C) 2013 Canonical Ltd.
446+#
447+# This program is free software: you can redistribute it and/or modify it under
448+# the terms of the GNU Affero General Public License version 3, as published by
449+# the Free Software Foundation.
450+#
451+# This program is distributed in the hope that it will be useful, but WITHOUT
452+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
453+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
454+# Affero General Public License for more details.
455+#
456+# You should have received a copy of the GNU Affero General Public License
457+# along with this program. If not, see <http://www.gnu.org/licenses/>.
458+
459+"""Tests for the Juju Quickstart CLI forms management."""
460+
461+from __future__ import unicode_literals
462+
463+import unittest
464+
465+import mock
466+import urwid
467+
468+from quickstart.cli import forms
469+from quickstart.models import fields
470+
471+
472+class TestGenerate(unittest.TestCase):
473+
474+ def test_generation(self):
475+ # The text of the given widget is set to the return value of the given
476+ # callable.
477+ generate_callable = lambda: 'generated'
478+ mock_edit_widget = mock.Mock()
479+ forms._generate(generate_callable, mock_edit_widget)
480+ mock_edit_widget.set_edit_text.assert_called_once_with('generated')
481+
482+
483+class TestCreateStringWidget(unittest.TestCase):
484+
485+ def inspect_widget(self, widget):
486+ """Return a list of sub-widgets composing the given string widget.
487+
488+ The sub-widgets are:
489+ - the wrapper edit widget;
490+ - the base edit widget;
491+ - the caption text widget;
492+ - the help widget;
493+ - the error text widget (or None if not present);
494+ - the generate text widget (or None if not present);
495+ - the choices text widget (or None if not present).
496+ """
497+ help = error = generate = choices = None
498+ # Retrieve the Pile contents ignoring the last Divider widget.
499+ contents = list(widget.contents)[:-1]
500+ first_widget = contents.pop(0)[0]
501+ if isinstance(first_widget, urwid.Text):
502+ # This is the error message.
503+ error = first_widget
504+ wrapper = contents.pop(0)[0]
505+ else:
506+ # The widget has no errors.
507+ wrapper = first_widget
508+ caption_attrs, edit_attrs = wrapper.base_widget.contents
509+ if contents:
510+ help = contents.pop(0)[0]
511+ if contents:
512+ if isinstance(contents[0][0], urwid.Text):
513+ choices = contents.pop(0)[0]
514+ else:
515+ generate = contents.pop(0)[0]
516+ if contents:
517+ choices = contents.pop(0)[0]
518+ if 'default if not set' in choices.get_text()[0]:
519+ choices = None
520+ return (
521+ wrapper, edit_attrs[0], caption_attrs[0],
522+ help, error, generate, choices,
523+ )
524+
525+ def test_widget_structure(self):
526+ # The widget includes all the information about a field.
527+ field = fields.StringField(
528+ 'first-name', label='first name', help='your first name',
529+ default='Jean')
530+ widget, value_getter = forms.create_string_widget(
531+ field, 'Luc', 'invalid name')
532+ sub_widgets = self.inspect_widget(widget)
533+ wrapper, edit, caption, help, error, generate, choices = sub_widgets
534+ # Since the field is not read-only, the widget is properly enabled.
535+ self.assertNotIsInstance(wrapper, urwid.WidgetDisable)
536+ # The edit widget is set to the given value.
537+ self.assertEqual('Luc', edit.get_edit_text())
538+ # The caption and help are properly set.
539+ self.assertEqual('\N{BULLET} first name: ', caption.text)
540+ self.assertEqual('your first name', help.text)
541+ # The error is displayed.
542+ self.assertEqual('invalid name', error.text)
543+ # The field is not able to generate a value, and there are no choices.
544+ self.assertIsNone(generate)
545+ self.assertIsNone(choices)
546+
547+ def test_empty_value(self):
548+ # The widget includes all the information about a field.
549+ field = fields.StringField(
550+ 'first-name', label='first name', help='your first name',
551+ default='Jean')
552+ widget, value_getter = forms.create_string_widget(
553+ field, None, 'empty name')
554+ sub_widgets = self.inspect_widget(widget)
555+ error = sub_widgets[4]
556+ self.assertEqual('empty name', error.text)
557+
558+ def test_nonunicode_value(self):
559+ # The widget includes all the information about a field.
560+ field = fields.StringField(
561+ 'first-name', label='first name', help='your first name',
562+ default='Jean')
563+ widget, value_getter = forms.create_string_widget(
564+ field, 42, 'non-unicode name')
565+ edit = self.inspect_widget(widget)[1]
566+ self.assertIsInstance(edit.get_edit_text(), unicode)
567+
568+ def test_readonly(self):
569+ # The widget includes all the information about a field.
570+ field = fields.StringField(
571+ 'first-name', label='first name', help='your first name',
572+ default='Jean', readonly=True)
573+ widget, value_getter = forms.create_string_widget(
574+ field, 42, 'disabled')
575+ disable = self.inspect_widget(widget)[0]
576+ self.assertIsInstance(disable, urwid.WidgetDisable)
577+
578+ def test_generate_method(self):
579+ # The widget includes all the information about a field.
580+ field = fields.AutoGeneratedStringField(
581+ 'first-name', label='first name', help='your first name',
582+ default='Jean')
583+ widget, value_getter = forms.create_string_widget(
584+ field, 42, 'disabled')
585+ generate = self.inspect_widget(widget)[5]
586+ # Check that an autogenerate button is included.
587+ self.assertIsInstance(generate.contents[1][0], urwid.Button)
588+
589+ def test_choices(self):
590+ pass
591
592=== modified file 'quickstart/tests/cli/test_views.py'
593--- quickstart/tests/cli/test_views.py 2013-12-20 10:30:37 +0000
594+++ quickstart/tests/cli/test_views.py 2013-12-24 19:19:29 +0000
595@@ -149,17 +149,28 @@
596 'No Juju environments already set up: please create one',
597 self.app.get_title())
598
599+ def test_view_contents(self):
600+ # The view displays a list of the environments in env_db, and buttons
601+ # to create new environments.
602+ env_db = helpers.make_env_db()
603+ views.env_index(self.app, self.env_type_db, env_db, self.save_callable)
604+ buttons = self.get_widgets_in_contents(
605+ filter_function=self.is_a(ui.MenuButton))
606+ # A button is created for each existing environment (see details) and
607+ # for each environment type supported by quickstart (create).
608+ env_types = envs.get_supported_env_types(self.env_type_db)
609+ expected_buttons_number = len(env_db['environments']) + len(env_types)
610+ self.assertEqual(expected_buttons_number, len(buttons))
611+
612 @mock.patch('quickstart.cli.views.env_detail')
613- def test_view_contents(self, mock_env_detail):
614- # The view displays a list of the environments in env_db.
615+ def test_environment_clicked(self, mock_env_detail):
616+ # The environment detail view is called when clicking an environment.
617 env_db = helpers.make_env_db()
618 views.env_index(self.app, self.env_type_db, env_db, self.save_callable)
619 buttons = self.get_widgets_in_contents(
620 filter_function=self.is_a(ui.MenuButton))
621 # The environments are listed in alphabetical order.
622 environments = sorted(env_db['environments'])
623- # A button is created for each environment.
624- self.assertEqual(len(environments), len(buttons))
625 for env_name, button in zip(environments, buttons):
626 env_data = envs.get_env_data(env_db, env_name)
627 # The caption includes the environment description.
628@@ -176,6 +187,30 @@
629 # loop cycle.
630 mock_env_detail.reset_mock()
631
632+ @mock.patch('quickstart.cli.views.env_edit')
633+ def test_create_new_environment_clicked(self, mock_env_edit):
634+ # The environment edit view is called when clicking to create a new
635+ # environment.
636+ env_db = helpers.make_env_db()
637+ views.env_index(self.app, self.env_type_db, env_db, self.save_callable)
638+ buttons = self.get_widgets_in_contents(
639+ filter_function=self.is_a(ui.MenuButton))
640+ env_types = envs.get_supported_env_types(self.env_type_db)
641+ for env_type, button in zip(env_types, buttons[-len(env_types):]):
642+ # The caption includes the environment type.
643+ expected_caption = 'create new {} environment'.format(env_type)
644+ caption = cli_helpers.get_button_caption(button)
645+ self.assertIn(expected_caption, caption)
646+ # When the button is clicked, the edit view is called passing the
647+ # corresponding environment data.
648+ cli_helpers.emit(button)
649+ mock_env_edit.assert_called_once_with(
650+ self.app, self.env_type_db, env_db, self.save_callable,
651+ {'type': env_type})
652+ # Reset the mock so that it does not include any calls on the next
653+ # loop cycle.
654+ mock_env_edit.reset_mock()
655+
656 def test_selected_environment(self):
657 # The default environment is already selected in the list.
658 env_db = helpers.make_env_db(default='lxc')
659@@ -261,28 +296,30 @@
660 self.assertEqual(expected_text, widget.text)
661
662 def test_view_buttons(self):
663- # The following buttons are displayed: "back", "use", "set default" and
664- # "remove".
665+ # The following buttons are displayed: "back", "use", "set default",
666+ # "edit" and "remove".
667 self.call_view(env_name='ec2-west')
668 buttons = self.get_control_buttons()
669 captions = map(cli_helpers.get_button_caption, buttons)
670- self.assertEqual(['back', 'use', 'set default', 'remove'], captions)
671+ self.assertEqual(
672+ ['back', 'use', 'set default', 'edit', 'remove'], captions)
673
674 def test_view_buttons_default(self):
675 # If the environment is the default one, the "set default" button is
676- # not displayed. The buttons we expect are "back", "use" and "remove".
677+ # not displayed. The buttons we expect are "back", "use", "edit" and
678+ # "remove".
679 self.call_view(env_name='lxc')
680 buttons = self.get_control_buttons()
681 captions = map(cli_helpers.get_button_caption, buttons)
682- self.assertEqual(['back', 'use', 'remove'], captions)
683+ self.assertEqual(['back', 'use', 'edit', 'remove'], captions)
684
685 def test_view_buttons_error(self):
686 # If the environment is not valid, the "use" button is not displayed.
687- # The buttons we expect are "back", "set default" and "remove".
688+ # The buttons we expect are "back", "set default", "edit" and "remove".
689 self.call_view(env_name='local-with-errors')
690 buttons = self.get_control_buttons()
691 captions = map(cli_helpers.get_button_caption, buttons)
692- self.assertEqual(['back', 'set default', 'remove'], captions)
693+ self.assertEqual(['back', 'set default', 'edit', 'remove'], captions)
694
695 @mock.patch('quickstart.cli.views.env_index')
696 def test_back_button(self, mock_env_index):
697@@ -323,6 +360,17 @@
698 # The new env_db has been saved.
699 self.save_callable.assert_called_once_with(new_env_db)
700
701+ @mock.patch('quickstart.cli.views.env_edit')
702+ def test_edit_button(self, mock_env_edit):
703+ # The edit view is called if the "edit" button is clicked.
704+ self.call_view(env_name='ec2-west')
705+ # The "edit" button is the fourth one.
706+ edit_button = self.get_control_buttons()[3]
707+ cli_helpers.emit(edit_button)
708+ mock_env_edit.assert_called_once_with(
709+ self.app, self.env_type_db, self.env_db, self.save_callable,
710+ self.env_data)
711+
712 def test_remove_button(self):
713 # A confirmation dialog is displayed if the "remove" button is clicked.
714 self.call_view(env_name='ec2-west')
715
716=== modified file 'quickstart/tests/models/test_envs.py'
717--- quickstart/tests/models/test_envs.py 2013-12-19 17:00:36 +0000
718+++ quickstart/tests/models/test_envs.py 2013-12-24 19:19:29 +0000
719@@ -386,6 +386,20 @@
720 self.env_db['environments']['new-one'])
721 self.assertEqual('new-one', self.env_db['default'])
722
723+ def test_new_environment_with_no_default(self):
724+ # A new environment is properly added in an env_db with no default.
725+ env_data = {
726+ 'default-series': 'edgy',
727+ 'is-default': False,
728+ 'name': 'new-one',
729+ }
730+ del self.env_db['default']
731+ envs.set_env_data(self.env_db, None, env_data)
732+ self.assertEqual(
733+ {'default-series': 'edgy'},
734+ self.env_db['environments']['new-one'])
735+ self.assertNotIn('default', self.env_db)
736+
737 def test_existing_environment_updated(self):
738 # An existing environment is properly updated.
739 env_data = {

Subscribers

People subscribed via source and target branches