Djangoで複数ファイル(画像)のプレビューとアップロード

Djangoで複数の画像をアップロードする際、少し手間取ったのでメモを残します。

アップロードの前にプレビューを挟み、画像を確認するコードもまとめます。

Settings.pyとurls.pyの設定

settings.pyに下記を追記します。

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

さらに、アプリケーションディレクトリのurls.pyに下記を追記します。

# To serve files uploaded by a user during development
from django.conf import settings
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

プロジェクトのルートの media ディレクトリにパスが通ります。

本番環境では別の方法を推奨されていますが、取り急ぎこれで進めます。

models.pyの設定

次のようにモデルを作成しました。

# models.py

def user_portfolio_directory_path(instance, filename):
    return 'image-{0}/{1}'.format(instance.id, filename)

class Image(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    image = models.ImageField(upload_to=user_portfolio_directory_path, null=True, blank=True)
    uploaded_at = models.DateTimeField(auto_now_add=True)

upload_to で画像の保存先を指定しています。

また、ImageFieldを使うにはPillowというパッケージが必要なので、インストールします。

pip install Pillow

forms.pyの設定

forms.pyを次のように設定します。

# forms.py
class ImageForm(forms.ModelForm):
    image = forms.ImageField(
        widget=forms.ClearableFileInput(attrs={'multiple': True}),
    )
    class Meta:
        model = Image
        fields = ('image',)

input タグには multiple という属性を付与することができます

multiple 属性を付与することで、複数のファイルを選択できるようになります。

# 例
<input type="file" name="image" multiple="" accept="image/*">

上記forms.pyのコードでは、 attrs={'multiple': True} の部分でmultiple属性を付与しています。

multiple属性は widget_tweaks を使って、テンプレート側で付与することもできます。わかりやすいので、私はこちらの方法を好んで使っています。

# template
{%!r(MISSING)ender_field form.image.image multiple="" %!}(MISSING)

画像のプレビュー

選択した画像をプレビューし、確認できるようにします。今回はjQueryで作りました。

# jsファイル

$(document).on('click', '.upload-portfolio-image-btn', function() {
    var inputToUploadPortfolioImage = $(this).next('.upload-portfolio-image')
    var portfolioImageGroup = inputToUploadPortfolioImage.closest('.form-row').next().next('.portfolio-image-group')

    inputToUploadPortfolioImage.click()
    inputToUploadPortfolioImage.off('change').on('change', function(e) {
      if (e.target.files && e.target.files[0]) {
        var files = e.target.files
        for (var i = 0; i < files.length; i++) {
          var file = files[i]
          var reader = new FileReader()
          reader.onload = function (e) {
            portfolioImageGroup.append(`
              <div class="form-group col-md-3">
                <div class="responsive-img-wrapper portfolio-image">
                  <div class="responsive-img" style="background-image: url(${e.target.result})">
                  </div>
                </div>
              </div>`)
          }
          reader.readAsDataURL(file)
        }
      }
    })
  })

↓Templateは次のような感じです。widget_tweaksを使っています。

# template

{%!l(MISSING)oad widget_tweaks %!}(MISSING)

<form class="post-form-to-edit-portfolio" method="post" enctype="multipart/form-data">
  {%!c(MISSING)srf_token %!}(MISSING)
  <div class="form-group">
    <div class="form-row">
      <div class="col-md-12">
        <div class="upload-portfolio-image-btn">
          クリックで画像をアップロード<br>
          JPEG / PNG / GIF形式に対応しています
        </div>
        {%!r(MISSING)ender_field form.image.image class="custom-file-input upload-portfolio-image" multiple="" %!}(MISSING)
        {%!f(MISSING)or error in form.image.image.errors %!}(MISSING)
        <span class="text-warning">{{ error }}</span>
        {%!e(MISSING)ndfor %!}(MISSING)
      </div>
    </div>
    <hr>
    <div class="form-row portfolio-image-group">
      <!-- ここで画像をプレビュー -->
    </div>
  </div>
  <div class="form-btn">
    <button type="button" class="btn btn-outline-info btn-to-cancel-upload">キャンセル</button>
    <button type="submit" class="btn btn-info btn-to-upload-images">更新</button>
  </div>
</form>

formタグに method="post" enctype="multipart/form-data" 属性をつけるの忘れずに。

cssでデザインし、下図のような感じになりました。

Djangoで複数画像をプレビュー

画像のアップロード

選択した画像をviews.pyに渡し、DBに保存します。

# views.py
...
def portfolio_edit_post(request):
    if request.method == 'POST':
        image_form = ImageForm(request.FILES)
        if image_form.is_valid():
            portfolio_images = request.FILES.getlist('image', False)
            for image in portfolio_images:
                image_instance = Image(
                    image=image,
                )
                image_instance.save()
                print("success save images.")
...

getlist()メソッドで、複数の画像をリストとして取得できます。

画像ファイルはリストとして送信されるので、for文で一つずつ処理します。

画像データのバリデーション

バリデーションは大事です。

アップロードされたファイルがウイルスだったり、GByteレベルの重いファイルだと困っちゃいますからね。

バリデーションにあたり考えるべき項目は次の3つです。

  1. ファイル名と拡張子
  2. メタデータ(コンテンツタイプとサイズ)
  3. 実際のデータの内容(コンテンツそのもの)

1と2は偽装やなりすましが簡単なので、データの内容そのものが正しいかチェックする3番のバリデーションが最も重要。だそうです。

が、ちょっと大変なので今回は飛ばして、そのうち実装します!

まとめ

Djangoで画像のアップロードができるようになりました。やったね!

さらにAjaxを使っていい感じにデータをやりとりできるようにもなりましたので、その方法についても後日まとめます。