アプロダ、REST APIの実装③

Django REST framework

概要

DRFとVueで、ファイルアップローダーを作るシリーズの1つです。Django REST frameworkの処理を更に作りこんでいきます。

バリデーション

Compositeの作成に関して、幾つかのよろしくないケースを考えます。

  • 親ディレクトリが自分
  • 同じ階層に、同じ名前のファイル・ディレクトリ
  • ファイルなのに、ファイルが添付されてない
  • ディレクトリなのに、ファイルが添付されてる

これらのチェックを書く場所は幾つかありますが、今回はシンプルに、シリアライザーのvalidateメソッドに書いていきます。

class CompositeSerializer(serializers.ModelSerializer):
    parent = SimpleCompositeRelation(queryset=Composite.objects.filter(is_dir=True), required=False, allow_null=True)
    composite_set = SimpleCompositeSerializer(read_only=True, many=True)

    class Meta:
        model = Composite
        fields = ('pk', 'name', 'is_dir', 'src', 'parent', 'zip_depth', 'composite_set')

    def validate(self, attrs):
        parent = attrs['parent']
        name = attrs['name']
        is_dir = attrs['is_dir']

        if (self.instance and parent) and (parent.pk == self.instance.pk):
            raise serializers.ValidationError('親ディレクトリが自分です')

        # 同名ファイル・ディレクトリがないかチェック
        same_names = Composite.objects.filter(parent=parent, name=name)
        if self.instance:  # 更新の場合は、自分が同名ファイルとして出てくるので、それは除く
            same_names = same_names.exclude(pk=self.instance.pk)
        if same_names.exists():
            raise serializers.ValidationError('同じ名前のファイル・ディレクトリが既に存在します')

        # ファイルの送信処理があった
        if 'src' in attrs:
            src = attrs['src']
            # 送られたファイルの中身があり、ディレクトリ指定
            if is_dir and src:
                raise serializers.ValidationError('ディレクトリの時は、ファイルを添付しないでください')

            # ファイルフラグだが、ファイルの中身は空
            if not is_dir and not src:
                raise serializers.ValidationError('ファイルの時は、ファイルを添付してください')

        # ファイルの送信はなかった
        else:
            if not self.instance:
                # ファイルの送信はなかったのに、ファイルフラグ
                if not is_dir:
                    raise serializers.ValidationError('ファイルの時は、ファイルを添付してください')
            else:
                src = self.instance.src
                # ファイルの送信はなかったし、アップロード済みでもないのにファイルフラグ
                if not is_dir and not src:
                    raise serializers.ValidationError('ファイルの時は、ファイルを添付してください')
        return attrs

愚直に各ケースを判断しています。こういったバリデーションはフロントエンド側でもある程度はできますが、サーバー側でも必要というか、サーバー側でのチェックは必須です。忘れずに実装したいところです。

それと、SimpleCompositeRelationにも処理を追加しましょう。

from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from .models import Composite


class SimpleCompositeRelation(serializers.RelatedField):
    default_error_messages = {
        'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
        'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
    }

    def to_representation(self, value):
        return SimpleCompositeSerializer(value).data

    def to_internal_value(self, data):
        try:
            return self.get_queryset().get(pk=data)
        except ObjectDoesNotExist:
            self.fail('does_not_exist', pk_value=data)
        except (TypeError, ValueError):
            self.fail('incorrect_type', data_type=type(data).__name__)

    def get_choices(self, cutoff=None):
        queryset = self.get_queryset()
        if queryset is None:
            return {}

        if cutoff is not None:
            queryset = queryset[:cutoff]

        # https://github.com/encode/django-rest-framework/issues/5141
        return dict([
            (
                item.pk,
                self.display_value(item)
            )
            for item in queryset
        ])

クラス属性としてdefault_error_messagesが追加されたのと、to_internal_valueに変更がありました。何をしたかというと、送信されたpkの値でオブジェクトが取得できなかったり、そもそもpkに変な文字列を指定されたとか、そういったケースに対応しました。serializers.PrimaryKeyRelatedFieldでやっていた処理を、そのまま持ってきています。

ホームのファイル・ディレクトリ一覧

今のところ、あるCompositeに紐づくCompositeの一覧は取得できています。しかし、何処にも紐づかないComposite、parentがNoneなComposite...最上位にあるファイル・ディレクトリの一覧が取得できません。専用のビューを作っても良いのですが、せっかくなので、今あるビュの一覧処理を上書きしましょう。nuploader1/views.pyです。

from rest_framework import viewsets
from rest_framework.response import Response
from .models import Composite
from .serializers import CompositeSerializer


class CompositeViewSet(viewsets.ModelViewSet):
    queryset = Composite.objects.all()
    serializer_class = CompositeSerializer

    def list(self, request, *args, **kwargs):
        queryset = Composite.objects.filter(parent__isnull=True)
        queryset = self.filter_queryset(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)

listメソッドは全てのデータを取得する際に呼ばれるメソッドです。具体的には、http://127.0.0.1:8000/uploader/api/composites/のURLで呼ばれるのが、このメソッドです。このURLへのアクセスだった場合にだけ、parentがNoneな一覧を取得するようにしています。中身の処理は、もともとのlistメソッドの処理とほぼ同じで、parentがNoneなものに絞り込んだだけです。

http://127.0.0.1:8000/uploader/api/composites/1/とかの単体データの取得では呼ばれないメソッドですし、もちろん、作成とか更新でも呼ばれませんので、他への影響はありません。とはいえ今回のように上書きすると、全てのデータを取得するエンドポイントがなくなります。それが嫌な方は、おとなしく別のビューとして定義しておくと良いでしょう。

ログイン必須にする

データの作成や更新、削除処理はログインしていないとできないようにしたいと思います。通常のDjangoではdjango.contrib.auth.mixins.LoginRequiredといったMixinがありましたが、DRFでは次のように書きます。

from rest_framework import viewsets, permissions  # 追加


class CompositeViewSet(viewsets.ModelViewSet):
    queryset = Composite.objects.all()
    serializer_class = CompositeSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]  # 追加

permissions.IsAuthenticatedOrReadOnlyは、読み取りは自由で、作成などの処理はログイン必須にします。

Relation Posts

Comment

記事にコメントする

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