ListSerializer を使ってImageFieldをbulk createする

# ゴール

ImageFieldを持つModelをRest API経由でcreateとbulk_createの2つを実現したい。

# 実装概要

1. [公式のドキュメント](https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-create)にあるように、`ListSerializer` をカスタムして `create` メソッドからModelの`bulk_create`を呼ぶようにする
1. Base64デコードするFieldクラスを作成する
1. `ModelSerializer` の `list_serializer_class` に カスタムした`Serializer` クラスを設定する
1. `ModelSerializer` に Base64デコードするFieldを追加する
1. `APIView` の `get_serializer` で POSTされたデータに基づき、`many=True` を設定する

# 前提

次のような、ユーザーが一枚一枚画像をアップロードできるフォームが作ってあるとする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/133372/b6b24d21-b9ac-25df-19f3-31b80675e95f.png)

このForm/Viewは維持したまま、以下のようなREST APIも提供したい。
bulk アップロードのデータ構造がよくないが、とりあえずこれにする。

```json: 一枚アップロード
{
  "name": "sample image 001",
  "description": "This is sample image for uploading single image",
  "image": "<Base64エンコードされた画像データ>"
}
```
```json: Bulkアップロード
[
  {
    "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を定義する。

```python: models.py
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,
    )
```
```python: forms.py
class IconUploadForm(ModelForm):
    class Meta:
        model = Icon
        fields = [ 'image', 'name', ]
````
```python: views.py
class IconUploadView(CreateView):
    template_name = 'upload.html'
    form_class = IconUploadForm
    success_url = reverse_lazy('index')
```

# Django REST frameworkでの実装方法

## Serializerの実装

### 1. Base64をデコードするFieldクラスを定義する

[ここらへん](https://github.com/Hipo/drf-extra-fields/blob/master/drf_extra_fields/fields.py) を参考に、`serializer.ImageField.to_interval_value` をオーバーライドして、デコード、ファイルデータ化、親クラスとしての`ImageField` への格納を行うクラスを作成する

```python: serializer.py
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 クラスを定義する

[公式のドキュメント](https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-create)にあるように、`ListSerializer` をカスタムして `create` メソッドからModelの`bulk_create`を呼ぶクラスを定義する。

```python:serializer.py
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](https://www.django-rest-framework.org/api-guide/fields/#source)

  `Meta` の `fields` には `Base64ImageField` の 変数名を指定しておく

  ```python
  image = Base64ImageField(source='image_path')
  ```

- カスタムしたListSerializerを設定

　　`Serializer` と `ListSerializer` の切り替えは、インスタンス化に際して `many=True` が与えられるかどうかで切り替える実装となっている。切り替えの実装は`BaseSerializer`クラスのコンストラクタを参照。

   [Django REST frameworkのドキュメント Customizing ListSerializer behavior](https://www.django-rest-framework.org/api-guide/serializers/#customizing-listserializer-behavior)

   ```python
   list_serializer_class = IconListSerializer
   ```

最終的な`Serializer`クラスは以下のような定義となる。

```python: serializer.py
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` を設定する・しないを切り替えるようにする。


```python:views.py
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`に設定する。

```python:urls.py
urlpatterns = [
    # いろいろ ----
    path('bulk', views.IconBulkUploadView.as_view(), name='bulk_upload'),
] 
```