/ PythonDjango

Djangoで、ファイルアップロード

概要

今回はちょっとしたファイルアップローダーを作りながら、幾つかのファイルアップロード方法についてを説明していきます。ファイルアップローダーを例に説明していきますが、それに限らずに色々と応用ができます。

まず、メディアファイルを扱うための設定をしておきましょう。

settings.py

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

urls.py

from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('app.urls')),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

単一ファイルのアップロード

1つのファイルをアップロードしたい場合の例です。

モデルを利用しない

ファイルアップロード機能が欲しいが、アップロードしたファイルのURLだけ教えてくれれば充分というケースがあります。
ファイルアップロード1

ファイルアップローダー以外だと、ブログの記事本文中に画像URLを埋め込みたい場合のバックエンドだとか、簡易的なお問い合わせページ(モデルとして管理していない)を作っていて、メールで内容と一緒にファイルのURLも受け取りたい場合にも応用できます。割と使い道がある方法です。

forms.pyです。

from django import forms
from django.core.files.storage import default_storage


class SingleUploadForm(forms.Form):
    file = forms.ImageField(label='画像ファイル')

    def save(self):
        upload_file = self.cleaned_data['file']
        file_name = default_storage.save(upload_file.name, upload_file)
        return default_storage.url(file_name)

これはあくまで、モデルフォームではなく通常のフォームです。save()メソッドは上書きした訳ではありませんforms.Formにはsave()メソッドはないです。

保存処理をするメソッドの名前として、そしてモデルフォームと似たような扱いができるようにsave()という名前にしているだけです。upload()とかそういう名前にしても大丈夫です。

saveメソッドは、ファイルの保存を行い、そのファイルURLのパスを返しています。/media/1_CIGg8bv.pngみたいな文字列を返します。ファイル名が被ったら、似たような名前を勝手につけてくれます。

    def save(self):
        upload_file = self.cleaned_data['file']
        file_name = default_storage.save(upload_file.name, upload_file)
        return default_storage.url(file_name)

ビューです。

from django.views import generic
from .forms import SingleUploadForm


class SingleUploadView(generic.FormView):
    form_class = SingleUploadForm
    template_name = 'app/upload.html'

    def form_valid(self, form):
        download_url = form.save()
        context = {
            'download_url': download_url,
            'form': form,
        }
        return self.render_to_response(context)

フォームのsave()メソッドでファイルURLのパスが帰るので、それを取得してテンプレートへ渡しています。

テンプレートは適当に作りました。upload.htmlは以下のようにしておきます。

    <form action="" method="POST" enctype="multipart/form-data">
        {{ form.as_p }}
        {% csrf_token %}
        <button type="submit">送信</button>
    </form>

    {% if download_url %}
        <a href="{{ download_url }}">{{ download_url }}</a>
    {% endif %}

enctype="multipart/form-data"を忘れないようにしましょう。

ファイルのフルなURLを返す必要がある場合は、ビューでフルパスを作る例テンプレートで作る例を参考にしてください。

モデルを利用する

アップロードされたファイルを一覧表示したり、更新したり、削除したり、そういった操作をしたくなったらモデルを利用します。もし使いやすいファイルアップローダーを作るならば、モデルを利用することになるでしょう。

今回は次のように、アップロードファイルの一覧を確認できるようにしていきます。 単一モデルあり

必要なのは、モデルにmodels.FileFieldやmodels.ImageFieldを定義することです。

from django.db import models


class UploadFile(models.Model):
    """アップロードされたファイルを表すモデル"""
    file = models.ImageField('画像ファイル')  # これが重要

    def __str__(self):
        """ファイルのURLを返す"""
        return self.file.url

forms.py。モデルフォームを利用すると簡単です。これだけで、HTML上にファイルのアップロード欄が作成されます。

...
from .models import UploadFile


class SingleUploadModelForm(forms.ModelForm):

    class Meta:
        model = UploadFile
        fields = '__all__'

ビューです。アップロード処理のビューと、ファイルの一覧表示ビューを定義しました。

from django.urls import reverse_lazy
from django.views import generic
from .forms import SingleUploadForm, SingleUploadModelForm
from .models import UploadFile
...
...
class SingleUploadWithModelView(generic.CreateView):
    """ファイルモデルのアップロードビュー"""
    model = UploadFile
    form_class = SingleUploadModelForm
    template_name = 'app/upload.html'
    success_url = reverse_lazy('app:file_list')


class FileListView(generic.ListView):
    """アップロードされたファイルの一覧ページ"""
    model = UploadFile

アップロードページは、先ほど作ったupload.htmlを利用しています。

ファイルの一覧ページはuploadfile_list.htmlですが、次のようにファイルのURLだけ表示しています。

{% extends 'app/base.html' %}

{% block content %}
    <p><a href="{% url 'app:single_upload_with_model' %}">ファイルアップロードへ</a></p>
    <h1>ファイルの一覧</h1>
    {% for uploadfile in uploadfile_list %}
        <p><a href="{{ uploadfile.file.url }}">{{ uploadfile.file.url }}</a></p>
    {% endfor %}
{% endblock %}

複数ファイルのアップロード

ファイルを複数アップロードしたくなると、少し複雑になってきます。

フォームセット(モデルなし)

Djangoで、フォームセットを使うシリーズでは主にモデルフォームセット、インラインフォームセットを紹介しました。しかし、モデルに紐づかない通常のフォームセットというのも作ることができます。これを利用して、複数ファイルをアップロードし、アップロードしたファイルのURLをまとめて受け取る処理を作っていきます。

次のような動作です。
複数ファイルをアップロードし、アップロードしたファイルのURLをまとめて受け取る

