DjangoとJavaScriptで画像複数同時アップロードの実装

# 本記事について
Djangoのライブラリでは，画像を複数同時にアップロードし，データベースに記録するという機能がない．ネットで調べても方法が見あたらなかったため，JavaScriptと組み合わせることで実装した．そのノウハウを備忘録及び，同じ問題に直面した人に貢献できるようにと記事を投稿する．

#もくじ
- 実装方法の説明
- Djangoの初期設定
- views.pyの記述
- テンプレートの記述
- 注意点
- まとめ
- 参考文献

##実装方法の説明
まず前提知識として，Djagoのモデルは，ImageFieldにおいて１フィールドにつき１つの画像しか保存することができない．したがって複数の画像を同時アップロードしたい場合は，必要な数だけフィールドを作成する必要がある．
以下に具体的な実装方法の手順を示す．なお，各詳細は後に説明する．

1. HTMLのmultipleのinputタグを作成
2. モデルフォームでアップロード枚数の上限とフォームセットを生成
3. 送信と同時に，JavaScriptのDataTransferで選択された画像の数だけオブジェクトを生成し，別々のinputタグに格納する
4. サーバーサイドのviews.pyで，formsetをデータベースに保存する

## Djangoの初期設定
ここでは，実際に実装する前に，一例として開発するtest用のDjangoの各ファイルの初期設定を行う．
以下に，urls.py, models.py, forms.pyについてのコードを記す．
なお，urls.pyのuploadfuncは後にviews.pyに記述する関数である．

``` urls.py
from django.urls import path, include
from .views import uploadfunc

urlpatterns = [
    path('upload/', uploadfunc, name='upload'),
]

```

``` models.py
from django.db import models

class TestModel(models.Model):
    files = models.ImageField('画像ファイル', null=True, blank=True, default='')
```

``` forms.py
from django import forms
from .models import TestModel

class SingleUploadModelForm(forms.ModelForm):
    class Meta:
        model = TestModel
        fields = '__all__'
```

## views.pyの記述
EXTRAは，アップロードする画像の上限枚数を意味している．
formsetはDjangoのformsのライブラリ（フォームモデル）を使用する．


``` views.py
from django.shortcuts import render, redirect
from django.contrib.auth.models import User
from .models import TestModel
from django import forms
from .forms import SingleUploadModelForm

def uploadfunc(request):
    EXTRA = 5
    UploadModelFormSet = forms.modelformset_factory(TestModel, form=SingleUploadModelForm,extra=EXTRA)
    formset = UploadModelFormSet(request.POST or None, files=request.FILES or None, queryset=TestModel.objects.none())
    if request.method == 'POST': #画像が選択され，送信されたとき
        formset.save() #データベースに保存
    context = {
        'form': formset,
        'number_list': list(range(EXTRA)),
        'total_number': EXTRA,
    }
    return render(request, 'upload.html', context)

```

## テンプレートの記述
views.pyのcontextで，'form'を返している．これは，テンプレートで{{ form }}と，フォームオブジェクトを変数表示させるだけで，入力データがセットされた入力フィールドを全て出力することができる．セットされたinputタグには，それぞれデフォルトでidがつけられており，画像ファイルでは，id_form-**num**-filesというidがつけられている．なお，今回はEXTRAは5と設定したため，5つのinputタグがセットされ，idは**num**の部分に0～4までの数字が入る．
今回，複数画像を選択するため，{{ form }}とは別に，multipleを有効としたinputタグを準備する．JavaScriptのDataTransfer()機能を使うことで，選択された画像を{{ form }}のinputタグ内にそれぞれ格納する．そうすることで，送信を完了させると，選択された画像がviews.pyへ返され，複数同時選択とアップロードが実現する．

以上のことを以下の画像で説明する．これは，EXTRA=5，そして画像を3枚選択したときの例である．
![説明図2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616807/6dde00af-9357-7a51-b39b-fb7f08327016.png)

今回{{ form }}の部分は非表示にしておく．送信ボタンが押されたとき，JavaScriptの関数(multipleFunction)を呼び出し，完了後views.pyへ返す．
以下にテンプレートファイルのコードと結果を示す．

``` upload.html
<form action="" method="POST" enctype="multipart/form-data" style="margin: 10px;">
    <div hidden>{{ form }}</div>
    {% csrf_token %}
    <p>＜同時に複数の写真を選択できます＞</p>
    <input type="file" multiple accept="image/*" id="multiple-files">
    <br>
    <button id="sending" type="submit" class="sending" onclick="multipleFunction()">送信</button>
</form>

<script type='text/javascript'>
    function multipleFunction() {
        for (let i = 0; i < document.getElementById('multiple-files').files.length; i++) {
            const dt = new DataTransfer();
            dt.items.add(document.getElementById('multiple-files').files[i]);
            document.getElementById("id_form-%number-files".replace("%number", i)).files = dt.files;
        }
    }
</script>
```

結果
![結果１.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/616807/babe338c-6161-c7c5-5cbc-ed14a9c457a7.png)


## 注意点
以上により画像複数同時アップロード機能の実装が完了した．
しかし，ここで注意点が２つがある．
１つ目は，JavaScriptのDataTrandsferを対応していないブラウザが多数あるという点である．[当サイト](https://developer.mozilla.org/ja/docs/Web/API/DataTransfer)によると，特にiOSのブラウザでは現在非対応なため，iPhoneやmacでは使用できないず，要注意である．

２つ目は，今回画像のみのアップロードをしたが，その他にメモや日付などといった他のデータを同時に保存したい場合は，上記のやり方では対応できないという点である．
例えばEXTRA=5に対して3枚の画像のみをアップロードする場合，差の2つは保存されないためうまくいく．しかし，他のフィールドを同時に保存しようとした場合，５つ全て保存してしまうため，画像が2つ選択されていないというエラーが生じてしまう．
今回その突破口については別の機会に記述するが，ヒントとしては，上記の例で言うと５つアップロードした後に，余計なを２つ削除するという記述をviews.pyに書く，ということである．

## まとめ
画像同時アップロードの実装方法を記述した．今回は，JavaScriptのDataTransfer()機能を用いて実装した．少々説明がわくりにくかったかもしれないが，わからなかった場合は各ファイルのコードを参考にして頂きたい．

## 参考文献
1. 横瀬明仁, 現場で使えるDjangoの教科書<<基礎編>>，2019