Skip to content

Commit

Permalink
Fixed #31007 -- Allowed specifying type of auto-created primary keys.
Browse files Browse the repository at this point in the history
This also changes the default type of auto-created primary keys
for new apps and projects to BigAutoField.
  • Loading branch information
orf authored and felixxm committed Dec 15, 2020
1 parent b960e4e commit b5e12d4
Show file tree
Hide file tree
Showing 28 changed files with 415 additions and 11 deletions.
10 changes: 10 additions & 0 deletions django/apps/config.py
Expand Up @@ -5,6 +5,7 @@

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'
Expand Down Expand Up @@ -55,6 +56,15 @@ def __init__(self, app_name, app_module):
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
Expand Down
1 change: 1 addition & 0 deletions django/conf/app_template/apps.py-tpl
Expand Up @@ -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 }}'
3 changes: 3 additions & 0 deletions django/conf/global_settings.py
Expand Up @@ -414,6 +414,9 @@ def gettext_noop(s):
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'

Expand Down
5 changes: 5 additions & 0 deletions django/conf/project_template/project_name/settings.py-tpl
Expand Up @@ -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'
1 change: 1 addition & 0 deletions django/contrib/admin/apps.py
Expand Up @@ -7,6 +7,7 @@
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")
Expand Down
1 change: 1 addition & 0 deletions django/contrib/auth/apps.py
Expand Up @@ -11,6 +11,7 @@


class AuthConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.auth'
verbose_name = _("Authentication and Authorization")

Expand Down
1 change: 1 addition & 0 deletions django/contrib/contenttypes/apps.py
Expand Up @@ -12,6 +12,7 @@


class ContentTypesConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.contenttypes'
verbose_name = _("Content Types")

Expand Down
1 change: 1 addition & 0 deletions django/contrib/flatpages/apps.py
Expand Up @@ -3,5 +3,6 @@


class FlatPagesConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.flatpages'
verbose_name = _("Flat Pages")
1 change: 1 addition & 0 deletions django/contrib/gis/apps.py
Expand Up @@ -4,6 +4,7 @@


class GISConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.gis'
verbose_name = _("GIS")

Expand Down
1 change: 1 addition & 0 deletions django/contrib/redirects/apps.py
Expand Up @@ -3,5 +3,6 @@


class RedirectsConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.redirects'
verbose_name = _("Redirects")
1 change: 1 addition & 0 deletions django/contrib/sitemaps/apps.py
Expand Up @@ -3,5 +3,6 @@


class SiteMapsConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.sitemaps'
verbose_name = _("Site Maps")
1 change: 1 addition & 0 deletions django/contrib/sites/apps.py
Expand Up @@ -8,6 +8,7 @@


class SitesConfig(AppConfig):
default_auto_field = 'django.db.models.AutoField'
name = 'django.contrib.sites'
verbose_name = _("Sites")

Expand Down
25 changes: 25 additions & 0 deletions django/db/models/base.py
Expand Up @@ -1290,10 +1290,35 @@ def check(cls, **kwargs):
*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."""
Expand Down
37 changes: 35 additions & 2 deletions django/db/models/options.py
Expand Up @@ -5,12 +5,13 @@

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

Expand Down Expand Up @@ -217,6 +218,37 @@ def _format_names_with_class(self, cls, objs):
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
Expand Down Expand Up @@ -250,7 +282,8 @@ def _prepare(self, model):
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):
Expand Down
11 changes: 11 additions & 0 deletions docs/ref/applications.txt
Expand Up @@ -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"

Expand Down Expand Up @@ -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
--------------------

Expand Down
2 changes: 2 additions & 0 deletions docs/ref/checks.txt
Expand Up @@ -378,6 +378,8 @@ Models
* **models.W040**: ``<database>`` does not support indexes with non-key
columns.
* **models.E041**: ``constraints`` refers to the joined field ``<field name>``.
* **models.W042**: Auto-created primary key used when not defining a primary
key type, by default ``django.db.models.AutoField``.

Security
--------
Expand Down
14 changes: 11 additions & 3 deletions docs/ref/models/fields.txt
Expand Up @@ -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
<django.apps.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 <Field.null>` and
Expand All @@ -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``
----------

Expand Down
11 changes: 11 additions & 0 deletions docs/ref/settings.txt
Expand Up @@ -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 <django.db.models.Field.primary_key>`.

.. setting:: DEFAULT_CHARSET

Expand Down
42 changes: 42 additions & 0 deletions docs/releases/3.2.txt
Expand Up @@ -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 <django.db.models.Field.primary_key>` 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 <django.apps.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 <django.apps.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
----------------------

Expand Down
14 changes: 10 additions & 4 deletions docs/topics/db/models.txt
Expand Up @@ -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
<django.apps.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 <Field.primary_key>` on one of your fields. If Django
Expand All @@ -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
<Field.primary_key>` (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
Expand Down
15 changes: 15 additions & 0 deletions tests/admin_scripts/tests.py
Expand Up @@ -61,6 +61,7 @@ def write_settings(self, filename, apps=None, is_dir=False, sdict=None, extra=No
settings_file.write("%s\n" % extra)
exports = [
'DATABASES',
'DEFAULT_AUTO_FIELD',
'ROOT_URLCONF',
'SECRET_KEY',
]
Expand Down Expand Up @@ -2188,6 +2189,20 @@ def test_overlaying_app(self):
"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."""
Expand Down

0 comments on commit b5e12d4

Please sign in to comment.