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

Fixed #21683.
This commit is contained in:
Aymeric Augustin 2013-12-31 16:23:42 +01:00
parent 5dfec4e23b
commit c40209dcc0
8 changed files with 83 additions and 31 deletions

View File

@ -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

View File

@ -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

View File

@ -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 = {}

View File

@ -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

View File

@ -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.

View File

@ -23,3 +23,8 @@ class NotAConfig(object):
class NoSuchApp(AppConfig):
name = 'there is no such app'
class RelabeledAppsConfig(AppConfig):
name = 'apps'
label = 'relabeled'

View File

@ -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.

View File

@ -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)