整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          第 14 篇:交流的橋梁“評(píng)論功能”-HelloDj

          第 14 篇:交流的橋梁“評(píng)論功能”-HelloDjango 系列教程

          者: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)開源~

          面這種單詞直接打斷換行:

          這里涉及到兩個(gè)css屬性(很多年傻傻分不清):

          1. word-break (當(dāng)行尾放不下一個(gè)單詞時(shí),決定單詞內(nèi)部該怎么擺放)

          2. 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>

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
          • 22
          • 23
          • 24
          • 25
          • 26
          • 27
          • 28
          • 29
          • 30
          • 31
          • 32
          • 33

          效果如下:


          主站蜘蛛池模板: 中文字幕一区二区三匹| 福利一区福利二区| 亚洲日韩精品一区二区三区无码| 免费看一区二区三区四区| 色妞色视频一区二区三区四区 | 国产一区二区不卡老阿姨| 国产福利一区二区精品秒拍| 国模无码一区二区三区不卡| 国产福利一区二区三区在线观看| 福利片免费一区二区三区| 国产在线精品一区二区三区直播| 97精品国产一区二区三区| 武侠古典一区二区三区中文| 亚洲爆乳精品无码一区二区三区| 精品国产一区在线观看 | 日韩av片无码一区二区不卡电影 | 中文字幕人妻AV一区二区| 一区二区三区四区精品| 3d动漫精品啪啪一区二区免费| 国产一区二区精品在线观看| 无码人妻精品一区二区三区久久 | 国产aⅴ精品一区二区三区久久| 亚洲无线码一区二区三区| 无码视频一区二区三区| 国模私拍一区二区三区| 日本一区二区三区在线观看视频 | 亚洲国产成人精品久久久国产成人一区二区三区综| 国产福利视频一区二区| 日本精品啪啪一区二区三区| 无码一区二区三区| 久久一区二区免费播放| 无码人妻精品一区二区三区不卡| 精品亚洲一区二区| 亚洲国产成人久久一区久久 | 日韩免费观看一区| 国产成人片视频一区二区| 怡红院美国分院一区二区| 熟女精品视频一区二区三区| 亚洲乱码国产一区网址| 国产成人久久一区二区不卡三区| 亚洲一区综合在线播放|