Model Mixins

drf-commons provides a comprehensive suite of Django model mixins. Each mixin addresses a specific concern and can be composed freely with others.

Base Mixins

BaseModelMixin

from drf_commons.models import BaseModelMixin

The canonical base model for drf-commons projects. Composes:

  • JsonModelMixin

  • UserActionMixin

  • TimeStampMixin

  • SoftDeleteMixin

  • models.Model

Fields provided:

Field

Type

Description

id

UUIDField

Primary key, auto-generated, non-editable

created_at

DateTimeField

Set on creation, never modified (auto_now_add=True)

updated_at

DateTimeField

Updated on every save (auto_now=True)

created_by

ForeignKey(User)

Populated from context user on first save

updated_by

ForeignKey(User)

Populated from context user on every save

is_active

BooleanField

Soft delete flag; True means record is live

deleted_at

DateTimeField

Timestamp of soft deletion; null when active

Usage:

from django.db import models
from drf_commons.models import BaseModelMixin

class Invoice(BaseModelMixin):
    number = models.CharField(max_length=64, unique=True)
    total = models.DecimalField(max_digits=12, decimal_places=2)

    class Meta:
        ordering = ["-created_at"]

TimeStampMixin

from drf_commons.models.base import TimeStampMixin

Adds created_at and updated_at fields with automatic population. Use when you need timestamping without the full BaseModelMixin stack.

UserActionMixin

from drf_commons.models.base import UserActionMixin

Adds created_by and updated_by ForeignKey fields. Overrides save() to auto-populate these fields from the current context user.

The implementation:

  • Sets created_by only on the first save (pk is None check)

  • Always updates updated_by on every save

  • Calls get_current_authenticated_user() — raises if no authenticated user

Important

UserActionMixin requires CurrentUserMiddleware to be installed. The application will raise ImproperlyConfigured at startup if this middleware is missing.

SoftDeleteMixin

from drf_commons.models.base import SoftDeleteMixin

Adds non-destructive deletion with restore capability.

Fields: is_active (BooleanField), deleted_at (DateTimeField)

Methods:

instance.soft_delete()   # is_active=False, deleted_at=timezone.now()
instance.restore()       # is_active=True, deleted_at=None

Property:

instance.is_deleted      # returns not self.is_active

JsonModelMixin

from drf_commons.models.mixins import JsonModelMixin

Provides get_json() for flexible model-to-JSON serialization.

Signature:

instance.get_json(
    fields: list = None,          # Explicit field inclusion list
    exclude_fields: list = None,  # Explicit field exclusion list
    exclude_audit: bool = False,  # Exclude audit fields
) -> str

Examples:

# All fields
article.get_json()

# Specific fields only
article.get_json(fields=["id", "title", "published"])

# Exclude audit trail fields
article.get_json(exclude_audit=True)

# Exclude specific fields
article.get_json(exclude_fields=["content", "deleted_at"])

Content Mixins

SlugMixin

from drf_commons.models.content import SlugMixin

Auto-generates URL-safe slug values with deterministic collision avoidance. Subclasses must implement get_slug_source() returning the string to slugify.

Fields: slug (SlugField, unique)

Abstract method required:

class Category(BaseModelMixin, SlugMixin):
    name = models.CharField(max_length=255)

    def get_slug_source(self):
        return self.name

Generated slugs:

  • "Product Category""product-category"

  • "Product Category" (collision) → "product-category-1"

  • "Product Category" (collision) → "product-category-2"

MetaMixin

from drf_commons.models.content import MetaMixin

Provides structured metadata, tagging, and notes on any model.

Fields:

Field

Type

Description

metadata

JSONField

Arbitrary JSON key-value store

tags

CharField

Comma-separated tag string

notes

TextField

Free-form text notes

Methods:

obj.get_tags_list()                      # ["tag1", "tag2"]
obj.add_tag("featured")                  # Adds tag if not present
obj.remove_tag("featured")               # Removes tag
obj.get_metadata_value("color")          # Returns value or None
obj.set_metadata_value("color", "blue")  # Sets key in metadata dict

VersionMixin

from drf_commons.models.content import VersionMixin

Implements optimistic locking. Raises VersionConflictError when a concurrent write is detected.

Fields:

Field

Type

Description

version

PositiveIntegerField

Current version number (starts at 1)

revision_notes

TextField

Optional change description for this version

Usage:

class Document(BaseModelMixin, VersionMixin):
    body = models.TextField()

# Increment before saving a new version
doc.revision_notes = "Updated introduction section."
doc.increment_version()
doc.save()

# On concurrent modification:
# Raises VersionConflictError

Person Mixins

IdentityMixin

from drf_commons.models.person import IdentityMixin

Provides standard personal identity fields with computed properties.

Fields: first_name, last_name, middle_name, email, phone, date_of_birth, birth_place, gender, nationality

Gender choices: MALE, FEMALE, OTHER, PREFER_NOT_TO_SAY

Properties:

person.full_name    # "Jane Doe" (first_name + last_name)
person.initials     # "J.D."
person.age          # Calculated from date_of_birth

AddressMixin

from drf_commons.models.person import AddressMixin

Provides structured postal address fields with coordinate support.

Fields: street_address, street_address_2, city, state_province, postal_code, country, latitude, longitude

Properties and methods:

address.full_address       # "123 Main St, New York, NY 10001, US"
address.short_address      # "New York, NY, US"
address.has_coordinates    # True if lat/lon both set
address.get_coordinates()  # (lat, lon) tuple or None

Model Fields

CurrentUserField

from drf_commons.models.fields import CurrentUserField

A ForeignKey subclass that auto-populates itself with the current authenticated user. Suitable for models where the relation must always reflect who performed the action.

Parameters:

Parameter

Description

on_update

If True, updates the field on every save() call. Default: False

Example:

class Comment(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    body = models.TextField()
    # Populated automatically from CurrentUserMiddleware
    author = CurrentUserField(on_update=False)  # Set only on creation

Note

CurrentUserField with on_update=False behaves like created_by in UserActionMixin — set once and never changed. With on_update=True it mirrors updated_by behavior.

Mixin Composition Examples

Not all models need the full BaseModelMixin stack. Compose exactly what you need:

from django.db import models
from drf_commons.models.base import TimeStampMixin, SoftDeleteMixin
from drf_commons.models.content import MetaMixin

# Timestamped + soft-deletable + taggable, no user tracking
class Tag(TimeStampMixin, SoftDeleteMixin, MetaMixin, models.Model):
    name = models.CharField(max_length=64, unique=True)
from drf_commons.models.base import TimeStampMixin
from drf_commons.models.content import VersionMixin

# Versioned document with timestamps only
class Policy(TimeStampMixin, VersionMixin, models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()