Merge lp:~rvb/maas/form-error-msg-bug-1308292 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: 2284
Proposed branch: lp:~rvb/maas/form-error-msg-bug-1308292
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 360 lines (+159/-24)
8 files modified
src/maasserver/api.py (+2/-2)
src/maasserver/fields.py (+43/-9)
src/maasserver/forms.py (+34/-5)
src/maasserver/tests/test_api_enlistment.py (+5/-2)
src/maasserver/tests/test_api_macaddress.py (+1/-1)
src/maasserver/tests/test_api_node.py (+3/-3)
src/maasserver/tests/test_fields.py (+31/-0)
src/maasserver/tests/test_forms.py (+40/-2)
To merge this branch: bzr merge lp:~rvb/maas/form-error-msg-bug-1308292
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+216416@code.launchpad.net

Commit message

Make NodeWithMACAddressesForm/AdminNodeWithMACAddressesForm return more meaningful errors when validation fails.

Description of the change

I had to create custom VerboseRegexValidator/VerboseRegexField because the the ones Django provides (RegexValidator/RegexField) don't include the value of the value checked in the error message that is returned when validation fails.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (7.1 KiB)

Nice improvement, thanks! Notes follow:

src/maasserver/fields.py
------------------------

This follows the pattern as it already was, so not a fault of this branch per se, but:

    mac_error_msg = "'%(value)s' is an invalid MAC address."

I often forget this, but I think we were supposed to stick to upper-case names for globals.

Could you make the message say “is not a valid MAC address” instead of “is an invalid MAC address”? The only reason to call the value a MAC address at all here is that the user, mistakenly, presented it as one. Echoing a part of their incorrect belief back to them while contradicting another part implies partial affirmation: “Yes, this is a MAC address like you said, but it's not a valid one.” That could mean “the vendor ID is a reserved value,” or who knows, “there's another NIC with the same MAC out there.” It will all work out in the end, but there is room for confusion.

Of course that does raise questions about the “One or more MAC addresses is invalid” elsewhere in the code. Not as serious, I think, because it's clear that the message doesn't go into the technical details. How about “Malformed value in MAC addresses”?

.

What is VerboseRegexValidator.code for? It looks mysterious. Comment?

.

The docstring for VerboseRegexValidator.__call__ says:

        Validates that the input matches the regular expression.

We normally put the first line of docstring right after the opening quotes, and if it's a single-line docstring and it will fit, we put the closing quotes on the same line. Did you mean to do some more work on this docstring before proposing the branch for merging? Was it originally written for __init__?

Also, this is a function docstring — that sentence should be in the imperative.

.

VerboseRegexField.__init__ has this docstring:

        regex can be either a string or a compiled regular expression object.
        error_message is an optional error message to use, if
        'Enter a valid value' is too generic for you.

That's not very easy to read, for several reasons. The parameter names make it impossible to start the sentences with capitals, so why not use the standard :param: fields? Single non-directional quotes look strange in running text, and resemble apostrophes. The ‘error_message’ parameter is actually just called ‘message.’ And, you're explaining ‘message’ in terms of an obscure default that is hidden away in Django somewhere, irrelevant to this function and, as you point out, uselessly generic. You did the right thing in not supporting the default; now remove it from the docstring!

.

src/maasserver/forms.py
-----------------------

It's a bit odd to have MAX_NUMBER_OF_MESSAGES be so verbose about “number of,” even while it abbreviates “maximum.” Maybe just call it MAX_MESSAGES?

.

I like merge_error_messages, and I don't want to slow you down too much on this, but manipulating text like this is always tricky. It's easy to produce gibberish; you'll know what I mean if you've ever seen MS-DOS or Windows 95 boot in a language other than English.

If you want to concatenate independently produced, stand-alone sentences or even brief paragraphs, they need very cl...

Read more...

review: Approve
Revision history for this message
Raphaël Badin (rvb) wrote :
Download full text (7.9 KiB)

> Nice improvement, thanks! Notes follow:

Thanks for the review!

>
> src/maasserver/fields.py
> ------------------------
>
> This follows the pattern as it already was, so not a fault of this branch per
> se, but:
>
> mac_error_msg = "'%(value)s' is an invalid MAC address."
>
> I often forget this, but I think we were supposed to stick to upper-case names
> for globals.
>
> Could you make the message say “is not a valid MAC address” instead of “is an
> invalid MAC address”? The only reason to call the value a MAC address at all
> here is that the user, mistakenly, presented it as one. Echoing a part of
> their incorrect belief back to them while contradicting another part implies
> partial affirmation: “Yes, this is a MAC address like you said, but it's not a
> valid one.” That could mean “the vendor ID is a reserved value,” or who
> knows, “there's another NIC with the same MAC out there.” It will all work
> out in the end, but there is room for confusion.
>
> Of course that does raise questions about the “One or more MAC addresses is
> invalid” elsewhere in the code. Not as serious, I think, because it's clear
> that the message doesn't go into the technical details. How about “Malformed
> value in MAC addresses”?

I think I prefer “is not a valid MAC address”. And I turned mac_error_msg into MAC_ERROR_MSG and mac_re into MAC_RE.

> What is VerboseRegexValidator.code for? It looks mysterious. Comment?

Done.

>
> .
>
> The docstring for VerboseRegexValidator.__call__ says:
>
> Validates that the input matches the regular expression.
>
> We normally put the first line of docstring right after the opening quotes,
> and if it's a single-line docstring and it will fit, we put the closing quotes
> on the same line. Did you mean to do some more work on this docstring before
> proposing the branch for merging? Was it originally written for __init__?
>
> Also, this is a function docstring — that sentence should be in the
> imperative.

Yeah, I've been lazy, this is a comment I copied straight from Django's code. Fixed.

> .
>
> VerboseRegexField.__init__ has this docstring:
>
> regex can be either a string or a compiled regular expression object.
> error_message is an optional error message to use, if
> 'Enter a valid value' is too generic for you.
>
> That's not very easy to read, for several reasons. The parameter names make
> it impossible to start the sentences with capitals, so why not use the
> standard :param: fields? Single non-directional quotes look strange in
> running text, and resemble apostrophes. The ‘error_message’ parameter is
> actually just called ‘message.’ And, you're explaining ‘message’ in terms of
> an obscure default that is hidden away in Django somewhere, irrelevant to this
> function and, as you point out, uselessly generic. You did the right thing in
> not supporting the default; now remove it from the docstring!

Same here (comment copied from Django), fixed.

> src/maasserver/forms.py
> -----------------------
>
> It's a bit odd to have MAX_NUMBER_OF_MESSAGES be so verbose about “number of,”
> even while it abbreviates “maximum.” Maybe just call i...

Read more...

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

> (Pop text-processing quiz: If x is an all-ASCII unicode string, is x.lower()
> guaranteed to be an all-ASCII unicode string? All code is broken. :-)

Looks like it, what am I missing? http://paste.ubuntu.com/7306338/

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

The Turkish language has two letters “i”: with the dot and without. As I understand it, and it looks very sensible to me, the lower-case version of ‘I’ is ‘ı’ whereas the upper-case version of ‘i’ is ‘İ’.

So presumably, with a Turkish locale, "HI".lower() == "hı"!

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

> The Turkish language has two letters “i”: with the dot and without. As I
> understand it, and it looks very sensible to me, the lower-case version of ‘I’
> is ‘ı’ whereas the upper-case version of ‘i’ is ‘İ’.
>
> So presumably, with a Turkish locale, "HI".lower() == "hı"!

Fail: http://paste.ubuntu.com/7306718/

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

You didn't actually print 'hI'.lower(), just 'hI'.

But yes, it looks like Python doesn't do Turkish upper/lower-casing! Try it using e.g. ‘awk’ and you'll get different results... All software is still broken. :-)

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (2.7 MiB)

