Commit 102d230f authored by Alex Gaynor's avatar Alex Gaynor
Browse files

Converted m2m_signals from doctests to unittests. Thanks to Gregor Müllegger...

Converted m2m_signals from doctests to unittests.  Thanks to Gregor Müllegger for the patch.  We have always been at war with doctests.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14548 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent 8a7a44ff
Loading
Loading
Loading
Loading
+1 −335
Original line number Diff line number Diff line
"""
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: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]
m2m_changed signal
instance: VW
action: post_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]

# 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: <class 'modeltests.m2m_signals.models.Car'>
objects: [<Car: BMW>, <Car: Toyota>]
m2m_changed signal
instance: Doors
action: post_add
reverse: True
model: <class 'modeltests.m2m_signals.models.Car'>
objects: [<Car: BMW>, <Car: Toyota>]

# 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: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Airbag>, <Part: Engine>]
m2m_changed signal
instance: VW
action: post_remove
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Airbag>, <Part: Engine>]

# 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: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Airbag>, <Part: Sunroof>]
m2m_changed signal
instance: VW
action: post_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Airbag>, <Part: Sunroof>]

# 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: <class 'modeltests.m2m_signals.models.Car'>
objects: [<Car: BMW>, <Car: Toyota>]
m2m_changed signal
instance: Airbag
action: post_add
reverse: True
model: <class 'modeltests.m2m_signals.models.Car'>
objects: [<Car: BMW>, <Car: Toyota>]

# 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: <class 'modeltests.m2m_signals.models.Car'>
objects: [<Car: VW>]
m2m_changed signal
instance: Airbag
action: post_remove
reverse: True
model: <class 'modeltests.m2m_signals.models.Car'>
objects: [<Car: VW>]

# clear all parts of the VW
>>> c1.default_parts.clear()
m2m_changed signal
instance: VW
action: pre_clear
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
m2m_changed signal
instance: VW
action: post_clear
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>

# take all the doors off of cars
>>> p2.car_set.clear()
m2m_changed signal
instance: Doors
action: pre_clear
reverse: True
model: <class 'modeltests.m2m_signals.models.Car'>
m2m_changed signal
instance: Doors
action: post_clear
reverse: True
model: <class 'modeltests.m2m_signals.models.Car'>

# 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: <class 'modeltests.m2m_signals.models.Car'>
m2m_changed signal
instance: Airbag
action: post_clear
reverse: True
model: <class 'modeltests.m2m_signals.models.Car'>

# alternative ways of setting relation:

>>> c1.default_parts.create(name='Windows')
m2m_changed signal
instance: VW
action: pre_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Windows>]
m2m_changed signal
instance: VW
action: post_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Windows>]
<Part: Windows>

# 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: <class 'modeltests.m2m_signals.models.Part'>
m2m_changed signal
instance: VW
action: post_clear
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
m2m_changed signal
instance: VW
action: pre_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]
m2m_changed signal
instance: VW
action: post_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Doors>, <Part: Engine>, <Part: Wheelset>]

# 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: <class 'modeltests.m2m_signals.models.Part'>
m2m_changed signal
instance: Bugatti
action: post_clear
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
m2m_changed signal
instance: Bugatti
action: pre_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Doors>]
m2m_changed signal
instance: Bugatti
action: post_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Part'>
objects: [<Part: Doors>]

>>> p3.car_set.add(c4)
m2m_changed signal
instance: Engine
action: pre_add
reverse: True
model: <class 'modeltests.m2m_signals.models.Car'>
objects: [<Car: Bugatti>]
m2m_changed signal
instance: Engine
action: post_add
reverse: True
model: <class 'modeltests.m2m_signals.models.Car'>
objects: [<Car: Bugatti>]

# 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: <class 'modeltests.m2m_signals.models.Person'>
m2m_changed signal
instance: Alice
action: post_clear
reverse: False
model: <class 'modeltests.m2m_signals.models.Person'>
m2m_changed signal
instance: Alice
action: pre_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Person'>
objects: [<Person: Bob>, <Person: Chuck>]
m2m_changed signal
instance: Alice
action: post_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Person'>
objects: [<Person: Bob>, <Person: Chuck>]

>>> p1.fans = [p4]
m2m_changed signal
instance: Alice
action: pre_clear
reverse: False
model: <class 'modeltests.m2m_signals.models.Person'>
m2m_changed signal
instance: Alice
action: post_clear
reverse: False
model: <class 'modeltests.m2m_signals.models.Person'>
m2m_changed signal
instance: Alice
action: pre_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Person'>
objects: [<Person: Daisy>]
m2m_changed signal
instance: Alice
action: post_add
reverse: False
model: <class 'modeltests.m2m_signals.models.Person'>
objects: [<Person: Daisy>]

>>> p3.idols = [p1,p2]
m2m_changed signal
instance: Chuck
action: pre_clear
reverse: True
model: <class 'modeltests.m2m_signals.models.Person'>
m2m_changed signal
instance: Chuck
action: post_clear
reverse: True
model: <class 'modeltests.m2m_signals.models.Person'>
m2m_changed signal
instance: Chuck
action: pre_add
reverse: True
model: <class 'modeltests.m2m_signals.models.Person'>
objects: [<Person: Alice>, <Person: Bob>]
m2m_changed signal
instance: Chuck
action: post_add
reverse: True
model: <class 'modeltests.m2m_signals.models.Person'>
objects: [<Person: Alice>, <Person: Bob>]

# 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)

"""}
+427 −0
Original line number Diff line number Diff line
"""
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)