Skip to content

9. Views and the API

The admin site asks the rules with no code. Django's views and DRF ask them through a few classes, and the serializer, the form and the template know nothing of access.

Django's views

"""The order pages, for a project on templates: the same rules as the API."""

from django.views.generic import ListView, UpdateView

from clearance.views import PolicyRequiredMixin, ProtectedFieldsViewMixin, ScopedQuerySetMixin
from shop.models import Order
from shop.policies import order_access


class OrderList(ProtectedFieldsViewMixin, PolicyRequiredMixin, ScopedQuerySetMixin, ListView):
    """Only the orders the person may view; the price only when they may read it."""

    policy = order_access.policy
    queryset = Order.objects.select_related('branch', 'customer')
    template_name = 'shop/orders.html'


class OrderEdit(ProtectedFieldsViewMixin, PolicyRequiredMixin, ScopedQuerySetMixin, UpdateView):
    """The form offers only the fields, and the status values, the person may write."""

    policy = order_access.policy
    queryset = Order.objects.all()
    fields = ('item', 'status', 'price')
    template_name = 'shop/order_form.html'
    success_url = '/orders/'
  • ScopedQuerySetMixin narrows the queryset to the rows the person reaches.
  • PolicyRequiredMixin asks the policy. The action is read off the view - ListView is list, UpdateView is edit - or named with policy_action.
  • ProtectedFieldsViewMixin builds the form for the person, and gives the template readable_fields:
{% if 'price' in readable_fields %}<td>{{ order.price }}</td>{% endif %}

The API

"""
The orders API. The serializer knows nothing of access: the viewset narrows
the rows, asks the policy, and leaves out the price for whoever may not read it.
"""

from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.response import Response

from clearance.drf.viewsets import ScopedModelViewSet
from shop.models import Order


class OrderSerializer(serializers.ModelSerializer):
    class Meta:
        model = Order
        fields = ('id', 'customer', 'branch', 'item', 'status', 'price')
        read_only_fields = ('customer',)


class OrderViewSet(ScopedModelViewSet):
    queryset = Order.objects.select_related('branch')
    serializer_class = OrderSerializer

    def perform_create(self, serializer):
        serializer.save(customer=self.request.user)

    @action(detail=True, methods=['post'])
    def print_invoice(self, request, pk=None):
        """Found in the code: shop.order_print_invoice is made by migrate."""
        order = self.get_object()
        return Response({'invoice': f'INV-{order.pk:05d}', 'item': order.item})

ScopedModelViewSet narrows the rows, asks the model's policy for each action, and leaves out the fields the person may not read. A write naming a field they may not write - or a status value they may not set - is a 400 naming the field.

print_invoice is an action found in the code: migrate makes its permission, shop.order_print_invoice, and only whoever holds it may call it.

A view that holds no record

With ModelPolicyPermission as the default, a view with no model and no policy is refused at start-up (E009) - DRF's own router root among them. The shop gives it the permission it needs:

from django.urls import include, path
from rest_framework import routers
from rest_framework.permissions import IsAuthenticated

from shop import api, views


class ShopRouter(routers.DefaultRouter):
    class APIRootView(routers.APIRootView):
        """
        The list of endpoints holds no record, so it asks no policy - said here,
        because every view asks one unless it says otherwise (E009).
        """

        permission_classes = (IsAuthenticated,)


router = ShopRouter()
router.register('orders', api.OrderViewSet)

urlpatterns = [
    path('orders/', views.OrderList.as_view(), name='orders'),
    path('orders/<int:pk>/', views.OrderEdit.as_view(), name='order'),
    path('api/', include(router.urls)),
]

Try it

Signed in as alice, post to /api/orders/:

{"branch": 1, "item": "Clock", "price": "1.00"}

The answer is a 400 naming price: a customer may place an order, and never price it.