diff --git a/django/core/checks/messages.py b/django/core/checks/messages.py index 9245842039..e770f9ccce 100644 --- a/django/core/checks/messages.py +++ b/django/core/checks/messages.py @@ -37,9 +37,7 @@ class CheckMessage(object): elif isinstance(self.obj, models.base.ModelBase): # We need to hardcode ModelBase and Field cases because its __str__ # method doesn't return "applabel.modellabel" and cannot be changed. - model = self.obj - app = model._meta.app_label - obj = '%s.%s' % (app, model._meta.object_name) + obj = self.obj._meta.label else: obj = force_str(self.obj) id = "(%s) " % self.id if self.id else "" diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index 0f7535a148..e678e78463 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -163,8 +163,8 @@ class DeserializedObject(object): self.m2m_data = m2m_data def __repr__(self): - return "" % ( - self.object._meta.app_label, self.object._meta.object_name, self.object.pk) + return "" % ( + self.object._meta.label, self.object.pk) def save(self, save_m2m=True, using=None): # Call save on the Model baseclass directly. This bypasses any diff --git a/django/db/migrations/state.py b/django/db/migrations/state.py index 8165a247a2..b1cba07a9e 100644 --- a/django/db/migrations/state.py +++ b/django/db/migrations/state.py @@ -362,10 +362,9 @@ class ModelState(object): try: fields.append((name, field_class(*args, **kwargs))) except TypeError as e: - raise TypeError("Couldn't reconstruct field %s on %s.%s: %s" % ( + raise TypeError("Couldn't reconstruct field %s on %s: %s" % ( name, - model._meta.app_label, - model._meta.object_name, + model._meta.label, e, )) if not exclude_rels: @@ -423,7 +422,7 @@ class ModelState(object): # Make our record bases = tuple( ( - "%s.%s" % (base._meta.app_label, base._meta.model_name) + base._meta.label_lower if hasattr(base, "_meta") else base ) diff --git a/django/db/models/base.py b/django/db/models/base.py index 82b428b0bd..27764bba84 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -326,9 +326,7 @@ class ModelBase(type): if cls.__doc__ is None: cls.__doc__ = "%s(%s)" % (cls.__name__, ", ".join(f.name for f in opts.fields)) - get_absolute_url_override = settings.ABSOLUTE_URL_OVERRIDES.get( - '%s.%s' % (opts.app_label, opts.model_name) - ) + get_absolute_url_override = settings.ABSOLUTE_URL_OVERRIDES.get(opts.label_lower) if get_absolute_url_override: setattr(cls, 'get_absolute_url', get_absolute_url_override) @@ -1248,10 +1246,7 @@ class Model(six.with_metaclass(ModelBase)): errors.append( checks.Error( "The model has two many-to-many relations through " - "the intermediate model '%s.%s'." % ( - f.remote_field.through._meta.app_label, - f.remote_field.through._meta.object_name - ), + "the intermediate model '%s'." % f.remote_field.through._meta.label, hint=None, obj=cls, id='models.E003', diff --git a/django/db/models/manager.py b/django/db/models/manager.py index e194e28feb..956a8cbd2a 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -76,9 +76,7 @@ class BaseManager(object): def __str__(self): """ Return "app_label.model_label.manager_name". """ - model = self.model - app = model._meta.app_label - return '%s.%s.%s' % (app, model._meta.object_name, self.name) + return '%s.%s' % (self.model._meta.label, self.name) def deconstruct(self): """ diff --git a/django/db/models/options.py b/django/db/models/options.py index a2cdca82ee..a76b934598 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -176,6 +176,14 @@ class Options(object): m2m = link.is_relation and link.many_to_many return link, model, direct, m2m + @property + def label(self): + return '%s.%s' % (self.app_label, self.object_name) + + @property + def label_lower(self): + return '%s.%s' % (self.app_label, self.model_name) + @property def app_config(self): # Don't go through get_app_config to avoid triggering imports. @@ -377,7 +385,6 @@ class Options(object): case insensitive, so we make sure we are case insensitive here. """ if self.swappable: - model_label = '%s.%s' % (self.app_label, self.model_name) swapped_for = getattr(settings, self.swappable, None) if swapped_for: try: @@ -389,7 +396,7 @@ class Options(object): # or as part of validation. return swapped_for - if '%s.%s' % (swapped_label, swapped_object.lower()) not in (None, model_label): + if '%s.%s' % (swapped_label, swapped_object.lower()) not in (None, self.label_lower): return swapped_for return None diff --git a/django/forms/models.py b/django/forms/models.py index 59f9d501d5..1ea0dea58b 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -973,12 +973,12 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): (fk.remote_field.model != parent_model and fk.remote_field.model not in parent_model._meta.get_parent_list()): raise ValueError( - "fk_name '%s' is not a ForeignKey to '%s.%s'." - % (fk_name, parent_model._meta.app_label, parent_model._meta.object_name)) + "fk_name '%s' is not a ForeignKey to '%s'." % (fk_name, parent_model._meta.label) + ) elif len(fks_to_parent) == 0: raise ValueError( - "'%s.%s' has no field named '%s'." - % (model._meta.app_label, model._meta.object_name, fk_name)) + "'%s' has no field named '%s'." % (model._meta.label, fk_name) + ) else: # Try to discover what the ForeignKey from model to parent_model is fks_to_parent = [ @@ -993,20 +993,16 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): if can_fail: return raise ValueError( - "'%s.%s' has no ForeignKey to '%s.%s'." % ( - model._meta.app_label, - model._meta.object_name, - parent_model._meta.app_label, - parent_model._meta.object_name, + "'%s' has no ForeignKey to '%s'." % ( + model._meta.label, + parent_model._meta.label, ) ) else: raise ValueError( - "'%s.%s' has more than one ForeignKey to '%s.%s'." % ( - model._meta.app_label, - model._meta.object_name, - parent_model._meta.app_label, - parent_model._meta.object_name, + "'%s' has more than one ForeignKey to '%s'." % ( + model._meta.label, + parent_model._meta.label, ) ) return fk diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index f9a17721cf..1dff215888 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -29,6 +29,12 @@ Available ``Meta`` options app_label = 'myapp' + .. versionadded:: 1.9 + + If you want to represent a model with the format ``app_label.object_name`` + or ``app_label.model_name`` you can use ``model._meta.label`` + or ``model._meta.label_lower`` respectively. + ``db_table`` ------------ @@ -397,3 +403,26 @@ Django quotes column and table names behind the scenes. verbose_name_plural = "stories" If this isn't given, Django will use :attr:`~Options.verbose_name` + ``"s"``. + +Read-only ``Meta`` attributes +============================= + +``label`` +--------- + +.. attribute:: Options.label + + .. versionadded:: 1.9 + + Representation of the object, returns ``app_label.object_name``, e.g. + ``'polls.Question'``. + +``label_lower`` +--------------- + +.. attribute:: Options.label_lower + + .. versionadded:: 1.9 + + Representation of the model, returns ``app_label.model_name``, e.g. + ``'polls.question'``. diff --git a/tests/model_meta/results.py b/tests/model_meta/results.py index 858b70b73a..0770b86b2a 100644 --- a/tests/model_meta/results.py +++ b/tests/model_meta/results.py @@ -791,4 +791,16 @@ TEST_RESULTS = { 'content_object_abstract', ], }, + 'labels': { + AbstractPerson: 'model_meta.AbstractPerson', + BasePerson: 'model_meta.BasePerson', + Person: 'model_meta.Person', + Relating: 'model_meta.Relating', + }, + 'lower_labels': { + AbstractPerson: 'model_meta.abstractperson', + BasePerson: 'model_meta.baseperson', + Person: 'model_meta.person', + Relating: 'model_meta.relating', + }, } diff --git a/tests/model_meta/tests.py b/tests/model_meta/tests.py index 4421c9ef44..390c0fe988 100644 --- a/tests/model_meta/tests.py +++ b/tests/model_meta/tests.py @@ -49,6 +49,17 @@ class GetFieldsTests(OptionsBaseTests): fields += ["errors"] +class LabelTests(OptionsBaseTests): + + def test_label(self): + for model, expected_result in TEST_RESULTS['labels'].items(): + self.assertEqual(model._meta.label, expected_result) + + def test_label_lower(self): + for model, expected_result in TEST_RESULTS['lower_labels'].items(): + self.assertEqual(model._meta.label_lower, expected_result) + + class DataTests(OptionsBaseTests): def test_fields(self):