Django

【Django】エラー文「ERROR: Could not install packages due to an OSError: [Errno 2] No such file or directory:」の対処法。

DjangoのアプリケーションをHerokuに上げようとした時に、パッケージのインストールエラーが起きてしまいました。

そのエラー内容と対処法をメモしておきます。

今回のエラー文

ERROR: Could not install packages due to an OSError: [Errno 2] No such file or directory:

エラーの対処法

このコマンドでエラー原因であるrequirementsファイルを適切な記載に書き換えてくれて解消されます

$ pip list --format=freeze > requirements.txt

エラーの原因:requirements.txtの記載ミス

今回のエラーは、requirements.txtの記載が間違っていることが原因でした。

エラー時:requirements.txt

エラー解消後:requirements.txt

【Django】”__init__() takes 1 positional argument but 2 were given”の対処法。

DjangoでAPIを作成していたら"init() takes 1 positional argument but 2 were given"というエラーが出てしまいました。

その際に解消できた対処法のメモです。

init() takes 1 positional argument but 2 were given

原因のコード

from django.urls import path, include
from .views import ArticleListView

urlpatterns = [
    path('articles/', IndexArticles, name='articles'),
]
class ArticleListView(generics.ListAPIView):
	queryset = Article.objects.all()
	serializer_class = ArticleSerializer
	permission_classes = (AllowAny,)

解決法 → urls.pyに.as_view()を記載する

urls.pyのに.as_view()を記載することで解消されました。

from django.urls import path, include
from .views import ArticleListView

urlpatterns = [
    path('articles/', IndexArticles.as_view(), name='articles'),  # 変更
]

“.as_view()”はどんな時に必要?

ビューには「関数ビュー」と「クラスビュー(class)」の2つがあり、クラスビューの場合は.as_view()の記載が必要なようです。

関数ビュー「.as_view()はいらない」

# 関数ビュー
def IndexView(request):
	
	params = {
		...,
	}
	return render(request, 'pages/index.html', params)
urlpatterns = [
    path('/', IndexView, name='index'),
    ...,
]

クラスビュー「.as_view()が必要」

# クラスビュー
class ArticleListView(generics.ListAPIView):
	queryset = Article.objects.all()
	serializer_class = ArticleSerializer
	permission_classes = (AllowAny,)
urlpatterns = [
    path('articles/', IndexArticles.as_view(), name='articles'),
    ...,
]

まとめ

ビューに「クラスビュー」と「関数ビュー」の2種類あるなんて知りませんでした…。

もっといろいろと触りながら少しずつDjangoの基本を探っていきたいなと思います!

【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】django-filterで複数キーワードAND・OR検索をする方法。

django-filterを使って、特定フィールド(今回は記事タイトル)に対して複数キーワードで「AND検索」や「OR検索」をしたい。

調べてながら試してみて実際に動作したコードをメモとして残しておきます。

今回フィルタをかける対象URL

全記事データを取得する下記URLに、パラメータを追加してフィルタをかけていきます。

http://127.0.0.1:8000/api/post/
 => 全ての記事を取得

複数キーワードで「AND検索」

記事タイトルに「テスト」「記事」のどちらも含まれる記事を取得したい。

コード

from ..models import Post
from ..serializers import Post
from rest_framework.permissions import AllowAny
from django_filters import rest_framework as filters
from django_filters.rest_framework import DjangoFilterBackend


# AND検索用のカスタムフィルタ
class MultiValueCharFilterAnd(filters.CharFilter):
	def filter(self, qs, value):
		values = value.split(',') or []

		for value in values:
			qs = super(MultiValueCharFilterAnd, self).filter(qs, value)
		return qs


# フィルタ
class PostFilter(filters.FilterSet):
	title = MultiValueCharFilterAnd(field_name='title', lookup_expr='contains')


# ビュー
class PostListView(generics.ListAPIView):
	queryset = Post.objects.all()
	serializer_class = PostListSerializer
	permission_classes = (AllowAny,)
	filter_backends = (DjangoFilterBackend,)
	filter_class = PostFilter  # フィルタを指定

エンドポイント(URL)

http://127.0.0.1:8000/api/post/?title=テスト,記事
 => titleに「テスト」「記事」のどちらも含まれる記事を取得

複数キーワードで「OR検索」

記事タイトルに「テスト」「記事」のいずれか一つでも含まれる記事を取得したい。

コード

from ..models import Post
from ..serializers import Post
from rest_framework.permissions import AllowAny
from django_filters import rest_framework as filters
from django_filters.rest_framework import DjangoFilterBackend
import operator
from functools import reduce
from django.db.models import Q


# OR検索用のカスタムフィルタ
class MultiValueCharFilterOr(filters.CharFilter):

	def filter(self, qs, value):
		values = value.split(',') or []
		expr = reduce(
			operator.or_,
			(Q(**{f'{self.field_name}__{self.lookup_expr}': v}) for v in values)
		)
		return qs.filter(expr)


# フィルタ
class PostFilter(filters.FilterSet):
	title = MultiValueCharFilterOr(field_name='title', lookup_expr='contains')


# ビュー
class PostListView(generics.ListAPIView):
	queryset = Post.objects.all()
	serializer_class = PostListSerializer
	permission_classes = (AllowAny,)
	filter_backends = (DjangoFilterBackend,)
	filter_class = PostFilter  # フィルタを指定

エンドポイント(URL)

http://127.0.0.1:8000/api/post/?title=テスト,記事
 => titleに「テスト」または「記事」が含まれる記事を取得

【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 = [
    ...,
]