Slide 1

Slide 1 text

What You Should Know About Django REST Framework Lacey Williams Henschel PyCascades, February 2021 @laceynwilliams | @revsys

Slide 2

Slide 2 text

What You Should Know About Django REST Framework: ViewSets Edition! @laceynwilliams | @revsys

Slide 3

Slide 3 text

ClassyDRF @laceynwilliams | @revsys

Slide 4

Slide 4 text

@laceynwilliams | @revsys

Slide 5

Slide 5 text

@laceynwilliams | @revsys

Slide 6

Slide 6 text

DRF has good docs DRF Docs on ViewSets @laceynwilliams | @revsys

Slide 7

Slide 7 text

ModelViewSet @laceynwilliams | @revsys

Slide 8

Slide 8 text

ModelViewSet = set of views to: ✓ create ✓ retrieve ✓ update ✓ list ✓ destroy @laceynwilliams | @revsys

Slide 9

Slide 9 text

class ModelViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet): """ A viewset that provides default `create()`, `retrieve()`, `update()`, `partial_update()`, `destroy()` and `list()` actions. """ pass @laceynwilliams | @revsys

Slide 10

Slide 10 text

Each *ModelMixin class has its own methods that perform the right ac4ons. CreateModelMixin has a create() method, instead of a post() method. @laceynwilliams | @revsys

Slide 11

Slide 11 text

This ViewSet... # views.py from rest_framework.viewsets import ModelViewSet from .models import Book from .serializers import BookSerializer class BookViewSet(ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer @laceynwilliams | @revsys

Slide 12

Slide 12 text

...gives us these endpoints1 GET /books/ GET /books/{id}/ POST /books/ PUT /books/{id}/ PATCH /books/{id}/ DELETE /books/{id}/ 1 you also need to hook the ViewSet to your urls @laceynwilliams | @revsys

Slide 13

Slide 13 text

A"ributes for your ModelViewSet @laceynwilliams | @revsys

Slide 14

Slide 14 text

What objects are you working with? from rest_framework.permissions import AllowAny from rest_framework.viewsets import ModelViewSet from .models import Book from .serializers import BookSerializer class BookViewSet(ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer permission_classes = [AllowAny] @laceynwilliams | @revsys

Slide 15

Slide 15 text

How should the data be serialized? from rest_framework.permissions import AllowAny from rest_framework.viewsets import ModelViewSet from .models import Book from .serializers import BookSerializer class BookViewSet(ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer permission_classes = [AllowAny] @laceynwilliams | @revsys

Slide 16

Slide 16 text

Who is allowed? from rest_framework.permissions import AllowAny from rest_framework.viewsets import ModelViewSet from .models import Book from .serializers import BookSerializer class BookViewSet(ModelViewSet): queryset = Book.objects.all() serializer_class = BookSerializer permission_classes = [AllowAny] @laceynwilliams | @revsys

Slide 17

Slide 17 text

Methods that come from GenericAPIView @laceynwilliams | @revsys

Slide 18

Slide 18 text

def get_queryset(self): assert self.queryset is not None, ( "'%s' should either include a `queryset` attribute, " "or override the `get_queryset()` method." % self.__class__.__name__ ) queryset = self.queryset if isinstance(queryset, QuerySet): queryset = queryset.all() return queryset @laceynwilliams | @revsys

Slide 19

Slide 19 text

When to override get_queryset(): When you need to filter your queryset with data you don't have un5l the 5me of the request (like filter by user) @laceynwilliams | @revsys

Slide 20

Slide 20 text

def get_object(self): queryset = self.filter_queryset(self.get_queryset()) lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field assert lookup_url_kwarg in self.kwargs, ( 'Expected view %s to be called with a URL keyword argument ' 'named "%s". Fix your URL conf, or set the `.lookup_field` ' 'attribute on the view correctly.' % (self.__class__.__name__, lookup_url_kwarg) ) # Uses the lookup_field attribute, which defaults to `pk` filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} obj = get_object_or_404(queryset, **filter_kwargs) # May raise a permission denied self.check_object_permissions(self.request, obj) return obj @laceynwilliams | @revsys

Slide 21

Slide 21 text

In your own methods, you can run: obj = self.get_object() instead of looking up the object yourself. @laceynwilliams | @revsys

Slide 22

Slide 22 text

def get_serializer_class(self): assert self.serializer_class is not None, ( "'%s' should either include a `serializer_class` attribute, " "or override the `get_serializer_class()` method." % self.__class__.__name__ ) # Returns the attribute you set return self.serializer_class @laceynwilliams | @revsys

Slide 23

Slide 23 text

When to override get_serializer_class(): When you want to use different serializers in different situa3ons @laceynwilliams | @revsys

Slide 24

Slide 24 text

def get_serializer(self, *args, **kwargs): serializer_class = self.get_serializer_class() # The context is where the request is added # to the serializer kwargs['context'] = self.get_serializer_context() return serializer_class(*args, **kwargs) @laceynwilliams | @revsys

Slide 25

Slide 25 text

def get_serializer(self, *args, **kwargs): serializer_class = self.get_serializer_class() # The context is where the request is added # to the serializer kwargs['context'] = self.get_serializer_context() return serializer_class(*args, **kwargs) @laceynwilliams | @revsys

Slide 26

Slide 26 text

def get_serializer_context(self): # Override this method to add more stuff # to the serializer context return { 'request': self.request, 'format': self.format_kwarg, 'view': self } @laceynwilliams | @revsys

Slide 27

Slide 27 text

When to override get_serializer_context(): When you need to add something to the context so your serializer can use it @laceynwilliams | @revsys

Slide 28

Slide 28 text

In your methods, you can run: serializer = self.get_serializer() instead of accessing your serializer directly. @laceynwilliams | @revsys

Slide 29

Slide 29 text

Methods that come with the mixins for ModelViewSet @laceynwilliams | @revsys

Slide 30

Slide 30 text

@laceynwilliams | @revsys

Slide 31

Slide 31 text

class CreateModelMixin: def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) @laceynwilliams | @revsys

Slide 32

Slide 32 text

class CreateModelMixin: def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) @laceynwilliams | @revsys

Slide 33

Slide 33 text

class CreateModelMixin: def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) @laceynwilliams | @revsys

Slide 34

Slide 34 text

class CreateModelMixin: def perform_create(self, serializer): serializer.save() @laceynwilliams | @revsys

Slide 35

Slide 35 text

class CreateModelMixin: def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) @laceynwilliams | @revsys

Slide 36

Slide 36 text

... Why did you show me a bunch of source code? @laceynwilliams | @revsys

Slide 37

Slide 37 text

"I want my books to have a lot of data for the detail endpoints, but only some of the data for the list endpoint. Can I do that?" @laceynwilliams | @revsys

Slide 38

Slide 38 text

from rest_framework.permissions import AllowAny from rest_framework.viewsets import ModelViewSet from .models import Book from .serializers import BookDetailSerializer, BookListSerializer class BookViewSet(ModelViewSet): queryset = Book.objects.all() serializer_class = BookDetailSerializer permission_classes = [AllowAny] def get_serializer_class(self): if self.action in ["list"]: return BookListSerializer return super().get_serializer_class() @laceynwilliams | @revsys

Slide 39

Slide 39 text

"A#er I create a book, I want to use a different, more detailed serializer to return it." @laceynwilliams | @revsys

Slide 40

Slide 40 text

Reminder class CreateModelMixin: def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) @laceynwilliams | @revsys

Slide 41

Slide 41 text

Reminder class CreateModelMixin: def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) @laceynwilliams | @revsys

Slide 42

Slide 42 text

from rest_framework.permissions import AllowAny from rest_framework.viewsets import ModelViewSet from .models import Book from .serializers import BookSerializer, BookCreatedSerializer class BookViewSet(ModelViewSet): ... def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) # self.perform_create(serializer) instance = serializer.save() return_serializer = BookCreatedSerializer(instance) headers = self.get_success_headers(return_serializer.data) return Response( return_serializer.data, status=status.HTTP_201_CREATED, headers=headers ) @laceynwilliams | @revsys

Slide 43

Slide 43 text

from rest_framework.permissions import AllowAny from rest_framework.viewsets import ModelViewSet from .models import Book from .serializers import BookSerializer, BookCreatedSerializer class BookViewSet(ModelViewSet): ... def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) # self.perform_create(serializer) instance = serializer.save() return_serializer = BookCreatedSerializer(instance) headers = self.get_success_headers(return_serializer.data) return Response( return_serializer.data, status=status.HTTP_201_CREATED, headers=headers ) @laceynwilliams | @revsys

Slide 44

Slide 44 text

