Django REST Framework

【Django】django-filterが使えない原因と解決法。

Django REST Frameworkのdjango-filterを使って「記事タイトルのキーワード検索するためのフィルタ機能」を作ろうとしていました。

ところが上手くdjango-filterが動かず少しハマってしまいました。

最終的に今回の場合は、一箇所の記述を変えるだけで動作したのですがその時のメモです。

django-filterが動作しない原因・解決法

views内で使用フィルタを指定するfilter_classfilterset_classに変更することで動作しました。

つまり原因はこのviews内のフィルタ指定の書き方が間違っていたみたいです。

ただ、以前作成したサービスではfilter_classでも動作しているので、もしかするとバージョンによる違いなのかもしれません。

動作しなかったコード

class Blog(models.Model):
    title = models.CharField(blank=True, null=True, max_length=255, verbose_name='タイトル')
    created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True, verbose_name='作成日時')
    updated_at = models.DateTimeField(auto_now=True, blank=True, null=True, verbose_name='更新日時')
# 【Blog】リスト取得View
class BlogListView(generics.ListAPIView):
    queryset = Blog.objects.all()       # 1: クエリセット
    permission_classes = (AllowAny,)    # 2: アクセス許可範囲を指定
    serializer_class = BlogSerializer   # 3: 利用するSerializer指定
    filter_class = BlogFilter           # 4: 利用するFilter指定


# 【Blog】シリアライザー
class BlogSerializer(serializers.ModelSerializer):

    class Meta:
        model = Blog
        fields = '__all__'


# 【Blog】フィルタ
class BlogFilter(serializers.ModelSerializer):
    title = filters.CharFilter(name="title", lookup_expr='contains')

    class Meta:
        model = Blog
        fields = '__all__'

修正後コード

# 【Blog】リスト取得View
class BlogListView(generics.ListAPIView):
    queryset = Blog.objects.all()       # 1: クエリセット
    permission_classes = (AllowAny,)    # 2: アクセス許可範囲を指定
    serializer_class = BlogSerializer   # 3: 利用するSerializer指定
    filterset_class = BlogFilter        # 【修正箇所】4: 利用するFilter指定


# 【Blog】シリアライザー
class BlogSerializer(serializers.ModelSerializer):

    class Meta:
        model = Blog
        fields = '__all__'


# 【Blog】フィルタ
class BlogFilter(serializers.ModelSerializer):
    title = filters.CharFilter(name="title", lookup_expr='contains')

    class Meta:
        model = Blog
        fields = '__all__'

まとめ

やっぱり常に公式ドキュメントを見る癖をつけないといけないですね。

あと他環境でfilter_classが動作して、今回は動作しなかった詳しい原因がわからないままですが、バージョン管理の重要さについても実感することができました。

【DRF】M2Mを含むインスタンスをcreate()する時に出たエラー。

今回新しくManyToManyフィールド(Tag)を含む投稿(Post)インスタンスを作成しようとしたところエラーが出たのでその解消法メモ。

M2M含むインスタンス作成時に出たエラー

実行したこと

class Tag(models.Model):
    name = models.CharField(blank=True, null=True, max_length=255)

class Post(models.Model):
    title = models.CharField(blank=True, null=True, max_length=255)
    tags = models.ManyToManyField("Tag", through="PostTagRelation", blank=True, null=True)
def create(self, validated_data):
    # 1: validated_dataからtagsを取り出す処理
    tags_data = self.validated_data.pop('tags')

    # 2: Postインスタンスを作成する処理
    post = Post.objects.create(**validated_data)
    
    # 3: tagsを一つずつPostへ追加する処理
    for tag_data in tags_data:
        tag_qs = Tag.objects.filter(id=tag_data['id'])
        if tag_qs.exists():
            tag = tag_qs.first()
        else:
            tag = Tag.objects.create(**tag_data)
			post.tags.add(tag)

    post.save()
    return post

実際のエラー文

TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use tags.set() instead.

解決法

def create(self, validated_data):
    # 1: validated_dataからtagsを取り出す処理
    tags_data = self.validated_data.pop('tags')

    # 2: Postインスタンスを作成する処理
    post = Post.objects.create(
        title=validated_data.get('title'),  # 追加: フィールドを指定
    )

    # 3: tagsを一つずつPostへ追加する処理
    for tag_data in tags_data:
        tag_qs = Tag.objects.filter(id=tag_data['id'])
        if tag_qs.exists():
            tag = tag_qs.first()
        else:
            tag = Tag.objects.create(**tag_data)
			post.tags.add(tag)

    post.save()
    return post

