diff options
| author | Loic Bistuer <loic.bistuer@sixmedia.com> | 2013-09-27 06:35:53 +0700 |
|---|---|---|
| committer | Anssi Kääriäinen <akaariai@gmail.com> | 2013-11-27 19:44:18 +0200 |
| commit | 17c3997f6828e88e4646071a8187c1318b65597d (patch) | |
| tree | ccc0344dcebc39987ced53c850a5bb8bdb97814f /tests/custom_managers | |
| parent | 0b3c8fc85168bac7327e7c4372e92b52575547e9 (diff) | |
Fixed #21169 -- Reworked RelatedManager methods use default filtering
The `remove()` and `clear()` methods of the related managers created by
`ForeignKey`, `GenericForeignKey`, and `ManyToManyField` suffered from a
number of issues. Some operations ran multiple data modifying queries without
wrapping them in a transaction, and some operations didn't respect default
filtering when it was present (i.e. when the default manager on the related
model implemented a custom `get_queryset()`).
Fixing the issues introduced some backward incompatible changes:
- The implementation of `remove()` for `ForeignKey` related managers changed
from a series of `Model.save()` calls to a single `QuerySet.update()` call.
The change means that `pre_save` and `post_save` signals aren't called anymore.
- The `remove()` and `clear()` methods for `GenericForeignKey` related
managers now perform bulk delete so `Model.delete()` isn't called anymore.
- The `remove()` and `clear()` methods for `ManyToManyField` related
managers perform nested queries when filtering is involved, which may
or may not be an issue depending on the database and the data itself.
Refs. #3871, #21174.
Thanks Anssi Kääriäinen and Tim Graham for the reviews.
Diffstat (limited to 'tests/custom_managers')
| -rw-r--r-- | tests/custom_managers/models.py | 20 | ||||
| -rw-r--r-- | tests/custom_managers/tests.py | 318 |
2 files changed, 311 insertions, 27 deletions
diff --git a/tests/custom_managers/models.py b/tests/custom_managers/models.py index c6a99620e9..726ece7dab 100644 --- a/tests/custom_managers/models.py +++ b/tests/custom_managers/models.py @@ -102,16 +102,36 @@ class Person(models.Model): @python_2_unicode_compatible +class FunPerson(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + fun = models.BooleanField(default=True) + + favorite_book = models.ForeignKey('Book', null=True, related_name='fun_people_favorite_books') + favorite_thing_type = models.ForeignKey('contenttypes.ContentType', null=True) + favorite_thing_id = models.IntegerField(null=True) + favorite_thing = generic.GenericForeignKey('favorite_thing_type', 'favorite_thing_id') + + objects = FunPeopleManager() + + def __str__(self): + return "%s %s" % (self.first_name, self.last_name) + +@python_2_unicode_compatible class Book(models.Model): title = models.CharField(max_length=50) author = models.CharField(max_length=30) is_published = models.BooleanField(default=False) published_objects = PublishedBookManager() authors = models.ManyToManyField(Person, related_name='books') + fun_authors = models.ManyToManyField(FunPerson, related_name='books') favorite_things = generic.GenericRelation(Person, content_type_field='favorite_thing_type', object_id_field='favorite_thing_id') + fun_people_favorite_things = generic.GenericRelation(FunPerson, + content_type_field='favorite_thing_type', object_id_field='favorite_thing_id') + def __str__(self): return self.title diff --git a/tests/custom_managers/tests.py b/tests/custom_managers/tests.py index 021decc455..af36110eca 100644 --- a/tests/custom_managers/tests.py +++ b/tests/custom_managers/tests.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.test import TestCase from django.utils import six -from .models import Person, Book, Car, PersonManager, PublishedBookManager +from .models import Person, FunPerson, Book, Car, PersonManager, PublishedBookManager class CustomManagerTests(TestCase): @@ -12,10 +12,11 @@ class CustomManagerTests(TestCase): title="How to program", author="Rodney Dangerfield", is_published=True) self.b2 = Book.published_objects.create( title="How to be smart", author="Albert Einstein", is_published=False) - self.p1 = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) - self.p2 = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) def test_manager(self): + Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) + droopy = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) + # Test a custom `Manager` method. self.assertQuerysetEqual( Person.objects.get_fun_people(), [ @@ -66,7 +67,7 @@ class CustomManagerTests(TestCase): # The RelatedManager used on the 'books' descriptor extends the default # manager - self.assertIsInstance(self.p2.books, PublishedBookManager) + self.assertIsInstance(droopy.books, PublishedBookManager) # The default manager, "objects", doesn't exist, because a custom one # was provided. @@ -113,78 +114,341 @@ class CustomManagerTests(TestCase): lambda c: c.name ) - def test_related_manager_fk(self): - self.p1.favorite_book = self.b1 - self.p1.save() - self.p2.favorite_book = self.b1 - self.p2.save() + def test_fk_related_manager(self): + Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_book=self.b1) + Person.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_book=self.b1) + FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_book=self.b1) + FunPerson.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_book=self.b1) self.assertQuerysetEqual( self.b1.favorite_books.order_by('first_name').all(), [ "Bugs", "Droopy", ], - lambda c: c.first_name + lambda c: c.first_name, + ordered=False, + ) + self.assertQuerysetEqual( + self.b1.fun_people_favorite_books.all(), [ + "Bugs", + ], + lambda c: c.first_name, + ordered=False, ) self.assertQuerysetEqual( self.b1.favorite_books(manager='boring_people').all(), [ "Droopy", ], - lambda c: c.first_name + lambda c: c.first_name, + ordered=False, ) self.assertQuerysetEqual( self.b1.favorite_books(manager='fun_people').all(), [ "Bugs", ], - lambda c: c.first_name + lambda c: c.first_name, + ordered=False, ) - def test_related_manager_gfk(self): - self.p1.favorite_thing = self.b1 - self.p1.save() - self.p2.favorite_thing = self.b1 - self.p2.save() + def test_gfk_related_manager(self): + Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_thing=self.b1) + Person.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_thing=self.b1) + FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_thing=self.b1) + FunPerson.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_thing=self.b1) self.assertQuerysetEqual( - self.b1.favorite_things.order_by('first_name').all(), [ + self.b1.favorite_things.all(), [ "Bugs", "Droopy", ], - lambda c: c.first_name + lambda c: c.first_name, + ordered=False, + ) + self.assertQuerysetEqual( + self.b1.fun_people_favorite_things.all(), [ + "Bugs", + ], + lambda c: c.first_name, + ordered=False, ) self.assertQuerysetEqual( self.b1.favorite_things(manager='boring_people').all(), [ "Droopy", ], - lambda c: c.first_name + lambda c: c.first_name, + ordered=False, ) self.assertQuerysetEqual( self.b1.favorite_things(manager='fun_people').all(), [ "Bugs", ], - lambda c: c.first_name + lambda c: c.first_name, + ordered=False, ) - def test_related_manager_m2m(self): - self.b1.authors.add(self.p1) - self.b1.authors.add(self.p2) + def test_m2m_related_manager(self): + bugs = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) + self.b1.authors.add(bugs) + droopy = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) + self.b1.authors.add(droopy) + bugs = FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True) + self.b1.fun_authors.add(bugs) + droopy = FunPerson.objects.create(first_name="Droopy", last_name="Dog", fun=False) + self.b1.fun_authors.add(droopy) self.assertQuerysetEqual( self.b1.authors.order_by('first_name').all(), [ "Bugs", "Droopy", ], - lambda c: c.first_name + lambda c: c.first_name, + ordered=False, + ) + self.assertQuerysetEqual( + self.b1.fun_authors.order_by('first_name').all(), [ + "Bugs", + ], + lambda c: c.first_name, + ordered=False, ) self.assertQuerysetEqual( self.b1.authors(manager='boring_people').all(), [ "Droopy", ], - lambda c: c.first_name + lambda c: c.first_name, + ordered=False, ) self.assertQuerysetEqual( self.b1.authors(manager='fun_people').all(), [ "Bugs", ], - lambda c: c.first_name + lambda c: c.first_name, + ordered=False, + ) + + def test_removal_through_default_fk_related_manager(self): + bugs = FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_book=self.b1) + droopy = FunPerson.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_book=self.b1) + + self.b1.fun_people_favorite_books.remove(droopy) + self.assertQuerysetEqual( + FunPerson._base_manager.filter(favorite_book=self.b1), [ + "Bugs", + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + + self.b1.fun_people_favorite_books.remove(bugs) + self.assertQuerysetEqual( + FunPerson._base_manager.filter(favorite_book=self.b1), [ + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + bugs.favorite_book = self.b1 + bugs.save() + + self.b1.fun_people_favorite_books.clear() + self.assertQuerysetEqual( + FunPerson._base_manager.filter(favorite_book=self.b1), [ + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + + def test_removal_through_specified_fk_related_manager(self): + Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_book=self.b1) + droopy = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_book=self.b1) + + # Check that the fun manager DOESN'T remove boring people. + self.b1.favorite_books(manager='fun_people').remove(droopy) + self.assertQuerysetEqual( + self.b1.favorite_books(manager='boring_people').all(), [ + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + # Check that the boring manager DOES remove boring people. + self.b1.favorite_books(manager='boring_people').remove(droopy) + self.assertQuerysetEqual( + self.b1.favorite_books(manager='boring_people').all(), [ + ], + lambda c: c.first_name, + ordered=False, + ) + droopy.favorite_book = self.b1 + droopy.save() + + # Check that the fun manager ONLY clears fun people. + self.b1.favorite_books(manager='fun_people').clear() + self.assertQuerysetEqual( + self.b1.favorite_books(manager='boring_people').all(), [ + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + self.assertQuerysetEqual( + self.b1.favorite_books(manager='fun_people').all(), [ + ], + lambda c: c.first_name, + ordered=False, + ) + + def test_removal_through_default_gfk_related_manager(self): + bugs = FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_thing=self.b1) + droopy = FunPerson.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_thing=self.b1) + + self.b1.fun_people_favorite_things.remove(droopy) + self.assertQuerysetEqual( + FunPerson._base_manager.order_by('first_name').filter(favorite_thing_id=self.b1.pk), [ + "Bugs", + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + + self.b1.fun_people_favorite_things.remove(bugs) + self.assertQuerysetEqual( + FunPerson._base_manager.order_by('first_name').filter(favorite_thing_id=self.b1.pk), [ + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + bugs.favorite_book = self.b1 + bugs.save() + + self.b1.fun_people_favorite_things.clear() + self.assertQuerysetEqual( + FunPerson._base_manager.order_by('first_name').filter(favorite_thing_id=self.b1.pk), [ + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + + def test_removal_through_specified_gfk_related_manager(self): + Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True, favorite_thing=self.b1) + droopy = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False, favorite_thing=self.b1) + + # Check that the fun manager DOESN'T remove boring people. + self.b1.favorite_things(manager='fun_people').remove(droopy) + self.assertQuerysetEqual( + self.b1.favorite_things(manager='boring_people').all(), [ + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + + # Check that the boring manager DOES remove boring people. + self.b1.favorite_things(manager='boring_people').remove(droopy) + self.assertQuerysetEqual( + self.b1.favorite_things(manager='boring_people').all(), [ + ], + lambda c: c.first_name, + ordered=False, + ) + droopy.favorite_thing = self.b1 + droopy.save() + + # Check that the fun manager ONLY clears fun people. + self.b1.favorite_things(manager='fun_people').clear() + self.assertQuerysetEqual( + self.b1.favorite_things(manager='boring_people').all(), [ + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + self.assertQuerysetEqual( + self.b1.favorite_things(manager='fun_people').all(), [ + ], + lambda c: c.first_name, + ordered=False, + ) + + def test_removal_through_default_m2m_related_manager(self): + bugs = FunPerson.objects.create(first_name="Bugs", last_name="Bunny", fun=True) + self.b1.fun_authors.add(bugs) + droopy = FunPerson.objects.create(first_name="Droopy", last_name="Dog", fun=False) + self.b1.fun_authors.add(droopy) + + self.b1.fun_authors.remove(droopy) + self.assertQuerysetEqual( + self.b1.fun_authors.through._default_manager.all(), [ + "Bugs", + "Droopy", + ], + lambda c: c.funperson.first_name, + ordered=False, + ) + + self.b1.fun_authors.remove(bugs) + self.assertQuerysetEqual( + self.b1.fun_authors.through._default_manager.all(), [ + "Droopy", + ], + lambda c: c.funperson.first_name, + ordered=False, + ) + self.b1.fun_authors.add(bugs) + + self.b1.fun_authors.clear() + self.assertQuerysetEqual( + self.b1.fun_authors.through._default_manager.all(), [ + "Droopy", + ], + lambda c: c.funperson.first_name, + ordered=False, + ) + + def test_removal_through_specified_m2m_related_manager(self): + bugs = Person.objects.create(first_name="Bugs", last_name="Bunny", fun=True) + self.b1.authors.add(bugs) + droopy = Person.objects.create(first_name="Droopy", last_name="Dog", fun=False) + self.b1.authors.add(droopy) + + # Check that the fun manager DOESN'T remove boring people. + self.b1.authors(manager='fun_people').remove(droopy) + self.assertQuerysetEqual( + self.b1.authors(manager='boring_people').all(), [ + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + + # Check that the boring manager DOES remove boring people. + self.b1.authors(manager='boring_people').remove(droopy) + self.assertQuerysetEqual( + self.b1.authors(manager='boring_people').all(), [ + ], + lambda c: c.first_name, + ordered=False, + ) + self.b1.authors.add(droopy) + + + # Check that the fun manager ONLY clears fun people. + self.b1.authors(manager='fun_people').clear() + self.assertQuerysetEqual( + self.b1.authors(manager='boring_people').all(), [ + "Droopy", + ], + lambda c: c.first_name, + ordered=False, + ) + self.assertQuerysetEqual( + self.b1.authors(manager='fun_people').all(), [ + ], + lambda c: c.first_name, + ordered=False, ) |
