Материализованные дебаты
Программирование 03.04.2009
Интро
Что главное в интернете? Полезная информация и общение. И все в сети строиться вокруг этих 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)
Вот и все. Дублирования больше нет. Меньше кода - меньше ошибок )))








С возвращением!
Добрый день, а почему бы не выложить код отдельным пакаджем на code.google.com или github.com ?
Вот так со временем блог php программиста превратился в блог о Django :-)
@Snowcore
Разве я когда-нибудь говорил, что данный блог о PHP?
Вверху же написано: “Блог о разработке и разработчике” и ни слова о том или ином языке программирования =)
Важно быть программистом. А на каком языке будем писать, это уже вторично.
Я не так давно столкнулся с проблемой, что в случае если JsonResponse возвращает данные с заголовком Content-Type: ‘application/json’, то jquery form плагин не парсит json-ответ, а FF вообще предлагает скачать этот json ответ в виде файла. В итоге поменял Content-Type на ‘text/html’ и все заработало, как привычно.
А с вложенными множествами тебе не встречалась подходящая реализация? Для небольших блогов такой вариант вполне сойдёт, но для больших ресурсов будет уже неприемлем. На Хабре сначала была подобная реализация, потом пришлось переделать на Nested Setss
@ur001
Присмотритесь к django-mptt там есть реализация Nested Sets.
Но для комментов Nested Sets не подходит, комменты добавляются регулярно и слишком часто придется перестраивать дерево.
ИМХО, для комментов либо “материализованные пути”, либо “списки смежности”.
Почему? Вы боитесь что не хватит поля для хранения длинного пути?
Ну, на Хабре сейчас Nested Sets. Тот что приводите вы просто не справлялся с нагрузкой на MySQL. А тонкости я уже не помню, к сожалению. Для своего проекта хочу использовать ваш класс, так как он прост :)
@larin.in а django-mptt спасибо, посмотрю
@ur001
И все же чему там не справляться с нагрузкой? Особенно если приложить к этому memcache?
Конечно, есть вариант что на нескольких миллионах комментариев начнет тормозить построение индекса по полю пути… но до этого еще дожить надо =)
Если будет критика или предложения - буду рад выслушать.
@larin
Боюсь обмануть, честно не помню что именно
А как вы привязываетесь к шаблону через JQuery.form?
Не понял вашего вопроса, к какому шаблону?
В JS приходит уже сформированный html, ни о каких шаблона JS знать не должен. Иначе будет дублирование кода шаблона.
Смотрите внимательнее на код контроллера в конце статьи, под заголовком Update (6 апреля 2009).