[PYTHON] Django Rest Framework Tips

version information

module version
Python 3.7.7
django-environ 0.4.5
Django 2.2.11
djangorestframework 3.11.0
djangorestframework-jwt 1.11.0
django-rest-swagger 2.2.0
django-filter 2.2.0
mysqlclient 1.4.6

I want to use Swagger

pip install django-rest-swagger

pip3 install django-rest-swagger==2.2.0

Modify settings.py

backend/src/config/settings/settings.py


.
..
...
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    )
}

Fixed development.py

backend/src/config/settings/developement.py


.
..
...
INSTALLED_APPS += [
    'rest_framework_swagger',
]

REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS'] = 'rest_framework.schemas.coreapi.AutoSchema'

Fixed urls.py

backend/src/config/urls.py


from django.contrib import admin
from django.urls import path
from django.conf import settings  #Postscript
from rest_framework_jwt.views import obtain_jwt_token  #Postscript

urlpatterns = [
    path('admin/', admin.site.urls),
    url('api/v1/login/', obtain_jwt_token),  #Swagger without one or more APIs-It seems that the UI cannot be opened, so register the login API for the time being
]

#Add all below
if settings.DEBUG:  # DEBUG=Used only when True (during development)
    from rest_framework.schemas import get_schema_view
    from rest_framework_swagger import renderers
    schema_view = get_schema_view(
        title='API list',
        public=True,
        renderer_classes=[renderers.OpenAPIRenderer, renderers.SwaggerUIRenderer])
    urlpatterns += [
        url(API_ROOT + 'api-auth/', include('rest_framework.urls')),
        url('swagger-ui/', schema_view),
    ]

Access Swagger-ui

If you access http://0.0.0.0:8000/swagger-ui/ and the following screen appears, the upper right Log in Press to log in image.png

image.png

image.png

View Sets

For the time being, I want to publish the API of GET, POST, PUT, DELETE ... for the model.

You can use the ModelViewSets that everyone loves.

backend/src/api/users/views.py


from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.viewsets import ModelViewSet  #this


class UserViewSets(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

Finished (using swagger-ui) image.png

I want to narrow down the HTTP request methods to be published

Method 1: Combine GenericViewSet with various mixins

If you want to increase the number of request methods later, you can add the corresponding mixins.

backend/src/api/users/views.py


from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.viewsets import GenericViewSet  #this
from rest_framework.mixins import CreateModelMixin  #this


class UserViewSets(GenericViewSet, CreateModelMixin):
    queryset = User.objects.all()
    serializer_class = UserSerializer

image.png

Method 2: Use generics

If you want to increase the number of request methods later, you can add the corresponding APIView.

backend/src/api/users/views.py


from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.generics import CreateAPIView  #this


class UserViewSets(CreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

For generics, the method of specifying urls.py is different

backend/src/config/urls.py


# =========== viewsets ===========
router = DefaultRouter()
router.register('user', user_views.UserViewSets)  #In the case of viewsets, router can be used

urlpatterns = [
    path('admin/', admin.site.urls),
    url('api/v1/login/', obtain_jwt_token),
    url('', include(router.urls)),
]

backend/src/config/urls.py


# =========== generics ===========
urlpatterns = [
    path('admin/', admin.site.urls),
    url('api/v1/login/', obtain_jwt_token),
    url('user/', user_views.UserViewSets.as_view()),  #as to url patterns_views()Add with
]

The result is the same as method 1 image.png

I want to use ModelViewSet, but I want to narrow down the HTTP request method (see)

For such selfishness, it's http_method_names. I wonder if it will be used in situations where I have implemented it but do not want to publish it yet.

backend/src/api/users/views.py


from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    http_method_names = ['get', 'post']  #HTTP request method is lowercase (important), not uppercase

image.png

I want to limit API requests to authenticated users only

Let's specify ʻIsAuthenticated` for permission_class.

First, modify settings.py.

backend/src/config/settings/settings.py


.
..
...
REST_FRAMEWORK = {
    #add to
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
        # 'common.permissions.IsSuperuser',  #If you created your own permission, add it here.
    ),
    ...
    ..
    .
}

If you request the API in the unauthenticated state, 403 will be returned.

backend/src/api/users/views.py


from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.permissions import IsAuthenticated  #add to
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    permission_classes = [IsAuthenticated,]  #add to
    queryset = User.objects.all()
    serializer_class = UserSerializer

I want to add a routable ad hoc method (detail = False)

At that time, it's a @ action decorator. (If the DRF version is older, this is the @ list_route or @ detail_route decorator. It looks like it's integrated into the @ action decorator.)

backend/src/api/users/views.py


from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.decorators import action  #add to
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    @action(detail=False, methods=['get'], url_path='get_user', url_name='get_user')
    def get_user(self, request, *kwargs):
        """Returns login user information
        """
        return UserSerializer(self.request.user).data

image.png

I want to add a routable ad hoc method (detail = True)

backend/src/api/users/views.py


from api.users.serializers import UserSerializer
from common.models import User
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    @action(detail=True, methods=['get'], url_path='get_user', url_name='get_user')
    def get_user(self, request, pk=None, **kwargs):
        """Returns login user information
        """
        #The process is defail=Same as False. I'm sorry(. _ .)
        return Response(
            status=status.HTTP_200_OK,
            data=UserSerializer(self.request.user).data)

It will be an API such as ʻapi_root / {pk} / url_name / `. image.png

I want to add a routable ad hoc method. I want to include the primary key in the URL, but False is good for the details of the @action decorator (see)

Such a selfish person is @ action decorator ʻurl_path`.

backend/src/api/users/views.py


from api.users.serializers import UserSerializer
from common.models import User
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    @action(detail=False, methods=['get'], url_path='get_user/(?P<user_id>[0-9]+)', url_name='get_user')
    def get_user(self, request, user_id=None):
        """Returns login user information
        """
        return Response(
            status=status.HTTP_200_OK,
            data=UserSerializer(self.request.user).data)

It may be recommended for those who do not like the URL generated by detail = True lol

image.png

I want to be able to search by query parameters

pip install django-filter

pip3 install django-filter==2.2.0

Create filters.py (file name is arbitrary) and define filters

backend/src/api/users/filters.py


from common.models import User
from django_filters import FilterSet


class UserSearchFilter(FilterSet):
    class Meta:
        model = User
        fields = '__all__'

        #Fields that you do not want to be specified in the query parameter should be added to exclude.
        # exclude = [
        #     'password',
        #     'date_created',
        #     'date_updated',
        # ]

Use the defined filter in views

backend/src/api/users/views.py


from api.users.serializers import UserSerializer
from common.models import User
from django_filters.rest_framework import DjangoFilterBackend  #add to
from api.users.filters import UserSearchFilter  #add to
from rest_framework.viewsets import ModelViewSet


class UserViewSets(ModelViewSet):
    filter_backends = [DjangoFilterBackend,]  #add to
    filter_class = UserSearchFilter  #add to
    queryset = User.objects.all()
    serializer_class = UserSerializer

You will be able to get by specifying the query parameters.

image.png

Serializer edition

Everyone loves Model Serializer

backend/src/api/users/serializers.py


from rest_framework.serializers import ModelSerializer
from common.models import User


class UserSerializer(ModelSerializer):
    """User serializer
    """
    class Meta:
        model = User
        fields = '__all__'

I want to narrow down the parameters

I don't want to throw groups or user_permissions as parameters

image.png

In such a case, specify ʻextra_kwargs`.

backend/src/api/users/serializers.py


from rest_framework.serializers import ModelSerializer
from common.models import User


class UserSerializer(ModelSerializer):
    """User serializer
    """
    class Meta:
        model = User
        fields = '__all__'
        extra_kwargs = {
            'is_superuser': {'read_only': True},
            'date_created': {'read_only': True},
            'date_deleted': {'read_only': True},
            'is_staff': {'read_only': True},
            'is_active': {'read_only': True},
            'groups': {'read_only': True},
            'user_permissions': {'read_only': True},
        }

I squeezed it safely ↓ image.png

Since it is only read-only (read_only), it will include the one specified in ʻextra_kwargs` at the time of acquisition (GET). image.png

I want to return the result of a function / method as a field value

For example, the value of the relation destination contains only the primary key.

