者:HelloGitHub-追夢(mèng)人物
截止到目前為止我們的 django blog 文章展示部分,已經(jīng)實(shí)現(xiàn)的“八九不離十”了。你以為本系列文章就要結(jié)束了嗎?不能夠!新的征程才剛剛開始,HelloDjango 系列文章剛剛過半,后面的文章你將接觸更多博客系統(tǒng)的細(xì)節(jié)。向著一個(gè)小而全的博客系統(tǒng)前進(jìn)、前進(jìn)、前進(jìn),你定會(huì)收獲頗多。
今天我們就來開啟博客的評(píng)論功能,建起和讀者的溝通橋梁。
創(chuàng)建評(píng)論應(yīng)用
相對(duì)來說,評(píng)論是另外一個(gè)比較獨(dú)立的功能。Django 提倡,如果功能相對(duì)比較獨(dú)立的話,最好是創(chuàng)建一個(gè)應(yīng)用,把相應(yīng)的功能代碼組織到這個(gè)應(yīng)用里。我們的第一個(gè)應(yīng)用叫 blog,它里面放了展示博客文章列表和詳情等相關(guān)功能的代碼。而這里我們?cè)賱?chuàng)建一個(gè)應(yīng)用,名為 comments 這里面將存放和評(píng)論功能相關(guān)的代碼。首先進(jìn)入到項(xiàng)目根目錄,然后輸入如下命令創(chuàng)建一個(gè)新的應(yīng)用:
> pipenv run python manage.py startapp comments
可以看到生成的 comments 應(yīng)用目錄結(jié)構(gòu)和 blog 應(yīng)用的目錄是類似的(關(guān)于創(chuàng)建應(yīng)用以及應(yīng)用的目錄結(jié)構(gòu)在 ["空空如也"的博客應(yīng)用](https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/60/ ""空空如也"的博客應(yīng)用") 中已經(jīng)有過詳細(xì)介紹)。
創(chuàng)建新的應(yīng)用后一定要記得在 settings.py 里注冊(cè)這個(gè)應(yīng)用,django 才知道這是一個(gè)應(yīng)用。
blogproject/settings.py ? ... INSTALLED_APPS=[ ... 'blog.apps.BlogConfig', # 注冊(cè) blog 應(yīng)用 'comments.apps.CommentsConfig', # 注冊(cè) comments 應(yīng)用 ]v ...
注意這里注冊(cè)的是 CommentsConfig 類,在 博客從“裸奔”到“有皮膚”[1] 中曾經(jīng)講過如何對(duì)應(yīng)用做一些初始化配置,例如讓 blog 應(yīng)用在 django 的 admin 后臺(tái)顯示中文名字。這里也對(duì)評(píng)論應(yīng)用做類似的配置:
comments/app.py ? from django.apps import AppConfig ? ? class CommentsConfig(AppConfig): name='comments' verbose_name='評(píng)論'
設(shè)計(jì)評(píng)論的數(shù)據(jù)庫(kù)模型
用戶評(píng)論的數(shù)據(jù)必須被存儲(chǔ)到數(shù)據(jù)庫(kù)里,以便其他用戶訪問時(shí) django 能從數(shù)據(jù)庫(kù)取回這些數(shù)據(jù)然后展示給訪問的用戶,因此我們需要為評(píng)論設(shè)計(jì)數(shù)據(jù)庫(kù)模型,這和設(shè)計(jì)文章、分類、標(biāo)簽的數(shù)據(jù)庫(kù)模型是一樣的,如果你忘了怎么做,再回顧一下 創(chuàng)建 Django 博客的數(shù)據(jù)庫(kù)模型[2] 中的做法。我們的評(píng)論模型設(shè)計(jì)如下(評(píng)論模型的代碼寫在 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('網(wǎng)址', blank=True) text=models.TextField('內(nèi)容') created_time=models.DateTimeField('創(chuàng)建時(shí)間', default=timezone.now) post=models.ForeignKey('blog.Post', verbose_name='文章', on_delete=models.CASCADE) ? class Meta: verbose_name='評(píng)論' verbose_name_plural=verbose_name ? def __str__(self): return '{}: {}'.format(self.name, self.text[:20])
評(píng)論會(huì)保存評(píng)論用戶的 name(名字)、email(郵箱)、url(個(gè)人網(wǎng)站,可以為空),用戶發(fā)表的內(nèi)容將存放在 text 字段里,created_time 記錄評(píng)論時(shí)間。最后,這個(gè)評(píng)論是關(guān)聯(lián)到某篇文章(Post)的,由于一個(gè)評(píng)論只能屬于一篇文章,一篇文章可以有多個(gè)評(píng)論,是一對(duì)多的關(guān)系,因此這里我們使用了 ForeignKey。關(guān)于 ForeignKey 我們前面已有介紹,這里不再贅述。
此外,在 博客從“裸奔”到“有皮膚”[3] 中提過,所有模型的字段都接受一個(gè) verbose_name 參數(shù)(大部分是第一個(gè)位置參數(shù)),django 在根據(jù)模型的定義自動(dòng)生成表單時(shí),會(huì)使用這個(gè)參數(shù)的值作為表單字段的 label,我們?cè)诤竺娑x的評(píng)論表單時(shí)會(huì)進(jìn)一步看到其作用。
創(chuàng)建了數(shù)據(jù)庫(kù)模型就要遷移數(shù)據(jù)庫(kù),遷移數(shù)據(jù)庫(kù)的命令也在前面講過。在項(xiàng)目根目錄下分別運(yùn)行下面兩條命令:
> pipenv run python manage.py makemigrations > pipenv run python manage.py migrate
注冊(cè)評(píng)論模型到 admin
既然已經(jīng)創(chuàng)建了模型,我們就可以將它注冊(cè)到 django admin 后臺(tái),方便管理員用戶對(duì)評(píng)論進(jìn)行管理,如何注冊(cè) admin 以及美化在 博客從“裸奔”到“有皮膚”[4] 有過詳細(xì)介紹,這里給出相關(guān)代碼:
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)
設(shè)計(jì)評(píng)論表單
這一節(jié)我們將學(xué)習(xí)一個(gè)全新的 django 知識(shí):表單。那么什么是表單呢?基本的 HTML 知識(shí)告訴我們,在 HTML 文檔中這樣的代碼表示一個(gè)表單:
<form action="" method="post"> <input type="text" name="username" /> <input type="password" name="password" /> <input type="submit" value="login" /> </form>
為什么需要表單呢?表單是用來收集并向服務(wù)器提交用戶輸入的數(shù)據(jù)的。考慮用戶在我們博客網(wǎng)站上發(fā)表評(píng)論的過程。當(dāng)用戶想要發(fā)表評(píng)論時(shí),他找到我們給他展示的一個(gè)評(píng)論表單(我們已經(jīng)看到在文章詳情頁(yè)的底部就有一個(gè)評(píng)論表單,你將看到表單呈現(xiàn)給我們的樣子),然后根據(jù)表單的要求填寫相應(yīng)的數(shù)據(jù)。之后用戶點(diǎn)擊評(píng)論按鈕,這些數(shù)據(jù)就會(huì)發(fā)送給某個(gè) URL。我們知道每一個(gè) URL 對(duì)應(yīng)著一個(gè) django 的視圖函數(shù),于是 django 調(diào)用這個(gè)視圖函數(shù),我們?cè)谝晥D函數(shù)中寫上處理用戶通過表單提交上來的數(shù)據(jù)的代碼,比如驗(yàn)證數(shù)據(jù)的合法性并且保存數(shù)據(jù)到數(shù)據(jù)庫(kù)中,那么用戶的評(píng)論就被 django 處理了。如果通過表單提交的數(shù)據(jù)存在錯(cuò)誤,那么我們把錯(cuò)誤信息返回給用戶,并在前端重新渲染表單,要求用戶根據(jù)錯(cuò)誤信息修正表單中不符合格式的數(shù)據(jù),再重新提交。
django 的表單功能就是幫我們完成上述所說的表單處理邏輯,表單對(duì) django 來說是一個(gè)內(nèi)容豐富的話題,很難通過教程中的這么一個(gè)例子涵蓋其全部用法。因此我們強(qiáng)烈建議你在完成本教程后接下來的學(xué)習(xí)中仔細(xì)閱讀 django 官方文檔關(guān)于 表單[5] 的介紹,因?yàn)楸韱卧?Web 開發(fā)中會(huì)經(jīng)常遇到。
下面開始編寫評(píng)論表單代碼。在 comments\ 目錄下(和 models.py 同級(jí))新建一個(gè) 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 的表單功能,我們首先導(dǎo)入 forms 模塊。django 的表單類必須繼承自 forms.Form 類或者 forms.ModelForm 類。如果表單對(duì)應(yīng)有一個(gè)數(shù)據(jù)庫(kù)模型(例如這里的評(píng)論表單對(duì)應(yīng)著評(píng)論模型),那么使用 ModelForm 類會(huì)簡(jiǎn)單很多,這是 django 為我們提供的方便。之后我們?cè)诒韱蔚膬?nèi)部類 Meta 里指定一些和表單相關(guān)的東西。model=Comment 表明這個(gè)表單對(duì)應(yīng)的數(shù)據(jù)庫(kù)模型是 Comment 類。fields=['name', 'email', 'url', 'text'] 指定了表單需要顯示的字段,這里我們指定了 name、email、url、text 需要顯示。
關(guān)于表單進(jìn)一步的解釋
django 為什么要給我們提供一個(gè)表單類呢?為了便于理解,我們可以把表單和前面講過的 django ORM 系統(tǒng)做類比。回想一下,我們使用數(shù)據(jù)庫(kù)保存創(chuàng)建的博客文章,但是從頭到尾沒有寫過任何和數(shù)據(jù)庫(kù)有關(guān)的代碼(要知道數(shù)據(jù)庫(kù)自身也有一門數(shù)據(jù)庫(kù)語(yǔ)言),這是因?yàn)?django 的 ORM 系統(tǒng)內(nèi)部幫我們做了一些事情。我們遵循 django 的規(guī)范寫的一些 Python 代碼,例如創(chuàng)建 Post、Category 類,然后通過運(yùn)行數(shù)據(jù)庫(kù)遷移命令將這些代碼反應(yīng)到數(shù)據(jù)庫(kù)。
django 的表單和這個(gè)思想類似,正常的前端表單代碼應(yīng)該是和本文開頭所提及的那樣的 HTML 代碼,但是我們目前并沒有寫這些代碼,而是寫了一個(gè) CommentForm 這個(gè) Python 類。通過調(diào)用這個(gè)類的一些方法和屬性,django 將自動(dòng)為我們創(chuàng)建常規(guī)的表單代碼,接下來的教程我們就會(huì)看到具體是怎么做的。
展示評(píng)論表單
表單類已經(jīng)定義完畢,現(xiàn)在的任務(wù)是在文章的詳情頁(yè)下方將這個(gè)表單展現(xiàn)給用戶,用戶便可以通過這個(gè)表單填寫評(píng)論數(shù)據(jù),從而發(fā)表評(píng)論。
那么怎么展現(xiàn)一個(gè)表單呢?django 會(huì)根據(jù)表單類的定義自動(dòng)生成表單的 HTML 代碼,我們要做的就是實(shí)例化這個(gè)表單類,然后將表單的實(shí)例傳給模板,讓 django 的模板引擎來渲染這個(gè)表單。
那怎么將表單類的實(shí)例傳給模板呢?因?yàn)楸韱纬霈F(xiàn)在文章詳情頁(yè),一種想法是修改文章詳情頁(yè) detail 視圖函數(shù),在這個(gè)視圖中實(shí)例化一個(gè)表單,然后傳遞給模板。然而這樣做的一個(gè)缺點(diǎn)就是需要修改 detail 視圖函數(shù)的代碼,而且 detail 視圖函數(shù)的作用主要就是處理文章詳情,一個(gè)視圖函數(shù)最好不要讓它做太多雜七雜八的事情。另外一種想法是使用自定義的模板標(biāo)簽,我們?cè)?頁(yè)面?zhèn)冗厵冢菏褂米远x模板標(biāo)簽[6] 中詳細(xì)介紹過如何自定義模板標(biāo)簽來渲染一個(gè)局部的 HTML 頁(yè)面,這里我們使用自定義模板標(biāo)簽的方法,來渲染表單頁(yè)面。
和 blog 應(yīng)用中定義模板標(biāo)簽的老套路一樣,首先建立評(píng)論應(yīng)用模板標(biāo)簽的文件結(jié)構(gòu),在 comments\ 文件夾下新建一個(gè) templatetags 文件夾,然后創(chuàng)建 __init__.py 文件使其成為一個(gè)包,再創(chuàng)建一個(gè) comments_extras.py 文件用于存放模板標(biāo)簽的代碼,文件結(jié)構(gòu)如下:
... blog\ comments\ templatetags\ __init__.py comments_extras.py ...
然后我們定義一個(gè) inclusion_tag 類型的模板標(biāo)簽,用于渲染評(píng)論表單,關(guān)于如何定義模板標(biāo)簽,在 頁(yè)面?zhèn)冗厵冢菏褂米远x模板標(biāo)簽[7] 中已經(jīng)有詳細(xì)介紹,這里不再贅述。
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 模板標(biāo)簽使用時(shí)會(huì)接受一個(gè) post(文章 Post 模型的實(shí)例)作為參數(shù),同時(shí)也可能傳入一個(gè)評(píng)論表單 CommentForm 的實(shí)例 form,如果沒有接受到評(píng)論表單參數(shù),模板標(biāo)簽就會(huì)新創(chuàng)建一個(gè) CommentForm 的實(shí)例(一個(gè)沒有綁定任何數(shù)據(jù)的空表單)傳給模板,否則就直接將接受到的評(píng)論表單實(shí)例直接傳給模板,這主要是為了復(fù)用已有的評(píng)論表單實(shí)例(后面會(huì)看到其用法)。
然后在 templates/comments/inclusions 目錄下(沒有就新建)新建一個(gè) _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">發(fā)表</button> </div> </div> <!-- row --> </form>
這個(gè)表單的模板有點(diǎn)復(fù)雜,一一講解一下。
首先 HTML 的 form 標(biāo)簽有 2 個(gè)重要的屬性,action 和 method。action 指定表單內(nèi)容提交的地址,這里我們提交給 comments:comment 視圖函數(shù)對(duì)應(yīng)的 URL(后面會(huì)創(chuàng)建這個(gè)視圖函數(shù)并綁定對(duì)應(yīng)的 URL),模板標(biāo)簽 url 的用法在 分類、歸檔和標(biāo)簽頁(yè)[8] 教程中有詳細(xì)介紹。method 指定提交表單時(shí)的 HTTP 請(qǐng)求類型,一般表單提交都是使用 POST。
然后我們看到 {% csrf_token %},這個(gè)模板標(biāo)簽在表單渲染時(shí)會(huì)自動(dòng)渲染為一個(gè)隱藏類型的 HTML input 控件,其值為一個(gè)隨機(jī)字符串,作用主要是為了防護(hù) CSRF(跨站請(qǐng)求偽造)攻擊。{% csrf_token %} 在模板中渲染出來的內(nèi)容大概如下所示:
<input type="hidden" name="csrfmiddlewaretoken" value="KH9QLnpQPv2IBcv3oLsksJXdcGvKSnC8t0mTfRSeNIlk5T1G1MBEIwVhK4eh6gIZ">
CSRF 攻擊是一種常見的 Web 攻擊手段。攻擊者利用用戶存儲(chǔ)在瀏覽器中的 cookie,向目標(biāo)網(wǎng)站發(fā)送 HTTP 請(qǐng)求,這樣在目標(biāo)網(wǎng)站看來,請(qǐng)求來自于用戶,而實(shí)際發(fā)送請(qǐng)求的人卻是攻擊者。例如假設(shè)我們的博客支持登錄功能(目前沒有),并使用 cookie(或者 session)記錄用戶的登錄狀態(tài),且評(píng)論表單沒有 csrf token 防護(hù)。用戶登錄了我們的博客后,又去訪問了一個(gè)小電影網(wǎng)站,小電影網(wǎng)站有一段惡意 JavaScript 腳本,它讀取用戶的 cookie,并構(gòu)造了評(píng)論表單的數(shù)據(jù),然后腳本使用這個(gè) cookie 向我們的博客網(wǎng)站發(fā)送一條 POST 請(qǐng)求,django 就會(huì)認(rèn)為這是來自該用戶的評(píng)論發(fā)布請(qǐng)求,便會(huì)在后臺(tái)創(chuàng)建一個(gè)該用戶的評(píng)論,而這個(gè)用戶全程一臉懵逼。
CSRF 的一個(gè)防范措施是,對(duì)所有訪問網(wǎng)站的用戶頒發(fā)一個(gè)令牌(token),對(duì)于敏感的 HTTP 請(qǐng)求,后臺(tái)會(huì)校驗(yàn)此令牌,確保令牌的確是網(wǎng)站頒發(fā)給指定用戶的。因此,當(dāng)用戶訪問別的網(wǎng)站時(shí),雖然攻擊者可以拿到用戶的 cookie,但是無(wú)法取得證明身份的令牌,因此發(fā)過來的請(qǐng)求便不會(huì)被受理。
以上是對(duì) CSRF 攻擊和防護(hù)措施的一個(gè)簡(jiǎn)單介紹,更加詳細(xì)的講解請(qǐng)使用搜索引擎搜索相關(guān)資料。
show_comment_form 模板標(biāo)簽給模板傳遞了一個(gè)模板變量 form,它是 CommentForm 的一個(gè)實(shí)例,表單的字段 {{ form.name }}、{{ form.email }}、{{ form.url }} 等將自動(dòng)渲染成表單控件,例如 <input> 控件。
注意到表單的定義中并沒有定義 name、email、url 等屬性,那它們是哪里來的呢?看到 CommentForm 中 Meta 下的 fields,django 會(huì)自動(dòng)將 fields 中聲明的模型字段設(shè)置為表單的屬性。
{{ form.name.errors }}、{{ form.email.errors }} 等將渲染表單對(duì)應(yīng)字段的錯(cuò)誤(如果有的話),例如用戶 email 格式填錯(cuò)了,那么 django 會(huì)檢查用戶提交的 email 的格式,然后將格式錯(cuò)誤信息保存到 errors 中,模板便將錯(cuò)誤信息渲染顯示。
{{ form.xxx.label }} 用來獲取表單的 label,之前說過,django 根據(jù)表單對(duì)應(yīng)的模型中字段的 verbose_name 參數(shù)生成。
然后我們就可以在 detail.html 中使用這個(gè)模板標(biāo)簽來渲染表單了,注意在使用前記得先 {% load comment_extras %} 這個(gè)模塊。而且為了避免可能的報(bào)錯(cuò),最好重啟一下開發(fā)服務(wù)器。
{% extends 'base.html' %} {% load comment_extras %} ... ? <h3>發(fā)表評(píng)論</h3> {% show_comment_form post %}
這里當(dāng)用戶訪問文章詳情頁(yè)面時(shí),我們給他展示一個(gè)空表單,所以這里只傳入了 post 參數(shù)需要的值,而沒有傳入 form 參數(shù)所需的值。可以看到表單渲染出來的結(jié)果了:
評(píng)論視圖函數(shù)
當(dāng)用戶提交表單中的數(shù)據(jù)后,django 需要調(diào)用相應(yīng)的視圖函數(shù)來處理這些數(shù)據(jù),下面開始寫我們視圖函數(shù)處理邏輯:
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): # 先獲取被評(píng)論的文章,因?yàn)楹竺嫘枰言u(píng)論和被評(píng)論的文章關(guān)聯(lián)起來。 # 這里我們使用了 django 提供的一個(gè)快捷函數(shù) get_object_or_404, # 這個(gè)函數(shù)的作用是當(dāng)獲取的文章(Post)存在時(shí),則獲取;否則返回 404 頁(yè)面給用戶。 post=get_object_or_404(Post, pk=post_pk) ? # django 將用戶提交的數(shù)據(jù)封裝在 request.POST 中,這是一個(gè)類字典對(duì)象。 # 我們利用這些數(shù)據(jù)構(gòu)造了 CommentForm 的實(shí)例,這樣就生成了一個(gè)綁定了用戶提交數(shù)據(jù)的表單。 form=CommentForm(request.POST) ? # 當(dāng)調(diào)用 form.is_valid() 方法時(shí),django 自動(dòng)幫我們檢查表單的數(shù)據(jù)是否符合格式要求。 if form.is_valid(): # 檢查到數(shù)據(jù)是合法的,調(diào)用表單的 save 方法保存數(shù)據(jù)到數(shù)據(jù)庫(kù), # commit=False 的作用是僅僅利用表單的數(shù)據(jù)生成 Comment 模型類的實(shí)例,但還不保存評(píng)論數(shù)據(jù)到數(shù)據(jù)庫(kù)。 comment=form.save(commit=False) ? # 將評(píng)論和被評(píng)論的文章關(guān)聯(lián)起來。 comment.post=post ? # 最終將評(píng)論數(shù)據(jù)保存進(jìn)數(shù)據(jù)庫(kù),調(diào)用模型實(shí)例的 save 方法 comment.save() ? # 重定向到 post 的詳情頁(yè),實(shí)際上當(dāng) redirect 函數(shù)接收一個(gè)模型的實(shí)例時(shí),它會(huì)調(diào)用這個(gè)模型實(shí)例的 get_absolute_url 方法, # 然后重定向到 get_absolute_url 方法返回的 URL。 return redirect(post) ? # 檢查到數(shù)據(jù)不合法,我們渲染一個(gè)預(yù)覽頁(yè)面,用于展示表單的錯(cuò)誤。 # 注意這里被評(píng)論的文章 post 也傳給了模板,因?yàn)槲覀冃枰鶕?jù) post 來生成表單的提交地址。 context={ 'post': post, 'form': form, } return render(request, 'comments/preview.html', context=context)
這個(gè)評(píng)論視圖相比之前的一些視圖復(fù)雜了很多,主要是處理評(píng)論的過程更加復(fù)雜。具體過程在代碼中已有詳細(xì)注釋,這里僅就視圖中出現(xiàn)了一些新的知識(shí)點(diǎn)進(jìn)行講解。
首先視圖函數(shù)被 require_POST 裝飾器裝飾,從裝飾器的名字就可以看出,其作用是限制這個(gè)視圖只能通過 POST 請(qǐng)求觸發(fā),因?yàn)閯?chuàng)建評(píng)論需要用戶通過表單提交的數(shù)據(jù),而提交表單通常都是限定為 POST 請(qǐng)求,這樣更加安全。
另外我們使用了 redirect 快捷函數(shù)。這個(gè)函數(shù)位于 django.shortcuts 模塊中,它的作用是對(duì) HTTP 請(qǐng)求進(jìn)行重定向(即用戶訪問的是某個(gè) URL,但由于某些原因,服務(wù)器會(huì)將用戶重定向到另外的 URL)。redirect 既可以接收一個(gè) URL 作為參數(shù),也可以接收一個(gè)模型的實(shí)例作為參數(shù)(例如這里的 post)。如果接收一個(gè)模型的實(shí)例,那么這個(gè)實(shí)例必須實(shí)現(xiàn)了 get_absolute_url 方法,這樣 redirect 會(huì)根據(jù) get_absolute_url 方法返回的 URL 值進(jìn)行重定向。
如果用戶提交的數(shù)據(jù)合法,我們就將評(píng)論數(shù)據(jù)保存到數(shù)據(jù)庫(kù),否則說明用戶提交的表單包含錯(cuò)誤,我們將渲染一個(gè) preview.html 頁(yè)面,來展示表單中的錯(cuò)誤,以便用戶修改后重新提交。preview.html 的代碼如下:
{% extends 'base.html' %} {% load comment_extras %} ? {% block main %} {% show_comment_form post form %} {% endblock main %}
這里還是使用 show_comment_form 模板標(biāo)簽來展示一個(gè)表單,然而不同的是,這里我們傳入由視圖函數(shù) comment 傳來的綁定了用戶提交的數(shù)據(jù)的表單實(shí)例 form,而不是渲染一個(gè)空表單。因?yàn)橐晥D函數(shù) comment 中的表單實(shí)例是綁定了用戶提交的評(píng)論數(shù)據(jù),以及對(duì)數(shù)據(jù)進(jìn)行過合法性校驗(yàn)的表單,因此當(dāng) django 渲染這個(gè)表單時(shí),會(huì)連帶渲染用戶已經(jīng)填寫的表單數(shù)據(jù)以及數(shù)據(jù)不合法的錯(cuò)誤提示信息,而不是一個(gè)空的表單了。例如下圖,我們提交的數(shù)據(jù)中 email 格式不合法,表單校驗(yàn)了數(shù)據(jù)格式,然后渲染錯(cuò)誤提示:
綁定 URL
視圖函數(shù)需要和 URL 綁定,這里我們?cè)?comment 應(yīng)用中再建一個(gè) urls.py 文件,寫上 URL 模式:
from django.urls import path ? from . import views ? app_name='comments' urlpatterns=[ path('comment/<int:post_pk>', views.comment, name='comment'), ]
別忘了給這個(gè)評(píng)論的 URL 模式規(guī)定命名空間,即 app_name='comments'。
最后要在項(xiàng)目的 blogproject\ 目錄的 urls.py 里包含 comments\urls.py 這個(gè)文件:
blogproject/urls.py ? urlpatterns=[ url(r'^admin/', admin.site.urls), url(r'', include('blog.urls')), url(r'', include('comments.urls')), ]
可以測(cè)試一下提交評(píng)論的功能了,首先嘗試輸入非法格式的數(shù)據(jù),例如將郵箱輸入為 xxx@xxx,那么評(píng)論視圖在校驗(yàn)表單數(shù)據(jù)合法性時(shí),發(fā)現(xiàn)郵箱格式不符,就會(huì)渲染 preview 頁(yè)面,展示表單中的錯(cuò)誤,將郵箱修改為正確的格式后,再次點(diǎn)擊發(fā)表,頁(yè)面就跳轉(zhuǎn)到了被評(píng)論文章的詳情頁(yè),說明視圖正確執(zhí)行了保存表單數(shù)據(jù)到數(shù)據(jù)庫(kù)的邏輯。
不過這里有一點(diǎn)不好的地方就是,評(píng)論成功后頁(yè)面直接跳轉(zhuǎn)到了被評(píng)論文章的詳情頁(yè),沒有任何提示,用戶也不知道評(píng)論究竟有沒有真的成功。這里我們使用 django 自帶的 messages 應(yīng)用來給用戶發(fā)送評(píng)論成功或者失敗的消息。
發(fā)送評(píng)論消息
django 默認(rèn)已經(jīng)為我們做好了 messages 的相關(guān)配置,直接用即可。
兩個(gè)地方需要發(fā)送消息,第一個(gè)是當(dāng)評(píng)論成功,即評(píng)論數(shù)據(jù)成功保存到數(shù)據(jù)庫(kù)后,因此在 comment 視圖中加一句。
from django.contrib import messages ? if form.is_valid(): ... # 最終將評(píng)論數(shù)據(jù)保存進(jìn)數(shù)據(jù)庫(kù),調(diào)用模型實(shí)例的 save 方法 comment.save() ? messages.add_message(request, messages.SUCCESS, '評(píng)論發(fā)表成功!', extra_tags='success') return redirect(post)
這里導(dǎo)入 django 的 messages 模塊,使用 add_message 方法增加了一條消息,消息的第一個(gè)參數(shù)是當(dāng)前請(qǐng)求,因?yàn)楫?dāng)前請(qǐng)求攜帶用戶的 cookie,django 默認(rèn)將詳細(xì)存儲(chǔ)在用戶的 cookie 中。第二個(gè)參數(shù)是消息級(jí)別,評(píng)論發(fā)表成功的消息設(shè)置為 messages.SUCCESS,這是 django 已經(jīng)默認(rèn)定義好的一個(gè)整數(shù),消息級(jí)別也可以自己定義。緊接著傳入消息的內(nèi)容,最后 extra_tags 給這條消息打上額外的標(biāo)簽,標(biāo)簽值可以在展示消息時(shí)使用,比如這里我們會(huì)把這個(gè)值用在模板中的 HTML 標(biāo)簽的 class 屬性,增加樣式。
同樣的,如果評(píng)論失敗了,也發(fā)送一條消息:
# 檢查到數(shù)據(jù)不合法,我們渲染一個(gè)預(yù)覽頁(yè)面,用于展示表單的錯(cuò)誤。 # 注意這里被評(píng)論的文章 post 也傳給了模板,因?yàn)槲覀冃枰鶕?jù) post 來生成表單的提交地址。 context={ 'post': post, 'form': form, } messages.add_message(request, messages.ERROR, '評(píng)論發(fā)表失敗!請(qǐng)修改表單中的錯(cuò)誤后重新提交。', extra_tags='danger')
發(fā)送的消息被緩存在 cookie 中,然后我們?cè)谀0逯蝎@取顯示即可。顯示消息比較好的地方是在導(dǎo)航條的下面,我們?cè)谀0?base.html 的導(dǎo)航條代碼下增加如下代碼:
<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 會(huì)通過全局上下文自動(dòng)把 messages 變量傳給模板,這個(gè)變量里存儲(chǔ)我們發(fā)送的消息內(nèi)容,然后就是循環(huán)顯示消息了。這里我們使用了 bootstrap 的一個(gè) alert 組件,為其設(shè)置不同的 class 會(huì)顯示不同的顏色,所以之前添加消息時(shí)傳入的 extra_tags 就派上了用場(chǎng)。比如這里 alert-{{ message.tags }},當(dāng)傳入的是 success 時(shí),類名就為 alert-success,這時(shí)顯示的消息背景顏色就是綠色,傳入的是 dangerous,則顯示的就是紅色。
評(píng)論發(fā)布成功和失敗的消息效果如下圖:
顯示評(píng)論內(nèi)容
為了不改動(dòng)已有的視圖函數(shù)的代碼,評(píng)論數(shù)據(jù)我們也使用自定義的模板標(biāo)簽來實(shí)現(xiàn)。模板標(biāo)簽代碼如下:
@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 對(duì)應(yīng)的全部評(píng)論。Comment 和Post 是通過 ForeignKey 關(guān)聯(lián)的,回顧一下我們當(dāng)初獲取某個(gè)分類 cate 下的全部文章時(shí)的代碼:Post.objects.filter(category=cate)。這里 post.comment_set.all() 也等價(jià)于 Comment.objects.filter(post=post),即根據(jù) post 來過濾該 post 下的全部評(píng)論。但既然我們已經(jīng)有了一個(gè) Post 模型的實(shí)例 post(它對(duì)應(yīng)的是 Post 在數(shù)據(jù)庫(kù)中的一條記錄),那么獲取和 post 關(guān)聯(lián)的評(píng)論列表有一個(gè)簡(jiǎn)單方法,即調(diào)用它的 xxx_set 屬性來獲取一個(gè)類似于 objects 的模型管理器,然后調(diào)用其 all 方法來返回這個(gè) post 關(guān)聯(lián)的全部評(píng)論。其中 xxx_set 中的 xxx 為關(guān)聯(lián)模型的類名(小寫)。例如 Post.objects.filter(category=cate) 也可以等價(jià)寫為 cate.post_set.all()。
模板 _list.html 代碼如下:
<h3>評(píng)論列表,共 <span>{{ comment_count }}</span> 條評(píng)論</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 %} 暫無(wú)評(píng)論 {% endfor %} </ul>
要注意這里 {{ comment.text|linebreaks }} 中對(duì)評(píng)論內(nèi)容使用的過濾器 linebreaks,瀏覽器會(huì)將換行以及連續(xù)的多個(gè)空格合并為一個(gè)空格。如果用戶評(píng)論的內(nèi)容中有換行,瀏覽器會(huì)將換行替換為空格,從而顯示的用戶評(píng)論內(nèi)容就會(huì)擠成一堆。linebreaks 過濾器預(yù)先將換行符替換為 br HTML 標(biāo)簽,這樣內(nèi)容就能換行顯示了。
然后將 detail.html 中此前占位用的評(píng)論模板替換為模板標(biāo)簽渲染的內(nèi)容:
<h3>發(fā)表評(píng)論</h3> {% show_comment_form post %} <div class="comment-list-panel"> {% show_comments post %} </div>
訪問文章詳情頁(yè),可以看到已經(jīng)發(fā)表的評(píng)論列表了:
大功告成!
References
[1] 博客從“裸奔”到“有皮膚”: https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/64/
[2] 創(chuàng)建 Django 博客的數(shù)據(jù)庫(kù)模型: https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/61/
[3] 博客從“裸奔”到“有皮膚”: https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/64/
[4] 博客從“裸奔”到“有皮膚”: https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/64/
[5] 表單: https://docs.djangoproject.com/en/2.2/topics/forms/
[6] 頁(yè)面?zhèn)冗厵冢菏褂米远x模板標(biāo)簽: https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/70/
[7]頁(yè)面?zhèn)冗厵冢菏褂米远x模板標(biāo)簽: https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/70/
[8] 分類、歸檔和標(biāo)簽頁(yè): https://www.zmrenwu.com/courses/hellodjango-blog-tutorial/materials/71/
『講解開源項(xiàng)目系列』——讓對(duì)開源項(xiàng)目感興趣的人不再畏懼、讓開源項(xiàng)目的發(fā)起者不再孤單。跟著我們的文章,你會(huì)發(fā)現(xiàn)編程的樂趣、使用和發(fā)現(xiàn)參與開源項(xiàng)目如此簡(jiǎn)單。歡迎加入我們,讓更多人愛上開源、貢獻(xiàn)開源~
面這種單詞直接打斷換行:
word-break (當(dāng)行尾放不下一個(gè)單詞時(shí),決定單詞內(nèi)部該怎么擺放)
word-wrap(當(dāng)行尾放不下時(shí),決定單詞內(nèi)是否允許換行)
normal 使用瀏覽器默認(rèn)的換行規(guī)則。
break-all 允許在單詞內(nèi)換行(強(qiáng)行上,擠不下的話剩下的就換下一行顯示)。
keep-all 只能在半角空格或連字符處換行(放不下了,那就另起一行展示)。
normal 只在允許的斷字點(diǎn)換行(單詞太長(zhǎng),換行顯示,再超過一行就溢出顯示)。
break-word 在長(zhǎng)單詞或 URL 地址內(nèi)部進(jìn)行換行(當(dāng)單詞太長(zhǎng)時(shí),先嘗試換行,換行后還是太長(zhǎng),單詞內(nèi)還可以換行)。
還有話說!
注意,上面這些換行神馬的都是針對(duì)英文單詞,像CJK(中文/日文/韓文)這樣的語(yǔ)言就算了,因?yàn)樗麄儾恍枰ㄕ娌挥绊戦喿x)。
在來看開頭那張圖里的問題,你加上 word-break: keep-all;word-wrap: break-word;正常就可以了。如有問題,歡迎討論評(píng)論區(qū)或者直接私信。
標(biāo)簽是默認(rèn)是自動(dòng)換行的,因此設(shè)置好寬度之后,能夠較好的實(shí)現(xiàn)效果,但是最近的項(xiàng)目中發(fā)現(xiàn),使用ajax加載數(shù)據(jù)之后,p標(biāo)簽內(nèi)的內(nèi)容沒有換行,導(dǎo)致布局錯(cuò)亂,于是嘗試著使用換行樣式,雖然解決了問題,但是并沒有發(fā)現(xiàn)本質(zhì)原因,本質(zhì)在于,我當(dāng)時(shí)獲取的數(shù)據(jù)是一長(zhǎng)串的數(shù)字,瀏覽器應(yīng)該是對(duì)數(shù)字和英文單詞處理方式相近,不會(huì)截?cái)唷?
先給出各種方式,再具體介紹每一個(gè)屬性。
強(qiáng)制不換行
p { white-space:nowrap; }
自動(dòng)換行
p { word-wrap:break-word; }
強(qiáng)制英文單詞斷行
p { word-break:break-all; }
*注意:設(shè)置強(qiáng)制將英文單詞斷行,需要將行內(nèi)元素設(shè)置為塊級(jí)元素。
超出顯示省略號(hào)
p{text-overflow:ellipsis;overflow:hidden;}
white-space: normal|pre|nowrap|pre-wrap|pre-line|inherit;
white-space 屬性設(shè)置如何處理元素內(nèi)的空白
normal 默認(rèn)。空白會(huì)被瀏覽器忽略。
pre 空白會(huì)被瀏覽器保留。其行為方式類似 HTML 中的 pre 標(biāo)簽。
nowrap 文本不會(huì)換行,文本會(huì)在在同一行上繼續(xù),直到遇到 br 標(biāo)簽為止。
pre-wrap 保留空白符序列,但是正常地進(jìn)行換行。
pre-line 合并空白符序列,但是保留換行符。
inherit 規(guī)定應(yīng)該從父元素繼承 white-space 屬性的值。
word-wrap: normal|break-word;
word-wrap 屬性用來標(biāo)明是否允許瀏覽器在單詞內(nèi)進(jìn)行斷句,這是為了防止當(dāng)一個(gè)字符串太長(zhǎng)而找不到它的自然斷句點(diǎn)時(shí)產(chǎn)生溢出現(xiàn)象。
normal: 只在允許的斷字點(diǎn)換行(瀏覽器保持默認(rèn)處理)
break-word:在長(zhǎng)單詞或URL地址內(nèi)部進(jìn)行換行
word-break: normal|break-all|keep-all;
word-break 屬性用來標(biāo)明怎么樣進(jìn)行單詞內(nèi)的斷句。
normal:使用瀏覽器默認(rèn)的換行規(guī)則。
break-all:允許再單詞內(nèi)換行
keep-all:只能在半角空格或連字符處換行
舉例看起區(qū)別:
<!doctype html>
<html lang="en">
<head>
<!--網(wǎng)站編碼格式,UTF-8 國(guó)際編碼,GBK或 gb2312 中文編碼-->
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<meta name="Keywords" content="關(guān)鍵詞一,關(guān)鍵詞二">
<meta name="Description" content="網(wǎng)站描述內(nèi)容">
<meta name="Author" content="Yvette Lau">
<title>Document</title>
<!--css js 文件的引入-->
<style>
.word{background:#E4FFE9;width:250px;margin:50px auto;padding:20px;font-family:"microsoft yahei";}
/* 強(qiáng)制不換行 */
.nowrap{white-space:nowrap;}
/* 允許單詞內(nèi)斷句,首先會(huì)嘗試挪到下一行,看看下一行的寬度夠不夠,
不夠的話就進(jìn)行單詞內(nèi)的斷句 */
.breakword{word-wrap: break-word;}
/* 斷句時(shí),不會(huì)把長(zhǎng)單詞挪到下一行,而是直接進(jìn)行單詞內(nèi)的斷句 */
.breakAll{word-break:break-all;}
/* 超出部分顯示省略號(hào) */
.ellipsis{text-overflow:ellipsis;overflow:hidden;}
</style>
</head>
<body>
<div class="word">
<p class="nowrap">wordwrap:breakword;absavhsafhuafdfbjhfvsalguvfaihuivfs</p>
<p class="breakword">wordwrap:break-word;absavhsafhuafdfbjhfvsalguvfaihui</p>
<p class="breakAll">wordwrap:break-word;absavhsafhuafdfbjhfvsalguvfaihuivf</p>
<p class="normal">wordwrap:breakword;absavhsafhuafdfbjhfvsalguvfaihuivfsa</p>
<p class="ellipsis">wordwrap:breakword;absavhsafhuafdfbjhfvsalguvfaihuivfsab</p>
</div>
</body>
</html>
效果如下:
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。