Djangoで、messages frameworkを使ったフラッシュメッセージを試してみた

Webアプリでフラッシュメッセージを表示する場合、ASP.NET MVCではTempData、Railsではflashあたりを使うと思います。

Djangoでは何を使うのかを調べてみたところ、標準で用意されているmessages frameworkを使うのが良さそうでした。
The messages framework | Django documentation | Django

そこで、公式ドキュメントに従い、ModelForm + CreateViewを使って試してみたときのメモを残します。

 

環境

  • Windows10
  • Python 3.5.1
  • Django 1.9.2
    • プロジェクト名myproject、アプリ名myappとしてDjangoアプリを作成

 

準備

myapp/models.py
from django.db import models

class Item(models.Model):
    name = models.CharField('Name', max_length=255)

 

myapp/forms.py
from django import forms
from .models import Item

class ItemForm(forms.ModelForm):
    class Meta:
        model = Item
        fields = '__all__'

 

バリデーションエラー時にフラッシュメッセージを表示

CreateViewでバリデーションエラーとなった場合 form_invalid()が呼ばれるため、そこに実装してみます。

Modelのnameフィールドがデフォルトだと入力必須になることを利用して、未入力時にフラッシュメッセージを出すようにします。
Models - field-options | Django documentation | Django

 

実装
myapp/views.py

messages.errorの第一引数に渡すrequestは、self.requestで取得できるのを使います。
django - Get request.session from a class-based generic view - Stack Overflow

...
from django.contrib import messages
...
class ItemErrorFlashView(CreateView):
    ...
    def form_invalid(self, form):
        if form.has_error('name'):
            # Modelのnameフィールドにエラーがある場合、フラッシュメッセージを表示
            messages.error(self.request, '!!ERROR!!')

        return super(ItemErrorFlashView, self).form_invalid(form)

 

myapp/templates/myapp/item_form.html

フラッシュメッセージを表示する部分を用意します。

<h1>Item Form</h1>

{% if messages %}
    <ul class="messages">
        {% for message in messages %}
            <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
        {% endfor %}
    </ul>
{% endif %}

...

 

動作確認

ブラウザで確認すると、フラッシュメッセージが表示されました。

...
<h1>Item Form</h1>
<ul class="messages">
    <li class="error">!!ERROR!!</li>
</ul>
...

message tagsはデフォルトのままなので、HTMLのclassがerrorとなっています。

 

Bootstrapと組み合わせる

フラッシュメッセージの見栄えを良くするため、BootstrapのAlertsと組み合わせてみます。
Components - Alerts · Bootstrap

Djangoのmessage tagがそのままHTMLのClassになりますが、上記で見た通りデフォルトだとerrorでありBootstrapのdangerではありません。

対応としては、

などが思いつきましたが、今回は後者で試します。

 

実装
myproject/settings.py

Level Constantに紐づくTagの値を変更するため、以下の公式ドキュメントを参考にMESSAGE_TAGSの設定を追加します。
Settings - MESSAGE_TAGS | Django documentation | Django

from django.contrib.messages import constants as message_constants
MESSAGE_TAGS = {
    message_constants.ERROR: 'alert alert-danger',
}

 

myapp/templates/myapp/item_form.html

HTMLにBootstrapを追加します。今回はCDNで提供されているものを使います。
Getting started - Bootstrap CDN · Bootstrap

<head>
    ...
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
</head>

 

動作確認

Bootstrapでフラッシュメッセージが表示されました。

f:id:thinkAmi:20160216184425p:plain

<ul class="messages">
  <li class="alert alert-danger">!!ERROR!!</li>
</ul>

 

フラッシュメッセージの削除

何らかの理由で、一度追加してしまったフラッシュメッセージを削除する方法も探してみました。

公式ドキュメントによると、

The messages are marked to be cleared when the storage instance is iterated (and cleared when the response is processed).

The messages framework - Expiration of messages | Django documentation | Django

とあり、一度イテレータを回せば良さそうでした。

 
イテレータを回す方法としては、

  • for文
  • 内包表記
  • list()

などがありますが、今回はlist()を使うことにしました。
Django: Remove message before they are displayed - Stack Overflow

 
なお、公式ドキュメントでcleared when the response is processedと書かれている通り、

  • post()でフラッシュメッセージを追加
  • form_invalid()でフラッシュメッセージを削除

