Browse Source

Made it possible to change an application's label in its configuration.

Fixed #21683.
Aymeric Augustin 11 years ago
parent
commit
c40209dcc0

+ 21 - 0
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

+ 1 - 3
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

+ 26 - 14
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 = {}
 

+ 19 - 10
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
 

+ 3 - 0
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.
 

+ 5 - 0
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'

+ 4 - 0
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.

+ 3 - 3
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)