とりあえず、な generic view の object_list の拡張

前回の続き。相変わらず情報収集をサボっていてあんまりよく知らないので自分で作るところからスタート。いいフレームワークっていうのは、調べるより自分で作った方が早いと思わせてくれるなあ。かといってそこで作っちゃ負けだけど。

誰も使わないとは思うけど、filter とか inclusion_tag とか simple_tag では実現できない複雑な templatetags の作り方とか参考になるといいなと思って晒してみる。

ちなみに、これは admin のテンプレートや CSS を流用して手軽に書けるようにするもの。SVN の HEAD ではもっと楽になってるのかなあ。

views.generic.py:

from django.http import HttpResponse
from django.views.generic.list_detail import object_list as _object_list

def object_list(request, queryset = None, *args, **kwargs):
    # queryset は ``Model.objects.all()`` みたいに直接指定する代わりに
    # ``lambda req: Model.objects.all()`` のように指定する。
    # 複雑な事をやろうとすると urlpatterns が長くなるのが問題になりそうだけど。
    qs = queryset(request)
    if request.GET.has_key('sort_by'):
        qs = qs.order_by(*request.GET.getlist('sort_by'))
    return _object_list(request, queryset = qs, *args, **kwargs)

templatetags/listutils.py:

from django.utils.html import escape
from django.utils.translation import gettext
from django.template import Library, Node, TemplateSyntaxError, TokenParser, resolve_variable

register = Library()


class SortableListHeaderTokenParser(TokenParser):
    def top(self):
        values = []
        while True:
            values.append(self.value())
            if not self.more():
                break
        return values

class SortableListHeaderNode(Node):
    class RenderHelper:
        def __init__(self, queryset):
            sorted_fields = {}
            # XXX: 本来は _order_by はのぞいちゃダメと思われる
            if hasattr(queryset._order_by, '__iter__'):
                for spec in queryset._order_by:
                    if spec[0] == '-':
                        sorted_fields[spec[1:]] = -1
                    else:
                        sorted_fields[spec] = 1
            self._sorted_fields = sorted_fields
            self._queryset = queryset

        def render(self, name):
            field_verbose_name = gettext(
                    self._queryset.model._meta.get_field(name).verbose_name)
            retval = ''
            spec = self._sorted_fields.get(name, None)
            if spec != None:
                retval += '<th class="sorted %s">' % (
                    spec > 0 and 'ascending' or 'descending', )
            else:
                retval += '<th>'
            retval += '<a href="?sort_by=%s%s">%s</a>' % (
                (self._sorted_fields.get(name, -1) > 0 and '-' or ''),
                name, escape(field_verbose_name), )
            retval += '</th>'
            return retval

    def __init__(self, queryset, *fields):
        self.queryset = queryset
        self.fields = fields

    def render_header_column(self, queryset, name):
        return retval

    def render(self, context):
        queryset = resolve_variable(self.queryset, context)
        rdr = self.RenderHelper(queryset)
        return ''.join([
            rdr.render(resolve_variable(field, context))
                for field in self.fields ])

def sortable_list_header(parser, token):
    args = SortableListHeaderTokenParser(token.contents).top()
    return SortableListHeaderNode(*args)

register.tag('sortable_list_header', sortable_list_header)

テンプレートの例 (admin のテンプレートを流用)

{% extends "main/base_site.html" %}
{% load listutils i18n %}
{% block content %}<div id="content-main">
{% block object-tools %}
<ul class="object-tools">
  <li><a href="+" class="addlink">{% trans 'Add a new project' %}</a></li>
</ul>
{% endblock %}
<table>
  <thead>
    <tr>
      {% sortable_list_header object_list 'id' 'name' %}
    </tr>
  </thead>
  <tbody>
    {% for object in object_list %}
    <tr>
      <td><a href="{{ object.get_absolute_url }}">{{ object.id }}</a></td>
      <td><a href="{{ object.get_absolute_url }}">{{ object.name }}</a></td>
    </tr>
    {% endfor %}
  </tbody>
</table>
</div>{% endblock %}