[PYTHON] Django REST framework A little useful to know.

Here's a summary of what I personally wanted to keep as a note of what I looked up when using Django's REST framework. If you look it up, you will find the contents that are obvious and the items that you have implemented yourself. If you have any suggestions in the content of this article that there is a better way to do it, or that it is wrong to face such a problem in the first place, please leave a comment.

I want to represent a nested URL

It's easy to create a rest api like this using the rest framework router.

/api/v1/groups/ GET POST
/api/v1/groups/1/ GET PUT PATCH DELETE
/api/v1/members/ GET POST
/api/v1/members/1/ GET PUT PATCH DELETE

However, it is difficult to create an api with nested urls with the router of the rest framework as shown below.

/api/v1/groups/ GET POST
/api/v1/groups/1/ GET PUT PATCH DELETE
/api/v1/groups/1/members/ GET POST
/api/v1/groups/1/members/1/ GET PUT PATCH DELETE

Solution

The solution is to use drf-nested-routers. drf-nested-routers is a library that makes it easy to implement nested urls on the rest framework.

Introduction method

Perform a pip installation.

$ pip install drf-nested-routers

Implementation

# urls.py

from rest_framework_nested import routers
from .views import *

router = routers.SimpleRouter()

router.register(r'groups', GroupViewSet)
groups_router = routers.NestedSimpleRouter(router, r'groups', lookup='group')
groups_router.register(r'members', MemberViewSet, base_name='groups-members')

urlpatterns = [
	url(r'^api/v1/', include(router.urls)),
	url(r'^api/v1/', include(groups_router.urls)),
]

You can get each primary_key with the argument as below. The keyword name of the argument is the lookup name + _pk specified in urls.py.

# views.py
class GroupViewSet(viewsets.ViewSet):
    def list(self, request):
        (...)
        return Response(serializer.data)

    def retrieve(self, request, pk=None):
        group = self.queryset.get(pk=pk)
        (...)
        return Response(serializer.data)


class MemberViewSet(viewsets.ViewSet):
    def list(self, request, group_pk=None):
        members = self.queryset.filter(group=group_pk)
        (...)
        return Response(serializer.data)

    def retrieve(self, request, pk=None, group_pk=None):
        member = self.queryset.get(pk=pk, group=group_pk)
        (...)
        return Response(serializer.data)

I want to create multiple models at once with POST of REST API implemented by ModelViewSet

In fact, the standard views.ModelViewSet create () method does not allow you to create multiple models at once. If you want to create multiple models, you have to hit the API accordingly.

Solution

Code created

So I created a decorator that allows you to create single or multiple models with views.ModelViewSet.

Copy the code below and save it in a suitable file.

from rest_framework.response import Response
from rest_framework import status

def multi_create(serializer_class=None):
    def __multi_create(function):
        def __wrapper(self, request, *args, **kwargs):
            many = False
            if isinstance(request.data, list):
                many = True
            serializer = serializer_class(data=request.data, many=many)
            if serializer.is_valid():
                serializer.save()
                headers = self.get_success_headers(serializer.data)
                data = serializer.data
                result = function(self, request, *args, **kwargs)
                if result is not None:
                    return result
                if many:
                    data = list(data)
                return Response(data,
                                status=status.HTTP_201_CREATED,
                                headers=headers)
            else:
                return Response(serializer.errors,
                                status=status.HTTP_400_BAD_REQUEST)
        return __wrapper
    return __multi_create

How to use

Import the multi_create decorator from the file you saved earlier and attach it to the ViewSet's create () method as shown below. The argument is the Serializer class corresponding to the model you want to create.

# views.py

from .decorators import multi_create

class MyViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer
  
	@multi_create(serializer_class=MySerializer)
    def create(self, request):
        pass

All you have to do is POST the JSON data in the list format as shown below.

[
	{"name": "hoge"},
	{"name": "fuga"}
]

The following response will be returned.

[
	{
		"id": 1,
		"name": "hoge"
	},
	{
		"id": 2,
		"name": "fuga"
	}
]

I want to dynamically determine the field value of Serializer

You may want to dynamically determine the serializer field values.

Solution

This time we will use serializers.SerializerMethodField (). By using serializers.SerializerMethodField (), you can determine the value of the field according to the result of the method.

Suppose you have a Model class and a hoge () method that returns name + _hoge like this:

# modles.py
class MyModel(models.Model):
    name = models.CharField(max_length=100)
    
    def hoge(self):
        return "{}_hoge".format(self.name)

In Serializer, the value of the value field is dynamically determined by specifying serializers.SerializerMethodField () as shown below. The method name applied is get_ + field name. This time, the return value of the get_value () method will be the value of value. It is also possible to specify the applicable method name with the argument method_name of SerializerMethodField ().

# serializer.py
class MySerializer(serializers.ModelSerializer):
    value = serializers.SerializerMethodField()
    
    class Meta:
        model = MyModel
    
    def get_value(self, obj):
        return obj.hoge()

I want to return an error that occurred in Model or Selializer as a response

Suppose the API is hit and the ViewSet's create () method is called. At that time, if an error occurs in the save () method of the Model class as shown below, how should I make an error response? There is no method I implemented in the MyViewSet class for error handling with try except, and the save () method of MyModel is called completely inside the black box.

# views.py
class MyViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer
# models.py
class MyModel(models.Model):
    name = models.CharField(max_length=100)

    def save(self, force_insert=False, force_update=False,
             using=None, update_fields=None):
        if self.hoge():
            raise HogeError('hoge error.')
        super(MyModel, self).save(*args, **kwargs)

    def hoge():
        (...)

Solution

1st

One solution is to override the create () method for error handling as shown below.

# views.py
class MyViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer

    def create(self, request):
        try:
           super(MyViewSet, self).create(*args, **kwargs) 
        except HogeError:
            (....)
            return Response(content, status=status.HTTP_400_BAD_REQUEST)

    def update(self, request):
        try:
           super(MyViewSet, self).update(*args, **kwargs) 
        except HogeError:
            (....)
            return Response(content, status=status.HTTP_400_BAD_REQUEST)

With this method, it is necessary to handle Error in the same way when creating and updating.

Second

So another solution is to override the handle_exception () method. handle_exception is a standard method of restframework that handles errors. For example, hitting an unauthorized HTTP method will return a response similar to the following:

HTTP/1.1 405 Method Not Allowed
Content-Type: application/json
Content-Length: 42

{"detail": "Method 'DELETE' not allowed."}

In this method, errors that are not excluded by handler_exception are excluded at the override destination.

# views.py
class MyViewSet(viewsets.ModelViewSet):
    queryset = MyModel.objects.all()
    serializer_class = MySerializer

    def handle_exception(self, exc):
        try:
            return super(MyViewSet, self).handle_exception(exc)
        except HogeError:
            content = {'detail': '{}'.format(exc.args)}
            return Response(content, status=status.HTTP_400_BAD_REQUEST)

By using this method, you can handle all the errors that occur in this MyViewSet. By the way, there is no problem with the method of determining the exc type by is instance instead of try except.

Third

The third method is to use custom_exception_handler ().

Describe the path of custom_exception_handler to be implemented in settings.py.

# settings.py
REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}

Implement custom_exception_handler () in the file specified in the path above.

# utils.py
from rest_framework.views import exception_handler

def custom_exception_handler(exc, context):

    response = exception_handler(exc, context)
    if isinstance(exc, HogeError):
        content = {'detail': '{}'.format(exc.args)}
        return Response(content, status=status.HTTP_400_BAD_REQUEST)
    return response

The feature of this method is that the errors that occur in all Views are aggregated in this custom_exception_handler.

Each of these methods has a different scope, so I would like to use them properly depending on the situation.

I want to pass the value of View to Serializer

Solution

If you think about the solution, you can pass it to the constructor (init) of Serializer as a matter of course. In this example, it is passed to the keyword argument user_data.

# views.py
class MyViewSet(views.ModelViewSet):
    def retrieve(self, request):
        user_data = request.GET['user_data']
        (...)
        serializer = MySerializer(My_list, many=True, user_data=user_data)

The recipient overrides init and receives it from the keyword argument.

# serializer.py
class MySerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel

    def __init__(self, *args, **kwargs):
        self.user_data = kwargs.pop('user_data', '')
        super(MySerializer, self).__init__(*args, **kwargs)

I don't think it's common to pass View values to Serializer, but it may be used when using serializers.SerializersMethodFiels () etc.

that's all.

reference

Recommended Posts

Django REST framework A little useful to know.
Python's itertools is a little useful to know
Create a REST API to operate dynamodb with the Django REST Framework
Django REST framework basics
How to create a Rest Api in Django
Django Rest Framework Tips
Create a Todo app with Django REST Framework + Angular
Create a Todo app with the Django REST framework
When you want to filter with Django REST framework
Django REST framework stumbling block
Django REST framework with Vue.js
Login with django rest framework
How to write custom validations in the Django REST Framework
How to reset password via API using Django rest framework
[Django] Use MessagePack with Django REST framework
Create RESTful APIs with Django Rest Framework
Logical deletion in Django, DRF (Django REST Framework)
Understand the benefits of the Django Rest Framework
ng-admin + Django REST framework ready-to-create administration tool
CRUD GET with Nuxt & Django REST Framework ②
CRUD POST with Nuxt & Django REST Framework
CRUD GET with Nuxt & Django REST Framework ①
Django REST Framework + Clean Architecture Design Consideration
How to deal with garbled characters in json of Django REST Framework
I made a webAPI! Build environment from Django Rest Framework 1 on EC2
I want to create an API that returns a model with a recursive relationship in the Django REST Framework
How to get people to try out django rest framework features in one file
The road to fighting the connection between Nginx, Django and uwsgi and winning a little
How to automatically generate API document with Django REST framework & POST from document screen
The story of Django creating a library that might be a little more useful
CRUD PUT, DELETE with Nuxt & Django REST Framework
How to develop a cart app with Django
Implement JWT login functionality in Django REST framework
I would like to know about Django pagination.
To myself as a Django beginner (3)-Hello world! ---
Implementing authentication in Django REST Framework with djoser
I want to upload a Django app to heroku
Sometimes you want to access View information from Serializer with DRF (Django REST Framework)
A little trick to know when writing a Twilio application using Python on AWS Lambda
A little addictive information about Cliff, the CLI framework
A memo to create a virtual environment (venv) before Django
I tried to create a table only with Django
More new user authentication methods with Django REST Framework
Python Ver. To introduce WebPay with a little code.
(Python) Try to develop a web application using Django
[CRUD] [Django] Create a CRUD site using the Python framework Django ~ 1 ~
How to deploy a Django application on Alibaba Cloud
Create APIs around user authentication with Django REST Framework
How to build a Django (python) environment on docker
Steps from installing Python 3 to creating a Django app
[Django Rest Framework] Customize the filter function using Django-Filter
[CRUD] [Django] Create a CRUD site using the Python framework Django ~ 2 ~
Transit to the update screen with the Django a tag
To myself as a Django beginner (1) --Create a project app--
How to run Django on IIS on a Windows server
How to reference static files in a Django project
To myself as a Django beginner (4) --Create a memo app--
[CRUD] [Django] Create a CRUD site using the Python framework Django ~ 3 ~
[CRUD] [Django] Create a CRUD site using the Python framework Django ~ 4 ~
[CRUD] [Django] Create a CRUD site using the Python framework Django ~ 5 ~
To myself as a Django beginner (2) --What is MTV?