пятница, 2 декабря 2011 г.

viewtags

Как скрестить view и template_tag?

Случалась ли у Вас ситуация когда у вас на страничке есть некий блок. При загрузке страницы он реализуется например как рендер template_tag'а:
@register.inclusion_tag('my_block.html', takes_context=True)
def my_block_tag(context):
    return {
        # some additional context
    }
В шаблоне как обычно:
{% load my_tags %}
{% my_block_tag %}
Дальше, этот блок имеет pagination, который реализуется Ajax-запросом на сервер, где соответствующий view рендерит тот же самый кусок страницы:
def my_block_ajax(request, ...):
    #формируем нужные данные для нужной страницы
    return JsonResponse("my_block.html", {
        # какие-то данные
    })

Все замечательно но у нас получается есть 2 вещи (template_tag и view) которые по сути делают одно и тоже - возвращают один и тот же блок.
Как уйти от повторения кода?
Самое просто решение которое приходит в голову - сделать во views импорт нужного template_tag'а и возвращать в view результат работы этого самого template_tag'a:
from myapp.templatetags.my_tags import my_block_tag

def my_block_ajax(request, ...):
    #формируем нужные данные для нужной страницы
    return my_block_tag(some_context)

С виду все хорошо но давайте посчитаем что у нас есть. Есть view и есть tag(есть даже библиотека тэгов - а это уже само по себе отдельная папка и пару файлов), есть записи в urls.py для каждого ajax-запроса:
url(r'ajax/my_block/$', 'my_block', name='my_block'),
Сильно много у нас всего. А если у нас таких блоков 10? 20? это просто огромная масса лишнего повторяющегося кода.
А самое главное у нас одно и тоже по сути логическое действие для одного блока на странице разнесено как минимум по 2м отдельным файлам - my_tags.py и views.py.

ViewTags
Дальше пойдет интересное решение этой проблемы. Оговорюсь сразу, идея не моя, реализация тоже, но этим механизмом я пользовался и мягко говоря впечатлился:
Итак, задача скрестить view и template_tag.
Создаем файл utils/viewtags.py
  1. Делаем декоратор
    import inspect
    from django import template
    from django.template.loader import get_template
    from django.http import Http404
    from django.shortcuts import render_to_response
    from django.utils.importlib import import_module
    from django.conf import settings
    
    
    register = template.Library()
    template.builtins.append(register)
    
    _viewtags_registry = {}
    
    
    def viewtag(template, args=[]):
        def decorator(func):
            _register_tag(func, template)
            _viewtags_registry[func.__name__] = (func, template, args)
    
            return func
        return decorator
    
    Этим декоратором мы будем оборачивать вьюшки.
  2. Вьюшки будем обрабатывать вот так
    def view(request, viewtag_name):
        try:
            func, template_, arg_processors = _viewtags_registry[viewtag_name]
        except KeyError:
            raise Http404()
    
        argnames = inspect.getargspec(func).args
        kwargs = {}
        for argname in argnames:
            if argname in request.REQUEST and argname != 'request':
                kwargs[argname] = request.REQUEST[argname]
        for proc in arg_processors:
            kwargs = proc(request, kwargs)
    
        return render_to_response(template_, func(request, **kwargs), template.RequestContext(request))
  3. Все наши viewtags будем хранить в app/viewtags.py
    Поэтому при старте сервера загружаем все наши viewtags
    def autodiscover():
        for app in settings.INSTALLED_APPS:
            try:
                import_module('%s.viewtags' % app)
            except ImportError, e:
                pass
    
  4. Авторегистрируем template_tags для наших viewtags
    def _register_tag(func, template):
        def tag(parser, token):
            parts = token.split_contents()
            args, kwargs = [], []
            for part in parts[1:]:
                if '=' in part:
                    kwargs.append(part.split('=', 1))
                else:
                    args.append(part)
    
            return TempNode(func, template, args, dict(kwargs))
        register.tag('vt:' + func.__name__, tag)
    
    class TempNode(template.Node):
        def __init__(self, func, template_, args, kwargs):
            self.func = func
            self.template = get_template(template_)
            self.args = [template.Variable(arg) for arg in args]
            self.kwargs = dict([(k, template.Variable(v)) for (k,v) in kwargs.items()])
    
        def render(self, context):
            args = [context['request']] + [arg.resolve(context) for arg in self.args]
            kwargs = dict([
                (key, val.resolve(context)) for (key, val) in self.kwargs.items()
            ])
            context.update(self.func(*args, **kwargs))
            res = self.template.render(context)
            context.pop()
            return res
    
Собственно все. Теперь, как этим добром пользоваться:
В urls.py единоразово(!) делаем запись
url(r'^viewtag/([-\w]+)/$', 'my_project.utils.viewtags.view', name='viewtag'),
В viewtags.py импортируем наш декоратор viewtag и оборачиваем вьюшку:
from my_project.utils.viewtags import viewtag

@viewtag('my_block.html')
def my_block(request, page=0):
    # делаем что нам там нужно
    return {
        #context
    }
Благодаря пункту 3 мы автоматически будем иметь в системе "запись" в urls.py для всех функций описанных в файлах viewtags.py

Теперь к шаблонам.
Что-бы указать блок на странице будем писать так:
{% vt:my_block params %}
В пункте 1 у нас зарегистрированы функции для тэгов которые называются "vt". То есть если есть функция обернутая в viewtag то можно писать в шаблоне {% vt:имя_функции %} и это будет работать.

Ну и пейджинация.
Если раньше у нас был url для каждой ajax-вьюшки отдельно:
$.get('{% url my_block_ajax %}', {page: 2}, function(data) {
        $('body').append(data);
    });
То теперь мы делаем url для вьюшки с названием "viewtag" и параметром передаем конкретное название нужного viewtag:
$.get('{% url viewtag "my_block" %}', {page: 2}, function(data) {
        $('body').append(data);
    });
Считаем еще раз. Библиотек своих template_tags у нас вообще нет. Кучи однотипных записей в urls.py для каждого отдельного ajax-запроса у нас тоже нет. Код для каждого блока представлен одной общей функцией в viewtags.py
По-моему симпатично. Лично у меня количество кода значительно уменьшилось и все стало более понятным.

Комментариев нет:

Отправить комментарий