Как скрестить 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
- Делаем декоратор
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
Этим декоратором мы будем оборачивать вьюшки.
- Вьюшки будем обрабатывать вот так
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))
- Все наши viewtags будем хранить в app/viewtags.py
Поэтому при старте сервера загружаем все наши viewtags
def autodiscover():
for app in settings.INSTALLED_APPS:
try:
import_module('%s.viewtags' % app)
except ImportError, e:
pass
- Авторегистрируем 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
По-моему симпатично. Лично у меня количество кода значительно уменьшилось и все стало более понятным.