The attempt to merge lp:~rvb/maas/form-error-msg-bug-1308292 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Get:2 http://security.ubuntu.com trusty-security Release [58.5 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [58.5 kB]
Get:5 http://security.ubuntu.com trusty-security/main Sources [970 B]
Get:6 http://security.ubuntu.com trusty-security/universe Sources [14 B]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [977 B]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [14 B]
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Ign http://security.ubuntu.com trusty-security/main Translation-en_US
Ign http://security.ubuntu.com trusty-security/universe Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [3,405 B]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [648 B]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [10.1 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [1,602 B]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en_US
Fetched 137 kB in 0s (669 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make postgresql python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-formencode python-httplib2 python-jinja2 python-jsonschem...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (2.7 MiB)

The attempt to merge lp:~rvb/maas/form-error-msg-bug-1308292 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Hit http://security.ubuntu.com trusty-security Release
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Ign http://security.ubuntu.com trusty-security/main Translation-en_US
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Ign http://security.ubuntu.com trusty-security/universe Translation-en_US
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en_US
Fetched 933 B in 0s (5,962 B/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make postgresql python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-formencode python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-netaddr python-netifaces python-oauth python-oops python-oops-amqp python-oops-dated...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (54.2 KiB)

The attempt to merge lp:~rvb/maas/form-error-msg-bug-1308292 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Hit http://security.ubuntu.com trusty-security Release
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Ign http://security.ubuntu.com trusty-security/main Translation-en_US
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Ign http://security.ubuntu.com trusty-security/universe Translation-en_US
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en_US
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make postgresql python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-formencode python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-netaddr python-netifaces python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-ws...

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (54.4 KiB)

The attempt to merge lp:~rvb/maas/form-error-msg-bug-1308292 into lp:maas failed. Below is the output from the failed tests.

Ign http://security.ubuntu.com trusty-security InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Get:2 http://security.ubuntu.com trusty-security Release [58.5 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [58.5 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:5 http://security.ubuntu.com trusty-security/main Sources [972 B]
Get:6 http://security.ubuntu.com trusty-security/universe Sources [14 B]
Get:7 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [3,407 B]
Get:8 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [648 B]
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [10.1 kB]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [1,602 B]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Get:11 http://security.ubuntu.com trusty-security/main amd64 Packages [971 B]
Get:12 http://security.ubuntu.com trusty-security/universe amd64 Packages [14 B]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Ign http://security.ubuntu.com trusty-security/main Translation-en_US
Ign http://security.ubuntu.com trusty-security/universe Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en_US
Fetched 137 kB in 0s (712 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make postgresql python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-formencode python-httplib2 python-jinja2 python-jsonschem...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2014-04-14 23:32:50 +0000
+++ src/maasserver/api.py 2014-04-23 07:31:18 +0000
@@ -172,7 +172,7 @@
172 Unauthorized,172 Unauthorized,
173 )173 )
174from maasserver.fields import (174from maasserver.fields import (
175 mac_re,175 MAC_RE,
176 validate_mac,176 validate_mac,
177 )177 )
178from maasserver.forms import (178from maasserver.forms import (
@@ -785,7 +785,7 @@
785 match_macs = get_optional_list(request.GET, 'mac_address')785 match_macs = get_optional_list(request.GET, 'mac_address')
786 if match_macs is not None:786 if match_macs is not None:
787 invalid_macs = [787 invalid_macs = [
788 mac for mac in match_macs if mac_re.match(mac) is None]788 mac for mac in match_macs if MAC_RE.match(mac) is None]
789 if len(invalid_macs) != 0:789 if len(invalid_macs) != 0:
790 raise ValidationError(790 raise ValidationError(
791 "Invalid MAC address(es): %s" % ", ".join(invalid_macs))791 "Invalid MAC address(es): %s" % ", ".join(invalid_macs))
792792
=== modified file 'src/maasserver/fields.py'
--- src/maasserver/fields.py 2014-02-21 06:59:04 +0000
+++ src/maasserver/fields.py 2014-04-23 07:31:18 +0000
@@ -17,6 +17,7 @@
17 "MACAddressField",17 "MACAddressField",
18 "MACAddressFormField",18 "MACAddressFormField",
19 "register_mac_type",19 "register_mac_type",
20 "VerboseRegexValidator",
20 ]21 ]
2122
22from copy import deepcopy23from copy import deepcopy
@@ -33,20 +34,40 @@
33 SubfieldBase,34 SubfieldBase,
34 )35 )
35from django.forms import (36from django.forms import (
37 CharField,
36 ModelChoiceField,38 ModelChoiceField,
37 RegexField,
38 )39 )
40from django.utils.encoding import force_text
39from maasserver.utils.orm import get_one41from maasserver.utils.orm import get_one
40import psycopg2.extensions42import psycopg2.extensions
41from south.modelsinspector import add_introspection_rules43from south.modelsinspector import add_introspection_rules
4244
4345
44mac_re = re.compile(r'^\s*([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}\s*$')46MAC_RE = re.compile(r'^\s*([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}\s*$')
4547
4648
47mac_error_msg = "Enter a valid MAC address (e.g. AA:BB:CC:DD:EE:FF)."49MAC_ERROR_MSG = "'%(value)s' is not a valid MAC address."
4850
49mac_validator = RegexValidator(regex=mac_re, message=mac_error_msg)51
52class VerboseRegexValidator(RegexValidator):
53 """A verbose `RegexValidator`.
54
55 This `RegexValidator` includes the checked value in the rendered error
56 message when the validation fails.
57 """
58 # Set a bugus code to circumvent Django's attempt to re-interpret a
59 # validator's error message using the field's message it is attached
60 # to.
61 code = 'bogus-code'
62
63 def __call__(self, value):
64 """Validates that the input matches the regular expression."""
65 if not self.regex.search(force_text(value)):
66 raise ValidationError(
67 self.message % {'value': value}, code=self.code)
68
69
70mac_validator = VerboseRegexValidator(regex=MAC_RE, message=MAC_ERROR_MSG)
5071
5172
52def validate_mac(value):73def validate_mac(value):
@@ -157,12 +178,25 @@
157 return nodegroup178 return nodegroup
158179
159180
160class MACAddressFormField(RegexField):181class VerboseRegexField(CharField):
182
183 def __init__(self, regex, message, *args, **kwargs):
184 """A field that validates its value with a regular expression.
185
186 :param regex: Either a string or a compiled regular expression object.
187 :param message: Error message to use when the validation fails.
188 """
189 super(VerboseRegexField, self).__init__(*args, **kwargs)
190 self.validators.append(
191 VerboseRegexValidator(regex=regex, message=message))
192
193
194class MACAddressFormField(VerboseRegexField):
161 """Form field type: MAC address."""195 """Form field type: MAC address."""
162196
163 def __init__(self, *args, **kwargs):197 def __init__(self, *args, **kwargs):
164 super(MACAddressFormField, self).__init__(198 super(MACAddressFormField, self).__init__(
165 regex=mac_re, error_message=mac_error_msg, *args, **kwargs)199 regex=MAC_RE, message=MAC_ERROR_MSG, *args, **kwargs)
166200
167201
168class MACAddressField(Field):202class MACAddressField(Field):
169203
=== modified file 'src/maasserver/forms.py'
--- src/maasserver/forms.py 2014-04-10 20:21:24 +0000
+++ src/maasserver/forms.py 2014-04-23 07:31:18 +0000
@@ -532,6 +532,33 @@
532532
533IP_BASED_HOSTNAME_REGEXP = re.compile('\d{1,3}-\d{1,3}-\d{1,3}-\d{1,3}')533IP_BASED_HOSTNAME_REGEXP = re.compile('\d{1,3}-\d{1,3}-\d{1,3}-\d{1,3}')
534534
535MAX_MESSAGES = 10
536
537
538def merge_error_messages(summary, errors, limit=MAX_MESSAGES):
539 """Merge a collection of errors into a summary message of limited size.
540
541 :param summary: The message summarizing the error.
542 :type summary: unicode
543 :param errors: The list of errors to merge.
544 :type errors: iterable
545 :param limit: The maximum number of individual error messages to include in
546 the summary error message.
547 :type limit: int
548 """
549 ellipsis_msg = ''
550 if len(errors) > limit:
551 nb_errors = len(errors) - limit
552 ellipsis_msg = (
553 " and %d more error%s" % (
554 nb_errors,
555 's' if nb_errors > 1 else ''))
556 return "%s (%s%s)" % (
557 summary,
558 ' \u2014 '.join(errors[:limit]),
559 ellipsis_msg
560 )
561
535562
536class WithMACAddressesMixin:563class WithMACAddressesMixin:
537 """A form mixin which dynamically adds a MultipleMACAddressField to the564 """A form mixin which dynamically adds a MultipleMACAddressField to the
@@ -558,17 +585,19 @@
558 self.errors.get('mac_addresses', None) is not None and585 self.errors.get('mac_addresses', None) is not None and
559 len(self.data['mac_addresses']) > 1)586 len(self.data['mac_addresses']) > 1)
560 if reformat_mac_address_error:587 if reformat_mac_address_error:
561 self.errors['mac_addresses'] = (588 self.errors['mac_addresses'] = [merge_error_messages(
562 ['One or more MAC addresses is invalid.'])589 "One or more MAC addresses is invalid.",
590 self.errors['mac_addresses'])]
563 return valid591 return valid
564592
565 def clean_mac_addresses(self):593 def clean_mac_addresses(self):
566 data = self.cleaned_data['mac_addresses']594 data = self.cleaned_data['mac_addresses']
595 errors = []
567 for mac in data:596 for mac in data:
568 if MACAddress.objects.filter(mac_address=mac.lower()).exists():597 if MACAddress.objects.filter(mac_address=mac.lower()).exists():
569 raise ValidationError(598 errors.append('MAC address %s already in use.' % mac)
570 {'mac_addresses': [599 if errors:
571 'Mac address %s already in use.' % mac]})600 raise ValidationError({'mac_addresses': errors})
572 return data601 return data
573602
574 def save(self):603 def save(self):
575604
=== modified file 'src/maasserver/tests/test_api_enlistment.py'
--- src/maasserver/tests/test_api_enlistment.py 2014-03-27 04:15:45 +0000
+++ src/maasserver/tests/test_api_enlistment.py 2014-04-23 07:31:18 +0000
@@ -280,7 +280,7 @@
280 self.assertEqual(httplib.BAD_REQUEST, response.status_code)280 self.assertEqual(httplib.BAD_REQUEST, response.status_code)
281 self.assertIn('application/json', response['Content-Type'])281 self.assertIn('application/json', response['Content-Type'])
282 self.assertEqual(282 self.assertEqual(
283 ["Mac address %s already in use." % mac],283 ["MAC address %s already in use." % mac],
284 parsed_result['mac_addresses'])284 parsed_result['mac_addresses'])
285285
286 def test_POST_fails_with_bad_operation(self):286 def test_POST_fails_with_bad_operation(self):
@@ -316,7 +316,10 @@
316 self.assertEqual(httplib.BAD_REQUEST, response.status_code)316 self.assertEqual(httplib.BAD_REQUEST, response.status_code)
317 self.assertIn('application/json', response['Content-Type'])317 self.assertIn('application/json', response['Content-Type'])
318 self.assertEqual(318 self.assertEqual(
319 ["One or more MAC addresses is invalid."],319 [
320 "One or more MAC addresses is invalid. "
321 "('invalid' is not a valid MAC address.)"
322 ],
320 parsed_result['mac_addresses'])323 parsed_result['mac_addresses'])
321324
322 def test_POST_invalid_architecture_returns_bad_request(self):325 def test_POST_invalid_architecture_returns_bad_request(self):
323326
=== modified file 'src/maasserver/tests/test_api_macaddress.py'
--- src/maasserver/tests/test_api_macaddress.py 2014-02-20 10:29:15 +0000
+++ src/maasserver/tests/test_api_macaddress.py 2014-04-23 07:31:18 +0000
@@ -120,7 +120,7 @@
120 self.assertEqual(400, response.status_code)120 self.assertEqual(400, response.status_code)
121 self.assertEqual(['mac_address'], list(parsed_result))121 self.assertEqual(['mac_address'], list(parsed_result))
122 self.assertEqual(122 self.assertEqual(
123 ["Enter a valid MAC address (e.g. AA:BB:CC:DD:EE:FF)."],123 ["'invalid-mac' is not a valid MAC address."],
124 parsed_result['mac_address'])124 parsed_result['mac_address'])
125125
126 def test_macs_DELETE_mac(self):126 def test_macs_DELETE_mac(self):
127127
=== modified file 'src/maasserver/tests/test_api_node.py'
--- src/maasserver/tests/test_api_node.py 2014-04-03 16:12:28 +0000
+++ src/maasserver/tests/test_api_node.py 2014-04-23 07:31:18 +0000
@@ -29,7 +29,7 @@
29 )29 )
30from maasserver.fields import (30from maasserver.fields import (
31 MAC,31 MAC,
32 mac_error_msg,32 MAC_ERROR_MSG,
33 )33 )
34from maasserver.models import Node34from maasserver.models import Node
35from maasserver.testing import (35from maasserver.testing import (
@@ -574,11 +574,11 @@
574 response = self.client_put(574 response = self.client_put(
575 self.get_node_uri(node),575 self.get_node_uri(node),
576 {'power_parameters_mac_address': new_power_address})576 {'power_parameters_mac_address': new_power_address})
577577 error_msg = MAC_ERROR_MSG % {'value': new_power_address}
578 self.assertEqual(578 self.assertEqual(
579 (579 (
580 httplib.BAD_REQUEST,580 httplib.BAD_REQUEST,
581 {'power_parameters': ["MAC Address: %s" % mac_error_msg]},581 {'power_parameters': ["MAC Address: %s" % error_msg]},
582 ),582 ),
583 (response.status_code, json.loads(response.content)))583 (response.status_code, json.loads(response.content)))
584584
585585
=== modified file 'src/maasserver/tests/test_fields.py'
--- src/maasserver/tests/test_fields.py 2014-02-21 03:49:40 +0000
+++ src/maasserver/tests/test_fields.py 2014-04-23 07:31:18 +0000
@@ -28,6 +28,8 @@
28 NodeGroupFormField,28 NodeGroupFormField,
29 register_mac_type,29 register_mac_type,
30 validate_mac,30 validate_mac,
31 VerboseRegexField,
32 VerboseRegexValidator,
31 )33 )
32from maasserver.models import (34from maasserver.models import (
33 MACAddress,35 MACAddress,
@@ -222,6 +224,35 @@
222 pass224 pass
223225
224226
227class TestVerboseRegexValidator(MAASServerTestCase):
228
229 def test_VerboseRegexValidator_validates_value(self):
230 validator = VerboseRegexValidator(
231 regex="test", message="Unknown value")
232 self.assertIsNone(validator('test'))
233
234 def test_VerboseRegexValidator_validation_error_includes_value(self):
235 message = "Unknown value: %(value)s"
236 validator = VerboseRegexValidator(regex="test", message=message)
237 value = factory.make_name('value')
238 error = self.assertRaises(ValidationError, validator, value)
239 self.assertEqual(message % {'value': value}, error.message)
240
241
242class TestVerboseRegexField(MAASServerTestCase):
243
244 def test_VerboseRegexField_accepts_valid_value(self):
245 field = VerboseRegexField(regex="test", message="Unknown value")
246 self.assertEqual('test', field.clean('test'))
247
248 def test_VerboseRegexField_validation_error_includes_value(self):
249 message = "Unknown value: %(value)s"
250 field = VerboseRegexField(regex="test", message=message)
251 value = factory.make_name('value')
252 error = self.assertRaises(ValidationError, field.clean, value)
253 self.assertEqual([message % {'value': value}], error.messages)
254
255
225class TestMACAddressField(MAASServerTestCase):256class TestMACAddressField(MAASServerTestCase):
226257
227 def test_mac_address_is_stored_normalized_and_loaded(self):258 def test_mac_address_is_stored_normalized_and_loaded(self):
228259
=== modified file 'src/maasserver/tests/test_forms.py'
--- src/maasserver/tests/test_forms.py 2014-04-10 15:30:11 +0000
+++ src/maasserver/tests/test_forms.py 2014-04-23 07:31:18 +0000
@@ -47,6 +47,8 @@
47 INTERFACES_VALIDATION_ERROR_MESSAGE,47 INTERFACES_VALIDATION_ERROR_MESSAGE,
48 list_all_usable_architectures,48 list_all_usable_architectures,
49 MACAddressForm,49 MACAddressForm,
50 MAX_MESSAGES,
51 merge_error_messages,
50 NetworkForm,52 NetworkForm,
51 NewUserCreationForm,53 NewUserCreationForm,
52 NO_ARCHITECTURES_AVAILABLE,54 NO_ARCHITECTURES_AVAILABLE,
@@ -274,7 +276,7 @@
274 self.assertFalse(form.is_valid())276 self.assertFalse(form.is_valid())
275 self.assertEqual(['mac_addresses'], list(form.errors))277 self.assertEqual(['mac_addresses'], list(form.errors))
276 self.assertEqual(278 self.assertEqual(
277 ['Enter a valid MAC address (e.g. AA:BB:CC:DD:EE:FF).'],279 ["'invalid' is not a valid MAC address."],
278 form.errors['mac_addresses'])280 form.errors['mac_addresses'])
279281
280 def test_NodeWithMACAddressesForm_multiple_invalid(self):282 def test_NodeWithMACAddressesForm_multiple_invalid(self):
@@ -287,7 +289,11 @@
287 self.assertFalse(form.is_valid())289 self.assertFalse(form.is_valid())
288 self.assertEqual(['mac_addresses'], list(form.errors))290 self.assertEqual(['mac_addresses'], list(form.errors))
289 self.assertEqual(291 self.assertEqual(
290 ['One or more MAC addresses is invalid.'],292 [
293 "One or more MAC addresses is invalid. "
294 "('invalid_1' is not a valid MAC address. \u2014"
295 " 'invalid_2' is not a valid MAC address.)"
296 ],
291 form.errors['mac_addresses'])297 form.errors['mac_addresses'])
292298
293 def test_NodeWithMACAddressesForm_empty(self):299 def test_NodeWithMACAddressesForm_empty(self):
@@ -782,6 +788,38 @@
782 list(form.fields))788 list(form.fields))
783789
784790
791class TestMergeErrorMessages(MAASServerTestCase):
792
793 def test_merge_error_messages_returns_summary_message(self):
794 summary = factory.make_name('summary')
795 errors = [factory.make_name('error') for _ in range(2)]
796 result = merge_error_messages(summary, errors, 5)
797 self.assertEqual(
798 "%s (%s)" % (summary, ' \u2014 '.join(errors)), result)
799
800 def test_merge_error_messages_includes_limited_number_of_msgs(self):
801 summary = factory.make_name('summary')
802 errors = [
803 factory.make_name('error')
804 for _ in range(MAX_MESSAGES + 2)]
805 result = merge_error_messages(summary, errors)
806 self.assertEqual(
807 "%s (%s and 2 more errors)" % (
808 summary, ' \u2014 '.join(errors[:MAX_MESSAGES])),
809 result)
810
811 def test_merge_error_messages_with_one_more_error(self):
812 summary = factory.make_name('summary')
813 errors = [
814 factory.make_name('error')
815 for _ in range(MAX_MESSAGES + 1)]
816 result = merge_error_messages(summary, errors)
817 self.assertEqual(
818 "%s (%s and 1 more error)" % (
819 summary, ' \u2014 '.join(errors[:MAX_MESSAGES])),
820 result)
821
822
785class TestMACAddressForm(MAASServerTestCase):823class TestMACAddressForm(MAASServerTestCase):
786824
787 def test_MACAddressForm_creates_mac_address(self):825 def test_MACAddressForm_creates_mac_address(self):