genericrelations on inherited classes use the correct content type the correct column name is used for the primary key on the originating model of a query see 12664 if the model cafe uses more than one level of multitable inheritance sql query parameters for generic relations are properly grouped when or is used 11535 in this bug the first query below works while the second with the query parameters the same but in reverse order does not the issue is that the generic relation conditions do not get properly grouped in parentheses search with a nonmatching note and a matching org name search again with the same query parameters in reverse order ordering over a generic relation does not include extraneous duplicate results nor excludes rows not participating in the relation saving a model with a genericforeignkey to a model instance whose len method returns 0 team len here shouldn t fail 13085 saving a model with a genericforeignkey to a model instance whose bool method returns false guild bool here shouldn t fail 13085 test related to 13085 saving model with genericforeignkey to model instance with an empty charfield pk b1 board objects createname tag taglabelvp contentobjectb1 tag save def testticket20378self create a couple of extra haslinkthing so that the autopk value isn t the same for link and haslinkthing hs1 haslinkthing objects create hs2 haslinkthing objects create hs3 haslinkthing objects create hs4 haslinkthing objects create l1 link objects createcontentobjecths3 l2 link objects createcontentobjecths4 self assertsequenceequalhaslinkthing objects filterlinksl1 hs3 self assertsequenceequalhaslinkthing objects filterlinksl2 hs4 self assertsequenceequal haslinkthing objects excludelinksl2 hs1 hs2 hs3 self assertsequenceequal haslinkthing objects excludelinksl1 hs1 hs2 hs4 def testticket20564self b1 b objects create b2 b objects create b3 b objects create c1 c objects createbb1 c2 c objects createbb2 c3 c objects createbb3 a objects createflagnone contentobjectb1 a objects createflagtrue contentobjectb2 self assertsequenceequalc objects filterbaflagnone c1 c3 self assertsequenceequalc objects excludebaflagnone c2 def testticket20564nullablefkself b1 b objects create b2 b objects create b3 b objects create d1 d objects createbb1 d2 d objects createbb2 d3 d objects createbb3 d4 d objects create a objects createflagnone contentobjectb1 a objects createflagtrue contentobjectb1 a objects createflagtrue contentobjectb2 self assertsequenceequald objects excludebaflagnone d2 self assertsequenceequald objects filterbaflagnone d1 d3 d4 self assertsequenceequalb objects filteraflagnone b1 b3 self assertsequenceequalb objects excludeaflagnone b2 def testextrajoinconditionself a crude check that contenttypeid is taken in account in the joinsubquery condition self assertin contenttypeid strb objects excludeaflagnone query lower no need for any joins the join from inner query can be trimmed in this case but not in the above case as no a objects at all for given b would then fail self assertnotin join strb objects excludeaflagtrue query lower self assertin contenttypeid strb objects excludeaflagtrue query lower def testannotateself hs1 haslinkthing objects create hs2 haslinkthing objects create haslinkthing objects create b board objects createnamestrhs1 pk link objects createcontentobjecths2 link link objects createcontentobjecths1 link objects createcontentobjectb qs haslinkthing objects annotatesumlinks filterpkhs1 pk if contenttype restriction isn t in the query s join condition then wrong results are produced here as the link to b will also match b and hs1 have equal pks self assertequalqs count 1 self assertequalqs0 linkssum link id link delete now if we don t have proper left join we will not produce any results at all here clear cached results qs qs all self assertequalqs count 1 note 0 here would be a nicer result self assertisqs0 linkssum none finally test that filtering works self assertequalqs filterlinkssumisnulltrue count 1 self assertequalqs filterlinkssumisnullfalse count 0 def testfiltertargetsrelatedpkself use hardcoded pks to ensure different pks for link and hs2 objects haslinkthing objects createpk1 hs2 haslinkthing objects createpk2 link link objects createcontentobjecths2 pk1 self assertnotequallink objectid link pk self assertsequenceequalhaslinkthing objects filterlinkslink pk hs2 def testeditablegenericrelself genericrelationform modelformfactoryhaslinkthing fieldsall form genericrelationform self assertinlinks form fields form genericrelationformlinks none self asserttrueform isvalid form save links haslinkthing meta getfieldlinks self assertequallinks saveformdatacalls 1 def testticket22998self related related objects create content content objects createrelatedobjrelated node objects createcontentcontent deleting the related cascades to the content cascades to the node where the predelete signal should fire and prevent deletion with self assertraisesprotectederror related delete def testticket22982self place place objects createnamemy place self assertingenericrelatedobjectmanager strplace links def testfilteronrelatedproxymodelself place place objects create link objects createcontentobjectplace self assertequalplace objects getlinkproxyobjectidplace id place def testgenericreverserelationwithmtiself place place objects createnametest place link link objects createcontentobjectplace result link objects filterplacesplace self assertcountequalresult link def testgenericreverserelationwithabcself thing haslinkthing objects create link link objects createcontentobjectthing self assertcountequallink targets all thing def testgenericreverserelationexcludefilterself place1 place objects createnametest place 1 place2 place objects createnametest place 2 link objects createcontentobjectplace1 link2 link objects createcontentobjectplace2 qs link objects filterqplacesnametest place 1 self assertsequenceequalqs link2 qs link objects excludeplacesnametest place 1 self assertsequenceequalqs link2 def testcheckcachedvaluepkdifferenttypeself genericrelations on inherited classes use the correct content type the correct column name is used for the primary key on the originating model of a query see 12664 if the model cafe uses more than one level of multi table inheritance sql query parameters for generic relations are properly grouped when or is used 11535 in this bug the first query below works while the second with the query parameters the same but in reverse order does not the issue is that the generic relation conditions do not get properly grouped in parentheses search with a non matching note and a matching org name search again with the same query parameters in reverse order ordering over a generic relation does not include extraneous duplicate results nor excludes rows not participating in the relation saving a model with a genericforeignkey to a model instance whose __len__ method returns 0 team __len__ here shouldn t fail 13085 saving a model with a genericforeignkey to a model instance whose __bool__ method returns false guild __bool__ here shouldn t fail 13085 test related to 13085 saving model with genericforeignkey to model instance with an empty charfield pk create a couple of extra haslinkthing so that the autopk value isn t the same for link and haslinkthing a crude check that content_type_id is taken in account in the join subquery condition no need for any joins the join from inner query can be trimmed in this case but not in the above case as no a objects at all for given b would then fail if content_type restriction isn t in the query s join condition then wrong results are produced here as the link to b will also match b and hs1 have equal pks now if we don t have proper left join we will not produce any results at all here clear cached results note 0 here would be a nicer result finally test that filtering works use hardcoded pks to ensure different pks for link and hs2 objects deleting the related cascades to the content cascades to the node where the pre_delete signal should fire and prevent deletion filtering with a reverse generic relation where the genericrelation comes from multi table inheritance the reverse generic relation accessor targets is created if the genericrelation comes from an abstract base model haslinks primary key is not checked if the content type doesn t match
from django.contrib.contenttypes.models import ContentType
from django.db.models import ProtectedError, Q, Sum
from django.forms.models import modelform_factory
from django.test import TestCase, skipIfDBFeature

