Django создает личный блог: используйте django-mptt для создания многоуровневой функции комментариев.

Django

Теперь в нашем блоге есть функция комментариев. По мере того, как количество комментаторов статьи увеличивается, иногда комментаторам необходимо общаться друг с другом, и даже некоторые комментарии могут быть объединены в небольшое целое. Так что было бы неплохо иметь какой-то способ группировать связанные комментарии вместе, когдаМногоуровневые комментарииЭто очень полезно.

Многоуровневые комментарии означают, что вам нужно реорганизовать модель, какдревовидная структура. «Корни» — это первичные комментарии, а многочисленные «листья» — вторичные комментарии. В этом руководстве будет использоваться сторонняя библиотекаdjango-mpttНа основе этого разработайте многоуровневую функцию комментариев.

Модуль django-mptt содержит древовидные структуры данных и многочисленные методы для запроса и изменения древовидных данных.

Везде, где требуется древовидная структура, ее можно построить с помощью django-mptt. такие как каталоги.

**Примечание: **В этой главе много новых знаний, пожалуйста, будьте готовы и терпеливо читайте.

Рефакторинг модели

Поскольку должна быть установлена ​​древовидная структура, старая модель комментариев должна быть изменена.

Установить первымdjango-mptt:

(env) > pip install django-mptt

После успешной установки в конфигурациирегистр:

my_blog/settings.py

...
INSTALLED_APPS = [
    ...
    'mptt',

    ...
]
...

Вы уже знакомы с ними.

Далее изменитеобзорная модель:

comment/models.py

...
# django-mptt
from mptt.models import MPTTModel, TreeForeignKey

# 替换 models.Model 为 MPTTModel
class Comment(MPTTModel):
    ...
    
    # 新增,mptt树形结构
    parent = TreeForeignKey(
        'self',
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='children'
    )

    # 新增,记录二级评论回复给谁, str
    reply_to = models.ForeignKey(
        User,
        null=True,
        blank=True,
        on_delete=models.CASCADE,
        related_name='replyers'
    )
    
    # 替换 Meta 为 MPTTMeta
    # class Meta:
    #     ordering = ('created',)
    class MPTTMeta:
        order_insertion_by = ['created']

    ...

сначала импортироватьMPTTСвязанные модули, а затем измените следующие позиции:

  • Модельбольше не наследуютВстроенныйmodels.Modelкласс, заменить наMPTTModel, поэтому ваша модель автоматически имеет несколько новых полей для алгоритма дерева. (Заинтересованные читатели могут просмотреть его в SQLiteStudio после переноса данных)
  • parentПоля должны быть определены, использоваться для хранения отношений между данными, не изменять их.
  • reply_toДля хранения внешних ключейчеловек, которого комментируют.
  • Будуclass Metaзаменитьclass MPTTMeta, параметры также имеют небольшие изменения, что является определением модуля по умолчанию, фактическая функция такая же.

Большинство этих изменений являютсядокументация по джанго-mpttнастройка по умолчанию. Следует отметить, что этоreply_to.

Подумайте сначала, допускает ли многоуровневый комментарий неограниченное количество уровней? Бесконечная серия звучит великолепно, но вложение слишком большого количества уровней может привести к загромождению структур и трудностям при наборе текста. Итак, вот оноОграничьте количество комментариев максимум двумя уровнями, комментарии с более чем двумя уровнями будут сброшены до двух уровней, а затем фактическое прокомментированное лицо будет сохранено вreply_toв поле.

Например: рецензент первого уровня — a, рецензент второго уровня — b (родитель — a), а рецензент третьего уровня — c (родитель — b). Поскольку мы не разрешаем комментировать более двух уровней, сбросьте родителя c на a и запишите answer_to на b, чтобы можно было правильно отследить реальный комментарий.

После модификации модели было добавлено много непустых полей, поэтому лучшеСначала удалите все данные комментариев, а затем выполните миграцию данных.

Не паникуйте, если во время миграции появится следующее приглашение, просто выберите первый пункт и заполните данные 0:

(env) > python manage.py makemigrations

You are trying to add a non-nullable field 'level' to comment without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py

Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> 0

Если это по-прежнему не работает, удалите файл базы данных и повторно перенесите его. Не имеет значения, если вы используете глупый метод на этапе разработки.

Миграция данных по-прежнему остается старым правилом:

