Commit e565e133 authored by Loic Bistuer's avatar Loic Bistuer Committed by Tim Graham
Browse files

Fixed #21275 -- Fixed a serializer error when generating migrations for contrib.auth.

The migration serializer now looks for a deconstruct method on any object.
parent 28b70425
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -362,7 +362,7 @@ class AbstractUser(AbstractBaseUser, PermissionsMixin):
        help_text=_('Required. 30 characters or fewer. Letters, numbers and '
                    '@/./+/-/_ characters'),
        validators=[
            validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
            validators.RegexValidator(r'^[\w.@+-]+$', _('Enter a valid username.'), 'invalid')
        ])
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
+9 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
import re

from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _, ungettext_lazy
from django.utils.encoding import force_text
from django.utils.ipv6 import is_valid_ipv6_address
@@ -14,6 +15,7 @@ from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit
EMPTY_VALUES = (None, '', [], (), {})


@deconstructible
class RegexValidator(object):
    regex = ''
    message = _('Enter a valid value.')
@@ -39,6 +41,7 @@ class RegexValidator(object):
            raise ValidationError(self.message, code=self.code)


@deconstructible
class URLValidator(RegexValidator):
    regex = re.compile(
        r'^(?:http|ftp)s?://'  # http:// or https://
@@ -77,6 +80,7 @@ def validate_integer(value):
        raise ValidationError(_('Enter a valid integer.'), code='invalid')


@deconstructible
class EmailValidator(object):
    message = _('Enter a valid email address.')
    code = 'invalid'
@@ -173,6 +177,7 @@ comma_separated_int_list_re = re.compile('^[\d,]+$')
validate_comma_separated_integer_list = RegexValidator(comma_separated_int_list_re, _('Enter only digits separated by commas.'), 'invalid')


@deconstructible
class BaseValidator(object):
    compare = lambda self, a, b: a is not b
    clean = lambda self, x: x
@@ -189,18 +194,21 @@ class BaseValidator(object):
            raise ValidationError(self.message, code=self.code, params=params)


@deconstructible
class MaxValueValidator(BaseValidator):
    compare = lambda self, a, b: a > b
    message = _('Ensure this value is less than or equal to %(limit_value)s.')
    code = 'max_value'


@deconstructible
class MinValueValidator(BaseValidator):
    compare = lambda self, a, b: a < b
    message = _('Ensure this value is greater than or equal to %(limit_value)s.')
    code = 'min_value'


@deconstructible
class MinLengthValidator(BaseValidator):
    compare = lambda self, a, b: a < b
    clean = lambda self, x: len(x)
@@ -211,6 +219,7 @@ class MinLengthValidator(BaseValidator):
    code = 'min_length'


@deconstructible
class MaxLengthValidator(BaseValidator):
    compare = lambda self, a, b: a > b
    clean = lambda self, x: len(x)
+3 −2
Original line number Diff line number Diff line
@@ -146,6 +146,9 @@ class MigrationWriter(object):
        elif isinstance(value, models.Field):
            attr_name, path, args, kwargs = value.deconstruct()
            return cls.serialize_deconstructed(path, args, kwargs)
        # Anything that knows how to deconstruct itself.
        elif hasattr(value, 'deconstruct'):
            return cls.serialize_deconstructed(*value.deconstruct())
        # Functions
        elif isinstance(value, (types.FunctionType, types.BuiltinFunctionType)):
            # @classmethod?
@@ -153,8 +156,6 @@ class MigrationWriter(object):
                klass = value.__self__
                module = klass.__module__
                return "%s.%s.%s" % (module, klass.__name__, value.__name__), set(["import %s" % module])
            elif hasattr(value, 'deconstruct'):
                return cls.serialize_deconstructed(*value.deconstruct())
            elif value.__name__ == '<lambda>':
                raise ValueError("Cannot serialize function: lambda")
            elif value.__module__ is None:
+35 −0
Original line number Diff line number Diff line
def deconstructible(*args, **kwargs):
    """
    Class decorator that allow the decorated class to be serialized
    by the migrations subsystem.

    Accepts an optional kwarg `path` to specify the import path.
    """
    path = kwargs.pop('path', None)

    def decorator(klass):
        def __new__(cls, *args, **kwargs):
            # We capture the arguments to make returning them trivial
            obj = super(klass, cls).__new__(cls)
            obj._constructor_args = (args, kwargs)
            return obj

        def deconstruct(obj):
            """
            Returns a 3-tuple of class import path, positional arguments,
            and keyword arguments.
            """
            return (
                path or '%s.%s' % (obj.__class__.__module__, obj.__class__.__name__),
                obj._constructor_args[0],
                obj._constructor_args[1],
            )

        klass.__new__ = staticmethod(__new__)
        klass.deconstruct = deconstruct

        return klass

    if not args:
        return decorator
    return decorator(*args, **kwargs)
+14 −0
Original line number Diff line number Diff line
@@ -6,11 +6,13 @@ import copy
import datetime
import os

from django.core.validators import RegexValidator, EmailValidator
from django.db import models, migrations
from django.db.migrations.writer import MigrationWriter
from django.db.models.loading import cache
from django.test import TestCase, override_settings
from django.utils import six
from django.utils.deconstruct import deconstructible
from django.utils.translation import ugettext_lazy as _


@@ -77,6 +79,18 @@ class WriterTests(TestCase):
        self.assertSerializedEqual(datetime.datetime.today)
        self.assertSerializedEqual(datetime.date.today())
        self.assertSerializedEqual(datetime.date.today)
        # Classes
        validator = RegexValidator(message="hello")
        string, imports = MigrationWriter.serialize(validator)
        self.assertEqual(string, "django.core.validators.RegexValidator(message=%s)" % repr("hello"))
        self.serialize_round_trip(validator)
        validator = EmailValidator(message="hello")  # Test with a subclass.
        string, imports = MigrationWriter.serialize(validator)
        self.assertEqual(string, "django.core.validators.EmailValidator(message=%s)" % repr("hello"))
        self.serialize_round_trip(validator)
        validator = deconstructible(path="custom.EmailValidator")(EmailValidator)(message="hello")
        string, imports = MigrationWriter.serialize(validator)
        self.assertEqual(string, "custom.EmailValidator(message=%s)" % repr("hello"))
        # Django fields
        self.assertSerializedFieldEqual(models.CharField(max_length=255))
        self.assertSerializedFieldEqual(models.TextField(null=True, blank=True))