diff options
| author | Andrew Godwin <andrew@aeracode.org> | 2013-05-18 13:48:57 +0200 |
|---|---|---|
| committer | Andrew Godwin <andrew@aeracode.org> | 2013-05-18 13:48:57 +0200 |
| commit | 028bbd15cad0e48d2b679b986e90c6af0f7e28a0 (patch) | |
| tree | 212204389adb44bfab60b3d3efd34965d7e429bc /django | |
| parent | 331546f6ee7f50a92c01f919e1bb4bea6ed32625 (diff) | |
Add a deconstruct() method to Fields.
This allows the field's initial argument to be obtained so it can be
serialised to, and re-created from, a textual format.
Diffstat (limited to 'django')
| -rw-r--r-- | django/db/models/fields/__init__.py | 188 | ||||
| -rw-r--r-- | django/db/models/fields/files.py | 19 | ||||
| -rw-r--r-- | django/db/models/fields/related.py | 41 |
3 files changed, 247 insertions, 1 deletions
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index b1c1601300..86a0711d7c 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -99,7 +99,8 @@ class Field(object): db_tablespace=None, auto_created=False, validators=[], error_messages=None): self.name = name - self.verbose_name = verbose_name + self.verbose_name = verbose_name # May be set by set_attributes_from_name + self._verbose_name = verbose_name # Store original for deconstruction self.primary_key = primary_key self.max_length, self._unique = max_length, unique self.blank, self.null = blank, null @@ -128,14 +129,99 @@ class Field(object): self.creation_counter = Field.creation_counter Field.creation_counter += 1 + self._validators = validators # Store for deconstruction later self.validators = self.default_validators + validators messages = {} for c in reversed(self.__class__.__mro__): messages.update(getattr(c, 'default_error_messages', {})) messages.update(error_messages or {}) + self._error_messages = error_messages # Store for deconstruction later self.error_messages = messages + def deconstruct(self): + """ + Returns enough information to recreate the field as a 4-tuple: + + * The name of the field on the model, if contribute_to_class has been run + * The import path of the field, including the class: django.db.models.IntegerField + This should be the most portable version, so less specific may be better. + * A list of positional arguments + * A dict of keyword arguments + + Note that the positional or keyword arguments must contain values of the + following types (including inner values of collection types): + + * None, bool, str, unicode, int, long, float, complex, set, frozenset, list, tuple, dict + * UUID + * datetime.datetime (naive), datetime.date + * top-level classes, top-level functions - will be referenced by their full import path + * Storage instances - these have their own deconstruct() method + + This is because the values here must be serialised into a text format + (possibly new Python code, possibly JSON) and these are the only types + with encoding handlers defined. + + There's no need to return the exact way the field was instantiated this time, + just ensure that the resulting field is the same - prefer keyword arguments + over positional ones, and omit parameters with their default values. + """ + # Short-form way of fetching all the default parameters + keywords = {} + possibles = { + "verbose_name": None, + "primary_key": False, + "max_length": None, + "unique": False, + "blank": False, + "null": False, + "db_index": False, + "default": NOT_PROVIDED, + "editable": True, + "serialize": True, + "unique_for_date": None, + "unique_for_month": None, + "unique_for_year": None, + "choices": [], + "help_text": '', + "db_column": None, + "db_tablespace": settings.DEFAULT_INDEX_TABLESPACE, + "auto_created": False, + "validators": [], + "error_messages": None, + } + attr_overrides = { + "unique": "_unique", + "choices": "_choices", + "error_messages": "_error_messages", + "validators": "_validators", + "verbose_name": "_verbose_name", + } + equals_comparison = set(["choices", "validators", "db_tablespace"]) + for name, default in possibles.items(): + value = getattr(self, attr_overrides.get(name, name)) + if name in equals_comparison: + if value != default: + keywords[name] = value + else: + if value is not default: + keywords[name] = value + # Work out path - we shorten it for known Django core fields + path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__) + if path.startswith("django.db.models.fields.related"): + path = path.replace("django.db.models.fields.related", "django.db.models") + if path.startswith("django.db.models.fields.files"): + path = path.replace("django.db.models.fields.files", "django.db.models") + if path.startswith("django.db.models.fields"): + path = path.replace("django.db.models.fields", "django.db.models") + # Return basic info - other fields should override this. + return ( + self.name, + path, + [], + keywords, + ) + def __eq__(self, other): # Needed for @total_ordering if isinstance(other, Field): @@ -584,6 +670,7 @@ class Field(object): return '<%s: %s>' % (path, name) return '<%s>' % path + class AutoField(Field): description = _("Integer") @@ -598,6 +685,12 @@ class AutoField(Field): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(AutoField, self).deconstruct() + del kwargs['blank'] + kwargs['primary_key'] = True + return name, path, args, kwargs + def get_internal_type(self): return "AutoField" @@ -645,6 +738,11 @@ class BooleanField(Field): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(BooleanField, self).deconstruct() + del kwargs['blank'] + return name, path, args, kwargs + def get_internal_type(self): return "BooleanField" @@ -745,6 +843,18 @@ class DateField(Field): kwargs['blank'] = True Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(DateField, self).deconstruct() + if self.auto_now: + kwargs['auto_now'] = True + del kwargs['editable'] + del kwargs['blank'] + if self.auto_now_add: + kwargs['auto_now_add'] = True + del kwargs['editable'] + del kwargs['blank'] + return name, path, args, kwargs + def get_internal_type(self): return "DateField" @@ -924,6 +1034,14 @@ class DecimalField(Field): self.max_digits, self.decimal_places = max_digits, decimal_places Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(DecimalField, self).deconstruct() + if self.max_digits: + kwargs['max_digits'] = self.max_digits + if self.decimal_places: + kwargs['decimal_places'] = self.decimal_places + return name, path, args, kwargs + def get_internal_type(self): return "DecimalField" @@ -983,6 +1101,12 @@ class EmailField(CharField): kwargs['max_length'] = kwargs.get('max_length', 75) CharField.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(EmailField, self).deconstruct() + if kwargs.get("max_length", None) == 75: + del kwargs['max_length'] + return name, path, args, kwargs + def formfield(self, **kwargs): # As with CharField, this will cause email validation to be performed # twice. @@ -1002,6 +1126,22 @@ class FilePathField(Field): kwargs['max_length'] = kwargs.get('max_length', 100) Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(FilePathField, self).deconstruct() + if self.path != '': + kwargs['path'] = self.path + if self.match is not None: + kwargs['match'] = self.match + if self.recursive is not False: + kwargs['recursive'] = self.recursive + if self.allow_files is not True: + kwargs['allow_files'] = self.allow_files + if self.allow_folders is not False: + kwargs['allow_folders'] = self.allow_folders + if kwargs.get("max_length", None) == 100: + del kwargs["max_length"] + return name, path, args, kwargs + def formfield(self, **kwargs): defaults = { 'path': self.path, @@ -1103,6 +1243,11 @@ class IPAddressField(Field): kwargs['max_length'] = 15 Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(IPAddressField, self).deconstruct() + del kwargs['max_length'] + return name, path, args, kwargs + def get_internal_type(self): return "IPAddressField" @@ -1119,12 +1264,23 @@ class GenericIPAddressField(Field): def __init__(self, verbose_name=None, name=None, protocol='both', unpack_ipv4=False, *args, **kwargs): self.unpack_ipv4 = unpack_ipv4 + self.protocol = protocol self.default_validators, invalid_error_message = \ validators.ip_address_validators(protocol, unpack_ipv4) self.default_error_messages['invalid'] = invalid_error_message kwargs['max_length'] = 39 Field.__init__(self, verbose_name, name, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(GenericIPAddressField, self).deconstruct() + if self.unpack_ipv4 is not False: + kwargs['unpack_ipv4'] = self.unpack_ipv4 + if self.protocol != "both": + kwargs['protocol'] = self.protocol + if kwargs.get("max_length", None) == 39: + del kwargs['max_length'] + return name, path, args, kwargs + def get_internal_type(self): return "GenericIPAddressField" @@ -1165,6 +1321,12 @@ class NullBooleanField(Field): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(NullBooleanField, self).deconstruct() + del kwargs['null'] + del kwargs['blank'] + return name, path, args, kwargs + def get_internal_type(self): return "NullBooleanField" @@ -1238,6 +1400,16 @@ class SlugField(CharField): kwargs['db_index'] = True super(SlugField, self).__init__(*args, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(SlugField, self).deconstruct() + if kwargs.get("max_length", None) == 50: + del kwargs['max_length'] + if self.db_index is False: + kwargs['db_index'] = False + else: + del kwargs['db_index'] + return name, path, args, kwargs + def get_internal_type(self): return "SlugField" @@ -1286,6 +1458,14 @@ class TimeField(Field): kwargs['blank'] = True Field.__init__(self, verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(TimeField, self).deconstruct() + if self.auto_now is not False: + kwargs["auto_now"] = self.auto_now + if self.auto_now_add is not False: + kwargs["auto_now_add"] = self.auto_now_add + return name, path, args, kwargs + def get_internal_type(self): return "TimeField" @@ -1345,6 +1525,12 @@ class URLField(CharField): CharField.__init__(self, verbose_name, name, **kwargs) self.validators.append(validators.URLValidator()) + def deconstruct(self): + name, path, args, kwargs = super(URLField, self).deconstruct() + if kwargs.get("max_length", None) == 200: + del kwargs['max_length'] + return name, path, args, kwargs + def formfield(self, **kwargs): # As with CharField, this will cause URL validation to be performed # twice. diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index e631f177e9..0a913e908b 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -227,6 +227,17 @@ class FileField(Field): kwargs['max_length'] = kwargs.get('max_length', 100) super(FileField, self).__init__(verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(FileField, self).deconstruct() + if kwargs.get("max_length", None) != 100: + kwargs["max_length"] = 100 + else: + del kwargs["max_length"] + kwargs['upload_to'] = self.upload_to + if self.storage is not default_storage: + kwargs['storage'] = self.storage + return name, path, args, kwargs + def get_internal_type(self): return "FileField" @@ -326,6 +337,14 @@ class ImageField(FileField): self.width_field, self.height_field = width_field, height_field super(ImageField, self).__init__(verbose_name, name, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(ImageField, self).deconstruct() + if self.width_field: + kwargs['width_field'] = self.width_field + if self.height_field: + kwargs['height_field'] = self.height_field + return name, path, args, kwargs + def contribute_to_class(self, cls, name): super(ImageField, self).contribute_to_class(cls, name) # Attach update_dimension_fields so that dimension fields declared diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 37fa8b1027..5ef713e5e6 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1146,6 +1146,27 @@ class ForeignKey(ForeignObject): ) super(ForeignKey, self).__init__(to, ['self'], [to_field], **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(ForeignKey, self).deconstruct() + # Handle the simpler arguments + if self.db_index: + del kwargs['db_index'] + else: + kwargs['db_index'] = False + if self.db_constraint is not True: + kwargs['db_constraint'] = self.db_constraint + if self.rel.on_delete is not CASCADE: + kwargs['on_delete'] = self.rel.on_delete + # Rel needs more work. + rel = self.rel + if self.rel.field_name: + kwargs['to_field'] = self.rel.field_name + if isinstance(self.rel.to, basestring): + kwargs['to'] = self.rel.to + else: + kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name) + return name, path, args, kwargs + @property def related_field(self): return self.foreign_related_fields[0] @@ -1263,6 +1284,12 @@ class OneToOneField(ForeignKey): kwargs['unique'] = True super(OneToOneField, self).__init__(to, to_field, OneToOneRel, **kwargs) + def deconstruct(self): + name, path, args, kwargs = super(OneToOneField, self).deconstruct() + if "unique" in kwargs: + del kwargs['unique'] + return name, path, args, kwargs + def contribute_to_related_class(self, cls, related): setattr(cls, related.get_accessor_name(), SingleRelatedObjectDescriptor(related)) @@ -1355,6 +1382,20 @@ class ManyToManyField(RelatedField): msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.') self.help_text = string_concat(self.help_text, ' ', msg) + def deconstruct(self): + name, path, args, kwargs = super(ManyToManyField, self).deconstruct() + # Handle the simpler arguments + if self.rel.db_constraint is not True: + kwargs['db_constraint'] = self.db_constraint + del kwargs['help_text'] + # Rel needs more work. + rel = self.rel + if isinstance(self.rel.to, basestring): + kwargs['to'] = self.rel.to + else: + kwargs['to'] = "%s.%s" % (self.rel.to._meta.app_label, self.rel.to._meta.object_name) + return name, path, args, kwargs + def _get_path_info(self, direct=False): """ Called by both direct an indirect m2m traversal. |
