Djangoで画像ファイルをアップロードする方法

DjangoでImageFieldを用いた画像ファイルをアップロードする方法をご紹介します。

条件

  • Django 2.1.4
  • Python 3.7.0

事前準備

Pillow

ImageFieldでは「Pillow」が必須となります。
以下のコマンド、またはPyCharmのファイル > 設定 > プロジェクト > プロジェクト・インタープリター > 使用可能なパッケージからPillowを追加します。

pip install Pillow

settings.py

settings.pyに以下の記述を追加します。

# settings.py抜粋

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

ちなみに、「MEDIA_URLで指定したパス」と下記のmodels.pyで定義する「ImageFieldのupload_toで指定したパス」を連結したパスが実際に画像ファイルがアップロードされるパスとなります。
ImageField(upload_to=”image/”)

⇒今回の場合、「プロジェクト\media\image」の下に画像ファイルがアップロードされます。

実装

ここでは、ユーザ毎に作品ポートフォリオを管理するサービスを例にとります。
モデルの関係は以下の通りです。

  • ユーザ:作品 = 1:N
  • 作品:画像 = 1:N

なお、本記事においてはログイン認証に関する説明は割愛します。

models.py

以下のようなモデルを作成します。

# models.py

from django.db import models
from django.urls import reverse


class Work(models.Model):
    """作品モデル"""
    class Meta:
        db_table = 'work'

    name = models.CharField(verbose_name='作品名', max_length=255)
    memo = models.CharField(verbose_name='メモ', max_length=255, default='', blank=True)
    author = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
    )
    created_at = models.DateTimeField(verbose_name='登録日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

    def __str__(self):
        return self.name

    @staticmethod
    def get_absolute_url(self):
        return reverse('pictures:index')


class Image(models.Model):
    """イメージモデル"""
    class Meta:
        db_table = 'image'

    work = models.ForeignKey(Work, verbose_name='作品', on_delete=models.PROTECT)
    image = models.ImageField(upload_to="image/", verbose_name='イメージ')
    created_at = models.DateTimeField(verbose_name='登録日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

    def __str__(self):
        return self.work.name + ":" + str(self.data_datetime)

urls.py

作品の一覧画面、詳細画面、画像ファイルアップロード画面のパスを定義します。

# urls.py

from django.urls import path
from . import views

app_name = 'pictures'

urlpatterns = [
    # トップ画面
    path('', views.IndexView.as_view(), name='index'),

    # 詳細画面
    path('pictures/<int:pk>/', views.DetailView.as_view(), name='detail'),

    # ファイルアップロード用
    path('pictures/<int:pk>/upload/', views.Upload.as_view(), name='upload'),
    path('pictures/<int:pk>/upload_complete/', views.UploadComplete.as_view(), name='upload_complete'),
]

forms.py

画像ファイルアップロード用のformを作成します。
ここでは拡張子をjpgに限定するバリデーションチェックを追加しています。

from django import forms
import os

VALID_EXTENSIONS = ['.jpg']


class UploadFileForm(forms.Form):
    image = forms.ImageField()

    def clean_image(self):
        image = self.cleaned_data['image']
        extension = os.path.splitext(image.name)[1]  # 拡張子を取得
        if not extension.lower() in VALID_EXTENSIONS:
            raise forms.ValidationError('jpgファイルを選択してください!')
        return image  # viewsでcleaned_dataを参照するためreturnする

views.py

views.pyを以下のように記述します。
アップロードのバリデーションが通った後の処理「def form_valid(self, form):」でDBにレコードを保存するところがポイントです。
画像ファイル自体は「プロジェクト\media\image」の下にアップロードされます。

# views.py

from django.shortcuts import redirect
from django.views import generic
from .models import Work, Image
from django.contrib.auth.mixins import LoginRequiredMixin
import logging
from .forms import UploadFileForm
from django.views.generic.edit import FormView

logger = logging.getLogger('development')


# 一覧画面
class IndexView(LoginRequiredMixin, generic.ListView):
    model = Work
    paginate_by = 5
    ordering = ['-updated_at']
    template_name = 'pictures/index.html'


# 詳細画面
class DetailView(generic.DetailView):
    model = Work
    template_name = 'pictures/detail.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        self.request.session.modified = True  # セッション更新
        context['work_pk'] = self.kwargs['pk']

        return context


# ファイルアップロード
class Upload(FormView):
    template_name = 'pictures/upload.html'
    form_class = UploadFileForm

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        form = self.get_form()
        context = {
            'work_pk': self.kwargs['pk'],
            'form': form,
        }
        return context

    def form_valid(self, form):

        image = Image()  # DBへの保存
        image.work_id = self.kwargs['pk']  # 作品ID
        image.image = form.cleaned_data['image']  # アップロードしたイメージパス
        image.save()

        return redirect('pictures:upload_complete', self.kwargs['pk'])  # アップロード完了画面にリダイレクト


# ファイルアップロード完了
class UploadComplete(FormView):
    template_name = 'pictures/upload_complete.html'
    form_class = UploadFileForm

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        self.request.session.modified = True  # セッション更新
        context['work_pk'] = self.kwargs['pk']

        return context

テンプレート

アップロード画面とアップロード完了画面のテンプレートファイルを作成します。
(プロジェクト\pictures\templates\picturesの下)

upload.html

<!-- upload.html -->
{% extends 'base.html' %}

{% block content %}

<h1>アップロード サンプル</h1>

<form method="POST" enctype="multipart/form-data">
    {% csrf_token %}
    {% if form.errors %}
        {% for field in form %}
            {% for error in field.errors %}
                <div class="alert alert-danger">
                    <strong>{{ error|escape }}</strong>
                </div>
            {% endfor %}
        {% endfor %}
        {% for error in form.non_field_errors %}
            <div class="alert alert-danger">
                <strong>{{ error|escape }}</strong>
            </div>
        {% endfor %}
    {% endif %}

    {% for field in form %}
    <div class="form-group form-inline">
        <div class="col-md-8">
            {{ field }}
        </div>
    </div>
    {% endfor %}
    <div class="form-group">
        <button type="submit">アップロード</button>
    </div>
</form>

    <section>
        <p><a href="javascript:history.back()">&lt; Back</a></p>
    </section>

{% endblock %}

upload_complete.html

{% extends 'base.html' %}

{% block content %}

<h1>アップロード完了</h1>

<button type="submit" onClick="javascript:history.back();">戻る</button>

{% endblock %}

動作確認

事前準備

「createsuperuser」でsuperuserを作成します。
(ここではadminという名前で作成)

サーバーを起動して管理者サイトでworkにレコードを追加します。

レコード状態

workに1レコード存在し、imageにはレコードが存在しない状態です。

アップロード実行

ログインすると一覧画面に遷移し、先ほど追加したworkの情報が表示されます。

work情報のリンクをクリックして詳細画面に遷移します。

メニューに「ファイルアップロード」のリンクが表示されるのでクリックします。
アップロード画面に遷移します。

「ファイルを選択」で任意のJPG画像ファイルを選択し、アップロードボタンを押します。
アップロード完了画面に遷移します。

media\imageというディレクトリが作成され、その下にtest.jpgがアップロードされます。

imageテーブルにレコードが追加されます。

再度同じ画像ファイルをアップロードすると、適当な文字列が付加されてアップロードされます。
(同じ名称の画像ファイルでも以前にアップロードした画像ファイルが上書きされることはありません。)

以上で画像ファイルアップロード方法の紹介は終わりです。

サンプルソース

GitHubに当該記事のサンプルソースを公開しています。

https://github.com/kzmrt/works

参考

Qiita:Django – 画像ファイルのアップロード処理

https://qiita.com/narupo/items/e3dbdd5d030952d10661

Django公式:特定のフィールド属性をクリーニングする

https://docs.djangoproject.com/ja/2.1/ref/forms/validation/#cleaning-a-specific-field-attribute

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です