From c40209dcc09f19524fb85251f39a4051491bbec0 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 31 Dec 2013 16:23:42 +0100 Subject: [PATCH] Made it possible to change an application's label in its configuration. Fixed #21683. --- django/apps/registry.py | 21 ++++++++++++++ django/db/migrations/state.py | 4 +-- django/db/models/base.py | 40 +++++++++++++++++--------- docs/ref/applications.txt | 31 +++++++++++++------- docs/releases/1.7.txt | 3 ++ tests/apps/apps.py | 5 ++++ tests/apps/tests.py | 4 +++ tests/proxy_model_inheritance/tests.py | 6 ++-- 8 files changed, 83 insertions(+), 31 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 - - Human-readable name for the application, e.g. "Admin". - - If this isn't provided, Django uses ``label.title()``. - -Read-only attributes --------------------- - .. attribute:: AppConfig.name Full Python path to the application, e.g. ``'django.contrib.admin'``. + This attribute defines which application the configuration applies to. It + must be set in all :class:`~django.apps.AppConfig` subclasses. + + It must be unique across a Django project. + .. attribute:: AppConfig.label - Last component of the Python path to the application, e.g. ``'admin'``. + Short name for the application, e.g. ``'admin'`` - This value must be unique across a Django project. + 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 + + Human-readable name for the application, e.g. "Admin". + + This attribute defaults to ``label.title()``. + +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)