from .models import (
    A,
    Address,
    B,
    Board,
    C,
    Cafe,
    CharLink,
    Company,
    Contact,
    Content,
    D,
    Guild,
    HasLinkThing,
    Link,
    Node,
    Note,
    OddRelation1,
    OddRelation2,
    Organization,
    Person,
    Place,
    Related,
    Restaurant,
    Tag,
    Team,
    TextLink,
)


class GenericRelationTests(TestCase):
    def test_inherited_models_content_type(self):
        
        p = Place.objects.create(name="South Park")
        r = Restaurant.objects.create(name="Chubby's")
        l1 = Link.objects.create(content_object=p)
        l2 = Link.objects.create(content_object=r)
        self.assertEqual(list(p.links.all()), [l1])
        self.assertEqual(list(r.links.all()), [l2])

    def test_reverse_relation_pk(self):
        
        p = Person.objects.create(account=23, name="Chef")
        Address.objects.create(
            street="123 Anywhere Place",
            city="Conifer",
            state="CO",
            zipcode="80433",
            content_object=p,
        )

        qs = Person.objects.filter(addresses__zipcode="80433")
        self.assertEqual(1, qs.count())
        self.assertEqual("Chef", qs[0].name)

    def test_charlink_delete(self):
        oddrel = OddRelation1.objects.create(name="clink")
        CharLink.objects.create(content_object=oddrel)
        oddrel.delete()

    def test_textlink_delete(self):
        oddrel = OddRelation2.objects.create(name="tlink")
        TextLink.objects.create(content_object=oddrel)
        oddrel.delete()

    def test_charlink_filter(self):
        oddrel = OddRelation1.objects.create(name="clink")
        CharLink.objects.create(content_object=oddrel, value="value")
        self.assertSequenceEqual(
            OddRelation1.objects.filter(clinks__value="value"), [oddrel]
        )

    def test_textlink_filter(self):
        oddrel = OddRelation2.objects.create(name="clink")
        TextLink.objects.create(content_object=oddrel, value="value")
        self.assertSequenceEqual(
            OddRelation2.objects.filter(tlinks__value="value"), [oddrel]
        )

    def test_coerce_object_id_remote_field_cache_persistence(self):
        restaurant = Restaurant.objects.create()
        CharLink.objects.create(content_object=restaurant)
        charlink = CharLink.objects.latest("pk")
        self.assertIs(charlink.content_object, charlink.content_object)
        
        cafe = Cafe.objects.create()
        CharLink.objects.create(content_object=cafe)
        charlink = CharLink.objects.latest("pk")
        self.assertIs(charlink.content_object, charlink.content_object)

    def test_q_object_or(self):
        
        note_contact = Contact.objects.create()
        org_contact = Contact.objects.create()
        Note.objects.create(note="note", content_object=note_contact)
        org = Organization.objects.create(name="org name")
        org.contacts.add(org_contact)
        
        qs = Contact.objects.filter(
            Q(notes__note__icontains=r"other note")
            | Q(organizations__name__icontains=r"org name")
        )
        self.assertIn(org_contact, qs)
        
        qs = Contact.objects.filter(
            Q(organizations__name__icontains=r"org name")
            | Q(notes__note__icontains=r"other note")
        )
        self.assertIn(org_contact, qs)

    def test_join_reuse(self):
        qs = Person.objects.filter(addresses__street="foo").filter(
            addresses__street="bar"
        )
        self.assertEqual(str(qs.query).count("JOIN"), 2)

    def test_generic_relation_ordering(self):
        
        p1 = Place.objects.create(name="South Park")
        p2 = Place.objects.create(name="The City")
        c = Company.objects.create(name="Chubby's Intl.")
        Link.objects.create(content_object=p1)
        Link.objects.create(content_object=c)

        places = list(Place.objects.order_by("links__id"))

        def count_places(place):
            return len([p for p in places if p.id == place.id])

        self.assertEqual(len(places), 2)
        self.assertEqual(count_places(p1), 1)
        self.assertEqual(count_places(p2), 1)

    def test_target_model_len_zero(self):
        
        team1 = Team.objects.create(name="Backend devs")
        note = Note(note="Deserve a bonus", content_object=team1)
        note.save()

    def test_target_model_bool_false(self):
        
        g1 = Guild.objects.create(name="First guild")
        note = Note(note="Note for guild", content_object=g1)
        note.save()

    @skipIfDBFeature("interprets_empty_strings_as_nulls")
    def test_gfk_to_model_with_empty_pk(self):
        
        
        
        b1 = Board.objects.create(name="")
        tag = Tag(label="VP", content_object=b1)
        tag.save()

    def test_ticket_20378(self):
        
        
        hs1 = HasLinkThing.objects.create()
        hs2 = HasLinkThing.objects.create()
        hs3 = HasLinkThing.objects.create()
        hs4 = HasLinkThing.objects.create()
        l1 = Link.objects.create(content_object=hs3)
        l2 = Link.objects.create(content_object=hs4)
        self.assertSequenceEqual(HasLinkThing.objects.filter(links=l1), [hs3])
        self.assertSequenceEqual(HasLinkThing.objects.filter(links=l2), [hs4])
        self.assertSequenceEqual(
            HasLinkThing.objects.exclude(links=l2), [hs1, hs2, hs3]
        )
        self.assertSequenceEqual(
            HasLinkThing.objects.exclude(links=l1), [hs1, hs2, hs4]
        )

    def test_ticket_20564(self):
        b1 = B.objects.create()
        b2 = B.objects.create()
        b3 = B.objects.create()
        c1 = C.objects.create(b=b1)
        c2 = C.objects.create(b=b2)
        c3 = C.objects.create(b=b3)
        A.objects.create(flag=None, content_object=b1)
        A.objects.create(flag=True, content_object=b2)
        self.assertSequenceEqual(C.objects.filter(b__a__flag=None), [c1, c3])
        self.assertSequenceEqual(C.objects.exclude(b__a__flag=None), [c2])

    def test_ticket_20564_nullable_fk(self):
        b1 = B.objects.create()
        b2 = B.objects.create()
        b3 = B.objects.create()
        d1 = D.objects.create(b=b1)
        d2 = D.objects.create(b=b2)
        d3 = D.objects.create(b=b3)
        d4 = D.objects.create()
        A.objects.create(flag=None, content_object=b1)
        A.objects.create(flag=True, content_object=b1)
        A.objects.create(flag=True, content_object=b2)
        self.assertSequenceEqual(D.objects.exclude(b__a__flag=None), [d2])
        self.assertSequenceEqual(D.objects.filter(b__a__flag=None), [d1, d3, d4])
        self.assertSequenceEqual(B.objects.filter(a__flag=None), [b1, b3])
        self.assertSequenceEqual(B.objects.exclude(a__flag=None), [b2])

    def test_extra_join_condition(self):
        
        
        self.assertIn(
            "content_type_id", str(B.objects.exclude(a__flag=None).query).lower()
        )
        
        
        
        self.assertNotIn(" join ", str(B.objects.exclude(a__flag=True).query).lower())
        self.assertIn(
            "content_type_id", str(B.objects.exclude(a__flag=True).query).lower()
        )

    def test_annotate(self):
        hs1 = HasLinkThing.objects.create()
        hs2 = HasLinkThing.objects.create()
        HasLinkThing.objects.create()
        b = Board.objects.create(name=str(hs1.pk))
        Link.objects.create(content_object=hs2)
        link = Link.objects.create(content_object=hs1)
        Link.objects.create(content_object=b)
        qs = HasLinkThing.objects.annotate(Sum("links")).filter(pk=hs1.pk)
        
        
        
        self.assertEqual(qs.count(), 1)
        self.assertEqual(qs[0].links__sum, link.id)
        link.delete()
        
        
        
        qs = qs.all()
        self.assertEqual(qs.count(), 1)
        
        self.assertIs(qs[0].links__sum, None)
        
        self.assertEqual(qs.filter(links__sum__isnull=True).count(), 1)
        self.assertEqual(qs.filter(links__sum__isnull=False).count(), 0)

    def test_filter_targets_related_pk(self):
        
        
        HasLinkThing.objects.create(pk=1)
        hs2 = HasLinkThing.objects.create(pk=2)
        link = Link.objects.create(content_object=hs2, pk=1)
        self.assertNotEqual(link.object_id, link.pk)
        self.assertSequenceEqual(HasLinkThing.objects.filter(links=link.pk), [hs2])

    def test_editable_generic_rel(self):
        GenericRelationForm = modelform_factory(HasLinkThing, fields="__all__")
        form = GenericRelationForm()
        self.assertIn("links", form.fields)
        form = GenericRelationForm({"links": None})
        self.assertTrue(form.is_valid())
        form.save()
        links = HasLinkThing._meta.get_field("links")
        self.assertEqual(links.save_form_data_calls, 1)

    def test_ticket_22998(self):
        related = Related.objects.create()
        content = Content.objects.create(related_obj=related)
        Node.objects.create(content=content)

        
        
        with self.assertRaises(ProtectedError):
            related.delete()

    def test_ticket_22982(self):
        place = Place.objects.create(name="My Place")
        self.assertIn("GenericRelatedObjectManager", str(place.links))

    def test_filter_on_related_proxy_model(self):
        place = Place.objects.create()
        Link.objects.create(content_object=place)
        self.assertEqual(Place.objects.get(link_proxy__object_id=place.id), place)

    def test_generic_reverse_relation_with_mti(self):
        
        place = Place.objects.create(name="Test Place")
        link = Link.objects.create(content_object=place)
        result = Link.objects.filter(places=place)
        self.assertCountEqual(result, [link])

    def test_generic_reverse_relation_with_abc(self):
        
        thing = HasLinkThing.objects.create()
        link = Link.objects.create(content_object=thing)
        self.assertCountEqual(link.targets.all(), [thing])

    def test_generic_reverse_relation_exclude_filter(self):
        place1 = Place.objects.create(name="Test Place 1")
        place2 = Place.objects.create(name="Test Place 2")
        Link.objects.create(content_object=place1)
        link2 = Link.objects.create(content_object=place2)
        qs = Link.objects.filter(~Q(places__name="Test Place 1"))
        self.assertSequenceEqual(qs, [link2])
        qs = Link.objects.exclude(places__name="Test Place 1")
        self.assertSequenceEqual(qs, [link2])

    def test_check_cached_value_pk_different_type(self):
        
        board = Board.objects.create(name="some test")
        oddrel = OddRelation1.objects.create(name="clink")
        charlink = CharLink.objects.create(content_object=oddrel)
        charlink = CharLink.objects.get(pk=charlink.pk)
        self.assertEqual(charlink.content_object, oddrel)
        charlink.object_id = board.pk
        charlink.content_type_id = ContentType.objects.get_for_model(Board).id
        self.assertEqual(charlink.content_object, board)