Loading django/db/models/fields/__init__.py +39 −13 Original line number Diff line number Diff line Loading @@ -113,6 +113,8 @@ class Field(RegisterLookupMixin): "%(date_field_label)s %(lookup_type)s."), } class_lookups = default_lookups.copy() system_check_deprecated_details = None system_check_removed_details = None # Generic field type description, usually overridden by subclasses def _description(self): Loading Loading @@ -191,6 +193,7 @@ class Field(RegisterLookupMixin): errors.extend(self._check_db_index()) errors.extend(self._check_null_allowed_for_primary_keys()) errors.extend(self._check_backend_specific_checks(**kwargs)) errors.extend(self._check_deprecation_details()) return errors def _check_field_name(self): Loading Loading @@ -290,6 +293,34 @@ class Field(RegisterLookupMixin): def _check_backend_specific_checks(self, **kwargs): return connection.validation.check_field(self, **kwargs) def _check_deprecation_details(self): if self.system_check_removed_details is not None: return [ checks.Error( self.system_check_removed_details.get( 'msg', '%s has been removed except for support in historical ' 'migrations.' % self.__class__.__name__ ), hint=self.system_check_removed_details.get('hint'), obj=self, id=self.system_check_removed_details.get('id', 'fields.EXXX'), ) ] elif self.system_check_deprecated_details is not None: return [ checks.Warning( self.system_check_deprecated_details.get( 'msg', '%s has been deprecated.' % self.__class__.__name__ ), hint=self.system_check_deprecated_details.get('hint'), obj=self, id=self.system_check_deprecated_details.get('id', 'fields.WXXX'), ) ] return [] def deconstruct(self): """ Returns enough information to recreate the field as a 4-tuple: Loading Loading @@ -1832,6 +1863,14 @@ class BigIntegerField(IntegerField): class IPAddressField(Field): empty_strings_allowed = False description = _("IPv4 address") system_check_deprecated_details = { 'msg': ( 'IPAddressField has been deprecated. Support for it (except in ' 'historical migrations) will be removed in Django 1.9.' ), 'hint': 'Use GenericIPAddressField instead.', 'id': 'fields.W900', } def __init__(self, *args, **kwargs): kwargs['max_length'] = 15 Loading @@ -1856,19 +1895,6 @@ class IPAddressField(Field): defaults.update(kwargs) return super(IPAddressField, self).formfield(**defaults) def check(self, **kwargs): errors = super(IPAddressField, self).check(**kwargs) errors.append( checks.Warning( 'IPAddressField has been deprecated. Support for it ' '(except in historical migrations) will be removed in Django 1.9.', hint='Use GenericIPAddressField instead.', obj=self, id='fields.W900', ) ) return errors class GenericIPAddressField(Field): empty_strings_allowed = True Loading docs/releases/1.8.txt +3 −0 Original line number Diff line number Diff line Loading @@ -438,6 +438,9 @@ Migrations * Migrations can now :ref:`serialize model managers <using-managers-in-migrations>` as part of the model state. * A :ref:`generic mechanism to handle the deprecation of model fields <migrations-removing-model-fields>` was added. Models ^^^^^^ Loading docs/topics/migrations.txt +48 −0 Original line number Diff line number Diff line Loading @@ -387,6 +387,54 @@ contains a reference to them. On the plus side, methods and managers from these base classes inherit normally, so if you absolutely need access to these you can opt to move them into a superclass. .. _migrations-removing-model-fields: Considerations when removing model fields ----------------------------------------- .. versionadded:: 1.8 Similar to the "references to historical functions" considerations described in the previous section, removing custom model fields from your project or third-party app will cause a problem if they are referenced in old migrations. To help with this situation, Django provides some model field attributes to assist with model field deprecation using the :doc:`system checks framework </topics/checks>`. Add the ``system_check_deprecated_details`` attribute to your model field similar to the following:: class IPAddressField(Field): system_check_deprecated_details = { 'msg': ( 'IPAddressField has been deprecated. Support for it (except ' 'in historical migrations) will be removed in Django 1.9.' ), 'hint': 'Use GenericIPAddressField instead.', # optional 'id': 'fields.W900', # pick a unique ID for your field. } After a deprecation period of your choosing (two major releases for fields in Django itself), change the ``system_check_deprecated_details`` attribute to ``system_check_removed_details`` and update the dictionary similar to:: class IPAddressField(Field): system_check_removed_details = { 'msg': ( 'IPAddressField has been removed except for support in ' 'historical migrations.' ), 'hint': 'Use GenericIPAddressField instead.', 'id': 'fields.E900', # pick a unique ID for your field. } You should keep the field's methods that are required for it to operate in database migrations such as ``__init__()``, ``deconstruct()``, and ``get_internal_type()``. Keep this stub field for as long as any migrations which reference the field exist. For example, after squashing migrations and removing the old ones, you should be able to remove the field completely. .. _data-migrations: Data Migrations Loading tests/check_framework/test_model_field_deprecation.py 0 → 100644 +83 −0 Original line number Diff line number Diff line from django.db import models from django.core import checks from django.test import SimpleTestCase class TestDeprecatedField(SimpleTestCase): def test_default_details(self): class MyField(models.Field): system_check_deprecated_details = {} class Model(models.Model): name = MyField() model = Model() self.assertEqual(model.check(), [ checks.Warning( msg='MyField has been deprecated.', hint=None, obj=Model._meta.get_field('name'), id='fields.WXXX', ) ]) def test_user_specified_details(self): class MyField(models.Field): system_check_deprecated_details = { 'msg': 'This field is deprecated and will be removed soon.', 'hint': 'Use something else.', 'id': 'fields.W999', } class Model(models.Model): name = MyField() model = Model() self.assertEqual(model.check(), [ checks.Warning( msg='This field is deprecated and will be removed soon.', hint='Use something else.', obj=Model._meta.get_field('name'), id='fields.W999', ) ]) class TestRemovedField(SimpleTestCase): def test_default_details(self): class MyField(models.Field): system_check_removed_details = {} class Model(models.Model): name = MyField() model = Model() self.assertEqual(model.check(), [ checks.Error( msg='MyField has been removed except for support in historical migrations.', hint=None, obj=Model._meta.get_field('name'), id='fields.EXXX', ) ]) def test_user_specified_details(self): class MyField(models.Field): system_check_removed_details = { 'msg': 'Support for this field is gone.', 'hint': 'Use something else.', 'id': 'fields.E999', } class Model(models.Model): name = MyField() model = Model() self.assertEqual(model.check(), [ checks.Error( msg='Support for this field is gone.', hint='Use something else.', obj=Model._meta.get_field('name'), id='fields.E999', ) ]) Loading
django/db/models/fields/__init__.py +39 −13 Original line number Diff line number Diff line Loading @@ -113,6 +113,8 @@ class Field(RegisterLookupMixin): "%(date_field_label)s %(lookup_type)s."), } class_lookups = default_lookups.copy() system_check_deprecated_details = None system_check_removed_details = None # Generic field type description, usually overridden by subclasses def _description(self): Loading Loading @@ -191,6 +193,7 @@ class Field(RegisterLookupMixin): errors.extend(self._check_db_index()) errors.extend(self._check_null_allowed_for_primary_keys()) errors.extend(self._check_backend_specific_checks(**kwargs)) errors.extend(self._check_deprecation_details()) return errors def _check_field_name(self): Loading Loading @@ -290,6 +293,34 @@ class Field(RegisterLookupMixin): def _check_backend_specific_checks(self, **kwargs): return connection.validation.check_field(self, **kwargs) def _check_deprecation_details(self): if self.system_check_removed_details is not None: return [ checks.Error( self.system_check_removed_details.get( 'msg', '%s has been removed except for support in historical ' 'migrations.' % self.__class__.__name__ ), hint=self.system_check_removed_details.get('hint'), obj=self, id=self.system_check_removed_details.get('id', 'fields.EXXX'), ) ] elif self.system_check_deprecated_details is not None: return [ checks.Warning( self.system_check_deprecated_details.get( 'msg', '%s has been deprecated.' % self.__class__.__name__ ), hint=self.system_check_deprecated_details.get('hint'), obj=self, id=self.system_check_deprecated_details.get('id', 'fields.WXXX'), ) ] return [] def deconstruct(self): """ Returns enough information to recreate the field as a 4-tuple: Loading Loading @@ -1832,6 +1863,14 @@ class BigIntegerField(IntegerField): class IPAddressField(Field): empty_strings_allowed = False description = _("IPv4 address") system_check_deprecated_details = { 'msg': ( 'IPAddressField has been deprecated. Support for it (except in ' 'historical migrations) will be removed in Django 1.9.' ), 'hint': 'Use GenericIPAddressField instead.', 'id': 'fields.W900', } def __init__(self, *args, **kwargs): kwargs['max_length'] = 15 Loading @@ -1856,19 +1895,6 @@ class IPAddressField(Field): defaults.update(kwargs) return super(IPAddressField, self).formfield(**defaults) def check(self, **kwargs): errors = super(IPAddressField, self).check(**kwargs) errors.append( checks.Warning( 'IPAddressField has been deprecated. Support for it ' '(except in historical migrations) will be removed in Django 1.9.', hint='Use GenericIPAddressField instead.', obj=self, id='fields.W900', ) ) return errors class GenericIPAddressField(Field): empty_strings_allowed = True Loading
docs/releases/1.8.txt +3 −0 Original line number Diff line number Diff line Loading @@ -438,6 +438,9 @@ Migrations * Migrations can now :ref:`serialize model managers <using-managers-in-migrations>` as part of the model state. * A :ref:`generic mechanism to handle the deprecation of model fields <migrations-removing-model-fields>` was added. Models ^^^^^^ Loading
docs/topics/migrations.txt +48 −0 Original line number Diff line number Diff line Loading @@ -387,6 +387,54 @@ contains a reference to them. On the plus side, methods and managers from these base classes inherit normally, so if you absolutely need access to these you can opt to move them into a superclass. .. _migrations-removing-model-fields: Considerations when removing model fields ----------------------------------------- .. versionadded:: 1.8 Similar to the "references to historical functions" considerations described in the previous section, removing custom model fields from your project or third-party app will cause a problem if they are referenced in old migrations. To help with this situation, Django provides some model field attributes to assist with model field deprecation using the :doc:`system checks framework </topics/checks>`. Add the ``system_check_deprecated_details`` attribute to your model field similar to the following:: class IPAddressField(Field): system_check_deprecated_details = { 'msg': ( 'IPAddressField has been deprecated. Support for it (except ' 'in historical migrations) will be removed in Django 1.9.' ), 'hint': 'Use GenericIPAddressField instead.', # optional 'id': 'fields.W900', # pick a unique ID for your field. } After a deprecation period of your choosing (two major releases for fields in Django itself), change the ``system_check_deprecated_details`` attribute to ``system_check_removed_details`` and update the dictionary similar to:: class IPAddressField(Field): system_check_removed_details = { 'msg': ( 'IPAddressField has been removed except for support in ' 'historical migrations.' ), 'hint': 'Use GenericIPAddressField instead.', 'id': 'fields.E900', # pick a unique ID for your field. } You should keep the field's methods that are required for it to operate in database migrations such as ``__init__()``, ``deconstruct()``, and ``get_internal_type()``. Keep this stub field for as long as any migrations which reference the field exist. For example, after squashing migrations and removing the old ones, you should be able to remove the field completely. .. _data-migrations: Data Migrations Loading
tests/check_framework/test_model_field_deprecation.py 0 → 100644 +83 −0 Original line number Diff line number Diff line from django.db import models from django.core import checks from django.test import SimpleTestCase class TestDeprecatedField(SimpleTestCase): def test_default_details(self): class MyField(models.Field): system_check_deprecated_details = {} class Model(models.Model): name = MyField() model = Model() self.assertEqual(model.check(), [ checks.Warning( msg='MyField has been deprecated.', hint=None, obj=Model._meta.get_field('name'), id='fields.WXXX', ) ]) def test_user_specified_details(self): class MyField(models.Field): system_check_deprecated_details = { 'msg': 'This field is deprecated and will be removed soon.', 'hint': 'Use something else.', 'id': 'fields.W999', } class Model(models.Model): name = MyField() model = Model() self.assertEqual(model.check(), [ checks.Warning( msg='This field is deprecated and will be removed soon.', hint='Use something else.', obj=Model._meta.get_field('name'), id='fields.W999', ) ]) class TestRemovedField(SimpleTestCase): def test_default_details(self): class MyField(models.Field): system_check_removed_details = {} class Model(models.Model): name = MyField() model = Model() self.assertEqual(model.check(), [ checks.Error( msg='MyField has been removed except for support in historical migrations.', hint=None, obj=Model._meta.get_field('name'), id='fields.EXXX', ) ]) def test_user_specified_details(self): class MyField(models.Field): system_check_removed_details = { 'msg': 'Support for this field is gone.', 'hint': 'Use something else.', 'id': 'fields.E999', } class Model(models.Model): name = MyField() model = Model() self.assertEqual(model.check(), [ checks.Error( msg='Support for this field is gone.', hint='Use something else.', obj=Model._meta.get_field('name'), id='fields.E999', ) ])