Merge lp:~rvb/maas/bug-1287310 into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 2135
Proposed branch: lp:~rvb/maas/bug-1287310
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 188 lines (+82/-22)
6 files modified
src/maasserver/clusterrpc/power_parameters.py (+5/-0)
src/maasserver/clusterrpc/tests/test_power_parameters.py (+5/-0)
src/maasserver/forms.py (+1/-1)
src/maasserver/forms_settings.py (+1/-21)
src/maasserver/utils/forms.py (+37/-0)
src/maasserver/utils/tests/test_forms.py (+33/-0)
To merge this branch: bzr merge lp:~rvb/maas/bug-1287310
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+211336@code.launchpad.net

Commit message

Fix the 'invalid choice' message for JSON-generated field so that the list of valid choices is included in the error message.

Description of the change

Drive-by fixes:
  - move the utility compose_invalid_choice_text into src/maasserver/utils/forms.py
  - fix the message generated by compose_invalid_choice_text so that the valid choices are quoted.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks good, nice improvement.

[1]

146     +    return "'%s' is not a valid %s.  It should be one of: %s." % (
147     +        "%(value)s",
148     +        choice_of_what,
149     +        ", ".join("'%s'" % name for name, value in valid_choices),
150     +    )

Consider using englist_list() from Launchpad to produce an even better
string of choices. I suspect Django has something similar though.

[2]

148     +        choice_of_what,
149     +        ", ".join("'%s'" % name for name, value in valid_choices),
150     +    )

Is valid_choices sorted?

[3]

