Django REST Framework を使用したバックエンド開発
はじめに
最近、DRFとReactの勉強をしていたのですが、基本的なToDoアプリを作成した後に何を作ろうか迷ってました。
そんな中で以下のURLを見つけて、Mediumのようなアプリを構築するのにいろんなフレームワークで書かれているコードが用意されていました。そのため、似たようなアプリをフロントエンドはReactで、バックエンドはDRFを選択して作っていこうと思います。
元のRealWorldと異なる点としては、1つ目はダッシュボードも作成したいので値段がついた本を記録するようなアプリに変更したこと、2つ目は練習として最低限の書き方の習得を目指したのでフォローや記事のお気に入りは除きました。フォローや記事のお気に入りは以下のGitHubの方のコードをそのまま追記すれば動くと思います。
[https://codebase.show/projects/realworld?category=frontend&language=all:embed:cite]
[https://github.com/gothinkster/django-realworld-example-app:embed:cite]
コード
djangoプロジェクト作成
カスタムユーザー用にaccounts
、APIのVersion切り替えが可能な用にapiv1
、本用にbooks
アプリを作成します。
mkdir backend
cd backend/
django-admin startproject config .
python manage.py startapp accounts
python manage.py startapp apiv1
python manage.py startapp books
`config/settings.py``を変えて、アプリなどのインポートをします。
以下では、APIドキュメント用の設定(drf_spectacular)やカスタムユーザーモデルでのJWT認証の設定も追加しています。
from datetime import timedelta
from pathlib import Path
...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3dr paty apps
'rest_framework',
'django_filters',
'drf_spectacular',
'djoser',
'rest_framework_simplejwt',
# my apps
'accounts.apps.AccountsConfig',
'apiv1.apps.Apiv1Config',
'books.apps.BooksConfig',
]
...
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
...
# REST_FRAMEWORK
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
}
# custom user model
AUTH_USER_MODEL = 'accounts.User'
# Configure the JWT settings
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
}
config/urls.py
にAPIドキュメントを開く際のパスを書いておきます。これによりDEBUG時にapi/schema/
にアクセスすることでAPIドキュメントを見ることができます。
from django.conf import settings
from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
urlpatterns = [
path('admin/', admin.site.urls),
]
if settings.DEBUG:
urlpatterns += [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'), # 追加
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), # 追加
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), # 追加
]
カスタムユーザーモデルのJWT認証
ユーザーモデルは後で変えようと思った時に大変なため、最初の段階でカスタムユーザーモデルを実装します。
また、認証はrest_framework_simplejwt
とdjoser
を用いてJWT認証を実装します。
カスタムユーザーモデルを作る前にmigrate
してしまうとエラーが発生する可能性があるので注意してください。
accounts/models.py
に以下のようにUser
モデルとカスタムユーザーモデルに必須なCustomUserManager
を定義しています。
ここでログイン時に必要なフィールドとしてemail
を指定しています。また、今回は必要ないのですが、次のアプリの時用に役職role
フィールドを追加しています。
import uuid
from django.db import models
from django.contrib.auth.models import (
AbstractBaseUser, BaseUserManager, PermissionsMixin
)
class User(AbstractBaseUser, PermissionsMixin):
# Choices of ROLE
ADMIN = 1
MANAGER = 2
EMPLOYEE = 3
ROLE_CHOICES = (
(ADMIN, 'Admin'),
(MANAGER, 'Manager'),
(EMPLOYEE, 'Employee')
)
id = models.UUIDField(primary_key=True, unique=True, editable=False, default=uuid.uuid4)
email = models.EmailField(db_index=True, unique=True)
username = models.CharField(db_index=True, max_length=50, unique=True)
role = models.PositiveSmallIntegerField(choices=ROLE_CHOICES, blank=True, null=True, default=3)
# ユーザーを削除せずにアクティブではなくするためのFlag
is_active = models.BooleanField(default=True)
# admin pageにログインできるかどうかのFlag
is_staff = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# ログイン時に使用するField
USERNAME_FIELD = 'email'
# 必要なField
REQUIRED_FIELDS = ['username']
# Custom User objectsを管理するためのMnagerクラス
objects = CustomUserManager()
def __str__(self):
return self.email
以下のようにカスタムユーザーモデルのためにManagerクラスを定義します。
class CustomUserManager(BaseUserManager):
"""
custom userの場合、custom User Managerを作成する必要がある。
userとsuperuserを作る関数のみを上書きする。
"""
def create_user(self, username, email, password, **extra_fields):
if not username:
raise ValueError('The username must be set.')
if not email:
raise ValueError('The email must be set.')
if not password:
raise ValueError('The password must be set.')
user = self.model(username=username, email=self.normalize_email(email), **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, username, email, password, **extra_fields):
if not username:
raise ValueError('The username must be set.')
if not email:
raise ValueError('The email must be set.')
if not password:
raise ValueError('The password must be set.')
extra_fields.setdefault('is_active', True)
extra_fields.setdefault('role', 1)
if extra_fields.get('role') != 1:
raise ValueError('Superuser must have role of Global Admin')
user = self.create_user(username, email, password, **extra_fields)
user.is_superuser = True
user.is_staff = True
user.save()
return user
accounts/urls.py
にdjoser
を使用したパスを追加します。
from django.urls import path, include
urlpatterns = [
path('',include('djoser.urls')),
path('',include('djoser.urls.jwt')),
]
また、config/urls.py
にaccounts/urls.py
を追加するコードを書きます。
from django.conf import settings
from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/auth/', include('accounts.urls')),
]
if settings.DEBUG:
urlpatterns += [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'), # 追加
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), # 追加
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), # 追加
]
カスタムユーザー周りの最後にadminページでユーザー情報をいじれるようにしていきます。
こちらを参考にしました。
account/forms.py
を新しく作成します。
from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from .models import User
class UserCreationForm(forms.ModelForm):
password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(
label='Password confirmation', widget=forms.PasswordInput)
class Meta:
model = User
fields = ('username', 'email')
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError("Passwords don't match")
return password2
def save(self, commit=True):
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField()
class Meta:
model = User
fields = ('username', 'email', 'password',
'role', 'is_active', 'is_staff')
また、accounts/admin.py
に以下を追記します。
ここでは、User
を追加し、デフォルトのGroup
を削除してます。
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from .forms import UserChangeForm, UserCreationForm
from .models import User
# Register your models here.
class UserAdmin(BaseUserAdmin):
form = UserChangeForm
add_form = UserCreationForm
list_display = ('email', 'username', 'role', 'is_active', 'is_staff')
list_filter = ('is_staff',)
fieldsets = (
(None, {'fields': ('username', 'email', 'password')}),
('Personal info', {'fields': ('role',)}),
('Permissions', {'fields': ('is_staff', 'is_active')}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'username', 'password1', 'password2')}
),
)
search_fields = ('email',)
ordering = ('email',)
filter_horizontal = ()
admin.site.register(User, UserAdmin)
admin.site.unregister(Group)
ここまで来たらmigrate
やユーザーの作成を行い、動作確認をしていきましょう。
ここでsuperuserを作成するときに独自の入力としてemailとusername、passwordが聞かれます。
(ここでは、デモなのでadmin@example.com, admin, pass12345を設定しました。)
python manage.py makemigrations
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
djoser
のendpointsとしては以下があります。
/users/
/users/me/
/users/confirm/
/users/resend_activation/
/users/set_password/
/users/reset_password/
/users/reset_password_confirm/
/users/set_username/
/users/reset_username/
/users/reset_username_confirm/
/token/login/ (Token Based Authentication)
/token/logout/ (Token Based Authentication)
/jwt/create/ (JSON Web Token Authentication)
/jwt/refresh/ (JSON Web Token Authentication)
/jwt/verify/ (JSON Web Token Authentication)
以下のように使用することができます。
新規ユーザーの作成
以下を実行した後に、実際にユーザーが作成されているかどうかhttp://localhost:8000/admin/
で確認しましょう。
curl -X POST http://127.0.0.1:8000/api/auth/users/ --data 'email=test1@example.com&username=test1&password=pass12345'
この段階ではユーザーが新規に作成されただけなので、以下のようなログイン情報を取得できません。
curl -LX GET http://127.0.0.1:8000/api/auth/users/me/
ログイン
先ほど作成したユーザー情報でjwt/create/
にアクセスすることでトークンを作成できます。
ちなみに、作成されていないユーザーですとアクティブなユーザーでないというレスポンスが返されます。
curl -X POST http://127.0.0.1:8000/api/auth/jwt/create/ --data 'email=test1@example.com&username=test1&password=pass12345'
上で取得したaccess tokenを利用するとログイン情報を取得できます。
curl -LX GET http://127.0.0.1:8000/api/auth/users/me/ -H 'Authorization: Bearer <access_token>'
パスワードの変更
何が違うのかわからないですが、uidの箇所でエラーが出て、users/reset_password_confirm/
がうまくできませんでした。
とりあえず、管理者側からは以下のようなコマンドで変更できます。
python manage.py changepassword <user_name>
Book作成
books/models.py
にBookモデルを定義します。この時、作成者をowner
として定義し、ユーザーが削除されても関連するレコードが削除されないようにon_delete=models.SET_NULL
を指定します。
import uuid
from django.db import models
class Book(models.Model):
""" Book Model """
class Meta:
db_table = 'book'
ordering = ['created_at',]
verbose_name = verbose_name_plural = '本'
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(verbose_name="タイトル", max_length=50)
content = models.TextField(verbose_name="内容", max_length=1000, null=True, blank=True)
price = models.IntegerField(verbose_name='価格', null=True)
owner = models.ForeignKey('accounts.User', verbose_name="作成者", on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
また、adminでいじれるようにbooks/admin.py
に以下を追記します。
from django.contrib import admin
from .models import Book
class BookModelAdmin(admin.ModelAdmin):
list_display = ('title', 'content', 'price', 'owner','created_at', 'updated_at',)
ordering = ('-created_at',)
readonly_fields = ('id', 'owner', 'created_at', 'updated_at', )
admin.site.register(Book, BookModelAdmin)
新しくテーブルを作ります。
その後、http://localhost:8000/admin/
にアクセスすると
python manage.py makemigrations books
python manage.py migrate books
Foreign keyがある場合は以下のようにapiv1/serializers.py
にSerializerを定義します。
from rest_framework import serializers
from books.models import Book
from accounts.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('username', 'email',)
read_only_fields = ('username', 'email',)
class BookSerializer(serializers.ModelSerializer):
""" Serializer for Book Model """
owner = UserSerializer(read_only=True)
class Meta:
model = Book
# publisher_id: モデルには存在しない追加する新フィールド
fields = ['id', 'title', 'content', 'price', 'owner']
apiv1/views.py
に以下のようなViewを設定します。 serializer.save(owner=request.user)
で作成者を保存登録してます。
import re
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from drf_spectacular.utils import extend_schema
from rest_framework import status, views, viewsets, generics, mixins
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import (
AllowAny, IsAuthenticated, IsAuthenticatedOrReadOnly
)
from books.models import Book
from .serializers import BookSerializer
class StandardResultsSetPagination(LimitOffsetPagination):
default_limit = 2
max_limit = 100
class BookFilter(filters.FilterSet):
""" Bookモデル用のフィルタ """
# 「price__lte」というキーで「priceが?円以下」という条件でフィルタリング可能に
price__lte = filters.NumberFilter(field_name='price', lookup_expr='lte')
class Meta:
model = Book
# フィルタリング可能なfieldを指定
fields = '__all__'
class BookFilter(filters.FilterSet):
""" Bookモデル用のフィルタ """
# 「price__lte」というキーで「priceが?円以下」という条件でフィルタリング可能に
price__lte = filters.NumberFilter(field_name='price', lookup_expr='lte')
class Meta:
model = Book
# フィルタリング可能なfieldを指定
fields = '__all__'
class BookViewSet(mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet):
queryset = Book.objects.all()
#permission_classes = (IsAuthenticated,)
serializer_class = BookSerializer
pagination_class = StandardResultsSetPagination
def create(self, request):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(owner=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request):
#queryset = self.filter_queryset(self.get_queryset())
filterset = BookFilter(request.query_params, queryset=self.queryset)
if not filterset.is_valid():
raise ValidationError(filterset.errors)
page = self.paginate_queryset(filterset.qs)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(filterset.qs, many=True)
return Response(serializer.data)
def retrieve(self, request, pk):
book = get_object_or_404(Book, pk=pk)
serializer = self.serializer_class(instance=book)
return Response(serializer.data, status=status.HTTP_200_OK)
def update(self, request, pk):
instance = get_object_or_404(Book, pk=pk)
serializer = self.serializer_class(
instance,
data=request.data,
partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, pk):
book = get_object_or_404(Book, pk=pk)
book.delete()
return Response(None, status=status.HTTP_204_NO_CONTENT)
最後にapiv1/urls.py
にルートを記載します。
from django.urls import path, include
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register('books', views.BookViewSet)
app_name = 'apiv1'
urlpatterns = [
path('', include(router.urls)),
]
CREATE
以下のコマンドでユーザーに紐づいたBookオブジェクトを登録できます。
curl -X POST http://127.0.0.1:8000/api/v1/books/ --data 'title=title6' -H 'Authorization: Bearer <access_token>'
GET
curl -LX GET http://127.0.0.1:8000/api/v1/books/ -H 'Authorization: Bearer <access_token>'
RETRIEVE
curl -LX GET http://127.0.0.1:8000/api/v1/books/<book_id>/ -H 'Authorization: Bearer <access_token>'
PUT
curl -X PUT http://127.0.0.1:8000/api/v1/books/<book_id>/ --data 'content=changed' -H 'Authorization: Bearer <access_token>'
DESTOROY
curl -LX DELETE http://127.0.0.1:8000/api/v1/books/<book_id>/ -H 'Authorization: Bearer <access_token>'
コメント実装
books/models.py
に以下のモデルを定義します。
class Comment(models.Model):
""" Comment Model """
class Meta:
db_table = 'comment'
ordering = ['created_at',]
verbose_name = verbose_name_plural = 'コメント'
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
body = models.TextField()
book = models.ForeignKey('books.Book', related_name='comments', on_delete=models.CASCADE)
owner = models.ForeignKey('accounts.User', related_name='comments', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
books/admin.py
に以下を追記します。
class CommentModelAdmin(admin.ModelAdmin):
list_display = ('body', 'book', 'owner','created_at', 'updated_at',)
ordering = ('-created_at',)
readonly_fields = ('id', 'created_at', 'updated_at', )
admin.site.register(Comment, CommentModelAdmin)
モデルを追加したのでマイグレートしておきましょう。
python manage.py makemigrations books
python manage.py migrate books
Bookと同じようにコメントのシリアライザを作成します。
from books.models import Book, Comment
class CommentSerializer(serializers.ModelSerializer):
""" Serializer for Comment Model """
owner = UserSerializer(read_only=True)
book = BookSerializer(read_only=True)
class Meta:
model = Comment
fields = ['id', 'body', 'book', 'owner']
viewは以下のように作成します。
books/<book_id>/comments/'
というURLでアクセスしたいので、lookup_field
とlookup_url_kwarg
にbook__id
とbook_id
を設定しています。これにより、BookのUIDに紐づいたコメントを作成/取得できます。
createはBookViewSetのcreateとほとんど同じでbook instanceを取得してownerと同時に登録しています。
また、listでは、filter_queryset
によりbook_idでフィルタリングを行い、bookに関連するコメントのみ取得しています。
class CommentListCreateAPIView(mixins.CreateModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
lookup_field = 'book__id'
lookup_url_kwarg = 'book_id'
queryset = Comment.objects.all()
#permission_classes = (IsAuthenticated,)
serializer_class = CommentSerializer
def filter_queryset(self, queryset):
# The built-in list function calls `filter_queryset`. Since we only
# want comments for a specific article, this is a good place to do
# that filtering.
filters = {self.lookup_field: self.kwargs[self.lookup_url_kwarg]}
return queryset.filter(**filters)
def create(self, request, book_id=None):
print("book_id", book_id)
book = get_object_or_404(Book, id=book_id)
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(owner=request.user, book=book)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class CommentDestroyAPIView(mixins.DestroyModelMixin,
viewsets.GenericViewSet):
queryset = Comment.objects.all()
#permission_classes = (IsAuthenticated,)
serializer_class = CommentSerializer
def destroy(self, request, pk):
comment = get_object_or_404(Comment, pk=pk)
comment.delete()
return Response(None, status=status.HTTP_204_NO_CONTENT)
apiv1/urls.py
にViewを追加します。routerで登録しない場合はas_view()
の中でpost, getなどを指定してあげる必要があります。
from django.urls import path, include
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register('books', views.BookViewSet)
app_name = 'apiv1'
urlpatterns = [
path('', include(router.urls)),
path('books/<book_id>/comments/', views.CommentListCreateAPIView.as_view({'post': 'create', 'get': 'list'})),
path('books/comments/<pk>/', views.CommentDestroyAPIView.as_view({'get': 'destroy'})),
]
コメント追加
curl -X POST http://127.0.0.1:8000/api/v1/books/<book_id>/comments/ --data 'body=body1' -H 'Authorization: Bearer <access_token>'
コメント取得
curl -LX GET http://127.0.0.1:8000/api/v1/books/<book_id>/comments/
コメント削除
curl -LX GET http://127.0.0.1:8000/api/v1/books/comments/<comment_id>
タグ作成
本につけるタグを作成します。
まずはbooks/models.py
にTagモデルを作成していきます。
また、タグと本は多対多の関係なので、models.ManyToManyField
を使用しています。
class Tag(models.Model):
""" Tag Model """
class Meta:
db_table = 'tag'
ordering = ['created_at',]
verbose_name = verbose_name_plural = 'タグ'
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
content = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.content
class Book(models.Model):
""" Book Model """
class Meta:
db_table = 'book'
ordering = ['created_at',]
verbose_name = verbose_name_plural = '本'
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
title = models.CharField(verbose_name="タイトル", max_length=50)
content = models.TextField(verbose_name="内容", max_length=1000, null=True, blank=True)
price = models.IntegerField(verbose_name='価格', null=True)
owner = models.ForeignKey('accounts.User', verbose_name="作成者", on_delete=models.SET_NULL, null=True, related_name='books')
tags = models.ManyToManyField('books.Tag', related_name='books')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
apiv1/serializers.py
を以下のように作成します。
ここで、タグを本につけて登録する際に、今までにないタグの場合は同時に登録したいのでserializers.RelatedField
を継承したFieldを作成しています。
継承している箇所は難しいのですが、既存のPrimaryKeyRelatedField
の中身を見ると理解しやすいです。(こちらを参考にしました。)
to_representation
はオブジェクトのpkを返すようになっています。また、to_internal_value
ではそれをdata
という引数で受け取って、モデルインスタンスを返す処理になっているようです。
class PrimaryKeyRelatedField(RelatedField):
def to_internal_value(self, data):
return self.get_queryset().get(pk=data)
def to_representation(self, value):
return value.pk
今回はTagを受け取った際にそのTagが未登録である場合も考えられます。
そのため、to_internal_value
の中でTag.objects.get_or_create(**data)
を使用して、
未登録であればCreateする、登録済みであればGetするという処理になっています。
class TagSerializer(serializers.ModelSerializer):
""" Serializer for Tag Model """
class Meta:
model = Tag
fields = ['id', 'content']
class TagRelatedField(serializers.RelatedField):
""" methodがいつ呼び出されるか不明?? """
def get_queryset(self):
return Tag.objects.all()
def to_internal_value(self, data):
""" tagがすでに登録されていないTagの時はcreate, ある時はget """
# data = {"content": "something"}
tag, created = Tag.objects.get_or_create(**data)
return tag
def to_representation(self, value):
return value.content
class BookSerializer(serializers.ModelSerializer):
""" Serializer for Book Model """
owner = UserSerializer(read_only=True)
tagList = TagRelatedField(many=True, source='tags')
class Meta:
model = Book
# publisher_id: モデルには存在しない追加する新フィールド
fields = ['id', 'title', 'content', 'price', 'owner', 'tagList']
def create(self, validated_data):
""" tagを同時に追加できるようにする """
tags = validated_data.pop('tags', [])
book = Book.objects.create(**validated_data)
for tag in tags:
book.tags.add(tag)
return book
BookのViewは変える必要ないのですが、Tagを取得するViewだけ追加しておきます。
apiv1/views.py
に以下を追加します。
from books.models import Book, Comment, Tag
from .serializers import BookSerializer, CommentSerializer, TagSerializer
class TagFilter(filters.FilterSet):
""" Tagモデル用のフィルタ """
class Meta:
model = Tag
# フィルタリング可能なfieldを指定
fields = '__all__'
class TagListAPIView(mixins.ListModelMixin,
viewsets.GenericViewSet):
queryset = Tag.objects.all()
#permission_classes = (IsAuthenticated,)
serializer_class = TagSerializer
def list(self, request):
#queryset = self.filter_queryset(self.get_queryset())
filterset = TagFilter(request.query_params, queryset=self.queryset)
if not filterset.is_valid():
raise ValidationError(filterset.errors)
serializer = self.get_serializer(filterset.qs, many=True)
return Response(serializer.data)
これに合うようにURLを追加しましょう。
app_name = 'apiv1'
urlpatterns = [
path('', include(router.urls)),
path('books/<book_id>/comments/', views.CommentListCreateAPIView.as_view({'post': 'create', 'get': 'list'})),
path('books/comments/<pk>/', views.CommentDestroyAPIView.as_view({'get': 'destroy'})),
path('tags/', views.TagListAPIView.as_view({'get': 'list'}))
]
タグ付きBook追加
curl -X POST http://127.0.0.1:8000/api/v1/books/ -d '{"title": "title7", "tagList": [{"content": "Action"}]}' -H 'Content-Type: application/json' -H 'Authorization: Bearer <accesss_token>'
タグの取得
curl -LX GET http://127.0.0.1:8000/api/v1/tags/
ユーザーに紐づくプロファイル作成
profilesアプリを作成します。また、既にユーザー等を作成している場合はDBをリセットする必要があります。
python manage.py startapp profiles
config/settinngs.py
にアプリを追加します。
また、画像をアップロードするためMEDIAを追加しています。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3dr paty apps
'rest_framework',
'django_filters',
'drf_spectacular',
'djoser',
'rest_framework_simplejwt',
# my apps
'accounts.apps.AccountsConfig',
'apiv1.apps.Apiv1Config',
'books.apps.BooksConfig',
'profiles.apps.ProfilesConfig',
]
...
# media
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
config/urls.py
にDEBUG時のMEDIAのPATHを追加します。
from django.conf import settings
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/auth/', include('accounts.urls')),
path('api/v1/', include('apiv1.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
profiles/models.py
にユーザーと紐づくProfile Modelを作成します。
from statistics import mode
import uuid
from django.db import models
class Profile(models.Model):
""" Profile Model """
class Meta:
db_table = 'profile'
ordering = ['created_at',]
verbose_name = verbose_name_plural = 'プロファイル'
user = models.OneToOneField('accounts.User', on_delete=models.CASCADE)
bio = models.TextField(blank=True, null=True)
image = models.FileField(upload_to="images/", blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.user.username
また、ユーザー作成と同時にプロファイルも作成されるようにsignalを作成します。
accounts/sigals.py
に以下を追加します。djoser.signals.user_registered
によりユーザーが登録された際に実行する処理を書くことができます。
from django.dispatch import receiver
from djoser.signals import user_registered
from profiles.models import Profile
@receiver(user_registered)
def create_related_profile(user, request, **kwargs):
Profile.objects.create(user=user)
また、signalを認識できるようにaccounts/apps.py
にready()
を書く必要があります。
from django.apps import AppConfig
class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts'
def ready(self):
try:
from . import signals
except ImportError:
raise Exception("Error: Cannot import accounts/signals")
apiv1/serializers.py
に以下のシリアライザを定義します。
ここで、画像のアップロード用とそれ以外用の2つ作成します。
#
# Profile
#
class ProfileSerializer(serializers.ModelSerializer):
""" Serializer for Profile Model """
user = UserSerializer(read_only=True)
class Meta:
model = Profile
fields = ['user', 'bio', 'image']
read_only_fields = ('image',)
class ProfileImageSerializer(serializers.ModelSerializer):
""" Serializer for Profile Model """
user = UserSerializer(read_only=True)
image = serializers.ImageField(use_url=True)
class Meta:
model = Profile
fields = '__all__'
apiv1/views.py
にviewを定義します。
ファイルをアップロードするときはparserparser_classes = [FormParser, MultiPartParser]
を定義します。
#
# Profile
#
class ProfileViewSet(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet):
queryset = Profile.objects.all()
#permission_classes = (IsAuthenticated,)
serializer_class = ProfileSerializer
def retrieve(self, request, username):
profile = self.queryset.get(user__username=username)
serializer = self.serializer_class(instance=profile)
return Response(serializer.data, status=status.HTTP_200_OK)
def update(self, request, username):
instance = get_object_or_404(Profile, user__username=username)
serializer = self.serializer_class(
instance,
data=request.data,
partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
class ProfileImageViewSet(mixins.UpdateModelMixin,
viewsets.GenericViewSet):
queryset = Profile.objects.all()
#permission_classes = (IsAuthenticated,)
serializer_class = ProfileImageSerializer
parser_classes = [FormParser, MultiPartParser]
def update(self, request, username):
instance = get_object_or_404(Profile, user__username=username)
serializer = self.serializer_class(
instance,
data=request.data,
partial=True
)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
apiv1/urls.py
に以下を定義します。<username>
でユーザー名を受け取り処理できるようにしています。
app_name = 'apiv1'
urlpatterns = [
path('', include(router.urls)),
path('books/<book_id>/comments/', views.CommentListCreateAPIView.as_view({'post': 'create', 'get': 'list'})),
path('books/comments/<pk>/', views.CommentDestroyAPIView.as_view({'get': 'destroy'})),
path('tags/', views.TagListAPIView.as_view({'get': 'list'})),
path('profiles/<username>/', views.ProfileViewSet.as_view({'get': 'retrieve', 'post': 'update'})),
path('profiles/image/<username>/', views.ProfileImageViewSet.as_view({'post': 'update'})),
]
取得
curl -LX GET http://127.0.0.1:8000/api/v1/profiles/<username>/ -H 'Authorization: Bearer <access_token>'
アップロード
ファイルパスは@
の後に書く必要があります。
curl -X POST http://127.0.0.1:8000/api/v1/profiles/image/<username>/ -F 'image=@<image_path>' -H 'Content-type: multipart/form-data' -H 'Authorization: Bearer <access_token>'
最後に
ここまででひとまずフロントエンド側の開発に進もうと思います。
まだダッシュボード用のAPIなどを用意してないのですが、追加したら追記していこうと思います。
DRFは英語・日本語ともに参考になる記事が少なく大変でした。同じことをしたい人に少しでも参考にしてもらえればと思います。
Discussion