From 102d230f94e2fc4e5bbe2d6b3a013b2f8bcd8415 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 12 Nov 2010 19:39:22 +0000 Subject: [PATCH] =?UTF-8?q?Converted=20m2m=5Fsignals=20from=20doctests=20t?= =?UTF-8?q?o=20unittests.=20=20Thanks=20to=20Gregor=20M=C3=BCllegger=20for?= =?UTF-8?q?=20the=20patch.=20=20We=20have=20always=20been=20at=20war=20wit?= =?UTF-8?q?h=20doctests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git-svn-id: http://code.djangoproject.com/svn/django/trunk@14548 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- tests/modeltests/m2m_signals/models.py | 336 +------------------ tests/modeltests/m2m_signals/tests.py | 427 +++++++++++++++++++++++++ 2 files changed, 428 insertions(+), 335 deletions(-) create mode 100644 tests/modeltests/m2m_signals/tests.py diff --git a/tests/modeltests/m2m_signals/models.py b/tests/modeltests/m2m_signals/models.py index c76cde46fc..526c4a782e 100644 --- a/tests/modeltests/m2m_signals/models.py +++ b/tests/modeltests/m2m_signals/models.py @@ -1,9 +1,6 @@ -""" -Testing signals emitted on changing m2m relations. -""" - from django.db import models + class Part(models.Model): name = models.CharField(max_length=20) @@ -37,334 +34,3 @@ class Person(models.Model): def __unicode__(self): return self.name - -def m2m_changed_test(signal, sender, **kwargs): - print 'm2m_changed signal' - print 'instance:', kwargs['instance'] - print 'action:', kwargs['action'] - print 'reverse:', kwargs['reverse'] - print 'model:', kwargs['model'] - if kwargs['pk_set']: - print 'objects:',kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) - - -__test__ = {'API_TESTS':""" -# Install a listener on one of the two m2m relations. ->>> models.signals.m2m_changed.connect(m2m_changed_test, Car.optional_parts.through) - -# Test the add, remove and clear methods on both sides of the -# many-to-many relation - ->>> c1 = Car.objects.create(name='VW') ->>> c2 = Car.objects.create(name='BMW') ->>> c3 = Car.objects.create(name='Toyota') ->>> p1 = Part.objects.create(name='Wheelset') ->>> p2 = Part.objects.create(name='Doors') ->>> p3 = Part.objects.create(name='Engine') ->>> p4 = Part.objects.create(name='Airbag') ->>> p5 = Part.objects.create(name='Sunroof') - -# adding a default part to our car - no signal listener installed ->>> c1.default_parts.add(p5) - -# Now install a listener ->>> models.signals.m2m_changed.connect(m2m_changed_test, Car.default_parts.through) - ->>> c1.default_parts.add(p1, p2, p3) -m2m_changed signal -instance: VW -action: pre_add -reverse: False -model: -objects: [, , ] -m2m_changed signal -instance: VW -action: post_add -reverse: False -model: -objects: [, , ] - -# give the BMW and Toyata some doors as well ->>> p2.car_set.add(c2, c3) -m2m_changed signal -instance: Doors -action: pre_add -reverse: True -model: -objects: [, ] -m2m_changed signal -instance: Doors -action: post_add -reverse: True -model: -objects: [, ] - -# remove the engine from the VW and the airbag (which is not set but is returned) ->>> c1.default_parts.remove(p3, p4) -m2m_changed signal -instance: VW -action: pre_remove -reverse: False -model: -objects: [, ] -m2m_changed signal -instance: VW -action: post_remove -reverse: False -model: -objects: [, ] - -# give the VW some optional parts (second relation to same model) ->>> c1.optional_parts.add(p4,p5) -m2m_changed signal -instance: VW -action: pre_add -reverse: False -model: -objects: [, ] -m2m_changed signal -instance: VW -action: post_add -reverse: False -model: -objects: [, ] - -# add airbag to all the cars (even though the VW already has one) ->>> p4.cars_optional.add(c1, c2, c3) -m2m_changed signal -instance: Airbag -action: pre_add -reverse: True -model: -objects: [, ] -m2m_changed signal -instance: Airbag -action: post_add -reverse: True -model: -objects: [, ] - -# remove airbag from the VW (reverse relation with custom related_name) ->>> p4.cars_optional.remove(c1) -m2m_changed signal -instance: Airbag -action: pre_remove -reverse: True -model: -objects: [] -m2m_changed signal -instance: Airbag -action: post_remove -reverse: True -model: -objects: [] - -# clear all parts of the VW ->>> c1.default_parts.clear() -m2m_changed signal -instance: VW -action: pre_clear -reverse: False -model: -m2m_changed signal -instance: VW -action: post_clear -reverse: False -model: - -# take all the doors off of cars ->>> p2.car_set.clear() -m2m_changed signal -instance: Doors -action: pre_clear -reverse: True -model: -m2m_changed signal -instance: Doors -action: post_clear -reverse: True -model: - -# take all the airbags off of cars (clear reverse relation with custom related_name) ->>> p4.cars_optional.clear() -m2m_changed signal -instance: Airbag -action: pre_clear -reverse: True -model: -m2m_changed signal -instance: Airbag -action: post_clear -reverse: True -model: - -# alternative ways of setting relation: - ->>> c1.default_parts.create(name='Windows') -m2m_changed signal -instance: VW -action: pre_add -reverse: False -model: -objects: [] -m2m_changed signal -instance: VW -action: post_add -reverse: False -model: -objects: [] - - -# direct assignment clears the set first, then adds ->>> c1.default_parts = [p1,p2,p3] -m2m_changed signal -instance: VW -action: pre_clear -reverse: False -model: -m2m_changed signal -instance: VW -action: post_clear -reverse: False -model: -m2m_changed signal -instance: VW -action: pre_add -reverse: False -model: -objects: [, , ] -m2m_changed signal -instance: VW -action: post_add -reverse: False -model: -objects: [, , ] - -# Check that signals still work when model inheritance is involved ->>> c4 = SportsCar.objects.create(name='Bugatti', price='1000000') ->>> c4.default_parts = [p2] -m2m_changed signal -instance: Bugatti -action: pre_clear -reverse: False -model: -m2m_changed signal -instance: Bugatti -action: post_clear -reverse: False -model: -m2m_changed signal -instance: Bugatti -action: pre_add -reverse: False -model: -objects: [] -m2m_changed signal -instance: Bugatti -action: post_add -reverse: False -model: -objects: [] - ->>> p3.car_set.add(c4) -m2m_changed signal -instance: Engine -action: pre_add -reverse: True -model: -objects: [] -m2m_changed signal -instance: Engine -action: post_add -reverse: True -model: -objects: [] - -# Now test m2m relations with self ->>> p1 = Person.objects.create(name='Alice') ->>> p2 = Person.objects.create(name='Bob') ->>> p3 = Person.objects.create(name='Chuck') ->>> p4 = Person.objects.create(name='Daisy') - ->>> models.signals.m2m_changed.connect(m2m_changed_test, Person.fans.through) ->>> models.signals.m2m_changed.connect(m2m_changed_test, Person.friends.through) - ->>> p1.friends = [p2, p3] -m2m_changed signal -instance: Alice -action: pre_clear -reverse: False -model: -m2m_changed signal -instance: Alice -action: post_clear -reverse: False -model: -m2m_changed signal -instance: Alice -action: pre_add -reverse: False -model: -objects: [, ] -m2m_changed signal -instance: Alice -action: post_add -reverse: False -model: -objects: [, ] - ->>> p1.fans = [p4] -m2m_changed signal -instance: Alice -action: pre_clear -reverse: False -model: -m2m_changed signal -instance: Alice -action: post_clear -reverse: False -model: -m2m_changed signal -instance: Alice -action: pre_add -reverse: False -model: -objects: [] -m2m_changed signal -instance: Alice -action: post_add -reverse: False -model: -objects: [] - ->>> p3.idols = [p1,p2] -m2m_changed signal -instance: Chuck -action: pre_clear -reverse: True -model: -m2m_changed signal -instance: Chuck -action: post_clear -reverse: True -model: -m2m_changed signal -instance: Chuck -action: pre_add -reverse: True -model: -objects: [, ] -m2m_changed signal -instance: Chuck -action: post_add -reverse: True -model: -objects: [, ] - -# Cleanup - disconnect all signal handlers ->>> models.signals.m2m_changed.disconnect(m2m_changed_test, Car.default_parts.through) ->>> models.signals.m2m_changed.disconnect(m2m_changed_test, Car.optional_parts.through) ->>> models.signals.m2m_changed.disconnect(m2m_changed_test, Person.fans.through) ->>> models.signals.m2m_changed.disconnect(m2m_changed_test, Person.friends.through) - -"""} diff --git a/tests/modeltests/m2m_signals/tests.py b/tests/modeltests/m2m_signals/tests.py new file mode 100644 index 0000000000..9e9158f571 --- /dev/null +++ b/tests/modeltests/m2m_signals/tests.py @@ -0,0 +1,427 @@ +""" +Testing signals emitted on changing m2m relations. +""" + +from django.db import models +from django.test import TestCase + +from models import Part, Car, SportsCar, Person + + +class ManyToManySignalsTest(TestCase): + def m2m_changed_signal_receiver(self, signal, sender, **kwargs): + message = { + 'instance': kwargs['instance'], + 'action': kwargs['action'], + 'reverse': kwargs['reverse'], + 'model': kwargs['model'], + } + if kwargs['pk_set']: + message['objects'] = list( + kwargs['model'].objects.filter(pk__in=kwargs['pk_set']) + ) + self.m2m_changed_messages.append(message) + + def setUp(self): + self.m2m_changed_messages = [] + + self.vw = Car.objects.create(name='VW') + self.bmw = Car.objects.create(name='BMW') + self.toyota = Car.objects.create(name='Toyota') + self.wheelset = Part.objects.create(name='Wheelset') + self.doors = Part.objects.create(name='Doors') + self.engine = Part.objects.create(name='Engine') + self.airbag = Part.objects.create(name='Airbag') + self.sunroof = Part.objects.create(name='Sunroof') + + self.alice = Person.objects.create(name='Alice') + self.bob = Person.objects.create(name='Bob') + self.chuck = Person.objects.create(name='Chuck') + self.daisy = Person.objects.create(name='Daisy') + + def tearDown(self): + # disconnect all signal handlers + models.signals.m2m_changed.disconnect( + self.m2m_changed_signal_receiver, Car.default_parts.through + ) + models.signals.m2m_changed.disconnect( + self.m2m_changed_signal_receiver, Car.optional_parts.through + ) + models.signals.m2m_changed.disconnect( + self.m2m_changed_signal_receiver, Person.fans.through + ) + models.signals.m2m_changed.disconnect( + self.m2m_changed_signal_receiver, Person.friends.through + ) + + def test_m2m_relations_add_remove_clear(self): + expected_messages = [] + + # Install a listener on one of the two m2m relations. + models.signals.m2m_changed.connect( + self.m2m_changed_signal_receiver, Car.optional_parts.through + ) + + # Test the add, remove and clear methods on both sides of the + # many-to-many relation + + # adding a default part to our car - no signal listener installed + self.vw.default_parts.add(self.sunroof) + + # Now install a listener + models.signals.m2m_changed.connect( + self.m2m_changed_signal_receiver, Car.default_parts.through + ) + + self.vw.default_parts.add(self.wheelset, self.doors, self.engine) + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors, self.engine, self.wheelset], + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors, self.engine, self.wheelset], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # give the BMW and Toyata some doors as well + self.doors.car_set.add(self.bmw, self.toyota) + expected_messages.append({ + 'instance': self.doors, + 'action': 'pre_add', + 'reverse': True, + 'model': Car, + 'objects': [self.bmw, self.toyota], + }) + expected_messages.append({ + 'instance': self.doors, + 'action': 'post_add', + 'reverse': True, + 'model': Car, + 'objects': [self.bmw, self.toyota], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # remove the engine from the self.vw and the airbag (which is not set + # but is returned) + self.vw.default_parts.remove(self.engine, self.airbag) + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_remove', + 'reverse': False, + 'model': Part, + 'objects': [self.airbag, self.engine], + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_remove', + 'reverse': False, + 'model': Part, + 'objects': [self.airbag, self.engine], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # give the self.vw some optional parts (second relation to same model) + self.vw.optional_parts.add(self.airbag, self.sunroof) + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_add', + 'reverse': False, + 'model': Part, + 'objects': [self.airbag, self.sunroof], + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_add', + 'reverse': False, + 'model': Part, + 'objects': [self.airbag, self.sunroof], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # add airbag to all the cars (even though the self.vw already has one) + self.airbag.cars_optional.add(self.vw, self.bmw, self.toyota) + expected_messages.append({ + 'instance': self.airbag, + 'action': 'pre_add', + 'reverse': True, + 'model': Car, + 'objects': [self.bmw, self.toyota], + }) + expected_messages.append({ + 'instance': self.airbag, + 'action': 'post_add', + 'reverse': True, + 'model': Car, + 'objects': [self.bmw, self.toyota], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # remove airbag from the self.vw (reverse relation with custom + # related_name) + self.airbag.cars_optional.remove(self.vw) + expected_messages.append({ + 'instance': self.airbag, + 'action': 'pre_remove', + 'reverse': True, + 'model': Car, + 'objects': [self.vw], + }) + expected_messages.append({ + 'instance': self.airbag, + 'action': 'post_remove', + 'reverse': True, + 'model': Car, + 'objects': [self.vw], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # clear all parts of the self.vw + self.vw.default_parts.clear() + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_clear', + 'reverse': False, + 'model': Part, + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_clear', + 'reverse': False, + 'model': Part, + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # take all the doors off of cars + self.doors.car_set.clear() + expected_messages.append({ + 'instance': self.doors, + 'action': 'pre_clear', + 'reverse': True, + 'model': Car, + }) + expected_messages.append({ + 'instance': self.doors, + 'action': 'post_clear', + 'reverse': True, + 'model': Car, + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # take all the airbags off of cars (clear reverse relation with custom + # related_name) + self.airbag.cars_optional.clear() + expected_messages.append({ + 'instance': self.airbag, + 'action': 'pre_clear', + 'reverse': True, + 'model': Car, + }) + expected_messages.append({ + 'instance': self.airbag, + 'action': 'post_clear', + 'reverse': True, + 'model': Car, + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # alternative ways of setting relation: + self.vw.default_parts.create(name='Windows') + p6 = Part.objects.get(name='Windows') + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_add', + 'reverse': False, + 'model': Part, + 'objects': [p6], + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_add', + 'reverse': False, + 'model': Part, + 'objects': [p6], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # direct assignment clears the set first, then adds + self.vw.default_parts = [self.wheelset,self.doors,self.engine] + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_clear', + 'reverse': False, + 'model': Part, + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_clear', + 'reverse': False, + 'model': Part, + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'pre_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors, self.engine, self.wheelset], + }) + expected_messages.append({ + 'instance': self.vw, + 'action': 'post_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors, self.engine, self.wheelset], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + # Check that signals still work when model inheritance is involved + c4 = SportsCar.objects.create(name='Bugatti', price='1000000') + c4b = Car.objects.get(name='Bugatti') + c4.default_parts = [self.doors] + expected_messages.append({ + 'instance': c4, + 'action': 'pre_clear', + 'reverse': False, + 'model': Part, + }) + expected_messages.append({ + 'instance': c4, + 'action': 'post_clear', + 'reverse': False, + 'model': Part, + }) + expected_messages.append({ + 'instance': c4, + 'action': 'pre_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors], + }) + expected_messages.append({ + 'instance': c4, + 'action': 'post_add', + 'reverse': False, + 'model': Part, + 'objects': [self.doors], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + self.engine.car_set.add(c4) + expected_messages.append({ + 'instance': self.engine, + 'action': 'pre_add', + 'reverse': True, + 'model': Car, + 'objects': [c4b], + }) + expected_messages.append({ + 'instance': self.engine, + 'action': 'post_add', + 'reverse': True, + 'model': Car, + 'objects': [c4b], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + def test_m2m_relations_with_self(self): + expected_messages = [] + + models.signals.m2m_changed.connect( + self.m2m_changed_signal_receiver, Person.fans.through + ) + models.signals.m2m_changed.connect( + self.m2m_changed_signal_receiver, Person.friends.through + ) + + self.alice.friends = [self.bob, self.chuck] + expected_messages.append({ + 'instance': self.alice, + 'action': 'pre_clear', + 'reverse': False, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'post_clear', + 'reverse': False, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'pre_add', + 'reverse': False, + 'model': Person, + 'objects': [self.bob, self.chuck], + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'post_add', + 'reverse': False, + 'model': Person, + 'objects': [self.bob, self.chuck], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + self.alice.fans = [self.daisy] + expected_messages.append({ + 'instance': self.alice, + 'action': 'pre_clear', + 'reverse': False, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'post_clear', + 'reverse': False, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'pre_add', + 'reverse': False, + 'model': Person, + 'objects': [self.daisy], + }) + expected_messages.append({ + 'instance': self.alice, + 'action': 'post_add', + 'reverse': False, + 'model': Person, + 'objects': [self.daisy], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages) + + self.chuck.idols = [self.alice,self.bob] + expected_messages.append({ + 'instance': self.chuck, + 'action': 'pre_clear', + 'reverse': True, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.chuck, + 'action': 'post_clear', + 'reverse': True, + 'model': Person, + }) + expected_messages.append({ + 'instance': self.chuck, + 'action': 'pre_add', + 'reverse': True, + 'model': Person, + 'objects': [self.alice, self.bob], + }) + expected_messages.append({ + 'instance': self.chuck, + 'action': 'post_add', + 'reverse': True, + 'model': Person, + 'objects': [self.alice, self.bob], + }) + self.assertEqual(self.m2m_changed_messages, expected_messages)