From 2ff93e027c7b35378cc450a926bc2e4a446cacf0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Fri, 24 Jan 2014 22:43:00 +0100 Subject: [PATCH] Fixed #21829 -- Added default AppConfigs. Thanks Russell for the report, Marc for the initial patch, Carl for the final review, and everyone who contributed to the design discussion. --- django/apps/base.py | 81 +++++++++++-------- .../project_template/project_name/settings.py | 2 +- django/contrib/admin/__init__.py | 5 +- django/contrib/admin/apps.py | 15 +++- django/contrib/admin/sites.py | 2 +- django/contrib/admindocs/__init__.py | 1 + django/contrib/auth/__init__.py | 9 +-- django/contrib/auth/apps.py | 5 ++ django/contrib/comments/__init__.py | 7 +- django/contrib/contenttypes/__init__.py | 9 +-- django/contrib/contenttypes/apps.py | 6 +- django/contrib/flatpages/__init__.py | 1 + django/contrib/formtools/__init__.py | 1 + django/contrib/gis/__init__.py | 3 + django/contrib/humanize/__init__.py | 1 + django/contrib/messages/__init__.py | 3 + django/contrib/redirects/__init__.py | 1 + django/contrib/sessions/__init__.py | 1 + django/contrib/sitemaps/__init__.py | 3 + django/contrib/sites/__init__.py | 1 + django/contrib/staticfiles/__init__.py | 1 + django/contrib/syndication/__init__.py | 1 + django/contrib/webdesign/__init__.py | 1 + django/core/checks/registry.py | 3 +- docs/intro/tutorial01.txt | 9 +-- docs/ref/applications.txt | 35 ++++++-- docs/ref/contrib/admin/index.txt | 51 +++++++----- docs/ref/contrib/gis/tutorial.txt | 2 +- docs/releases/1.7.txt | 17 ++-- docs/topics/auth/default.txt | 5 +- tests/admin_scripts/tests.py | 2 +- tests/apps/default_config_app/__init__.py | 1 + tests/apps/default_config_app/apps.py | 5 ++ tests/apps/tests.py | 6 ++ tests/i18n/tests.py | 2 +- tests/runtests.py | 7 +- 36 files changed, 194 insertions(+), 111 deletions(-) create mode 100644 tests/apps/default_config_app/__init__.py create mode 100644 tests/apps/default_config_app/apps.py diff --git a/django/apps/base.py b/django/apps/base.py index ea28797108..2ae40e2454 100644 --- a/django/apps/base.py +++ b/django/apps/base.py @@ -61,13 +61,12 @@ class AppConfig(object): Factory that creates an app config from an entry in INSTALLED_APPS. """ try: - # If import_module succeeds, entry is a path to an app module. + # If import_module succeeds, entry is a path to an app module, + # which may specify an app config class with default_app_config. # Otherwise, entry is a path to an app config class or an error. module = import_module(entry) except ImportError: - # Avoid django.utils.module_loading.import_by_path because it - # masks errors -- it reraises ImportError as ImproperlyConfigured. mod_path, _, cls_name = entry.rpartition('.') # Raise the original exception when entry cannot be a path to an @@ -75,39 +74,51 @@ class AppConfig(object): if not mod_path: raise - mod = import_module(mod_path) - try: - cls = getattr(mod, cls_name) - except AttributeError: - # Emulate the error that "from import " - # would raise when exists but not , with - # more context (Python just says "cannot import name ..."). - raise ImportError( - "cannot import name '%s' from '%s'" % (cls_name, mod_path)) - - # Check for obvious errors. (This check prevents duck typing, but - # it could be removed if it became a problem in practice.) - if not issubclass(cls, AppConfig): - raise ImproperlyConfigured( - "'%s' isn't a subclass of AppConfig." % entry) - - # Obtain app name here rather than in AppClass.__init__ to keep - # all error checking for entries in INSTALLED_APPS in one place. - try: - app_name = cls.name - except AttributeError: - raise ImproperlyConfigured( - "'%s' must supply a name attribute." % entry) - - # Ensure app_name points to a valid module. - app_module = import_module(app_name) - - # Entry is a path to an app config class. - return cls(app_name, app_module) - else: - # Entry is a path to an app module. - return cls(entry, module) + try: + # If this works, the app module specifies an app config class. + entry = module.default_app_config + except AttributeError: + # Otherwise, it simply uses the default app config class. + return cls(entry, module) + else: + mod_path, _, cls_name = entry.rpartition('.') + + # If we're reaching this point, we must load the app config class + # located at .. + + # Avoid django.utils.module_loading.import_by_path because it + # masks errors -- it reraises ImportError as ImproperlyConfigured. + mod = import_module(mod_path) + try: + cls = getattr(mod, cls_name) + except AttributeError: + # Emulate the error that "from import " + # would raise when exists but not , with + # more context (Python just says "cannot import name ..."). + raise ImportError( + "cannot import name '%s' from '%s'" % (cls_name, mod_path)) + + # Check for obvious errors. (This check prevents duck typing, but + # it could be removed if it became a problem in practice.) + if not issubclass(cls, AppConfig): + raise ImproperlyConfigured( + "'%s' isn't a subclass of AppConfig." % entry) + + # Obtain app name here rather than in AppClass.__init__ to keep + # all error checking for entries in INSTALLED_APPS in one place. + try: + app_name = cls.name + except AttributeError: + raise ImproperlyConfigured( + "'%s' must supply a name attribute." % entry) + + # Ensure app_name points to a valid module. + app_module = import_module(app_name) + + # Entry is a path to an app config class. + return cls(app_name, app_module) + def get_model(self, model_name): """ diff --git a/django/conf/project_template/project_name/settings.py b/django/conf/project_template/project_name/settings.py index 198a3e0d92..efe8091e81 100644 --- a/django/conf/project_template/project_name/settings.py +++ b/django/conf/project_template/project_name/settings.py @@ -30,7 +30,7 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = ( - 'django.contrib.admin.apps.AdminConfig', + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py index 530badf1df..2218dfd23f 100644 --- a/django/contrib/admin/__init__.py +++ b/django/contrib/admin/__init__.py @@ -1,6 +1,5 @@ # ACTION_CHECKBOX_NAME is unused, but should stay since its import from here # has been referenced in documentation. -from django.contrib.admin.checks import check_admin_app from django.contrib.admin.decorators import register from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL @@ -9,7 +8,6 @@ from django.contrib.admin.filters import (ListFilter, SimpleListFilter, FieldListFilter, BooleanFieldListFilter, RelatedFieldListFilter, ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter) from django.contrib.admin.sites import AdminSite, site -from django.core import checks from django.utils.module_loading import autodiscover_modules __all__ = [ @@ -24,4 +22,5 @@ __all__ = [ def autodiscover(): autodiscover_modules('admin', register_to=site) -checks.register('admin')(check_admin_app) + +default_app_config = 'django.contrib.admin.apps.AdminConfig' diff --git a/django/contrib/admin/apps.py b/django/contrib/admin/apps.py index a8f0e91ea6..93dab8a787 100644 --- a/django/contrib/admin/apps.py +++ b/django/contrib/admin/apps.py @@ -1,11 +1,22 @@ from django.apps import AppConfig - +from django.core import checks +from django.contrib.admin.checks import check_admin_app from django.utils.translation import ugettext_lazy as _ -class AdminConfig(AppConfig): +class SimpleAdminConfig(AppConfig): + """Simple AppConfig which does not do automatic discovery.""" + name = 'django.contrib.admin' verbose_name = _("administration") def ready(self): + checks.register('admin')(check_admin_app) + + +class AdminConfig(SimpleAdminConfig): + """The default AppConfig for admin which does autodiscovery.""" + + def ready(self): + super(AdminConfig, self).ready() self.module.autodiscover() diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index ba7c38dcad..f25055baf7 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -161,7 +161,7 @@ class AdminSite(object): installed, as well as the auth context processor. """ if not apps.is_installed('django.contrib.admin'): - raise ImproperlyConfigured("Put 'django.contrib.admin.apps.AdminConfig' in " + raise ImproperlyConfigured("Put 'django.contrib.admin' in " "your INSTALLED_APPS setting in order to use the admin application.") if not apps.is_installed('django.contrib.contenttypes'): raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in " diff --git a/django/contrib/admindocs/__init__.py b/django/contrib/admindocs/__init__.py index e69de29bb2..733519ac54 100644 --- a/django/contrib/admindocs/__init__.py +++ b/django/contrib/admindocs/__init__.py @@ -0,0 +1 @@ +default_app_config = 'django.contrib.admindocs.apps.AdminDocsConfig' diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 24c870dafe..77b2482e31 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -2,8 +2,6 @@ import inspect import re from django.conf import settings -from django.contrib.auth.checks import check_user_model -from django.core import checks from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.utils.module_loading import import_by_path from django.middleware.csrf import rotate_token @@ -15,10 +13,6 @@ BACKEND_SESSION_KEY = '_auth_user_backend' REDIRECT_FIELD_NAME = 'next' -# Register the user model checks -checks.register('models')(check_user_model) - - def load_backend(path): return import_by_path(path)() @@ -164,3 +158,6 @@ def get_permission_codename(action, opts): Returns the codename of the permission for the specified action. """ return '%s_%s' % (action, opts.model_name) + + +default_app_config = 'django.contrib.auth.apps.AuthConfig' diff --git a/django/contrib/auth/apps.py b/django/contrib/auth/apps.py index e44ad1a0c7..a0389acabe 100644 --- a/django/contrib/auth/apps.py +++ b/django/contrib/auth/apps.py @@ -1,4 +1,6 @@ from django.apps import AppConfig +from django.core import checks +from django.contrib.auth.checks import check_user_model from django.utils.translation import ugettext_lazy as _ @@ -6,3 +8,6 @@ from django.utils.translation import ugettext_lazy as _ class AuthConfig(AppConfig): name = 'django.contrib.auth' verbose_name = _("authentication and authorization") + + def ready(self): + checks.register('models')(check_user_model) diff --git a/django/contrib/comments/__init__.py b/django/contrib/comments/__init__.py index 19dd7ef9a1..a62fff1b56 100644 --- a/django/contrib/comments/__init__.py +++ b/django/contrib/comments/__init__.py @@ -1,6 +1,6 @@ from importlib import import_module import warnings -from django.apps import apps +from django.apps import apps as django_apps from django.conf import settings from django.core import urlresolvers from django.core.exceptions import ImproperlyConfigured @@ -16,7 +16,7 @@ def get_comment_app(): Get the comment app (i.e. "django.contrib.comments") as defined in the settings """ try: - app_config = apps.get_app_config(get_comment_app_name().rpartition(".")[2]) + app_config = django_apps.get_app_config(get_comment_app_name().rpartition(".")[2]) except LookupError: raise ImproperlyConfigured("The COMMENTS_APP (%r) " "must be in INSTALLED_APPS" % settings.COMMENTS_APP) @@ -85,3 +85,6 @@ def get_approve_url(comment): else: return urlresolvers.reverse("django.contrib.comments.views.moderation.approve", args=(comment.id,)) + + +default_app_config = 'django.contrib.comments.apps.CommentsConfig' diff --git a/django/contrib/contenttypes/__init__.py b/django/contrib/contenttypes/__init__.py index 5a04849f06..9789f36eff 100644 --- a/django/contrib/contenttypes/__init__.py +++ b/django/contrib/contenttypes/__init__.py @@ -1,8 +1 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.contrib.contenttypes.checks import check_generic_foreign_keys -from django.core import checks - - -checks.register('models')(check_generic_foreign_keys) +default_app_config = 'django.contrib.contenttypes.apps.ContentTypesConfig' diff --git a/django/contrib/contenttypes/apps.py b/django/contrib/contenttypes/apps.py index 0e55e8d66d..4e515d8a00 100644 --- a/django/contrib/contenttypes/apps.py +++ b/django/contrib/contenttypes/apps.py @@ -1,8 +1,12 @@ from django.apps import AppConfig - +from django.contrib.contenttypes.checks import check_generic_foreign_keys +from django.core import checks from django.utils.translation import ugettext_lazy as _ class ContentTypesConfig(AppConfig): name = 'django.contrib.contenttypes' verbose_name = _("content types") + + def ready(self): + checks.register('models')(check_generic_foreign_keys) diff --git a/django/contrib/flatpages/__init__.py b/django/contrib/flatpages/__init__.py index e69de29bb2..865acd855b 100644 --- a/django/contrib/flatpages/__init__.py +++ b/django/contrib/flatpages/__init__.py @@ -0,0 +1 @@ +default_app_config = 'django.contrib.flatpages.apps.FlatPagesConfig' diff --git a/django/contrib/formtools/__init__.py b/django/contrib/formtools/__init__.py index e69de29bb2..b82588d8e7 100644 --- a/django/contrib/formtools/__init__.py +++ b/django/contrib/formtools/__init__.py @@ -0,0 +1 @@ +default_app_config = 'django.contrib.formtools.apps.FormToolsConfig' diff --git a/django/contrib/gis/__init__.py b/django/contrib/gis/__init__.py index c996fdfdc2..84b3552ce7 100644 --- a/django/contrib/gis/__init__.py +++ b/django/contrib/gis/__init__.py @@ -4,3 +4,6 @@ if six.PY3: memoryview = memoryview else: memoryview = buffer + + +default_app_config = 'django.contrib.gis.apps.GISConfig' diff --git a/django/contrib/humanize/__init__.py b/django/contrib/humanize/__init__.py index e69de29bb2..9ca1642fae 100644 --- a/django/contrib/humanize/__init__.py +++ b/django/contrib/humanize/__init__.py @@ -0,0 +1 @@ +default_app_config = 'django.contrib.humanize.apps.HumanizeConfig' diff --git a/django/contrib/messages/__init__.py b/django/contrib/messages/__init__.py index 40ba6ea6c7..a0cb24b2d9 100644 --- a/django/contrib/messages/__init__.py +++ b/django/contrib/messages/__init__.py @@ -1,2 +1,5 @@ from django.contrib.messages.api import * # NOQA from django.contrib.messages.constants import * # NOQA + + +default_app_config = 'django.contrib.messages.apps.MessagesConfig' diff --git a/django/contrib/redirects/__init__.py b/django/contrib/redirects/__init__.py index e69de29bb2..65364a440b 100644 --- a/django/contrib/redirects/__init__.py +++ b/django/contrib/redirects/__init__.py @@ -0,0 +1 @@ +default_app_config = 'django.contrib.redirects.apps.RedirectsConfig' diff --git a/django/contrib/sessions/__init__.py b/django/contrib/sessions/__init__.py index e69de29bb2..0382f95d7e 100644 --- a/django/contrib/sessions/__init__.py +++ b/django/contrib/sessions/__init__.py @@ -0,0 +1 @@ +default_app_config = 'django.contrib.sessions.apps.SessionsConfig' diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py index 4396708098..ee3d22090d 100644 --- a/django/contrib/sitemaps/__init__.py +++ b/django/contrib/sitemaps/__init__.py @@ -133,3 +133,6 @@ class GenericSitemap(Sitemap): if self.date_field is not None: return getattr(item, self.date_field) return None + + +default_app_config = 'django.contrib.sitemaps.apps.SiteMapsConfig' diff --git a/django/contrib/sites/__init__.py b/django/contrib/sites/__init__.py index e69de29bb2..b554240bb2 100644 --- a/django/contrib/sites/__init__.py +++ b/django/contrib/sites/__init__.py @@ -0,0 +1 @@ +default_app_config = 'django.contrib.sites.apps.SitesConfig' diff --git a/django/contrib/staticfiles/__init__.py b/django/contrib/staticfiles/__init__.py index e69de29bb2..8a07b83d2f 100644 --- a/django/contrib/staticfiles/__init__.py +++ b/django/contrib/staticfiles/__init__.py @@ -0,0 +1 @@ +default_app_config = 'django.contrib.staticfiles.apps.StaticFilesConfig' diff --git a/django/contrib/syndication/__init__.py b/django/contrib/syndication/__init__.py index e69de29bb2..2c3f52578d 100644 --- a/django/contrib/syndication/__init__.py +++ b/django/contrib/syndication/__init__.py @@ -0,0 +1 @@ +default_app_config = 'django.contrib.syndication.apps.SyndicationConfig' diff --git a/django/contrib/webdesign/__init__.py b/django/contrib/webdesign/__init__.py index e69de29bb2..2340116bc8 100644 --- a/django/contrib/webdesign/__init__.py +++ b/django/contrib/webdesign/__init__.py @@ -0,0 +1 @@ +default_app_config = 'django.contrib.webdesign.apps.WebDesignConfig' diff --git a/django/core/checks/registry.py b/django/core/checks/registry.py index efb5b4b892..21efbf317e 100644 --- a/django/core/checks/registry.py +++ b/django/core/checks/registry.py @@ -29,7 +29,8 @@ class CheckRegistry(object): def inner(check): check.tags = tags - self.registered_checks.append(check) + if check not in self.registered_checks: + self.registered_checks.append(check) return check return inner diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index 1bf11e47ae..16b2596da9 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -435,7 +435,7 @@ look like this: :filename: mysite/settings.py INSTALLED_APPS = ( - 'django.contrib.admin.apps.AdminConfig', + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -444,13 +444,6 @@ look like this: 'polls', ) -.. admonition:: Doesn't match what you see? - - If you're seeing ``'django.contrib.admin'`` instead of - ``'django.contrib.admin.apps.AdminConfig'``, you're probably using a - version of Django that doesn't match this tutorial version. You'll want - to either switch to the older tutorial or the newer Django version. - Now Django knows to include the ``polls`` app. Let's run another command: .. code-block:: bash diff --git a/docs/ref/applications.txt b/docs/ref/applications.txt index e496f24487..33f885e416 100644 --- a/docs/ref/applications.txt +++ b/docs/ref/applications.txt @@ -49,9 +49,15 @@ Configuring applications To configure an application, subclass :class:`~django.apps.AppConfig` and put the dotted path to that subclass in :setting:`INSTALLED_APPS`. -Django uses the default :class:`~django.apps.AppConfig` class when -:setting:`INSTALLED_APPS` simply contains the dotted path to an application -module. +When :setting:`INSTALLED_APPS` simply contains the dotted path to an +application module, Django checks for a ``default_app_config`` variable in +that module. + +If it's defined, it's the dotted path to the :class:`~django.apps.AppConfig` +subclass for that application. + +If there is no ``default_app_config``, Django uses the base +:class:`~django.apps.AppConfig` class. For application authors ----------------------- @@ -67,8 +73,23 @@ would provide a proper name for the admin:: name = 'rock_n_roll' verbose_name = "Rock ā€™nā€™ roll" -You would then tell your users to add ``'rock_n_roll.apps.RockNRollConfig'`` -to their :setting:`INSTALLED_APPS`. +You can make your application load this :class:`~django.apps.AppConfig` +subclass by default as follows:: + + # rock_n_roll/__init__.py + + default_app_config = 'rock_n_roll.apps.RockNRollConfig' + +That will cause ``RockNRollConfig`` to be used when :setting:`INSTALLED_APPS` +just contains ``'rock_n_roll'``. This allows you to make use of +:class:`~django.apps.AppConfig` features without requiring your users to +update their :setting:`INSTALLED_APPS` setting. + +Of course, you can also tell your users to put +``'rock_n_roll.apps.RockNRollConfig'`` in their :setting:`INSTALLED_APPS` +setting. You can even provide several different +:class:`~django.apps.AppConfig` subclasses with different behaviors and allow +your users to choose one via their :setting:`INSTALLED_APPS` setting. The recommended convention is to put the configuration class in a submodule of the application called ``apps``. However, this isn't enforced by Django. @@ -87,7 +108,7 @@ configuration:: # anthology/apps.py - from rock_n_roll.app import RockNRollConfig + from rock_n_roll.apps import RockNRollConfig class GypsyJazzConfig(RockNRollConfig): verbose_name = "Gypsy jazz" @@ -213,7 +234,7 @@ Application registry .. method:: apps.is_installed(app_name) Checks whether an application with the given name exists in the registry. - ``app_name`` is the full name of the app, e.g. 'django.contrib.admin'. + ``app_name`` is the full name of the app, e.g. ``'django.contrib.admin'``. Unlike :meth:`~django.apps.apps.get_app_config`, this method can be called safely at import time. If the registry is still being populated, it may diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 7fc34c96de..281ea4674b 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -23,12 +23,7 @@ The admin is enabled in the default project template used by For reference, here are the requirements: -1. Add ``'django.contrib.admin.apps.AdminConfig'`` to your - :setting:`INSTALLED_APPS` setting. - - .. versionchanged:: 1.7 - - :setting:`INSTALLED_APPS` used to contain ``'django.contrib.admin'``. +1. Add ``'django.contrib.admin'`` to your :setting:`INSTALLED_APPS` setting. 2. The admin has four dependencies - :mod:`django.contrib.auth`, :mod:`django.contrib.contenttypes`, @@ -136,16 +131,23 @@ The register decorator Discovery of admin files ------------------------ -The admin needs to be instructed to look for ``admin.py`` files in your project. -The easiest solution is to put ``'django.contrib.admin.apps.AdminConfig'`` in -your :setting:`INSTALLED_APPS` setting. +When you put ``'django.contrib.admin'`` in your :setting:`INSTALLED_APPS` +setting, Django automatically looks for an ``admin`` module in each +application and imports it. .. class:: apps.AdminConfig .. versionadded:: 1.7 - This class calls :func:`~django.contrib.admin.autodiscover()` when Django - starts. + This is the default :class:`~django.apps.AppConfig` class for the admin. + It calls :func:`~django.contrib.admin.autodiscover()` when Django starts. + +.. class:: apps.SimpleAdminConfig + + .. versionadded:: 1.7 + + This class works like :class:`~django.contrib.admin.apps.AdminConfig`, + except it doesn't call :func:`~django.contrib.admin.autodiscover()`. .. function:: autodiscover @@ -155,13 +157,23 @@ your :setting:`INSTALLED_APPS` setting. .. versionchanged:: 1.7 Previous versions of Django recommended calling this function directly - in the URLconf. :class:`~django.contrib.admin.apps.AdminConfig` - replaces that mechanism and is more robust. + in the URLconf. As of Django 1.7 this isn't needed anymore. + :class:`~django.contrib.admin.apps.AdminConfig` takes care of running + the auto-discovery automatically. If you are using a custom ``AdminSite``, it is common to import all of the ``ModelAdmin`` subclasses into your code and register them to the custom -``AdminSite``. In that case, simply put ``'django.contrib.admin'`` in your -:setting:`INSTALLED_APPS` setting, as you don't need autodiscovery. +``AdminSite``. In that case, in order to disable auto-discovery, you should +put ``'django.contrib.admin.apps.SimpleAdminConfig'`` instead of +``'django.contrib.admin'`` in your :setting:`INSTALLED_APPS` setting. + +.. versionchanged:: 1.7 + + In previous versions, the admin needed to be instructed to look for + ``admin.py`` files with :func:`~django.contrib.admin.autodiscover()`. + As of Django 1.7, auto-discovery is enabled by default and must be + explicitly disabled when it's undesirable. + ``ModelAdmin`` options ---------------------- @@ -2426,11 +2438,12 @@ In this example, we register the ``AdminSite`` instance (r'^myadmin/', include(admin_site.urls)), ) -Note that you don't need autodiscovery of ``admin`` modules when using your +Note that you may not want autodiscovery of ``admin`` modules when using your own ``AdminSite`` instance since you will likely be importing all the per-app -``admin`` modules in your ``myproject.admin`` module. This means you likely do -not need ``'django.contrib.admin.app.AdminConfig'`` in your -:setting:`INSTALLED_APPS` and can just use ``'django.contrib.admin'``. +``admin`` modules in your ``myproject.admin`` module. This means you need to +put ``'django.contrib.admin.app.SimpleAdminConfig'`` instead of +``'django.contrib.admin'`` in your :setting:`INSTALLED_APPS` setting. + Multiple admin sites in the same URLconf ---------------------------------------- diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt index f3eeb64e59..ff27c9b6ec 100644 --- a/docs/ref/contrib/gis/tutorial.txt +++ b/docs/ref/contrib/gis/tutorial.txt @@ -115,7 +115,7 @@ In addition, modify the :setting:`INSTALLED_APPS` setting to include and ``world`` (your newly created application):: INSTALLED_APPS = ( - 'django.contrib.admin.apps.AdminConfig', + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index df115c6c15..684daa988c 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -89,17 +89,14 @@ Improvements thus far include: * The name of applications can be customized in the admin with the :attr:`~django.apps.AppConfig.verbose_name` of application configurations. +* The admin automatically calls :func:`~django.contrib.admin.autodiscover()` + when Django starts. You can consequently remove this line from your + URLconf. + * Django imports all application configurations and models as soon as it starts, through a deterministic and straightforward process. This should make it easier to diagnose import issues such as import loops. -* The admin has an :class:`~django.contrib.admin.apps.AdminConfig` application - configuration class. When Django starts, this class takes care of calling - :func:`~django.contrib.admin.autodiscover()`. To use it, simply replace - ``'django.contrib.admin'`` in :setting:`INSTALLED_APPS` with - ``'django.contrib.admin.apps.AdminConfig'`` and remove - ``admin.autodiscover()`` from your URLconf. - New method on Field subclasses ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -698,6 +695,12 @@ regressions cannot be ruled out. You may encounter the following exceptions: results. The code will be executed when you first need its results. This concept is known as "lazy evaluation". +* ``django.contrib.admin`` will now automatically perform autodiscovery of + ``admin`` modules in installed applications. To prevent it, change your + :setting:`INSTALLED_APPS` to contain + ``'django.contrib.admin.apps.SimpleAdminConfig'`` instead of + ``'django.contrib.admin'``. + Standalone scripts ^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index bfac4c922e..a721fb548c 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -67,9 +67,8 @@ Creating superusers ------------------- :djadmin:`manage.py migrate ` prompts you to create a superuser the -first time you run it with ``'django.contrib.auth'`` in your -:setting:`INSTALLED_APPS`. If you need to create a superuser at a later date, -you can use a command line utility:: +first time you run it with ``'django.contrib.auth'`` installed. If you need to +create a superuser at a later date, you can use a command line utility:: $ python manage.py createsuperuser --username=joe --email=joe@example.com diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 225441827f..f1a4d55521 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -1113,7 +1113,7 @@ class ManageCheck(AdminScriptTestCase): apps=[ 'admin_scripts.complex_app', 'admin_scripts.simple_app', - 'django.contrib.admin', + 'django.contrib.admin.apps.SimpleAdminConfig', 'django.contrib.auth', 'django.contrib.contenttypes', ], diff --git a/tests/apps/default_config_app/__init__.py b/tests/apps/default_config_app/__init__.py new file mode 100644 index 0000000000..aebe0350d9 --- /dev/null +++ b/tests/apps/default_config_app/__init__.py @@ -0,0 +1 @@ +default_app_config = 'apps.default_config_app.apps.CustomConfig' diff --git a/tests/apps/default_config_app/apps.py b/tests/apps/default_config_app/apps.py new file mode 100644 index 0000000000..ba859a4750 --- /dev/null +++ b/tests/apps/default_config_app/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CustomConfig(AppConfig): + name = 'apps.default_config_app' diff --git a/tests/apps/tests.py b/tests/apps/tests.py index 139bc06a66..814ef0186f 100644 --- a/tests/apps/tests.py +++ b/tests/apps/tests.py @@ -7,6 +7,7 @@ from django.db import models from django.test import TestCase, override_settings from django.utils import six +from .default_config_app.apps import CustomConfig from .models import TotallyNormal, SoAlternative, new_apps @@ -82,6 +83,11 @@ class AppsTests(TestCase): with self.settings(INSTALLED_APPS=['apps.apps.NoSuchConfig']): pass + def test_default_app_config(self): + with self.settings(INSTALLED_APPS=['apps.default_config_app']): + config = apps.get_app_config('default_config_app') + self.assertIsInstance(config, CustomConfig) + @override_settings(INSTALLED_APPS=SOME_INSTALLED_APPS) def test_get_app_configs(self): """ diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 116364790f..fd909e4c51 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -1049,7 +1049,7 @@ class AppResolutionOrderI18NTests(ResolutionOrderI18NTests): # Doesn't work because it's added later in the list. self.assertUgettext('Date/time', 'Datum/Zeit') - with self.modify_settings(INSTALLED_APPS={'remove': 'django.contrib.admin'}): + with self.modify_settings(INSTALLED_APPS={'remove': 'django.contrib.admin.apps.SimpleAdminConfig'}): self.flush_caches() activate('de') diff --git a/tests/runtests.py b/tests/runtests.py index 8009136ac3..b2c0837222 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -44,7 +44,7 @@ ALWAYS_INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.comments', - 'django.contrib.admin', + 'django.contrib.admin.apps.SimpleAdminConfig', 'django.contrib.admindocs', 'django.contrib.staticfiles', 'django.contrib.humanize', @@ -168,12 +168,11 @@ def setup(verbosity, test_labels): for label in test_labels_set) installed_app_names = set(get_installed()) - if module_found_in_labels: + if module_found_in_labels and module_label not in installed_app_names: if verbosity >= 2: print("Importing application %s" % module_name) # HACK. - if module_label not in installed_app_names: - settings.INSTALLED_APPS.append(module_label) + settings.INSTALLED_APPS.append(module_label) app_config = AppConfig.create(module_label) apps.app_configs[app_config.label] = app_config app_config.import_models(apps.all_models[app_config.label])