まずですが、以下のようなフォームを定義しておきます。

class BaseUploadFormSet(forms.BaseFormSet):

    def save(self):
        # ['/media/1.png', '/media/2.png']のようなファイルのURLが入ったリストになる
        url_list = []

        # SingleUploadFormのsaveを順に呼び出し、ファイルURLを集める
        for form in self.forms:
            try:
                url = form.save()
            except KeyError:
                # ファイルがアップロードされていないフォームは、KeyErrorになるので、ここで無視する
                pass
            else:
                url_list.append(url)
        return url_list


UploadFormSet = forms.formset_factory(SingleUploadForm, formset=BaseUploadFormSet, extra=5)

上で説明した単一ファイルのアップロード モデルなしで、SingleUploadFormという、save()の呼び出しでアップロードファイルのURLを返す(モデルフォームじゃない)通常のフォームを作成しました。それを利用したフォームセットです。

通常、フォームセットオブジェクトはforms.formset_factory()で作るのですが、今回はURLのリストを一括で受け取れるメソッドを定義したかったので、BaseUploadFormSetというクラスを定義してsave()メソッドを作っています。送信されたファイルを保存し、それらのURLをリストで返します。

そのカスタマイズしたフォームセットクラスを、formset_factory()のformset引数に指定しています。モデルフォームセットやインラインフォームセットでも、今回のようにフォームセット自体の挙動をカスタマイズできます。

ビューです。download_url_listという、URLのリストをテンプレートへ渡します。

from .forms import SingleUploadForm, SingleUploadModelForm, UploadFormSet
...
...
class MultiUploadView(generic.FormView):
    form_class = UploadFormSet
    template_name = 'app/upload.html'

    def form_valid(self, form):
        download_url_list = form.save()
        context = {
            'download_url_list': download_url_list,
            'form': form,
        }
        return self.render_to_response(context)

upload.htmlを少し改良して、上のdownload_url_listを表示できるようにします。

{% extends 'app/base.html' %}

{% block content %}
    <form action="" method="POST" enctype="multipart/form-data">
        {{ form.as_p }}
        {% csrf_token %}
        <button type="submit">送信</button>
    </form>

    {% if download_url %}
        <a href="{{ download_url }}">{{ download_url }}</a>
    {% endif %}

    <!-- ここが増えた -->
    {% for download_url in download_url_list %}
        <p><a href="{{ download_url }}">{{ download_url }}</a></p>
    {% endfor %}
{% endblock %}

モデルフォームセット(モデルあり)

上でモデルを使ったファイルアップロードの例を紹介しましたが、それの一括アップロード版です。モデルフォームセットや場合によってはインラインフォームセットも良ければご覧ください。ファイルアップローダーなら欲しい機能ですね。

次のような感じです。
一括アップロード、モデルあり

forms.pyです。前に作ったモデルフォームを利用します。

UploadModelFormSet = forms.modelformset_factory(
    UploadFile, form=SingleUploadModelForm,
    extra=3
)

views.pyです。一括アップロードしたら、ファイルの一覧ページへ戻るようにします。クラスベースビューでは面倒な部分があるので、関数ビューにしました。

from django.shortcuts import render, redirect
from .forms import SingleUploadForm, SingleUploadModelForm, UploadFormSet, UploadModelFormSet
...
...
def multi_upload_with_model(request):
    formset = UploadModelFormSet(request.POST or None, files=request.FILES or None, queryset=UploadFile.objects.none())
    if request.method == 'POST' and formset.is_valid():
        formset.save()
        return redirect('app:file_list')

    context = {
        'form': formset
    }

    return render(request, 'app/upload.html', context)

queryset=UploadFile.objects.none()は、既にアップロード済みのファイルは表示しないという設定で、これをしないとアップロード済みファイルの更新フォームも表示されてしまいます。

input type="file" multiple(モデルなし)

スマホ環境では使えないこともあるのですが、こちらも紹介しておきます。UI的に、こちらの方が好きという方もいるかもしれません。

input type="file" multiple>な要素を使うと、Ctrlを押しながら複数のファイルを選択することができます。 複数ファイルを選択できる

まずフォームです。

class MultipleUploadForm(forms.Form):
    file = forms.ImageField(
        label='画像ファイル',
        widget=forms.ClearableFileInput(attrs={'multiple': True})
    )

    def save(self):
        url_list = []
        for upload_file in self.files.getlist('file'):
            file_name = default_storage.save(upload_file.name, upload_file)
            file_path = default_storage.url(file_name)
            url_list.append(file_path)
        return url_list

< input type="file">を、<input type="file" multiple>に変更する必要があります。そのため、widget引数を上書きしてそのように設定します。

保存処理ですが、cleaned_dataだと最後にアップロードされたファイルしか取得できないので、self.files.getlist()とします。ユーザーがアップロードしたファイルはrequest.FILESの中に管理され、フォームにはfiles=request.FILES のように渡されてきます。フォーム側からはself.filesとして、アップロードされたrequest.FILESにアクセスができるということです。

ビューは、複数ファイルアップロード(モデルなし)とほぼ同様です。

class InputMultiUploadView(generic.FormView):
    form_class = MultipleUploadForm
    template_name = 'app/upload.html'

    def form_valid(self, form):
        download_url_list = form.save()
        context = {
            'download_url_list': download_url_list,
            'form': form,
        }
        return self.render_to_response(context)

バリデーションですが、上の例ならばアップロードされたすべてのファイルに対してImageFieldとしてのバリデーションが行われます。安心です。

input type="file" multiple(モデルあり)

(途中。時間ができたら、専用のモデルフィールドとして作る)

ソースコード

Github