13 ноября 2013 г.

django: contrib.comments работаем через ajax

Тут рассказ о том, как заставить стандартный django.contrib.comments работать через ajax. Цель была такова: минимальные изменения в клиентских view, чтобы работали стандартные templates перегружаемые, полноценная админка (обычная от приложения comments), да и вообще максимальное использование оригинального кода. Короче, в итоге после нескольких самопроизвольных переписываний (вместе с использованием подобного решения в проектах) получилась минимальная обёртка, которую по идее тоже можно вынести в отдельный app.

Да, скоро это уже не совсем актуально станет (django.contrib.comments устаревший и с версии 1.6 вынесен в отдельный проект), но решение отточилось на третьей версии и мне жалко его терять просто :) Пример тут для версии 1.5.

Вьюшки получают нужный экземпляр модели и передают в шаблоны для рендера их через стандартные теги (такова задумка оригинального приложения), см. ниже. Вьюшка postcomment помимо этого делает небольшой хак, чтобы отловить валидацию формы и постинг (используется почти полностью родной код). Я накомментил очень богато, всё должно быть понятно.

views.py
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import render

# Здесь мы намеренно не получаем напрямую список для рендера, т.к. хотит больше отдать
# стандартному рендеру и стандартным шаблонам контриб-аппа 'comments'
@login_required
def listcomments(request, eid, model):
    # получаем объект сущности
    try:
        ieid = int(eid)
        # получаем сущность
        ct = ContentType.objects.get_for_model(model)
        entity = ct.get_object_for_this_type(pk=ieid)
    except (ValueError, KeyError):
        return HttpResponseBadRequest('error params')
    except ObjectDoesNotExist:
        return HttpResponseNotFound('entity %s id=%i not found' % (model.__name__, ieid))
    return render(request, 'commentsajax/list.html', {"entity": entity, })


@login_required
def postcomment(request, eid, model):
    # получаем объект сущности
    try:
        ieid = int(eid)
        # получаем сущность
        ct = ContentType.objects.get_for_model(model)
        entity = ct.get_object_for_this_type(pk=ieid)
    except (ValueError, KeyError):
        return HttpResponseBadRequest('error params')
    except ObjectDoesNotExist:
        return HttpResponseNotFound('entity %s id=%i not found' % (model.__name__, ieid))
    # работа непосредственно
    if request.method == 'POST':
        # надо проверить юзера и мыло, чтобы форма свалидировалась
        # у нас не используется имя и мыло и не заполняется в форме так что считаем что пришли пустые.
        # но юзер проверится и поставится в оригинальном методе из полного имени или юзернема, а мыло может остаться пустым
        # юзер у нас всегда залогинен по определению.
        if not request.user.email:
            request.POST = request.POST.copy()
            request.POST["email"] = "none@none.none"
        # передаём управление оригинальному коду
        from django.contrib.comments.views.comments import post_comment
        response = post_comment(request)
        # проверяем вернутый статус
        if response.status_code == 302:
            # если редирект, то было норм
            return HttpResponse('ok')
        else:
            # иначе там нам рисуется ошибка, мы переопределили comments/preview.html как раз чтобы туда оно и вывелось точь-в-точь как наша форма
            return response
    # запрос формы
    else:
        return render(request, 'commentsajax/form.html', {"entity": entity, })

Эти шаблоны внутри нашего приложения рендерят через стандартные теги список комментариев и форму:

templates/commentsajax/form.html
{% load comments %}
    
{% render_comment_form for entity %}

templates/commentsajax/list.html
{% load comments %}

{% render_comment_list for entity %}

Эти шаблоны менять не нужно по моей задумке. Они нужны, чтобы максимизировать количество используемого кода оригинального приложения. Чтобы нарисовать свой список комментариев и свою форму надо менять стандартные шаблоны, например такие минимальные могут быть:

templates/comments/form.html
{% include "comments/preview.html" %}

templates/comments/list.html
{% for comment in comment_list %}
    

{{ comment.comment }}

{{ comment.name }}, {{ comment.submit_date }}
{% endfor %}

templates/comments/preview.html
{% load comments %}
{% csrf_token %}
{{ form.honeypot }}
{{ form.content_type }} {{ form.object_pk }} {{ form.timestamp }} {{ form.security_hash }}

Здесь form.html повторяет preview.html, который мы перегрузили чтобы отловить ошибки от формы не меняя ничего во view, ведь туда приходит ошибки по задумке оригинального приложения, напоминаю. Кнопки в форме нет, т.к. я использовал в своих примерах бутстраповские диалоги с родной их кнопкой, но суть понятна - по нажатию на неё отправляем ажаксом эту форму. Вообще с клиентской стороны запрашивается список каментов через GET. Сама форма через GET запрашивается и через POST отдаются данные, если вернулось не "ok", то значит вернулась форма с отрендереными ошибками, которые надо заново перепоказать.

Такая сложная схема в view с eid и model нужна для того, чтобы настраивать привязку комментариев на уровне url, т.к. у меня комментарии привязывались к нескольким сущностям, а в случае ajax мне лично удобнее тупо слать пост на ajax_listcomments/ и получать комментарии именно для этой сущности. Например, так у меня url настроены:

...
    # это обязательно надо при сохранении камента, для резолва 'comments-comment-done' внутри views оригинального аппа
    (r'^comments/', include('django.contrib.comments.urls')),
    # комментарии к проекту
    url(r'^project/(?P<eid>[^/]+)/ajax_listcomments/$',
        views_comment.listcomments,
        {'model': Project},
        name='project-ajax-listcomments',
    ),
    url(r'^project/(?P<eid>[^/]+)/ajax_postcomment/$',
        views_comment.postcomment,
        {'model': Project},
        name='project-ajax-postcomment',
    ),
    # комментарии к контрагенту
    url(r'^contragent/(?P<eid>[^/]+)/ajax_listcomments/$',
        views_comment.listcomments,
        {'model': Contragent},
        name='contragent-ajax-listcomments',
    ),
    url(r'^contragent/(?P<eid>[^/]+)/ajax_postcomment/$',
        views_comment.postcomment,
        {'model': Contragent},
        name='contragent-ajax-postcomment',
    ),
...

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

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