1. from io import StringIO
    
  2. 
    
  3. from django.contrib.auth.models import User
    
  4. from django.core import management
    
  5. from django.test import TestCase
    
  6. 
    
  7. from .models import Car, CarDriver, Driver, Group, Membership, Person, UserMembership
    
  8. 
    
  9. 
    
  10. class M2MThroughTestCase(TestCase):
    
  11.     @classmethod
    
  12.     def setUpTestData(cls):
    
  13.         cls.bob = Person.objects.create(name="Bob")
    
  14.         cls.jim = Person.objects.create(name="Jim")
    
  15. 
    
  16.         cls.rock = Group.objects.create(name="Rock")
    
  17.         cls.roll = Group.objects.create(name="Roll")
    
  18. 
    
  19.         cls.frank = User.objects.create_user("frank", "[email protected]", "password")
    
  20.         cls.jane = User.objects.create_user("jane", "[email protected]", "password")
    
  21. 
    
  22.         # normal intermediate model
    
  23.         cls.bob_rock = Membership.objects.create(person=cls.bob, group=cls.rock)
    
  24.         cls.bob_roll = Membership.objects.create(
    
  25.             person=cls.bob, group=cls.roll, price=50
    
  26.         )
    
  27.         cls.jim_rock = Membership.objects.create(
    
  28.             person=cls.jim, group=cls.rock, price=50
    
  29.         )
    
  30. 
    
  31.         # intermediate model with custom id column
    
  32.         cls.frank_rock = UserMembership.objects.create(user=cls.frank, group=cls.rock)
    
  33.         cls.frank_roll = UserMembership.objects.create(user=cls.frank, group=cls.roll)
    
  34.         cls.jane_rock = UserMembership.objects.create(user=cls.jane, group=cls.rock)
    
  35. 
    
  36.     def test_retrieve_reverse_m2m_items(self):
    
  37.         self.assertCountEqual(self.bob.group_set.all(), [self.rock, self.roll])
    
  38. 
    
  39.     def test_retrieve_forward_m2m_items(self):
    
  40.         self.assertSequenceEqual(self.roll.members.all(), [self.bob])
    
  41. 
    
  42.     def test_retrieve_reverse_m2m_items_via_custom_id_intermediary(self):
    
  43.         self.assertCountEqual(self.frank.group_set.all(), [self.rock, self.roll])
    
  44. 
    
  45.     def test_retrieve_forward_m2m_items_via_custom_id_intermediary(self):
    
  46.         self.assertSequenceEqual(self.roll.user_members.all(), [self.frank])
    
  47. 
    
  48.     def test_join_trimming_forwards(self):
    
  49.         """
    
  50.         Too many copies of the intermediate table aren't involved when doing a
    
  51.         join (#8046, #8254).
    
  52.         """
    
  53.         self.assertSequenceEqual(
    
  54.             self.rock.members.filter(membership__price=50),
    
  55.             [self.jim],
    
  56.         )
    
  57. 
    
  58.     def test_join_trimming_reverse(self):
    
  59.         self.assertSequenceEqual(
    
  60.             self.bob.group_set.filter(membership__price=50),
    
  61.             [self.roll],
    
  62.         )
    
  63. 
    
  64. 
    
  65. class M2MThroughSerializationTestCase(TestCase):
    
  66.     @classmethod
    
  67.     def setUpTestData(cls):
    
  68.         cls.bob = Person.objects.create(name="Bob")
    
  69.         cls.roll = Group.objects.create(name="Roll")
    
  70.         cls.bob_roll = Membership.objects.create(person=cls.bob, group=cls.roll)
    
  71. 
    
  72.     def test_serialization(self):
    
  73.         "m2m-through models aren't serialized as m2m fields. Refs #8134"
    
  74.         pks = {
    
  75.             "p_pk": self.bob.pk,
    
  76.             "g_pk": self.roll.pk,
    
  77.             "m_pk": self.bob_roll.pk,
    
  78.             "app_label": "m2m_through_regress",
    
  79.         }
    
  80. 
    
  81.         out = StringIO()
    
  82.         management.call_command(
    
  83.             "dumpdata", "m2m_through_regress", format="json", stdout=out
    
  84.         )
    
  85.         self.assertJSONEqual(
    
  86.             out.getvalue().strip(),
    
  87.             '[{"pk": %(m_pk)s, "model": "m2m_through_regress.membership", '
    
  88.             '"fields": {"person": %(p_pk)s, "price": 100, "group": %(g_pk)s}}, '
    
  89.             '{"pk": %(p_pk)s, "model": "m2m_through_regress.person", '
    
  90.             '"fields": {"name": "Bob"}}, '
    
  91.             '{"pk": %(g_pk)s, "model": "m2m_through_regress.group", '
    
  92.             '"fields": {"name": "Roll"}}]' % pks,
    
  93.         )
    
  94. 
    
  95.         out = StringIO()
    
  96.         management.call_command(
    
  97.             "dumpdata", "m2m_through_regress", format="xml", indent=2, stdout=out
    
  98.         )
    
  99.         self.assertXMLEqual(
    
  100.             out.getvalue().strip(),
    
  101.             """
    
  102. <?xml version="1.0" encoding="utf-8"?>
    
  103. <django-objects version="1.0">
    
  104.   <object pk="%(m_pk)s" model="%(app_label)s.membership">
    
  105.     <field to="%(app_label)s.person" name="person" rel="ManyToOneRel">%(p_pk)s</field>
    
  106.     <field to="%(app_label)s.group" name="group" rel="ManyToOneRel">%(g_pk)s</field>
    
  107.     <field type="IntegerField" name="price">100</field>
    
  108.   </object>
    
  109.   <object pk="%(p_pk)s" model="%(app_label)s.person">
    
  110.     <field type="CharField" name="name">Bob</field>
    
  111.   </object>
    
  112.   <object pk="%(g_pk)s" model="%(app_label)s.group">
    
  113.     <field type="CharField" name="name">Roll</field>
    
  114.   </object>
    
  115. </django-objects>
    
  116.         """.strip()
    
  117.             % pks,
    
  118.         )
    
  119. 
    
  120. 
    
  121. class ToFieldThroughTests(TestCase):
    
  122.     @classmethod
    
  123.     def setUpTestData(cls):
    
  124.         cls.car = Car.objects.create(make="Toyota")
    
  125.         cls.driver = Driver.objects.create(name="Ryan Briscoe")
    
  126.         CarDriver.objects.create(car=cls.car, driver=cls.driver)
    
  127.         # We are testing if wrong objects get deleted due to using wrong
    
  128.         # field value in m2m queries. So, it is essential that the pk
    
  129.         # numberings do not match.
    
  130.         # Create one intentionally unused driver to mix up the autonumbering
    
  131.         cls.unused_driver = Driver.objects.create(name="Barney Gumble")
    
  132.         # And two intentionally unused cars.
    
  133.         cls.unused_car1 = Car.objects.create(make="Trabant")
    
  134.         cls.unused_car2 = Car.objects.create(make="Wartburg")
    
  135. 
    
  136.     def test_to_field(self):
    
  137.         self.assertSequenceEqual(self.car.drivers.all(), [self.driver])
    
  138. 
    
  139.     def test_to_field_reverse(self):
    
  140.         self.assertSequenceEqual(self.driver.car_set.all(), [self.car])
    
  141. 
    
  142.     def test_to_field_clear_reverse(self):
    
  143.         self.driver.car_set.clear()
    
  144.         self.assertSequenceEqual(self.driver.car_set.all(), [])
    
  145. 
    
  146.     def test_to_field_clear(self):
    
  147.         self.car.drivers.clear()
    
  148.         self.assertSequenceEqual(self.car.drivers.all(), [])
    
  149. 
    
  150.     # Low level tests for _add_items and _remove_items. We test these methods
    
  151.     # because .add/.remove aren't available for m2m fields with through, but
    
  152.     # through is the only way to set to_field currently. We do want to make
    
  153.     # sure these methods are ready if the ability to use .add or .remove with
    
  154.     # to_field relations is added some day.
    
  155.     def test_add(self):
    
  156.         self.assertSequenceEqual(self.car.drivers.all(), [self.driver])
    
  157.         # Yikes - barney is going to drive...
    
  158.         self.car.drivers._add_items("car", "driver", self.unused_driver)
    
  159.         self.assertSequenceEqual(
    
  160.             self.car.drivers.all(),
    
  161.             [self.unused_driver, self.driver],
    
  162.         )
    
  163. 
    
  164.     def test_m2m_relations_unusable_on_null_to_field(self):
    
  165.         nullcar = Car(make=None)
    
  166.         msg = (
    
  167.             '"<Car: None>" needs to have a value for field "make" before this '
    
  168.             "many-to-many relationship can be used."
    
  169.         )
    
  170.         with self.assertRaisesMessage(ValueError, msg):
    
  171.             nullcar.drivers.all()
    
  172. 
    
  173.     def test_m2m_relations_unusable_on_null_pk_obj(self):
    
  174.         msg = (
    
  175.             "'Car' instance needs to have a primary key value before a "
    
  176.             "many-to-many relationship can be used."
    
  177.         )
    
  178.         with self.assertRaisesMessage(ValueError, msg):
    
  179.             Car(make="Ford").drivers.all()
    
  180. 
    
  181.     def test_add_related_null(self):
    
  182.         nulldriver = Driver.objects.create(name=None)
    
  183.         msg = 'Cannot add "<Driver: None>": the value for field "driver" is None'
    
  184.         with self.assertRaisesMessage(ValueError, msg):
    
  185.             self.car.drivers._add_items("car", "driver", nulldriver)
    
  186. 
    
  187.     def test_add_reverse(self):
    
  188.         car2 = Car.objects.create(make="Honda")
    
  189.         self.assertCountEqual(self.driver.car_set.all(), [self.car])
    
  190.         self.driver.car_set._add_items("driver", "car", car2)
    
  191.         self.assertCountEqual(self.driver.car_set.all(), [self.car, car2])
    
  192. 
    
  193.     def test_add_null_reverse(self):
    
  194.         nullcar = Car.objects.create(make=None)
    
  195.         msg = 'Cannot add "<Car: None>": the value for field "car" is None'
    
  196.         with self.assertRaisesMessage(ValueError, msg):
    
  197.             self.driver.car_set._add_items("driver", "car", nullcar)
    
  198. 
    
  199.     def test_add_null_reverse_related(self):
    
  200.         nulldriver = Driver.objects.create(name=None)
    
  201.         msg = (
    
  202.             '"<Driver: None>" needs to have a value for field "name" before '
    
  203.             "this many-to-many relationship can be used."
    
  204.         )
    
  205.         with self.assertRaisesMessage(ValueError, msg):
    
  206.             nulldriver.car_set._add_items("driver", "car", self.car)
    
  207. 
    
  208.     def test_remove(self):
    
  209.         self.assertSequenceEqual(self.car.drivers.all(), [self.driver])
    
  210.         self.car.drivers._remove_items("car", "driver", self.driver)
    
  211.         self.assertSequenceEqual(self.car.drivers.all(), [])
    
  212. 
    
  213.     def test_remove_reverse(self):
    
  214.         self.assertSequenceEqual(self.driver.car_set.all(), [self.car])
    
  215.         self.driver.car_set._remove_items("driver", "car", self.car)
    
  216.         self.assertSequenceEqual(self.driver.car_set.all(), [])
    
  217. 
    
  218. 
    
  219. class ThroughLoadDataTestCase(TestCase):
    
  220.     fixtures = ["m2m_through"]
    
  221. 
    
  222.     def test_sequence_creation(self):
    
  223.         """
    
  224.         Sequences on an m2m_through are created for the through model, not a
    
  225.         phantom auto-generated m2m table (#11107).
    
  226.         """
    
  227.         out = StringIO()
    
  228.         management.call_command(
    
  229.             "dumpdata", "m2m_through_regress", format="json", stdout=out
    
  230.         )
    
  231.         self.assertJSONEqual(
    
  232.             out.getvalue().strip(),
    
  233.             '[{"pk": 1, "model": "m2m_through_regress.usermembership", '
    
  234.             '"fields": {"price": 100, "group": 1, "user": 1}}, '
    
  235.             '{"pk": 1, "model": "m2m_through_regress.person", '
    
  236.             '"fields": {"name": "Guido"}}, '
    
  237.             '{"pk": 1, "model": "m2m_through_regress.group", '
    
  238.             '"fields": {"name": "Python Core Group"}}]',
    
  239.         )