Views

drf-commons provides a complete set of pre-composed ViewSet classes and the action mixins from which they are built. Every ViewSet class is a transparent composition of mixins — no hidden behavior, no magic dispatch.

Pre-Composed ViewSets

Class

Provided Actions

BaseViewSet

CRUD + file export

BulkViewSet

CRUD + bulk create/update/delete + file export

ReadOnlyViewSet

List + retrieve + file export

CreateListViewSet

Create + list + file export

BulkCreateViewSet

Bulk create only

BulkUpdateViewSet

Bulk update only

BulkDeleteViewSet

Bulk delete only

BulkOnlyViewSet

Bulk create + update + delete

ImportableViewSet

CRUD + file import + file export

BulkImportableViewSet

CRUD + bulk ops + file import + file export

Import:

from drf_commons.views import (
    BaseViewSet,
    BulkViewSet,
    ReadOnlyViewSet,
    CreateListViewSet,
    BulkCreateViewSet,
    BulkUpdateViewSet,
    BulkDeleteViewSet,
    BulkOnlyViewSet,
    ImportableViewSet,
    BulkImportableViewSet,
)

CRUD Mixins

CreateModelMixin

Handles POST /resource/ for single-object creation.

Configuration:

class MyViewSet(BaseViewSet):
    return_data_on_create = True  # Default: True
    # If False, returns empty data on 201. Reduces response payload.

Response: HTTP 201 with the serialized created object or empty data.

ListModelMixin

Handles GET /resource/ for listing objects.

Pagination control:

The list action respects the paginated query parameter:

GET /articles/?paginated=true   — paginated results
GET /articles/?paginated=false  — all results (use with caution)
GET /articles/                  — uses ViewSet default

Index appending:

When append_indexes = True (default on most ViewSets), each result object receives a sequential index field:

{"id": "...", "title": "...", "index": 1}

This is useful for client-side display ordering.

Configuration:

class MyViewSet(BaseViewSet):
    append_indexes = True      # Default: True
    pagination_class = StandardPageNumberPagination

RetrieveModelMixin

Handles GET /resource/{id}/.

Returns standardized success response with the serialized object.

UpdateModelMixin

Handles PUT /resource/{id}/ (full update) and PATCH /resource/{id}/ (partial update).

Configuration:

class MyViewSet(BaseViewSet):
    return_data_on_update = True  # Default: True
    # If False, returns empty data on 200.

DestroyModelMixin

Handles DELETE /resource/{id}/.

Returns HTTP 204 with standardized success response.

class MyViewSet(BaseViewSet):
    def perform_destroy(self, instance):
        # Override for soft delete:
        instance.soft_delete()

Bulk Operation Mixins

BulkCreateModelMixin

Provides POST /resource/bulk-create/.

  • Accepts a JSON array of objects

  • Validates array format and size against BULK_OPERATION_BATCH_SIZE

  • Wraps in transaction.atomic()

  • Returns HTTP 201 with created objects (or count if return_data_on_create=False)

# POST /articles/bulk-create/
# Body:
[
  {"title": "Article 1", "content": "..."},
  {"title": "Article 2", "content": "..."}
]

BulkUpdateModelMixin

Provides PUT /resource/bulk-update/ and PATCH /resource/bulk-update/.

  • PUT validates all fields (full update)

  • PATCH uses partial update semantics

  • Accepts a JSON array with id field in each object

  • Instance count must exactly match incoming data count

Bulk update mode (default, use_save_on_bulk_update = False):

Issues a single bulk_update() SQL statement. Audit fields auto-populated.

Save mode (use_save_on_bulk_update = True):

Calls instance.save() for each object. Triggers signals.

class ProductViewSet(BulkViewSet):
    use_save_on_bulk_update = False  # default
    bulk_batch_size = 500           # override global setting
# PATCH /products/bulk-update/
[
  {"id": "uuid-1", "price": "29.99"},
  {"id": "uuid-2", "price": "49.99"}
]

BulkDeleteModelMixin

Provides:

  • DELETE /resource/bulk-delete/ — Hard delete

  • DELETE /resource/bulk-soft-delete/ — Soft delete (requires SoftDeleteMixin)

Accepts a JSON array of IDs. Returns a detailed deletion report:

{
  "success": true,
  "data": {
    "requested_count": 10,
    "count": 8,
    "missing_ids": ["uuid-3", "uuid-7"]
  }
}

Import/Export Mixins

FileExportMixin

Provides POST /resource/export/.

Request body:

{
  "file_type": "xlsx",
  "includes": ["title", "status", "created_at"],
  "column_config": {
    "title": {"width": 40, "header": "Article Title"}
  },
  "data": []
}

Supported file types: csv, xlsx, pdf

Returns an HttpResponse with the file as an attachment.

ViewSet configuration:

class ArticleViewSet(BaseViewSet):
    export_field_config = {
        "title": "Title",
        "published": "Published",
        "created_at": "Created At",
    }

FileImportMixin

Provides POST /resource/import-from-file/.

Request: multipart/form-data with:

  • file — CSV, XLS, or XLSX file

  • append_datatrue to merge with existing, false to replace

ViewSet configuration:

class EmployeeViewSet(ImportableViewSet):
    import_file_config = {
        "file_format": "xlsx",
        "order": ["department", "employee"],
        "models": {
            "department": {
                "model": Department,
                "fields": ["name", "code"],
                "unique_fields": ["code"],
            },
            "employee": {
                "model": Employee,
                "fields": ["first_name", "last_name", "email"],
                "unique_fields": ["email"],
            },
        },
    }

    # Optional: transform imported values
    import_transforms = {
        "email": lambda v: v.strip().lower(),
    }

See Services for the full import configuration reference.

Custom ViewSet Composition

When no pre-composed ViewSet matches your requirements, compose exactly what you need:

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

class AppendOnlyViewSet(
    ListModelMixin,
    BulkCreateModelMixin,
    FileExportMixin,
    GenericViewSet,
):
    """
    A resource that allows listing, bulk creation, and export,
    but not individual create, update, or delete.
    """
    queryset = LogEntry.objects.all()
    serializer_class = LogEntrySerializer