【DRF】validated_data.get()の第二引数って何?

Django REST Frameworkでvalidated_dataの取得に使う.get()の第二引数が何かわからなかったのでメモ。

def update(self, instance, validated_data):
    instance.name = validated_data.get('name', instance.name)

validated_data.get()の第二引数は「初期値」

validated_data.get('name', instance.name)の第二引数(instance.nameの部分)は、第一引数に要素がなかった場合の初期値になるようです。

【Django】DRFでManyToManyフィールドを含むデータ更新でハマった点。

Django REST Frameworkを使って、ManyToManyフィールドを含むデータの更新をしようとしてハマったのメモ。

今回の課題

やりたいことは「投稿(Post)と一緒に、タグ(Tag: M2M)も更新すること」です。

ただ更新したいデータをDjango側で受け取った時に、validated_dataの「Tag」部分データが受け取れませんでした

送信したデータ

{
    "uuid":"af773f1b3184",
    "title":"「HTTPリクエスト/HTTPレスポンス」って何?",
    "tags":[
        {"id": 2},
    ]
}

Djangoで受け取ったデータ(validated_data

OrderedDict([
    ('tags', [OrderedDict()]),
    ('uuid', 'af773f1b3184'),
    ('title', '「HTTPリクエスト/HTTPレスポンス」って何?')
])

tagsのOrderedDict()の中身が空になってしまっています…。

今回のコード

Models.py

from django.db import models


class Tag(models.Model):
    name = models.CharField(blank=True, null=True, max_length=255)


class Post(models.Model):
    uuid = models.CharField(blank=True, null=True, max_length=255)
    title = models.CharField(blank=True, null=True, max_length=255)
    tags = models.ManyToManyField("Tag", through="PostTagRelation", blank=True, null=True) 


class PostTagRelation(models.Model):
    post = models.ForeignKey("Post", on_delete=models.CASCADE)
    tag = models.ForeignKey("Tag", on_delete=models.CASCADE)

Views

class PostUpdateView(generics.UpdateAPIView):
    queryset = Post.objects.prefetch_related('tags').all()
    serializer_class = PostUpdateSerializer
    permission_classes = (AllowAny,)
    lookup_field = 'uuid'

Serielizers

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = '__all__'


class PostUpdateSerializer(serializers.ModelSerializer):
    tags = TagSerializer(many=True)

    class Meta:
        model = Post
        fields = '__all__'

解決法

TagSerializerのidをオーバーライドする(下記のように追記)ことで受け取ることができました。

TagSerializerのidがデフォルトでread_only = Trueになっていたようで、validated_dataに含まれなかったようです。

class TagSerializer(serializers.ModelSerializer):
    id = serializers.IntegerField(label='id')  # 追加(idをオーバーライド)

    class Meta:
        model = Post
        fields = '__all__'

【Django REST Framework】JWTを使った「ユーザー認証機能」を作る。

Django REST Frameworkを使ったAPIを作る中で、ユーザーの新規追加ユーザー権限ごとのアクセス制限設定などをするために必要な「ユーザー認証機能」を作っていきたいと思います。

今回は、「JWT」を使ったユーザー認証機能を作る方法をメモしていきます。

大まかな作業の流れ

  1. 必要なライブラリをインストールする
  2. settings.pyを編集する
  3. serializers.pyに「UserSerializerクラス」を追加する
  4. views.pyに「CreateUserViewクラス」を追加する
  5. urls.pyを編集する
  6. 操作に認証制限をかける

必要なライブラリをインストール

$ pip install django-cors-headers
$ pip install djangorestframework-simplejwt
$ pip install djoser

settingsの編集

from datetime import timedelta


INSTALLED_APPS = [
    ...,
    'corsheaders',  # 追加
    'djoser',  # 追加
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # 追加
    ...
]

# 追加
CORS_ORIGIN_WHITELIST = [
	"http://localhost:8000",
]

# 追加
SIMPLE_JWT = {
	'AUTH_HEADER_TYPES': ('JWT',),
	'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),  # アクセストークンの賞味期限
}

# 追加
REST_FRAMEWORK = {
	'DEFAULT_PERMISSION_CLASSES': [
		'rest_framework.permissions.IsAuthenticated',  # デフォルトのアクセス制限
	],
	'DEFAULT_AUTHENTICATION_CLASSES': [
		'rest_framework_simplejwt.authentication.JWTAuthentication',  # 認証に「JWT」を利用する設定
	],
}

serializers.pyに「UserSerializerクラス」

from rest_framework import serializers
from django.contrib.auth.models import User


class UserSerializer(serializers.ModelSerializer):

    Meta:
        model = User
        fields = ('id', 'username', 'password')
        extra_kwargs = {'password': {'write_only': True, 'required': True}}

    def create(self, validated_data):
        user = User.objects.create_user(**validated_data)
        return user

views.pyに「CreateUserViewクラス」

from rest_framework import generics
from .serializers import UserSerializer
from rest_framework.permissions import AllowAny


class CreateUserView(generics.CreateAPIView):
    serializer_class = UserSerializer
    permission_classes = (AllowAny,)

urls.pyを編集する

from api.views import CreateUserView


urlpatterns = [
    ...,
    path('register/', CreateUserView.as_view(), name='register'),  # 追加
    path('auth/', include('djoser.urls.jwt')),  # 追加
]

viewにアクセス制限を設定する

class PostListView(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = (AllowAny,)  # 誰でもアクセス可能

アクセス制限のある操作を利用する方法

  1. 登録ユーザーのアクセストークンを発行する
  2. アクセストークンを使ってAPIのリクエストをする

ユーザーのアクセストークンを発行する

/auth/jwt/create」にusernameとpasswordをPOSTすると、アクセストークンリフレッシュトークンが返ってきます。

アクセストークンを使ってAPIのリクエストをする

APIのリクエストヘッダに「Authorization: JWT_[発行したアクセストークン]」を入れてリクエストすると、このアクセストークンで認証をしてくれて通れば利用できる。

Django REST Frameworkでviewにアクセス制限を設定する。

Django REST FrameworkでAPIを作成していると「このviewの操作は管理者以外は利用させたくない」という場面があります。

そこでviewにアクセスするユーザーの制限をかける設定を調べてみました。

アクセス制限(Permissions)の設定をする方法

APIのviewにアクセス制限をつける方法には、「Viewクラスごとに設定する方法」と「デフォルトのアクセス制限の設定を変える方法」の2つのパターンがあります。

デフォルトの設定をする

「settings.py」にあらかじめ下記コードを記載すると、デフォルトの設定ができます。viewにアクセス制限の設定がない場合(permission_classesの記載がない場合)は、この設定が適用されることになります。

# viewへのアクセス制限のデフォルト設定
REST_FRAMEWORK = {
	'DEFAULT_PERMISSION_CLASSES': [
		'rest_framework.permissions.IsAuthenticated',  # ←ここがデフォルトの設定になる
	],
}

Viewクラスごとに設定する

Viewクラスにアクセス制限を設定したい場合は、「permission_classes」に記載します。

from rest_framework.permissions import AllowAny
from rest_framework import generics
from .serializers import PostSerializer


class PostListView(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = (AllowAny,)  # ←ここ

アクセス制限(Permissions)の種類

種類内容
AllowAny誰でも利用できる
IsAuthenticated認証済みのユーザーのみ利用できる
IsAdminUser管理者権限を持つユーザーのみ利用できる
IsAuthenticatedOrReadOnly認証済みのユーザーは利用できる
それ以外のユーザーは読み取りのみできる(GETなど)
DjangoModelPermissions※調べ中…
DjangoModelPermissionsOrAnonReadOnly※調べ中…
DjangoObjectPermissions※調べ中…

「DjangoModelPermissions」「DjangoModelPermissionsOrAnonReadOnly」「DjangoObjectPermissions」は、まだ把握できていないので今後調べて更新する予定です。

詳しく知りたい方はこちらの記事を参考にしてみてください。

【DRF】views.pyで「APIview」と「modelViewSet」どっちを使う?違いがよく分からない。

DjangoでAPIを作る時などにviews.py内で「generics.◯◯APIView」や「viewsets.ModelViewSet」を使っていたのですが、あまり内容を理解していなかったので「結局どっちを使ったらいいのか」を中心に調べてみました。

またそれぞれの違いや使い方もメモとして残しておきます。

結論:どちらを使っても問題なさそう

少し調べた結論としては、機能的には「どちらを使っても問題ない」という印象を受けました。APIviewもModelViewSetもあらゆる操作や認証制限などは一通りできるようです。

ただし、CRUDを作成する場合は「ModelViewSet」が手っ取り早いかもしれません。後述します。

調べる中でより詳しく違いが見つかり次第更新していきます。(詳しい方もしあれば教えてください…)

APIView と ModelViewSetの2つの違い

大きなわかりやすい違いはこの2つかなと感じました。

URLを自分で決められるかどうか

ModelViewSet」は、あらかじめ操作ごとのエンドポイント(URI)が決められています。「APIView」はそれぞれ自分で定めることができて自由度が高いんですね。

CRUDを1つのクラスで作成できるかどうか

ModelViewSet」はCRUDを作成する場合は1つのクラスで作成することができます。一方「APIView」はCRUDの全操作ができるビュークラスがないので、2つ以上のクラスを書く必要があるためコード量が少し増えてしまうという点があります。

「APIView」の種類・書き方

APIViewの種類できること
ListAPIView一覧取得(GET)
RetrieveAPIView詳細取得(GET)
CreateAPIView作成(CREATE)
UpdateAPIView更新(PATCH)
DestroyAPIView削除(DELETE)
APIViewの種類できること
ListCreateAPIView一覧取得、作成
RetrieveUpdateAPIView詳細取得、更新
RetrieveDestroyAPIView詳細取得、削除
RetrieveUpdateDestroyAPIView詳細取得、更新、削除

書き方の例

from rest_framework import generics
from .models import Post
from .serializers import PostSerializer


class PostList(generics.ListCreateAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
from api.views import PostList

urlpatterns = [
    path('post-list/', PostList.as_view(), name='post-list'),
    ...,
]

「ModelViewSet」の種類・書き方

ModelViewSetの種類できること
ModelViewSet一覧取得、詳細取得、作成、更新、削除
ReadOnlyModelViewSet一覧取得、詳細取得

書き方の例

from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer


class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
from rest_framework import routers
from api.views import TaskViewSet


router = routers.DefaultRouter()
router.register('posts', PostViewSet, basename='posts')

urlpatterns = [
    ...,
]

「Django REST Framework」でAPIを作る。

Django REST Frameworkを使ってAPIを作る過程のメモです。(途中)

この記事ではAPIを作るまでのコマンドやコードの中身を中心に書いていきます。各作業の解説をあわせて書くと長くなりそうなので、それぞれ別記事にしてまとめいこうと考えていきます。

今回の完成イメージ

  • 「記事一覧データ」を取得できる(List)
  • 「記事詳細データ」を取得できる(Detail)
  • ユーザー登録なしでAPIが利用できる状態(※今回は認証機能なし)

全体の作業フロー

  1. 必要なパッケージをインストール
  2. Djangoのプロジェクトを作る
  3. Djangoのアプリケーションを作る
  4. settings.py」を変更する
  5. models.py」を変更する
  6. serializers.py」を変更する
  7. urls.py」を変更する
  8. views.py」を変更する
  9. Githubにアップロードする
  10. Herokuにデプロイする

1. 必要なパッケージをインストールする

$ pip install django
$ pip install django-cors-headers
$ pip install djangorestframework
$ pip install djoser
$ pip install python-decouple
$ pip install dj-database-url
$ pip install dj_static

$ pip install django django-cors-headers djangorestframework djoser python-decouple dj-database-url dj_static

2. Djangoのプロジェクトを作成する

まずはDjangoのプロジェクトを置くためのディレクトリを作成し、そのディレクトリに移動します。

$ mkdir rest_api_project    # 「rest_api_project」部分は自由(ディレクトリ名)
$ cd rest_api_project

作成したディレクトリ内で、Djangoのプロジェクトを作成します。

$ django-admin startproject rest_api .    # 「rest_api」部分は自由(プロジェクト名)

3. Djangoのアプリケーションを作成する

$ django-admin startapp api    # 「api」部分は自由(アプリケーション名)
【この時点でのディレクトリ構造】

rest_api_project
 ├ api
 ├ rest_api
 └ manage.py

4. 「settings.py」を変更する

rest_api直下にある「settings.py」を編集していきます。

from datetime import timedelta  # 追加
from decouple import config  # 追加
from dj_database_url import parse as dburl  # 追加


INSTALLED_APPS = [
  ...,
  'rest_framework',  # 追加
	'api.apps.ApiConfig',  # 追加
	'corsheaders',  # 追加
	'djoser',  # 追加
]

MIDDLEWARE = [
  'corsheaders.middleware.CorsMiddleware',  # 追加
  ...
]

# 追加
CORS_ORIGIN_WHITELIST = [
	"http://localhost:3000",
]

default_dburl = 'sqlite:///' + str(BASE_DIR / "db.sqlite3")  # 追加

# "DATABASES"を変更
DATABASES = {
	'default': config('DATABASE_URL', default=default_dburl, cast=dburl),
}

TIME_ZONE = 'Asia/Tokyo'  # 変更

# 追加
STATIC_URL = '/static/'
STATIC_ROOT = str(BASE_DIR / 'staticfiles')

「.env」を作成する(環境変数の設定)

トップディレクトリ直下に「.env」ファイルを作成します。

api/settings.pyに記載されている「SECRET_KEY」と「DEBUG」をenvファイルに記載します。

SECRET_KEY=[シークレットキー]
DEBUG=False

また元々のapi/settings.pyの記載を変更して、「.env」ファイルから読み込むようにします。

from decouple import config  # 追加

SECRET_KEY = config('SECRET_KEY')  # 追加
DEBUG = config('DEBUG')  # 追加

5. 「models.py」(admin.py)を変更する

「api」ディレクトリ直下にある「models.py」を編集します。これでデータベースのテーブルとカラムが定義されます。

from django.db import models

class Post(models.Model):
	title = models.CharField(max_length=50)
	content = models.CharField(max_length=500)
	created_at = models.DateTimeField(auto_now_add=True)

	def __str__(self):
		return self.title

コマンドを入力してデータベースを作成/更新します。

$ python manage.py makemigrations
$ python manage.py migrate

6. 「serializers.py」を変更する

「api」ディレクトリ直下に「serializers.py」を作成します。

from rest_framework import serializers
from .models import Post


class PostSerializer(serializers.ModelSerializer):
	created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)

	class Meta:
		model = Post
		fields = ('id', 'title', 'content', 'created_at')

7. 「urls.py」を変更する

「api」ディレクトリ直下に「urls.py」を作成します。

from django.urls import path
from api.views import PostListView, PostRetrieveView


urlpatterns = [
    path('posts/', PostListView.as_view(), name='posts'),
    path('post/<str:pk>/', PostRetrieveView.as_view(), name='post-detail'),
]

「rest_api」ディレクトリ直下に「urls.py」を作成します。

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
  path('admin/', admin.site.urls),
	path('api/', include('api.urls')),  # 追加
]

8. 「views.py」を変更する

「api」ディレクトリ直下にある「views.py」を編集します。

from .models import Post
from .serializers import PostSerializer
from rest_framework import generics
from rest_framework.permissions import AllowAny


class PostListView(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = (AllowAny,)


class PostRetrieveView(generics.RetrieveAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = (AllowAny,)

9. Githubにアップロードする

「.gitignore」を作成する

Githubのリポジトリを作成する

Githubにプッシュする

10. Herokuにデプロイする

必要なライブラリをインストールする

$ pip install gunicorn
$ pip install psycopg2-binary

$ pip install gunicorn psycopg2-binary

「requirements.txt」を作成する

$ pip freeze > requirements.txt  

「Procfile」を作成する

web: gunicorn rest_api.wsgi --log-file -    # 「rest_api」部分は自由(プロジェクト名)

「runtime.txt」を作成する

python-3.8.12

「settings.py」を編集する

# 変更
ALLOWED_HOSTS = ['nextjs-restapi.herokuapp.com']

↑↑↑
ALLOWED_HOSTS = ['[herokuのアプリケーション名].herokuapp.com']

11. 動作確認テストする

admin.pyで管理画面の設定をする

from django.contrib import admin
from .models import Post

admin.site.register(Post)

スーパーユーザーを作成する

管理画面を操作するためのスーパーユーザーを作成します。

$ python manage.py createsuperuser  # (ローカル環境に作成)
$ heroku run python manage.py createsuperuser -app [herokuリポジトリ名]  # (heroku環境に作成)

テストデータを入れる

エンドポイントにアクセスして確認する

おまけ:仮想環境で開発する