From f05722a08a11b2e6c6c9acdf2bacd6a6128ccbb0 Mon Sep 17 00:00:00 2001 From: James Pulec Date: Tue, 29 Sep 2015 10:52:26 -0700 Subject: [PATCH] Fixed #25354 -- Added class/app_label interpolation for related_query_name. --- django/db/models/fields/related.py | 7 ++++ docs/ref/models/fields.txt | 3 ++ docs/releases/1.10.txt | 4 +++ docs/topics/db/models.txt | 51 ++++++++++++++++++++---------- tests/model_inheritance/models.py | 7 +++- tests/model_inheritance/tests.py | 9 ++++++ 6 files changed, 63 insertions(+), 18 deletions(-) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 2071d5ff96..08d2aa7a43 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -299,6 +299,13 @@ class RelatedField(Field): } self.remote_field.related_name = related_name + if self.remote_field.related_query_name: + related_query_name = force_text(self.remote_field.related_query_name) % { + 'class': cls.__name__.lower(), + 'app_label': cls._meta.app_label.lower(), + } + self.remote_field.related_query_name = related_query_name + def resolve_related_class(model, related, field): field.remote_field.model = related field.do_related_class(related, model) diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 0a33e96122..28c93bf60c 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -1344,6 +1344,9 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in # That's now the name of the reverse filter Article.objects.filter(tag__name="important") + Like :attr:`related_name`, ``related_query_name`` supports app label and + class interpolation via :ref:`some special syntax `. + .. attribute:: ForeignKey.to_field The field on the related object that the relation is to. By default, Django diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index bab0f900bf..1f9cc69453 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -268,6 +268,10 @@ Models * :meth:`QuerySet.in_bulk() ` may be called without any arguments to return all objects in the queryset. +* :attr:`~django.db.models.ForeignKey.related_query_name` now supports + app label and class interpolation using the ``'%(app_label)s'`` and + ``'%(class)s'`` strings. + Requests and Responses ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index 08ea426690..05b8bfcad9 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -967,18 +967,23 @@ the same database table, which is almost certainly not what you want. .. _abstract-related-name: -Be careful with ``related_name`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Be careful with ``related_name`` and ``related_query_name`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you are using the :attr:`~django.db.models.ForeignKey.related_name` attribute on a ``ForeignKey`` or -``ManyToManyField``, you must always specify a *unique* reverse name for the -field. This would normally cause a problem in abstract base classes, since the -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. +If you are using :attr:`~django.db.models.ForeignKey.related_name` or +:attr:`~django.db.models.ForeignKey.related_query_name` on a ``ForeignKey`` or +``ManyToManyField``, you must always specify a *unique* reverse name and query +name for the field. This would normally cause a problem in abstract base +classes, since the 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` and +:attr:`~django.db.models.ForeignKey.related_query_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 contain -``'%(app_label)s'`` and ``'%(class)s'``. +To work around this problem, when you are using +:attr:`~django.db.models.ForeignKey.related_name` or +:attr:`~django.db.models.ForeignKey.related_query_name` in an abstract base +class (only), part of the value 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. @@ -992,7 +997,11 @@ For example, given an app ``common/models.py``:: from django.db import models class Base(models.Model): - m2m = models.ManyToManyField(OtherModel, related_name="%(app_label)s_%(class)s_related") + m2m = models.ManyToManyField( + OtherModel, + related_name="%(app_label)s_%(class)s_related", + related_query_name="%(app_label)s_%(class)ss", + ) class Meta: abstract = True @@ -1011,12 +1020,15 @@ Along with another app ``rare/models.py``:: pass The reverse name of the ``common.ChildA.m2m`` field will be -``common_childa_related``, while 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 perform system checks (or run :djadmin:`migrate`). +``common_childa_related`` and the reverse query name will be ``common_childas``. +The reverse name of the ``common.ChildB.m2m`` field will be +``common_childb_related`` and the reverse query name will be +``common_childbs``. Finally, the reverse name of the ``rare.ChildB.m2m`` field +will be ``rare_childb_related`` and the reverse query name will be +``rare_childbs``. It's up to you how you use the `'%(class)s'`` and +``'%(app_label)s`` portion to construct your related name or related query name +but if you forget to use it, Django will raise errors when you perform system +checks (or run :djadmin:`migrate`). 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 @@ -1027,6 +1039,11 @@ 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. +.. versionchanged:: 1.10 + + Interpolation of ``'%(app_label)s'`` and ``'%(class)s'`` for + ``related_query_name`` was added. + .. _multi-table-inheritance: Multi-table inheritance diff --git a/tests/model_inheritance/models.py b/tests/model_inheritance/models.py index 22086ca11d..652eea4eaa 100644 --- a/tests/model_inheritance/models.py +++ b/tests/model_inheritance/models.py @@ -56,7 +56,12 @@ class Post(models.Model): @python_2_unicode_compatible class Attachment(models.Model): - post = models.ForeignKey(Post, models.CASCADE, related_name='attached_%(class)s_set') + post = models.ForeignKey( + Post, + models.CASCADE, + related_name='attached_%(class)s_set', + related_query_name='attached_%(app_label)s_%(class)ss', + ) content = models.TextField() class Meta: diff --git a/tests/model_inheritance/tests.py b/tests/model_inheritance/tests.py index 336ddf3e15..a6661d439e 100644 --- a/tests/model_inheritance/tests.py +++ b/tests/model_inheritance/tests.py @@ -72,6 +72,15 @@ class ModelInheritanceTests(TestCase): AttributeError, getattr, post, "attached_%(class)s_set" ) + def test_model_with_distinct_related_query_name(self): + self.assertQuerysetEqual(Post.objects.filter(attached_model_inheritance_comments__is_spam=True), []) + + # The Post model doesn't have a related query accessor based on + # related_name (attached_comment_set). + msg = "Cannot resolve keyword 'attached_comment_set' into field." + with self.assertRaisesMessage(FieldError, msg): + Post.objects.filter(attached_comment_set__is_spam=True) + def test_meta_fields_and_ordering(self): # Make sure Restaurant and ItalianRestaurant have the right fields in # the right order.