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

投稿日:   |  最終更新日:

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

DjangoDjango REST frameworkPython

DRFのアップローダアプリを作成します。アップローダーをフォルダの入れ子に改造します。

ディレクトリの入れ子構造の作成

前回、REST APIはJSONやXMLなどでデータのやり取りをする基本的な部分を作成しました。

今回は、アップローダーをツリー構造にします。例えば、トップページでは「file1.txt」、「file2.png」、dir1(ディレクトリ)、dir2(ディレクトリ)が表示されます。

  • file1.txt
  • file2.png
  • dir1/
  • dir2/

「dir1/」というURLでアクセスすると「file3.txt」と「dir3」が表示されます。

  • file1.txt
  • file2.png
  • dir1/
  • file3.txt
  • dir3/
  • dir2/

「dir2/」であれば「file4.txt」が表示されます。つまり、対象ディレクトリをURLで指定して、中のファイル・ディレクトリ一覧を表示します。

  • file1.txt
  • file2.png
  • dir1/
  • dir2/
  • file4.txt

これを表現するには、ある単体のデータを取得すると以下のようなJSONの結果となります。

{
    "pk": 3,
    "name": "dir1",
    "is_dir": true,
    "src": null,
    "parent": null,
    "zip_depth": 0
}

この段階で、次のように紐づく子データも取得します。

{
    "pk": 3,
    "name": "dir1",
    "is_dir": true,
    "src": null,
    "parent": null,
    "zip_depth": 0,
    "composite_set": [
        {
            "pk": 4,
            "name": "file3.txt",
            "is_dir": false,
            "src": "http://127.0.0.1:8000/media/file3.txt",
            "parent": 3,
            "zip_depth": 0,
        },
        {
            "pk": 5,
            "name": "dir3",
            "is_dir": true,
            "src": null,
            "parent": 3,
            "zip_depth": 0,
        },
    ]
}

作業の流れ

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】

※今回の記事は、既に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に以下の内容を記述します。

from rest_framework import serializers
from .models import Composite

class CompositeSerializer(serializers.ModelSerializer):

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

解説

DRFのシリアライザとは、データの相互変換をします。JSON等を送るとそれをモデルに変換したり、モデルを基にJSON等を作ったりします。通常のDjangoで言うと、フォームと似たことを行います。

fieldsに「composite_set」追加します。

from django.db import models
 
class Composite(models.Model):
    name = models.CharField('名前', max_length=255)
    is_dir = models.BooleanField('ディレクトリか', default=True)
    src = models.FileField('ファイルソース', blank=True, null=True)
    parent = models.ForeignKey(
        'self', verbose_name='親ディレクトリ', on_delete=models.CASCADE,
        blank=True, null=True, limit_choices_to={'is_dir': True}
    )
    zip_depth = models.PositiveIntegerField('zipファイルの深さ', default=0)
 
    class Meta:
        ordering = ('-is_dir', 'name')
 
    def __str__(self):
        if self.is_dir:
            return f'{self.pk} - {self.name}/'
        else:
            return f'{self.pk} - {self.name}'
 
    def get_display_name(self):
        if self.is_dir:
            return f'{self.name}/'
        else:
            return f'{self.name}'

Compositeモデルは再帰的な構造のモデルで、「parent = models.ForeignKey(Composite)」といった感じのフィールドがあります。この場合、あるCompositeモデルインスタンスに紐づくCompositeの一覧は「composite.composite_set.all()」のようにして取り出せます。

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

③runserverで起動し、「http://192.168.33.10:8000/uploader/api/」にアクセスします。

python3 manage.py runserver 0.0.0.0:8080

通常のモデルフィールドと同様に、composite_setのような属性もfieldsに指定することができます。

作られるJSONにもcomposite_setというリストが増えました。

フィールドの読み取り専用モード

画像下部の入力フォーム部分を見ると、composite_setという項目があって、紐づくCompositeを選択できるようになっています。

シリアライザーはJSON等への変換を行いますが、送信されてきたデータの変換も行います。イメージとしては、入力フォームの各入力欄等に似ています。状況によっては、表示だけに使いたいフィールドも発生します。そういう場合は、以下のようにして表示専用のフィールドに変更できます。

①nuploader1/serializers.pyに以下の内容を記述します。

from rest_framework import serializers
from .models import Composite

class CompositeSerializer(serializers.ModelSerializer):

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

解説

「read_only_fields」を追加します。このシリアライザーを使っての処理でcomposite_setを変更不可なります。具体的には、CompositeViewSetでは変更できなくなります。

Django管理サイトでは変更できます。また、変更できるシリアライザーを別途作ることもできます。


フィールドの詳細なデータを表示

現状、composite_setはpkのリストです。pk以外(name、is_dir)のフィールドも使うかもしれません。

from rest_framework import serializers
from .models import Composite

class CommentSerializer(serializers.ModelSerializer):

    class Meta:
        model = Composite
        fields = ('pk', 'name', 'target')

class PostSerializer(serializers.ModelSerializer):
    comment_set = CommentSerializer(many=True, read_only=True)

    class Meta:
        model = Composite
        fields = ('pk', 'title', 'text', 'comment_set')

解説

ブログの記事とコメントのような関係だった場合を例に説明します。

「Commentモデル」は、「target=models.ForeignKey(Post)」のようにしてPostモデルと紐づいているとします。

fields = ('pk', 'title', 'text', 'comment_set')

上記では、「pk、title、text、comment_set」が表示されますが、「comment_set」は「pk(主キー)」のリストです。「comment_set」の表示を上書きするために、クラス属性に

comment_set = CommentSerializer(many=True, read_only=True)

として上書きすることができます。

数値や文字列ではなく、

