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

注意

この記事は、id:SumiTomohiko:20070126:1169768661の続きです。

環境

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

画像を表示する。

日本ひげ男協会のサイトでは、会員のひげの写真をアップロードして、みんなで自慢しあえるようにしたいと思っています。写真をアップロードする機能は既にあるので、今回は表示する機能を実装します。

まず始めに、ファイルをアップロードしたときのDjangoの挙動について説明します。django.db.models.ImageFieldオブジェクトをデータベースに追加したとき、データベースにはファイルのパスだけが登録されます。ファイル自体は、次の場所に保存されます。

<settings.pyのMEDIA_ROOT>/<ImageFieldのupload_to>/<ファイル名>

例えば、settings.pyに、

MEDIA_ROOT = '/home/tom/projects/juma/media/'

とあり、user/model.pyのUserクラスの定義で、

    image = models.ImageField(blank=True, upload_to='image')

としていたとき、foo.jpgというファイルをアップロードしたら、ファイルは/home/tom/projects/juma/media/image/foo.jpgに保存されます。データベースに保存されるファイルのパスは、MEDIA_ROOTからの相対パスになります。先ほどの例の場合だと、/image/foo.jpgです。

ここで、アップロードしたファイルのパスを得るためには、get_<フィールド名>_urlメソッドを使用します。このメソッドは、ImageFieldクラスの親クラスであるFileFieldクラスのcontribute_to_classメソッドで、動的に定義されます(ファイルでいうと、/usr/lib/python2.4/site-packages/Django-0.95-py2.4.egg/django/db/models/fields/__init__pyにあります)。

    def contribute_to_class(self, cls, name):
        super(FileField, self).contribute_to_class(cls, name)
        setattr(cls, 'get_%s_filename' % self.name, curry(cls._get_FIELD_filename, field=self))
        setattr(cls, 'get_%s_url' % self.name, curry(cls._get_FIELD_url, field=self))
        setattr(cls, 'get_%s_size' % self.name, curry(cls._get_FIELD_size, field=self))
        setattr(cls, 'save_%s_file' % self.name, lambda instance, filename, raw_contents: instance._save_FIELD_file(self, filename, raw_contents))
        dispatcher.connect(self.delete_file, signal=signals.post_delete, sender=cls)

このメソッドを使用すると、次の値が得られます。

<settings.pyのMEDIA_URL>/<MEDIA_ROOTからの相対パス>

先ほどの例でいうと、settings.pyに、

MEDIA_URL = '/user/media/'

とあった場合、Userクラスのget_image_urlメソッドを呼び出すと、/user/media/image/foo.jpgが得られます。ただし、Djangoがやってくれるのはここまでです。実際にファイルをブラウザに返すコードは、自分で実装することになります。

では、実装に入ります。まずURLを設計する必要があります。関わる設定が、settings.pyのMEDIA_URLと、user/mode.pyのUserクラスのImageFieldオブジェクトのupload_toの2つであることが、少しいやらしいです。今回はひげの画像の1種類しか扱いませんが、複数の種類のファイルを扱うようになった場合、

  1. ファイルを扱うURLは、ひとつにまとめたい。
  2. その下で、ファイルの種類ごとにディレクトリを分割したい。

と思ったので、上の例のままでいくことにしました。

ただし、このURL設計には問題があります。MEDIA_ROOTやMEDIA_URLがプロジェクトのルートディレクトリにあるsettings.pyで設定されていることから、ファイルは/userといったアプリケーションごとに管理するものではないと、Djangoでは設計されています。そのため、ファイルをダウンロードするURLが/user/mediaであり、userアプリケーションに任されるというのは、Djangoの設計思想と食い違います。もっとDjangoらしい解決方法があるのかもしれません(ファイルのダウンロードを行うmediaアプリケーションを追加しようともしたのですが、/mediaというURLはまた別のことに使われていたので、見送りました)。

さて、URLの設定は、user/urls.pyに追加します。

from django.conf.urls.defaults import *
from juma.user.models import User

urlpatterns = patterns('',
    (r'^add/$', 'juma.user.views.add'), 
    (r'^list/$', 'django.views.generic.list_detail.object_list', { 'queryset': User.objects.all(), 'allow_empty': True, 'paginate_by': 20 }), 
    (r'^media/image/(?P<file_name>[-\w\d_\.]+)$', 'juma.user.views.image'), 
)

# vim: tabstop=4 shiftwidth=4 expandtab

ビューuser/views.pyは、次のようになります。エラーの判定を除けば、単にファイルを読んで送り返しているだけです。

from django.http import HttpResponseForbidden, HttpResponse
from django.http import HttpResponseRedirect, HttpResponseNotFound
from django.shortcuts import render_to_response
from juma.user.models import User
import os.path
import juma.user.manipulators
import juma.settings

(略)

def image(request, file_name):
    if file_name.find("..") != -1:
        return HttpResponseForbidden("<h1>403 Forbidden</h1>")
    file_path = os.path.join(juma.settings.MEDIA_ROOT, "image", file_name)
    if not os.path.exists(file_path):
        return HttpResponseNotFound("<h1>404 Not Found</h1>")
    f = open(file_path, "r")
    s = f.read()
    f.close()
    return HttpResponse(s)

これで、画像の表示ができるようになりました。

日本語のファイル名を扱えるのかとか、画像のサイズ(容量)を制限しなくていいのかとか、画像が大きい場合は縮小して表示した方がいいのではないかとか、懸念される事項はありますが、いったん保留にして先に進むことにします。

著作権表示

サンプルの画像に含まれるウェルシュコーギーの画像の著作権は、フォトカフェさんが保持します。