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()">< 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

