シンプルブログ、非公開記事の作成

Django Django REST framework Vue.js

概要

DRFとVue.jsで、シンプルブログを作るシリーズの一つです。今回は非公開の記事を作れるようにし、非公開の記事は管理者のみが閲覧できるようにしていきます。この機能は、記事の下書きがしたいときに便利です。

モデルの変更

記事モデルに、公開なのか非公開なのかを表すフィールドを追加します。

class Post(models.Model):
    title = models.CharField('タイトル', max_length=40)
    thumbnail = models.ImageField('サムネイル', blank=True, null=True)
    category = models.ForeignKey(Category, on_delete=models.PROTECT, verbose_name='カテゴリ')
    lead_text = models.TextField('紹介文')
    main_text = models.TextField('本文')
    is_public = models.BooleanField('公開可能か', default=True)  # 追加
    created_at = models.DateTimeField('作成日', default=timezone.now)

is_publicというフィールドを追加しました。

ログイン情報を送信する

今のところ、開発時はhttp://127.0.0.1:8080(Vue)でページにアクセスし、そこから127.0.0.1:8000(Django)とAjaxでのデータ取得などを行います。オリジンが違う状態なのですが、オリジンが違う場合でもレスポンスが読めるようにdjango-cors-headersを以前に導入済みです。しかし、クッキー等の認証情報の送信といった設定は別途必要です。

まず、settings.pyに追記します。

if DEBUG:
    INSTALLED_APPS += ['corsheaders']
    MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware'] + MIDDLEWARE
    CORS_ORIGIN_WHITELIST = (
        'http://127.0.0.1:8080',
        'http://localhost:8080',
    )
    CORS_ALLOW_CREDENTIALS = True  # これを追記

CORS_ALLOW_CREDENTIALS = Trueを追加しました。

次に、Post.vueを次のように書き換え、認証情報を送信するようにします。

this.$http(`${this.$httpPosts}${this.id}/`)
↓
this.$http(`${this.$httpPosts}${this.id}/`, {credentials: "include",})

PostList.vueも同様です。

this.$http(postURL)
↓
this.$http(postURL, {credentials: "include",})

オリジンが別になるのは開発時だけでなく、プロジェクトの規模が大きいとサーバーを分けることも多いので、そういった場合もオリジンが別になることがあります。そのような場合も、今回のように設定することになります。

permission_classesで権限を設定

記事詳細ビューですが、is_publicがTrueかスーパーユーザーのときにだけ記事情報を返すようにしたいと思います。nblog3/permissions.pyを作り、中身をまず次のようにします。

from rest_framework.permissions import BasePermission


class PublicOrSuperUser(BasePermission):

    def has_object_permission(self, request, view, object):
        return bool(object.is_public or request.user.is_superuser)

そして、views.pyのPostDetailに追記します。

# 追加
from .permissions import PublicOrSuperUser


class PostDetail(generics.RetrieveAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [PublicOrSuperUser]  # 追加

クラス属性permission_classesには、権限チェックの為の処理を好きに追加することができます。例えば単純なログイン済みかのチェックは、rest_framework.permissions.IsAuthenticatedのようにDRF側で用意されていることもありますが、なさそうな場合は自作することになります。

今回はPublicOrSuperUserというクラスを追加しました。こういった権限チェックの処理を自分で作るのは簡単で、rest_framework.permissions.BasePermissionを継承したクラスを作り、has_permissionhas_object_permissionメソッドを実装し、それらの中に処理を書きます。前者は必ず呼ばれるもので、ビュー自体へアクセスできるか?といったチェックを書くのに最適です。後者はRetrieveAPIView等、単一のオブジェクトを取得する処理ではだいたい呼ばれます。そのデータを取得するのに必要な権限はあるか?といったチェックを書くのに最適です。今回のように。

また、permission_classes = [IsAuthenticated|ReadOnly]のように|演算子を使うこともできます。上で書いた権限チェックも2つにわけることができそうです。次のようにしました。

from rest_framework.permissions import BasePermission


class IsPublicPost(BasePermission):

    def has_object_permission(self, request, view, object):
        return bool(object.is_public)


class IsSuperUser(BasePermission):

    def has_object_permission(self, request, view, object):
        return bool(request.user.is_superuser)

from .permissions import IsPublicPost, IsSuperUser

class PostDetail(generics.RetrieveAPIView):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    permission_classes = [IsPublicPost|IsSuperUser]

権限チェックに失敗した際のメッセージはmessage属性で定義できます。詳しくは公式ドキュメントを見てください。IPでのブロック等のサンプルもありますよ・

filter_backends

記事の一覧ビューですが、スーパーユーザーなら全記事表示、そうでなければis_publicがTrueなものだけ返すようにしたいと思います。合わせて、記事の検索処理をリファクタリングします。

nblog3/filters.pyを作り、中身を次のようにします。

from django.db.models import Q
from rest_framework import filters


class IsPublicOrSuperAll(filters.BaseFilterBackend):

    def filter_queryset(self, request, queryset, view):
        if request.user.is_superuser:
            return queryset
        else:
            return queryset.filter(is_public=True)


class PostSearch(filters.BaseFilterBackend):

    def filter_queryset(self, request, queryset, view):
        keyword = request.query_params.get('keyword', None)
        if keyword:
            queryset = queryset.filter(
                Q(title__icontains=keyword) | Q(lead_text__icontains=keyword) | Q(main_text__icontains=keyword))

        category = request.query_params.get('category', None)
        if category:
            queryset = queryset.filter(category=category)

        return queryset

views.pyのPostListビューを変更します。

# 追加
from .filters import IsPublicOrSuperAll, PostSearch

class PostList(generics.ListAPIView):
    queryset = Post.objects.all()
    serializer_class = SimplePostSerializer
    pagination_class = StandardResultsSetPagination
    filter_backends = [IsPublicOrSuperAll, PostSearch]  # 追加


クエリセットを絞り込みたい場合ですが、ビューのget_querysetメソッドを上書きするという原始的な方法のほかに、フィルタリング用のクラスを定義するという方法もあります。rest_framework.filters.BaseFilterBackendを継承して、filter_querysetメソッドを実装するだけです。アプリケーション全体で繰り返し使うフィルターは、このように定義しておくと捗ります。

検索処理は、django-filterというサードパーティ製の柔軟なライブラリがあったり、単一クエリパラメータでの検索ならばrest_framework.filters.SearchFilterがあったりもしますが、練習用に今回は自分で作っています。

詳しい内容は公式ドキュメントを見てください。

Relation Posts

Comment

記事にコメントする

まだコメントはありません。