pythondjangodjango-modelswagtail

Wagtail one-to-one object hierarchy with enum type field


I have a wagtail site and I want to create an object hierarchy in a one-to-one matter, but with multiple options. Basically, I want that the database setup look slike this:

CREATE TABLE products (
    id PRIMARY KEY,
    product_type VARCHAR(10) CHECK (product_type IN ('BOOK', 'SHIRT', ...)),
    product_name VARCHAR(255),
    product_description TEXT,
    ...
);
CREATE TABLE product_shirts (
    id PRIMARY KEY,
    product_id integer REFERENCES products (id),
    size varchar(255),
    ...
);
CREATE TABLE product_books (
    id PRIMARY KEY,
    product_id integer REFERENCES products (id),
    author varchar(255),
    ...
);

It is pretty straigt forward to create a regular one-to-one relationship with setting ParentalKey in the derived model. However, I want to also have an enum-type field in the parent model to check which product type we have, so that I can do something like that in my ProductsView:

if object.product_type == 'SHIRT':
    # display additional shirt attributes
elif object.product_type == 'BOOK':
    # display book attributes
else:
    # unknown type, should not happen

I know, that with a one-to-one relationship in wagtail I could just simply call product.shirtwhich would raise an exception, if the product is not a shirt. But it seems very cumbersome to have nested try-catch blocks if I have many different product types...

Any better idea to solve this in a django/wagtail style?


Solution

  • django allows multi-table inheritance, meaning each subclass gets its own table while maintaining a one-to-one relationship with the parent model. You can also use the ContentType framework to link child models dynamically

    first of you make a product model.

    from django.db import models
    from django.contrib.contenttypes.models import ContentType
    from django.contrib.contenttypes.fields import GenericForeignKey
    
    class Product(models.Model):
        PRODUCT_TYPES = [
            ('BOOK', 'Book'),
            ('SHIRT', 'Shirt'),
            #add as per your requirements
        ]
    
        product_name = models.CharField(max_length=255)
        product_description = models.TextField()
        product_type = models.CharField(max_length=10, choices=PRODUCT_TYPES)
        
        # use the framework to form the relation
        content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
        object_id = models.PositiveIntegerField(null=True, blank=True)
        specific_product = GenericForeignKey('content_type', 'object_id')
    
        def get_specific_product(self):
           # it returns the specific child product model instance
            if self.specific_product:
                return self.specific_product
            return self
    
        def __str__(self):
            return f"{self.product_name} ({self.product_type})"
    

    now each product type extends Product using django’s multi-table inheritance each child model will have a one-to-one relationship with Product.

    class Book(Product):
        author = models.CharField(max_length=255)
    
        def save(self, *args, **kwargs):
            if not self.pk:
                self.product_type = 'BOOK'  # Ensure type is set automatically
            super().save(*args, **kwargs)
    
    class Shirt(Product):
        size = models.CharField(max_length=50)
    
        def save(self, *args, **kwargs):
            if not self.pk:
                self.product_type = 'SHIRT'
            super().save(*args, **kwargs)
    

    Override the save method to ensure that content_type is automatically set.

    def save(self, *args, **kwargs):
        if not self.content_type:
            self.content_type = ContentType.objects.get_for_model(self)
        super().save(*args, **kwargs)
    

    retrive the data instances:

    
    product = Product.objects.get(id=some_id)
    specific_product = product.get_specific_product()
    

    Hope it helps!