1. import copy
    
  2. import datetime
    
  3. import pickle
    
  4. from operator import attrgetter
    
  5. 
    
  6. from django.core.exceptions import FieldError
    
  7. from django.db import models
    
  8. from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
    
  9. from django.test.utils import isolate_apps
    
  10. from django.utils import translation
    
  11. 
    
  12. from .models import (
    
  13.     Article,
    
  14.     ArticleIdea,
    
  15.     ArticleTag,
    
  16.     ArticleTranslation,
    
  17.     Country,
    
  18.     Friendship,
    
  19.     Group,
    
  20.     Membership,
    
  21.     NewsArticle,
    
  22.     Person,
    
  23. )
    
  24. 
    
  25. # Note that these tests are testing internal implementation details.
    
  26. # ForeignObject is not part of public API.
    
  27. 
    
  28. 
    
  29. class MultiColumnFKTests(TestCase):
    
  30.     @classmethod
    
  31.     def setUpTestData(cls):
    
  32.         # Creating countries
    
  33.         cls.usa = Country.objects.create(name="United States of America")
    
  34.         cls.soviet_union = Country.objects.create(name="Soviet Union")
    
  35.         # Creating People
    
  36.         cls.bob = Person.objects.create(name="Bob", person_country=cls.usa)
    
  37.         cls.jim = Person.objects.create(name="Jim", person_country=cls.usa)
    
  38.         cls.george = Person.objects.create(name="George", person_country=cls.usa)
    
  39. 
    
  40.         cls.jane = Person.objects.create(name="Jane", person_country=cls.soviet_union)
    
  41.         cls.mark = Person.objects.create(name="Mark", person_country=cls.soviet_union)
    
  42.         cls.sam = Person.objects.create(name="Sam", person_country=cls.soviet_union)
    
  43. 
    
  44.         # Creating Groups
    
  45.         cls.kgb = Group.objects.create(name="KGB", group_country=cls.soviet_union)
    
  46.         cls.cia = Group.objects.create(name="CIA", group_country=cls.usa)
    
  47.         cls.republican = Group.objects.create(name="Republican", group_country=cls.usa)
    
  48.         cls.democrat = Group.objects.create(name="Democrat", group_country=cls.usa)
    
  49. 
    
  50.     def test_get_succeeds_on_multicolumn_match(self):
    
  51.         # Membership objects have access to their related Person if both
    
  52.         # country_ids match between them
    
  53.         membership = Membership.objects.create(
    
  54.             membership_country_id=self.usa.id,
    
  55.             person_id=self.bob.id,
    
  56.             group_id=self.cia.id,
    
  57.         )
    
  58. 
    
  59.         person = membership.person
    
  60.         self.assertEqual((person.id, person.name), (self.bob.id, "Bob"))
    
  61. 
    
  62.     def test_get_fails_on_multicolumn_mismatch(self):
    
  63.         # Membership objects returns DoesNotExist error when there is no
    
  64.         # Person with the same id and country_id
    
  65.         membership = Membership.objects.create(
    
  66.             membership_country_id=self.usa.id,
    
  67.             person_id=self.jane.id,
    
  68.             group_id=self.cia.id,
    
  69.         )
    
  70. 
    
  71.         with self.assertRaises(Person.DoesNotExist):
    
  72.             getattr(membership, "person")
    
  73. 
    
  74.     def test_reverse_query_returns_correct_result(self):
    
  75.         # Creating a valid membership because it has the same country has the person
    
  76.         Membership.objects.create(
    
  77.             membership_country_id=self.usa.id,
    
  78.             person_id=self.bob.id,
    
  79.             group_id=self.cia.id,
    
  80.         )
    
  81. 
    
  82.         # Creating an invalid membership because it has a different country has
    
  83.         # the person.
    
  84.         Membership.objects.create(
    
  85.             membership_country_id=self.soviet_union.id,
    
  86.             person_id=self.bob.id,
    
  87.             group_id=self.republican.id,
    
  88.         )
    
  89. 
    
  90.         with self.assertNumQueries(1):
    
  91.             membership = self.bob.membership_set.get()
    
  92.             self.assertEqual(membership.group_id, self.cia.id)
    
  93.             self.assertIs(membership.person, self.bob)
    
  94. 
    
  95.     def test_query_filters_correctly(self):
    
  96.         # Creating a to valid memberships
    
  97.         Membership.objects.create(
    
  98.             membership_country_id=self.usa.id,
    
  99.             person_id=self.bob.id,
    
  100.             group_id=self.cia.id,
    
  101.         )
    
  102.         Membership.objects.create(
    
  103.             membership_country_id=self.usa.id,
    
  104.             person_id=self.jim.id,
    
  105.             group_id=self.cia.id,
    
  106.         )
    
  107. 
    
  108.         # Creating an invalid membership
    
  109.         Membership.objects.create(
    
  110.             membership_country_id=self.soviet_union.id,
    
  111.             person_id=self.george.id,
    
  112.             group_id=self.cia.id,
    
  113.         )
    
  114. 
    
  115.         self.assertQuerysetEqual(
    
  116.             Membership.objects.filter(person__name__contains="o"),
    
  117.             [self.bob.id],
    
  118.             attrgetter("person_id"),
    
  119.         )
    
  120. 
    
  121.     def test_reverse_query_filters_correctly(self):
    
  122.         timemark = datetime.datetime.now(tz=datetime.timezone.utc).replace(tzinfo=None)
    
  123.         timedelta = datetime.timedelta(days=1)
    
  124. 
    
  125.         # Creating a to valid memberships
    
  126.         Membership.objects.create(
    
  127.             membership_country_id=self.usa.id,
    
  128.             person_id=self.bob.id,
    
  129.             group_id=self.cia.id,
    
  130.             date_joined=timemark - timedelta,
    
  131.         )
    
  132.         Membership.objects.create(
    
  133.             membership_country_id=self.usa.id,
    
  134.             person_id=self.jim.id,
    
  135.             group_id=self.cia.id,
    
  136.             date_joined=timemark + timedelta,
    
  137.         )
    
  138. 
    
  139.         # Creating an invalid membership
    
  140.         Membership.objects.create(
    
  141.             membership_country_id=self.soviet_union.id,
    
  142.             person_id=self.george.id,
    
  143.             group_id=self.cia.id,
    
  144.             date_joined=timemark + timedelta,
    
  145.         )
    
  146. 
    
  147.         self.assertQuerysetEqual(
    
  148.             Person.objects.filter(membership__date_joined__gte=timemark),
    
  149.             ["Jim"],
    
  150.             attrgetter("name"),
    
  151.         )
    
  152. 
    
  153.     def test_forward_in_lookup_filters_correctly(self):
    
  154.         Membership.objects.create(
    
  155.             membership_country_id=self.usa.id,
    
  156.             person_id=self.bob.id,
    
  157.             group_id=self.cia.id,
    
  158.         )
    
  159.         Membership.objects.create(
    
  160.             membership_country_id=self.usa.id,
    
  161.             person_id=self.jim.id,
    
  162.             group_id=self.cia.id,
    
  163.         )
    
  164. 
    
  165.         # Creating an invalid membership
    
  166.         Membership.objects.create(
    
  167.             membership_country_id=self.soviet_union.id,
    
  168.             person_id=self.george.id,
    
  169.             group_id=self.cia.id,
    
  170.         )
    
  171. 
    
  172.         self.assertQuerysetEqual(
    
  173.             Membership.objects.filter(person__in=[self.george, self.jim]),
    
  174.             [
    
  175.                 self.jim.id,
    
  176.             ],
    
  177.             attrgetter("person_id"),
    
  178.         )
    
  179.         self.assertQuerysetEqual(
    
  180.             Membership.objects.filter(person__in=Person.objects.filter(name="Jim")),
    
  181.             [
    
  182.                 self.jim.id,
    
  183.             ],
    
  184.             attrgetter("person_id"),
    
  185.         )
    
  186. 
    
  187.     def test_double_nested_query(self):
    
  188.         m1 = Membership.objects.create(
    
  189.             membership_country_id=self.usa.id,
    
  190.             person_id=self.bob.id,
    
  191.             group_id=self.cia.id,
    
  192.         )
    
  193.         m2 = Membership.objects.create(
    
  194.             membership_country_id=self.usa.id,
    
  195.             person_id=self.jim.id,
    
  196.             group_id=self.cia.id,
    
  197.         )
    
  198.         Friendship.objects.create(
    
  199.             from_friend_country_id=self.usa.id,
    
  200.             from_friend_id=self.bob.id,
    
  201.             to_friend_country_id=self.usa.id,
    
  202.             to_friend_id=self.jim.id,
    
  203.         )
    
  204.         self.assertSequenceEqual(
    
  205.             Membership.objects.filter(
    
  206.                 person__in=Person.objects.filter(
    
  207.                     from_friend__in=Friendship.objects.filter(
    
  208.                         to_friend__in=Person.objects.all()
    
  209.                     )
    
  210.                 )
    
  211.             ),
    
  212.             [m1],
    
  213.         )
    
  214.         self.assertSequenceEqual(
    
  215.             Membership.objects.exclude(
    
  216.                 person__in=Person.objects.filter(
    
  217.                     from_friend__in=Friendship.objects.filter(
    
  218.                         to_friend__in=Person.objects.all()
    
  219.                     )
    
  220.                 )
    
  221.             ),
    
  222.             [m2],
    
  223.         )
    
  224. 
    
  225.     def test_select_related_foreignkey_forward_works(self):
    
  226.         Membership.objects.create(
    
  227.             membership_country=self.usa, person=self.bob, group=self.cia
    
  228.         )
    
  229.         Membership.objects.create(
    
  230.             membership_country=self.usa, person=self.jim, group=self.democrat
    
  231.         )
    
  232. 
    
  233.         with self.assertNumQueries(1):
    
  234.             people = [
    
  235.                 m.person
    
  236.                 for m in Membership.objects.select_related("person").order_by("pk")
    
  237.             ]
    
  238. 
    
  239.         normal_people = [m.person for m in Membership.objects.order_by("pk")]
    
  240.         self.assertEqual(people, normal_people)
    
  241. 
    
  242.     def test_prefetch_foreignkey_forward_works(self):
    
  243.         Membership.objects.create(
    
  244.             membership_country=self.usa, person=self.bob, group=self.cia
    
  245.         )
    
  246.         Membership.objects.create(
    
  247.             membership_country=self.usa, person=self.jim, group=self.democrat
    
  248.         )
    
  249. 
    
  250.         with self.assertNumQueries(2):
    
  251.             people = [
    
  252.                 m.person
    
  253.                 for m in Membership.objects.prefetch_related("person").order_by("pk")
    
  254.             ]
    
  255. 
    
  256.         normal_people = [m.person for m in Membership.objects.order_by("pk")]
    
  257.         self.assertEqual(people, normal_people)
    
  258. 
    
  259.     def test_prefetch_foreignkey_reverse_works(self):
    
  260.         Membership.objects.create(
    
  261.             membership_country=self.usa, person=self.bob, group=self.cia
    
  262.         )
    
  263.         Membership.objects.create(
    
  264.             membership_country=self.usa, person=self.jim, group=self.democrat
    
  265.         )
    
  266.         with self.assertNumQueries(2):
    
  267.             membership_sets = [
    
  268.                 list(p.membership_set.all())
    
  269.                 for p in Person.objects.prefetch_related("membership_set").order_by(
    
  270.                     "pk"
    
  271.                 )
    
  272.             ]
    
  273. 
    
  274.         with self.assertNumQueries(7):
    
  275.             normal_membership_sets = [
    
  276.                 list(p.membership_set.all()) for p in Person.objects.order_by("pk")
    
  277.             ]
    
  278.         self.assertEqual(membership_sets, normal_membership_sets)
    
  279. 
    
  280.     def test_m2m_through_forward_returns_valid_members(self):
    
  281.         # We start out by making sure that the Group 'CIA' has no members.
    
  282.         self.assertQuerysetEqual(self.cia.members.all(), [])
    
  283. 
    
  284.         Membership.objects.create(
    
  285.             membership_country=self.usa, person=self.bob, group=self.cia
    
  286.         )
    
  287.         Membership.objects.create(
    
  288.             membership_country=self.usa, person=self.jim, group=self.cia
    
  289.         )
    
  290. 
    
  291.         # Bob and Jim should be members of the CIA.
    
  292. 
    
  293.         self.assertQuerysetEqual(
    
  294.             self.cia.members.all(), ["Bob", "Jim"], attrgetter("name")
    
  295.         )
    
  296. 
    
  297.     def test_m2m_through_reverse_returns_valid_members(self):
    
  298.         # We start out by making sure that Bob is in no groups.
    
  299.         self.assertQuerysetEqual(self.bob.groups.all(), [])
    
  300. 
    
  301.         Membership.objects.create(
    
  302.             membership_country=self.usa, person=self.bob, group=self.cia
    
  303.         )
    
  304.         Membership.objects.create(
    
  305.             membership_country=self.usa, person=self.bob, group=self.republican
    
  306.         )
    
  307. 
    
  308.         # Bob should be in the CIA and a Republican
    
  309.         self.assertQuerysetEqual(
    
  310.             self.bob.groups.all(), ["CIA", "Republican"], attrgetter("name")
    
  311.         )
    
  312. 
    
  313.     def test_m2m_through_forward_ignores_invalid_members(self):
    
  314.         # We start out by making sure that the Group 'CIA' has no members.
    
  315.         self.assertQuerysetEqual(self.cia.members.all(), [])
    
  316. 
    
  317.         # Something adds jane to group CIA but Jane is in Soviet Union which
    
  318.         # isn't CIA's country.
    
  319.         Membership.objects.create(
    
  320.             membership_country=self.usa, person=self.jane, group=self.cia
    
  321.         )
    
  322. 
    
  323.         # There should still be no members in CIA
    
  324.         self.assertQuerysetEqual(self.cia.members.all(), [])
    
  325. 
    
  326.     def test_m2m_through_reverse_ignores_invalid_members(self):
    
  327.         # We start out by making sure that Jane has no groups.
    
  328.         self.assertQuerysetEqual(self.jane.groups.all(), [])
    
  329. 
    
  330.         # Something adds jane to group CIA but Jane is in Soviet Union which
    
  331.         # isn't CIA's country.
    
  332.         Membership.objects.create(
    
  333.             membership_country=self.usa, person=self.jane, group=self.cia
    
  334.         )
    
  335. 
    
  336.         # Jane should still not be in any groups
    
  337.         self.assertQuerysetEqual(self.jane.groups.all(), [])
    
  338. 
    
  339.     def test_m2m_through_on_self_works(self):
    
  340.         self.assertQuerysetEqual(self.jane.friends.all(), [])
    
  341. 
    
  342.         Friendship.objects.create(
    
  343.             from_friend_country=self.jane.person_country,
    
  344.             from_friend=self.jane,
    
  345.             to_friend_country=self.george.person_country,
    
  346.             to_friend=self.george,
    
  347.         )
    
  348. 
    
  349.         self.assertQuerysetEqual(
    
  350.             self.jane.friends.all(), ["George"], attrgetter("name")
    
  351.         )
    
  352. 
    
  353.     def test_m2m_through_on_self_ignores_mismatch_columns(self):
    
  354.         self.assertQuerysetEqual(self.jane.friends.all(), [])
    
  355. 
    
  356.         # Note that we use ids instead of instances. This is because instances
    
  357.         # on ForeignObject properties will set all related field off of the
    
  358.         # given instance.
    
  359.         Friendship.objects.create(
    
  360.             from_friend_id=self.jane.id,
    
  361.             to_friend_id=self.george.id,
    
  362.             to_friend_country_id=self.jane.person_country_id,
    
  363.             from_friend_country_id=self.george.person_country_id,
    
  364.         )
    
  365. 
    
  366.         self.assertQuerysetEqual(self.jane.friends.all(), [])
    
  367. 
    
  368.     def test_prefetch_related_m2m_forward_works(self):
    
  369.         Membership.objects.create(
    
  370.             membership_country=self.usa, person=self.bob, group=self.cia
    
  371.         )
    
  372.         Membership.objects.create(
    
  373.             membership_country=self.usa, person=self.jim, group=self.democrat
    
  374.         )
    
  375. 
    
  376.         with self.assertNumQueries(2):
    
  377.             members_lists = [
    
  378.                 list(g.members.all()) for g in Group.objects.prefetch_related("members")
    
  379.             ]
    
  380. 
    
  381.         normal_members_lists = [list(g.members.all()) for g in Group.objects.all()]
    
  382.         self.assertEqual(members_lists, normal_members_lists)
    
  383. 
    
  384.     def test_prefetch_related_m2m_reverse_works(self):
    
  385.         Membership.objects.create(
    
  386.             membership_country=self.usa, person=self.bob, group=self.cia
    
  387.         )
    
  388.         Membership.objects.create(
    
  389.             membership_country=self.usa, person=self.jim, group=self.democrat
    
  390.         )
    
  391. 
    
  392.         with self.assertNumQueries(2):
    
  393.             groups_lists = [
    
  394.                 list(p.groups.all()) for p in Person.objects.prefetch_related("groups")
    
  395.             ]
    
  396. 
    
  397.         normal_groups_lists = [list(p.groups.all()) for p in Person.objects.all()]
    
  398.         self.assertEqual(groups_lists, normal_groups_lists)
    
  399. 
    
  400.     @translation.override("fi")
    
  401.     def test_translations(self):
    
  402.         a1 = Article.objects.create(pub_date=datetime.date.today())
    
  403.         at1_fi = ArticleTranslation(
    
  404.             article=a1, lang="fi", title="Otsikko", body="Diipadaapa"
    
  405.         )
    
  406.         at1_fi.save()
    
  407.         at2_en = ArticleTranslation(
    
  408.             article=a1, lang="en", title="Title", body="Lalalalala"
    
  409.         )
    
  410.         at2_en.save()
    
  411. 
    
  412.         self.assertEqual(Article.objects.get(pk=a1.pk).active_translation, at1_fi)
    
  413. 
    
  414.         with self.assertNumQueries(1):
    
  415.             fetched = Article.objects.select_related("active_translation").get(
    
  416.                 active_translation__title="Otsikko"
    
  417.             )
    
  418.             self.assertEqual(fetched.active_translation.title, "Otsikko")
    
  419.         a2 = Article.objects.create(pub_date=datetime.date.today())
    
  420.         at2_fi = ArticleTranslation(
    
  421.             article=a2, lang="fi", title="Atsikko", body="Diipadaapa", abstract="dipad"
    
  422.         )
    
  423.         at2_fi.save()
    
  424.         a3 = Article.objects.create(pub_date=datetime.date.today())
    
  425.         at3_en = ArticleTranslation(
    
  426.             article=a3, lang="en", title="A title", body="lalalalala", abstract="lala"
    
  427.         )
    
  428.         at3_en.save()
    
  429.         # Test model initialization with active_translation field.
    
  430.         a3 = Article(id=a3.id, pub_date=a3.pub_date, active_translation=at3_en)
    
  431.         a3.save()
    
  432.         self.assertEqual(
    
  433.             list(Article.objects.filter(active_translation__abstract=None)), [a1, a3]
    
  434.         )
    
  435.         self.assertEqual(
    
  436.             list(
    
  437.                 Article.objects.filter(
    
  438.                     active_translation__abstract=None,
    
  439.                     active_translation__pk__isnull=False,
    
  440.                 )
    
  441.             ),
    
  442.             [a1],
    
  443.         )
    
  444. 
    
  445.         with translation.override("en"):
    
  446.             self.assertEqual(
    
  447.                 list(Article.objects.filter(active_translation__abstract=None)),
    
  448.                 [a1, a2],
    
  449.             )
    
  450. 
    
  451.     def test_foreign_key_raises_informative_does_not_exist(self):
    
  452.         referrer = ArticleTranslation()
    
  453.         with self.assertRaisesMessage(
    
  454.             Article.DoesNotExist, "ArticleTranslation has no article"
    
  455.         ):
    
  456.             referrer.article
    
  457. 
    
  458.     def test_foreign_key_related_query_name(self):
    
  459.         a1 = Article.objects.create(pub_date=datetime.date.today())
    
  460.         ArticleTag.objects.create(article=a1, name="foo")
    
  461.         self.assertEqual(Article.objects.filter(tag__name="foo").count(), 1)
    
  462.         self.assertEqual(Article.objects.filter(tag__name="bar").count(), 0)
    
  463.         msg = (
    
  464.             "Cannot resolve keyword 'tags' into field. Choices are: "
    
  465.             "active_translation, active_translation_q, articletranslation, "
    
  466.             "id, idea_things, newsarticle, pub_date, tag"
    
  467.         )
    
  468.         with self.assertRaisesMessage(FieldError, msg):
    
  469.             Article.objects.filter(tags__name="foo")
    
  470. 
    
  471.     def test_many_to_many_related_query_name(self):
    
  472.         a1 = Article.objects.create(pub_date=datetime.date.today())
    
  473.         i1 = ArticleIdea.objects.create(name="idea1")
    
  474.         a1.ideas.add(i1)
    
  475.         self.assertEqual(Article.objects.filter(idea_things__name="idea1").count(), 1)
    
  476.         self.assertEqual(Article.objects.filter(idea_things__name="idea2").count(), 0)
    
  477.         msg = (
    
  478.             "Cannot resolve keyword 'ideas' into field. Choices are: "
    
  479.             "active_translation, active_translation_q, articletranslation, "
    
  480.             "id, idea_things, newsarticle, pub_date, tag"
    
  481.         )
    
  482.         with self.assertRaisesMessage(FieldError, msg):
    
  483.             Article.objects.filter(ideas__name="idea1")
    
  484. 
    
  485.     @translation.override("fi")
    
  486.     def test_inheritance(self):
    
  487.         na = NewsArticle.objects.create(pub_date=datetime.date.today())
    
  488.         ArticleTranslation.objects.create(
    
  489.             article=na, lang="fi", title="foo", body="bar"
    
  490.         )
    
  491.         self.assertSequenceEqual(
    
  492.             NewsArticle.objects.select_related("active_translation"), [na]
    
  493.         )
    
  494.         with self.assertNumQueries(1):
    
  495.             self.assertEqual(
    
  496.                 NewsArticle.objects.select_related("active_translation")[
    
  497.                     0
    
  498.                 ].active_translation.title,
    
  499.                 "foo",
    
  500.             )
    
  501. 
    
  502.     @skipUnlessDBFeature("has_bulk_insert")
    
  503.     def test_batch_create_foreign_object(self):
    
  504.         objs = [
    
  505.             Person(name="abcd_%s" % i, person_country=self.usa) for i in range(0, 5)
    
  506.         ]
    
  507.         Person.objects.bulk_create(objs, 10)
    
  508. 
    
  509.     def test_isnull_lookup(self):
    
  510.         m1 = Membership.objects.create(
    
  511.             membership_country=self.usa, person=self.bob, group_id=None
    
  512.         )
    
  513.         m2 = Membership.objects.create(
    
  514.             membership_country=self.usa, person=self.bob, group=self.cia
    
  515.         )
    
  516.         self.assertSequenceEqual(
    
  517.             Membership.objects.filter(group__isnull=True),
    
  518.             [m1],
    
  519.         )
    
  520.         self.assertSequenceEqual(
    
  521.             Membership.objects.filter(group__isnull=False),
    
  522.             [m2],
    
  523.         )
    
  524. 
    
  525. 
    
  526. class TestModelCheckTests(SimpleTestCase):
    
  527.     @isolate_apps("foreign_object")
    
  528.     def test_check_composite_foreign_object(self):
    
  529.         class Parent(models.Model):
    
  530.             a = models.PositiveIntegerField()
    
  531.             b = models.PositiveIntegerField()
    
  532. 
    
  533.             class Meta:
    
  534.                 unique_together = (("a", "b"),)
    
  535. 
    
  536.         class Child(models.Model):
    
  537.             a = models.PositiveIntegerField()
    
  538.             b = models.PositiveIntegerField()
    
  539.             value = models.CharField(max_length=255)
    
  540.             parent = models.ForeignObject(
    
  541.                 Parent,
    
  542.                 on_delete=models.SET_NULL,
    
  543.                 from_fields=("a", "b"),
    
  544.                 to_fields=("a", "b"),
    
  545.                 related_name="children",
    
  546.             )
    
  547. 
    
  548.         self.assertEqual(Child._meta.get_field("parent").check(from_model=Child), [])
    
  549. 
    
  550.     @isolate_apps("foreign_object")
    
  551.     def test_check_subset_composite_foreign_object(self):
    
  552.         class Parent(models.Model):
    
  553.             a = models.PositiveIntegerField()
    
  554.             b = models.PositiveIntegerField()
    
  555.             c = models.PositiveIntegerField()
    
  556. 
    
  557.             class Meta:
    
  558.                 unique_together = (("a", "b"),)
    
  559. 
    
  560.         class Child(models.Model):
    
  561.             a = models.PositiveIntegerField()
    
  562.             b = models.PositiveIntegerField()
    
  563.             c = models.PositiveIntegerField()
    
  564.             d = models.CharField(max_length=255)
    
  565.             parent = models.ForeignObject(
    
  566.                 Parent,
    
  567.                 on_delete=models.SET_NULL,
    
  568.                 from_fields=("a", "b", "c"),
    
  569.                 to_fields=("a", "b", "c"),
    
  570.                 related_name="children",
    
  571.             )
    
  572. 
    
  573.         self.assertEqual(Child._meta.get_field("parent").check(from_model=Child), [])
    
  574. 
    
  575. 
    
  576. class TestExtraJoinFilterQ(TestCase):
    
  577.     @translation.override("fi")
    
  578.     def test_extra_join_filter_q(self):
    
  579.         a = Article.objects.create(pub_date=datetime.datetime.today())
    
  580.         ArticleTranslation.objects.create(
    
  581.             article=a, lang="fi", title="title", body="body"
    
  582.         )
    
  583.         qs = Article.objects.all()
    
  584.         with self.assertNumQueries(2):
    
  585.             self.assertEqual(qs[0].active_translation_q.title, "title")
    
  586.         qs = qs.select_related("active_translation_q")
    
  587.         with self.assertNumQueries(1):
    
  588.             self.assertEqual(qs[0].active_translation_q.title, "title")
    
  589. 
    
  590. 
    
  591. class TestCachedPathInfo(TestCase):
    
  592.     def test_equality(self):
    
  593.         """
    
  594.         The path_infos and reverse_path_infos attributes are equivalent to
    
  595.         calling the get_<method>() with no arguments.
    
  596.         """
    
  597.         foreign_object = Membership._meta.get_field("person")
    
  598.         self.assertEqual(
    
  599.             foreign_object.path_infos,
    
  600.             foreign_object.get_path_info(),
    
  601.         )
    
  602.         self.assertEqual(
    
  603.             foreign_object.reverse_path_infos,
    
  604.             foreign_object.get_reverse_path_info(),
    
  605.         )
    
  606. 
    
  607.     def test_copy_removes_direct_cached_values(self):
    
  608.         """
    
  609.         Shallow copying a ForeignObject (or a ForeignObjectRel) removes the
    
  610.         object's direct cached PathInfo values.
    
  611.         """
    
  612.         foreign_object = Membership._meta.get_field("person")
    
  613.         # Trigger storage of cached_property into ForeignObject's __dict__.
    
  614.         foreign_object.path_infos
    
  615.         foreign_object.reverse_path_infos
    
  616.         # The ForeignObjectRel doesn't have reverse_path_infos.
    
  617.         foreign_object.remote_field.path_infos
    
  618.         self.assertIn("path_infos", foreign_object.__dict__)
    
  619.         self.assertIn("reverse_path_infos", foreign_object.__dict__)
    
  620.         self.assertIn("path_infos", foreign_object.remote_field.__dict__)
    
  621.         # Cached value is removed via __getstate__() on ForeignObjectRel
    
  622.         # because no __copy__() method exists, so __reduce_ex__() is used.
    
  623.         remote_field_copy = copy.copy(foreign_object.remote_field)
    
  624.         self.assertNotIn("path_infos", remote_field_copy.__dict__)
    
  625.         # Cached values are removed via __copy__() on ForeignObject for
    
  626.         # consistency of behavior.
    
  627.         foreign_object_copy = copy.copy(foreign_object)
    
  628.         self.assertNotIn("path_infos", foreign_object_copy.__dict__)
    
  629.         self.assertNotIn("reverse_path_infos", foreign_object_copy.__dict__)
    
  630.         # ForeignObjectRel's remains because it's part of a shallow copy.
    
  631.         self.assertIn("path_infos", foreign_object_copy.remote_field.__dict__)
    
  632. 
    
  633.     def test_deepcopy_removes_cached_values(self):
    
  634.         """
    
  635.         Deep copying a ForeignObject removes the object's cached PathInfo
    
  636.         values, including those of the related ForeignObjectRel.
    
  637.         """
    
  638.         foreign_object = Membership._meta.get_field("person")
    
  639.         # Trigger storage of cached_property into ForeignObject's __dict__.
    
  640.         foreign_object.path_infos
    
  641.         foreign_object.reverse_path_infos
    
  642.         # The ForeignObjectRel doesn't have reverse_path_infos.
    
  643.         foreign_object.remote_field.path_infos
    
  644.         self.assertIn("path_infos", foreign_object.__dict__)
    
  645.         self.assertIn("reverse_path_infos", foreign_object.__dict__)
    
  646.         self.assertIn("path_infos", foreign_object.remote_field.__dict__)
    
  647.         # Cached value is removed via __getstate__() on ForeignObjectRel
    
  648.         # because no __deepcopy__() method exists, so __reduce_ex__() is used.
    
  649.         remote_field_copy = copy.deepcopy(foreign_object.remote_field)
    
  650.         self.assertNotIn("path_infos", remote_field_copy.__dict__)
    
  651.         # Field.__deepcopy__() internally uses __copy__() on both the
    
  652.         # ForeignObject and ForeignObjectRel, so all cached values are removed.
    
  653.         foreign_object_copy = copy.deepcopy(foreign_object)
    
  654.         self.assertNotIn("path_infos", foreign_object_copy.__dict__)
    
  655.         self.assertNotIn("reverse_path_infos", foreign_object_copy.__dict__)
    
  656.         self.assertNotIn("path_infos", foreign_object_copy.remote_field.__dict__)
    
  657. 
    
  658.     def test_pickling_foreignobjectrel(self):
    
  659.         """
    
  660.         Pickling a ForeignObjectRel removes the path_infos attribute.
    
  661. 
    
  662.         ForeignObjectRel implements __getstate__(), so copy and pickle modules
    
  663.         both use that, but ForeignObject implements __reduce__() and __copy__()
    
  664.         separately, so doesn't share the same behaviour.
    
  665.         """
    
  666.         foreign_object_rel = Membership._meta.get_field("person").remote_field
    
  667.         # Trigger storage of cached_property into ForeignObjectRel's __dict__.
    
  668.         foreign_object_rel.path_infos
    
  669.         self.assertIn("path_infos", foreign_object_rel.__dict__)
    
  670.         foreign_object_rel_restored = pickle.loads(pickle.dumps(foreign_object_rel))
    
  671.         self.assertNotIn("path_infos", foreign_object_rel_restored.__dict__)
    
  672. 
    
  673.     def test_pickling_foreignobject(self):
    
  674.         """
    
  675.         Pickling a ForeignObject does not remove the cached PathInfo values.
    
  676. 
    
  677.         ForeignObject will always keep the path_infos and reverse_path_infos
    
  678.         attributes within the same process, because of the way
    
  679.         Field.__reduce__() is used for restoring values.
    
  680.         """
    
  681.         foreign_object = Membership._meta.get_field("person")
    
  682.         # Trigger storage of cached_property into ForeignObjectRel's __dict__
    
  683.         foreign_object.path_infos
    
  684.         foreign_object.reverse_path_infos
    
  685.         self.assertIn("path_infos", foreign_object.__dict__)
    
  686.         self.assertIn("reverse_path_infos", foreign_object.__dict__)
    
  687.         foreign_object_restored = pickle.loads(pickle.dumps(foreign_object))
    
  688.         self.assertIn("path_infos", foreign_object_restored.__dict__)
    
  689.         self.assertIn("reverse_path_infos", foreign_object_restored.__dict__)