summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAymeric Augustin <aymeric.augustin@m4x.org>2013-12-31 16:23:42 +0100
committerAymeric Augustin <aymeric.augustin@m4x.org>2013-12-31 17:30:58 +0100
commitc40209dcc09f19524fb85251f39a4051491bbec0 (patch)
treef7788f18e608acf0befab73b3391fea4c21a7066
parent5dfec4e23b4a3b81f8ec19bf0cf45147ad6b18e5 (diff)
Made it possible to change an application's label in its configuration.
Fixed #21683.
-rw-r--r--django/apps/registry.py21
-rw-r--r--django/db/migrations/state.py4
-rw-r--r--django/db/models/base.py40
-rw-r--r--docs/ref/applications.txt29
-rw-r--r--docs/releases/1.7.txt3
-rw-r--r--tests/apps/apps.py5
-rw-r--r--tests/apps/tests.py4
-rw-r--r--tests/proxy_model_inheritance/tests.py6
8 files changed, 82 insertions, 30 deletions
diff --git a/django/apps/registry.py b/django/apps/registry.py
index 317bde33fe..7ff1c6625d 100644
--- a/django/apps/registry.py
+++ b/django/apps/registry.py
@@ -201,6 +201,27 @@ class Apps(object):
app_config = self.app_configs.get(app_name.rpartition(".")[2])
return app_config is not None and app_config.name == app_name
+ def get_containing_app_config(self, object_name):
+ """
+ Look for an app config containing a given object.
+
+ object_name is the dotted Python path to the object.
+
+ Returns the app config for the inner application in case of nesting.
+ Returns None if the object isn't in any registered app config.
+
+ It's safe to call this method at import time, even while the registry
+ is being populated.
+ """
+ candidates = []
+ for app_config in self.app_configs.values():
+ if object_name.startswith(app_config.name):
+ subpath = object_name[len(app_config.name):]
+ if subpath == '' or subpath[0] == '.':
+ candidates.append(app_config)
+ if candidates:
+ return sorted(candidates, key=lambda ac: -len(ac.name))[0]
+
def get_registered_model(self, app_label, model_name):
"""
Similar to get_model(), but doesn't require that an app exists with
diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py
index ff2e26c7a0..3bc7837089 100644
--- a/django/db/migrations/state.py
+++ b/django/db/migrations/state.py
@@ -76,9 +76,7 @@ class AppConfigStub(AppConfig):
Stubs a Django AppConfig. Only provides a label and a dict of models.
"""
def __init__(self, label):
- self.label = label
- self.path = None
- super(AppConfigStub, self).__init__(None, None)
+ super(AppConfigStub, self).__init__(label, None)
def import_models(self, all_models):
self.models = all_models
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 03f1e8b693..0de21fafc9 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -86,23 +86,35 @@ class ModelBase(type):
meta = attr_meta
base_meta = getattr(new_class, '_meta', None)
+ # Look for an application configuration to attach the model to.
+ app_config = apps.get_containing_app_config(module)
+
if getattr(meta, 'app_label', None) is None:
- # Figure out the app_label by looking one level up from the package
- # or module named 'models'. If no such package or module exists,
- # fall back to looking one level up from the module this model is
- # defined in.
- # For 'django.contrib.sites.models', this would be 'sites'.
- # For 'geo.models.places' this would be 'geo'.
+ if app_config is None:
+ # If the model is imported before the configuration for its
+ # application is created (#21719), or isn't in an installed
+ # application (#21680), use the legacy logic to figure out the
+ # app_label by looking one level up from the package or module
+ # named 'models'. If no such package or module exists, fall
+ # back to looking one level up from the module this model is
+ # defined in.
+
+ # For 'django.contrib.sites.models', this would be 'sites'.
+ # For 'geo.models.places' this would be 'geo'.
+
+ model_module = sys.modules[new_class.__module__]
+ package_components = model_module.__name__.split('.')
+ package_components.reverse() # find the last occurrence of 'models'
+ try:
+ app_label_index = package_components.index(MODELS_MODULE_NAME) + 1
+ except ValueError:
+ app_label_index = 1
+ kwargs = {"app_label": package_components[app_label_index]}
+
+ else:
+ kwargs = {"app_label": app_config.label}
- model_module = sys.modules[new_class.__module__]
- package_components = model_module.__name__.split('.')
- package_components.reverse() # find the last occurrence of 'models'
- try:
- app_label_index = package_components.index(MODELS_MODULE_NAME) + 1
- except ValueError:
- app_label_index = 1
- kwargs = {"app_label": package_components[app_label_index]}
else:
kwargs = {}
diff --git a/docs/ref/applications.txt b/docs/ref/applications.txt
index eadf9895ca..38bf474371 100644
--- a/docs/ref/applications.txt
+++ b/docs/ref/applications.txt
@@ -114,24 +114,33 @@ Application configuration
Configurable attributes
-----------------------
-.. attribute:: AppConfig.verbose_name
+.. attribute:: AppConfig.name
- Human-readable name for the application, e.g. "Admin".
+ Full Python path to the application, e.g. ``'django.contrib.admin'``.
- If this isn't provided, Django uses ``label.title()``.
+ This attribute defines which application the configuration applies to. It
+ must be set in all :class:`~django.apps.AppConfig` subclasses.
-Read-only attributes
---------------------
+ It must be unique across a Django project.
-.. attribute:: AppConfig.name
+.. attribute:: AppConfig.label
- Full Python path to the application, e.g. ``'django.contrib.admin'``.
+ Short name for the application, e.g. ``'admin'``
-.. attribute:: AppConfig.label
+ This attribute allows relabelling an application when two applications
+ have conflicting labels. It defaults to the last component of ``name``.
+ It should be a valid Python identifier.
+
+ It must be unique across a Django project.
+
+.. attribute:: AppConfig.verbose_name
- Last component of the Python path to the application, e.g. ``'admin'``.
+ Human-readable name for the application, e.g. "Admin".
+
+ This attribute defaults to ``label.title()``.
- This value must be unique across a Django project.
+Read-only attributes
+--------------------
.. attribute:: AppConfig.path
diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt
index c17f62aeee..453714b6f1 100644
--- a/docs/releases/1.7.txt
+++ b/docs/releases/1.7.txt
@@ -79,6 +79,9 @@ Improvements thus far include:
* It is possible to omit ``models.py`` entirely if an application doesn't
have any models.
+* Applications can be relabeled with the :attr:`~django.apps.AppConfig.label`
+ attribute of application configurations, to work around label conflicts.
+
* The name of applications can be customized in the admin with the
:attr:`~django.apps.AppConfig.verbose_name` of application configurations.
diff --git a/tests/apps/apps.py b/tests/apps/apps.py
index 22c8f3062f..c9d761deb0 100644
--- a/tests/apps/apps.py
+++ b/tests/apps/apps.py
@@ -23,3 +23,8 @@ class NotAConfig(object):
class NoSuchApp(AppConfig):
name = 'there is no such app'
+
+
+class RelabeledAppsConfig(AppConfig):
+ name = 'apps'
+ label = 'relabeled'
diff --git a/tests/apps/tests.py b/tests/apps/tests.py
index 2a41acf2e8..495d24dc3e 100644
--- a/tests/apps/tests.py
+++ b/tests/apps/tests.py
@@ -111,6 +111,10 @@ class AppsTests(TestCase):
self.assertTrue(apps.has_app('django.contrib.staticfiles'))
self.assertFalse(apps.has_app('django.contrib.webdesign'))
+ @override_settings(INSTALLED_APPS=['apps.apps.RelabeledAppsConfig'])
+ def test_relabeling(self):
+ self.assertEqual(apps.get_app_config('relabeled').name, 'apps')
+
def test_models_py(self):
"""
Tests that the models in the models.py file were loaded correctly.
diff --git a/tests/proxy_model_inheritance/tests.py b/tests/proxy_model_inheritance/tests.py
index 06b47ff82d..5ad5e08c25 100644
--- a/tests/proxy_model_inheritance/tests.py
+++ b/tests/proxy_model_inheritance/tests.py
@@ -1,4 +1,4 @@
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
import os
import sys
@@ -29,8 +29,8 @@ class ProxyModelInheritanceTests(TransactionTestCase):
def test_table_exists(self):
with self.modify_settings(INSTALLED_APPS={'append': ['app1', 'app2']}):
call_command('migrate', verbosity=0)
- from .app1.models import ProxyModel
- from .app2.models import NiceModel
+ from app1.models import ProxyModel
+ from app2.models import NiceModel
self.assertEqual(NiceModel.objects.all().count(), 0)
self.assertEqual(ProxyModel.objects.all().count(), 0)