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:
Transforms: Per-field data transformation functions (see Services)
Progress callbacks: For monitoring long-running imports
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")