ホーム >  Python > Django REST framework >  【API】DRFのアプロダ作成 その③バリデーション【Django】

投稿日:   |  最終更新日:

【API】DRFのアプロダ作成 その③バリデーション【Django】

Django REST frameworkPython

DRFのアップローダアプリを作成します。フォルダ操作・ファイル操作に対してバリデーションを作成します。

バリデーションの追加

前回、アップローダーをツリー構造(入れ子)にしました。今回は、バリデーション(エラーチェック)を追加します。Compositeの作成に関して、エラーメッセージのケースを考えます。

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

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

作業の流れ

mac側

1.VSCODEを起動します。

サンドボックス(Vagrant側)

1.vagrantを起動して、Amazon Linux2にログインします。

2.Djnangoプロジェクトを作成したディレクトリへ移動します。

前回まで

今回の作業は、以下の続きです。

【仮想環境】MacにVirtualBoxをインストール【Mac】

【Vagrant】MacにAWS公式のAmazon Linux2 Vagrant 環境を作成【オンプレミス】

【Vagrant】MacにAWS公式のAmazon Linux2 Vagrant Boxを作成、起動【オンプレミス】

Vagrant】Macに用意したVagrantのAmazon Linux2 環境にVirtualBox Guest Additionsをインストール【オンプレミス】

【Vagrant】Macに用意したAmazon Linux2 環境にPythonをインストール【オンプレミス】

【Vagrant】Macに用意したAmazon Linux2 環境にuwsgi、Django、Django Restframeworkをインストール【オンプレミス】

【Vagrant】Macに用意したAmazon Linux2 環境にsqlite3、nginxをインストール【オンプレミス】

【Vagrant】Macに用意したAmazon Linux2 環境にDjangoプロジェクト作成【オンプレミス】

【Vagrant】DRFアプリケーション作成、テスト起動【Django】

【API】DRFのアプロダ作成 その①シリアライザを作る【Django】

【API】DRFのアプロダ作成 その②ディレクトリを入れ子にする【Django】

※今回の記事は、既にAmazonLinux2のVagrant環境を作成済みであるとします。

環境

私のPC環境は以下の通りです。

PC MacBook Air (Retina, 13-inch, 2019)
CPU 1.6 GHz デュアルコアIntel Core i5
メモリ 16 GB 2133 MHz LPDDR3
OS 10.15.2
VirtualBox 6.1.12r139181
Vagrant 2.2.9

仮想環境

仮想環境は以下の通りです。

OS Amazon Linux release 2 (Karoo)
Python 3.7.9
Django 3.2
djangorestframework 3.12.4
djangorestframework-jwt 1.11.0
django-cleanup 5.2.0
uWSGI 2.0.19.1
Nginx 1.18.0
Sqlite3 1.18.0

Vagrantのディレクトリ構成

  • ~/Work/
  • Vagrant/
  • AmazonLinux2/


AmazonLinux2仮想環境へログイン

①「ターミナル」を開いて下記のコマンドを入力します。

cd ~/Work/Vagrant/AmazonLinux2

②下記のコマンドを入力して仮想環境を起動します。

Vagrant up

③下記のコマンドを入力してログインします。

vagrant ssh

シリアライザにバリデーション追加

①以下のようなDjangoアプリ構成を作ります。プロジェクト名が「config」で、アプリケーション名が「nuploader1」です。プロジェクトの設定は済ませたものとします。赤字のファイルは、今回変更する部分です。

  • mysite/
  • config/
  • __init__.py
  • settings.py
  • urls.py
  • wsgi.py
  • nuploader1/
  • __init__.py
  • __pycache__
  • migrations
  • models.py
  • serializers.py
  • urls.py
  • views.py
  • templates
  • db.sqlite3
  • manage.py

②nuploader1/serializers.pyに以下の内容を記述します。チェックを書く場所は幾つかありますが、シリアライザーのvalidateメソッドに書いていきます。

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

class CompositeSerializer(serializers.ModelSerializer):
  parent = SimpleCompositeRelation(queryset=Composite.objects.filter(is_dir=True), required=False, allow_null=True)
  composite_set = SimpleCompositeSerializer(many=True, read_only=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

こういったバリデーションはフロントエンド側でもある程度はできますが、サーバー側でもチェックは必須です。

解説

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

Validate関数またはビューでカスタム検証を適用します。

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

指定した親ディレクトリが自分だった場合、「親ディレクトリが自分です」を表示します。

③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 CompositeSerializer(serializers.ModelSerializer):
~(省略)~

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
        ])

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

あるCompositeに紐づくCompositeの一覧は取得できています。しかし、何処にも紐づかないComposite、parentがNoneなComposite…最上位にあるファイル・ディレクトリの一覧が取得できません。

  • mysite/
  • config/
  • __init__.py
  • settings.py
  • urls.py
  • wsgi.py
  • nuploader1/
  • __init__.py
  • __pycache__
  • migrations
  • models.py
  • serializers.py
  • urls.py
  • views.py
  • templates
  • db.sqlite3
  • manage.py

①今あるビュの一覧処理を上書きします。nuploader1/views.pyに記述します。

from django.shortcuts import render  # render(request, 'hello.html', context)
from django.http import HttpResponse  # HttpResponse('Hello, World !!')
from django.shortcuts import render
from django.views import View
from .models import Composite
from .serializers import CompositeSerializer
from rest_framework import viewsets, permissions  # 追加

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

    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)

解説

    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メソッドは全てのデータを取得する際に呼ばれるメソッドです。listメソッドは、「http://127.0.0.1:8000/uploader/api/composites/」のURLで呼ばれます。

        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

このURLへのアクセスだった場合にだけ、parentがNoneな一覧を取得するようにしています。中身の処理は、もともとのlistメソッドの処理とほぼ同じで、parentがNoneなものに絞り込んだだけです。

「http://127.0.0.1:8000/uploader/api/composites/1/」とかの単体データの取得では呼ばれないメソッドです。また、作成や更新でも呼ばれないため、他への影響はありません。

ただし、今回のように上書きすると全てのデータを取得するエンドポイントがなくなります。


ログイン必須にする

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

  • mysite/
  • config/
  • __init__.py
  • settings.py
  • urls.py
  • wsgi.py
  • nuploader1/
  • __init__.py
  • __pycache__
  • migrations
  • models.py
  • serializers.py
  • urls.py
  • views.py
  • templates
  • db.sqlite3
  • manage.py
from django.shortcuts import render  # render(request, 'hello.html', context)
from django.http import HttpResponse  # HttpResponse('Hello, World !!')
from django.shortcuts import render
from django.views import View
from .models import Composite
from .serializers import CompositeSerializer
from rest_framework import viewsets, permissions  # 追加

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

〜(省略)〜

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

次回

フロントエンド(Vue.js)の準備をします。

【AmazonLinux2】AmazonLinux2にNode.jsとVueをインストールする【Vue】


トラックバック用のURL
プロフィール

名前:イワサキ ユウタ 職業:システムエンジニア、ウェブマスター、フロントエンドエンジニア 誕生:1986年生まれ 出身:静岡県 特技:ウッドベース 略歴 20

最近の投稿
人気記事
カテゴリー
広告