Ticket #3871: r17204-custom-reverse-managers.diff

File r17204-custom-reverse-managers.diff, 16.7 KB (added by sebastian, 4 years ago)

Revised patch.

  • docs/ref/models/relations.txt

     
    103103
    104104        Just like ``remove()``, ``clear()`` is only available on
    105105        :class:`~django.db.models.ForeignKey`\s where ``null=True``.
     106
     107    .. versionadded:: 1.4
     108
     109    .. method:: manager(manager)
     110
     111        Returns a new related manager which uses the named ``manager`` instead
     112        of the default manager on the related class to look up related objects::
     113
     114            class Reporter(models.Model):
     115                ...
     116
     117            class Article(models.Model):
     118                reporter = models.ForeignKey(Reporter)
     119                ...
     120                articles = models.Manager()
     121                published_articles = PublishedManager()
     122
     123        In the above example, ``reporter.article_set`` is a manager for all articles
     124        written by ``reporter``, whereas ``reporter.article_set.manager("published_articles")``
     125        returns a manager with only published articles written by ``reporter``.
     126
     127        Related managers returned by ``manager()`` do not provide a ``remove()``
     128        method. This is enforced to avoid mistakes: ``remove()`` would only check if
     129        the given objects are related at all, but not if they are within the domain
     130        of the selected manager. For the same reason, managers returned by ``manager()``
     131        for either side of a :class:`~django.db.models.ManyToManyField` relation do not
     132        have a ``clear()`` method.
  • django/db/models/fields/related.py

     
    99from django.db.models.query import QuerySet
    1010from django.db.models.query_utils import QueryWrapper
    1111from django.db.models.deletion import CASCADE
     12from django.db.models.manager import Manager
    1213from django.utils.encoding import smart_unicode
    1314from django.utils.translation import ugettext_lazy as _, string_concat
    1415from django.utils.functional import curry, cached_property
     
    439440    def related_manager_cls(self):
    440441        # Dynamically create a class that subclasses the related model's default
    441442        # manager.
    442         superclass = self.related.model._default_manager.__class__
     443        return self._related_manager_cls(self.related.model._default_manager.__class__)
     444
     445    def _related_manager_cls(self, superclass, uses_default_manager=True):
    443446        rel_field = self.related.field
    444447        rel_model = self.related.model
    445448        attname = rel_field.rel.get_related_field().attname
    446449
     450        def create_related_manager_cls(manager):
     451            return self._related_manager_cls(superclass=manager.__class__, uses_default_manager=False)
     452
    447453        class RelatedManager(superclass):
    448454            def __init__(self, instance):
    449455                super(RelatedManager, self).__init__()
     
    471477                        False,
    472478                        rel_field.related_query_name())
    473479
     480            def manager(self, manager):
     481                """
     482                Selects a manager from the list of the model's managers, useful
     483                in the case of choosing a manager which is not the default, in a reverse relation.
     484                """
     485                other_manager = getattr(rel_model, manager, None)
     486                if isinstance(other_manager, Manager):
     487                    rel_manager_cls = create_related_manager_cls(other_manager)
     488                    return rel_manager_cls(self.instance)
     489                else:
     490                    raise AttributeError("Manager '%s' does not exist" % manager)
     491
    474492            def add(self, *objs):
    475493                for obj in objs:
    476494                    if not isinstance(obj, self.model):
     
    495513
    496514            # remove() and clear() are only provided if the ForeignKey can have a value of null.
    497515            if rel_field.null:
    498                 def remove(self, *objs):
    499                     val = getattr(self.instance, attname)
    500                     for obj in objs:
    501                         # Is obj actually part of this descriptor set?
    502                         if getattr(obj, rel_field.attname) == val:
    503                             setattr(obj, rel_field.name, None)
    504                             obj.save()
    505                         else:
    506                             raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, self.instance))
    507                 remove.alters_data = True
     516                # If we are dealing with an explicit manager given through the manager method, we
     517                # also do not allow the remove() method. We enforce this to avoid surprises: the
     518                # current implementation checks only whether the given objects are related, but
     519                # not whether they are in the actual domain of the current manager.
     520                if uses_default_manager:
     521                    def remove(self, *objs):
     522                        val = getattr(self.instance, attname)
     523                        for obj in objs:
     524                            # Is obj actually part of this descriptor set?
     525                            if getattr(obj, rel_field.attname) == val:
     526                                setattr(obj, rel_field.name, None)
     527                                obj.save()
     528                            else:
     529                                raise rel_field.rel.to.DoesNotExist("%r is not related to %r." % (obj, self.instance))
     530                    remove.alters_data = True
    508531
     532                # This works also for explicit managers, in which case it does the expected thing.
    509533                def clear(self):
    510534                    self.update(**{rel_field.name: None})
    511535                clear.alters_data = True
     
    513537        return RelatedManager
    514538
    515539
    516 def create_many_related_manager(superclass, rel):
     540def create_many_related_manager(superclass, rel, uses_default_manager=True):
    517541    """Creates a manager that subclasses 'superclass' (which is a Manager)
    518542    and adds behavior for many-to-many related objects."""
    519543    class ManyRelatedManager(superclass):
     
    570594                    False,
    571595                    self.prefetch_cache_name)
    572596
     597        def manager(self, manager):
     598            """
     599            Selects a manager from the list of the model's managers, useful
     600            in the case of choosing a manager which is not the default, in a reverse relation.
     601            """
     602            other_manager = getattr(self.model, manager, None)
     603            if isinstance(other_manager, Manager):
     604                rel_manager_cls = create_many_related_manager(other_manager.__class__, rel, uses_default_manager=False)
     605                return rel_manager_cls(
     606                    model=self.model, query_field_name=self.query_field_name, instance=self.instance, symmetrical=self.symmetrical,
     607                    source_field_name=self.source_field_name, target_field_name=self.target_field_name, reverse=self.reverse,
     608                    through=self.through, prefetch_cache_name=self.prefetch_cache_name)
     609            else:
     610                raise AttributeError("Manager '%s' does not exist" % manager)
     611
    573612        # If the ManyToMany relation has an intermediary model,
    574613        # the add and remove methods do not exist.
    575614        if rel.through._meta.auto_created:
     
    581620                    self._add_items(self.target_field_name, self.source_field_name, *objs)
    582621            add.alters_data = True
    583622
    584             def remove(self, *objs):
    585                 self._remove_items(self.source_field_name, self.target_field_name, *objs)
     623            # The remove method also does not exist when we are
     624            # dealing with an explicit manager defined through the
     625            # manager method, to avoid mistakes: there is no explicit
     626            # check if the removed objects are actually in the related
     627            # manager.
     628            if uses_default_manager:
     629                def remove(self, *objs):
     630                    self._remove_items(self.source_field_name, self.target_field_name, *objs)
    586631
    587                 # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
    588                 if self.symmetrical:
    589                     self._remove_items(self.target_field_name, self.source_field_name, *objs)
    590             remove.alters_data = True
     632                    # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table
     633                    if self.symmetrical:
     634                        self._remove_items(self.target_field_name, self.source_field_name, *objs)
     635                remove.alters_data = True
    591636
    592         def clear(self):
    593             self._clear_items(self.source_field_name)
     637        # If an explicit manager has been defined through the manager method,
     638        # the clear method is unavailable for the same reason as the remove method.
     639        if uses_default_manager:
     640            def clear(self):
     641                self._clear_items(self.source_field_name)
    594642
    595             # If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table
    596             if self.symmetrical:
    597                 self._clear_items(self.target_field_name)
    598         clear.alters_data = True
     643                # If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table
     644                if self.symmetrical:
     645                    self._clear_items(self.target_field_name)
     646            clear.alters_data = True
    599647
    600648        def create(self, **kwargs):
    601649            # This check needs to be done here, since we can't later remove this
  • tests/modeltests/custom_managers/tests.py

     
    22
    33from django.test import TestCase
    44
    5 from .models import Person, Book, Car, PersonManager, PublishedBookManager
     5from .models import Person, Book, Manufacturer, Color, Car, PersonManager, PublishedBookManager
    66
    77
    88class CustomManagerTests(TestCase):
     
    4242            lambda b: b.title
    4343        )
    4444
    45         c1 = Car.cars.create(name="Corvette", mileage=21, top_speed=180)
    46         c2 = Car.cars.create(name="Neon", mileage=31, top_speed=100)
     45    def test_related_manager(self):
     46        chevy = Manufacturer.objects.create(name="Chevrolet", country="USA")
     47        dodge = Manufacturer.objects.create(name="Dodge", country="USA")
    4748
     49        red = Color.objects.create(name="Red", surcharge=False)
     50        white = Color.objects.create(name="White", surcharge=False)
     51        silver = Color.objects.create(name="Silver", surcharge=True)
     52
     53        chevy_corvette = Car.cars.create(name="Corvette", mileage=21, top_speed=180, manufacturer=chevy)
     54        chevy_corvette.available_colors.add(red)
     55        chevy_corvette.available_colors.add(white)
     56
     57        dodge_neon = Car.cars.create(name="Neon", mileage=31, top_speed=100, manufacturer=dodge)
     58        dodge_neon.available_colors.add(white)
     59        dodge_neon.available_colors.add(silver)
     60
     61        dodge_viper = Car.cars.create(name="Viper", mileage=14, top_speed=200, manufacturer=dodge)
     62        dodge_viper.available_colors.add(red)
     63
    4864        self.assertQuerysetEqual(
    4965            Car.cars.order_by("name"), [
    5066                "Corvette",
    5167                "Neon",
     68                "Viper",
    5269            ],
    5370            lambda c: c.name
    5471        )
    5572
    5673        self.assertQuerysetEqual(
    57             Car.fast_cars.all(), [
     74            Car.fast_cars.order_by("name"), [
    5875                "Corvette",
     76                "Viper",
    5977            ],
    6078            lambda c: c.name
    6179        )
    6280
     81        self.assertQuerysetEqual(
     82            dodge.car_set.order_by("name"), [
     83                "Neon",
     84                "Viper",
     85            ],
     86            lambda c: c.name
     87        )
     88
     89        self.assertQuerysetEqual(
     90            white.car_set.order_by("name"), [
     91                "Corvette",
     92                "Neon",
     93            ],
     94            lambda c: c.name
     95        )
     96
     97        self.assertQuerysetEqual(
     98            dodge_neon.available_colors.order_by("name"), [
     99                "Silver",
     100                "White",
     101            ],
     102            lambda c: c.name
     103        )
     104
     105        # The remove and clear methods must be available. Check only
     106        # for availability here but don't actually run those methods
     107        # -> no "()".
     108        dodge.car_set.remove
     109        dodge.car_set.clear
     110        white.car_set.remove
     111        white.car_set.clear
     112        dodge_neon.available_colors.remove
     113        dodge_neon.available_colors.clear
     114
    63115        # Each model class gets a "_default_manager" attribute, which is a
    64116        # reference to the first manager defined in the class. In this case,
    65117        # it's "cars".
    66 
    67118        self.assertQuerysetEqual(
    68119            Car._default_manager.order_by("name"), [
    69120                "Corvette",
    70121                "Neon",
     122                "Viper",
    71123            ],
    72124            lambda c: c.name
    73125        )
     126
     127        # Choosing a custom manager in a reverse relation.
     128        self.assertQuerysetEqual(
     129            dodge.car_set.manager("fast_cars").all(), [
     130                "Viper",
     131            ],
     132            lambda c: c.name
     133        )
     134
     135        # Choosing a custom manager in a many-to-many reverse relation.
     136        self.assertQuerysetEqual(
     137            white.car_set.manager("fast_cars").all(), [
     138                "Corvette",
     139            ],
     140            lambda c: c.name
     141        )
     142
     143        # Choosing a custom manager in many-to-many forward relation.
     144        self.assertQuerysetEqual(
     145            dodge_neon.available_colors.manager("free_of_charge_colors").all(), [
     146                "White",
     147            ],
     148            lambda c: c.name
     149        )
     150
     151        # Unknown managers must raise exception.
     152        self.assertRaises(
     153            AttributeError,
     154            dodge.car_set.manager, "red_cars"
     155        )
     156        self.assertRaises(
     157            AttributeError,
     158            white.car_set.manager, "red_cars"
     159        )
     160        self.assertRaises(
     161            AttributeError,
     162            dodge_neon.available_colors.manager, "beautiful_colors"
     163        )
     164
     165        # Removing from managers with explicitly selected related manager
     166        # must not be possible to avoid mistakes (as the related manager
     167        # does not check if the given objects are actually part of the
     168        # domain selected by that manager).
     169        # clear on (regular) reverse relations is the only exception.
     170        self.assertRaises(AttributeError, lambda: dodge.car_set.manager("fast_cars").remove)
     171        dodge.car_set.manager("fast_cars").clear  # check availability, but don't run method
     172        self.assertRaises(AttributeError, lambda: white.car_set.manager("fast_cars").remove)
     173        self.assertRaises(AttributeError, lambda: white.car_set.manager("fast_cars").clear)
     174        self.assertRaises(AttributeError, lambda: dodge_neon.available_colors.manager("free_of_charge_colors").remove)
     175        self.assertRaises(AttributeError, lambda: dodge_neon.available_colors.manager("free_of_charge_colors").clear)
     176
     177        # Clearing reverse relation with custom manager must only
     178        # remove objects selected by that manager.
     179        self.assertEqual(dodge.car_set.count(), 2)
     180        dodge.car_set.manager("fast_cars").clear()
     181        self.assertEqual(dodge.car_set.count(), 1)
     182        dodge.car_set.clear()
     183        self.assertEqual(dodge.car_set.count(), 0)
  • tests/modeltests/custom_managers/models.py

     
    4444
    4545# An example of providing multiple custom managers.
    4646
     47class Manufacturer(models.Model):
     48    name = models.CharField(max_length=10)
     49    country = models.CharField(max_length=20)
     50
     51class FreeOfChargeColorManager(models.Manager):
     52    def get_query_set(self):
     53        return super(FreeOfChargeColorManager, self).get_query_set().filter(surcharge=False)
     54
     55class Color(models.Model):
     56    name = models.CharField(max_length=10)
     57    surcharge = models.BooleanField()
     58    objects = models.Manager()
     59    free_of_charge_colors = FreeOfChargeColorManager()
     60
    4761class FastCarManager(models.Manager):
    4862    def get_query_set(self):
    4963        return super(FastCarManager, self).get_query_set().filter(top_speed__gt=150)
    5064
    5165class Car(models.Model):
    5266    name = models.CharField(max_length=10)
     67    manufacturer = models.ForeignKey(Manufacturer, null=True)
     68    available_colors = models.ManyToManyField(Color)
    5369    mileage = models.IntegerField()
    5470    top_speed = models.IntegerField(help_text="In miles per hour.")
    5571    cars = models.Manager()
Back to Top