146     +    return "'%s' is not a valid %s.  It should be one of: %s." % (
147     +        "%(value)s",

Or double-up the percent symbol:

    return "'%%(value)s' is not a valid %s.  It should be one of: %s." % (

[4]

182     +        choices = [
183     +            (factory.make_name('value'), factory.make_name('key'))
184     +            for _ in range(2)]

I think value and key are the wrong way round here.

review: Approve
Revision history for this message
Raphaël Badin (rvb) wrote :

> Looks good, nice improvement.

Thanks for the review!

> [1]
>
> 146     +    return "'%s' is not a valid %s.  It should be one of: %s." % (
> 147     +        "%(value)s",
> 148     +        choice_of_what,
> 149     +        ", ".join("'%s'" % name for name, value in valid_choices),
> 150     +    )
>
> Consider using englist_list() from Launchpad to produce an even better
> string of choices. I suspect Django has something similar though.

Not that I can find right now. I'll keep it like this for now… we can always improve it later.

> [2]
>
> 148     +        choice_of_what,
> 149     +        ", ".join("'%s'" % name for name, value in valid_choices),
> 150     +    )
>
> Is valid_choices sorted?

No, but it's a list of tuples so it's better to just keep the ordering of that list.

> [3]
>
> 146     +    return "'%s' is not a valid %s.  It should be one of: %s." % (
> 147     +        "%(value)s",
>
> Or double-up the percent symbol:

Hum, didn't know that trick!

> [4]
>
> 182     +        choices = [
> 183     +            (factory.make_name('value'), factory.make_name('key'))
> 184     +            for _ in range(2)]
>
> I think value and key are the wrong way round here.

Well spotted, fixed.

Revision history for this message
MAAS Lander (maas-lander) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Tuesday 18 Mar 2014 06:38:44 you wrote:
> > Consider using englist_list() from Launchpad to produce an even better
> > string of choices. I suspect Django has something similar though.
>
> Not that I can find right now. I'll keep it like this for now… we can
> always improve it later.

He's just punting some code he wrote years ago in LP :)

(not that I am bitter that it was chosen in favour of mine because he used the
Oxford comma, apparently there were better things to worry about)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/clusterrpc/power_parameters.py'
2--- src/maasserver/clusterrpc/power_parameters.py 2014-03-11 06:55:34 +0000
3+++ src/maasserver/clusterrpc/power_parameters.py 2014-03-18 06:39:09 +0000
4@@ -40,6 +40,7 @@
5 from maasserver.clusterrpc.utils import call_clusters
6 from maasserver.config_forms import DictCharField
7 from maasserver.fields import MACAddressFormField
8+from maasserver.utils.forms import compose_invalid_choice_text
9 from provisioningserver.power_schema import (
10 JSON_POWER_TYPE_SCHEMA,
11 POWER_TYPE_PARAMETER_FIELD_SCHEMA,
12@@ -66,9 +67,13 @@
13 field_class = FIELD_TYPE_MAPPINGS.get(
14 json_field['field_type'], forms.CharField)
15 if json_field['field_type'] == 'choice':
16+ invalid_choice_message = compose_invalid_choice_text(
17+ json_field['name'], json_field['choices'])
18 extra_parameters = {
19 'choices': json_field['choices'],
20 'initial': json_field['default'],
21+ 'error_messages': {
22+ 'invalid_choice': invalid_choice_message},
23 }
24 else:
25 extra_parameters = {}
26
27=== modified file 'src/maasserver/clusterrpc/tests/test_power_parameters.py'
28--- src/maasserver/clusterrpc/tests/test_power_parameters.py 2014-03-07 06:20:27 +0000
29+++ src/maasserver/clusterrpc/tests/test_power_parameters.py 2014-03-18 06:39:09 +0000
30@@ -33,6 +33,7 @@
31 MAASServerTestCase,
32 MAASTestCase,
33 )
34+from maasserver.utils.forms import compose_invalid_choice_text
35 from maastesting.matchers import MockCalledOnceWith
36 from mock import sentinel
37 from provisioningserver.power.poweraction import PowerAction
38@@ -142,6 +143,10 @@
39 django_field = make_form_field(json_field)
40 self.assertIsInstance(django_field, forms.ChoiceField)
41 self.assertEqual(json_field['choices'], django_field.choices)
42+ invalid_msg = compose_invalid_choice_text(
43+ json_field['name'], json_field['choices'])
44+ self.assertEqual(
45+ invalid_msg, django_field.error_messages['invalid_choice'])
46 self.assertEqual(json_field['default'], django_field.initial)
47
48 def test_creates_mac_address_field_for_mac_addresses(self):
49
50=== modified file 'src/maasserver/forms.py'
51--- src/maasserver/forms.py 2014-03-11 06:46:26 +0000
52+++ src/maasserver/forms.py 2014-03-18 06:39:09 +0000
53@@ -83,7 +83,6 @@
54 NodeGroupFormField,
55 )
56 from maasserver.forms_settings import (
57- compose_invalid_choice_text,
58 CONFIG_ITEMS_KEYS,
59 get_config_field,
60 INVALID_DISTRO_SERIES_MESSAGE,
61@@ -109,6 +108,7 @@
62 compile_node_actions,
63 )
64 from maasserver.utils import strip_domain
65+from maasserver.utils.forms import compose_invalid_choice_text
66 from maasserver.utils.network import make_network
67 from metadataserver.fields import Bin
68 from metadataserver.models import CommissioningScript
69
70=== modified file 'src/maasserver/forms_settings.py'
71--- src/maasserver/forms_settings.py 2014-02-27 14:37:46 +0000
72+++ src/maasserver/forms_settings.py 2014-03-18 06:39:09 +0000
73@@ -13,7 +13,6 @@
74
75 __metaclass__ = type
76 __all__ = [
77- 'compose_invalid_choice_text',
78 'CONFIG_ITEMS',
79 'CONFIG_ITEMS_KEYS',
80 'get_config_field',
81@@ -29,26 +28,7 @@
82 DISTRO_SERIES,
83 DISTRO_SERIES_CHOICES,
84 )
85-
86-
87-def compose_invalid_choice_text(choice_of_what, valid_choices):
88- """Compose an "invalid choice" string for form error messages.
89-
90- This returns a template string that is intended to be used as the
91- argument to the 'error_messages' parameter in a Django form.
92-
93- :param choice_of_what: The name for what the selected item is supposed
94- to be, to be inserted into the error string.
95- :type choice_of_what: unicode
96- :param valid_choices: Valid choices, in Django choices format:
97- (name, value).
98- :type valid_choices: sequence
99- """
100- return "%s is not a valid %s. It should be one of: %s." % (
101- "%(value)s",
102- choice_of_what,
103- ", ".join(name for name, value in valid_choices),
104- )
105+from maasserver.utils.forms import compose_invalid_choice_text
106
107
108 INVALID_URL_MESSAGE = "Enter a valid url (e.g. http://host.example.com)."
109
110=== added file 'src/maasserver/utils/forms.py'
111--- src/maasserver/utils/forms.py 1970-01-01 00:00:00 +0000
112+++ src/maasserver/utils/forms.py 2014-03-18 06:39:09 +0000
113@@ -0,0 +1,37 @@
114+# Copyright 2014 Canonical Ltd. This software is licensed under the
115+# GNU Affero General Public License version 3 (see the file LICENSE).
116+
117+"""Form utilities."""
118+
119+from __future__ import (
120+ absolute_import,
121+ print_function,
122+ unicode_literals,
123+)
124+
125+str = None
126+
127+__metaclass__ = type
128+__all__ = [
129+ 'compose_invalid_choice_text',
130+]
131+
132+
133+def compose_invalid_choice_text(choice_of_what, valid_choices):
134+ """Compose an "invalid choice" string for form error messages.
135+
136+ This returns a template string that is intended to be used as the
137+ argument to the 'error_messages' parameter in a Django form.
138+
139+ :param choice_of_what: The name for what the selected item is supposed
140+ to be, to be inserted into the error string.
141+ :type choice_of_what: unicode
142+ :param valid_choices: Valid choices, in Django choices format:
143+ (name, value).
144+ :type valid_choices: sequence
145+ """
146+ return "'%s' is not a valid %s. It should be one of: %s." % (
147+ "%(value)s",
148+ choice_of_what,
149+ ", ".join("'%s'" % name for name, value in valid_choices),
150+ )
151
152=== added file 'src/maasserver/utils/tests/test_forms.py'
153--- src/maasserver/utils/tests/test_forms.py 1970-01-01 00:00:00 +0000
154+++ src/maasserver/utils/tests/test_forms.py 2014-03-18 06:39:09 +0000
155@@ -0,0 +1,33 @@
156+# Copyright 2014 Canonical Ltd. This software is licensed under the
157+# GNU Affero General Public License version 3 (see the file LICENSE).
158+
159+"""Tests for forms helpers."""
160+
161+from __future__ import (
162+ absolute_import,
163+ print_function,
164+ unicode_literals,
165+ )
166+
167+str = None
168+
169+__metaclass__ = type
170+__all__ = []
171+
172+
173+from maasserver.testing.factory import factory
174+from maasserver.utils.forms import compose_invalid_choice_text
175+from maastesting.testcase import MAASTestCase
176+from testtools.matchers import ContainsAll
177+
178+
179+class TestComposeInvalidChoiceText(MAASTestCase):
180+
181+ def test_map_enum_includes_all_enum_values(self):
182+ choices = [
183+ (factory.make_name('key'), factory.make_name('value'))
184+ for _ in range(2)]
185+ msg = compose_invalid_choice_text(factory.make_name(), choices)
186+ self.assertThat(
187+ msg,
188+ ContainsAll(["'%s'" % key for key, val in choices]))