Материализованные дебаты
by Larin
Интро
Что главное в интернете? Полезная информация и общение. И все в сети строиться вокруг этих 2-х понятий.
Сегодня мы займемся общением =) Точнее инструментом для общения – написанием древовидных комментариев на Django. Созданием универсальных древовидных комментариев с добавлением при помощи Ajax.
Материализованные пути
Вначале расскажу об одном из представлений деревьев в БД. Материализованные пути (Materialized Path) – на мой взгляд, это представление наиболее полно подходит для реализации древовидных комментариев.
Идея состоит в том, что для каждой вершины дерева храниться полный путь от корня.
| ID | Title | Path |
| 1 | Первый | 1 |
| 2 | Второй | 1.2 |
| 3 | Третий | 1.2.3 |
| 4 | Четвертый | 4 |
| 5 | Пятый | 4.5 |
В итоге, сделав сортировку по полю Path мы получим следующее:
где уровень вложенности (отступа) равен количеству разделителей в пути.
Данное представление гораздо легче в понимании нежели вложенные множества (Nested Sets), но так же позволяет нам выбирать дерево целиком используя один простой sql-запрос. Я естественно, не говорю, что материализованные пути полноая замена вложенных множеств и наоборот. Нет. Для каждой задачи нужно искать подходящее решение. Универсальных инструментов не бывает.
Модель
И так приступим. Т.к. задача довольно простая – этап проектирования и построения диаграмм я пропущу. Будем считать, что он пройден. =)
Приступим сразу к построению Django-модели:
# -*- coding: utf-8 -*-
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from people.models import People
class Comment(models.Model):
path = models.CharField(max_length=255)
text = models.TextField()
date = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(People, verbose_name=u"Пользователь")
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
class Meta:
ordering = ['path']
db_table = 'comment'
verbose_name = u'Комментарий'
verbose_name_plural = u'Комментарии'
def __init__(self, *args, **kwargs):
self.parent_id = None
super(Comment,self).__init__(*args, **kwargs)
@property
def level(self):
return max(0,len(self.path)/8-1)
@property
def html_level(self):
return self.level * 3;
def get_absolute_url(self):
return '%s#comment%s' % (self.content_object.get_absolute_url() , self.id)
def __unicode__(self):
return self.text
def save(self):
super(Comment,self).save()
if not self.path:
if self.parent_id:
try:
parent_path = Comment.objects.get(pk=self.parent_id).path
except:
parent_path = ''
else:
parent_path = ''
self.path = '%s%08d' % (parent_path, self.id)
super(Comment,self).save()
В принципе, код не сложный. И думаю, пояснить стоит только два момента:
- вместо путей с разделителями, используются пути состоящие из частей постоянно длины (8 символов), т.е. для нашего примера, путь до вершины 2 будет: 0000000100000002.
- комментарии у нас будут универсальными, т.е. мы сможем их “прикрутить” к любому объекту на сайте. Для этого используем Generic relations, входящие в состав Django.
Теперь смело можно выполнять:
# cd project_path
# python manage.py syncdb
И в нашей базе появится новая таблица (comment).
(Данная статья хоть и расчитана на начинающих программистов на Django, но основ работы с фраймворком не раскрывает, за основами советую обратиться сюда и сюда.)
Форма
И так с моделью все ясно. Перейдем к ее формам ))) Точнее, к форме добавления.
# -*- coding: utf-8 -*-
from django import forms
class CommentForm(forms.Form):
parent_id = forms.IntegerField(widget=forms.HiddenInput(), initial = 0)
message = forms.CharField(widget=forms.Textarea(), label = u'Комментарий')
content_id = forms.IntegerField(widget=forms.HiddenInput())
content_type = forms.IntegerField(widget=forms.HiddenInput())
def save(self, auth_user):
from people.models import People
from comment.models import Comment
from django.contrib.contenttypes.models import ContentType
if self.is_valid() and auth_user.is_authenticated:
content_id = self.cleaned_data.get('content_id')
content_type = self.cleaned_data.get('content_type')
parent_id = self.cleaned_data.get('parent_id')
message = self.cleaned_data.get('message')
object = ContentType.objects.get(pk=content_type).get_object_for_this_type(pk=content_id)
c = Comment(
text = message,
user = People.objects.get(username = auth_user),
content_object = object
)
c.parent_id = parent_id
c.save()
return c
else:
return False
Данный код строит и при необходимости сохраняет форму добавления комментария.
Т.к. обсуждения мы планируем сделать универсальными, соответственно вызовов такой формы на сайте может быть очень много и дабы избежать дублирования кода, для вставки формы в шаблон сделаем TemplateTag:
# -*- coding: utf-8 -*-
from django import template
from comment.forms import CommentForm
register = template.Library()
@register.inclusion_tag('comments.html')
def tree_comments_by_object(object):
from django.contrib.contenttypes.models import ContentType
comments = object.comments.select_related().all()
content_type = ContentType.objects.get_for_model(object)
form = CommentForm(initial={'content_id': object.id, 'content_type': content_type.id})
return {'comments': comments, 'comment_form': form}
И так, пояти все готово ) Осталось только визуальное представление – html+css+js данного тега.
<h2>Комментарии </h2>
<div id="comments_container">
{% for comment in comments %}
<div style="margin: 0 0 20px {{ comment.html_level }}%" id="comment{{ comment.id }}">
{{ comment.user.username }} <small>{{ comment.date|date:"d.m.Y G:i" }}</small>
<a href="{{ comment.get_absolute_url }}">#</a>
<div>{{ comment.text }}</div>
<a href="#" onclick="return reply_comment({{ comment.id }});">ответить</a>
</div>
{% endfor %}
</div>
<h2>Добавить комментарий</h2>
<form method="post" action="/comment/add/" id="comment_add">
<ul>
{{ comment_form.as_ul}}
<li><input type="submit" name="send" value="Отправить" /></li>
</ul>
</form>
Для AJAX отправки данных формы я использую jQuery и плагин jQuery.form. Написание JavaScript-кода я оставляю вам ) Единственно, что скажу – он не должен получится больше 6-10 строк )))
И последний штрих – обработчик запросов от формы – он же контроллер.
Контроллер
Тут вообще комментарии излишни:
# -*- coding: utf-8 -*-
from comment.forms import CommentForm
from tools.ajax import JsonResponse
from datetime import datetime
def add(request):
if request.method == 'POST':
form = CommentForm(request.POST)
comment = form.save(request.user)
result = {'errors': False, 'data': {}}
if comment:
result['data'] = {
'id': comment.id,
'parent_id': comment.parent_id,
'text': comment.text,
'url': comment.get_absolute_url(),
'date': datetime.strftime(comment.date, '%d.%m.%Y %H:%M'),
'level': comment.html_level,
'user': comment.user.username,
}
else:
result['errors'] = True
return JsonResponse(result)
Разве, что пояснения откуда взялся JsonResponse – это небольшой класс, для уменьшения дублирования рутинного кода:
# -*- coding: utf-8 -*-
from django.utils import simplejson
from django.http import HttpResponse
class JsonResponse(HttpResponse):
def __init__(self, data):
HttpResponse.__init__(self,
content = simplejson.dumps(data),
mimetype = 'application/json')
Вот и все. Получилось немного сумбурно, но я думаю если будут вопросы, мы сможем разрешить их при помощи комментариев! )))
Update (6 апреля 2009)
На выходных задумался об удалении дублирования шаблона отдельного комментария. Сейчас у нас он присутствует в шаблоне темплейт-тега и собирается в JS. Что есть не хорошо =) Уберем это дублирование.
Для этого вынесем часть шаблона отвечающую за отображения одного комментария в файл, например, comment_item.html:
<div style="margin: 0 0 20px {{ comment.html_level }}%" id="comment{{ comment.id }}">
<a href="{{ comment.user.get_absolute_url }}">{{ comment.user.username }}</a>
<small>{{ comment.date|date:"d.m.Y G:i" }}</small> <a href="{{ comment.get_absolute_url }}">#</a>
<div>{{ comment.text }}</div>
<a href="#" onclick="return reply_comment({{ comment.id }});">ответить</a>
</div>
В “главном” шаблоне темплейт-тега заменим вхождение этого кода на {% include ”comment_item.html” %}, получим:
<h2>Комментарии </h2>
<div id="comments_container">
{% for comment in comments %}
{% include "comment_item.html" %}
{% endfor %}
</div>
И немного изменим контроллер:
# -*- coding: utf-8 -*-
from comment.forms import CommentForm
from tools.ajax import JsonResponse
from datetime import datetime
from django.template.loader import render_to_string
def add(request):
if request.method == 'POST':
form = CommentForm(request.POST)
comment = form.save(request.user)
result = {'errors': False, 'data': {}}
if comment:
result['data']['parent_id'] = comment.parent_id;
result['data']['html'] = render_to_string('comment_item.html', {'comment': comment})
else:
result['errors'] = True
return JsonResponse(result)
Вот и все. Дублирования больше нет. Меньше кода – меньше ошибок )))
