Extensibility

Every component in drf-commons is designed for extension. This page documents the primary extension patterns.

Custom ViewSet Compositions

Pre-composed ViewSet classes cover the most common patterns. For unusual requirements, compose directly from the mixin layer:

from rest_framework.viewsets import GenericViewSet
from drf_commons.views.mixins import (
    ListModelMixin,
    RetrieveModelMixin,
    BulkCreateModelMixin,
    BulkDeleteModelMixin,
    FileExportMixin,
)

class AuditableAppendOnlyViewSet(
    ListModelMixin,
    RetrieveModelMixin,
    BulkCreateModelMixin,
    BulkDeleteModelMixin,
    FileExportMixin,
    GenericViewSet,
):
    """
    Records can be listed, retrieved, bulk-created, and bulk-deleted,
    but not individually created or updated.
    """
    pass

Adding Custom Actions

DRF’s @action decorator works normally on drf-commons ViewSets:

from rest_framework.decorators import action
from drf_commons.response import success_response
from drf_commons.views import BaseViewSet

class ArticleViewSet(BaseViewSet):
    @action(detail=True, methods=["post"], url_path="publish")
    def publish(self, request, pk=None):
        article = self.get_object()
        article.published = True
        article.save()
        return success_response(
            data=self.get_serializer(article).data,
            message="Article published.",
        )

Extending Model Mixins

Subclass any mixin to extend or override behavior:

from drf_commons.models.base import SoftDeleteMixin

class CascadingSoftDeleteMixin(SoftDeleteMixin):
    """
    Extends SoftDeleteMixin to cascade soft deletes to related objects.
    """
    # Override in concrete model to specify related managers to cascade
    soft_delete_related = []

    def soft_delete(self):
        for related_manager_name in self.soft_delete_related:
            manager = getattr(self, related_manager_name)
            for obj in manager.filter(is_active=True):
                obj.soft_delete()
        super().soft_delete()

Custom Serializer Fields

The configurable field system is extensible. All field types are built on ConfigurableRelatedField. Implement a custom field by providing the input parsing and output transformation:

from drf_commons.serializers.fields.base import ConfigurableRelatedField

class SlugToDataField(ConfigurableRelatedField):
    """
    Accept a slug string on write, return nested serializer data on read.
    """

    def to_internal_value(self, data: str):
        """Resolve slug to model instance."""
        try:
            return self.get_queryset().get(slug=data)
        except self.get_queryset().model.DoesNotExist:
            self.fail("does_not_exist", pk_value=data)

    def to_representation(self, value):
        """Serialize instance using the configured serializer."""
        if isinstance(value, dict):
            return value
        serializer = self.serializer_class(value, context=self.context)
        return serializer.data

# Usage:
class ArticleSerializer(BaseModelSerializer):
    category = SlugToDataField(
        queryset=Category.objects.all(),
        serializer=CategorySerializer,
    )

Custom Response Envelope

If your project requires a different envelope structure, override the utility functions in your own module:

# myapp/response.py
from datetime import datetime, timezone
from rest_framework.response import Response

def success_response(data=None, message="", status_code=200, **kwargs):
    return Response(
        {
            "ok": True,
            "ts": datetime.now(timezone.utc).isoformat(),
            "msg": message,
            "payload": data,
            **kwargs,
        },
        status=status_code,
    )

Then import from myapp.response rather than drf_commons.response throughout your project.

If you want to customize only specific ViewSet responses, override the mixin methods:

from drf_commons.views.mixins import ListModelMixin
from myapp.response import success_response

class CustomListMixin(ListModelMixin):
    def list(self, request, *args, **kwargs):
        response = super().list(request, *args, **kwargs)
        response.data["_api_version"] = "v2"
        return response

Custom Pagination

from drf_commons.pagination import StandardPageNumberPagination

class TinyPagePagination(StandardPageNumberPagination):
    page_size = 5
    max_page_size = 20

class LargeDataViewSet(BaseViewSet):
    pagination_class = TinyPagePagination

Custom Settings

Extend the default settings by reading from COMMON with custom keys:

# myapp/conf.py
from drf_commons.common_conf.settings import CommonSettings

class AppSettings(CommonSettings):
    @property
    def RATE_LIMIT_PER_MINUTE(self):
        return self._get("RATE_LIMIT_PER_MINUTE", default=60)

    @property
    def ENABLE_WEBHOOKS(self):
        return self._get("ENABLE_WEBHOOKS", default=False)

app_settings = AppSettings()

# settings.py
COMMON = {
    "RATE_LIMIT_PER_MINUTE": 120,
    "ENABLE_WEBHOOKS": True,
}

Extending the Import Pipeline

The import pipeline can be extended through:

  1. Transforms: Per-field data transformation functions (see Services)

  2. Progress callbacks: For monitoring long-running imports

  3. Custom FileReader: For non-standard file formats (internal API)

For deeply custom import logic, extend FileImportService directly and override the processing stages:

from drf_commons.services.import_from_file import FileImportService

class ValidatingImportService(FileImportService):
    def _validate_row(self, model_key, row):
        # Custom cross-row validation logic
        super()._validate_row(model_key, row)
        if model_key == "employee" and not row.get("department_code"):
            raise ValueError("Employee row missing department_code")