(env) > python manage.py makemigrations
(env) > python manage.py migrate

Это завершено.

Посмотреть

Представление было написано в предыдущей главеpost_commentдля обработки комментариев мы будеммультиплексэто, чтобы упростить код.

Изменения большие, код весь выложен, пожалуйста, сравните изменения:

comment/views.py

...
# 记得引入 Comment !
from .models import Comment

...
@login_required(login_url='/userprofile/login/')
# 新增参数 parent_comment_id
def post_comment(request, article_id, parent_comment_id=None):
    article = get_object_or_404(ArticlePost, id=article_id)

    # 处理 POST 请求
    if request.method == 'POST':
        comment_form = CommentForm(request.POST)
        if comment_form.is_valid():
            new_comment = comment_form.save(commit=False)
            new_comment.article = article
            new_comment.user = request.user

            # 二级回复
            if parent_comment_id:
                parent_comment = Comment.objects.get(id=parent_comment_id)
                # 若回复层级超过二级,则转换为二级
                new_comment.parent_id = parent_comment.get_root().id
                # 被回复人
                new_comment.reply_to = parent_comment.user
                new_comment.save()
                return HttpResponse('200 OK')

            new_comment.save()
            return redirect(article)
        else:
            return HttpResponse("表单内容有误,请重新填写。")
    # 处理 GET 请求
    elif request.method == 'GET':
        comment_form = CommentForm()
        context = {
            'comment_form': comment_form,
            'article_id': article_id,
            'parent_comment_id': parent_comment_id
        }
        return render(request, 'comment/reply.html', context)
    # 处理其他请求
    else:
        return HttpResponse("仅接受GET/POST请求。")

Есть 3 основных изменения:

  • параметры просмотрановыйохватыватьparent_comment_id=None. Этот параметр представляетродительский комментарийизidзначение, еслиNoneЭто означает, что комментарий является комментарием первого уровня, а при наличии определенного значения — многоуровневым комментарием.
  • Если представление обрабатывает многоуровневые комментарии, используйтеMPTTизget_root()способ сделать этоРодитель сбрасывается на комментарий нижнего уровня в древовидной структуре., затем вreply_toСохраните фактического респондента в и сохраните. То, что наконец возвращает представление,HttpResponseСтрока, которая будет использоваться позже.
  • Добавить обработкуGETЛогика запроса дляПредоставьте пустую форму для дополнительных ответов. будет использоваться позже.

Отлично, теперь естьparent_comment_idПараметр используется для различения многоуровневых комментариев, поэтому требует некоторыхurlПередайте этот параметр, некоторые не передаются, например:

comment/urls.py

...
urlpatterns = [
    # 已有代码,处理一级回复
    path('post-comment/<int:article_id>', views.post_comment, name='post_comment'),
    # 新增代码,处理二级回复
    path('post-comment/<int:article_id>/<int:parent_comment_id>', views.post_comment, name='comment_reply')
]

дваpathвсе использованота самая функция просмотра, но входящийпараметры разные,Смотри внимательно. Первыйpathнетparent_comment_idПараметры, поэтому представление используется.Значение по умолчаниюNone, чтобы достичь цели различения уровней комментариев.

внешний рендеринг

В логике внешнего интерфейса наши идеалы очень полны:

  • Вторичные ответы также используют редактор форматированного текста.
  • Не могу покинуть текущую страницу при ответе
  • Когда загружено несколько ckeditors, проблем с производительностью быть не может.

Однако чем богаче идеал, тем больнее писать код.

Первыйdetail.htmlКод должен быть сильно изменен, в основном с упором наПоказать раздел комментариеви связанные с нимиJavaScript.

Сначала публикуются все места, которые необходимо изменить:

templates/article/detail.html

...

