Viewsets and custom actions¶
The standard actions¶
ServiceViewSet serves list, retrieve, create, update,
partial_update and destroy, each from its entry in action_specs.
SelectorViewSet serves list and retrieve. A write with no entry answers
405, and Allow leaves it out. A read with no entry falls back to DRF's own
behaviour (get_queryset(), get_object()), which needs rows and a serializer:
on a viewset with no queryset or get_queryset(), or no serializer_class,
get_serializer_class() or get_serializer(), that read answers 405 too, and
Allow and the schema leave it out. A list, a retrieve or a get_object()
of your own keeps the route yours.
So a create-only ServiceViewSet answers POST and nothing else. A router
still mounts its detail route, where every method answers 405, Allow names
OPTIONS alone and the schema has no path. For no detail route at all, declare
ServiceCreateMixin over GenericViewSet.
get_queryset() answers the list spec's set on list alone, from the one
dispatch the list presents; every other action, a custom @action included,
reads the viewset's queryset. So a viewset under DRF's
DjangoModelPermissions, which asks get_queryset() on every action, sets
queryset. The schema needs none: a lookup is documented from the read its
spec declares (see OpenAPI).
from django_spec_rest import ServiceViewSet
class InvoiceViewSet(ServiceViewSet):
action_specs = {
"list": LIST_SPEC,
"retrieve": RETRIEVE_SPEC,
"create": CREATE_SPEC,
"update": UPDATE_SPEC,
# "partial_update" is optional: PATCH falls back to "update",
# validated partially. Declare it to give PATCH a spec of its own,
# or alone for a PATCH-only route.
}
An entry under list or retrieve must be a SelectorSpec, and under a write
key a ServiceSpec. An entry under a key no action dispatches is refused at
as_view(), since nothing would ask its permissions: a custom route takes its
spec through a decorator.
A custom write: @service_action¶
from typing import Any
from django_spec_rest import ServiceViewSet, service_action
class InvoiceViewSet(ServiceViewSet):
action_specs = {...}
@service_action(SEND_SPEC, detail=True, methods=["post"])
def send(self, request: Any, pk: Any = None) -> None:
"""Mark a draft as sent."""
The method's body never runs: the decorator supplies the handler, and the
method gives DRF's @action its name. detail, methods, url_path,
url_name and any other keyword go to DRF's @action. On a detail route the
row is the spec's own target selector's, read from the route's pk, never
get_object()'s. The answer is the spec's presenter's, under the action's
success_status and through its response_finalizer where the decorator
declares them, else the viewset's (200 by default):
@service_action(SEND_SPEC, detail=True, methods=["post"], success_status=202).
See Per-action answers.
A custom read: @selector_action¶
from django_spec_rest import SelectorViewSet, selector_action
class InvoiceViewSet(SelectorViewSet):
@selector_action(OVERDUE_SPEC)
def overdue(self, request: Any) -> None:
"""Invoices past their due date."""
The spec's kind sets the route's shape: a LIST spec is a collection action,
paginated where the viewset paginates, and a RETRIEVE spec a detail action,
answering 404 for a miss or null under allow_none.
Composing your own viewset¶
Each standard action is a mixin over GenericViewSet, so a viewset takes only
the actions it serves:
from rest_framework.viewsets import GenericViewSet
from django_spec_rest import (
ActionSerializerResolver,
SelectorListMixin,
ServiceCreateMixin,
)
class InboxViewSet(ServiceCreateMixin, SelectorListMixin, ActionSerializerResolver, GenericViewSet):
action_specs = {"list": INBOX_SPEC, "create": POST_MESSAGE_SPEC}
ActionSerializerResolver answers get_serializer_class() per action: a write
spec's input serializer where the viewset sets no serializer_class, which
DRF's browsable API asks to draw its forms, and the viewset's
serializer_class otherwise.
A standalone view¶
A single route needs no viewset: