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

注意

この記事は、id:SumiTomohiko:20070119:1169239577の続きです。

前回からの変更点

アプリケーション名の変更

前回はアプリケーション名を"web"として作成しましたが、これを"user"に変更しました。

Userモデルの変更

会員の情報を見直し、モデルを変更しました。新しいUserクラスは、以下のフィールドを持ちます。

  1. 名前
  2. ひげをはやし始めた年(西暦)
  3. ひげの写真
  4. 自己紹介
  5. WebサイトのURL
  6. ブログのURL
  7. 作成日時

user/mode.pyは、以下のようになります。

from django.db import models

class User(models.Model):
    class Admin:
        pass

    name = models.CharField(maxlength=1024)
    introduction = models.TextField(blank=True, maxlength=8192)
    image = models.ImageField(blank=True, upload_to='image')
    from_year = models.IntegerField(null=True)
    web = models.URLField(blank=True, maxlength=8192)
    blog = models.URLField(blank=True, maxlength=8192)
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return "name=\"%s\", introduction=\"%s\", from_year=%d, web=\"%s\", blog=\"%s\")" % (self.name, self.introduction, self.from_year, self.web, self.blog)

# vim: tabstop=4 shiftwidth=4 expandtab

会員登録画面の作成

サイトの最初の機能として、会員登録画面を作成します。通常Djangoは、あるURLに対して実行する関数を記述していくのですが、あるモデルを登録したり一覧表示したりといった単純な関数については、Djangoが既に用意してあります。これを汎用ビューといいます。新しいモデルを登録する汎用ビューは、django.views.generic.create_update.create_objectです。では、設定をしてみます。

user/url.pyを作成し、以下のように記述します。

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

urlpatterns = patterns('',
    (r'^add/$', 'django.views.generic.create_update.create_object', { 'model': User, 'post_save_redirect': '/user/list/', 'extra_context': { 'title': '会員登録' }}), 
)

# vim: tabstop=4 shiftwidth=4 expandtab

これは、"/user/add/"にアクセスされたら、django.views.generic.create_update.create_objectを実行する、という意味です。続く辞書は、以下を意味します。

キー 説明
model 追加するモデルのクラス。
post_save_redirect 登録後に遷移するURL(/user/list/は、まだ作っていません)。
extra_context テンプレートに渡す追加の情報。今回は、画面の名前を渡しています。

次に、URLの設定がuser/url.pyにあることを、Djangoに教えます。トップディレクトリのurl.pyを、以下のようにします。

from django.conf.urls.defaults import *

urlpatterns = patterns('juma.user.views',
    (r'^user/', include('juma.user.urls')), 

    # Uncomment this for admin:
     (r'^admin/', include('django.contrib.admin.urls')),
)

# vim: tabstop=4 shiftwidth=4 expandtab

これで、"/user"にアクセスされたら、user/url.pyの設定が適用されることになります。

続いて、画面のテンプレートを用意します。template/user/user_form.htmlにテンプレートを作成します。なぜなら、django.views.generic.create_update.create_objectは、<アプリケーション名>/<モデル名>_form.htmlをテンプレートにするからです(templateディレクトリの存在については、あとでDjangoに教えます)。template/user/user_form.htmlは、以下のようにします。

<h1>{{ title }}</h1>

<p>
<form action="" enctype="multipart/form-data" method="post">
    <table>
        <tr>
            <td align="right">名前</td>
            <td>
                {{ form.name }}
                &nbsp;
                {{ form.name.errors|join:"&nbsp;" }}
            </td>
        </tr>
        <tr>
            <td align="right">ひげをはやし始めた年(西暦)</td>
            <td>
                {{ form.from_year }}
                &nbsp;
                {{ form.from_year.errors|join:"&nbsp;" }}
            </td>
        </tr>
        <tr>
            <td align="right">ひげの写真</td>
            <td>
                {{ form.image }}{{ form.image_file }}
                &nbsp;
                {{ form.image_file.errors|join:"&nbsp;" }}
            </td>
        </tr>
        <tr>
            <td align="right">自己紹介</td>
            <td>
                {{ form.introduction }}
                &nbsp;
                {{ form.introduction.errors|join:"&nbsp;" }}
            </td>
        </tr>
        <tr>
            <td align="right">Webサイト</td>
            <td>
                {{ form.web }}
                &nbsp;
                {{ form.web.errors|join:"&nbsp;" }}
            </td>
        </tr>
        <tr>
            <td align="right">ブログ</td>
            <td>
                {{ form.blog }}
                &nbsp;
                {{ form.blog.errors|join:"&nbsp;" }}
            </td>
        </tr>
        <tr>
            <td align="center" colspan="2">
                <input type="submit" value="登録する"/>
            </td>
        </tr>
    </table>
</form>
</p>
<!--
    vim: tabstop=4 shiftwidth=4 expandtab
-->

テンプレート内では、form.<フィールド名>と記述すれば、モデルから適切な入力項目が作られます。注意するのは、ImageFieldを設定したimageフィールドで、imageそのままだと現在のファイル名となり、image_fileとすると<input type="file">が作られます。フォームの送信先は空文字列にして、画面を表示するのと同じURLに送信させます。なぜならば、django.views.generic.create_update.create_objectは以下のように、送信されたメソッドがGETなのかPOSTなのかで処理を分けているからです。