<!-- 改动 显示评论 部分 -->
<!-- 不要漏了 load mptt_tags! -->
{% load mptt_tags %}
<h4>共有{{ comments.count }}条评论</h4>
<div class="row">
    <!-- 遍历树形结构 -->
    {% recursetree comments %}
        <!-- 给 node 取个别名 comment -->
        {% with comment=node %}
            <div class="{% if comment.reply_to %}
                        offset-1 col-11
                        {% else %}
                        col-12
                        {% endif %}"
            >
                <hr>
                <p>
                    <strong style="color: pink">
                        {{ comment.user }}
                    </strong> 

                    {% if comment.reply_to %}
                        <i class="far fa-arrow-alt-circle-right" 
                           style="color: cornflowerblue;"
                        ></i>
                        <strong style="color: pink">
                            {{ comment.reply_to }}
                        </strong> 
                    {% endif %}

                </p>
                <div>{{ comment.body|safe }}</div>

                <div>
                    <span style="color: gray">
                        {{ comment.created|date:"Y-m-d H:i" }}
                    </span>

                    <!-- modal 按钮 -->
                    <button type="button" 
                            class="btn btn-light btn-sm text-muted" 
                            onclick="load_modal({{ article.id }}, {{ comment.id }})"
                    >
                        回复
                    </button>
                </div>

                <!-- Modal -->
                <div class="modal fade" 
                     id="comment_{{ comment.id }}" 
                     tabindex="-1" 
                     role="dialog" 
                     aria-labelledby="CommentModalCenter" 
                     aria-hidden="true"
                >
                    <div class="modal-dialog modal-dialog-centered modal-lg" role="document">
                        <div class="modal-content" style="height: 480px">
                            <div class="modal-header">
                                <h5 class="modal-title" id="exampleModalCenterTitle">回复 {{ comment.user }}:</h5>
                            </div>
                            <div class="modal-body" id="modal_body_{{ comment.id }}"></div>
                        </div>

                    </div>
                </div>

                {% if not comment.is_leaf_node %}
                    <div class="children">
                        {{ children }}
                    </div>
                {% endif %}
            </div>
            

        {% endwith %}
    {% endrecursetree %}
</div>

...

{% block script %}
...

<!-- 新增代码,唤醒二级回复的 modal -->
<script>
    // 加载 modal
    function load_modal(article_id, comment_id) {
        let modal_body = '#modal_body_' + comment_id;
        let modal_id = '#comment_' + comment_id;
        
        // 加载编辑器
        if ($(modal_body).children().length === 0) {
            let content = '<iframe src="/comment/post-comment/' + 
                article_id + 
                '/' + 
                comment_id + 
                '"' + 
                ' frameborder="0" style="width: 100%; height: 100%;" id="iframe_' + 
                comment_id + 
                '"></iframe>';
            $(modal_body).append(content);
        };

        $(modal_id).modal('show');
    }
</script>
{% endblock script %}

Такой длинный абзац определенно вызовет у вас головокружение, не волнуйтесь, давайте разберем его, чтобы объяснить.

пройти через дерево

Первый вопрос, как пройти по древовидной структуре?

django-mptt предоставляет ярлык:

{% load mptt_tags %}
<ul>
    {% recursetree objs %}
        <li>
            {{ node.your_field }}
            {% if not node.is_leaf_node %}
                <ul class="children">
                    {{ children }}
                </ul>
            {% endif %}
        </li>
    {% endrecursetree %}
</ul>

Вам не нужно беспокоиться о внутренней реализации, просто используйте ее как черный ящик.objsнеобходимо пройтинабор данных,nodeодин изотдельные данные. Следует отметить два места:

  • {% load mptt_tags %}не забудь написать
  • nodeИмя переменной слишком широкое, используйте{% with comment=node %}дать ему псевдоним

Modal

ModalдаBootstrapВстроенное всплывающее окно. Соответствующий код этой статьи выглядит следующим образом:

<!-- modal 按钮 -->
<button type="button" 
        class="btn btn-light btn-sm text-muted" 
        onclick="load_modal({{ article.id }}, {{ comment.id }})"
>
    回复
</button>

<!-- Modal -->
<div class="modal fade" 
     id="comment_{{ comment.id }}" 
     tabindex="-1" 
     role="dialog" 
     aria-labelledby="CommentModalCenter" 
     aria-hidden="true"
     >
    <div class="modal-dialog modal-dialog-centered modal-lg" role="document">
        <div class="modal-content" style="height: 480px">
            <div class="modal-header">
                <h5 class="modal-title" id="exampleModalCenterTitle">回复 {{ comment.user }}:</h5>
            </div>
            <div class="modal-body" id="modal_body_{{ comment.id }}"></div>
        </div>
    </div>
</div>

это почти изОфициальная документация по бутстрапуСкопируйте его (чтобы читатели чаще посещали официальный сайт). Немного отличается то, что в этой статье не используются собственные кнопки, а используютсяJavaScriptЗагружен модальный; добавлено несколько контейнеровidатрибут, удобный на потомJavaScriptЗапрос.

и использованный в предыдущих главахLayer.jsв сравнении с,BootstrapВсплывающее окно немного больше и изящнее, что делает его идеальным для использования здесь.

Модальная загрузка

Вероятно, самым сложным для понимания является этот раздел загрузки Modal.JavaScriptкод вверх:

// 加载 modal
function load_modal(article_id, comment_id) {
    let modal_body = '#modal_body_' + comment_id;
    let modal_id = '#comment_' + comment_id;

    // 加载编辑器
    if ($(modal_body).children().length === 0) {
        let content = '<iframe src="/comment/post-comment/' + 
            article_id + 
            '/' + 
            comment_id + 
            '" frameborder="0" style="width: 100%; height: 100%;"></iframe>';
        $(modal_body).append(content);
    };

    $(modal_id).modal('show');
}

На самом деле основная логика состоит всего из 3 шагов:

  • Просыпаться при нажатии кнопки ответаload_modal()и передайте в нее идентификатор статьи и идентификатор родительского комментария.
  • $(modal_body).append(content)Найдите контейнер, соответствующий Modal, и поместитеiframeКонтейнеры добавляются динамически
  • $(modal_id).modal('show')Найдите соответствующий модальный режим и разбудите его.

Зачемiframeнужнодинамическая нагрузка? это дляИзбегайте потенциальных проблем с производительностью. Вы действительно можете поставить всеiframeВсе рендерится хорошо, но требует дополнительного времени, и большинство пользователей Modal вообще не будут его использовать, что нерентабельно.

ifФункция утверждения состоит в том, чтобы судить о том, что если модальное окно уже было загружено, оно больше не будет загружено.

Наконец, что такоеiframe? Это новая функция в HTML5, которую можно понимать какДругая отдельная веб-страница, вложенная в текущую веб-страницу.. Поскольку это независимая веб-страница, она, естественно, такжеСамостоятельно запрашивать данные из фона. Смотри внимательноsrcЗапрашиваемая должность вurls.pyНаписано во второмpath. что соответствуетpost_commentс учетомGETлогика:

comment/views.py

def post_comment(request, article_id, parent_comment_id=None):
    ...
    # 处理 GET 请求
    elif request.method == 'GET':
        ...
        return render(request, 'comment/reply.html', context)
    ...

просмотр возвращенcomment/reply.htmlШаблон еще не написан, так что давайте напишем его дальше.

Честно говоря, использование iframe для загрузки всплывающего окна ckeditor не очень «элегантно». Блогер не смог попробовать динамическую загрузку, получение значений и передачу параметров нескольких ckeditors на одной странице. Заинтересованные читатели могут связаться со мной.

Форма отправки Ajax

существуетtemplatesЧжунсинcommentкаталог и создайте новыйreply.html, напишите код:

templates/comment/reply.html

<!-- 载入静态文件 -->
{% load staticfiles %}

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="{% static 'bootstrap/css/bootstrap.min.css' %}">
</head>

<body>
    <form 
    action="." 
    method="POST"
    id="reply_form" 
    >
        {% csrf_token %}
        <div class="form-group">
            <div id="test">
                {{ comment_form.media }}
                {{ comment_form.body }}
            </div>
        </div>
    </form>
    <!-- 提交按钮 -->
    <button onclick="confirm_submit({{ article_id }}, {{ parent_comment_id }})" class="btn btn-primary">发送</button>

    <script src="{% static 'jquery/jquery-3.3.1.js' %}"></script>
    <script src="{% static 'popper/popper-1.14.4.js' %}"></script>
    <script src="{% static 'bootstrap/js/bootstrap.min.js' %}"></script>

    <!-- csrf token -->
    <script src="{% static 'csrf.js' %}"></script>
    
    <script>
    $(function(){
        $(".django-ckeditor-widget").removeAttr('style');
    });

    function confirm_submit(article_id, comment_id){
        // 从 ckeditor 中取值
        let content = CKEDITOR.instances['id_body'].getData();
        // 调用 ajax 与后端交换数据
        $.ajax({
            url: '/comment/post-comment/' + article_id + '/' + comment_id,
            type: 'POST',
            data: {body: content},
            // 成功回调
            success: function(e){
                if(e === '200 OK'){
                    parent.location.reload();
                }
            }
        })
    }
    </script>

