1. from datetime import date, datetime, timedelta
    
  2. from operator import attrgetter
    
  3. 
    
  4. from django.db import IntegrityError
    
  5. from django.test import TestCase
    
  6. 
    
  7. from .models import (
    
  8.     CustomMembership,
    
  9.     Employee,
    
  10.     Event,
    
  11.     Friendship,
    
  12.     Group,
    
  13.     Ingredient,
    
  14.     Invitation,
    
  15.     Membership,
    
  16.     Person,
    
  17.     PersonChild,
    
  18.     PersonSelfRefM2M,
    
  19.     Recipe,
    
  20.     RecipeIngredient,
    
  21.     Relationship,
    
  22.     SymmetricalFriendship,
    
  23. )
    
  24. 
    
  25. 
    
  26. class M2mThroughTests(TestCase):
    
  27.     @classmethod
    
  28.     def setUpTestData(cls):
    
  29.         cls.bob = Person.objects.create(name="Bob")
    
  30.         cls.jim = Person.objects.create(name="Jim")
    
  31.         cls.jane = Person.objects.create(name="Jane")
    
  32.         cls.rock = Group.objects.create(name="Rock")
    
  33.         cls.roll = Group.objects.create(name="Roll")
    
  34. 
    
  35.     def test_reverse_inherited_m2m_with_through_fields_list_hashable(self):
    
  36.         reverse_m2m = Person._meta.get_field("events_invited")
    
  37.         self.assertEqual(reverse_m2m.through_fields, ["event", "invitee"])
    
  38.         inherited_reverse_m2m = PersonChild._meta.get_field("events_invited")
    
  39.         self.assertEqual(inherited_reverse_m2m.through_fields, ["event", "invitee"])
    
  40.         self.assertEqual(hash(reverse_m2m), hash(inherited_reverse_m2m))
    
  41. 
    
  42.     def test_retrieve_intermediate_items(self):
    
  43.         Membership.objects.create(person=self.jim, group=self.rock)
    
  44.         Membership.objects.create(person=self.jane, group=self.rock)
    
  45. 
    
  46.         expected = ["Jane", "Jim"]
    
  47.         self.assertQuerysetEqual(self.rock.members.all(), expected, attrgetter("name"))
    
  48. 
    
  49.     def test_get_on_intermediate_model(self):
    
  50.         Membership.objects.create(person=self.jane, group=self.rock)
    
  51. 
    
  52.         queryset = Membership.objects.get(person=self.jane, group=self.rock)
    
  53. 
    
  54.         self.assertEqual(repr(queryset), "<Membership: Jane is a member of Rock>")
    
  55. 
    
  56.     def test_filter_on_intermediate_model(self):
    
  57.         m1 = Membership.objects.create(person=self.jim, group=self.rock)
    
  58.         m2 = Membership.objects.create(person=self.jane, group=self.rock)
    
  59. 
    
  60.         queryset = Membership.objects.filter(group=self.rock)
    
  61. 
    
  62.         self.assertSequenceEqual(queryset, [m1, m2])
    
  63. 
    
  64.     def test_add_on_m2m_with_intermediate_model(self):
    
  65.         self.rock.members.add(
    
  66.             self.bob, through_defaults={"invite_reason": "He is good."}
    
  67.         )
    
  68.         self.assertSequenceEqual(self.rock.members.all(), [self.bob])
    
  69.         self.assertEqual(self.rock.membership_set.get().invite_reason, "He is good.")
    
  70. 
    
  71.     def test_add_on_m2m_with_intermediate_model_callable_through_default(self):
    
  72.         def invite_reason_callable():
    
  73.             return "They were good at %s" % datetime.now()
    
  74. 
    
  75.         self.rock.members.add(
    
  76.             self.bob,
    
  77.             self.jane,
    
  78.             through_defaults={"invite_reason": invite_reason_callable},
    
  79.         )
    
  80.         self.assertSequenceEqual(self.rock.members.all(), [self.bob, self.jane])
    
  81.         self.assertEqual(
    
  82.             self.rock.membership_set.filter(
    
  83.                 invite_reason__startswith="They were good at ",
    
  84.             ).count(),
    
  85.             2,
    
  86.         )
    
  87.         # invite_reason_callable() is called once.
    
  88.         self.assertEqual(
    
  89.             self.bob.membership_set.get().invite_reason,
    
  90.             self.jane.membership_set.get().invite_reason,
    
  91.         )
    
  92. 
    
  93.     def test_set_on_m2m_with_intermediate_model_callable_through_default(self):
    
  94.         self.rock.members.set(
    
  95.             [self.bob, self.jane],
    
  96.             through_defaults={"invite_reason": lambda: "Why not?"},
    
  97.         )
    
  98.         self.assertSequenceEqual(self.rock.members.all(), [self.bob, self.jane])
    
  99.         self.assertEqual(
    
  100.             self.rock.membership_set.filter(
    
  101.                 invite_reason__startswith="Why not?",
    
  102.             ).count(),
    
  103.             2,
    
  104.         )
    
  105. 
    
  106.     def test_add_on_m2m_with_intermediate_model_value_required(self):
    
  107.         self.rock.nodefaultsnonulls.add(
    
  108.             self.jim, through_defaults={"nodefaultnonull": 1}
    
  109.         )
    
  110.         self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
    
  111. 
    
  112.     def test_add_on_m2m_with_intermediate_model_value_required_fails(self):
    
  113.         with self.assertRaises(IntegrityError):
    
  114.             self.rock.nodefaultsnonulls.add(self.jim)
    
  115. 
    
  116.     def test_create_on_m2m_with_intermediate_model(self):
    
  117.         annie = self.rock.members.create(
    
  118.             name="Annie", through_defaults={"invite_reason": "She was just awesome."}
    
  119.         )
    
  120.         self.assertSequenceEqual(self.rock.members.all(), [annie])
    
  121.         self.assertEqual(
    
  122.             self.rock.membership_set.get().invite_reason, "She was just awesome."
    
  123.         )
    
  124. 
    
  125.     def test_create_on_m2m_with_intermediate_model_callable_through_default(self):
    
  126.         annie = self.rock.members.create(
    
  127.             name="Annie",
    
  128.             through_defaults={"invite_reason": lambda: "She was just awesome."},
    
  129.         )
    
  130.         self.assertSequenceEqual(self.rock.members.all(), [annie])
    
  131.         self.assertEqual(
    
  132.             self.rock.membership_set.get().invite_reason,
    
  133.             "She was just awesome.",
    
  134.         )
    
  135. 
    
  136.     def test_create_on_m2m_with_intermediate_model_value_required(self):
    
  137.         self.rock.nodefaultsnonulls.create(
    
  138.             name="Test", through_defaults={"nodefaultnonull": 1}
    
  139.         )
    
  140.         self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
    
  141. 
    
  142.     def test_create_on_m2m_with_intermediate_model_value_required_fails(self):
    
  143.         with self.assertRaises(IntegrityError):
    
  144.             self.rock.nodefaultsnonulls.create(name="Test")
    
  145. 
    
  146.     def test_get_or_create_on_m2m_with_intermediate_model_value_required(self):
    
  147.         self.rock.nodefaultsnonulls.get_or_create(
    
  148.             name="Test", through_defaults={"nodefaultnonull": 1}
    
  149.         )
    
  150.         self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
    
  151. 
    
  152.     def test_get_or_create_on_m2m_with_intermediate_model_value_required_fails(self):
    
  153.         with self.assertRaises(IntegrityError):
    
  154.             self.rock.nodefaultsnonulls.get_or_create(name="Test")
    
  155. 
    
  156.     def test_update_or_create_on_m2m_with_intermediate_model_value_required(self):
    
  157.         self.rock.nodefaultsnonulls.update_or_create(
    
  158.             name="Test", through_defaults={"nodefaultnonull": 1}
    
  159.         )
    
  160.         self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
    
  161. 
    
  162.     def test_update_or_create_on_m2m_with_intermediate_model_value_required_fails(self):
    
  163.         with self.assertRaises(IntegrityError):
    
  164.             self.rock.nodefaultsnonulls.update_or_create(name="Test")
    
  165. 
    
  166.     def test_remove_on_m2m_with_intermediate_model(self):
    
  167.         Membership.objects.create(person=self.jim, group=self.rock)
    
  168.         self.rock.members.remove(self.jim)
    
  169.         self.assertSequenceEqual(self.rock.members.all(), [])
    
  170. 
    
  171.     def test_remove_on_m2m_with_intermediate_model_multiple(self):
    
  172.         Membership.objects.create(person=self.jim, group=self.rock, invite_reason="1")
    
  173.         Membership.objects.create(person=self.jim, group=self.rock, invite_reason="2")
    
  174.         self.assertSequenceEqual(self.rock.members.all(), [self.jim, self.jim])
    
  175.         self.rock.members.remove(self.jim)
    
  176.         self.assertSequenceEqual(self.rock.members.all(), [])
    
  177. 
    
  178.     def test_set_on_m2m_with_intermediate_model(self):
    
  179.         members = list(Person.objects.filter(name__in=["Bob", "Jim"]))
    
  180.         self.rock.members.set(members)
    
  181.         self.assertSequenceEqual(self.rock.members.all(), [self.bob, self.jim])
    
  182. 
    
  183.     def test_set_on_m2m_with_intermediate_model_value_required(self):
    
  184.         self.rock.nodefaultsnonulls.set(
    
  185.             [self.jim], through_defaults={"nodefaultnonull": 1}
    
  186.         )
    
  187.         self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
    
  188.         self.rock.nodefaultsnonulls.set(
    
  189.             [self.jim], through_defaults={"nodefaultnonull": 2}
    
  190.         )
    
  191.         self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 1)
    
  192.         self.rock.nodefaultsnonulls.set(
    
  193.             [self.jim], through_defaults={"nodefaultnonull": 2}, clear=True
    
  194.         )
    
  195.         self.assertEqual(self.rock.testnodefaultsornulls_set.get().nodefaultnonull, 2)
    
  196. 
    
  197.     def test_set_on_m2m_with_intermediate_model_value_required_fails(self):
    
  198.         with self.assertRaises(IntegrityError):
    
  199.             self.rock.nodefaultsnonulls.set([self.jim])
    
  200. 
    
  201.     def test_clear_removes_all_the_m2m_relationships(self):
    
  202.         Membership.objects.create(person=self.jim, group=self.rock)
    
  203.         Membership.objects.create(person=self.jane, group=self.rock)
    
  204. 
    
  205.         self.rock.members.clear()
    
  206. 
    
  207.         self.assertQuerysetEqual(self.rock.members.all(), [])
    
  208. 
    
  209.     def test_retrieve_reverse_intermediate_items(self):
    
  210.         Membership.objects.create(person=self.jim, group=self.rock)
    
  211.         Membership.objects.create(person=self.jim, group=self.roll)
    
  212. 
    
  213.         expected = ["Rock", "Roll"]
    
  214.         self.assertQuerysetEqual(self.jim.group_set.all(), expected, attrgetter("name"))
    
  215. 
    
  216.     def test_add_on_reverse_m2m_with_intermediate_model(self):
    
  217.         self.bob.group_set.add(self.rock)
    
  218.         self.assertSequenceEqual(self.bob.group_set.all(), [self.rock])
    
  219. 
    
  220.     def test_create_on_reverse_m2m_with_intermediate_model(self):
    
  221.         funk = self.bob.group_set.create(name="Funk")
    
  222.         self.assertSequenceEqual(self.bob.group_set.all(), [funk])
    
  223. 
    
  224.     def test_remove_on_reverse_m2m_with_intermediate_model(self):
    
  225.         Membership.objects.create(person=self.bob, group=self.rock)
    
  226.         self.bob.group_set.remove(self.rock)
    
  227.         self.assertSequenceEqual(self.bob.group_set.all(), [])
    
  228. 
    
  229.     def test_set_on_reverse_m2m_with_intermediate_model(self):
    
  230.         members = list(Group.objects.filter(name__in=["Rock", "Roll"]))
    
  231.         self.bob.group_set.set(members)
    
  232.         self.assertSequenceEqual(self.bob.group_set.all(), [self.rock, self.roll])
    
  233. 
    
  234.     def test_clear_on_reverse_removes_all_the_m2m_relationships(self):
    
  235.         Membership.objects.create(person=self.jim, group=self.rock)
    
  236.         Membership.objects.create(person=self.jim, group=self.roll)
    
  237. 
    
  238.         self.jim.group_set.clear()
    
  239. 
    
  240.         self.assertQuerysetEqual(self.jim.group_set.all(), [])
    
  241. 
    
  242.     def test_query_model_by_attribute_name_of_related_model(self):
    
  243.         Membership.objects.create(person=self.jim, group=self.rock)
    
  244.         Membership.objects.create(person=self.jane, group=self.rock)
    
  245.         Membership.objects.create(person=self.bob, group=self.roll)
    
  246.         Membership.objects.create(person=self.jim, group=self.roll)
    
  247.         Membership.objects.create(person=self.jane, group=self.roll)
    
  248. 
    
  249.         self.assertQuerysetEqual(
    
  250.             Group.objects.filter(members__name="Bob"), ["Roll"], attrgetter("name")
    
  251.         )
    
  252. 
    
  253.     def test_order_by_relational_field_through_model(self):
    
  254.         today = datetime.now()
    
  255.         yesterday = today - timedelta(days=1)
    
  256.         CustomMembership.objects.create(
    
  257.             person=self.jim, group=self.rock, date_joined=yesterday
    
  258.         )
    
  259.         CustomMembership.objects.create(
    
  260.             person=self.bob, group=self.rock, date_joined=today
    
  261.         )
    
  262.         CustomMembership.objects.create(
    
  263.             person=self.jane, group=self.roll, date_joined=yesterday
    
  264.         )
    
  265.         CustomMembership.objects.create(
    
  266.             person=self.jim, group=self.roll, date_joined=today
    
  267.         )
    
  268.         self.assertSequenceEqual(
    
  269.             self.rock.custom_members.order_by("custom_person_related_name"),
    
  270.             [self.jim, self.bob],
    
  271.         )
    
  272.         self.assertSequenceEqual(
    
  273.             self.roll.custom_members.order_by("custom_person_related_name"),
    
  274.             [self.jane, self.jim],
    
  275.         )
    
  276. 
    
  277.     def test_query_first_model_by_intermediate_model_attribute(self):
    
  278.         Membership.objects.create(
    
  279.             person=self.jane, group=self.roll, invite_reason="She was just awesome."
    
  280.         )
    
  281.         Membership.objects.create(
    
  282.             person=self.jim, group=self.roll, invite_reason="He is good."
    
  283.         )
    
  284.         Membership.objects.create(person=self.bob, group=self.roll)
    
  285. 
    
  286.         qs = Group.objects.filter(membership__invite_reason="She was just awesome.")
    
  287.         self.assertQuerysetEqual(qs, ["Roll"], attrgetter("name"))
    
  288. 
    
  289.     def test_query_second_model_by_intermediate_model_attribute(self):
    
  290.         Membership.objects.create(
    
  291.             person=self.jane, group=self.roll, invite_reason="She was just awesome."
    
  292.         )
    
  293.         Membership.objects.create(
    
  294.             person=self.jim, group=self.roll, invite_reason="He is good."
    
  295.         )
    
  296.         Membership.objects.create(person=self.bob, group=self.roll)
    
  297. 
    
  298.         qs = Person.objects.filter(membership__invite_reason="She was just awesome.")
    
  299.         self.assertQuerysetEqual(qs, ["Jane"], attrgetter("name"))
    
  300. 
    
  301.     def test_query_model_by_related_model_name(self):
    
  302.         Membership.objects.create(person=self.jim, group=self.rock)
    
  303.         Membership.objects.create(person=self.jane, group=self.rock)
    
  304.         Membership.objects.create(person=self.bob, group=self.roll)
    
  305.         Membership.objects.create(person=self.jim, group=self.roll)
    
  306.         Membership.objects.create(person=self.jane, group=self.roll)
    
  307. 
    
  308.         self.assertQuerysetEqual(
    
  309.             Person.objects.filter(group__name="Rock"),
    
  310.             ["Jane", "Jim"],
    
  311.             attrgetter("name"),
    
  312.         )
    
  313. 
    
  314.     def test_query_model_by_custom_related_name(self):
    
  315.         CustomMembership.objects.create(person=self.bob, group=self.rock)
    
  316.         CustomMembership.objects.create(person=self.jim, group=self.rock)
    
  317. 
    
  318.         self.assertQuerysetEqual(
    
  319.             Person.objects.filter(custom__name="Rock"),
    
  320.             ["Bob", "Jim"],
    
  321.             attrgetter("name"),
    
  322.         )
    
  323. 
    
  324.     def test_query_model_by_intermediate_can_return_non_unique_queryset(self):
    
  325.         Membership.objects.create(person=self.jim, group=self.rock)
    
  326.         Membership.objects.create(
    
  327.             person=self.jane, group=self.rock, date_joined=datetime(2006, 1, 1)
    
  328.         )
    
  329.         Membership.objects.create(
    
  330.             person=self.bob, group=self.roll, date_joined=datetime(2004, 1, 1)
    
  331.         )
    
  332.         Membership.objects.create(person=self.jim, group=self.roll)
    
  333.         Membership.objects.create(
    
  334.             person=self.jane, group=self.roll, date_joined=datetime(2004, 1, 1)
    
  335.         )
    
  336. 
    
  337.         qs = Person.objects.filter(membership__date_joined__gt=datetime(2004, 1, 1))
    
  338.         self.assertQuerysetEqual(qs, ["Jane", "Jim", "Jim"], attrgetter("name"))
    
  339. 
    
  340.     def test_custom_related_name_forward_empty_qs(self):
    
  341.         self.assertQuerysetEqual(self.rock.custom_members.all(), [])
    
  342. 
    
  343.     def test_custom_related_name_reverse_empty_qs(self):
    
  344.         self.assertQuerysetEqual(self.bob.custom.all(), [])
    
  345. 
    
  346.     def test_custom_related_name_forward_non_empty_qs(self):
    
  347.         CustomMembership.objects.create(person=self.bob, group=self.rock)
    
  348.         CustomMembership.objects.create(person=self.jim, group=self.rock)
    
  349. 
    
  350.         self.assertQuerysetEqual(
    
  351.             self.rock.custom_members.all(), ["Bob", "Jim"], attrgetter("name")
    
  352.         )
    
  353. 
    
  354.     def test_custom_related_name_reverse_non_empty_qs(self):
    
  355.         CustomMembership.objects.create(person=self.bob, group=self.rock)
    
  356.         CustomMembership.objects.create(person=self.jim, group=self.rock)
    
  357. 
    
  358.         self.assertQuerysetEqual(self.bob.custom.all(), ["Rock"], attrgetter("name"))
    
  359. 
    
  360.     def test_custom_related_name_doesnt_conflict_with_fky_related_name(self):
    
  361.         c = CustomMembership.objects.create(person=self.bob, group=self.rock)
    
  362.         self.assertSequenceEqual(self.bob.custom_person_related_name.all(), [c])
    
  363. 
    
  364.     def test_through_fields(self):
    
  365.         """
    
  366.         Relations with intermediary tables with multiple FKs
    
  367.         to the M2M's ``to`` model are possible.
    
  368.         """
    
  369.         event = Event.objects.create(title="Rockwhale 2014")
    
  370.         Invitation.objects.create(event=event, inviter=self.bob, invitee=self.jim)
    
  371.         Invitation.objects.create(event=event, inviter=self.bob, invitee=self.jane)
    
  372.         self.assertQuerysetEqual(
    
  373.             event.invitees.all(), ["Jane", "Jim"], attrgetter("name")
    
  374.         )
    
  375. 
    
  376. 
    
  377. class M2mThroughReferentialTests(TestCase):
    
  378.     def test_self_referential_empty_qs(self):
    
  379.         tony = PersonSelfRefM2M.objects.create(name="Tony")
    
  380.         self.assertQuerysetEqual(tony.friends.all(), [])
    
  381. 
    
  382.     def test_self_referential_non_symmetrical_first_side(self):
    
  383.         tony = PersonSelfRefM2M.objects.create(name="Tony")
    
  384.         chris = PersonSelfRefM2M.objects.create(name="Chris")
    
  385.         Friendship.objects.create(
    
  386.             first=tony, second=chris, date_friended=datetime.now()
    
  387.         )
    
  388. 
    
  389.         self.assertQuerysetEqual(tony.friends.all(), ["Chris"], attrgetter("name"))
    
  390. 
    
  391.     def test_self_referential_non_symmetrical_second_side(self):
    
  392.         tony = PersonSelfRefM2M.objects.create(name="Tony")
    
  393.         chris = PersonSelfRefM2M.objects.create(name="Chris")
    
  394.         Friendship.objects.create(
    
  395.             first=tony, second=chris, date_friended=datetime.now()
    
  396.         )
    
  397. 
    
  398.         self.assertQuerysetEqual(chris.friends.all(), [])
    
  399. 
    
  400.     def test_self_referential_non_symmetrical_clear_first_side(self):
    
  401.         tony = PersonSelfRefM2M.objects.create(name="Tony")
    
  402.         chris = PersonSelfRefM2M.objects.create(name="Chris")
    
  403.         Friendship.objects.create(
    
  404.             first=tony, second=chris, date_friended=datetime.now()
    
  405.         )
    
  406. 
    
  407.         chris.friends.clear()
    
  408. 
    
  409.         self.assertQuerysetEqual(chris.friends.all(), [])
    
  410. 
    
  411.         # Since this isn't a symmetrical relation, Tony's friend link still exists.
    
  412.         self.assertQuerysetEqual(tony.friends.all(), ["Chris"], attrgetter("name"))
    
  413. 
    
  414.     def test_self_referential_non_symmetrical_both(self):
    
  415.         tony = PersonSelfRefM2M.objects.create(name="Tony")
    
  416.         chris = PersonSelfRefM2M.objects.create(name="Chris")
    
  417.         Friendship.objects.create(
    
  418.             first=tony, second=chris, date_friended=datetime.now()
    
  419.         )
    
  420.         Friendship.objects.create(
    
  421.             first=chris, second=tony, date_friended=datetime.now()
    
  422.         )
    
  423. 
    
  424.         self.assertQuerysetEqual(tony.friends.all(), ["Chris"], attrgetter("name"))
    
  425. 
    
  426.         self.assertQuerysetEqual(chris.friends.all(), ["Tony"], attrgetter("name"))
    
  427. 
    
  428.     def test_through_fields_self_referential(self):
    
  429.         john = Employee.objects.create(name="john")
    
  430.         peter = Employee.objects.create(name="peter")
    
  431.         mary = Employee.objects.create(name="mary")
    
  432.         harry = Employee.objects.create(name="harry")
    
  433. 
    
  434.         Relationship.objects.create(source=john, target=peter, another=None)
    
  435.         Relationship.objects.create(source=john, target=mary, another=None)
    
  436.         Relationship.objects.create(source=john, target=harry, another=peter)
    
  437. 
    
  438.         self.assertQuerysetEqual(
    
  439.             john.subordinates.all(), ["peter", "mary", "harry"], attrgetter("name")
    
  440.         )
    
  441. 
    
  442.     def test_self_referential_symmetrical(self):
    
  443.         tony = PersonSelfRefM2M.objects.create(name="Tony")
    
  444.         chris = PersonSelfRefM2M.objects.create(name="Chris")
    
  445.         SymmetricalFriendship.objects.create(
    
  446.             first=tony,
    
  447.             second=chris,
    
  448.             date_friended=date.today(),
    
  449.         )
    
  450.         self.assertSequenceEqual(tony.sym_friends.all(), [chris])
    
  451.         # Manually created symmetrical m2m relation doesn't add mirror entry
    
  452.         # automatically.
    
  453.         self.assertSequenceEqual(chris.sym_friends.all(), [])
    
  454.         SymmetricalFriendship.objects.create(
    
  455.             first=chris, second=tony, date_friended=date.today()
    
  456.         )
    
  457.         self.assertSequenceEqual(chris.sym_friends.all(), [tony])
    
  458. 
    
  459.     def test_add_on_symmetrical_m2m_with_intermediate_model(self):
    
  460.         tony = PersonSelfRefM2M.objects.create(name="Tony")
    
  461.         chris = PersonSelfRefM2M.objects.create(name="Chris")
    
  462.         date_friended = date(2017, 1, 3)
    
  463.         tony.sym_friends.add(chris, through_defaults={"date_friended": date_friended})
    
  464.         self.assertSequenceEqual(tony.sym_friends.all(), [chris])
    
  465.         self.assertSequenceEqual(chris.sym_friends.all(), [tony])
    
  466.         friendship = tony.symmetricalfriendship_set.get()
    
  467.         self.assertEqual(friendship.date_friended, date_friended)
    
  468. 
    
  469.     def test_set_on_symmetrical_m2m_with_intermediate_model(self):
    
  470.         tony = PersonSelfRefM2M.objects.create(name="Tony")
    
  471.         chris = PersonSelfRefM2M.objects.create(name="Chris")
    
  472.         anne = PersonSelfRefM2M.objects.create(name="Anne")
    
  473.         kate = PersonSelfRefM2M.objects.create(name="Kate")
    
  474.         date_friended_add = date(2013, 1, 5)
    
  475.         date_friended_set = date.today()
    
  476.         tony.sym_friends.add(
    
  477.             anne,
    
  478.             chris,
    
  479.             through_defaults={"date_friended": date_friended_add},
    
  480.         )
    
  481.         tony.sym_friends.set(
    
  482.             [anne, kate],
    
  483.             through_defaults={"date_friended": date_friended_set},
    
  484.         )
    
  485.         self.assertSequenceEqual(tony.sym_friends.all(), [anne, kate])
    
  486.         self.assertSequenceEqual(anne.sym_friends.all(), [tony])
    
  487.         self.assertSequenceEqual(kate.sym_friends.all(), [tony])
    
  488.         self.assertEqual(
    
  489.             kate.symmetricalfriendship_set.get().date_friended,
    
  490.             date_friended_set,
    
  491.         )
    
  492.         # Date is preserved.
    
  493.         self.assertEqual(
    
  494.             anne.symmetricalfriendship_set.get().date_friended,
    
  495.             date_friended_add,
    
  496.         )
    
  497.         # Recreate relationship.
    
  498.         tony.sym_friends.set(
    
  499.             [anne],
    
  500.             clear=True,
    
  501.             through_defaults={"date_friended": date_friended_set},
    
  502.         )
    
  503.         self.assertSequenceEqual(tony.sym_friends.all(), [anne])
    
  504.         self.assertSequenceEqual(anne.sym_friends.all(), [tony])
    
  505.         self.assertEqual(
    
  506.             anne.symmetricalfriendship_set.get().date_friended,
    
  507.             date_friended_set,
    
  508.         )
    
  509. 
    
  510. 
    
  511. class M2mThroughToFieldsTests(TestCase):
    
  512.     @classmethod
    
  513.     def setUpTestData(cls):
    
  514.         cls.pea = Ingredient.objects.create(iname="pea")
    
  515.         cls.potato = Ingredient.objects.create(iname="potato")
    
  516.         cls.tomato = Ingredient.objects.create(iname="tomato")
    
  517.         cls.curry = Recipe.objects.create(rname="curry")
    
  518.         RecipeIngredient.objects.create(recipe=cls.curry, ingredient=cls.potato)
    
  519.         RecipeIngredient.objects.create(recipe=cls.curry, ingredient=cls.pea)
    
  520.         RecipeIngredient.objects.create(recipe=cls.curry, ingredient=cls.tomato)
    
  521. 
    
  522.     def test_retrieval(self):
    
  523.         # Forward retrieval
    
  524.         self.assertSequenceEqual(
    
  525.             self.curry.ingredients.all(), [self.pea, self.potato, self.tomato]
    
  526.         )
    
  527.         # Backward retrieval
    
  528.         self.assertEqual(self.tomato.recipes.get(), self.curry)
    
  529. 
    
  530.     def test_choices(self):
    
  531.         field = Recipe._meta.get_field("ingredients")
    
  532.         self.assertEqual(
    
  533.             [choice[0] for choice in field.get_choices(include_blank=False)],
    
  534.             ["pea", "potato", "tomato"],
    
  535.         )