Django、ファイルアップロード付きテキストエリアを作る

2019-07-12 / PythonDjangoDjangoカスタムウィジェット

概要

文章の中に画像を挿入したいという例があります。ブログの本文は典型的な例ですし、コメント欄でもそのような機能が欲しい場合があります。

Djangoで、ブログアプリの本文に画像を挿入する方法まとめ で幾つかの方法を紹介しましたが、今回はテキストエリアにドラッグアンドドロップをすると、ファイルのアップロードと本文へのURL挿入を行うウィジェットを作成していきます。

管理画面でも、ドラッグアンドドロップでファイルが挿入されますし... 管理画面

通常のページでも同様です。 通常のページ

少し複雑なので、先にソースコードのダウンロードをしたほうがわかりやすいと思います。

ウィジェットを自作する

まずwidgets.pyというファイルを作ります。

from django import forms
from django.urls import reverse_lazy


class FileUploadableTextArea(forms.Textarea):
    """画像アップロード可能なテキストエリア"""

    class Media:
        js = ['blog/csrf.js', 'blog/upload.js']

    def __init__(self, attrs=None):
        super().__init__(attrs)
        if 'class' in self.attrs:
            self.attrs['class'] += ' uploadable vLargeTextField'
        else:
            self.attrs['class'] = 'uploadable vLargeTextField'
        self.attrs['data-url'] = reverse_lazy('blog:upload')

forms.Textareaウィジェットを継承し、新しいウィジェットクラスを作りました。

__init__()は、cssのクラスを追加するために上書きしたメソッドです。uploadableというクラス名を持つテキストエリアにJavaScriptでイベントを設定する為に、このウィジェットはcssのクラス名としてuploadableを持つことが保証されるようになります。vLargeTextFieldというクラス名は、Django管理サイト用の指定です。

self.attrs['data-url'] = reverse_lazy('blog:upload')は、ファイルアップロード用のビューURLを持たせています。ビューは後で作成します。

class Mediaは、このウィジェットを利用する際に読み込まれるcssやjsファイルを記述する場所です。テンプレートで{{ form.media }}とすると、ここに書いたファイルが自動的に読み込まれます。

csrf.jsは次のようにします。Djangoで、Ajaxでも紹介しましたが、POSTメソッドでAjaxを利用する際に必要な記述です。

const getCookie = name => {
    if (document.cookie && document.cookie !== '') {
        for (const cookie of document.cookie.split(';')) {
            const [key, value] = cookie.trim().split('=');
            if (key === name) {
                return decodeURIComponent(value);
            }
        }
    }
};
const csrftoken = getCookie('csrftoken');

upload.jsです。

const insertText = (textarea, text) => {
    const cursorPos      = textarea.selectionStart;
    const last      = textarea.value.length;
    const before   = textarea.value.substr(0, cursorPos);
    const after    = textarea.value.substr(cursorPos, last);
    textarea.value = before + text + after;
};

const upload = (uploadFile, textarea) => {
    const formData = new FormData();
    formData.append('file', uploadFile);
    fetch(textarea.dataset.url, {
        method: 'POST',
        body: formData,
        headers: {
            'X-CSRFToken': csrftoken,
        },
    }).then(response => {
        return response.json();
    }).then(response => {
        const extension = response.url.split('.').pop().toLowerCase();

        // 画像ならimg要素に、そうでなければa要素に
        if (['png', 'jpg', 'gif', 'jpeg', 'bmp'].includes(extension)) {
            html = `<a href="${response.url}"><img src="${response.url}"></a>`;
        } else {
            html= `<a href="${response.url}">${response.url}</a>`;
        }

        insertText(textarea, html);

    }).catch(error => {
        console.log(error);
    });
};


document.addEventListener('DOMContentLoaded', e => {
    for (const textarea of document.querySelectorAll('textarea.uploadable')) {
        textarea.addEventListener('dragover', e => {
            e.preventDefault();
        });

        textarea.addEventListener('drop', e => {
            e.preventDefault();
            upload(e.dataTransfer.files[0], textarea);
        });
    }
});

処理の流れは次のような感じです。

  1. テキストエリアにファイルがドラッグアンドドロップされる

  2. AjaxでファイルをDjangoのビューに送信する

  3. ビューはファイルを保存し、ファイルのURLを返す

  4. 受け取ったURLをテキストエリアの最後に挿入する。画像ならimg要素、それなければ単純なa要素

forms.pyで、ファイルを受け取るフォームを作成しておきます。

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


class FileUploadForm(forms.Form):
    """ファイルのアップロードフォーム"""
    file = forms.FileField()

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