</body>
</html>

Роль этого шаблона заключается в предоставлении редактора ckeditor, поэтому нет наследования.base.html. Давайте сломаем это.

что такое аякс

использоватьТехнология Ajaxподать форму, сильно отличающуюся от традиционного метода.

Традиционный метод отправляет запрос на серверную часть при отправке формы. После того, как бэкэнд обработает запрос, он вернетсовершенно новыйстраница в Интернете. Эта практика тратит много трафика, потому что большая часть контента на двух страницах до и после часто одинакова. Напротив, технология AJAX может только отправлять и возвращать необходимые данные на сервер и использовать их на стороне клиента.JavaScriptОбработать ответ от сервера. Поскольку объем данных, которыми обмениваются сервер и браузер, значительно сокращается, сервер отвечает быстрее.

Хотя в этом руководстве Ajax используется лишь немного, применение Ajax очень обширно, и читателям рекомендуется узнать о нем больше.

Здесь используется Ajax не из-за его высокой эффективности, а потому, что Ajax может получать обратную связь после успешной отправки формы, чтобы можно было обновить страницу.

Основной код выглядит следующим образом:

function confirm_submit(article_id, comment_id){
    // 从 ckeditor 中取值
    let content = CKEDITOR.instances['id_body'].getData();
    // 调用 ajax 与后端交换数据
    $.ajax({
        url: '/comment/post-comment/' + article_id + '/' + comment_id,
        type: 'POST',
        data: {body: content},
        // 成功回调
        success: function(e){
            if(e === '200 OK'){
                parent.location.reload();
            }
        }
    })
}
  • CKEDITORэто глобальная переменная, предоставленная редактором, используемая здесьCKEDITOR.instances['id_body'].getData()Получить содержимое, введенное пользователем в текущем редакторе.
  • Далее вызывается ajax-метод Jquery для обмена данными с представлением. Ajax определяет URL представления, метод запроса и отправленные данные.
  • successэто функция обратного вызова ajax. Когда соответствующее представление получено, выполняется внутренняя функция.

Когда вы пишете представление, вы вернетесь после отправки комментария второго уровня.200 OK, после того как callback-функция получит этот сигнал, она вызоветreload()метод для обновления текущей родительской страницы (то есть страницы, на которой находится статья) для обновления данных.

csrf-проблема

В коде есть строчка:

<script src="{% static 'csrf.js' %}"></script>

Без этой строки бэкенд вернул бы403 Forbiddenошибка, и отправка формы не удалась.

Помните при отправке традиционных форм ранее{% csrf_token %}? Чтобы предотвратить междоменные атаки, Django требует, чтобы форма предоставила этот токен для проверки личности отправителя.

Вопрос в том, как решить эту проблему в Ajax? Один из способов - вставить это на страницуcsrf.jsмодуль.

в статическом каталогеcsrf.jsВставьте файл и укажите его на странице, и эта проблема может быть решена.

Файл csrf.js можно найти по адресуСкачать из моего репозитория GitHub.

тестовое задание!

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

При нажатии на кнопку ответа открывается всплывающее окно с текстовым редактором:

Нажмите кнопку «Отправить», страница автоматически обновится, а также появятся дополнительные комментарии:

По-прежнему можно продолжать комментировать рецензентов второго уровня, но комментарии более высокого уровня будут переведены в комментарии второго уровня:

Функция работает нормально.

Заинтересованные читатели могут открыть SQLiteStudio и изучить структуру таблицы данных комментариев.

Суммировать

Учащиеся, внимательно прочитавшие эту главу и получившие многоуровневые комментарии, могут поаплодировать себе. Эта глава должна быть самой информативной и сложной главой в учебнике, охватывающей различные интерфейсные и серверные технологии, такие как MTV, Jquery, Ajax, iframe и modal.

Не теряйте терпения, если у вас ничего не получится, для веб-разработки нормально идти в обход. Больше наблюдайте за сообщениями об ошибках Django и консоли, найдите проблему и решите ее.


  • Если у вас есть какие-либо вопросы, пожалуйстаЛичный сайт ДьюсиОставьте сообщение, и я отвечу как можно скорее.
  • Или напишите мне: dusaiphoto@foxmail.com
  • Полный код проекта:Django_blog_tutorial