diff --git a/AUTHORS b/AUTHORS index 6ad345a971..4bb5f4e225 100644 --- a/AUTHORS +++ b/AUTHORS @@ -266,7 +266,7 @@ answer newbie questions, and generally made Django that much better: kurtiss@meetro.com Denis Kuzmichyov Panos Laganakos - lakin.wecker@gmail.com + Lakin Wecker Nick Lane Stuart Langridge Paul Lanier diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index ac26de21fe..8fec836baf 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -94,7 +94,10 @@ class RelatedField(object): sup.contribute_to_class(cls, name) if not cls._meta.abstract and self.rel.related_name: - self.rel.related_name = self.rel.related_name % {'class': cls.__name__.lower()} + self.rel.related_name = self.rel.related_name % { + 'class': cls.__name__.lower(), + 'app_label': cls._meta.app_label.lower(), + } other = self.rel.to if isinstance(other, basestring) or other._meta.pk is None: diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index 1e69009827..0cca1c7392 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -871,13 +871,19 @@ fields on this class are included into each of the child classes, with exactly the same values for the attributes (including :attr:`~django.db.models.ForeignKey.related_name`) each time. To work around this problem, when you are using :attr:`~django.db.models.ForeignKey.related_name` in an -abstract base class (only), part of the name should be the string -``'%(class)s'``. This is replaced by the lower-cased name of the child class -that the field is used in. Since each class has a different name, each related -name will end up being different. For example:: +abstract base class (only), part of the name should contain +``'%(app_label)s'`` and ``'%(class)s'``. + +- ``'%(class)s'`` is replaced by the lower-cased name of the child class + that the field is used in. +- ``'%(app_label)s'`` is replaced by the lower-cased name of the app the child + class is contained within. Each installed application name must be unique + and the model class names within each app must also be unique, therefore the + resulting name will end up being different. For example, given an app + ``common/models.py``:: class Base(models.Model): - m2m = models.ManyToManyField(OtherModel, related_name="%(class)s_related") + m2m = models.ManyToManyField(OtherModel, related_name="%(app_label)s_%(class)s_related") class Meta: abstract = True @@ -888,19 +894,28 @@ name will end up being different. For example:: class ChildB(Base): pass -The reverse name of the ``ChildA.m2m`` field will be ``childa_related``, -whilst the reverse name of the ``ChildB.m2m`` field will be -``childb_related``. It is up to you how you use the ``'%(class)s'`` portion to -construct your related name, but if you forget to use it, Django will raise +Along with another app ``rare/models.py``:: + from common.models import Base + + class ChildB(Base): + pass + +The reverse name of the ``commmon.ChildA.m2m`` field will be +``common_childa_related``, whilst the reverse name of the +``common.ChildB.m2m`` field will be ``common_childb_related``, and finally the +reverse name of the ``rare.ChildB.m2m`` field will be ``rare_childb_related``. +It is up to you how you use the ``'%(class)s'`` and ``'%(app_label)s`` portion +to construct your related name, but if you forget to use it, Django will raise errors when you validate your models (or run :djadmin:`syncdb`). -If you don't specify a :attr:`~django.db.models.ForeignKey.related_name` attribute for a field in an -abstract base class, the default reverse name will be the name of the -child class followed by ``'_set'``, just as it normally would be if -you'd declared the field directly on the child class. For example, in -the above code, if the :attr:`~django.db.models.ForeignKey.related_name` attribute was omitted, the -reverse name for the ``m2m`` field would be ``childa_set`` in the -``ChildA`` case and ``childb_set`` for the ``ChildB`` field. +If you don't specify a :attr:`~django.db.models.ForeignKey.related_name` +attribute for a field in an abstract base class, the default reverse name will +be the name of the child class followed by ``'_set'``, just as it normally +would be if you'd declared the field directly on the child class. For example, +in the above code, if the :attr:`~django.db.models.ForeignKey.related_name` +attribute was omitted, the reverse name for the ``m2m`` field would be +``childa_set`` in the ``ChildA`` case and ``childb_set`` for the ``ChildB`` +field. .. _multi-table-inheritance: diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py index 26ec0be503..36fc9fef1b 100644 --- a/tests/modeltests/model_inheritance/models.py +++ b/tests/modeltests/model_inheritance/models.py @@ -116,6 +116,31 @@ class ParkingLot(Place): def __unicode__(self): return u"%s the parking lot" % self.name +# +# Abstract base classes with related models where the sub-class has the +# same name in a different app and inherits from the same abstract base +# class. +# NOTE: The actual API tests for the following classes are in +# model_inheritance_same_model_name/models.py - They are defined +# here in order to have the name conflict between apps +# + +class Title(models.Model): + title = models.CharField(max_length=50) + +class NamedURL(models.Model): + title = models.ForeignKey(Title, related_name='attached_%(app_label)s_%(class)s_set') + url = models.URLField() + + class Meta: + abstract = True + +class Copy(NamedURL): + content = models.TextField() + + def __unicode__(self): + return self.content + __test__ = {'API_TESTS':""" # The Student and Worker models both have 'name' and 'age' fields on them and # inherit the __unicode__() method, just as with normal Python subclassing. diff --git a/tests/modeltests/model_inheritance_same_model_name/__init__.py b/tests/modeltests/model_inheritance_same_model_name/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/model_inheritance_same_model_name/models.py b/tests/modeltests/model_inheritance_same_model_name/models.py new file mode 100644 index 0000000000..40de02764a --- /dev/null +++ b/tests/modeltests/model_inheritance_same_model_name/models.py @@ -0,0 +1,19 @@ +""" +XX. Model inheritance + +Model inheritance across apps can result in models with the same name resulting +in the need for an %(app_label)s format string. This app specifically tests +this feature by redefining the Copy model from model_inheritance/models.py +""" + +from django.db import models +from modeltests.model_inheritance.models import NamedURL + +# +# Abstract base classes with related models +# +class Copy(NamedURL): + content = models.TextField() + + def __unicode__(self): + return self.content diff --git a/tests/modeltests/model_inheritance_same_model_name/tests.py b/tests/modeltests/model_inheritance_same_model_name/tests.py new file mode 100644 index 0000000000..3f1e3458e6 --- /dev/null +++ b/tests/modeltests/model_inheritance_same_model_name/tests.py @@ -0,0 +1,32 @@ +from django.test import TestCase +from modeltests.model_inheritance.models import Title + +class InheritanceSameModelNameTests(TestCase): + + def setUp(self): + # The Title model has distinct accessors for both + # model_inheritance.Copy and model_inheritance_same_model_name.Copy + # models. + self.title = Title.objects.create(title='Lorem Ipsum') + + def test_inheritance_related_name(self): + from modeltests.model_inheritance.models import Copy + self.assertEquals( + self.title.attached_model_inheritance_copy_set.create( + content='Save $ on V1agr@', + url='http://v1agra.com/', + title='V1agra is spam', + ), Copy.objects.get(content='Save $ on V1agr@')) + + def test_inheritance_with_same_model_name(self): + from modeltests.model_inheritance_same_model_name.models import Copy + self.assertEquals( + self.title.attached_model_inheritance_same_model_name_copy_set.create( + content='The Web framework for perfectionists with deadlines.', + url='http://www.djangoproject.com/', + title='Django Rocks' + ), Copy.objects.get(content='The Web framework for perfectionists with deadlines.')) + + def test_related_name_attribute_exists(self): + # The Post model doesn't have an attribute called 'attached_%(app_label)s_%(class)s_set'. + self.assertEqual(hasattr(self.title, 'attached_%(app_label)s_%(class)s_set'), False)