In such a case, you can use SerializerMethodField. After defining the field, we will prepare a function for get_field name.

backend/src/api/users/serializers.py


import json  #add to
from django.core.serializers import serialize  #add to
from rest_framework.serializers import ModelSerializer, SerializerMethodField  #add to
from common.models import User


class UserSerializer(ModelSerializer):
    """User serializer
    """
    school = SerializerMethodField()  #Add field

    # get_Added function for field name
    def get_school(self, obj):
        """Returns a JSON serialized object of the school object

        Args:
            obj (User):User object
        
        Returns:
            dict:An object that serializes the User object into JSON format
        """
        if obj.school is not None:
            data = serialize('json', [obj.school,])
            objs = json.loads(data)
            return json.dumps(objs[0]['fields'])
        return None

    class Meta:
        ...

The school object was expanded to school, which was the primary key earlier (. _.) SerializerMethodField is convenient because you can return any value together.

image.png

I want to return the object of the relation destination. But I don't understand SerializerMethodField (Teru)

Such selfishness is a depth option.

backend/src/api/users/serializers.py


from common.models import User


class UserSerializer(ModelSerializer):
    """User serializer
    """
    class Meta:
        model = User
        fields = '__all__'
        extra_kwargs = {
            'is_superuser': {'read_only': True},
            'date_created': {'read_only': True},
            'date_deleted': {'read_only': True},
            'is_staff': {'read_only': True},
            'is_active': {'read_only': True},
            'groups': {'read_only': True},
            'user_permissions': {'read_only': True},
        }
        depth = 1  #The default is 0, so it doesn't make sense to specify 0

image.png

If you do depth = 2, it will get even further relations. (In this example, if school has more relations ahead, it will also return that object)

djangorestframework-jwt edition

I want to include user information in addition to token in the response of obtain_jwt_token

By default, it only returns token. image.png

If you want to include user information in addition to token here, create your own jwt_response_payload_handler. Create jwt_utils.py (file name is arbitrary)

backend/src/common/jwt_utils.py


def jwt_response_payload_handler(token, user=None, request=None):
    """JWT authentication custom response
    """
    return {
        'token': token,
        'first_login': user.first_login,
        'id': user.id,
    }

Modify settings.py.

backend/src/config/settings/settings.py


.
..
...
# JWT_Add AUTH
JWT_AUTH = {
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'common.jwt_utils.jwt_response_payload_handler',  # JWT_RESPONSE_PAYLOAD_HANDLER self-made jwt_response_payload_Specify handler
}

I was able to increase the response (. _.)

image.png

Django Project, Introduction to Django Rest Framework

pip install django-environ, Django and djangorest framework

pip3 install django-environ==0.4.5 Django==2.2.11 djangorestframework==3.11.0 mysqlclient==1.4.6

Create project root and backend src directories

mkdir -p project_root/backend/src

The directory structure is as follows

project_root #Project root
└── backend
    └── src

Django project creation

cd project_root/backend/src
django-admin startproject confing .

The directory structure is as follows (configuration files can be collected in config)

project_root #Project root
└── backend
     └── src
         ├── config
         │    ├── __init__.py
         │    ├── settings.py
         │    ├── urls.py
         │    └── wsgi.py
         │
         └── manage.py

Divide the configuration file into development and production

project_root #Project root
└── backend
     └── src
         ├── config
         │    ├── __init__.py
         │    ├── settings
         │    │    ├── settings.py
         │    │    ├── development.py  #For development
         │    │    └── production.py  #For production
         │    │
         │    ├── urls.py
         │    └── wsgi.py
         └── manage.py

Correction due to configuration file division

Modify settings.py (Fixed BASE_DIR to be the src directory)

backend/src/config/settings/settings.py


.
..
...
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # >> /src/confing/settings
 │
 ↓
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))  # >> /src
...
..
.

Fix manage.py

backend/src/manage.py


.
..
...
def main():
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings')
     │
     ↓
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings.development')  #Read development
...
..
.

Modify wsgi.py for deployment

backend/src/config/wsgi.py


os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings')
 │
 ↓
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings.production')

Fixed development.py

backend/src/config/settings/development.py


from confing.settings.settings import *

ALLOWED_HOSTS = ['*']

Modify production.py

backend/src/config/settings/development.py


from confing.settings.settings import *

Make DRF available

Modify settings.py

backend/src/config/settings.settings.py


INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',  #Postscript
]

Launch web and db container with Docker

Prepare docker-compose.yml and Dockerfile

project_root #Project root
├── backend
│    ├── src
│    │   ├── config
│    │   │    ├── __init__.py
│    │   │    ├── settings
│    │   │    │    ├── settings.py
│    │   │    │    ├── development.py
│    │   │    │    └── production.py
│    │   │    │
│    │   │    ├── urls.py
│    │   │    └── wsgi.py
│    │   |
│    │   └── manage.py
│    │
│    └── Dockerfile  #add to
│
└── docker-compose.yml  #add to

docker-compose.yml


version: '3'
services:
  web:
    build:
      context: ./
      dockerfile: ./backend/Dockerfile
    container_name: drf_web
    volumes:
      - './backend/src:/src'
    environment:
      - LC_ALL=ja_JP.UTF-8
    ports:
      - '8000:8000'
    depends_on:
      - db
    command: python3 manage.py runserver 0.0.0.0:8000
    restart: always
    tty: true

  db:
    image: mariadb:latest
    container_name: drf_db
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    environment:
      - MYSQL_ROOT_USER=root
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=db_drf
      - MYSQL_USER=user
      - MYSQL_PASSWORD=user
    volumes:
      - db_data:/var/lib/mysql
    ports:
      - '3306:3306'

volumes:
  db_data:
    driver: local
