第 14 篇:交流的橋樑“評論功能”——HelloDjango 系列教程
截止到目前為止我們的 django blog 文章展示部分,已經實現的“八九不離十”了。你以為本系列文章就要結束了嗎?不能夠!新的征程才剛剛開始,HelloDjango 系列文章剛剛過半,後面的文章你將接觸更多部落格系統的細節。向著一個小而全的部落格系統前進、前進、前進,你定會收穫頗多。
今天我們就來開啟部落格的評論功能,建起和讀者的溝通橋樑。
建立評論應用
相對來說,評論是另外一個比較獨立的功能。Django 提倡,如果功能相對比較獨立的話,最好是建立一個應用,把相應的功能程式碼組織到這個應用裡。我們的第一個應用叫 blog,它裡面放了展示部落格文章列表和詳情等相關功能的程式碼。而這裡我們再建立一個應用,名為 comments 這裡面將存放和評論功能相關的程式碼。首先進入到專案根目錄,然後輸入如下命令建立一個新的應用:
> pipenv run python manage.py startapp comments
可以看到生成的 comments 應用目錄結構和 blog 應用的目錄是類似的(關於建立應用以及應用的目錄結構在 "空空如也"的部落格應用 中已經有過詳細介紹)。
建立新的應用後一定要記得在 settings.py 裡註冊這個應用,django 才知道這是一個應用。
blogproject/settings.py ... INSTALLED_APPS = [ ... 'blog.apps.BlogConfig', # 註冊 blog 應用 'comments.apps.CommentsConfig', # 註冊 comments 應用 ]v ...
注意這裡註冊的是 CommentsConfig
類,在 部落格從“裸奔”到“有面板” 中曾經講過如何對應用做一些初始化配置,例如讓 blog 應用在 django 的 admin 後臺顯示中文名字。這裡也對評論應用做類似的配置:
comments/app.py
from django.apps import AppConfig
class CommentsConfig(AppConfig):
name = 'comments'
verbose_name = '評論'
設計評論的資料庫模型
使用者評論的資料必須被儲存到資料庫裡,以便其他使用者訪問時 django 能從資料庫取回這些資料然後展示給訪問的使用者,因此我們需要為評論設計資料庫模型,這和設計文章、分類、標籤的資料庫模型是一樣的,如果你忘了怎麼做,再回顧一下 建立 Django 部落格的資料庫模型 中的做法。我們的評論模型設計如下(評論模型的程式碼寫在 comments\models.py 裡):
comments/models.py
from django.db import models
from django.utils import timezone
class Comment(models.Model):
name = models.CharField('名字', max_length=50)
email = models.EmailField('郵箱')
url = models.URLField('網址', blank=True)
text = models.TextField('內容')
created_time = models.DateTimeField('建立時間', default=timezone.now)
post = models.ForeignKey('blog.Post', verbose_name='文章', on_delete=models.CASCADE)
class Meta:
verbose_name = '評論'
verbose_name_plural = verbose_name
def __str__(self):
return '{}: {}'.format(self.name, self.text[:20])
評論會儲存評論使用者的 name
(名字)、email
(郵箱)、url
(個人網站,可以為空),使用者發表的內容將存放在 text
欄位裡,created_time
記錄評論時間。最後,這個評論是關聯到某篇文章(Post)的,由於一個評論只能屬於一篇文章,一篇文章可以有多個評論,是一對多的關係,因此這裡我們使用了 ForeignKey
。關於 ForeignKey
我們前面已有介紹,這裡不再贅述。
此外,在 部落格從“裸奔”到“有面板” 中提過,所有模型的欄位都接受一個 verbose_name
引數(大部分是第一個位置引數),django 在根據模型的定義自動生成表單時,會使用這個引數的值作為表單欄位的 label,我們在後面定義的評論表單時會進一步看到其作用。
建立了資料庫模型就要遷移資料庫,遷移資料庫的命令也在前面講過。在專案根目錄下分別執行下面兩條命令:
> pipenv run python manage.py makemigrations
> pipenv run python manage.py migrate
註冊評論模型到 admin
既然已經建立了模型,我們就可以將它註冊到 django admin 後臺,方便管理員使用者對評論進行管理,如何註冊 admin 以及美化在 部落格從“裸奔”到“有面板” 有過詳細介紹,這裡給出相關程式碼:
comments/admin.py
from django.contrib import admin
from .models import Comment
class CommentAdmin(admin.ModelAdmin):
list_display = ['name', 'email', 'url', 'post', 'created_time']
fields = ['name', 'email', 'url', 'text', 'post']
admin.site.register(Comment, CommentAdmin)
設計評論表單
這一節我們將學習一個全新的 django 知識:表單。那麼什麼是表單呢?基本的 HTML 知識告訴我們,在 HTML 文件中這樣的程式碼表示一個表單:
<form action="" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="submit" value="login" />
</form>
為什麼需要表單呢?表單是用來收集並向伺服器提交使用者輸入的資料的。考慮使用者在我們部落格網站上發表評論的過程。當用戶想要發表評論時,他找到我們給他展示的一個評論表單(我們已經看到在文章詳情頁的底部就有一個評論表單,你將看到表單呈現給我們的樣子),然後根據表單的要求填寫相應的資料。之後使用者點選評論按鈕,這些資料就會發送給某個 URL。我們知道每一個 URL 對應著一個 django 的檢視函式,於是 django 呼叫這個檢視函式,我們在檢視函式中寫上處理使用者通過表單提交上來的資料的程式碼,比如驗證資料的合法性並且儲存資料到資料庫中,那麼使用者的評論就被 django 處理了。如果通過表單提交的資料存在錯誤,那麼我們把錯誤資訊返回給使用者,並在前端重新渲染表單,要求使用者根據錯誤資訊修正表單中不符合格式的資料,再重新提交。
django 的表單功能就是幫我們完成上述所說的表單處理邏輯,表單對 django 來說是一個內容豐富的話題,很難通過教程中的這麼一個例子涵蓋其全部用法。因此我們強烈建議你在完成本教程後接下來的學習中仔細閱讀 django 官方文件關於 表單 的介紹,因為表單在 Web 開發中會經常遇到。
下面開始編寫評論表單程式碼。在 comments 目錄下(和 models.py 同級)新建一個 forms.py 檔案,用來存放表單程式碼,我們的表單程式碼如下:
comments/forms.py
from django import forms
from .models import Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['name', 'email', 'url', 'text']
要使用 django 的表單功能,我們首先匯入 forms 模組。django 的表單類必須繼承自 forms.Form
類或者 forms.ModelForm
類。如果表單對應有一個數據庫模型(例如這裡的評論表單對應著評論模型),那麼使用 ModelForm
類會簡單很多,這是 django 為我們提供的方便。之後我們在表單的內部類 Meta
裡指定一些和表單相關的東西。model = Comment
表明這個表單對應的資料庫模型是 Comment
類。fields = ['name', 'email', 'url', 'text']
指定了表單需要顯示的欄位,這裡我們指定了 name、email、url、text 需要顯示。
關於表單進一步的解釋
django 為什麼要給我們提供一個表單類呢?為了便於理解,我們可以把表單和前面講過的 django ORM 系統做類比。回想一下,我們使用資料庫儲存建立的部落格文章,但是從頭到尾沒有寫過任何和資料庫有關的程式碼(要知道資料庫自身也有一門資料庫語言),這是因為 django 的 ORM 系統內部幫我們做了一些事情。我們遵循 django 的規範寫的一些 Python 程式碼,例如建立 Post、Category 類,然後通過執行資料庫遷移命令將這些程式碼反應到資料庫。
django 的表單和這個思想類似,正常的前端表單程式碼應該是和本文開頭所提及的那樣的 HTML 程式碼,但是我們目前並沒有寫這些程式碼,而是寫了一個 CommentForm
這個 Python 類。通過呼叫這個類的一些方法和屬性,django 將自動為我們建立常規的表單程式碼,接下來的教程我們就會看到具體是怎麼做的。
展示評論表單
表單類已經定義完畢,現在的任務是在文章的詳情頁下方將這個表單展現給使用者,使用者便可以通過這個表單填寫評論資料,從而發表評論。
那麼怎麼展現一個表單呢?django 會根據表單類的定義自動生成表單的 HTML 程式碼,我們要做的就是例項化這個表單類,然後將表單的例項傳給模板,讓 django 的模板引擎來渲染這個表單。
那怎麼將表單類的例項傳給模板呢?因為表單出現在文章詳情頁,一種想法是修改文章詳情頁 detail
檢視函式,在這個檢視中例項化一個表單,然後傳遞給模板。然而這樣做的一個缺點就是需要修改 detail
檢視函式的程式碼,而且 detail
檢視函式的作用主要就是處理文章詳情,一個檢視函式最好不要讓它做太多雜七雜八的事情。另外一種想法是使用自定義的模板標籤,我們在 頁面側邊欄:使用自定義模板標籤 中詳細介紹過如何自定義模板標籤來渲染一個區域性的 HTML 頁面,這裡我們使用自定義模板標籤的方法,來渲染表單頁面。
和 blog 應用中定義模板標籤的老套路一樣,首先建立評論應用模板標籤的檔案結構,在 comments 資料夾下新建一個 templatetags 資料夾,然後建立 __init__.py 檔案使其成為一個包,再建立一個 comments_extras.py 檔案用於存放模板標籤的程式碼,檔案結構如下:
...
blog\
comments\
templatetags\
__init__.py
comments_extras.py
...
然後我們定義一個 inclusion_tag
型別的模板標籤,用於渲染評論表單,關於如何定義模板標籤,在 頁面側邊欄:使用自定義模板標籤 中已經有詳細介紹,這裡不再贅述。
from django import template
from ..forms import CommentForm
register = template.Library()
@register.inclusion_tag('comments/inclusions/_form.html', takes_context=True)
def show_comment_form(context, post, form=None):
if form is None:
form = CommentForm()
return {
'form': form,
'post': post,
}
從定義可以看到,show_comment_form
模板標籤使用時會接受一個 post(文章 Post 模型的例項)作為引數,同時也可能傳入一個評論表單 CommentForm 的例項 form,如果沒有接受到評論表單引數,模板標籤就會新建立一個 CommentForm
的例項(一個沒有繫結任何資料的空表單)傳給模板,否則就直接將接受到的評論表單例項直接傳給模板,這主要是為了複用已有的評論表單例項(後面會看到其用法)。
然後在 templates/comments/inclusions 目錄下(沒有就新建)新建一個 _form.html 模板,寫上程式碼:
<form action="{% url 'comments:comment' post.pk %}" method="post" class="comment-form">
{% csrf_token %}
<div class="row">
<div class="col-md-4">
<label for="{{ form.name.id_for_label }}">{{ form.name.label }}:</label>
{{ form.name }}
{{ form.name.errors }}
</div>
<div class="col-md-4">
<label for="{{ form.email.id_for_label }}">{{ form.email.label }}:</label>
{{ form.email }}
{{ form.email.errors }}
</div>
<div class="col-md-4">
<label for="{{ form.url.id_for_label }}">{{ form.url.label }}:</label>
{{ form.url }}
{{ form.url.errors }}
</div>
<div class="col-md-12">
<label for="{{ form.text.id_for_label }}">{{ form.text.label }}:</label>
{{ form.text }}
{{ form.text.errors }}
<button type="submit" class="comment-btn">發表</button>
</div>
</div> <!-- row -->
</form>
這個表單的模板有點複雜,一一講解一下。
首先 HTML 的 form 標籤有 2 個重要的屬性,action
和 method
。action
指定表單內容提交的地址,這裡我們提交給 comments:comment
檢視函式對應的 URL(後面會建立這個檢視函式並繫結對應的 URL),模板標籤 url
的用法在 分類、歸檔和標籤頁 教程中有詳細介紹。method
指定提交表單時的 HTTP 請求型別,一般表單提交都是使用 POST。
然後我們看到 {% csrf_token %}
,這個模板標籤在表單渲染時會自動渲染為一個隱藏型別的 HTML input 控制元件,其值為一個隨機字串,作用主要是為了防護 CSRF(跨站請求偽造)攻擊。{% csrf_token %}
在模板中渲染出來的內容大概如下所示:
<input type="hidden" name="csrfmiddlewaretoken" value="KH9QLnpQPv2IBcv3oLsksJXdcGvKSnC8t0mTfRSeNIlk5T1G1MBEIwVhK4eh6gIZ">
CSRF 攻擊是一種常見的 Web 攻擊手段。攻擊者利用使用者儲存在瀏覽器中的 cookie,向目標網站傳送 HTTP 請求,這樣在目標網站看來,請求來自於使用者,而實際傳送請求的人卻是攻擊者。例如假設我們的部落格支援登入功能(目前沒有),並使用 cookie(或者 session)記錄使用者的登入狀態,且評論表單沒有 csrf token 防護。使用者登入了我們的部落格後,又去訪問了一個小電影網站,小電影網站有一段惡意 JavaScript 指令碼,它讀取使用者的 cookie,並構造了評論表單的資料,然後指令碼使用這個 cookie 向我們的部落格網站傳送一條 POST 請求,django 就會認為這是來自該使用者的評論釋出請求,便會在後臺建立一個該使用者的評論,而這個使用者全程一臉懵逼。
CSRF 的一個防範措施是,對所有訪問網站的使用者頒發一個令牌(token),對於敏感的 HTTP 請求,後臺會校驗此令牌,確保令牌的確是網站頒發給指定使用者的。因此,當用戶訪問別的網站時,雖然攻擊者可以拿到使用者的 cookie,但是無法取得證明身份的令牌,因此發過來的請求便不會被受理。
以上是對 CSRF 攻擊和防護措施的一個簡單介紹,更加詳細的講解請使用搜索引擎搜尋相關資料。
show_comment_form
模板標籤給模板傳遞了一個模板變數 form,它是 CommentForm
的一個例項,表單的欄位 {{ form.name }}
、{{ form.email }}
、{{ form.url }}
等將自動渲染成表單控制元件,例如 <input>
控制元件。
注意到表單的定義中並沒有定義
name
、url
等屬性,那它們是哪裡來的呢?看到CommentForm
中Meta
下的fields
,django 會自動將fields
中宣告的模型欄位設定為表單的屬性。
{{ form.name.errors }}
、{{ form.email.errors }}
等將渲染表單對應欄位的錯誤(如果有的話),例如使用者 email 格式填錯了,那麼 django 會檢查使用者提交的 email 的格式,然後將格式錯誤資訊儲存到 errors
中,模板便將錯誤資訊渲染顯示。
{{ form.xxx.label }}
用來獲取表單的 label,之前說過,django 根據表單對應的模型中欄位的 verbose_name
引數生成。
然後我們就可以在 detail.html 中使用這個模板標籤來渲染表單了,注意在使用前記得先 {% load comment_extras %}
這個模組。而且為了避免可能的報錯,最好重啟一下開發伺服器。
{% extends 'base.html' %}
{% load comment_extras %}
...
<h3>發表評論</h3>
{% show_comment_form post %}
這裡當用戶訪問文章詳情頁面時,我們給他展示一個空表單,所以這裡只傳入了 post 引數需要的值,而沒有傳入 form 引數所需的值。可以看到表單渲染出來的結果了:
評論檢視函式
當用戶提交表單中的資料後,django 需要呼叫相應的檢視函式來處理這些資料,下面開始寫我們檢視函式處理邏輯:
from blog.models import Post
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_POST
from .forms import CommentForm
@require_POST
def comment(request, post_pk):
# 先獲取被評論的文章,因為後面需要把評論和被評論的文章關聯起來。
# 這裡我們使用了 django 提供的一個快捷函式 get_object_or_404,
# 這個函式的作用是當獲取的文章(Post)存在時,則獲取;否則返回 404 頁面給使用者。
post = get_object_or_404(Post, pk=post_pk)
# django 將使用者提交的資料封裝在 request.POST 中,這是一個類字典物件。
# 我們利用這些資料構造了 CommentForm 的例項,這樣就生成了一個綁定了使用者提交資料的表單。
form = CommentForm(request.POST)
# 當呼叫 form.is_valid() 方法時,django 自動幫我們檢查表單的資料是否符合格式要求。
if form.is_valid():
# 檢查到資料是合法的,呼叫表單的 save 方法儲存資料到資料庫,
# commit=False 的作用是僅僅利用表單的資料生成 Comment 模型類的例項,但還不儲存評論資料到資料庫。
comment = form.save(commit=False)
# 將評論和被評論的文章關聯起來。
comment.post = post
# 最終將評論資料儲存進資料庫,呼叫模型例項的 save 方法
comment.save()
# 重定向到 post 的詳情頁,實際上當 redirect 函式接收一個模型的例項時,它會呼叫這個模型例項的 get_absolute_url 方法,
# 然後重定向到 get_absolute_url 方法返回的 URL。
return redirect(post)
# 檢查到資料不合法,我們渲染一個預覽頁面,用於展示表單的錯誤。
# 注意這裡被評論的文章 post 也傳給了模板,因為我們需要根據 post 來生成表單的提交地址。
context = {
'post': post,
'form': form,
}
return render(request, 'comments/preview.html', context=context)
這個評論檢視相比之前的一些檢視複雜了很多,主要是處理評論的過程更加複雜。具體過程在程式碼中已有詳細註釋,這裡僅就檢視中出現了一些新的知識點進行講解。
首先檢視函式被 require_POST
裝飾器裝飾,從裝飾器的名字就可以看出,其作用是限制這個檢視只能通過 POST 請求觸發,因為建立評論需要使用者通過表單提交的資料,而提交表單通常都是限定為 POST 請求,這樣更加安全。
另外我們使用了 redirect
快捷函式。這個函式位於 django.shortcuts 模組中,它的作用是對 HTTP 請求進行重定向(即使用者訪問的是某個 URL,但由於某些原因,伺服器會將使用者重定向到另外的 URL)。redirect
既可以接收一個 URL 作為引數,也可以接收一個模型的例項作為引數(例如這裡的 post)。如果接收一個模型的例項,那麼這個例項必須實現了 get_absolute_url
方法,這樣 redirect
會根據 get_absolute_url
方法返回的 URL 值進行重定向。
如果使用者提交的資料合法,我們就將評論資料儲存到資料庫,否則說明使用者提交的表單包含錯誤,我們將渲染一個 preview.html 頁面,來展示表單中的錯誤,以便使用者修改後重新提交。preview.html 的程式碼如下:
{% extends 'base.html' %}
{% load comment_extras %}
{% block main %}
{% show_comment_form post form %}
{% endblock main %}
這裡還是使用 show_comment_form
模板標籤來展示一個表單,然而不同的是,這裡我們傳入由檢視函式 comment
傳來的綁定了使用者提交的資料的表單例項 form
,而不是渲染一個空表單。因為檢視函式 comment
中的表單例項是綁定了使用者提交的評論資料,以及對資料進行過合法性校驗的表單,因此當 django 渲染這個表單時,會連帶渲染使用者已經填寫的表單資料以及資料不合法的錯誤提示資訊,而不是一個空的表單了。例如下圖,我們提交的資料中 email 格式不合法,表單校驗了資料格式,然後渲染錯誤提示:
繫結 URL
檢視函式需要和 URL 繫結,這裡我們在 comment 應用中再建一個 urls.py 檔案,寫上 URL 模式:
from django.urls import path
from . import views
app_name = 'comments'
urlpatterns = [
path('comment/<int:post_pk>', views.comment, name='comment'),
]
別忘了給這個評論的 URL 模式規定名稱空間,即 app_name = 'comments'
。
最後要在專案的 blogproject 目錄的 urls.py 裡包含 comments\urls.py 這個檔案:
blogproject/urls.py
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'', include('blog.urls')),
url(r'', include('comments.urls')),
]
可以測試一下提交評論的功能了,首先嚐試輸入非法格式的資料,例如將郵箱輸入為 xxx@xxx,那麼評論檢視在校驗表單資料合法性時,發現郵箱格式不符,就會渲染 preview 頁面,展示表單中的錯誤,將郵箱修改為正確的格式後,再次點擊發表,頁面就跳轉到了被評論文章的詳情頁,說明檢視正確執行了儲存表單資料到資料庫的邏輯。
不過這裡有一點不好的地方就是,評論成功後頁面直接跳轉到了被評論文章的詳情頁,沒有任何提示,使用者也不知道評論究竟有沒有真的成功。這裡我們使用 django 自帶的 messages 應用來給使用者傳送評論成功或者失敗的訊息。
傳送評論訊息
django 預設已經為我們做好了 messages 的相關配置,直接用即可。
兩個地方需要傳送訊息,第一個是當評論成功,即評論資料成功儲存到資料庫後,因此在 comment 檢視中加一句。
from django.contrib import messages
if form.is_valid():
...
# 最終將評論資料儲存進資料庫,呼叫模型例項的 save 方法
comment.save()
messages.add_message(request, messages.SUCCESS, '評論發表成功!', extra_tags='success')
return redirect(post)
這裡匯入 django 的 messages 模組,使用 add_message
方法增加了一條訊息,訊息的第一個引數是當前請求,因為當前請求攜帶使用者的 cookie,django 預設將詳細儲存在使用者的 cookie 中。第二個引數是訊息級別,評論發表成功的訊息設定為 messages.SUCCESS,這是 django 已經預設定義好的一個整數,訊息級別也可以自己定義。緊接著傳入訊息的內容,最後 extra_tags
給這條訊息打上額外的標籤,標籤值可以在展示訊息時使用,比如這裡我們會把這個值用在模板中的 HTML 標籤的 class 屬性,增加樣式。
同樣的,如果評論失敗了,也傳送一條訊息:
# 檢查到資料不合法,我們渲染一個預覽頁面,用於展示表單的錯誤。
# 注意這裡被評論的文章 post 也傳給了模板,因為我們需要根據 post 來生成表單的提交地址。
context = {
'post': post,
'form': form,
}
messages.add_message(request, messages.ERROR, '評論發表失敗!請修改表單中的錯誤後重新提交。', extra_tags='danger')
傳送的訊息被快取在 cookie 中,然後我們在模板中獲取顯示即可。顯示訊息比較好的地方是在導航條的下面,我們在模板 base.html 的導航條程式碼下增加如下程式碼:
<header>
...
</header>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span
aria-hidden="true">×</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
這裡 django 會通過全域性上下文自動把 messages
變數傳給模板,這個變數裡儲存我們傳送的訊息內容,然後就是迴圈顯示訊息了。這裡我們使用了 bootstrap 的一個 alert 元件,為其設定不同的 class 會顯示不同的顏色,所以之前新增訊息時傳入的 extra_tags 就派上了用場。比如這裡 alert-{{ message.tags }},當傳入的是 success 時,類名就為 alert-success,這時顯示的訊息背景顏色就是綠色,傳入的是 dangerous,則顯示的就是紅色。
評論釋出成功和失敗的訊息效果如下圖:
顯示評論內容
為了不改動已有的檢視函式的程式碼,評論資料我們也使用自定義的模板標籤來實現。模板標籤程式碼如下:
@register.inclusion_tag('comments/inclusions/_list.html', takes_context=True)
def show_comments(context, post):
comment_list = post.comment_set.all().order_by('-created_time')
comment_count = comment_list.count()
return {
'comment_count': comment_count,
'comment_list': comment_list,
}
我們使用了 post.comment_set.all()
來獲取 post
對應的全部評論。 Comment
和Post
是通過 ForeignKey
關聯的,回顧一下我們當初獲取某個分類 cate
下的全部文章時的程式碼:Post.objects.filter(category=cate)
。這裡 post.comment_set.all()
也等價於 Comment.objects.filter(post=post)
,即根據 post
來過濾該 post
下的全部評論。但既然我們已經有了一個 Post
模型的例項 post
(它對應的是 Post
在資料庫中的一條記錄),那麼獲取和 post
關聯的評論列表有一個簡單方法,即呼叫它的 xxx_set
屬性來獲取一個類似於 objects
的模型管理器,然後呼叫其 all
方法來返回這個 post
關聯的全部評論。 其中 xxx_set
中的 xxx 為關聯模型的類名(小寫)。例如 Post.objects.filter(category=cate)
也可以等價寫為 cate.post_set.all()
。
模板 _list.html 程式碼如下:
<h3>評論列表,共 <span>{{ comment_count }}</span> 條評論</h3>
<ul class="comment-list list-unstyled">
{% for comment in comment_list %}
<li class="comment-item">
<span class="nickname">{{ comment.name }}</span>
<time class="submit-date" datetime="{{ comment.created_time }}">{{ comment.created_time }}</time>
<div class="text">
{{ comment.text|linebreaks }}
</div>
</li>
{% empty %}
暫無評論
{% endfor %}
</ul>
要注意這裡 {{ comment.text|linebreaks }}
中對評論內容使用的過濾器 linebreaks
,瀏覽器會將換行以及連續的多個空格合併為一個空格。如果使用者評論的內容中有換行,瀏覽器會將換行替換為空格,從而顯示的使用者評論內容就會擠成一堆。linebreaks
過濾器預先將換行符替換為 br
HTML 標籤,這樣內容就能換行顯示了。
然後將 detail.html 中此前佔位用的評論模板替換為模板標籤渲染的內容:
<h3>發表評論</h3>
{% show_comment_form post %}
<div class="comment-list-panel">
{% show_comments post %}
</div>
訪問文章詳情頁,可以看到已經發表的評論列表了:
大功告成!
歡迎關注 HelloGitHub 公眾號,獲取更多開源專案的資料和內容