Now we have comments on our blog. As the number of commenters increases, sometimes commenters need to communicate with each other, and even some comments can be merged into a small whole. Therefore, it is best to have some way of aggregating related comments, where multi-level comments can be very useful.
Multi-level reviews mean that you need to reorganize the model into a tree structure. “Roots” is a first grade comment, and “leaves” is a second. This tutorial will build on the third-party library Django-MPTT to develop multi-level comment capabilities.
The Django-MPTT module contains tree data structures and many methods for querying and modifying tree data.
Anything that requires a tree structure can be constructed using Django-mpTT. Like directories.
** Note: ** This chapter is a lot of new knowledge, please be prepared, be patient to read.
Refactoring model
Now that the tree structure is to be established, the old comment model must be modified.
Install Django-mptt first:
(env) > pip install django-mptt
Copy the code
After the installation is successful, register in the configuration:
my_blog/settings.py
...
INSTALLED_APPS = [
...
'mptt'. ] .Copy the code
You already know how to do this.
Next, modify the comment model:
comment/models.py
...
# django-mptt
from mptt.models import MPTTModel, TreeForeignKey
# Replace models.Model with MPTTModel
class Comment(MPTTModel):.# Add, MPTT tree structure
parent = TreeForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children'
)
# add, record secondary comments to whom, STR
reply_to = models.ForeignKey(
User,
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='replyers'
)
# Replace Meta with MPTTMeta
# class Meta:
# ordering = ('created',)
class MPTTMeta:
order_insertion_by = ['created']...Copy the code
First introduce the MPTT-related module and then change the following positions:
- modelNo longer inheritThe built-in
models.Model
Class, replaced byMPTTModel
So your model automatically has several new fields for the tree algorithm. (Interested readers can view it in SQLiteStudio after migrating the data.) parent
A field must be defined to store the relationship between data. Do not modify it.reply_to
Foreign keys are used for storageBy critics.- will
class Meta
Replace withclass MPTTMeta
This is the default definition of the module. The actual function is the same.
Most of these changes are the default Settings for the Django-MPTT document. The reply_to should be explained.
Let’s start by thinking about whether multilevel reviews allow infinite progression. Infinite series sounds nice, but too many nested levels can lead to a messy structure and make typography difficult. So this limits comments to a maximum of two levels, resets comments above two levels to two levels, and then stores the actual comments in the Reply_TO field.
For example, the first level reviewer is A, the second level reviewer is B (parent is A), and the third level reviewer is C (parent is B). Since we don’t allow comments to be more than two levels, reset c’s parent to A and reply_to to B so that the true commenter can be traced back correctly.
The model has been modified and many non-empty fields have been added, so it is best to clear all comment data before migrating it.
Do not panic when the following prompts appear during migration, always select the first item, fill in the data 0 can be ok:
(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
Copy the code
If not, delete the database files and relocate them. It’s okay to be a little stupid in development.
Data migration remains the same:
(env) > python manage.py makemigrations
(env) > python manage.py migrate
Copy the code
And we’re done.
view
We’ve already written a view, post_comment, for handling comments in the previous section, and we’ll reuse it to simplify the code.
Major changes, the code is posted, please compare the changes:
comment/views.py
...
# Remember to introduce Comment!
from .models import Comment
...
@login_required(login_url='/userprofile/login/')
Parent_comment_id is added
def post_comment(request, article_id, parent_comment_id=None):
article = get_object_or_404(ArticlePost, id=article_id)
# handle POST requests
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
# Secondary reply
if parent_comment_id:
parent_comment = Comment.objects.get(id=parent_comment_id)
If the response level exceeds 2, convert to 2
new_comment.parent_id = parent_comment.get_root().id
# Respondent
new_comment.reply_to = parent_comment.user
new_comment.save()
return HttpResponse('200 OK')
new_comment.save()
return redirect(article)
else:
return HttpResponse("There is something wrong with the form. Please fill it out again.")
# handle GET requests
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)
# Handle other requests
else:
return HttpResponse("Only GET/POST requests are accepted.")
Copy the code
There are three main changes:
- View parametersnewthe
parent_comment_id=None
. This parameter representsThe parent commenttheid
If a value, forNone
If there is a specific value, it is a multi-level comment. - If the view handles multi-level comments
MPTT
theget_root()
Methods theThe parent is reset to the lowest level comment in the treeAnd then inreply_to
Save and save the actual respondent. The view ultimately returnsHttpResponse
String, which we’ll use later. - The new treatment
GET
Request logic forProvide a blank form for secondary responses. We’ll use it later.
Well, there is now a parent_comment_id parameter in the view to distinguish between multi-level comments, so some urls pass this parameter and others don’t, as follows:
comment/urls.py
...
urlpatterns = [
# Existing code to handle level 1 reply
path('post-comment/<int:article_id>', views.post_comment, name='post_comment'),
# add code to handle secondary reply
path('post-comment/<int:article_id>/<int:parent_comment_id>', views.post_comment, name='comment_reply')]Copy the code
Both paths use the same view function, but the number of arguments passed is different. Look carefully. The first path does not have the parent_comment_id parameter, so the view uses the default value None to separate the comment hierarchy.
The front-end rendering
In terms of front-end logic, our ideal is full:
- Secondary replies also use a rich text editor
- Do not leave the current page when replying
- There should be no performance issues when loading multiple Ckeditors
However, the fuller the ideal, the more painful the code.
The first is that the detail.html code will be overwritten, focusing on displaying the comments section and the associated JavaScript.
Post all changes that need to be made first:
templates/article/detail.html
...
<! -- Change the display comment section -->
<! -- Don't forget load MPtt_tags! -->
{% load mptt_tags %}
<h4>There are {{comments.count}} comments</h4>
<div class="row">
<! -- Traversal the tree structure -->
{% recursetree comments %}
<! 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 -->
<button type="button"
class="btn btn-light btn-sm text-muted"
onclick="load_modal({{ article.id }}, {{ comment.id }})"
>reply</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 %} ...<! -- Add code to wake up secondary modal reply -->
<script>
/ / load modal
function load_modal(article_id, comment_id) {
let modal_body = '#modal_body_' + comment_id;
let modal_id = '#comment_' + comment_id;
// Load the editor
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 %}
Copy the code
Such a long paragraph must confuse you, don’t worry, let’s break it down to explain.
Traverse the tree
First question, how do you traverse a tree?
Django-mptt provides a shortcut:
{% 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>
Copy the code
You don’t have to worry about the internal implementation, just use it as a black box. Objs is the data set that needs to be traversed, and Node is the individual data in it. There are two things to note:
{% load mptt_tags %}
Don’t forget to writenode
This variable name is too broad{% with comment=node %}
I gave it a different name
Modal
Modal is a popover built into Bootstrap. The relevant codes of this article are as follows:
<! -- Modal button -->
<button type="button"
class="btn btn-light btn-sm text-muted"
onclick="load_modal({{ article.id }}, {{ comment.id }})"
>reply</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>
Copy the code
It was almost copied from the official Bootstrap documentation (so check out the official website). A little bit different is that instead of using a native button, this article uses JavaScript loaded Modal; Also added several container ID attributes to facilitate later JavaScript queries.
Compared to the layer. js used in the previous section, Bootstrap’s popover is a bit heavier and more refined, which makes it suitable for use here.
Load the Modal
Perhaps the hardest thing to understand is this JavaScript code that loads Modal:
/ / load modal
function load_modal(article_id, comment_id) {
let modal_body = '#modal_body_' + comment_id;
let modal_id = '#comment_' + comment_id;
// Load the editor
if ($(modal_body).children().length === 0) {
let content = '<iframe src="/comment/post-comment/' +
article_id +
'/' +
comment_id +
'" frameborder="0" style="width: 100%; height: 100%;" >';
$(modal_body).append(content);
};
$(modal_id).modal('show');
}
Copy the code
There are really only three steps to the core logic:
- Woke up when you hit the reply button
load_modal()
Function and pass in the article ID and parent comment ID $(modal_body).append(content)
Find the container that corresponds to Modal and put oneiframe
Containers are added dynamically$(modal_id).modal('show')
Find the corresponding Modal and wake it up
Why does iframe need to be loaded dynamically? This is to avoid potential performance problems. It’s true that you can render all iframes when the page is initially loaded, but this takes extra time, and most Modal users don’t use it at all, so it’s not cost-effective.
The purpose of the if statement is to determine in Modal if it’s already loaded, it shouldn’t be loaded again.
Finally, what is an iframe? This is a new feature in HTML5 and can be thought of as a separate web page nested within the current web page. Since it is an independent page, it will naturally be independent to the background request data. If you look closely at the requested location in SRC, it’s the second path we wrote earlier in urls.py. This corresponds to the GET logic in the post_comment view:
comment/views.py
def post_comment(request, article_id, parent_comment_id=None):.# handle GET requests
elif request.method == 'GET':...return render(request, 'comment/reply.html', context)
...
Copy the code
The comment/reply.html template returned by the view has not been written yet, so let’s write it.
To be honest, loading the Ckeditor popover with iframe isn’t very “elegant”. The blogger failed to successfully load, value and upload multiple Ckeditor on a single page. Interested readers can talk to me.
Ajax submission form
Create a new comment directory in templates, create reply.html, and write:
templates/comment/reply.html
<! Load static file -->
{% load staticfiles %}
<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>
<! -- Submit button -->
<button onclick="confirm_submit({{ article_id }}, {{ parent_comment_id }})" class="btn btn-primary">send</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){
// From ckeditor
let content = CKEDITOR.instances['id_body'].getData();
// Call Ajax to exchange data with the back end
$.ajax({
url: '/comment/post-comment/' + article_id + '/' + comment_id,
type: 'POST'.data: {body: content},
// Successful callback
success: function(e){
if(e === '200 OK'){ parent.location.reload(); }}})}</script>
</body>
</html>
Copy the code
The purpose of this template is to provide a Ckeditor editor, so it does not inherit base.html. Let’s break it down.
Ajax is what
Submitting forms using Ajax techniques is very different from traditional methods.
The traditional way to submit a form is to submit a request to the back end. The back end processes the request and returns a brand new web page. This wastes a lot of bandwidth because most of the content on the first and second pages is the same. In contrast, AJAX techniques can simply send and get back the necessary data to the server and use JavaScript on the client side to handle the response from the server. Because there is a lot less data being exchanged between the server and the browser, the server responds faster.
Although this tutorial only scratches the surface of Ajax, it is widely used, and I encourage you to learn more about it.
Ajax is used here, not because it’s efficient, but because Ajax can get feedback after a successful form submission so that the page can be refreshed.
The core code is as follows:
function confirm_submit(article_id, comment_id){
// From ckeditor
let content = CKEDITOR.instances['id_body'].getData();
// Call Ajax to exchange data with the back end
$.ajax({
url: '/comment/post-comment/' + article_id + '/' + comment_id,
type: 'POST'.data: {body: content},
// Successful callback
success: function(e){
if(e === '200 OK'){ parent.location.reload(); }}})}Copy the code
- CKEDITORIs a global variable provided by the editor, used here
CKEDITOR.instances['id_body'].getData()
Gets the user input in the current editor. - Next, Jquery’s Ajax methods are called to exchange data with the view. Ajax defines the URL of the view, the method of the request, and the submitted data.
success
Is an Ajax callback function. The internal function is executed when the corresponding view is obtained.
When the view was written, the secondary comment returned 200 OK, and when the callback received this signal, it called the reload() method to refresh the parent page (the page where the article was), updating the data.
CSRF problem
The code has this line:
<script src="{% static 'csrf.js' %}"></script>
Copy the code
Without this line, the back end returns a 403 Forbidden error and the form submission fails.
Remember the {% cSRF_Token %} when you submitted the traditional form earlier? To prevent cross-domain attacks, Django requires forms to provide this token to verify the identity of the submitter.
The question is how do you solve this problem in Ajax? One way to do this is to insert the csrf.js module into the page.
You can solve this problem by pasting in the csrf.js file in the static directory and referencing it in the page.
The csrf.js file can be downloaded from my GitHub repository.
The test!
Enter the article page, there is a button next to the comment, you can comment on the reviewer:
Click the Reply button to pop up a popup with a rich text editor:
Click the send button and the page automatically refreshes and secondary comments appear:
You can continue commenting on secondary reviewers, but more advanced comments are forced to secondary comments:
The function is working properly.
Interested readers can open up SQLiteStudio and explore the structure of the comment data table.
conclusion
Give yourself a round of applause for reading this chapter and implementing multi-level reviews. This is probably the most knowledgeable and complex chapter of the tutorial so far, covering MTV, Jquery, Ajax, IFrame, Modal, and many other front-end technologies.
Don’t be impatient if you don’t succeed. For Web development, detours are normal. Keep an eye out for Django and console error messages to find problems and fix them.
- If you have any questions please leave a message on Doucet’s personal website and I will reply as soon as possible.
- Or Email me a private message: [email protected]
- Project code: Django_blog_tutorial