Commit e043aae9 authored by Loic Bistuer's avatar Loic Bistuer
Browse files

Fixed #23550 -- Normalized get_queryset() of RelatedObjectDescriptor

and ReverseSingleRelatedObjectDescriptor so they actually return QuerySet
instances.

Also ensured that SingleRelatedObjectDescriptor.get_queryset() accounts
for use_for_related_fields=True.

This cleanup lays the groundwork for #23533.

Thanks Anssi Kääriäinen for the review.
parent 1fd6e13b
Loading
Loading
Loading
Loading
+12 −14
Original line number Diff line number Diff line
@@ -370,14 +370,16 @@ class SingleRelatedObjectDescriptor(object):
        return hasattr(instance, self.cache_name)

    def get_queryset(self, **hints):
        # Gotcha: we return a `Manager` instance (i.e. not a `QuerySet`)!
        return self.related.model._base_manager.db_manager(hints=hints)
        manager = self.related.model._default_manager
        # If the related manager indicates that it should be used for
        # related fields, respect that.
        if not getattr(manager, 'use_for_related_fields', False):
            manager = self.related.model._base_manager
        return manager.db_manager(hints=hints).all()

    def get_prefetch_queryset(self, instances, queryset=None):
        if queryset is None:
            # Despite its name `get_queryset()` returns an instance of
            # `Manager`, therefore we call `all()` to normalize to `QuerySet`.
            queryset = self.get_queryset().all()
            queryset = self.get_queryset()
        queryset._add_hints(instance=instances[0])

        rel_obj_attr = attrgetter(self.related.field.attname)
@@ -499,20 +501,16 @@ class ReverseSingleRelatedObjectDescriptor(object):
        return hasattr(instance, self.cache_name)

    def get_queryset(self, **hints):
        rel_mgr = self.field.rel.to._default_manager.db_manager(hints=hints)
        manager = self.field.rel.to._default_manager
        # If the related manager indicates that it should be used for
        # related fields, respect that.
        if getattr(rel_mgr, 'use_for_related_fields', False):
            # Gotcha: we return a `Manager` instance (i.e. not a `QuerySet`)!
            return rel_mgr
        else:
            return QuerySet(self.field.rel.to, hints=hints)
        if not getattr(manager, 'use_for_related_fields', False):
            manager = self.field.rel.to._base_manager
        return manager.db_manager(hints=hints).all()

    def get_prefetch_queryset(self, instances, queryset=None):
        if queryset is None:
            # Despite its name `get_queryset()` may return an instance of
            # `Manager`, therefore we call `all()` to normalize to `QuerySet`.
            queryset = self.get_queryset().all()
            queryset = self.get_queryset()
        queryset._add_hints(instance=instances[0])

        rel_obj_attr = self.field.get_foreign_related_value
+21 −0
Original line number Diff line number Diff line
@@ -96,3 +96,24 @@ class Pointer2(models.Model):

class HiddenPointer(models.Model):
    target = models.OneToOneField(Target, related_name='hidden+')


# Test related objects visibility.
class SchoolManager(models.Manager):
    def get_queryset(self):
        return super(SchoolManager, self).get_queryset().filter(is_public=True)


class School(models.Model):
    is_public = models.BooleanField(default=False)
    objects = SchoolManager()


class DirectorManager(models.Manager):
    def get_queryset(self):
        return super(DirectorManager, self).get_queryset().filter(is_temp=False)

class Director(models.Model):
    is_temp = models.BooleanField(default=False)
    school = models.OneToOneField(School)
    objects = DirectorManager()
+57 −1
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@ from django.db import transaction, IntegrityError, connection
from django.test import TestCase

from .models import (Bar, Favorites, HiddenPointer, ManualPrimaryKey, MultiModel,
    Place, RelatedModel, Restaurant, Target, UndergroundBar, Waiter)
    Place, RelatedModel, Restaurant, School, Director, Target, UndergroundBar, Waiter)


class OneToOneTests(TestCase):
@@ -382,3 +382,59 @@ class OneToOneTests(TestCase):
        self.assertFalse(
            hasattr(Target, HiddenPointer._meta.get_field('target').related.get_accessor_name())
        )

    def test_related_object(self):
        public_school = School.objects.create(is_public=True)
        public_director = Director.objects.create(school=public_school, is_temp=False)

        private_school = School.objects.create(is_public=False)
        private_director = Director.objects.create(school=private_school, is_temp=True)

        # Only one school is available via all() due to the custom default manager.
        self.assertQuerysetEqual(
            School.objects.all(),
            ["<School: School object>"]
        )

        # Only one director is available via all() due to the custom default manager.
        self.assertQuerysetEqual(
            Director.objects.all(),
            ["<Director: Director object>"]
        )

        self.assertEqual(public_director.school, public_school)
        self.assertEqual(public_school.director, public_director)

        # Make sure the base manager is used so that the related objects
        # is still accessible even if the default manager doesn't normally
        # allow it.
        self.assertEqual(private_director.school, private_school)

        # Make sure the base manager is used so that an student can still access
        # its related school even if the default manager doesn't normally
        # allow it.
        self.assertEqual(private_school.director, private_director)

        # If the manager is marked "use_for_related_fields", it'll get used instead
        # of the "bare" queryset. Usually you'd define this as a property on the class,
        # but this approximates that in a way that's easier in tests.
        School.objects.use_for_related_fields = True
        try:
            private_director = Director._base_manager.get(pk=private_director.pk)
            self.assertRaises(School.DoesNotExist, lambda: private_director.school)
        finally:
            School.objects.use_for_related_fields = False

        Director.objects.use_for_related_fields = True
        try:
            private_school = School._base_manager.get(pk=private_school.pk)
            self.assertRaises(Director.DoesNotExist, lambda: private_school.director)
        finally:
            Director.objects.use_for_related_fields = False

    def test_hasattr_related_object(self):
        # The exception raised on attribute access when a related object
        # doesn't exist should be an instance of a subclass of `AttributeError`
        # refs #21563
        self.assertFalse(hasattr(Director(), 'director'))
        self.assertFalse(hasattr(School(), 'school'))