Made it possible to change an application's label in its configuration.
Fixed #21683.
This commit is contained in:
parent
5dfec4e23b
commit
c40209dcc0
|
@ -201,6 +201,27 @@ class Apps(object):
|
||||||
app_config = self.app_configs.get(app_name.rpartition(".")[2])
|
app_config = self.app_configs.get(app_name.rpartition(".")[2])
|
||||||
return app_config is not None and app_config.name == app_name
|
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):
|
def get_registered_model(self, app_label, model_name):
|
||||||
"""
|
"""
|
||||||
Similar to get_model(), but doesn't require that an app exists with
|
Similar to get_model(), but doesn't require that an app exists with
|
||||||
|
|
|
@ -76,9 +76,7 @@ class AppConfigStub(AppConfig):
|
||||||
Stubs a Django AppConfig. Only provides a label and a dict of models.
|
Stubs a Django AppConfig. Only provides a label and a dict of models.
|
||||||
"""
|
"""
|
||||||
def __init__(self, label):
|
def __init__(self, label):
|
||||||
self.label = label
|
super(AppConfigStub, self).__init__(label, None)
|
||||||
self.path = None
|
|
||||||
super(AppConfigStub, self).__init__(None, None)
|
|
||||||
|
|
||||||
def import_models(self, all_models):
|
def import_models(self, all_models):
|
||||||
self.models = all_models
|
self.models = all_models
|
||||||
|
|
|
@ -86,23 +86,35 @@ class ModelBase(type):
|
||||||
meta = attr_meta
|
meta = attr_meta
|
||||||
base_meta = getattr(new_class, '_meta', None)
|
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:
|
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'.
|
if app_config is None:
|
||||||
# For 'geo.models.places' this would be 'geo'.
|
# 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:
|
else:
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
|
|
|
@ -114,24 +114,33 @@ Application configuration
|
||||||
Configurable attributes
|
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
|
.. attribute:: AppConfig.name
|
||||||
|
|
||||||
Full Python path to the application, e.g. ``'django.contrib.admin'``.
|
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
|
.. 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
|
.. attribute:: AppConfig.path
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,9 @@ Improvements thus far include:
|
||||||
* It is possible to omit ``models.py`` entirely if an application doesn't
|
* It is possible to omit ``models.py`` entirely if an application doesn't
|
||||||
have any models.
|
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
|
* The name of applications can be customized in the admin with the
|
||||||
:attr:`~django.apps.AppConfig.verbose_name` of application configurations.
|
:attr:`~django.apps.AppConfig.verbose_name` of application configurations.
|
||||||
|
|
||||||
|
|
|
@ -23,3 +23,8 @@ class NotAConfig(object):
|
||||||
|
|
||||||
class NoSuchApp(AppConfig):
|
class NoSuchApp(AppConfig):
|
||||||
name = 'there is no such app'
|
name = 'there is no such app'
|
||||||
|
|
||||||
|
|
||||||
|
class RelabeledAppsConfig(AppConfig):
|
||||||
|
name = 'apps'
|
||||||
|
label = 'relabeled'
|
||||||
|
|
|
@ -111,6 +111,10 @@ class AppsTests(TestCase):
|
||||||
self.assertTrue(apps.has_app('django.contrib.staticfiles'))
|
self.assertTrue(apps.has_app('django.contrib.staticfiles'))
|
||||||
self.assertFalse(apps.has_app('django.contrib.webdesign'))
|
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):
|
def test_models_py(self):
|
||||||
"""
|
"""
|
||||||
Tests that the models in the models.py file were loaded correctly.
|
Tests that the models in the models.py file were loaded correctly.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
@ -29,8 +29,8 @@ class ProxyModelInheritanceTests(TransactionTestCase):
|
||||||
def test_table_exists(self):
|
def test_table_exists(self):
|
||||||
with self.modify_settings(INSTALLED_APPS={'append': ['app1', 'app2']}):
|
with self.modify_settings(INSTALLED_APPS={'append': ['app1', 'app2']}):
|
||||||
call_command('migrate', verbosity=0)
|
call_command('migrate', verbosity=0)
|
||||||
from .app1.models import ProxyModel
|
from app1.models import ProxyModel
|
||||||
from .app2.models import NiceModel
|
from app2.models import NiceModel
|
||||||
self.assertEqual(NiceModel.objects.all().count(), 0)
|
self.assertEqual(NiceModel.objects.all().count(), 0)
|
||||||
self.assertEqual(ProxyModel.objects.all().count(), 0)
|
self.assertEqual(ProxyModel.objects.all().count(), 0)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue