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.
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
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.
Perform a pip installation.
$ pip install drf-nested-routers
# 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)
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.
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
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"
}
]
You may want to dynamically determine the serializer field values.
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()
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():
(...)
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.
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.
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.
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.
Recommended Posts