FileUploadFormは、Ajaxで送信されたファイルを処理するためのフォームです。定義したsave()は、ファイルを保存してファイルURLを返しています。Djangoで、ファイルアップロードでも紹介したフォームです。

ちなみにですが、今回の例はモデルを利用しないで画像を保存しています。

次に、Ajaxでファイルを受け取るビューを作成します。まずはurl.pyに定義しておきましょう。

path('upload/', views.upload, name='upload'),

そして、次のようなビューを作っておきます。これがAjaxでのアップロード処理を受け取るビューです。

from django.http import HttpResponseBadRequest, JsonResponse
from .forms import FileUploadForm


def upload(request):
    """ファイルのアップロード用ビュー"""
    form = FileUploadForm(files=request.FILES)
    if form.is_valid():
        url = form.save()
        return JsonResponse({'url': url})
    return HttpResponseBadRequest()

URLを完全なURLにしたい場合は、次のようになるでしょう。Djangoで、SNSシェアリンク・ボタンを作る でも紹介しました。

def upload(request):
    """ファイルのアップロード用ビュー"""
    form = FileUploadForm(files=request.FILES)
    if form.is_valid():
        path = form.save()
        url = '{0}://{1}{2}'.format(
            request.scheme,
            request.get_host(),
            path,
        )
        return JsonResponse({'url': url})
    return HttpResponseBadRequest()

自作ウィジェットを使ってみる

準備が終わったので、上で作ったカスタムウィジェットの利用方法を説明します。説明と言っても大したことはなく、アップロード可能にしたいテキストエリアのウィジェットをFileUploadableTextAreaに変更するだけです。

モデルフォームならば、'text': FileUploadableTextAreaのようにします。

from django import forms
from django.core.files.storage import default_storage
from .models import Post
from .widgets import FileUploadableTextArea


class PostForm(forms.ModelForm):
    """記事の追加フォーム"""

    class Meta:
        model = Post
        fields = '__all__'
        widgets = {
            'text': FileUploadableTextArea,  # このように!
        }


class FileUploadForm(forms.Form):
    """ファイルのアップロードフォーム"""
    file = forms.FileField()

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


通常のフォームで使う場合は、次のようになるでしょう。

class PostForm(forms.Form):
    text = forms.CharField(label='本文', max_length=255, widget= FileUploadableTextArea)

テンプレート側の記述ですが、自分のページで使う際は、{{ form.media }}という記述が必要なこと以外は通常のフォームとして利用できます。

<form action="" method="POST">
    {{ form.as_p }}
    {{ form.media }}<!-- これが必要 -->
    {% csrf_token %}
    <button type="submit">送信</button>
</form>

管理画面で使う際は、記事本文のウィジェットを今回作ったFileUploadableTextAreaに変更してもらう必要があります。色々方法はありますが、シンプルに済ませるならば、次のようにform属性を上書きするだけで大丈夫です。

from django.contrib import admin
from .forms import PostForm
from .models import Post


class PostAdmin(admin.ModelAdmin):
    # 管理画面でこのPostFormを使ってもらう
    # PostFormは、記事本文のウィジェットをUploadableTextAreaにしている
    form = PostForm


admin.site.register(Post, PostAdmin)

今回のアップロード処理は、どんなファイルであろうと受け付けます。自分で使う用ならば問題はないのですが、第三者が使う場合...例えばコメント欄なんかで使いたい場合は、セキュリティ的な面をもう少し考えることになります。

この記事の関連記事

Djangoで、サジェスト機能付きフォームを作る

2018-12-30 / PythonDjangoBulmaJavaScriptAjaxDjangoカスタムウィジェット

- DjangoとJavaScriptを使い、入力内容に応じてデータをサジェストするフォームを作成していきます。

Djangoで、ブログアプリの本文に画像を挿入する方法まとめ

2019-05-14 / PythonDjango

- ブログアプリケーションを作成した際の、記事本文へ画像を差し込む方法についてよく質問が来ますので、それについてを説明していきます。これは記事をどうやって書いているかによって、方法が変わります。

Djangoで、プレビュー付き画像アップロード欄を作る

2019-05-17 / PythonDjangoDjangoカスタムウィジェット

- DjangoでImageFieldを使うと画像のアップロード欄が作られますが、プレビューは表示されません。ウィジェットを自作し、プレビュー付きの画像アップロード欄を作ります。

Djangoで、表示・非表示切り替え可能なチェックボックスを作る

2019-07-02 / PythonDjangoDjangoカスタムウィジェット

- Djangoで、表示・非表示切り替え可能なチェックボックスとなるカスタムウィジェットを作成していきます。

コメント欄

記事にコメントする

まだコメントはありません。