123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- """
- Testing signals emitted on changing m2m relations.
- """
- from django.db import models
- from django.test import TestCase
- from .models import Car, Part, Person, SportsCar
- 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 _initialize_signal_car(self, add_default_parts_before_set_signal=False):
- """ Install a listener on the two m2m relations. """
- models.signals.m2m_changed.connect(
- self.m2m_changed_signal_receiver, Car.optional_parts.through
- )
- if add_default_parts_before_set_signal:
- # adding a default part to our car - no signal listener installed
- self.vw.default_parts.add(self.sunroof)
- models.signals.m2m_changed.connect(
- self.m2m_changed_signal_receiver, Car.default_parts.through
- )
- def test_m2m_relations_add_remove_clear(self):
- expected_messages = []
- self._initialize_signal_car(add_default_parts_before_set_signal=True)
- 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 Toyota 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)
- def test_m2m_relations_signals_remove_relation(self):
- self._initialize_signal_car()
- # 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)
- self.assertEqual(self.m2m_changed_messages, [
- {
- 'instance': self.vw,
- 'action': 'pre_remove',
- 'reverse': False,
- 'model': Part,
- 'objects': [self.airbag, self.engine],
- }, {
- 'instance': self.vw,
- 'action': 'post_remove',
- 'reverse': False,
- 'model': Part,
- 'objects': [self.airbag, self.engine],
- }
- ])
- def test_m2m_relations_signals_give_the_self_vw_some_optional_parts(self):
- expected_messages = []
- self._initialize_signal_car()
- # 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)
- def test_m2m_relations_signals_reverse_relation_with_custom_related_name(self):
- self._initialize_signal_car()
- # remove airbag from the self.vw (reverse relation with custom
- # related_name)
- self.airbag.cars_optional.remove(self.vw)
- self.assertEqual(self.m2m_changed_messages, [
- {
- 'instance': self.airbag,
- 'action': 'pre_remove',
- 'reverse': True,
- 'model': Car,
- 'objects': [self.vw],
- }, {
- 'instance': self.airbag,
- 'action': 'post_remove',
- 'reverse': True,
- 'model': Car,
- 'objects': [self.vw],
- }
- ])
- def test_m2m_relations_signals_clear_all_parts_of_the_self_vw(self):
- self._initialize_signal_car()
- # clear all parts of the self.vw
- self.vw.default_parts.clear()
- self.assertEqual(self.m2m_changed_messages, [
- {
- 'instance': self.vw,
- 'action': 'pre_clear',
- 'reverse': False,
- 'model': Part,
- }, {
- 'instance': self.vw,
- 'action': 'post_clear',
- 'reverse': False,
- 'model': Part,
- }
- ])
- def test_m2m_relations_signals_all_the_doors_off_of_cars(self):
- self._initialize_signal_car()
- # take all the doors off of cars
- self.doors.car_set.clear()
- self.assertEqual(self.m2m_changed_messages, [
- {
- 'instance': self.doors,
- 'action': 'pre_clear',
- 'reverse': True,
- 'model': Car,
- }, {
- 'instance': self.doors,
- 'action': 'post_clear',
- 'reverse': True,
- 'model': Car,
- }
- ])
- def test_m2m_relations_signals_reverse_relation(self):
- self._initialize_signal_car()
- # take all the airbags off of cars (clear reverse relation with custom
- # related_name)
- self.airbag.cars_optional.clear()
- self.assertEqual(self.m2m_changed_messages, [
- {
- 'instance': self.airbag,
- 'action': 'pre_clear',
- 'reverse': True,
- 'model': Car,
- }, {
- 'instance': self.airbag,
- 'action': 'post_clear',
- 'reverse': True,
- 'model': Car,
- }
- ])
- def test_m2m_relations_signals_alternative_ways(self):
- expected_messages = []
- self._initialize_signal_car()
- # 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.set([self.wheelset, self.doors, self.engine])
- expected_messages.append({
- 'instance': self.vw,
- 'action': 'pre_remove',
- 'reverse': False,
- 'model': Part,
- 'objects': [p6],
- })
- expected_messages.append({
- 'instance': self.vw,
- 'action': 'post_remove',
- 'reverse': False,
- 'model': Part,
- 'objects': [p6],
- })
- 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)
- def test_m2m_relations_signals_clearing_removing(self):
- expected_messages = []
- self._initialize_signal_car(add_default_parts_before_set_signal=True)
- # set by clearing.
- self.vw.default_parts.set([self.wheelset, self.doors, self.engine], clear=True)
- 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)
- # set by only removing what's necessary.
- self.vw.default_parts.set([self.wheelset, self.doors], clear=False)
- expected_messages.append({
- 'instance': self.vw,
- 'action': 'pre_remove',
- 'reverse': False,
- 'model': Part,
- 'objects': [self.engine],
- })
- expected_messages.append({
- 'instance': self.vw,
- 'action': 'post_remove',
- 'reverse': False,
- 'model': Part,
- 'objects': [self.engine],
- })
- self.assertEqual(self.m2m_changed_messages, expected_messages)
- def test_m2m_relations_signals_when_inheritance(self):
- expected_messages = []
- self._initialize_signal_car(add_default_parts_before_set_signal=True)
- # 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.set([self.doors])
- 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 _initialize_signal_person(self):
- # Install a listener on the two m2m relations.
- 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
- )
- def test_m2m_relations_with_self_add_friends(self):
- self._initialize_signal_person()
- self.alice.friends.set([self.bob, self.chuck])
- self.assertEqual(self.m2m_changed_messages, [
- {
- 'instance': self.alice,
- 'action': 'pre_add',
- 'reverse': False,
- 'model': Person,
- 'objects': [self.bob, self.chuck],
- }, {
- 'instance': self.alice,
- 'action': 'post_add',
- 'reverse': False,
- 'model': Person,
- 'objects': [self.bob, self.chuck],
- }
- ])
- def test_m2m_relations_with_self_add_fan(self):
- self._initialize_signal_person()
- self.alice.fans.set([self.daisy])
- self.assertEqual(self.m2m_changed_messages, [
- {
- 'instance': self.alice,
- 'action': 'pre_add',
- 'reverse': False,
- 'model': Person,
- 'objects': [self.daisy],
- }, {
- 'instance': self.alice,
- 'action': 'post_add',
- 'reverse': False,
- 'model': Person,
- 'objects': [self.daisy],
- }
- ])
- def test_m2m_relations_with_self_add_idols(self):
- self._initialize_signal_person()
- self.chuck.idols.set([self.alice, self.bob])
- self.assertEqual(self.m2m_changed_messages, [
- {
- 'instance': self.chuck,
- 'action': 'pre_add',
- 'reverse': True,
- 'model': Person,
- 'objects': [self.alice, self.bob],
- }, {
- 'instance': self.chuck,
- 'action': 'post_add',
- 'reverse': True,
- 'model': Person,
- 'objects': [self.alice, self.bob],
- }
- ])
|