diff --git a/django/apps/config.py b/django/apps/config.py index 928e19ac44f..6d794eee3aa 100644 --- a/django/apps/config.py +++ b/django/apps/config.py @@ -5,6 +5,7 @@ from importlib import import_module from django.core.exceptions import ImproperlyConfigured from django.utils.deprecation import RemovedInDjango41Warning +from django.utils.functional import cached_property from django.utils.module_loading import import_string, module_has_submodule APPS_MODULE_NAME = 'apps' @@ -55,6 +56,15 @@ class AppConfig: def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.label) + @cached_property + def default_auto_field(self): + from django.conf import settings + return settings.DEFAULT_AUTO_FIELD + + @property + def _is_default_auto_field_overridden(self): + return self.__class__.default_auto_field is not AppConfig.default_auto_field + def _path_from_module(self, module): """Attempt to determine app's filesystem path from its module.""" # See #21874 for extended discussion of the behavior of this method in diff --git a/django/conf/app_template/apps.py-tpl b/django/conf/app_template/apps.py-tpl index 9b2ce5289c5..b7053521812 100644 --- a/django/conf/app_template/apps.py-tpl +++ b/django/conf/app_template/apps.py-tpl @@ -2,4 +2,5 @@ from django.apps import AppConfig class {{ camel_case_app_name }}Config(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' name = '{{ app_name }}' diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 381ad63ae62..cf9fae496e3 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -414,6 +414,9 @@ THOUSAND_SEPARATOR = ',' DEFAULT_TABLESPACE = '' DEFAULT_INDEX_TABLESPACE = '' +# Default primary key field type. +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' + # Default X-Frame-Options header value X_FRAME_OPTIONS = 'DENY' diff --git a/django/conf/project_template/project_name/settings.py-tpl b/django/conf/project_template/project_name/settings.py-tpl index 444c899b2b3..7830fb2f3c8 100644 --- a/django/conf/project_template/project_name/settings.py-tpl +++ b/django/conf/project_template/project_name/settings.py-tpl @@ -118,3 +118,8 @@ USE_TZ = True # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/ STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/django/contrib/admin/apps.py b/django/contrib/admin/apps.py index 85e9ff830a8..c4fba8837c1 100644 --- a/django/contrib/admin/apps.py +++ b/django/contrib/admin/apps.py @@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _ class SimpleAdminConfig(AppConfig): """Simple AppConfig which does not do automatic discovery.""" + default_auto_field = 'django.db.models.AutoField' default_site = 'django.contrib.admin.sites.AdminSite' name = 'django.contrib.admin' verbose_name = _("Administration") diff --git a/django/contrib/auth/apps.py b/django/contrib/auth/apps.py index b9d271bb1f0..4e4ef06d271 100644 --- a/django/contrib/auth/apps.py +++ b/django/contrib/auth/apps.py @@ -11,6 +11,7 @@ from .signals import user_logged_in class AuthConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' name = 'django.contrib.auth' verbose_name = _("Authentication and Authorization") diff --git a/django/contrib/contenttypes/apps.py b/django/contrib/contenttypes/apps.py index 1a8e25b98e2..390afb3fcf9 100644 --- a/django/contrib/contenttypes/apps.py +++ b/django/contrib/contenttypes/apps.py @@ -12,6 +12,7 @@ from .management import ( class ContentTypesConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' name = 'django.contrib.contenttypes' verbose_name = _("Content Types") diff --git a/django/contrib/flatpages/apps.py b/django/contrib/flatpages/apps.py index 330ee050636..4f5ef17004c 100644 --- a/django/contrib/flatpages/apps.py +++ b/django/contrib/flatpages/apps.py @@ -3,5 +3,6 @@ from django.utils.translation import gettext_lazy as _ class FlatPagesConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' name = 'django.contrib.flatpages' verbose_name = _("Flat Pages") diff --git a/django/contrib/gis/apps.py b/django/contrib/gis/apps.py index 662ae43d8de..e582e767602 100644 --- a/django/contrib/gis/apps.py +++ b/django/contrib/gis/apps.py @@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _ class GISConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' name = 'django.contrib.gis' verbose_name = _("GIS") diff --git a/django/contrib/redirects/apps.py b/django/contrib/redirects/apps.py index cab67424e72..c1d80ee3c13 100644 --- a/django/contrib/redirects/apps.py +++ b/django/contrib/redirects/apps.py @@ -3,5 +3,6 @@ from django.utils.translation import gettext_lazy as _ class RedirectsConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' name = 'django.contrib.redirects' verbose_name = _("Redirects") diff --git a/django/contrib/sitemaps/apps.py b/django/contrib/sitemaps/apps.py index 502d6890d02..ec795eab877 100644 --- a/django/contrib/sitemaps/apps.py +++ b/django/contrib/sitemaps/apps.py @@ -3,5 +3,6 @@ from django.utils.translation import gettext_lazy as _ class SiteMapsConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' name = 'django.contrib.sitemaps' verbose_name = _("Site Maps") diff --git a/django/contrib/sites/apps.py b/django/contrib/sites/apps.py index ed52d34786e..7f820dcc79c 100644 --- a/django/contrib/sites/apps.py +++ b/django/contrib/sites/apps.py @@ -8,6 +8,7 @@ from .management import create_default_site class SitesConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' name = 'django.contrib.sites' verbose_name = _("Sites") diff --git a/django/db/models/base.py b/django/db/models/base.py index de044886c3e..822aad080db 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1290,10 +1290,35 @@ class Model(metaclass=ModelBase): *cls._check_indexes(databases), *cls._check_ordering(), *cls._check_constraints(databases), + *cls._check_default_pk(), ] return errors + @classmethod + def _check_default_pk(cls): + if ( + cls._meta.pk.auto_created and + not settings.is_overridden('DEFAULT_AUTO_FIELD') and + not cls._meta.app_config._is_default_auto_field_overridden + ): + return [ + checks.Warning( + f"Auto-created primary key used when not defining a " + f"primary key type, by default " + f"'{settings.DEFAULT_AUTO_FIELD}'.", + hint=( + f"Configure the DEFAULT_AUTO_FIELD setting or the " + f"{cls._meta.app_config.__class__.__qualname__}." + f"default_auto_field attribute to point to a subclass " + f"of AutoField, e.g. 'django.db.models.BigAutoField'." + ), + obj=cls, + id='models.W042', + ), + ] + return [] + @classmethod def _check_swappable(cls): """Check if the swapped model exists.""" diff --git a/django/db/models/options.py b/django/db/models/options.py index 0e28b6812ac..4028e05b997 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -5,12 +5,13 @@ from collections import defaultdict from django.apps import apps from django.conf import settings -from django.core.exceptions import FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured from django.db import connections from django.db.models import AutoField, Manager, OrderWrt, UniqueConstraint from django.db.models.query_utils import PathInfo from django.utils.datastructures import ImmutableList, OrderedSet from django.utils.functional import cached_property +from django.utils.module_loading import import_string from django.utils.text import camel_case_to_spaces, format_lazy from django.utils.translation import override @@ -217,6 +218,37 @@ class Options: new_objs.append(obj) return new_objs + def _get_default_pk_class(self): + pk_class_path = getattr( + self.app_config, + 'default_auto_field', + settings.DEFAULT_AUTO_FIELD, + ) + if self.app_config and self.app_config._is_default_auto_field_overridden: + app_config_class = type(self.app_config) + source = ( + f'{app_config_class.__module__}.' + f'{app_config_class.__qualname__}.default_auto_field' + ) + else: + source = 'DEFAULT_AUTO_FIELD' + if not pk_class_path: + raise ImproperlyConfigured(f'{source} must not be empty.') + try: + pk_class = import_string(pk_class_path) + except ImportError as e: + msg = ( + f"{source} refers to the module '{pk_class_path}' that could " + f"not be imported." + ) + raise ImproperlyConfigured(msg) from e + if not issubclass(pk_class, AutoField): + raise ValueError( + f"Primary key '{pk_class_path}' referred by {source} must " + f"subclass AutoField." + ) + return pk_class + def _prepare(self, model): if self.order_with_respect_to: # The app registry will not be ready at this point, so we cannot @@ -250,7 +282,8 @@ class Options: field.primary_key = True self.setup_pk(field) else: - auto = AutoField(verbose_name='ID', primary_key=True, auto_created=True) + pk_class = self._get_default_pk_class() + auto = pk_class(verbose_name='ID', primary_key=True, auto_created=True) model.add_to_class('id', auto) def add_manager(self, manager): diff --git a/docs/ref/applications.txt b/docs/ref/applications.txt index 765e9e8d3c2..2cf175e9f0d 100644 --- a/docs/ref/applications.txt +++ b/docs/ref/applications.txt @@ -90,6 +90,7 @@ would provide a proper name for the admin:: from django.apps import AppConfig class RockNRollConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' name = 'rock_n_roll' verbose_name = "Rock ā€™nā€™ roll" @@ -219,6 +220,16 @@ Configurable attributes By default, this attribute isn't set. +.. attribute:: AppConfig.default_auto_field + + .. versionadded:: 3.2 + + The implicit primary key type to add to models within this app. You can + use this to keep :class:`~django.db.models.AutoField` as the primary key + type for third party applications. + + By default, this is the value of :setting:`DEFAULT_AUTO_FIELD`. + Read-only attributes -------------------- diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index 2ac5d4cb1d8..a6e66d462f5 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -378,6 +378,8 @@ Models * **models.W040**: ```` does not support indexes with non-key columns. * **models.E041**: ``constraints`` refers to the joined field ````. +* **models.W042**: Auto-created primary key used when not defining a primary + key type, by default ``django.db.models.AutoField``. Security -------- diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 93c011fe2cd..759e004fcf1 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -415,9 +415,12 @@ cross-site scripting attack. If ``True``, this field is the primary key for the model. If you don't specify ``primary_key=True`` for any field in your model, Django -will automatically add an :class:`AutoField` to hold the primary key, so you -don't need to set ``primary_key=True`` on any of your fields unless you want to -override the default primary-key behavior. For more, see +will automatically add a field to hold the primary key, so you don't need to +set ``primary_key=True`` on any of your fields unless you want to override the +default primary-key behavior. The type of auto-created primary key fields can +be specified per app in :attr:`AppConfig.default_auto_field +` or globally in the +:setting:`DEFAULT_AUTO_FIELD` setting. For more, see :ref:`automatic-primary-key-fields`. ``primary_key=True`` implies :attr:`null=False ` and @@ -428,6 +431,11 @@ The primary key field is read-only. If you change the value of the primary key on an existing object and then save it, a new object will be created alongside the old one. +.. versionchanged:: 3.2 + + In older versions, auto-created primary key fields were always + :class:`AutoField`\s. + ``unique`` ---------- diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index bfa62f15089..8cb14e97682 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1245,6 +1245,17 @@ format has higher precedence and will be applied instead. See also :setting:`NUMBER_GROUPING`, :setting:`THOUSAND_SEPARATOR` and :setting:`USE_THOUSAND_SEPARATOR`. +.. setting:: DEFAULT_AUTO_FIELD + +``DEFAULT_AUTO_FIELD`` +---------------------- + +.. versionadded:: 3.2 + +Default: ``'``:class:`django.db.models.AutoField`\ ``'`` + +Default primary key field type to use for models that don't have a field with +:attr:`primary_key=True `. .. setting:: DEFAULT_CHARSET diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index 93b60690b3f..5b1969698c3 100644 --- a/docs/releases/3.2.txt +++ b/docs/releases/3.2.txt @@ -53,6 +53,48 @@ needed. As a consequence, it's deprecated. See :ref:`configuring-applications-ref` for full details. +Customizing type of auto-created primary keys +--------------------------------------------- + +When defining a model, if no field in a model is defined with +:attr:`primary_key=True ` an implicit +primary key is added. The type of this implicit primary key can now be +controlled via the :setting:`DEFAULT_AUTO_FIELD` setting and +:attr:`AppConfig.default_auto_field ` +attribute. No more needing to override primary keys in all models. + +Maintaining the historical behavior, the default value for +:setting:`DEFAULT_AUTO_FIELD` is :class:`~django.db.models.AutoField`. Starting +with 3.2 new projects are generated with :setting:`DEFAULT_AUTO_FIELD` set to +:class:`~django.db.models.BigAutoField`. Also, new apps are generated with +:attr:`AppConfig.default_auto_field ` +set to :class:`~django.db.models.BigAutoField`. In a future Django release the +default value of :setting:`DEFAULT_AUTO_FIELD` will be changed to +:class:`~django.db.models.BigAutoField`. + +To avoid unwanted migrations in the future, either explicitly set +:setting:`DEFAULT_AUTO_FIELD` to :class:`~django.db.models.AutoField`:: + + DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' + +or configure it on a per-app basis:: + + from django.apps import AppConfig + + class MyAppConfig(AppConfig): + default_auto_field = 'django.db.models.AutoField' + name = 'my_app' + +or on a per-model basis:: + + from django.db import models + + class MyModel(models.Model): + id = models.AutoField(primary_key=True) + +In anticipation of the changing default, a system check will provide a warning +if you do not have an explicit setting for :setting:`DEFAULT_AUTO_FIELD`. + ``pymemcache`` support ---------------------- diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index 81eb5b516bb..26a6d7dc587 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -259,11 +259,12 @@ details can be found in the :ref:`common model field option reference Automatic primary key fields ---------------------------- -By default, Django gives each model the following field:: +By default, Django gives each model an auto-incrementing primary key with the +type specified per app in :attr:`AppConfig.default_auto_field +` or globally in the +:setting:`DEFAULT_AUTO_FIELD` setting. For example:: - id = models.AutoField(primary_key=True) - -This is an auto-incrementing primary key. + id = models.BigAutoField(primary_key=True) If you'd like to specify a custom primary key, specify :attr:`primary_key=True ` on one of your fields. If Django @@ -273,6 +274,11 @@ sees you've explicitly set :attr:`Field.primary_key`, it won't add the automatic Each model requires exactly one field to have :attr:`primary_key=True ` (either explicitly declared or automatically added). +.. versionchanged:: 3.2 + + In older versions, auto-created primary key fields were always + :class:`AutoField`\s. + .. _verbose-field-names: Verbose field names diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index a82f5be5ed6..7a38306d179 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -61,6 +61,7 @@ class AdminScriptTestCase(SimpleTestCase): settings_file.write("%s\n" % extra) exports = [ 'DATABASES', + 'DEFAULT_AUTO_FIELD', 'ROOT_URLCONF', 'SECRET_KEY', ] @@ -2188,6 +2189,20 @@ class StartApp(AdminScriptTestCase): "won't replace conflicting files." ) + def test_template(self): + out, err = self.run_django_admin(['startapp', 'new_app']) + self.assertNoOutput(err) + app_path = os.path.join(self.test_dir, 'new_app') + self.assertIs(os.path.exists(app_path), True) + with open(os.path.join(app_path, 'apps.py')) as f: + content = f.read() + self.assertIn('class NewAppConfig(AppConfig)', content) + self.assertIn( + "default_auto_field = 'django.db.models.BigAutoField'", + content, + ) + self.assertIn("name = 'new_app'", content) + class DiffSettings(AdminScriptTestCase): """Tests for diffsettings management command.""" diff --git a/tests/apps/apps.py b/tests/apps/apps.py index d322b28f2b9..efd2983779f 100644 --- a/tests/apps/apps.py +++ b/tests/apps/apps.py @@ -31,3 +31,8 @@ class PlainAppsConfig(AppConfig): class RelabeledAppsConfig(AppConfig): name = 'apps' label = 'relabeled' + + +class ModelPKAppsConfig(AppConfig): + name = 'apps' + default_auto_field = 'django.db.models.BigAutoField' diff --git a/tests/apps/tests.py b/tests/apps/tests.py index 26d135f244f..0e1f918bccf 100644 --- a/tests/apps/tests.py +++ b/tests/apps/tests.py @@ -102,8 +102,8 @@ class AppsTests(SimpleTestCase): def test_no_such_app_config_with_choices(self): msg = ( "Module 'apps.apps' does not contain a 'NoSuchConfig' class. " - "Choices are: 'BadConfig', 'MyAdmin', 'MyAuth', 'NoSuchApp', " - "'PlainAppsConfig', 'RelabeledAppsConfig'." + "Choices are: 'BadConfig', 'ModelPKAppsConfig', 'MyAdmin', " + "'MyAuth', 'NoSuchApp', 'PlainAppsConfig', 'RelabeledAppsConfig'." ) with self.assertRaisesMessage(ImportError, msg): with self.settings(INSTALLED_APPS=['apps.apps.NoSuchConfig']): @@ -436,6 +436,30 @@ class AppConfigTests(SimpleTestCase): ac = AppConfig('label', Stub(__path__=['a'])) self.assertEqual(repr(ac), '') + @override_settings( + INSTALLED_APPS=['apps.apps.ModelPKAppsConfig'], + DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField', + ) + def test_app_default_auto_field(self): + apps_config = apps.get_app_config('apps') + self.assertEqual( + apps_config.default_auto_field, + 'django.db.models.BigAutoField', + ) + self.assertIs(apps_config._is_default_auto_field_overridden, True) + + @override_settings( + INSTALLED_APPS=['apps.apps.PlainAppsConfig'], + DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField', + ) + def test_default_auto_field_setting(self): + apps_config = apps.get_app_config('apps') + self.assertEqual( + apps_config.default_auto_field, + 'django.db.models.SmallAutoField', + ) + self.assertIs(apps_config._is_default_auto_field_overridden, False) + class NamespacePackageAppTests(SimpleTestCase): # We need nsapp to be top-level so our multiple-paths tests can add another diff --git a/tests/check_framework/apps.py b/tests/check_framework/apps.py new file mode 100644 index 00000000000..c57994f7221 --- /dev/null +++ b/tests/check_framework/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class CheckDefaultPKConfig(AppConfig): + name = 'check_framework' + + +class CheckPKConfig(AppConfig): + name = 'check_framework' + default_auto_field = 'django.db.models.BigAutoField' diff --git a/tests/check_framework/test_model_checks.py b/tests/check_framework/test_model_checks.py index ce2d73fa200..d4342ada073 100644 --- a/tests/check_framework/test_model_checks.py +++ b/tests/check_framework/test_model_checks.py @@ -1,3 +1,5 @@ +from unittest import mock + from django.core import checks from django.core.checks import Error, Warning from django.db import models @@ -358,3 +360,58 @@ class ConstraintNameTests(TestCase): constraints = [constraint] self.assertEqual(checks.run_checks(app_configs=apps.get_app_configs()), []) + + +def mocked_is_overridden(self, setting): + # Force treating DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' as a not + # overridden setting. + return ( + setting != 'DEFAULT_AUTO_FIELD' or + self.DEFAULT_AUTO_FIELD != 'django.db.models.AutoField' + ) + + +@mock.patch('django.conf.UserSettingsHolder.is_overridden', mocked_is_overridden) +@override_settings(DEFAULT_AUTO_FIELD='django.db.models.AutoField') +@isolate_apps('check_framework.apps.CheckDefaultPKConfig', attr_name='apps') +@override_system_checks([checks.model_checks.check_all_models]) +class ModelDefaultAutoFieldTests(SimpleTestCase): + def test_auto_created_pk(self): + class Model(models.Model): + pass + + self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), [ + Warning( + "Auto-created primary key used when not defining a primary " + "key type, by default 'django.db.models.AutoField'.", + hint=( + "Configure the DEFAULT_AUTO_FIELD setting or the " + "CheckDefaultPKConfig.default_auto_field attribute to " + "point to a subclass of AutoField, e.g. " + "'django.db.models.BigAutoField'." + ), + obj=Model, + id='models.W042', + ), + ]) + + @override_settings(DEFAULT_AUTO_FIELD='django.db.models.BigAutoField') + def test_default_auto_field_setting(self): + class Model(models.Model): + pass + + self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), []) + + def test_explicit_pk(self): + class Model(models.Model): + id = models.BigAutoField(primary_key=True) + + self.assertEqual(checks.run_checks(app_configs=self.apps.get_app_configs()), []) + + @isolate_apps('check_framework.apps.CheckPKConfig', kwarg_name='apps') + def test_app_default_auto_field(self, apps): + class ModelWithPkViaAppConfig(models.Model): + class Meta: + app_label = 'check_framework.apps.CheckPKConfig' + + self.assertEqual(checks.run_checks(app_configs=apps.get_app_configs()), []) diff --git a/tests/model_options/apps.py b/tests/model_options/apps.py new file mode 100644 index 00000000000..be2250fe7a4 --- /dev/null +++ b/tests/model_options/apps.py @@ -0,0 +1,25 @@ +from django.apps import AppConfig + + +class ModelDefaultPKConfig(AppConfig): + name = 'model_options' + + +class ModelPKConfig(AppConfig): + name = 'model_options' + default_auto_field = 'django.db.models.SmallAutoField' + + +class ModelPKNonAutoConfig(AppConfig): + name = 'model_options' + default_auto_field = 'django.db.models.TextField' + + +class ModelPKNoneConfig(AppConfig): + name = 'model_options' + default_auto_field = None + + +class ModelPKNonexistentConfig(AppConfig): + name = 'model_options' + default_auto_field = 'django.db.models.NonexistentAutoField' diff --git a/tests/model_options/test_default_pk.py b/tests/model_options/test_default_pk.py new file mode 100644 index 00000000000..b6788489160 --- /dev/null +++ b/tests/model_options/test_default_pk.py @@ -0,0 +1,101 @@ +from django.core.exceptions import ImproperlyConfigured +from django.db import models +from django.test import SimpleTestCase, override_settings +from django.test.utils import isolate_apps + + +@isolate_apps('model_options') +class TestDefaultPK(SimpleTestCase): + @override_settings(DEFAULT_AUTO_FIELD='django.db.models.NonexistentAutoField') + def test_default_auto_field_setting_nonexistent(self): + msg = ( + "DEFAULT_AUTO_FIELD refers to the module " + "'django.db.models.NonexistentAutoField' that could not be " + "imported." + ) + with self.assertRaisesMessage(ImproperlyConfigured, msg): + class Model(models.Model): + pass + + @isolate_apps('model_options.apps.ModelPKNonexistentConfig') + def test_app_default_auto_field_nonexistent(self): + msg = ( + "model_options.apps.ModelPKNonexistentConfig.default_auto_field " + "refers to the module 'django.db.models.NonexistentAutoField' " + "that could not be imported." + ) + with self.assertRaisesMessage(ImproperlyConfigured, msg): + class Model(models.Model): + pass + + @override_settings(DEFAULT_AUTO_FIELD='django.db.models.TextField') + def test_default_auto_field_setting_non_auto(self): + msg = ( + "Primary key 'django.db.models.TextField' referred by " + "DEFAULT_AUTO_FIELD must subclass AutoField." + ) + with self.assertRaisesMessage(ValueError, msg): + class Model(models.Model): + pass + + @isolate_apps('model_options.apps.ModelPKNonAutoConfig') + def test_app_default_auto_field_non_auto(self): + msg = ( + "Primary key 'django.db.models.TextField' referred by " + "model_options.apps.ModelPKNonAutoConfig.default_auto_field must " + "subclass AutoField." + ) + with self.assertRaisesMessage(ValueError, msg): + class Model(models.Model): + pass + + @override_settings(DEFAULT_AUTO_FIELD=None) + def test_default_auto_field_setting_none(self): + msg = 'DEFAULT_AUTO_FIELD must not be empty.' + with self.assertRaisesMessage(ImproperlyConfigured, msg): + class Model(models.Model): + pass + + @isolate_apps('model_options.apps.ModelPKNoneConfig') + def test_app_default_auto_field_none(self): + msg = ( + 'model_options.apps.ModelPKNoneConfig.default_auto_field must not ' + 'be empty.' + ) + with self.assertRaisesMessage(ImproperlyConfigured, msg): + class Model(models.Model): + pass + + @isolate_apps('model_options.apps.ModelDefaultPKConfig') + @override_settings(DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField') + def test_default_auto_field_setting(self): + class Model(models.Model): + pass + + self.assertIsInstance(Model._meta.pk, models.SmallAutoField) + + @isolate_apps('model_options.apps.ModelPKConfig') + @override_settings(DEFAULT_AUTO_FIELD='django.db.models.AutoField') + def test_app_default_auto_field(self): + class Model(models.Model): + pass + + self.assertIsInstance(Model._meta.pk, models.SmallAutoField) + + @isolate_apps('model_options.apps.ModelDefaultPKConfig') + @override_settings(DEFAULT_AUTO_FIELD='django.db.models.SmallAutoField') + def test_m2m_default_auto_field_setting(self): + class M2MModel(models.Model): + m2m = models.ManyToManyField('self') + + m2m_pk = M2MModel._meta.get_field('m2m').remote_field.through._meta.pk + self.assertIsInstance(m2m_pk, models.SmallAutoField) + + @isolate_apps('model_options.apps.ModelPKConfig') + @override_settings(DEFAULT_AUTO_FIELD='django.db.models.AutoField') + def test_m2m_app_default_auto_field(self): + class M2MModel(models.Model): + m2m = models.ManyToManyField('self') + + m2m_pk = M2MModel._meta.get_field('m2m').remote_field.through._meta.pk + self.assertIsInstance(m2m_pk, models.SmallAutoField) diff --git a/tests/test_sqlite.py b/tests/test_sqlite.py index f1b65f7d018..099f37e56dc 100644 --- a/tests/test_sqlite.py +++ b/tests/test_sqlite.py @@ -27,3 +27,5 @@ SECRET_KEY = "django_tests_secret_key" PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.MD5PasswordHasher', ] + +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'