/ PythonDjangoDjangoカスタムウィジェット

Djangoで、ブログの記事本文に画像を挿入する

概要

ブログはWebアプリの基本を学ぶには良い題材です。中には自分で作ったブログを実際に使いつつ、継続的に開発している方も多いようです。このブログもその1つです。

その中でよく来る質問の1つが、記事本文へ画像を挿入する方法です。これは記事をどうやって書いているかにもよります。

例えばMarkdownxで本文を書いているならば、画像を本文に埋め込むには次のように書きます。![説明テキスト](URL)という部分が画像になります。

## 今日の朝食
今日は納豆ご飯にオリーブオイルをかけて食べました。

朝食の画像↓  
![画像の説明テキスト](画像URL)

## 今日の昼食
...
...

HTMLで直接書いている方は、次のようにimg要素を使います。

<h2>今日の朝食</h2>
<p>今日は納豆ご飯にオリーブオイルをかけて食べました。</p>

<p>朝食の画像↓</p>
<img src="画像URL" alt="説明テキスト">

django-summernote等のDjangoライブラリを使うと、テキストエリアが便利になります。次のような感じで書くことができます。 django-summernoteのデモ

画像の埋め込みも簡単で、ボタンを押してその場で埋め込むことができます。ちなみに、画像ファイルをテキストエリアへドラッグアンドドロップでも可能です。django-summernote以外のDjangoライブラリも似たような機能を持っていることが多く、私が使っているdjango-markdownx も同じことができます。

こういったDjangoライブラリを使うと画像の埋め込みが簡単ですが、今回はそういったものを使っていない方のために幾つかのやり方を紹介していきます。

ほとんどのケースにおいて、画像を本文に埋め込むためには画像URLが必要です。画像URLをどうやって取得するかというのが重要になってきます。

方法1. アップロード用モデルを別に利用する

次のようなモデルを定義しておきます。

class Image(models.Model):
    file = models.ImageField('画像ファイル')

    def __str__(self):
        """画像のURLを返す"""
        return self.file.url

そして、次のような流れで本文に画像URLを貼り付けます。
面倒な方法

画像をアップロードして、画像URLをコピーし、それを本文に張り付けるという流れです。画面を行ったり来たりで面倒です。

方法2. アップロード用モデルをインラインで表示する

上の方法を、同じ画面内で行うというのが趣旨になります。

まず、モデルを少し変更します。画像に、紐づく記事のフィールドを追加します。

class Image(models.Model):
    file = models.ImageField('画像ファイル')
    target = models.ForeignKey(Post, verbose_name='紐づく記事', on_delete=models.CASCADE)

    def __str__(self):
        """画像となるimg要素を返す"""
        return f'<img src="{self.file.url}">'  # HTMLを本文に直接書く例

今回は管理画面から記事を作っているので、admin管理画面でインラインフォームを使います。admin.pyを編集します。

from django.contrib import admin
from .models import Post, Image


class ImageInline(admin.StackedInline):
    model = Image
    extra = 3


class PostAdmin(admin.ModelAdmin):
    inlines = [ImageInline]


admin.site.register(Post, PostAdmin)
admin.site.register(Image)

すると、記事の作成画面に画像のアップロードもできるようになります。
さっきよりはマシな方法

自作のページで行う場合は、Django、インラインフォームセットを使うを参考にしてください。

さっきよりはマシですが、画像のURLを取得するためには一度保存する必要があり、やや面倒です。

方法3. ドラッグアンドドロップでアップロードさせる

最初に紹介したdjango-summernoteのように、ボタンクリックやドラッグアンドドロップで画像を差し込めるのが理想です。なので、この機能を作ってみましょう。少し複雑なので、先にソースコードのダウンロードをしたほうがわかりやすいと思います。

テキストエリアに画像をドラッグアンドドロップすると、画像URLが本文に挿入される機能を簡単に作っていきます。 管理画面での動作

自作ページでの動作

今回はアップロード可能なテキストエリアとして、ウィジェットクラスを自作します。

準備1. カスタムウィジェットの作成

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

from django import forms


class ImageUploadableTextArea(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'

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

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

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です。

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

        textarea.addEventListener('drop', e => {
            e.preventDefault();
            const formData = new FormData();
            formData.append('file', e.dataTransfer.files[0]);
            fetch('/upload/', {
                method: 'POST',
                body: formData,
                headers: {
                    'X-CSRFToken': csrftoken,
                },
            }).then(response => {
                return response.json();
            }).then(response => {
                textarea.value += response.url;
            }).catch(error => {
                console.log(error);
            });

        });
    }
});


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

  1. テキストエリアに画像がドラッグアンドドロップされる
  2. Ajaxで画像をDjangoのビューに送信する
  3. ビューは画像を保存し、画像のURLを返す
  4. 受け取ったURLをテキストエリアの最後に挿入する

準備2. アップロード画像を受け取るフォームの作成

forms.pyに次のように書いておきます。

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


class ImageUploadForm(forms.Form):
    """画像のアップロードフォーム"""
    file = forms.ImageField()

    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 f'<img src="{file_url}">'

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

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

準備3. アップロード画像を処理するビューの作成

Ajaxは、/upload/というURLに対して送信します。なので、url.pyに定義しておきます。

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

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

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


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

使い方

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

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

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


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

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


class ImageUploadForm(forms.Form):
    """画像のアップロードフォーム"""
    file = forms.ImageField()

    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 f'<img src="{file_url}">'

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

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

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

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

管理画面で使う際は、記事本文のウィジェットを今回作ったUploadableTextAreaに変更してもらう必要があります。色々方法はありますが、シンプルに済ませるならば、次のように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)

ソースコードのダウンロード

Github