Ticket #3871: r17204-custom-reverse-managers.diff
File r17204-custom-reverse-managers.diff, 16.7 KB (added by , 13 years ago) |
---|
-
docs/ref/models/relations.txt
103 103 104 104 Just like ``remove()``, ``clear()`` is only available on 105 105 :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
9 9 from django.db.models.query import QuerySet 10 10 from django.db.models.query_utils import QueryWrapper 11 11 from django.db.models.deletion import CASCADE 12 from django.db.models.manager import Manager 12 13 from django.utils.encoding import smart_unicode 13 14 from django.utils.translation import ugettext_lazy as _, string_concat 14 15 from django.utils.functional import curry, cached_property … … 439 440 def related_manager_cls(self): 440 441 # Dynamically create a class that subclasses the related model's default 441 442 # 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): 443 446 rel_field = self.related.field 444 447 rel_model = self.related.model 445 448 attname = rel_field.rel.get_related_field().attname 446 449 450 def create_related_manager_cls(manager): 451 return self._related_manager_cls(superclass=manager.__class__, uses_default_manager=False) 452 447 453 class RelatedManager(superclass): 448 454 def __init__(self, instance): 449 455 super(RelatedManager, self).__init__() … … 471 477 False, 472 478 rel_field.related_query_name()) 473 479 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 474 492 def add(self, *objs): 475 493 for obj in objs: 476 494 if not isinstance(obj, self.model): … … 495 513 496 514 # remove() and clear() are only provided if the ForeignKey can have a value of null. 497 515 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 508 531 532 # This works also for explicit managers, in which case it does the expected thing. 509 533 def clear(self): 510 534 self.update(**{rel_field.name: None}) 511 535 clear.alters_data = True … … 513 537 return RelatedManager 514 538 515 539 516 def create_many_related_manager(superclass, rel ):540 def create_many_related_manager(superclass, rel, uses_default_manager=True): 517 541 """Creates a manager that subclasses 'superclass' (which is a Manager) 518 542 and adds behavior for many-to-many related objects.""" 519 543 class ManyRelatedManager(superclass): … … 570 594 False, 571 595 self.prefetch_cache_name) 572 596 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 573 612 # If the ManyToMany relation has an intermediary model, 574 613 # the add and remove methods do not exist. 575 614 if rel.through._meta.auto_created: … … 581 620 self._add_items(self.target_field_name, self.source_field_name, *objs) 582 621 add.alters_data = True 583 622 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) 586 631 587 # If this is a symmetrical m2m relation to self, remove the mirror entry in the m2m table588 if self.symmetrical:589 self._remove_items(self.target_field_name, self.source_field_name, *objs)590 remove.alters_data = True632 # 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 591 636 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) 594 642 595 # If this is a symmetrical m2m relation to self, clear the mirror entry in the m2m table596 if self.symmetrical:597 self._clear_items(self.target_field_name)598 clear.alters_data = True643 # 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 599 647 600 648 def create(self, **kwargs): 601 649 # This check needs to be done here, since we can't later remove this -
tests/modeltests/custom_managers/tests.py
2 2 3 3 from django.test import TestCase 4 4 5 from .models import Person, Book, Car, PersonManager, PublishedBookManager5 from .models import Person, Book, Manufacturer, Color, Car, PersonManager, PublishedBookManager 6 6 7 7 8 8 class CustomManagerTests(TestCase): … … 42 42 lambda b: b.title 43 43 ) 44 44 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") 47 48 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 48 64 self.assertQuerysetEqual( 49 65 Car.cars.order_by("name"), [ 50 66 "Corvette", 51 67 "Neon", 68 "Viper", 52 69 ], 53 70 lambda c: c.name 54 71 ) 55 72 56 73 self.assertQuerysetEqual( 57 Car.fast_cars. all(), [74 Car.fast_cars.order_by("name"), [ 58 75 "Corvette", 76 "Viper", 59 77 ], 60 78 lambda c: c.name 61 79 ) 62 80 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 63 115 # Each model class gets a "_default_manager" attribute, which is a 64 116 # reference to the first manager defined in the class. In this case, 65 117 # it's "cars". 66 67 118 self.assertQuerysetEqual( 68 119 Car._default_manager.order_by("name"), [ 69 120 "Corvette", 70 121 "Neon", 122 "Viper", 71 123 ], 72 124 lambda c: c.name 73 125 ) 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
44 44 45 45 # An example of providing multiple custom managers. 46 46 47 class Manufacturer(models.Model): 48 name = models.CharField(max_length=10) 49 country = models.CharField(max_length=20) 50 51 class FreeOfChargeColorManager(models.Manager): 52 def get_query_set(self): 53 return super(FreeOfChargeColorManager, self).get_query_set().filter(surcharge=False) 54 55 class 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 47 61 class FastCarManager(models.Manager): 48 62 def get_query_set(self): 49 63 return super(FastCarManager, self).get_query_set().filter(top_speed__gt=150) 50 64 51 65 class Car(models.Model): 52 66 name = models.CharField(max_length=10) 67 manufacturer = models.ForeignKey(Manufacturer, null=True) 68 available_colors = models.ManyToManyField(Color) 53 69 mileage = models.IntegerField() 54 70 top_speed = models.IntegerField(help_text="In miles per hour.") 55 71 cars = models.Manager()