バージョン情報
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 |
Swaggerを使用したい
django-rest-swaggerをpipインストール
pip3 install django-rest-swagger==2.2.0
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',
)
}
development.pyを修正
.
..
...
INSTALLED_APPS += [
'rest_framework_swagger',
]
REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS'] = 'rest_framework.schemas.coreapi.AutoSchema'
urls.pyを修正
from django.contrib import admin
from django.urls import path
from django.conf import settings # 追記
from rest_framework_jwt.views import obtain_jwt_token # 追記
urlpatterns = [
path('admin/', admin.site.urls),
url('api/v1/login/', obtain_jwt_token), # なんか1つ以上APIがないとSwagger-UIを開けないみたいなので、とりあえずログインのAPIを登録
]
# 以下全て追記
if settings.DEBUG: # DEBUG=True(開発時)の場合のみ使用
from rest_framework.schemas import get_schema_view
from rest_framework_swagger import renderers
schema_view = get_schema_view(
title='API一覧',
public=True,
renderer_classes=[renderers.OpenAPIRenderer, renderers.SwaggerUIRenderer])
urlpatterns += [
url(API_ROOT + 'api-auth/', include('rest_framework.urls')),
url('swagger-ui/', schema_view),
]
Swagger-uiにアクセス
http://0.0.0.0:8000/swagger-ui/にアクセスして下記のような画面が出たら右上のLog in
を押下してログインする
ViewSets編
とりあえずモデルに対するGET, POST, PUT, DELETE...のAPIを公開したい
みんな大好きModelViewSets
が使えます。
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リクエストメソッドは絞りたい
方法1: GenericViewSetと各種mixinsを組み合わせる
後でリクエストメソッドを増やしたいといった場合は対応するmixinsをつけてあげればいい
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.viewsets import GenericViewSet # コレ
from rest_framework.mixins import CreateModelMixin # コレ
class UserViewSets(GenericViewSet, CreateModelMixin):
queryset = User.objects.all()
serializer_class = UserSerializer
方法2: genericsを使用する
後でリクエストメソッドを増やしたいといった場合は対応するAPIViewをつけてあげればいい
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.generics import CreateAPIView # コレ
class UserViewSets(CreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
genericsの場合、urls.pyの指定方法が異なる
# =========== viewsets ===========
router = DefaultRouter()
router.register('user', user_views.UserViewSets) # viewsetsの場合はrouterが使える
urlpatterns = [
path('admin/', admin.site.urls),
url('api/v1/login/', obtain_jwt_token),
url('', include(router.urls)),
]
# =========== generics ===========
urlpatterns = [
path('admin/', admin.site.urls),
url('api/v1/login/', obtain_jwt_token),
url('user/', user_views.UserViewSets.as_view()), # urlpatternsにas_views()で追加する
]
ModelViewSetを使いたい、でもHTTPリクエストメソッドは絞りたい(照)
そんなワガママさんにはhttp_method_names
じゃ。
実装はしたけどまだ公開したくない、みたいな場面で使うのかな・・・。
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リクエストメソッドは大文字じゃなくて、小文字(重要)
APIをリクエストできるのは認証済みのユーザーだけに絞りたい
permission_classにIsAuthenticated
を指定しましょう。
まずsettings.pyを修正します。
.
..
...
REST_FRAMEWORK = {
# 追加
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
# 'common.permissions.IsSuperuser', # 自作のpermissionを作成した場合は、ここに追加。
),
...
..
.
}
未認証状態でAPIをリクエストすると、403
が返ってきます。
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.permissions import IsAuthenticated # 追加
from rest_framework.viewsets import ModelViewSet
class UserViewSets(ModelViewSet):
permission_classes = [IsAuthenticated,] # 追加
queryset = User.objects.all()
serializer_class = UserSerializer
ルーティング可能なアドホックメソッドを追加したい(detail=False)
そんな時は@action
デコレータじゃ。
(DRFバージョンが古いと、@list_route
や@detail_route
デコレータがこれにあたります。@action
デコレータに統合されたようですね。)
from api.users.serializers import UserSerializer
from common.models import User
from rest_framework.decorators import action # 追加
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):
""" ログインユーザー情報を返す
"""
return UserSerializer(self.request.user).data
ルーティング可能なアドホックメソッドを追加したい(detail=True)
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):
""" ログインユーザー情報を返す
"""
# 処理はdefail=Falseと同じです。すみません(. _ .)
return Response(
status=status.HTTP_200_OK,
data=UserSerializer(self.request.user).data)
api_root/{pk}/url_name/
といったAPIになります。
ルーティング可能なアドホックメソッドを追加したい。URLに主キー含めたいけど、@actionデコレータのdetailはFalseがいい(照)
そんなワガママさんは@action
デコレーターのurl_path
じゃ。
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):
""" ログインユーザー情報を返す
"""
return Response(
status=status.HTTP_200_OK,
data=UserSerializer(self.request.user).data)
detail=True
で生成されるURLが気に食わない人にはおすすめかもしれません笑
クエリパラメータでの検索を可能にしたい
django-filterをpipインストール
pip3 install django-filter==2.2.0
filters.py(ファイル名は任意)を作成して、filterを定義する
from common.models import User
from django_filters import FilterSet
class UserSearchFilter(FilterSet):
class Meta:
model = User
fields = '__all__'
# クエリパラメータに指定されたくないフィールドはexcludeに追加してあげると良い
# exclude = [
# 'password',
# 'date_created',
# 'date_updated',
# ]
定義したfilterをviewsで使用する
from api.users.serializers import UserSerializer
from common.models import User
from django_filters.rest_framework import DjangoFilterBackend # 追加
from api.users.filters import UserSearchFilter # 追加
from rest_framework.viewsets import ModelViewSet
class UserViewSets(ModelViewSet):
filter_backends = [DjangoFilterBackend,] # 追加
filter_class = UserSearchFilter # 追加
queryset = User.objects.all()
serializer_class = UserSerializer
クエリパラメータを指定してGETできるようになります。
Serializer編
みんな大好きModelSerializer
from rest_framework.serializers import ModelSerializer
from common.models import User
class UserSerializer(ModelSerializer):
""" ユーザーシリアライザー
"""
class Meta:
model = User
fields = '__all__'
パラメータを絞りたい
groupsとかuser_permissionsとかをパラメータで投げられたくない
そんな時はextra_kwargs
に指定します。
from rest_framework.serializers import ModelSerializer
from common.models import User
class UserSerializer(ModelSerializer):
""" ユーザーシリアライザー
"""
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},
}
読み取り専用(read_only)にしただけなので、取得(GET)時はextra_kwargs
に指定したものも含めてくれます。
関数/メソッドの結果をフィールドの値として返したい
例えば、リレーション先の値とかは主キーだけが入った状態です。
※下記画像school
こんな時はSerializerMethodField
が使えます。
フィールドを定義したら、get_フィールド名
の関数を用意してあげます。
import json # 追加
from django.core.serializers import serialize # 追加
from rest_framework.serializers import ModelSerializer, SerializerMethodField # 追加
from common.models import User
class UserSerializer(ModelSerializer):
""" ユーザーシリアライザー
"""
school = SerializerMethodField() # フィールド追加
# get_フィールド名の関数追加
def get_school(self, obj):
""" schoolオブジェクトをJSONシリアライズしたオブジェクトを返す
Args:
obj (User): Userオブジェクト
Returns:
dict: UserオブジェクトをJSON形式にシリアライズしたオブジェクト
"""
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:
...
さっき主キーだったschool
に、schoolオブジェクトが展開されました(. _ .)
SerializerMethodFieldを使えば割と任意の値を一緒に返せるので便利です。
リレーション先のオブジェクトを返したい。でもSerializerMethodFieldよくわからん(照)
そんなワガママさんはdepth
オプションじゃ。
from common.models import User
class UserSerializer(ModelSerializer):
""" ユーザーシリアライザー
"""
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 # デフォルトは0なので、0を指定しても意味ない
depth = 2
とかすると、さらに先のリレーションまで取得してくれます。(この例だと、school
がさらにリレーションを先を持っている場合、そのオブジェクトも返してくれる)
※ただ、POSTで新規登録する場合めんどくさくなるのでやっぱりSerializerMethodField
がオススメです。GETでしか使わない場合とかには有効かもしれない。
djangorestframework-jwt編
obtain_jwt_tokenのレスポンスにtokenの他にユーザー情報も含めたい
ここにtokenの他にユーザー情報を含めたい場合はjwt_response_payload_handler
を自作します。
jwt_utils.pyを作成(ファイル名は任意)
def jwt_response_payload_handler(token, user=None, request=None):
""" JWT認証のカスタムレスポンス
"""
return {
'token': token,
'first_login': user.first_login,
'id': user.id,
}
settings.pyを修正します。
.
..
...
# JWT_AUTHを追加
JWT_AUTH = {
'JWT_RESPONSE_PAYLOAD_HANDLER': 'common.jwt_utils.jwt_response_payload_handler', # JWT_RESPONSE_PAYLOAD_HANDLERに自作のjwt_response_payload_handlerを指定する
}
レスポンスを増やすことができました(. _ .)
Djangoプロジェクト, DjangoRestFrameworkの導入編
django-environ, Djangoとdjangorestframeworkをpipインストール
pip3 install django-environ==0.4.5 Django==2.2.11 djangorestframework==3.11.0 mysqlclient==1.4.6
プロジェクトルートとバックエンドのsrcディレクトリを作成
mkdir -p project_root/backend/src
下記の様なディレクトリ構成になる
project_root # プロジェクトルート
└── backend
└── src
Djangoプロジェクト作成
cd project_root/backend/src
django-admin startproject confing .
下記の様なディレクトリ構成になる(configに設定ファイルをまとめられる)
project_root # プロジェクトルート
└── backend
└── src
├── config
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
│
└── manage.py
設定ファイルを開発用と本番用とで分割する
project_root # プロジェクトルート
└── backend
└── src
├── config
│ ├── __init__.py
│ ├── settings
│ │ ├── settings.py
│ │ ├── development.py # 開発用
│ │ └── production.py # 本番用
│ │
│ ├── urls.py
│ └── wsgi.py
└── manage.py
設定ファイル分割に伴う修正
settings.pyを修正
(BASE_DIRがsrc
ディレクトリになるよう修正)
.
..
...
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
...
..
.
manage.pyを修正
.
..
...
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings')
│
↓
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings.development') # developmentを読み込む様にする
...
..
.
デプロイを考慮してwsgi.pyを修正
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings')
│
↓
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'confing.settings.production')
development.pyを修正
from confing.settings.settings import *
ALLOWED_HOSTS = ['*']
production.pyを修正
from confing.settings.settings import *
DRFを使えるようにする
settings.pyを修正
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # 追記
]
Dockerでwebとdbコンテナを立ち上げる
docker-compose.ymlとDockerfileを用意する
project_root # プロジェクトルート
├── backend
│ ├── src
│ │ ├── config
│ │ │ ├── __init__.py
│ │ │ ├── settings
│ │ │ │ ├── settings.py
│ │ │ │ ├── development.py
│ │ │ │ └── production.py
│ │ │ │
│ │ │ ├── urls.py
│ │ │ └── wsgi.py
│ │ |
│ │ └── manage.py
│ │
│ └── Dockerfile # 追加
│
└── 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
Dockerコンテナ起動
docker-compose up -d
※もしかしたらwebコンテナがDB接続エラーで立ち上げ失敗してしまうかもしれませんが、その時はdocker-compose stop
してからdocker-compose up -d
してください。その対応が微妙だという方はwait-for-it.sh
の導入を検討してください。
Djangoのスタートアップページにアクセスする
.env.developmentファイルを作成
cd backend/src/
touch .env.development
.env.developmentファイルの内容
DEBUG=True
DATABASE_URL=mysql://user:user@db:3306/db_drf
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() # DB接続情報読み込み(.env.developmentのDATABASE_URLを読み込んでくれる)
...
..
.
ベースモデルの実装
baseアプリケーションを作成
django-admin startapp base
settings.pyのINSTALLED_APPSに追記
INSTALLED_APPS = [
.
..
...
'base',
]
BaseModelを記述
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):
""" ベースモデル
"""
date_created = models.DateTimeField('作成日時', default=timezone.now)
date_updated = models.DateTimeField('最終更新日時', auto_now_add=True)
date_deleted = models.DateTimeField('削除日時', null=True)
class Meta:
abstract = True # ← 必須
カスタムユーザーモデルの実装
commonアプリケーションを作成
django-admin startapp common
settings.pyのINSTALLED_APPSに追記
INSTALLED_APPS = [
.
..
...
'base',
'common',
]
まずユーザーマネージャーを実装
manager.pyを作成
cd backend/src/base
touch 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)
カスタムユーザーモデル実装
公式がカスタムユーザーモデルの実装を推奨しているので従いましょう(. _ .)
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('メールアドレス', blank=True, null=True)
username = models.CharField('ユーザー名', max_length=150, unique=True)
display_name = models.CharField('画面表示名', max_length=30, blank=True, null=True)
last_name = models.CharField('姓', max_length=150, blank=True, null=True)
first_name = models.CharField('名', max_length=30, blank=True, null=True)
is_staff = models.BooleanField('スタッフフラグ', default=False)
is_active = models.BooleanField('有効フラグ', default=True)
first_login = models.BooleanField('初回ログイン', default=True)
objects = UserManager()
EMAIL_FIELD = 'email'
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
class Meta:
verbose_name = verbose_name_plural = 'users'
db_table = 'user'
settings.pyを修正する
.
..
...
AUTH_USER_MODEL = 'common.User' # 認証に使用するユーザーモデルに、カスタムユーザーモデルを指定。
AUTH_USER_MODEL
の指定を忘れると、以下のようなエラーで悩まされます。
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'.
マイグレーションファイルを生成
python3 manage.py makemigrations
マイグレーション実行
python3 manage.py migrate