def create_object(request, model, template_name=None,
        template_loader=loader, extra_context=None, post_save_redirect=None,
        login_required=False, follow=None, context_processors=None):
    """
    Generic object-creation function.

    Templates: ``<app_label>/<model_name>_form.html``
    Context:
        form
            the form wrapper for the object
    """
    if extra_context is None: extra_context = {}
    if login_required and not request.user.is_authenticated():
        return redirect_to_login(request.path)

    manipulator = model.AddManipulator(follow=follow)
    if request.POST:
        # If data was POSTed, we're trying to create a new object
        new_data = request.POST.copy()

        if model._meta.has_field_type(FileField):
            new_data.update(request.FILES)

        # Check for errors
        errors = manipulator.get_validation_errors(new_data)
        manipulator.do_html2python(new_data)

        if not errors:
            # No errors -- this means we can save the data!
            new_object = manipulator.save(new_data)

            if request.user.is_authenticated():
                request.user.message_set.create(message="The %s was created successfully." % model._meta.verbose_name)

            # Redirect to the new object: first by trying post_save_redirect,
            # then by obj.get_absolute_url; fail if neither works.
            if post_save_redirect:
                return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
            elif hasattr(new_object, 'get_absolute_url'):
                return HttpResponseRedirect(new_object.get_absolute_url())
            else:
                raise ImproperlyConfigured("No URL to redirect to from generic create view.")
    else:
        # No POST, so we want a brand new form without any data or errors
        errors = {}
        new_data = manipulator.flatten_data()

    # Create the FormWrapper, template, context, response
    form = forms.FormWrapper(manipulator, new_data, errors)
    if not template_name:
        template_name = "%s/%s_form.html" % (model._meta.app_label, model._meta.object_name.lower())
    t = template_loader.get_template(template_name)
    c = RequestContext(request, {
        'form': form,
    }, context_processors)
    for key, value in extra_context.items():
        if callable(value):
            c[key] = value()
        else:
            c[key] = value
    return HttpResponse(t.render(c))

ここで、templateディレクトリ以下にテンプレートがあることを、Djangoに教えます。settings.pyに、以下を記述します。

TEMPLATE_DIRS = (
    'template', 
)

以上で、会員登録画面は完成です。入力値検証も働きます。

会員一覧画面

続いて、会員一覧画面を作成します。この画面はUserモデルを並べて表示するだけですが、このような機能に対する汎用ビューも用意されています。django.views.generic.list_detail.object_listがそれです。user/urls.pyを以下のようにします。

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

urlpatterns = patterns('',
    (r'^add/$', 'django.views.generic.create_update.create_object', { 'model': User, 'post_save_redirect': '/user/list/', 'extra_context': { 'title': '会員登録' }}), 
    (r'^list/$', 'django.views.generic.list_detail.object_list', { 'queryset': User.objects.all(), 'allow_empty': True, 'paginate_by': 20 }), 
)

# vim: tabstop=4 shiftwidth=4 expandtab

これにより、"/user/list/"にアクセスすると、django.views.generic.list_detail.object_listによって一覧画面が表示されます。続く辞書のquerysetは、表示するオブジェクトのリストを指定し、allow_emptyはデータが一件もなくても表示することを指示し、paginate_byは1ページ20件表示することを設定します。

テンプレートは、template/user/user_list.htmlに作成します。これも新規登録時と同様、django.views.generic.list_detail.object_listが<アプリケーション名>/<モデル名>_list.htmlを使用するからです。内容は、以下のようにします。

<h1>会員一覧</h1>

<p>
<table border="1">
    <tr>
        <th>名前</th>
        <th>ひげをはやし始めた年(西暦)</th>
        <th>ひげの写真</th>
        <th>自己紹介</th>
        <th>Webサイト</th>
        <th>ブログ</th>
    </tr>
{% for object in object_list %}
    <tr>
        <td>{{ object.name|escape }}</td>
        <td align="right">{{ object.from_year }}</td>
        <td>
    {% if object.image %}
            <img src="{{ object.get_image_url }}"/>
    {% else %}
            &nbsp;
    {% endif %}
        </td>
        <td>
    {% if object.introduction %}
        {{ object.introduction|escape|linebreaks }}
    {% else %}
            &nbsp;
    {% endif %}
        </td>
        <td>
    {% if object.web %}
        {{ object.web|urlize }}
    {% else %}
            &nbsp;
    {% endif %}
        </td>
        <td>
    {% if object.blog %}
        {{ object.blog|urlize }}
    {% else %}
            &nbsp;
    {% endif %}
        </td>
    </tr>
{% endfor %}
</table>
</p>

{% if is_paginated %}
<p>
    {% if has_previous %}
<a href="./?page={{ previous }}">前のページへ</a>
    {% else %}
前のページへ
    {% endif %}
&nbsp;{{ page }}&nbsp;/&nbsp;{{ pages }}&nbsp;
    {% if has_next %}
<a href="./?page={{ next }}">次のページへ</a>
    {% else %}
次のページへ
    {% endif %}
</p>
{% endif %}
<!--
    vim: tabstop=4 shiftwidth=4 expandtab
-->

テンプレート内では、object_listという変数によって、オブジェクトのリストを参照できます。またここでは、いくつかフィルタを使用しています。

        {{ object.introduction|escape|linebreaks }}

というのは、objectのintroductionフィールドに含まれるHTMLのメタ文字 (<, >, ", &) をエスケープし (escape) 、改行を<br>に変換する (linebreaks) という意味です。また、

        {{ object.blog|urlize }}

というのは、URLをリンクにします。これで会員一覧画面ができました。