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/'
ScopedQuerySetMixinnarrows the queryset to the rows the person reaches.PolicyRequiredMixinasks the policy. The action is read off the view -ListViewislist,UpdateViewisedit- or named withpolicy_action.ProtectedFieldsViewMixinbuilds the form for the person, and gives the templatereadable_fields:
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/:
The answer is a 400 naming price: a customer may place an order, and never
price it.