{pk: 1, name: "名無し", target: 1}

のようなオブジェクト表現が欲しい場合はシリアライザーを利用します。

複数のデータが予想されるので、many=Trueを記述します。

read_only引数を指定して読み取り専用にします。

1モデルに複数シリアライザを定義できる

1モデルにつき、複数のシリアライザを定義できます。Compositeモデルは再帰的な構造ですが、やることはほぼ同じです。「nuploader1/serializers.py」を以下のように変更します。

from rest_framework import serializers
from .models import Composite

class SimpleCompositeSerializer(serializers.ModelSerializer):

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

class CompositeSerializer(serializers.ModelSerializer):
    composite_set = SimpleCompositeSerializer(many=True, read_only=True)

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

「http://192.168.33.10:8080/uploader/api/composites/5/」(dir1ディレクトリ)のURLを指定し、フォームを入力して「PUT」ボタンをクリックします。

〜省略〜
        fields = ('pk', 'name', 'is_dir', 'zip_depth', 'parent')

〜省略〜

SimpleCompositeSerializerのfieldsは、不要なフィールド(src、composite_set)を省きました。

  • src・・・この後作るVue.js側の処理で、特に必要なかったので消しています。
  • composite_set・・・含めてしまうと再帰的に表示され、ディレクトリの階層が100ぐらい深い物があれば、それら全てを表示してしまいます。

必要なのは、単一のCompositeに紐づくComposite(ディレクトリ内直下のファイル・ディレクトリの一覧)だけ欲しいので、その中や更に中のものはオブジェクトとしては不要です。

Metaのオプションにはdepth = 1のような指定ができ、これによってもオブジェクトとして表示することもできるのですが、処理をカスタマイズしたい場合には使えません。今回の場合、次の説明するparent詳細表示で特殊なカスタマイズが必要なので、depthオプションは見送りました。


親データの詳細表示

CompositeSerializerのparentフィールドも、詳細表示できるように変更します。

from rest_framework import serializers
from .models import Composite

class SimpleCompositeSerializer(serializers.ModelSerializer):

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

class CompositeSerializer(serializers.ModelSerializer):
    parent = SimpleCompositeSerializer()
    composite_set = SimpleCompositeSerializer(many=True, read_only=True)

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

parentの値は変更できるようにしたいので、read_onlyは不要です。また、単体のデータなので、many=Trueも不要です。試しにブラウザで確認してみると、一見大丈夫表示そうですが、ページ下部が問題です。親ディレクトリを変更したいだけなのに、フォームだらけです。

実際に送信してみると、

The.update()method does not support writable nested fields by default. Write an explicit.update()method for serializernuploader1.serializers.CompositeSerializer, or setread_only=Trueon nested serializer fields.

というエラーも出てしまいます。

目的は、parentの値を設定・変更したい場合はpkを直接送信します。表示するときは、SimpleCompositeSerializerを使ってオブジェクトとして表示したいのです。

from rest_framework import serializers
from .models import Composite

class SimpleCompositeSerializer(serializers.ModelSerializer):

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

class SimpleCompositeRelation(serializers.RelatedField):

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

    def to_internal_value(self,value):
        return self.get_queryset().get(pk=data)

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

解説

class SimpleCompositeRelation(serializers.RelatedField):

serializers.RelatedFieldを継承したSimpleCompositeRelationクラスです。これはForeignKeyやOneToOne、ManyToManyなフィールドを表現するのに使えます。parentはForeignKeyです。複雑な動作をさせたい場合は、このクラスを使って細かい挙動を制御します。

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

モデルインスタンスをどのように表現するかを記述します。「SimpleCompositeSerializer(value).data」とすると、SimpleCompositeSerializerを使ってオブジェクトとして表現するようにします。「{pk: 1, name: “dir2″…}」みたいな感じです。

    def to_internal_value(self, data):
        return self.get_queryset().get(pk=data)

モデルインスタンスへの複合化をどのようにするかの処理です。pkが送信されてくる予定なので、「self.get_queryset().get(pk=data)」としています。これは「Composite.objects.all().get(pk=data)」のような処理を内部で行います。queryset引数と関連しています。

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)

このように作ったクラスを、

parent = SimpleCompositeRelation(
  queryset=Composite.objects.filter(is_dir=True),
  required=False,
  allow_null=True
)

として使っています。

ここで渡したqueryset引数が、「to_internal_value」の「get_queryset()」で参照されます。ですので先ほどのコードは、「Composite.objects.filter(is_dir=True).get(pk=data)」となります。

「is_dir=True」に絞り込んでいるのは、親ディレクトリの指定なのでファイルは除外するからです。

required=Falseですが、これは以下のような送信データを許可します。

{
    name: "dir3",
    is_dir: true,
    zip_depth: 0,
}

もし全てのフィールドがrequired=Trueならば、以下のように送信する必要があります。

{
    name: "dir3",
    is_dir: true,
    zip_depth: 0,
    src: null,
    parent: null,
}

PUTではなくPATCHメソッドを使った場合も同様で、値を送信しなくてもOKです。

極端な話、PATCHでは入力欄がなくて、送信ボタンしかないようなフォームでも多くの場合は動作します。PUTだった場合、今回のモデルではnameフィールドが「required=True」になっているので、送信ボタンしかないフォームで送信するとnameを送信しろと怒られます。

PUTとPATCHの使い分け

PATCHは一部更新に使うもので、PUTは更新というよりもデータそのものを置き換えるといったニュアンスに近いです。ですので、PUTは必須のフィールドの値を含めていないと、「その情報だけでは置き換える為の新しいデータが作れない」といった感じでエラーになります。


次回

工事中。


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

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

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