ゴール
ImageFieldを持つModelをRest API経由でcreateとbulk_createの2つを実現したい。
実装概要
-
公式のドキュメントにあるように、
ListSerializer
をカスタムしてcreate
メソッドからModelのbulk_create
を呼ぶようにする - Base64デコードするFieldクラスを作成する
-
ModelSerializer
のlist_serializer_class
に カスタムしたSerializer
クラスを設定する -
ModelSerializer
に Base64デコードするFieldを追加する -
APIView
のget_serializer
で POSTされたデータに基づき、many=True
を設定する
前提
次のような、ユーザーが一枚一枚画像をアップロードできるフォームが作ってあるとする。
このForm/Viewは維持したまま、以下のようなREST APIも提供したい。
bulk アップロードのデータ構造がよくないが、とりあえずこれにする。
{
"name": "sample image 001",
"description": "This is sample image for uploading single image",
"image": "<Base64エンコードされた画像データ>"
}
[
{
"name": "sample image 001",
"description": "This is the 1st sample image for bulk uploading multiple images",
"image": "<Base64エンコードされた画像データ>"
},
{
"name": "sample image 002",
"description": "This is the 2nd sample image for bulk uploading multiple images",
"image": "<Base64エンコードされた画像データ>"
},
]
Django の Model、Form、View
妥当性やら脆弱性はこの際無視して、画像を保存できるようなModelと、ユーザーが一枚一枚画像をアップロードできるForm、Viewを定義する。
class Icon(models.Model):
description= models.TextField(
null=True,
blank=True,
)
# API では image にしているが名前が違う
image_path = models.ImageField(
storage=select_storage,
default='blank.svg',
blank=False
)
name = models.CharField(
max_length=ICON_NAME_MAX_LENGTH,
validators=[
MinLengthValidator(ICON_NAME_MIN_LENGTH)
],
null=False,
blank=False,
)
class IconUploadForm(ModelForm):
class Meta:
model = Icon
fields = [ 'image', 'name', ]
class IconUploadView(CreateView):
template_name = 'upload.html'
form_class = IconUploadForm
success_url = reverse_lazy('index')
Django REST frameworkでの実装方法
Serializerの実装
1. Base64をデコードするFieldクラスを定義する
ここらへん を参考に、serializer.ImageField.to_interval_value
をオーバーライドして、デコード、ファイルデータ化、親クラスとしてのImageField
への格納を行うクラスを作成する
class Base64ImageField(serializers.ImageField):
def to_internal_value(self, data):
if isinstance(data, str) and data.startswith('data:image'):
format, imgstr = data.split(';base64,')
ext = format.split('/')[-1]
name = f"{uuid.uuid4().urn[9:]}.{ext}"
data = ContentFile(name=name, content=base64.b64decode(imgstr))
return super().to_internal_value(data)
2. カスタムなListSerializer クラスを定義する
公式のドキュメントにあるように、ListSerializer
をカスタムして create
メソッドからModelのbulk_create
を呼ぶクラスを定義する。
class IconListSerializer(serializers.ListSerializer):
def create(self, validated_data):
icon_data_list = [Icon(**icon) for icon in validated_data]
return Icon.objects.bulk_create(icon_data_list)
objects.bulk_create
を使用しない場合、この定義は不要。
ドキュメントに記載されている通り、既定ではSerializer
は serializers.ListSerilizer
を呼び出す。
3. ModelSerializerを作成し、カスタムしたクラスを設定する
-
Iconモデルの参照先名前を指定
Base64ImageField
をModelSerializserのフィールドとして設定する。このとき、models.py
ではimage_path
になっているので、source
パラメーターでIconモデルの参照先名前を指定する。Django REST frameworkのドキュメント Serializer fields
Meta
のfields
にはBase64ImageField
の 変数名を指定しておくimage = Base64ImageField(source='image_path')
-
カスタムしたListSerializerを設定
Serializer
と ListSerializer
の切り替えは、インスタンス化に際して many=True
が与えられるかどうかで切り替える実装となっている。切り替えの実装はBaseSerializer
クラスのコンストラクタを参照。
Django REST frameworkのドキュメント Customizing ListSerializer behavior
list_serializer_class = IconListSerializer
最終的なSerializer
クラスは以下のような定義となる。
class IconSerializer(serializers.ModelSerializer):
image = Base64ImageField(source='image_path')
class Meta:
model = Icon
list_serializer_class = IconListSerializer
fields = [
'name', 'description', 'image'
]
Viewの実装
serializer_class
は get_serializer
メソッドでインスタンス化される。
generics.APIView
の実装を見ると、get_serializer
に与える *args
、**kwargs
は、そのまま serializer_class
のコンストラクタに引き渡される。結果、get_serializer
メソッドにmany=True
を引数として与えれば、serializer_class
の list_serializer_class
がインスタンスにとして返却される。
つまり、受信データがbulkアップロードのデータ構造となっているかどうかを判定して、親クラスのget_serializer
の引数に many=True
を設定する・しないを切り替えるようにする。
class IconBulkUploadView(generics.CreateAPIView):
serializer_class = IconSerializer
def get_serializer(self, *args, **kwargs):
# ここで受信データがリスト形式か、オブジェクト形式かを判定する
# リスト形式なら many=True を設定する
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
# many=True`を引数として与えれば、`list_serializer_class` のインスタンスが返却される
return super().get_serializer(*args, **kwargs)
残りのもろもろ
urls.py
では views.py
の generics
の view であるから、 router
は使用せずに、そのままurlpatterns
に設定する。
urlpatterns = [
# いろいろ ----
path('bulk', views.IconBulkUploadView.as_view(), name='bulk_upload'),
]