FROM ubuntu:18.04
RUN apt update && apt install -y locales python3-pip python3.7 python3-dev libssl-dev libffi-dev libgeos-dev libmysqlclient-dev
RUN apt-get update && apt-get install -y mysql-client python3-gdal
RUN mkdir /src \
    && rm -rf /var/lib/apt/lists/* \
    && echo "ja_JP UTF-8" > /etc/locale.gen \
    && locale-gen
WORKDIR /src
ADD ./backend/src /src/
RUN LC_ALL=ja_JP.UTF-8 pip3 install -r requirements.txt

Start Docker container

docker-compose up -d

Visit the Django startup page

Success if this screen appears image.png

Create an .env.development file

cd backend/src/
touch .env.development

Contents of the .env.development file

backend/src/.env.development


DEBUG=True
DATABASE_URL=mysql://user:user@db:3306/db_drf

Fixed development.py

backend/src/config/settings/development.py


import environ

ENV_FILE = os.path.join(BASE_DIR, '.env.development')
ENV = environ.Env()
ENV.read_env(ENV_FILE)

DEBUG = ENV.get_value('DEBUG', cast=bool)
DATABASES['default'] = ENV.db()  #Read DB connection information (.env.development DATABASE_Reads the URL)
...
..
.

Base model implementation

Create a base application

django-admin startapp base

Added to INSTALLED_APPS in settings.py

INSTALLED_APPS = [
    .
    ..
    ...
    'base',
]

Describe BaseModel

backend/src/base/models.py


from django.db import models
from django.utils import timezone

# Create your models here.
from django.db import models
from django.utils import timezone

# Create your models here.
class BaseModel(models.Model):
    """Base model
    """
    date_created = models.DateTimeField('Creation date and time', default=timezone.now)
    date_updated = models.DateTimeField('Last Modified', auto_now_add=True)
    date_deleted = models.DateTimeField('Delete date and time', null=True)

    class Meta:
        abstract = True  #← Required

Implementation of custom user model

Create a common application

django-admin startapp common

Added to INSTALLED_APPS in settings.py

INSTALLED_APPS = [
    .
    ..
    ...
    'base',
    'common',
]

First implement user manager

Create manager.py

cd backend/src/base
touch manager.py

backend/src/base/manager.py


from django.contrib.auth.models import UserManager


class UserManager(UserManager):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
    
    def _create_user(self, username, email, password, **extra_fields):
        """
        Create and save a user with the given username, email, and password.
        """
        if not username:
            raise ValueError('The given username must be set')
        email = self.normalize_email(email)
        username = self.model.normalize_username(username)
        user = self.model(username=username, email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, username, email=None, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(username, email, password, **extra_fields)

    def create_superuser(self, username, email, password, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_staff') is not True:
            raise ValueError('Superuser must have is_staff=True.')
        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(username, email, password, **extra_fields)

Custom user model implementation

The official recommends implementing a custom user model, so follow it (. _.)

backend/src/common/models.py


from django.db import models
from django.contrib.auth.base_user import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from base.manager import UserManager
from base.models import BaseModel


class User(AbstractBaseUser, PermissionsMixin, BaseModel):
    email = models.EmailField('mail address', blank=True, null=True)
    username = models.CharField('username', max_length=150, unique=True)
    display_name = models.CharField('Screen display name', max_length=30, blank=True, null=True)
    last_name = models.CharField('Surname', max_length=150, blank=True, null=True)
    first_name = models.CharField('Name', max_length=30, blank=True, null=True)
    is_staff = models.BooleanField('Staff flag', default=False)
    is_active = models.BooleanField('Valid flag', default=True)
    first_login = models.BooleanField('First login', default=True)

    objects = UserManager()

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = verbose_name_plural = 'users'
        db_table = 'user'

Modify settings.py

backend/src/config/settings/settings.py


.
..
...
AUTH_USER_MODEL = 'common.User'  #Specify a custom user model for the user model used for authentication.

If you forget to specify ʻAUTH_USER_MODEL`, you will suffer from the following error.

python3 manage.py makemigrations
SystemCheckError: System check identified some issues:

ERRORS:
auth.User.groups: (fields.E304) Reverse accessor for 'User.groups' clashes with reverse accessor for 'User.groups'.
	HINT: Add or change a related_name argument to the definition for 'User.groups' or 'User.groups'.
auth.User.user_permissions: (fields.E304) Reverse accessor for 'User.user_permissions' clashes with reverse accessor for 'User.user_permissions'.
	HINT: Add or change a related_name argument to the definition for 'User.user_permissions' or 'User.user_permissions'.
common.User.groups: (fields.E304) Reverse accessor for 'User.groups' clashes with reverse accessor for 'User.groups'.
	HINT: Add or change a related_name argument to the definition for 'User.groups' or 'User.groups'.
common.User.user_permissions: (fields.E304) Reverse accessor for 'User.user_permissions' clashes with reverse accessor for 'User.user_permissions'.
	HINT: Add or change a related_name argument to the definition for 'User.user_permissions' or 'User.user_permissions'.

Generate migration file

python3 manage.py makemigrations

Migration execution

python3 manage.py migrate

Recommended Posts

Django Rest Framework Tips
Django REST framework basics
Django REST framework stumbling block
Django REST framework with Vue.js
Login with django rest framework
[Django] Use MessagePack with Django REST framework
Django Template Tips
Create RESTful APIs with 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 ②
Miscellaneous notes about the Django REST framework
CRUD POST with Nuxt & Django REST Framework
CRUD GET with Nuxt & Django REST Framework ①
Django REST Framework + Clean Architecture Design Consideration
Django python web framework
CRUD PUT, DELETE with Nuxt & Django REST Framework
Django REST framework A little useful to know.
Implement JWT login functionality in Django REST framework
Implementing authentication in Django REST Framework with djoser
Create a Todo app with Django REST Framework + Angular
More new user authentication methods with Django REST Framework
Create a Todo app with the Django REST framework
Create APIs around user authentication with Django REST Framework
When you want to filter with Django REST framework
List method for nested resources in Django REST framework
Implement APIs at explosive speed using Django REST Framework
Implement hierarchical URLs with drf-nested-routers in Django REST framework
How to write custom validations in the Django REST Framework
How to reset password via API using Django rest framework
Django rest framework decorators ʻaction decorator replaces list_route and detail_route`
Implementation of CRUD using REST API with Python + Django Rest framework + igGrid
Django
Install Python framework django using pip
Create a REST API to operate dynamodb with the Django REST Framework
Implementation of custom user model authentication in Django REST Framework with djoser
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
Solution when Not Found appears when hitting the Django REST Framework API from the outside
How to automatically generate API document with Django REST framework & POST from document screen