from rest_framework.permissions import AllowAny from rest_framework.viewsets import ModelViewSet from .models import Book from .serializers import BookSerializer, BookCreatedSerializer class BookViewSet(ModelViewSet): ... def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) # self.perform_create(serializer) instance = serializer.save() return_serializer = BookCreatedSerializer(instance) headers = self.get_success_headers(return_serializer.data) return Response( return_serializer.data, status=status.HTTP_201_CREATED, headers=headers ) @laceynwilliams | @revsys

Slide 45

Slide 45 text

"But I don't need ALL those endpoints. Only most of them." @laceynwilliams | @revsys

Slide 46

Slide 46 text

Do everything except delete from rest_framework.generics import GenericAPIView from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, ListModelMixin # import models and serializers... class BookViewSet( CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, ListModelMixin, GenericAPIView ): ... @laceynwilliams | @revsys

Slide 47

Slide 47 text

DRF mixes and matches for you ✓ UpdateAPIView = UpdateModelMixin + GenericAPIView ✓ CreateAPIView ✓ ListAPIView ✓ RetrieveAPIView ✓ DestroyAPIView ✓ ReadOnlyModelViewSet ✓ RetrieveDestroyAPIView ✓ RetrieveUpdateDestroy APIView ✓ ListCreateAPIView ✓ RetrieveUpdateAPIView @laceynwilliams | @revsys

Slide 48

Slide 48 text

"Can we have an endpoint that just shows our featured books?" @laceynwilliams | @revsys

Slide 49

Slide 49 text

from rest_framework.decorators import action class BookViewSet(ModelViewSet): queryset = Book.objects.all() serializer_class = BookDetailSerializer def get_serializer_class(self): if self.action in ["list", "featured"]: return BookListSerializer return super().get_serializer_class() @action(detail=False, methods=["get"]) def featured(self, request): books = self.get_queryset().filter(featured=True) serializer = self.get_serializer(books, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @laceynwilliams | @revsys

Slide 50

Slide 50 text

from rest_framework.decorators import action class BookViewSet(ModelViewSet): queryset = Book.objects.all() serializer_class = BookDetailSerializer def get_serializer_class(self): if self.action in ["list", "featured"]: return BookListSerializer return super().get_serializer_class() @action(detail=False, methods=["get"]) def featured(self, request): books = self.get_queryset().filter(featured=True) serializer = self.get_serializer(books, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @laceynwilliams | @revsys

Slide 51

Slide 51 text

url = GET /books/featured/ @laceynwilliams | @revsys

Slide 52

Slide 52 text

from rest_framework.decorators import action class BookViewSet(ModelViewSet): queryset = Book.objects.all() serializer_class = BookDetailSerializer def get_serializer_class(self): if self.action in ["list", "featured"]: return BookListSerializer return super().get_serializer_class() @action(detail=False, methods=["get"]) def featured(self, request): books = self.get_queryset().filter(featured=True) serializer = self.get_serializer(books, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @laceynwilliams | @revsys

Slide 53

Slide 53 text

from rest_framework.decorators import action class BookViewSet(ModelViewSet): queryset = Book.objects.all() serializer_class = BookDetailerializer def get_serializer_class(self): if self.action in ["list", "featured"]: return BookListSerializer return super().get_serializer_class() @action(detail=False, methods=["get"]) def featured(self, request): books = self.get_queryset().filter(featured=True) serializer = self.get_serializer(books, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @laceynwilliams | @revsys

Slide 54

Slide 54 text

from rest_framework.decorators import action class BookViewSet(ModelViewSet): queryset = Book.objects.all() serializer_class = BookDetailerializer def get_serializer_class(self): if self.action in ["list", "featured"]: return BookListSerializer return super().get_serializer_class() @action(detail=False, methods=["get"]) def featured(self, request): books = self.get_queryset().filter(featured=True) serializer = self.get_serializer(books, many=True) return Response(serializer.data, status=status.HTTP_200_OK) @laceynwilliams | @revsys

Slide 55

Slide 55 text

Thank you! To Tom Chris*e and the maintainers of DRF. To Vinta So*ware and the maintainers of Classy DRF. To Beki Post and Jeff Triple3 for their help with this talk. To Vancouver PyLadies for le4ng me preview this talk with you. To PyCascades! @laceynwilliams | @revsys

Slide 56

Slide 56 text

[email protected] " @laceynwilliams # laceyhenschel.com # revsys.com @laceynwilliams | @revsys