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.shirt
which 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?
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!