Django、メッセージフレームワークの使い方

/ PythonDjangoBulmaJavaScript

32日前に更新

概要

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.parentElement.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.parentElement.classList.add('is-hidden');
        });
    }

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

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-header > .delete')) {
        element.addEventListener('click', e => {
            e.target.parentElement.parentElement.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にそれを指定することで実現できます。

この記事の関連記事

コメント欄

記事にコメントする

ポポ

ナリトさん 投稿内容とドンピシャのところがなかったのですが、 ここの記事が一番近いかと思って質問しました。 form_valid についての質問です。

私は現在とてもシンプルなブログを作ってて チュートリアルにしたがってやって、ほとんど重要な機能は できつつあります。どうしても苦労してる部分がありまして、もしNaritoさんが 何かご存知でしたら、ぜひアドバイスいただきたいと思い質問させていただきました。

管理者がブログを投稿し、その投稿に対して、ログインしたユーザーがコメントを残せるという 機能をつけようと試行錯誤しております。 CBV の CreateView を拡張して、現在のシンプルなモデルに合わせて CommentCreateViewを作成し、 url, template も作成しました。

CommentCreateViewで、fields = ('comment', 'author', 'article', ) とすると、フォームから問題なく特定の記事にコメントを投稿できます。 form_valid というメソッドを使って、'author' field は自動的に選ばれるようには設定できました。 最後に、'article' field も自動で選ばれるようにしたいのですが、これに苦労しております。

アドバイスいただけると大変助かります。

I followed along tutorials and mostly done. But last thing I want to do is that when users make comments to any articles, I want author field and article field to be automatically set. Currently, users need to choose author field and article field to make comments as well as comment texts.

The tutorial that I followed along uses form_valid and by using form_valid, now I don't need to choose author. But I have been struggling with how to automatically set article field by using form_valid.

I have a simple models.py and views.py below.

<models.py>

class Article(models.Model):
    title = models.CharField(max_length=255)
    body = models.TextField()
    date = models.DateTimeField(auto_now_add=True)
    author = models.ForeignKey(
        get_user_model(),
        on_delete=models.CASCADE,
        )

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('article_detail', args=[str(self.id)])


class Comment(models.Model): # new
    article = models.ForeignKey(
        Article,
        on_delete=models.CASCADE,
        related_name = 'comments',
        )
    comment = models.CharField(max_length=140)
    author = models.ForeignKey(
        get_user_model(),
        on_delete=models.CASCADE,
    )

    def __str__(self):
        return self.comment

    def get_absolute_url(self):
        return reverse('article_list')

<views.py>

(not showing all)
#for comment
class ArticleCommentCreateView(LoginRequiredMixin, CreateView):
    model = Comment
    template_name = 'article_comment_new.html'
    fields = ('comment',)
    login_url = 'login'

    def form_valid(self, form):
        form.instance.author = self.request.user
        form.instance.article = self.request.article
        return super().form_valid(form)

エラー

AttributeError at /articles/2/comment/
'WSGIRequest' object has no attribute 'article'
Request Method: POST
Request URL:    http://127.0.0.1:8000/articles/2/comment/
Django Version: 2.1.5
Exception Type: AttributeError
Exception Value:    
'WSGIRequest' object has no attribute 'article'
Exception Location: /Users/Koitaro/Desktop/Web_Development/MMBlog/articles/views.py in form_valid, line 75
Python Executable:  /Users/Koitaro/.local/share/virtualenvs/MMBlog-58h299OP/bin/python
Python Version: 3.6.5
Python Path:    
['/Users/Koitaro/Desktop/Web_Development/MMBlog',
 '/Users/Koitaro/.local/share/virtualenvs/MMBlog-58h299OP/lib/python36.zip',
 '/Users/Koitaro/.local/share/virtualenvs/MMBlog-58h299OP/lib/python3.6',
 '/Users/Koitaro/.local/share/virtualenvs/MMBlog-58h299OP/lib/python3.6/lib-dynload',
 '/Applications/anaconda3/lib/python3.6',
 '/Users/Koitaro/.local/share/virtualenvs/MMBlog-58h299OP/lib/python3.6/site-packages']
Server time:    Sat, 27 Apr 2019 21:24:45 +0000

コメントに返信する

なりと

articleCommentに紐づかせる必要があります。具体的に言うと、ArticleCommentCreateViewarticlepkを渡す必要があります。

方法は幾つかあります。例えば、URLにArticleのpkを含める方法です。

<form action="{% url 'comment_create' article.pk %}" ...

urls.pyでもpkを受け取るようにしておきます(エラーログを見る限り、あなたはここまでの作業を既にしているかもしれません)。

path('articles/<int:pk>/comment/')

ビューでは、次のようにしてCommentにArticleを指定します。

from django.shortcuts import get_object_or_404
...
...
    def form_valid(self, form):
        form.instance.author = self.request.user
        form.instance.article = get_object_or_404(Article, pk=self.kwargs['pk'])
        return super().form_valid(form)
ポポ

ありがとうございます! うまく動きました。

他の方法として、Comment モデルの article と author で blank=True とすることで、ArticleCommentCreateView のフィールドに author と article をセットしなくて良くなりました。 そして、ArticleCommentCreateView で 新たな function を以下のように作れました。

def post(self, request, args, *kwargs): pk = kwargs['pk'] form = CommentForm(request.POST, pk) if form.is_valid(): new_comment = form.save(commit=False) new_comment.author = request.user new_comment.article = Article.objects.get(id=pk) new_comment.save() return HttpResponseRedirect('/')

さらに CommentForm をforms.py に作り、fields = ['comment'] を設定すると、コメントフォームでコメントのみの入力になって、かつ author と article が自動的に選択されてデータベースに保存されました。

しかし、Narito さんのアプローチがとてもいいので、ぜひそちらで行こうと思います! お忙しいところアドバイスいただきありがとうございます!