ゴール
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'),
]
