NARITO BLOG

Django、メッセージフレームワークの基本

Python, Django, Bulma, HTML・CSS・JavaScript,

概要

Djangoのメッセージフレームワークの簡単なサンプルです。BulmaのNotificationやMessageを使った表示もしていきます。

データの作成や更新、削除をすると、次のように上側にフラッシュメッセージが表示されます。

メッセージが表示される

これは各ユーザーに向けた一過性の通知メッセージで、1度表示されたら消去されます。

Github

少し長めなので、Githubにソースコードを置きました。

models.py

モデルは次のようなものを使いましたが、何でもよいです。

from django.db import models


class Post(models.Model):
    title = models.CharField('タイトル', max_length=255)

    def __str__(self):
        return self.title

urls.py

適当なURLとビューを紐づけます。

from django.urls import path
from . import views

app_name = 'app'

urlpatterns = [
    path('', views.PostList.as_view(), name='post_list'),
    path('create/', views.PostCreate.as_view(), name='post_create'),
    path('update/<int:pk>/', views.PostUpdate.as_view(), name='post_update'),
    path('delete/<int:pk>/', views.PostDelete.as_view(), name='post_delete')
]

views.py

generic.ListViewgeneric.UpdateViewgeneric.DeleteViewを使い、form_valid()delete()メソッドを上書きしています。

from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views import generic
from .models import Post


class PostList(generic.ListView):
    model = Post


class PostCreate(generic.CreateView):
    model = Post
    fields = '__all__'
    success_url = reverse_lazy('app:post_list')

    def form_valid(self, form):
        self.object = post = form.save()
        messages.info(self.request, f'記事を作成しました。 タイトル:{post.title} pk:{post.pk}')
        return redirect(self.get_success_url())


class PostUpdate(generic.UpdateView):
    model = Post
    fields = '__all__'
    success_url = reverse_lazy('app:post_list')

    def form_valid(self, form):
        self.object = post = form.save()
        messages.info(self.request, f'記事を更新しました。 タイトル:{post.title} pk:{post.pk}')
        return redirect(self.get_success_url())


class PostDelete(generic.DeleteView):
    model = Post
    success_url = reverse_lazy('app:post_list')

    def delete(self, request, *args, **kwargs):
        self.object = post = self.get_object()
        message = f'記事を削除しました。 タイトル:{post.title} pk:{post.pk}'
        post.delete()
        messages.info(self.request, message)
        return redirect(self.get_success_url())

メッセージフレームワークの使い方は幾つかあるのですが、基本的にはfrom django.contrib import messagesとしてimportし、messages.info(requestオブジェクト, メッセージ)のように使います。

これにより、そのユーザーへの通知メッセージが保存され、テンプレートでそれを取り出すのが一般的な流れです。

self.object = post =という記述が幾つかあります。データの保存や削除をしたら当然return redirect(self.get_success_url())としてリダイレクト先に遷移させたいのですが、get_success_url()を呼び出すためにはself.objectとしてモデルインスタンスを格納しておく必要があるのです。しかし、f'記事を更新しました。 タイトル:{self.object.title} pk:{self.object.pk}'のようにいちいちself.objectをつけるのは面倒なので、postという変数名でも同様に扱えるようにしています。

base.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hello Bulma!</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css">
    <script defer src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
</head>
<body>
<!-- ナビバー部分 -->
<nav class="navbar is-black" role="navigation" aria-label="main navigation">
    <div class="container">
        <div class="navbar-brand">
            <a role="button" class="navbar-burger burger" aria-label="menu" aria-expanded="false"
               data-target="navbar-menu">
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
                <span aria-hidden="true"></span>
            </a>
        </div>
        <div id="navbar-menu" class="navbar-menu">
            <div class="navbar-start">
                <a class="navbar-item" href="{% url 'app:post_list' %}">Home</a>
                <a class="navbar-item" href="{% url 'app:post_create' %}">New</a>
            </div>
        </div>
</nav>

<!-- メッセージフレームワーク -->
{% if messages %}
<div class="container" style="margin-top:1rem;">
    <div class="notification is-info">
        <button class="delete" type="button"></button>
        {% for message in messages %}
        <p> {{ message }}</p>
        {% endfor %}
    </div>
</div>
{% endif %}}


<!-- メインコンテンツ -->
<main>
    {% block content %}{% endblock %}
</main>

<script>
    // notificationを×押下で閉じれるように。
    for (const element of document.querySelectorAll('.notification > .delete')) {
        element.addEventListener('click', e => {
            e.target.parentNode.classList.add('is-hidden');
        });
    }

    // ナビバーの開閉を設定
    for (const element of document.querySelectorAll('.navbar-burger')) {
        const menuId = element.dataset.target;
        const menu = document.getElementById(menuId);
        element.addEventListener('click', e => {
            element.classList.toggle('is-active');
            menu.classList.toggle('is-active');
        });
    }

</script>
</body>
</html>

Bulmaのスターターテンプレートを基に、ナビバーと、フラッシュメッセージをNotificationというBulmaのウィジェットで表示しています。また、スマホ等のサイズでナビバーを開閉、Notificationを×閉じするためのJavaScriptも書いています。

フラッシュメッセージを取り出しているのは次の部分です。

