Моя библиотека - социальная сеть любителей книг

Интро

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

В принципе, код не сложный. И думаю, пояснить стоит только два момента:

  1. вместо путей с разделителями, используются пути состоящие из частей постоянно длины (8 символов), т.е. для нашего примера, путь до вершины 2 будет: 0000000100000002.
  2. комментарии у нас будут универсальными, т.е. мы сможем их “прикрутить” к любому объекту на сайте. Для этого используем 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 }}&nbsp;&nbsp;<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>
    &nbsp;&nbsp;<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)

Вот и все. Дублирования больше нет. Меньше кода - меньше ошибок )))



Комментарии (13) на запись «Материализованные дебаты»

  1. adw0rd MonsterID Icon adw0rd | 05.04.2009 в 18:19

    С возвращением!

  2. ramusus MonsterID Icon ramusus | 24.04.2009 в 18:02

    Добрый день, а почему бы не выложить код отдельным пакаджем на code.google.com или github.com ?

  3. Snowcore MonsterID Icon Snowcore | 26.05.2009 в 16:08

    Вот так со временем блог php программиста превратился в блог о Django :-)

  4. Larin MonsterID Icon Larin | 26.05.2009 в 16:14

    @Snowcore
    Разве я когда-нибудь говорил, что данный блог о PHP?
    Вверху же написано: “Блог о разработке и разработчике” и ни слова о том или ином языке программирования =)

    Важно быть программистом. А на каком языке будем писать, это уже вторично.

  5. ramusus MonsterID Icon ramusus | 10.07.2009 в 10:54

    Я не так давно столкнулся с проблемой, что в случае если JsonResponse возвращает данные с заголовком Content-Type: ‘application/json’, то jquery form плагин не парсит json-ответ, а FF вообще предлагает скачать этот json ответ в виде файла. В итоге поменял Content-Type на ‘text/html’ и все заработало, как привычно.

  6. ur001 MonsterID Icon ur001 | 28.07.2009 в 17:05

    А с вложенными множествами тебе не встречалась подходящая реализация? Для небольших блогов такой вариант вполне сойдёт, но для больших ресурсов будет уже неприемлем. На Хабре сначала была подобная реализация, потом пришлось переделать на Nested Setss

  7. Larin MonsterID Icon Larin | 28.07.2009 в 17:27

    @ur001
    Присмотритесь к django-mptt там есть реализация Nested Sets.

    Но для комментов Nested Sets не подходит, комменты добавляются регулярно и слишком часто придется перестраивать дерево.

    ИМХО, для комментов либо “материализованные пути”, либо “списки смежности”.

    Для небольших блогов такой вариант вполне сойдёт, но для больших ресурсов будет уже неприемлем.

    Почему? Вы боитесь что не хватит поля для хранения длинного пути?

  8. ur001 MonsterID Icon ur001 | 28.07.2009 в 17:46

    Но для комментов Nested Sets не подходит, комменты добавляются регулярно и слишком часто придется перестраивать дерево.

    Почему? Вы боитесь что не хватит поля для хранения длинного пути?

    Ну, на Хабре сейчас Nested Sets. Тот что приводите вы просто не справлялся с нагрузкой на MySQL. А тонкости я уже не помню, к сожалению. Для своего проекта хочу использовать ваш класс, так как он прост :)

  9. ur001 MonsterID Icon ur001 | 28.07.2009 в 17:48

    @larin.in а django-mptt спасибо, посмотрю

  10. Larin MonsterID Icon Larin | 28.07.2009 в 18:00

    @ur001

    Тот что приводите вы просто не справлялся с нагрузкой на MySQL.

    И все же чему там не справляться с нагрузкой? Особенно если приложить к этому memcache?

    Конечно, есть вариант что на нескольких миллионах комментариев начнет тормозить построение индекса по полю пути… но до этого еще дожить надо =)

    Для своего проекта хочу использовать ваш класс, так как он прост :)

    Если будет критика или предложения - буду рад выслушать.

  11. ur001 MonsterID Icon ur001 | 28.07.2009 в 18:35

    @larin

    И все же чему там не справляться с нагрузкой? Особенно если приложить к этому memcache?

    Конечно, есть вариант что на нескольких миллионах комментариев начнет тормозить построение индекса по полю пути… но до этого еще дожить надо =)

    Боюсь обмануть, честно не помню что именно

  12. сынок MonsterID Icon сынок | 31.08.2010 в 12:57

    А как вы привязываетесь к шаблону через JQuery.form?

  13. Larin MonsterID Icon Larin | 31.08.2010 в 19:18

    Не понял вашего вопроса, к какому шаблону?
    В JS приходит уже сформированный html, ни о каких шаблона JS знать не должен. Иначе будет дублирование кода шаблона.

    Смотрите внимательнее на код контроллера в конце статьи, под заголовком Update (6 апреля 2009).

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


Copyright, 1983 – 2010