をCreateViewのみで実装・確認した場合にはフラッシュメッセージが削除されませんでした。

そのため、

  • CreateViewにて、form_valid()でフラッシュメッセージを追加し、直後に削除
  • CreateViewからDetailViewへリダイレクト
  • リダイレクト先のDetailViewにて、フラッシュメッセージが削除されていることを確認

という形にしました。

 
また、いくつもCreateViewを用意するのは手間だったので、

  • POST /mysite/remove-flash/ (クエリ文字列なし)
    • フラッシュメッセージなし
  • POST /mysite/remove-flash/?a=1 (クエリ文字列あり)
    • フラッシュメッセージあり

という動作にしました。

クエリ文字列の有無については、以下を参考にrequest.META['QUERY_STRING']を使うこととしました。
django - Best way to get query string from a URL in python? - Stack Overflow

 

実装
myapp/views.py
class ItemFlashRemoveView(CreateView):
    model = Item
    form_class = ItemForm
    
    def get_success_url(self):
        return reverse('my:item-detail', args=(self.object.id,))
        
    def form_valid(self, form):
        messages.success(self.request, 'Success (Remove ver.)')
        
        if self.request.META['QUERY_STRING'] == '':
            list(messages.get_messages(self.request))
    
        return super(ItemFlashRemoveView, self).form_valid(form)
myapp/urls.py
urlpatterns = [
    url(r'^remove-flash/$', views.ItemFlashRemoveView.as_view(), name='item-remove-flash'),
    url(r'^(?P<pk>[0-9]+)/$', DetailView.as_view(model=models.Item), name='item-detail'),
]
myapp/templates/myapp/item_detail.html
<h1>Item Detail</h1>

{% if messages %}
    <ul class="messages">
        {% for message in messages %}
            <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
        {% endfor %}
    </ul>
{% endif %}

<div id="main">
    <p>Name: {{ object.name }}</p>
</div>
myproject/settings.py
MESSAGE_TAGS = {
    message_constants.SUCCESS: 'alert alert-success',
    message_constants.ERROR: 'alert alert-danger',
}

 

動作確認
クエリ文字列なし

f:id:thinkAmi:20160216185652p:plain

 

クエリ文字列あり

f:id:thinkAmi:20160216185658p:plain

 

両者の実行時ログ

前者はPOST /mysite/remove-flash/、後者はPOST /mysite/remove-flash/?a=1の結果であることが確認できました。

[16/Feb/2016 18:54:21] "GET /mysite/remove-flash/ HTTP/1.1" 200 883
[16/Feb/2016 18:54:55] "POST /mysite/remove-flash/ HTTP/1.1" 302 0
[16/Feb/2016 18:54:56] "GET /mysite/23/ HTTP/1.1" 200 490
[16/Feb/2016 18:55:16] "GET /mysite/remove-flash/?a=1 HTTP/1.1" 200 883
[16/Feb/2016 18:55:20] "POST /mysite/remove-flash/?a=1 HTTP/1.1" 302 0
[16/Feb/2016 18:55:20] "GET /mysite/24/ HTTP/1.1" 200 646

 

フラッシュメッセージがクリアされるのを防ぐ

例えば、

  • ログにフラッシュメッセージの内容を表示
  • 画面にもフラッシュメッセージを表示

とする場合があるとします。

その時は

messages.success(self.request, 'Success (Reuse ver.)')
msgs = messages.get_messages(self.request)
for m in msgs:
    print(m)
msgs.used = False

のように、usedFalseを設定すれば、一度イテレータを回してもフラッシュメッセージがクリアされません。

 

SuccessMessageMixinを使う

CreateViewを使う場合、SuccessMessageMixinをミックスインすることで、処理成功時のフラッシュメッセージを簡単に設定できます。
The messages framework | Django documentation | Django

なお、UpdateViewにもSuccessMessageMixinをミックスインできますが、今のところ、DeleteViewにはミックスインできないようです。

また、extra_tagsを設定する方法も見当たりませんでした。

 
実装はこんな感じです。

class ItemFlashMixinView(SuccessMessageMixin, CreateView):
    ...
    success_message = "%(name)s was created successfully"

%(name)sについては、%(field_name)sという表記で、ModelであるItemのnameフィールドにある値、という意味になります。

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Django_messages_framework_sample

フラッシュメッセージがクリアされるのを防ぐサンプルやSuccessMessageMixinを使うサンプルも含めています。