[Django] 日本ひげ男協会のサイトを作成する。その9

注意

この記事は、id:SumiTomohiko:20070127:1169923192の続きです。

環境

この記事は、Ubuntu 6.10, Python 2.4, Django 0.95, SQLite 3.3.5で確認しました。

フィルタ

Webアプリケーションを作成すると、モデルの内容を加工して表示する場面がしょっちゅうあります。例えば、モデルが持つURLをリンクとして表示する場合などです。Model-View-Controller(DjangoではModel-Template-View)モデルにおける役割分担の上では、このような変換はビュー(Djangoではテンプレート)で行うことになります。URLをリンクに変換する例の場合、テンプレートで次のように記述します。

{{ object.web|urlize }}

この、"|urlize"と記述するとURLがリンクになる機能を、フィルタといいます(PHPに詳しい方なら、Smartyにも同じ機能があることをご存知かと思います)。

日本ひげ男協会のサイトでも、このフィルタを使いたい箇所があります。具体的には、以下のところです。

  1. ひげをはやし始めた年から、現在までの経過年数(ひげ年齢)を計算する。
  2. 自己紹介文が長い場合、会員一覧画面では最初の方しか表示しない。
  3. 会員一覧画面で、ひげの写真を縮小して表示するため、画像の大きさから<img>タグのheight属性とwidth属性を計算する。

これらの実現方法を、個別に説明します。

ひげ年齢

ひげ年齢は、ひげをはやし始めた年から計算できますので、テンプレートで、

{{ object.from_year|age }}

と書いたら年齢が表示されるようにしたいです。

そのためには、まず始めにuser/templatetagsディレクトリを作成します。それから、user/templatetagsディレクトリに、__init__.pyという空のファイルを作成します。フィルタの実装は、user/templatetags/user_extras.pyに記述します。

from django import template
import datetime

register = template.Library()

@register.filter(name="age")
def _age(value):
    return datetime.datetime.now().year - value

この中の、@register.filter(name="age")というデコレータで、_age関数をageという名前で、使用できるフィルタとして登録しています。_age関数には、フィルタの前にある値が、value引数を通して渡されます。_age関数の戻り値が、実際に表示される(さらにフィルタが続いていたら、次のフィルタに渡される)値になります。

このフィルタを使うテンプレートでは、以下の一文を挿入します。

{% load user_extras %}

これにより、user/templatetags/user_extras.pyに含まれるフィルタが使用できるようになります。使いかたは、上の例で示した通りです。

自己紹介文を省略する。

自己紹介文が長い場合、全文は詳細画面で読んでもらうことにして、一覧画面では何文字かだけ表示するようにします。このとき、何文字表示するかもテンプレートで指定できたら、デザインの変更に柔軟に対応できて便利です。

このような場合、フィルタに引数を与えることができます。例えば、以下の通りです。

{{ object.introduction|cut:"20" }}

このとき、cutフィルタには、"20"という引数が与えられます。このようなフィルタは、次のようにして記述します。

@register.filter(name="cut")
def _cut(value, length):
    try:
        length = int(length)
    except ValueError:
        return ""

    value = unicode(value, "utf-8")     # Only for SQLite?

    if len(value) <= length:
        return value
    return value[:length] + "..."

単に、フィルタ本体の関数に引数が増えるだけです。ただし、引数は文字列となっているので、内部で適切な型に変換してから使用してください。

なお、上の_cut関数の途中にある、

    value = unicode(value, "utf-8")     # Only for SQLite?

は、文字列をユニコード文字列に変換する、という意味です。データベースにはSQLiteを使用しているのですが、データベースから文字列を読み込むと、(ユニコード文字列ではなく)ただの文字列となってしまうようで、len関数やスライスが思い通りに動作しません。なので、上の一文をいれて回避しています。本当はデータベースから読み込むところで変換した方がいいのでしょうが、データベースの種類が変わったら必要なくなりそうなので、今回は応急手当で済ませています。

画像を縮小表示する。

一覧画面では、画像を縮小表示、いわゆるサムネイルを表示します。ここでも横着します。本当は、画像がアップロードされた時点でサムネイル用の画像を生成し、閲覧時にダウンロードする容量を節約するのが筋なのですが、今回は単に<img>タグのheight属性とwidth属性の値を小さく指定するだけにします。フィルタは、height用とwidth用の2つが必要になります。

フィルタの実装は、次のようになります。

def _get_thumbnail_size(user, max_height=None, max_width=None):
    if max_height == None:
        max_height = juma.user.settings.THUMBNAIL_MAX_HEIGHT
    if max_width == None:
        max_width = juma.user.settings.THUMBNAIL_MAX_WIDTH
    height = user.get_image_height()
    width = user.get_image_width()
    if (height <= max_height) and (width <= max_width):
        return (height, width)
    elif (height <= max_height) and (max_width < width):
        ratio = float(max_width) / float(width)
        return (int(height * ratio), max_width)
    elif (max_height < height) and (width <= max_width):
        ratio = float(max_height) / float(height)
        return (max_height, int(width * ratio))
    else:
        height_ratio = float(max_height) / float(height)
        width_ratio = float(max_width) / float(width)
        if height_ratio < width_ratio:
            return (int(height * height_ratio), int(width * height_ratio))
        else:
            return (int(height * width_ratio), int(width * width_ratio))

@register.filter(name="thumbnail_height")
def _get_thumbnail_height(user, max_height=None, max_width=None):
    return _get_thumbnail_size(user, max_height, max_width)[0]

@register.filter(name="thumbnail_width")
def _get_thumbnail_width(user, max_height=None, max_width=None):
    return _get_thumbnail_size(user, max_height, max_width)[1]

使うときは、次のように記述します。

<img height="{{ object|thumbnail_height }}" width="{{ object|thumbnail_width }}" src="foo"/>

テンプレートの継承

日本ひげ男協会のサイトでは、テンプレートの継承を利用しています。これは、各画面の共通点をひとつのファイルに記述しておき、各画面はそこからの差分のみを記述する、という方法です。これに関しては、テンプレート作者のための Django テンプレート言語ガイド以上に説明することがないので、そちらをご参照下さい。

タイムゾーン

日本では、settings.pyのTIME_ZONEを、以下のように設定します。

TIME_ZONE = 'Asia/Tokyo'