Eliminate boredom and reduce tension with Abstract models in Django

Eliminate boredom and reduce tension with Abstract models in Django

In this article, I will show you how to leverage inheritance in your Django models to ensure elegant, clean and neat code.

Working with Django, we get lost in love, wonder and praise as we discover neat stuffs we can work with, one of such being abstract models.

I've come to use UUIDs in all my django models by default. With time, I've wanted to make things easier for myself and try to leverage abstraction in OOP to ensure my models could automatically have uuid fields without having to explicitly specify them.

As usual, we'll begin with a problem:

The problem

Isaac from my previous article who as usual loves shady deals has been leveraged with a small project from LEFTR (LEague of Fundamental Thieves and Robbers). They want a managed record of their members who have been caught by the law and the police agent involved in bringing them to book. They call the project, Ole.

Isaac then churned the following elegant code in the models.py of Ole project:

import uuid

from django.db import models
from django.template.defaultfilters import slugify

class Police(models.Model):
    uuid = models.UUIDField(default=uuid.uuid4)
    first_name = models.CharField(max_length=32)
    last_name = models.CharField(max_length=32)
    nickname = models.CharField(max_length=32)
    avatar = models.ImageField(upload_to='police')
    slug = models.Slugfield(blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.nickname

    def save(self, *args, **kwargs):
        identifier = f"{self.first_name} {self.last_name}"
        self.slug = slugify(identifier)
        return super().save(*args, **kwargs)


class Thief(models.Model):
    uuid = models.UUIDField(default=uuid.uuid4)
    first_name = models.CharField(max_length=32)
    last_name = models.CharField(max_length=32)
    street_name = models.CharField(max_length=32)
    avatar = models.ImageField(upload_to='thief')
    slug = models.Slugfield(blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.street_name

    def save(self, *args, **kwargs):
        identifier = f"{self.first_name} {self.last_name}"
        self.slug = slugify(identifier)
        return super().save(*args, **kwargs)

That wasn't too bad was it?

The issue

Isaac feels this code should be improved since it's his first real project. In addition, he might never make it outside the room he'll present the code in, to the thieves. So, he's particular about not dying while decrepit code stays on his Github account.

The actual issue

As you can see, there are lots of fields that both Thief and Police share. Normally, under OOP circumstances, you'd be reeling with "Why isn't Isaac doing some inheritance here?". Thing is, we're dealing with databases here and this is Django and not largely-unopinionated frameworks like Express or Flask where you can likely do what you want. This means you've got to do it in an intuitive and Django-friendly manner.

How to do?

Here comes abstract models. What we'll do is to extract common functionality into a model and tag it as abstract, meaning it is not concrete enough to be a table in the database but it's solid enough to be a structure for other tables.

So, Isaac defines a BaseModel like so:

class BaseModel(models.Model):
    uuid = models.UUIDField(default=uuid.uuid4)
    first_name = models.CharField(max_length=32)
    last_name = models.CharField(max_length=32)
    slug = models.Slugfield(blank=True)
    timestamp = models.DateTimeField(auto_now_add=True)

    def save(self, *args, **kwargs):
        identifier = f"{self.first_name} {self.last_name}"
        self.slug = slugify(identifier)
        return super().save(*args, **kwargs)

    class Meta:
        abstract = True

And then reworks the other models to look like so:

class Police(BaseModel):
    nickname = models.CharField(max_length=32)
    avatar = models.ImageField(upload_to='police')

    def __str__(self):
        return self.nickname


class Thief(BaseModel):
    street_name = models.CharField(max_length=32)
    avatar = models.ImageField(upload_to='thief')

    def __str__(self):
        return self.street_name

Let's explain

The BaseModel is basically inheritance at work. We extract common functionalities in both Thief and Police models and make it a parent class BaseModel. Since we don't want a table to be created for BaseModel, we add a Meta subclass and add the abstract = True attribute.

If abstract = True is omitted, we'll be having three tables in our database when we run python manage.py makemigrations: One for Thief, one for Police and one for BaseModel and that's not what we want. There are cases where you might want that though.

As you can see, all methods of the parent class are inherited by the children classes.

I am your saviour

There, little children, you can rework your abandoned projects to look more elegant with abstract models, especially in stuffs such as UUID usage across all models or even timestamps.

May the sublime force

be with you.

Photo credits: Maksim Shutov on Unsplash