This article is a Private article. Only a writer and users who know the URL can access it.
Please change open range to public in publish setting if you want to share this article with other users.

More than 3 years have passed since last update.

[Day 24] テンプレートのフィルターを使う

Last updated at Posted at 2021-01-31

January 31, 2021
←前回:Day 23 テンプレートタグを使ってサイドバーを作成する
「Djangoを学びたい」とのことでありましたら[Day 1]Djangoの開発環境から読むことをおすすめします。

はじめに

前回テンプレートタグを作成しました。今回はテンプレート上で用いる独自フィルターを使用していきます。

Djangoテンプレートのフィルター

Djangoのテンプレートには他のフレームワーク同様にテンプレート上でフィルター処理を行えます。
そのため、予め組み込まれた組み込みフィルターが用意されています。
例えば、truncatecharsという組込みフィルターがありますが、これは{{ context.hoge | truncatechars:10 }}と書くと10文字に切り詰めて表示するという機能を持つフィルターです。

カスタムフィルターを作る

ではカスタムフィルターを作成していきましょう。
今回はコメントにURLが記載された場合リンクとして表示するフィルターを作成しましょう。
thread/templatetags/threadfilters.pyを作成します。

thread/templatetags/threadfilters.py

from django.template import Library

register = Library()

@register.filter
def comment_filter(text):
    return ''.join(list(map(convert_url, text.split('\n'))))

def convert_url(text_line):
    '''
    URLリンク行をaタグ付きの行に変換
    '''
    if 'https://' in text_line or 'http://' in text_line:
        return '<a href="' + text_line + '" target="_blank" rel="noopener noreferrer">' + text_line + '</a>'
    else:
        return text_line

register.filterメソッドは引数nameを省略すると関数の名前がフィルター名として適用されます。
今回はcomment_filterですね。
前回のテンプレートタグと同様にthreadfiltersをロードしてcomment_filterを適用することにします。

templates/thread/detail_topic.html

{% extends 'base/base.html' %}
{% block title %}トピック作成 - {{ block.super }}{% endblock %}
{% block content %}
{% load threadfilters %}
<div class="ui grid stackable">
    <div class="eleven wide column">
        <div class="ui breadcrumb">
            <a href="{% url 'base:top' %}" class="section">TOP</a>
            <i class="right angle icon divider"></i>
            <a href="{% url 'thread:category' url_code=topic.category.url_code %}" class="section">{{topic.category.name}}</a>
            <i class="right angle icon divider"></i>
            <a class="active section">{{topic.title}}</a>
        </div>
        <div class="ui segment">
            <div class="content">
                <div class="header"><h3>{{topic.title}}</h3></div>
                <p>{{topic.user_name}} - {{topic.created}}</p>
                <div class="ui segment">
                    <p><pre>{{topic.message}}</pre></p>
                </div>
            </div>
        </div>
        <!--コメント表示-->
        <div class="ui segment">
            {% if comment_list %}
            {% for comment in comment_list %}
            <div class="ui segment secondary">
                <p>{{comment.no}}. {{comment.user_name}}<br>{{comment.created}}</p>
                {% if comment.pub_flg %}
                <p>{{comment.message | comment_filter}}</p>
                {% else %}
                <p style="color: #aaa">このコメントは非表示となりました。</p>
                {% endif %}
            </div>
            {% endfor %}
            {% else %}
            <div class="ui warning message"><p>まだコメントはありません</p></div>
            {% endif %}
        </div>
        <!--//コメント表示-->
        <!--コメント投稿-->
        <h4>コメント投稿</h4>
        <div class="ui segment">
            <form class="ui form" action="" method="POST">
                {% csrf_token %}
                {{form.as_p}}
                <button class="ui button orange" type="submit">コメント投稿</button>
            </form>
        </div>
        <!--//コメント投稿-->
    </div>
    {% include 'base/sidebar.html' %}
</div>
{% endblock %}

このようになります。{% load threadfilters %}でフィルターを読み込んでいます。
{{comment.message | comment_filter}}の部分でコンテキストで渡された文字列を変換しています。
ではブラウザで確認してみましょう。

URLを含んだ投稿をします。(今回のケースではURL行は改行される必要があります。)

image.png

変換された結果

image.png

あれ?ちょっとおかしな結果になりましたね。原因を考えていきましょう。

Djangoテンプレートエスケープ処理

確かに文字列は変換されていますが、HTMLタグがそのまま表示されてしまいました。
これはどういうことでしょうか?
実はDjangoテンプレートはデフォルトでエスケープ機能がONになっていて、コンテキストで渡された文字列はエスケープ処理が施されて出力されることになっています。
PHPでいうとhtmlspecialchars関数が適用されたような状態です。
これにより万が一コンテキストに悪意のあるスクリプトが渡されたとしても実行を防ぐ作用をしています。(ただし過信は禁物です。)

今回のようにフィルターでHTMLを生成する場合にはエスケープ機能をOFFにする必要があります。
しかし、この機能を外す際にはプログラマーは十分に注意する必要があります。今回はbleachライブラリを使用してエスケープ処理をした後にHTML変換を書けることで対応してみます。

まずはbleachをインストールしましょう。


(venv)$ pip install bleach

次にthread/templatetags/threadfilters.pyを下記のように書き換えます。

thread/templatetags/threadfilters/py

from django.template import Library

import bleach

register = Library()

@register.filter
def comment_filter(text):
    return ''.join(list(map(convert_url, bleach.clean(text).split('\n'))))

def convert_url(text_line):
    '''
    URLリンク行をaタグ付きの行に変換
    '''
    if 'https://' in text_line or 'http://' in text_line:
        return '$lt;a href="' + text_line + '" target="_blank" rel="noopener noreferrer">' + text_line + '</a>'
    else:
        return text_line

先で説明した通り、テンプレートの機能でエスケープ処理が行われないために、関数内部でエスケープ処理をしています。ではテンプレートを修正しましょう。

templates/thread/detail_topic.html(一部抜粋)

- <p>{{comment.message | comment_filter}}</p>
+ <p>{{comment.message | comment_filter | safe}}</p>

このようにsafeフィルターを付けることでテンプレートのエスケープ処理が外れます。
これはプログラマが’safe’な文字列であることを保証するという意味です。
その意味を考えたら気軽には使えませんよね。
今回はbleach関数で手軽に事前エスケープで対応しましたが、実際はもっと気を配るケースが多いと思います。

ではブラウザでどのように表示されるかを確認してみましょう。
URLリンクが想定通り出力されたでしょうか?

image.png

おわりに

今日で2021年1月も終わりですね。
もうすぐ節分です。
僕のバイト先でも恵方巻きの準備が着々と進んでいます。
毎年、恵方巻き残りすぎニュースを目にすると思いますが、僕のバイト先では毎回全部売り切ります。
ニュースが先行してしまって「恵方巻き=残るもの」と世間では、なってしまっていますが、大量に残るのは特定の店なのかな?と思ったりします。(大量には変わりないのですが。。。)

それではまたまた

←前回:Day 23 テンプレートタグを使ってサイドバーを作成する
→次回:Day 25 DjangoのAPIとAjax通信する「いいねボタン」を作成する

0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up