1. """
    
  2. Testing signals emitted on changing m2m relations.
    
  3. """
    
  4. 
    
  5. from django.db import models
    
  6. from django.test import TestCase
    
  7. 
    
  8. from .models import Car, Part, Person, SportsCar
    
  9. 
    
  10. 
    
  11. class ManyToManySignalsTest(TestCase):
    
  12.     @classmethod
    
  13.     def setUpTestData(cls):
    
  14.         cls.vw = Car.objects.create(name="VW")
    
  15.         cls.bmw = Car.objects.create(name="BMW")
    
  16.         cls.toyota = Car.objects.create(name="Toyota")
    
  17. 
    
  18.         cls.wheelset = Part.objects.create(name="Wheelset")
    
  19.         cls.doors = Part.objects.create(name="Doors")
    
  20.         cls.engine = Part.objects.create(name="Engine")
    
  21.         cls.airbag = Part.objects.create(name="Airbag")
    
  22.         cls.sunroof = Part.objects.create(name="Sunroof")
    
  23. 
    
  24.         cls.alice = Person.objects.create(name="Alice")
    
  25.         cls.bob = Person.objects.create(name="Bob")
    
  26.         cls.chuck = Person.objects.create(name="Chuck")
    
  27.         cls.daisy = Person.objects.create(name="Daisy")
    
  28. 
    
  29.     def setUp(self):
    
  30.         self.m2m_changed_messages = []
    
  31. 
    
  32.     def m2m_changed_signal_receiver(self, signal, sender, **kwargs):
    
  33.         message = {
    
  34.             "instance": kwargs["instance"],
    
  35.             "action": kwargs["action"],
    
  36.             "reverse": kwargs["reverse"],
    
  37.             "model": kwargs["model"],
    
  38.         }
    
  39.         if kwargs["pk_set"]:
    
  40.             message["objects"] = list(
    
  41.                 kwargs["model"].objects.filter(pk__in=kwargs["pk_set"])
    
  42.             )
    
  43.         self.m2m_changed_messages.append(message)
    
  44. 
    
  45.     def tearDown(self):
    
  46.         # disconnect all signal handlers
    
  47.         models.signals.m2m_changed.disconnect(
    
  48.             self.m2m_changed_signal_receiver, Car.default_parts.through
    
  49.         )
    
  50.         models.signals.m2m_changed.disconnect(
    
  51.             self.m2m_changed_signal_receiver, Car.optional_parts.through
    
  52.         )
    
  53.         models.signals.m2m_changed.disconnect(
    
  54.             self.m2m_changed_signal_receiver, Person.fans.through
    
  55.         )
    
  56.         models.signals.m2m_changed.disconnect(
    
  57.             self.m2m_changed_signal_receiver, Person.friends.through
    
  58.         )
    
  59. 
    
  60.     def _initialize_signal_car(self, add_default_parts_before_set_signal=False):
    
  61.         """Install a listener on the two m2m relations."""
    
  62.         models.signals.m2m_changed.connect(
    
  63.             self.m2m_changed_signal_receiver, Car.optional_parts.through
    
  64.         )
    
  65.         if add_default_parts_before_set_signal:
    
  66.             # adding a default part to our car - no signal listener installed
    
  67.             self.vw.default_parts.add(self.sunroof)
    
  68.         models.signals.m2m_changed.connect(
    
  69.             self.m2m_changed_signal_receiver, Car.default_parts.through
    
  70.         )
    
  71. 
    
  72.     def test_pk_set_on_repeated_add_remove(self):
    
  73.         """
    
  74.         m2m_changed is always fired, even for repeated calls to the same
    
  75.         method, but the behavior of pk_sets differs by action.
    
  76. 
    
  77.         - For signals related to `add()`, only PKs that will actually be
    
  78.           inserted are sent.
    
  79.         - For `remove()` all PKs are sent, even if they will not affect the DB.
    
  80.         """
    
  81.         pk_sets_sent = []
    
  82. 
    
  83.         def handler(signal, sender, **kwargs):
    
  84.             if kwargs["action"] in ["pre_add", "pre_remove"]:
    
  85.                 pk_sets_sent.append(kwargs["pk_set"])
    
  86. 
    
  87.         models.signals.m2m_changed.connect(handler, Car.default_parts.through)
    
  88. 
    
  89.         self.vw.default_parts.add(self.wheelset)
    
  90.         self.vw.default_parts.add(self.wheelset)
    
  91. 
    
  92.         self.vw.default_parts.remove(self.wheelset)
    
  93.         self.vw.default_parts.remove(self.wheelset)
    
  94. 
    
  95.         expected_pk_sets = [
    
  96.             {self.wheelset.pk},
    
  97.             set(),
    
  98.             {self.wheelset.pk},
    
  99.             {self.wheelset.pk},
    
  100.         ]
    
  101.         self.assertEqual(pk_sets_sent, expected_pk_sets)
    
  102. 
    
  103.         models.signals.m2m_changed.disconnect(handler, Car.default_parts.through)
    
  104. 
    
  105.     def test_m2m_relations_add_remove_clear(self):
    
  106.         expected_messages = []
    
  107. 
    
  108.         self._initialize_signal_car(add_default_parts_before_set_signal=True)
    
  109. 
    
  110.         self.vw.default_parts.add(self.wheelset, self.doors, self.engine)
    
  111.         expected_messages.append(
    
  112.             {
    
  113.                 "instance": self.vw,
    
  114.                 "action": "pre_add",
    
  115.                 "reverse": False,
    
  116.                 "model": Part,
    
  117.                 "objects": [self.doors, self.engine, self.wheelset],
    
  118.             }
    
  119.         )
    
  120.         expected_messages.append(
    
  121.             {
    
  122.                 "instance": self.vw,
    
  123.                 "action": "post_add",
    
  124.                 "reverse": False,
    
  125.                 "model": Part,
    
  126.                 "objects": [self.doors, self.engine, self.wheelset],
    
  127.             }
    
  128.         )
    
  129.         self.assertEqual(self.m2m_changed_messages, expected_messages)
    
  130. 
    
  131.         # give the BMW and Toyota some doors as well
    
  132.         self.doors.car_set.add(self.bmw, self.toyota)
    
  133.         expected_messages.append(
    
  134.             {
    
  135.                 "instance": self.doors,
    
  136.                 "action": "pre_add",
    
  137.                 "reverse": True,
    
  138.                 "model": Car,
    
  139.                 "objects": [self.bmw, self.toyota],
    
  140.             }
    
  141.         )
    
  142.         expected_messages.append(
    
  143.             {
    
  144.                 "instance": self.doors,
    
  145.                 "action": "post_add",
    
  146.                 "reverse": True,
    
  147.                 "model": Car,
    
  148.                 "objects": [self.bmw, self.toyota],
    
  149.             }
    
  150.         )
    
  151.         self.assertEqual(self.m2m_changed_messages, expected_messages)
    
  152. 
    
  153.     def test_m2m_relations_signals_remove_relation(self):
    
  154.         self._initialize_signal_car()
    
  155.         # remove the engine from the self.vw and the airbag (which is not set
    
  156.         # but is returned)
    
  157.         self.vw.default_parts.remove(self.engine, self.airbag)
    
  158.         self.assertEqual(
    
  159.             self.m2m_changed_messages,
    
  160.             [
    
  161.                 {
    
  162.                     "instance": self.vw,
    
  163.                     "action": "pre_remove",
    
  164.                     "reverse": False,
    
  165.                     "model": Part,
    
  166.                     "objects": [self.airbag, self.engine],
    
  167.                 },
    
  168.                 {
    
  169.                     "instance": self.vw,
    
  170.                     "action": "post_remove",
    
  171.                     "reverse": False,
    
  172.                     "model": Part,
    
  173.                     "objects": [self.airbag, self.engine],
    
  174.                 },
    
  175.             ],
    
  176.         )
    
  177. 
    
  178.     def test_m2m_relations_signals_give_the_self_vw_some_optional_parts(self):
    
  179.         expected_messages = []
    
  180. 
    
  181.         self._initialize_signal_car()
    
  182. 
    
  183.         # give the self.vw some optional parts (second relation to same model)
    
  184.         self.vw.optional_parts.add(self.airbag, self.sunroof)
    
  185.         expected_messages.append(
    
  186.             {
    
  187.                 "instance": self.vw,
    
  188.                 "action": "pre_add",
    
  189.                 "reverse": False,
    
  190.                 "model": Part,
    
  191.                 "objects": [self.airbag, self.sunroof],
    
  192.             }
    
  193.         )
    
  194.         expected_messages.append(
    
  195.             {
    
  196.                 "instance": self.vw,
    
  197.                 "action": "post_add",
    
  198.                 "reverse": False,
    
  199.                 "model": Part,
    
  200.                 "objects": [self.airbag, self.sunroof],
    
  201.             }
    
  202.         )
    
  203.         self.assertEqual(self.m2m_changed_messages, expected_messages)
    
  204. 
    
  205.         # add airbag to all the cars (even though the self.vw already has one)
    
  206.         self.airbag.cars_optional.add(self.vw, self.bmw, self.toyota)
    
  207.         expected_messages.append(
    
  208.             {
    
  209.                 "instance": self.airbag,
    
  210.                 "action": "pre_add",
    
  211.                 "reverse": True,
    
  212.                 "model": Car,
    
  213.                 "objects": [self.bmw, self.toyota],
    
  214.             }
    
  215.         )
    
  216.         expected_messages.append(
    
  217.             {
    
  218.                 "instance": self.airbag,
    
  219.                 "action": "post_add",
    
  220.                 "reverse": True,
    
  221.                 "model": Car,
    
  222.                 "objects": [self.bmw, self.toyota],
    
  223.             }
    
  224.         )
    
  225.         self.assertEqual(self.m2m_changed_messages, expected_messages)
    
  226. 
    
  227.     def test_m2m_relations_signals_reverse_relation_with_custom_related_name(self):
    
  228.         self._initialize_signal_car()
    
  229.         # remove airbag from the self.vw (reverse relation with custom
    
  230.         # related_name)
    
  231.         self.airbag.cars_optional.remove(self.vw)
    
  232.         self.assertEqual(
    
  233.             self.m2m_changed_messages,
    
  234.             [
    
  235.                 {
    
  236.                     "instance": self.airbag,
    
  237.                     "action": "pre_remove",
    
  238.                     "reverse": True,
    
  239.                     "model": Car,
    
  240.                     "objects": [self.vw],
    
  241.                 },
    
  242.                 {
    
  243.                     "instance": self.airbag,
    
  244.                     "action": "post_remove",
    
  245.                     "reverse": True,
    
  246.                     "model": Car,
    
  247.                     "objects": [self.vw],
    
  248.                 },
    
  249.             ],
    
  250.         )
    
  251. 
    
  252.     def test_m2m_relations_signals_clear_all_parts_of_the_self_vw(self):
    
  253.         self._initialize_signal_car()
    
  254.         # clear all parts of the self.vw
    
  255.         self.vw.default_parts.clear()
    
  256.         self.assertEqual(
    
  257.             self.m2m_changed_messages,
    
  258.             [
    
  259.                 {
    
  260.                     "instance": self.vw,
    
  261.                     "action": "pre_clear",
    
  262.                     "reverse": False,
    
  263.                     "model": Part,
    
  264.                 },
    
  265.                 {
    
  266.                     "instance": self.vw,
    
  267.                     "action": "post_clear",
    
  268.                     "reverse": False,
    
  269.                     "model": Part,
    
  270.                 },
    
  271.             ],
    
  272.         )
    
  273. 
    
  274.     def test_m2m_relations_signals_all_the_doors_off_of_cars(self):
    
  275.         self._initialize_signal_car()
    
  276.         # take all the doors off of cars
    
  277.         self.doors.car_set.clear()
    
  278.         self.assertEqual(
    
  279.             self.m2m_changed_messages,
    
  280.             [
    
  281.                 {
    
  282.                     "instance": self.doors,
    
  283.                     "action": "pre_clear",
    
  284.                     "reverse": True,
    
  285.                     "model": Car,
    
  286.                 },
    
  287.                 {
    
  288.                     "instance": self.doors,
    
  289.                     "action": "post_clear",
    
  290.                     "reverse": True,
    
  291.                     "model": Car,
    
  292.                 },
    
  293.             ],
    
  294.         )
    
  295. 
    
  296.     def test_m2m_relations_signals_reverse_relation(self):
    
  297.         self._initialize_signal_car()
    
  298.         # take all the airbags off of cars (clear reverse relation with custom
    
  299.         # related_name)
    
  300.         self.airbag.cars_optional.clear()
    
  301.         self.assertEqual(
    
  302.             self.m2m_changed_messages,
    
  303.             [
    
  304.                 {
    
  305.                     "instance": self.airbag,
    
  306.                     "action": "pre_clear",
    
  307.                     "reverse": True,
    
  308.                     "model": Car,
    
  309.                 },
    
  310.                 {
    
  311.                     "instance": self.airbag,
    
  312.                     "action": "post_clear",
    
  313.                     "reverse": True,
    
  314.                     "model": Car,
    
  315.                 },
    
  316.             ],
    
  317.         )
    
  318. 
    
  319.     def test_m2m_relations_signals_alternative_ways(self):
    
  320.         expected_messages = []
    
  321. 
    
  322.         self._initialize_signal_car()
    
  323. 
    
  324.         # alternative ways of setting relation:
    
  325.         self.vw.default_parts.create(name="Windows")
    
  326.         p6 = Part.objects.get(name="Windows")
    
  327.         expected_messages.append(
    
  328.             {
    
  329.                 "instance": self.vw,
    
  330.                 "action": "pre_add",
    
  331.                 "reverse": False,
    
  332.                 "model": Part,
    
  333.                 "objects": [p6],
    
  334.             }
    
  335.         )
    
  336.         expected_messages.append(
    
  337.             {
    
  338.                 "instance": self.vw,
    
  339.                 "action": "post_add",
    
  340.                 "reverse": False,
    
  341.                 "model": Part,
    
  342.                 "objects": [p6],
    
  343.             }
    
  344.         )
    
  345.         self.assertEqual(self.m2m_changed_messages, expected_messages)
    
  346. 
    
  347.         # direct assignment clears the set first, then adds
    
  348.         self.vw.default_parts.set([self.wheelset, self.doors, self.engine])
    
  349.         expected_messages.append(
    
  350.             {
    
  351.                 "instance": self.vw,
    
  352.                 "action": "pre_remove",
    
  353.                 "reverse": False,
    
  354.                 "model": Part,
    
  355.                 "objects": [p6],
    
  356.             }
    
  357.         )
    
  358.         expected_messages.append(
    
  359.             {
    
  360.                 "instance": self.vw,
    
  361.                 "action": "post_remove",
    
  362.                 "reverse": False,
    
  363.                 "model": Part,
    
  364.                 "objects": [p6],
    
  365.             }
    
  366.         )
    
  367.         expected_messages.append(
    
  368.             {
    
  369.                 "instance": self.vw,
    
  370.                 "action": "pre_add",
    
  371.                 "reverse": False,
    
  372.                 "model": Part,
    
  373.                 "objects": [self.doors, self.engine, self.wheelset],
    
  374.             }
    
  375.         )
    
  376.         expected_messages.append(
    
  377.             {
    
  378.                 "instance": self.vw,
    
  379.                 "action": "post_add",
    
  380.                 "reverse": False,
    
  381.                 "model": Part,
    
  382.                 "objects": [self.doors, self.engine, self.wheelset],
    
  383.             }
    
  384.         )
    
  385.         self.assertEqual(self.m2m_changed_messages, expected_messages)
    
  386. 
    
  387.     def test_m2m_relations_signals_clearing_removing(self):
    
  388.         expected_messages = []
    
  389. 
    
  390.         self._initialize_signal_car(add_default_parts_before_set_signal=True)
    
  391. 
    
  392.         # set by clearing.
    
  393.         self.vw.default_parts.set([self.wheelset, self.doors, self.engine], clear=True)
    
  394.         expected_messages.append(
    
  395.             {
    
  396.                 "instance": self.vw,
    
  397.                 "action": "pre_clear",
    
  398.                 "reverse": False,
    
  399.                 "model": Part,
    
  400.             }
    
  401.         )
    
  402.         expected_messages.append(
    
  403.             {
    
  404.                 "instance": self.vw,
    
  405.                 "action": "post_clear",
    
  406.                 "reverse": False,
    
  407.                 "model": Part,
    
  408.             }
    
  409.         )
    
  410.         expected_messages.append(
    
  411.             {
    
  412.                 "instance": self.vw,
    
  413.                 "action": "pre_add",
    
  414.                 "reverse": False,
    
  415.                 "model": Part,
    
  416.                 "objects": [self.doors, self.engine, self.wheelset],
    
  417.             }
    
  418.         )
    
  419.         expected_messages.append(
    
  420.             {
    
  421.                 "instance": self.vw,
    
  422.                 "action": "post_add",
    
  423.                 "reverse": False,
    
  424.                 "model": Part,
    
  425.                 "objects": [self.doors, self.engine, self.wheelset],
    
  426.             }
    
  427.         )
    
  428.         self.assertEqual(self.m2m_changed_messages, expected_messages)
    
  429. 
    
  430.         # set by only removing what's necessary.
    
  431.         self.vw.default_parts.set([self.wheelset, self.doors], clear=False)
    
  432.         expected_messages.append(
    
  433.             {
    
  434.                 "instance": self.vw,
    
  435.                 "action": "pre_remove",
    
  436.                 "reverse": False,
    
  437.                 "model": Part,
    
  438.                 "objects": [self.engine],
    
  439.             }
    
  440.         )
    
  441.         expected_messages.append(
    
  442.             {
    
  443.                 "instance": self.vw,
    
  444.                 "action": "post_remove",
    
  445.                 "reverse": False,
    
  446.                 "model": Part,
    
  447.                 "objects": [self.engine],
    
  448.             }
    
  449.         )
    
  450.         self.assertEqual(self.m2m_changed_messages, expected_messages)
    
  451. 
    
  452.     def test_m2m_relations_signals_when_inheritance(self):
    
  453.         expected_messages = []
    
  454. 
    
  455.         self._initialize_signal_car(add_default_parts_before_set_signal=True)
    
  456. 
    
  457.         # Signals still work when model inheritance is involved
    
  458.         c4 = SportsCar.objects.create(name="Bugatti", price="1000000")
    
  459.         c4b = Car.objects.get(name="Bugatti")
    
  460.         c4.default_parts.set([self.doors])
    
  461.         expected_messages.append(
    
  462.             {
    
  463.                 "instance": c4,
    
  464.                 "action": "pre_add",
    
  465.                 "reverse": False,
    
  466.                 "model": Part,
    
  467.                 "objects": [self.doors],
    
  468.             }
    
  469.         )
    
  470.         expected_messages.append(
    
  471.             {
    
  472.                 "instance": c4,
    
  473.                 "action": "post_add",
    
  474.                 "reverse": False,
    
  475.                 "model": Part,
    
  476.                 "objects": [self.doors],
    
  477.             }
    
  478.         )
    
  479.         self.assertEqual(self.m2m_changed_messages, expected_messages)
    
  480. 
    
  481.         self.engine.car_set.add(c4)
    
  482.         expected_messages.append(
    
  483.             {
    
  484.                 "instance": self.engine,
    
  485.                 "action": "pre_add",
    
  486.                 "reverse": True,
    
  487.                 "model": Car,
    
  488.                 "objects": [c4b],
    
  489.             }
    
  490.         )
    
  491.         expected_messages.append(
    
  492.             {
    
  493.                 "instance": self.engine,
    
  494.                 "action": "post_add",
    
  495.                 "reverse": True,
    
  496.                 "model": Car,
    
  497.                 "objects": [c4b],
    
  498.             }
    
  499.         )
    
  500.         self.assertEqual(self.m2m_changed_messages, expected_messages)
    
  501. 
    
  502.     def _initialize_signal_person(self):
    
  503.         # Install a listener on the two m2m relations.
    
  504.         models.signals.m2m_changed.connect(
    
  505.             self.m2m_changed_signal_receiver, Person.fans.through
    
  506.         )
    
  507.         models.signals.m2m_changed.connect(
    
  508.             self.m2m_changed_signal_receiver, Person.friends.through
    
  509.         )
    
  510. 
    
  511.     def test_m2m_relations_with_self_add_friends(self):
    
  512.         self._initialize_signal_person()
    
  513.         self.alice.friends.set([self.bob, self.chuck])
    
  514.         self.assertEqual(
    
  515.             self.m2m_changed_messages,
    
  516.             [
    
  517.                 {
    
  518.                     "instance": self.alice,
    
  519.                     "action": "pre_add",
    
  520.                     "reverse": False,
    
  521.                     "model": Person,
    
  522.                     "objects": [self.bob, self.chuck],
    
  523.                 },
    
  524.                 {
    
  525.                     "instance": self.alice,
    
  526.                     "action": "post_add",
    
  527.                     "reverse": False,
    
  528.                     "model": Person,
    
  529.                     "objects": [self.bob, self.chuck],
    
  530.                 },
    
  531.             ],
    
  532.         )
    
  533. 
    
  534.     def test_m2m_relations_with_self_add_fan(self):
    
  535.         self._initialize_signal_person()
    
  536.         self.alice.fans.set([self.daisy])
    
  537.         self.assertEqual(
    
  538.             self.m2m_changed_messages,
    
  539.             [
    
  540.                 {
    
  541.                     "instance": self.alice,
    
  542.                     "action": "pre_add",
    
  543.                     "reverse": False,
    
  544.                     "model": Person,
    
  545.                     "objects": [self.daisy],
    
  546.                 },
    
  547.                 {
    
  548.                     "instance": self.alice,
    
  549.                     "action": "post_add",
    
  550.                     "reverse": False,
    
  551.                     "model": Person,
    
  552.                     "objects": [self.daisy],
    
  553.                 },
    
  554.             ],
    
  555.         )
    
  556. 
    
  557.     def test_m2m_relations_with_self_add_idols(self):
    
  558.         self._initialize_signal_person()
    
  559.         self.chuck.idols.set([self.alice, self.bob])
    
  560.         self.assertEqual(
    
  561.             self.m2m_changed_messages,
    
  562.             [
    
  563.                 {
    
  564.                     "instance": self.chuck,
    
  565.                     "action": "pre_add",
    
  566.                     "reverse": True,
    
  567.                     "model": Person,
    
  568.                     "objects": [self.alice, self.bob],
    
  569.                 },
    
  570.                 {
    
  571.                     "instance": self.chuck,
    
  572.                     "action": "post_add",
    
  573.                     "reverse": True,
    
  574.                     "model": Person,
    
  575.                     "objects": [self.alice, self.bob],
    
  576.                 },
    
  577.             ],
    
  578.         )