<!-- メッセージフレームワーク -->
{% if messages %}
<div class="container" style="margin-top:1rem;">
    <div class="notification is-info">
        <button class="delete" type="button"></button>
        {% for message in messages %}
        <p> {{ message }}</p>
        {% endfor %}
    </div>
</div>
{% endif %}}

テンプレートへは、messagesという名前でメッセージオブジェクトの詰まったリストが返されます。場合によっては、メッセージは1つだけではなく複数になることもあります。

メッセージがあれば({% if messages %})、BulmaのNotificationを作って、{% for message in messages %}としてメッセージを格納していきます。

このNotificationを閉じれるように、<button class="delete" type="button"></button>として×閉じ部分を作っています。実際に閉じる処理は、次の部分です。

    // notificationを×押下で閉じれるように。
    for (const element of document.querySelectorAll('.notification > .delete')) {
        element.addEventListener('click', e => {
            e.target.parentNode.classList.add('is-hidden');
        });
    }

やっているのは、<div class="notification is-info">is-hiddenを足して見えなくしているだけです。

Notification以外にも、BulmaではJavaScriptを書く機会が多いです。Bulmaで良く使うJavaScriptコードも御覧ください。

post_list.html

<div class="box">で各記事をカッコよく線で囲んで、10列をタイトル部分、残り2列を更新・削除ボタン部分にしています。

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

{% block content %}
<section class="section">
    <div class="container">
        <h1 class="title">記事一覧</h1>
        {% for post in post_list %}
        <div class="box">
            <div class="columns">
                <div class="column is-10">
                    <h2>{{ post.title }}</h2>
                </div>
                <div class="column is-2">
                    <a href="{% url 'app:post_update' post.pk %}" class="button is-info">更新</a>
                    <a href="{% url 'app:post_delete' post.pk %}" class="button is-danger">削除</a>
                </div>
            </div>
        </div>
        {% endfor %}
    </div>
</section>
{% endblock %}

BulmaのMessageウィジェットで表示する

BulmaにはMessageというウィジェットもあり、こちらも中々に見た目が良いです。

<!-- メッセージフレームワーク -->
{% if messages %}
<div class="container" style="margin-top:1rem;">
    <div class="message is-info">
        <div class="message-header">
            <p>お知らせ</p>
            <button class="delete" type="button"></button>
        </div>
        <div class="message-body">
            {% for message in messages %}
            <p> {{ message }}</p>
            {% endfor %}
        </div>
    </div>
</div>
{% endif %}
    // Messageを×押下で閉じれるように。
    for (const element of document.querySelectorAll('.message .delete')) {
        element.addEventListener('click', e => {
            e.target.parentNode.parentNode.classList.add('is-hidden');
        });
    }

Messageで表示

メッセージ毎にNotificationを作る

メッセージ1つにつきNotificationを作りたいこともあるかもしれません。

お知らせが沢山

これは簡単で、テンプレートの記述を次のようにします。for毎にNotificationを作る感じですね。

<!-- メッセージフレームワーク -->
{% for message in messages %}
<div class="container" style="margin-top:1rem;">
    <div class="notification is-info">
        <button class="delete" type="button"></button>
        <p> {{ message }}</p>
    </div>
</div>
 {% endfor %}

ビューでは、例えば次のように。messages.info()を複数回呼ぶだけですね。

    def form_valid(self, form):
        self.object = post = form.save()
        messages.info(self.request, f'記事を作成しました。 タイトル:{post.title} pk:{post.pk}')
        messages.info(self.request, 'お知らせ2')
        messages.info(self.request, 'お知らせ3')
        return redirect(self.get_success_url())

もしかしたら、警告やエラーなどの他のメッセージを、色も別にして表示したいかもしれません。

様々な色で表示する

テンプレートを、次のようにします。

<!-- メッセージフレームワーク -->
{% for message in messages %}
<div class="container" style="margin-top:1rem;">
    <div class="notification is-{{ message.tags }}">
        <button class="delete" type="button"></button>
        <p> {{ message }}</p>
    </div>
</div>
 {% endfor %}

変わったのは<div class="notification is-{{ message.tags }}">の、is-{{ message.tags }}の部分です。

ビューでmessages.info()としていれば、そのメッセージにはinfoというタグがつけられて、{{ message.tags }}とするとinfoという文字列が取得できます。ビューで呼び出した関数の種類によってタグ的なものがつけられるということですね。

Djangoのメッセージフレームワークが標準で用意しているのはinfoのほかに、debug, success, warning, errorがあり、Bulmaではis-primary, is-link, is-info, is-success, is-warning, is-danger等の色が使えます。

実際のビューのコードも見てみましょう。

    def form_valid(self, form):
        self.object = post = form.save()
        messages.info(self.request, f'記事を作成しました。 タイトル:{post.title} pk:{post.pk}')
        messages.warning(self.request, '何らかの警告')
        messages.success(self.request, '何らかの成功')
        messages.error(self.request, '何らかのエラー', extra_tags='danger')
        return redirect(self.get_success_url())

info, warning, successに関してはDjangoにもBulmaにもあるのでそのまま使えます。Bulmaのis-dangerが使いたい場合もあると思いますが、そのような場合はmessages.error(self.request, '何らかのエラー', extra_tags='danger')のようにして、extra_tagsにそれを指定することで実現できます。