Django Foreign Key To Any Subclass of Abstract Model

142 Views Asked by At

I have an abstract Django model with two concrete implementations. E.g.

class Animal(models.Model):
    name = models.CharField("name", max_length=100)

    class Meta:
        abstract = True


class Dog(Animal):
    ...


class Cat(Animal):
   ...

And then I want to create a generic foreign key that points to a Dog or a Cat. From the documentation about GenericForeignKeys I know that I can do something like

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models

class Appointment(models.Model):
    
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    patient = GenericForeignKey("content_type", "object_id")

But

  1. I have to do this for every model that can point to a Dog or a Cat.
  2. It doesn't enforce that the foreign key actual points to an Animal.

Is there a way to set up my Animal model such that in the Appointment model I can just do

class Apointment(models.Model):
   patient = GenericRelation(Animal)
1

There are 1 best solutions below

0
Mohammad Golam Dostogir On

To address your requirement of creating a foreign key in Django that points specifically to subclasses of an abstract Animal model (like Dog or Cat in your case), without having to repeat the setup for every model and ensuring that the foreign key indeed points to an Animal subclass, you can create a custom Model field. This custom field for example AnimalGenericForeignKey, would be a specialized version of Django's GenericForeignKey.

This custom field would internally use the mechanisms of GenericForeignKey but with added logic to ensure the relationship is only with Animal subclasses. You need to define AnimalGenericForeignKey once and then use it in any model where you need to reference an Animal subclass. When you declare this field in a model like Appointment, it would manage the relationship and enforce that the linked object is a Dog or a Cat, or any other subclass of Animal class you have.

Keep your Animal, Dog, Cat model class as it is.

As said earlier create a new Custom Field I name it as AnimalGenericForeignKey:

from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError

class AnimalGenericForeignKey(GenericForeignKey):
    def validate(self, value, model_instance):
        super().validate(value, model_instance)
        if not issubclass(value.__class__, Animal):
            raise ValidationError("This field must be set to an Animal subclass instance.")

This custom field will extend Django's GenericForeignKey and include validation to ensure it only points to an Animal subclass.

Use this custom fields in anywhere you want in your case let's use this in Appointment model class.

from django.contrib.contenttypes.models import ContentType
class Appointment(models.Model):
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, limit_choices_to={'model__in': ('dog', 'cat')})
    object_id = models.PositiveIntegerField()
    patient = AnimalGenericForeignKey('content_type', 'object_id')

    #Do what you need..... 

Ref. Model ForeignKey limit choices

I hope this will help. In case any questions feel free to ask in comment.