《Django by Example》第二章 使用Django高級功能增強你的Blog

在上一個章節(jié),我們已經(jīng)創(chuàng)建了一個基礎(chǔ)的Blog程序。現(xiàn)在我們將使用一些Dajngo高級功能,去實現(xiàn)一個完整的blog網(wǎng)站,比如通過email進行分享,允許用戶進行評論,可以添加標簽和對帖子進行搜索檢索,在這一章,我們將學(xué)到如下的內(nèi)容:

? 1、使用Django去發(fā)送一封郵件

?2、創(chuàng)建表單并在視圖中處理他們

3、從models中創(chuàng)建表單

4、整合集成一些第三方的應(yīng)用

5、創(chuàng)建復(fù)雜的查詢

第一:使用Djngo去發(fā)送一封郵件

首先,我們將實現(xiàn)讓用戶通過郵件去分享自己的帖子的功能,在未看指南之前,先自己花點時間想想,你將如何使用上一章節(jié)我們學(xué)過的URLS\VIEWS\TEMPLATE等知識,來自己設(shè)計實現(xiàn)一個發(fā)送郵件的功能?現(xiàn)在檢查你需要什么去允許你的用戶發(fā)送通過郵件發(fā)送帖子。你將要:

1、創(chuàng)建一個表單,讓用戶可以去輸入 名字、email、接收人和可選的評論

2、在views.py里面創(chuàng)建一個視圖,處理帖子的內(nèi)容并發(fā)出郵件

3、在blog程序的url 地址映射表中,將新增加的視圖,添加到地址映射表

4、創(chuàng)建一個展示表單的網(wǎng)頁模板(Template)

在Django中創(chuàng)建/使用 表單

讓我們從創(chuàng)建分享帖子的表單開始,Django有一個內(nèi)置的表單框架,讓你可以用一個簡單的方式去創(chuàng)建一個表單。這個表單的框架允許你去定義你表單的字段,標記他們是否必須顯示,驗證輸入的數(shù)據(jù)是否準確。Django的表單架構(gòu)還是提供處理數(shù)據(jù)和渲染表單的一種靈活的方式。

Django有兩個基礎(chǔ)類去創(chuàng)建表單:

Form ?允許你去創(chuàng)建標準的表單

ModelForm ?允許你創(chuàng)建表單來創(chuàng)建和更新model實例

首先,我們在blog程序的目錄下面,創(chuàng)建一個forms.py,并在里面寫上代碼:

from django import forms

class EmailPostForm(forms.Form):

name=forms.CharField(max_length=25)

email = forms.EmailField()

to = Forms.EmailField()

comments = forms.CharField(required = False,widget = froms.Textarea)

這是你的第一個Django表單,讓我們再看看code:我們通過基礎(chǔ)的Form類,來創(chuàng)建一個表單,我們使用了Django的不同類型的字段,并根據(jù)每個字段類型的不同去驗證數(shù)據(jù)的有效性。

注意: 表單可以寫在Django項目的任何地方,如寫到views里面或者寫在文件里面或者寫在models里面,但一般來說,大家約定是在每個應(yīng)用程序中創(chuàng)建一個forms.py,將表單寫在這個文件中。

在上面的表單中,name字段是一個CharField類型,這個類型的字段在網(wǎng)頁上呈現(xiàn)的是一個<input type='text'>的 Html ?元素。每一個字段類似都有一個默認的網(wǎng)頁元素widget與之對應(yīng)。字段的默認網(wǎng)頁元素widget屬性可以被重寫,后面將會介紹到如何重寫一個表單字段的默認widget屬性。在comments字段中,我們使用widget=forms.Textarea來使用html的Textarea 元素替代默認的字段顯示為 <input>元素。意思說,如果我們不在字段屬性后面使用 widget=froms.Textarea,那么 comments字段的默認widget是input。

字段校驗同樣依賴于字段類型。例如,email和to這兩個字段的類型是EmailField。這些字段就需要用戶輸入一個正確的e-mail地址,否則校驗的時候會拋出一個forms.ValidationError異常,并且表單不會通過驗證。其他的參數(shù)同樣需要考慮到表單的驗證:我們定義了一個最大長度為25個字符的name字段,我們讓comments字段含有一個required=False參數(shù)來指明comments是一個可選的,不用必須輸入。所有這些都需要考慮到字段的校驗。上面表單的字段操作知識只是Django 表單字段的一部分,對于所有表單的可用字段列表說明,你可以訪問:https://docs.djangoproject.com/en/1.8/ref/forms/fields/.

在視圖中處理表單

當(dāng)表單成功提交后,為了能夠處理表單的數(shù)據(jù)并發(fā)送一個e-mail你需要去創(chuàng)建一個新的view。編輯你的blog程序下的views.py文件,將下面的代碼添加進去:

from .forms import EmailPostForm

def post_share(request,post_id):

?#Retrive post by id ,通過id接受

post = get_object_or_404(Post,id=post_id,status='published')

if request.method == 'POST':

#Form was submitted

form = EmailPostForm(request.POST)

if form.is_valid():

#Form fields passed validation

cd =form.cleaned_data

#...send email

else:

form =EmailPostForm()

return render (request,'blog/post/share.html',{'post':post,'form':form})

這個視圖的工作過程如下:

我們定義了一個post_share視圖,使用request對象和post_id做為參數(shù)

我們使用get_object_or_404()快捷操作,通過id檢索post并確實檢索到的post是published狀態(tài)

我們?nèi)匀挥眠@個view去顯示初始化的表單并處理提交的數(shù)據(jù).我們對表單提交的方法進行了區(qū)分。我們用POST方法去提交表單數(shù)據(jù),用get方法去獲取表單數(shù)據(jù)。同時我們需要判斷以下,如果我們得到的是一個GET請求,那么我們就返回一個空的表格去進行顯示,如果我們得到的是一個POST請求,那么我們會將表單提交并進行處理。因此,我們使用request.method =='POST'在這兩個場景間去進行區(qū)分。

下面是去處理顯示和處理表單:

1、當(dāng)這個視圖通過GET請求進行初始化請求時,我們創(chuàng)建一個新的表單實體,并將被用來在模板中顯示一個空表單

form =EmailPostForm()

2、用戶填充進入表單并通過POST渠道提交,這個時候,我們創(chuàng)建一個表單實體使用提交數(shù)據(jù)包含在request.POST:

if request.method =="POST":

#Form was submitted

form = EmailPostForm(request.POST)

3、做完這些,我們通過使用表單的is_valid()方法去進行數(shù)據(jù)的校驗。這個方法在表單中校驗數(shù)據(jù)的信息并且返回True加入所有的字段都檢驗數(shù)據(jù)正確。加入有任意的一個字段校驗是錯誤數(shù)據(jù) ,這個時候 is_valid() 將返回False.你可以通過訪問form.errors看到一個校驗錯誤信息的列表.

4、假如表格有非法的信息,我們使用提交的數(shù)據(jù)在模板里面渲染表單。我們將在模板里面顯示錯誤信息。

5、假如表單沒有非法的信息,我們通過訪問form.cleaned_date來檢索驗證后的數(shù)據(jù)。表單的字段和表單值將以字段的形式組織起來。

注意:假如你的表單數(shù)據(jù)沒有校驗 cleaned_date將只包含有效的字段。

現(xiàn)在,你需要去學(xué)習(xí),怎么使用Django發(fā)送一個email,去把他們整合在一起。

通過Django發(fā)送e-mails

通過django發(fā)送e-mails是很簡單。首先,你需要有個本地的SMTP服務(wù)器或者通過在項目的settings.py里面添加下面的步驟定義一個外部SMTP服務(wù)器。

EMAIL_HOST:SMTP服務(wù)器的host。默認是本地的

EMAIL_POST:SMTP服務(wù)器的端口,默認是25

EMAIL_HOST_USER:SMTP服務(wù)器的用戶名

EMAIL_HOST_PASSWORD:SMTP服務(wù)器的密碼

EMAIL_USE_TLS:是否去使用TLS安全連接

EMAIL_USE_SSL:是否去使用隱藏TLS安全連接

假如你沒有本地服務(wù)器,你可以使用你的email提供商的SMTP服務(wù)器。下面是一個通過使用GMAIL服務(wù)器發(fā)送郵件的例子。

EMAL_HOST ='smtp.gmail.com'

EMAIL_HOST_USER='your_account@gmail'

EMAIL_HOST_PASSWORD='your password'

EMAIL_post=587

EMAIL_USE_TLS=True

在命令行運行python manage.py命令打開python的shell,像下面這樣發(fā)送一個e-mail試試:

>>>from django.core.mail import send_mail

>>>send_mail('Django mail','this e-mail was send with django.','your_account@gmail',['your_account@gmail.com'],fail_silently=False)

send_mail() 使用 主題、消息、發(fā)送者、收件人列表做為參數(shù).通過設(shè)置可選參數(shù)fail_silently=False,我們告訴服務(wù)器,當(dāng)e-mail不能被正確投送時,會拋出一個異常,如果你看到值為1,這個時候你的email被發(fā)送成功。如果你通過前面配置的gmail服務(wù)器,你需要去允許訪問低安全度在https://www.google.com/settings/security/lesssecureapps.

現(xiàn)在,我們開始把這些添加到我們的視圖中,在blog程序的views.py中編輯post_share視圖,讓他像下面這樣:

from django.core.mail import send_mail

def post_share(request,post_id):

#retrieve post by id

post = get_object_or_404(Post,id=post_id,status='published')

sent = False

if request.method == 'POST':

#form was submitted

from = EmailPostForm(request.POST)

if form.is_valid():

cd = form.cleaned_date

post_url = request.build_absolute_url(post.get_absolute_url())

subject='{}({})recomends you reading "{}"'.format(cd['name'],cd['email'],post.title)

message='Read "{}" at {}\b\n{}\'s comments:{}'.format(post.title,post_url,cd['name'],cd['comments'])

send_mail(subject,message,'admin@myblog.com',[cd['to']])

sent =True

else:

form = EmailPostForm()

return render(request,'blog/post/share.html',{'post':post,'form':form,'sent':sent})

注意,我們在發(fā)送post的時候聲明了一個sent變量并將其設(shè)置為True。當(dāng)表單成功提交后,我們將在模板中使用這個變量,去顯示一個成功的信息。因為我們必須在電子郵件中加入一個post連接,我們得用get_absolute_url()來檢索post的絕對路徑。

我們使用這個路徑做為request.build_absolute_uri()的輸入,去構(gòu)建一個完整的URL鏈接,在這個URL鏈接中包含HTTP模式和域名,我們通過使用已驗證過的、被清洗后的表單數(shù)據(jù)構(gòu)建郵件的標題和郵件正文,并最終發(fā)送郵件到表單里面e-mail字段的 地址中。

現(xiàn)在你的視圖已經(jīng)完成,記住需要為它添加一個新的url表達式。打開blog程序下的urls.py文件,添加post_share url表達式,完成后如下:

urlpatterns =[

#.....

url(r'^(?P<post_id>\d+)/share/$',views.post_share,name='post_share'),

]


在網(wǎng)頁模板中渲染表單

在創(chuàng)建了模板,編寫了view的代碼,并在urls 地址路由表中添加了url表達式,我們就只缺少構(gòu)建一個該視圖的網(wǎng)頁模板了。在blog/templates/blog/post/目錄下添加一個名字叫做share.html的新文件,并加入如下的代碼:

{% entends "blog/base.html" %}

{% block title %} Share a post {% endblock %}

{% block content %}

{% if sent %}

? <h1>E-mail successfully sent</h1>

<p>

"{{post.title}}" was successfully sent to {{cd.to}}

</p>

{% else %}

<h1>Share "{{post.title}}"by e-mail</h1>

<form action ="." method ="post">

{{form.as_p}}

{% crst_token %}

<input type="submit" value = "send e-mail">

</form>

{% endif %}

{% endblock %}

這個模板用來去顯示表單或者當(dāng)他被發(fā)送時顯示發(fā)送成功的信息。正如你所看到的,我們創(chuàng)建這個HTML表單元素去標明 ,表單必須通過POST方法去提交。

<form action ="." method = "post">

然后我們包含了這個實際的表單實例,我們告訴Django在HTML段落 <p>元素中,用表單的as_p方法去渲染表單的字段,我們還可以通過調(diào)用as_ul渲染表單為一個無序列表或者調(diào)用as_table渲染成為一個html 表。如果我們想渲染每一個字段,我們還可以通過遍歷字段。向下面的例子:

{% for field in form %}

<div>

{{field.errors}}

{{field.label_tag}}{{field}}

</div>

{% endfor %}

{% csrf_token %}模板標簽 引入一個隱藏字段,帶有一個自動生成的token,去規(guī)避 CROSS-Site Request Forgery(CSRF)攻擊。這些攻擊由一些惡意的網(wǎng)站或者程序組成,去執(zhí)行一些你的網(wǎng)站用戶不想發(fā)生的有害的行為。

在https://en.wikipedia.org/wiki/Cross-site_request_forgery你可以發(fā)現(xiàn)更多的關(guān)于CSRF信息。

前面說的這個標簽生成一個隱藏的字段,像下面這里:

<input type='hidden' name ='csrfmiddlewatetoken' value ='26JjKo2lcEtYkGoV9z4XmJIEHLXN5LDR'/>

注意:默認的,Django在所有的POST請求中檢查CSRF token。記住在所有的表單中包含csrf_token標簽并通過POST渠道去提交。

編輯你的 blog/post/detail.html模板,添加下述鏈接到分享帖子的url中,在{{post.body|linebreaks}}變量后。

<p>

<a href ="{% url "blog:post_share" post.id %}">share this post</a>

<p>

記住,我們正通過Django提供的 {% url %}模板標簽去動態(tài)構(gòu)建url鏈接。我們通過名字回調(diào)blog并把url命名為 post_share,我們通過傳遞post id參數(shù)去創(chuàng)建絕對路徑。

現(xiàn)在,使用python manage.py runserver命令啟動開發(fā)服務(wù)器,在瀏覽器打開http://127.0.0.1:8000/blog/,點擊任意一個帖子標題進入詳情頁。在正文的下面,你將看到我們剛剛添加的鏈接,如下圖所示:

XXXXX圖片。。

點擊 Share this post ,你將看到一個包含通過email去分享這個post表單的頁面,如下圖所示:

表單的CSS樣式表包含在 例子代碼的 static/css/blog.css文件中,當(dāng)你點擊Send e-mail按鈕,這個表單將提交并進行驗證,如果所有的字段都是有效的數(shù)據(jù),你將得到一個如下圖所示的發(fā)送成功的消息。

如果你輸入了錯誤的數(shù)據(jù),你將看到,表單被在再次渲染,并包含所有的校驗錯誤信息。

創(chuàng)建一個評論系統(tǒng)

現(xiàn)在,我們開始為這個blog程序開發(fā)一個評論系統(tǒng),在里面用戶可以對某個帖子發(fā)表評論。去創(chuàng)建這樣一個評論系統(tǒng),你將要做:

-創(chuàng)建保存評論的數(shù)據(jù)模型;

-創(chuàng)建一個表單去提交評論并驗證輸入的數(shù)據(jù)正確性;

-添加一個處理表單并保存評論記錄到數(shù)據(jù)庫中的視圖;

-編輯帖子的detail模板,增加評論列表在詳情頁的顯示,并含有讓用戶進行評論的表單單元;

首先,讓我們創(chuàng)建一個保存評論的數(shù)據(jù)庫模型。打開你的blog程序的models.py文件,并在里面寫上如下的代碼:

class Comment(models.Model):

post = models.ForeignKey(Post,related_name = 'comments')

name = models.CharField(max_length = 80)

email = models.EmailField()

body = models.TextField()

created = models.DateTimeField(auto_now_add=True)

updated = models.DateTimeField(auto_now = True)

active ?= models.BooleanField(default= True)

class Meta:

ordering = ('created',)

def __str__(self):

return 'Comment by {} on {}'.format(self.name,self,post)

這就是我們的Comment數(shù)據(jù)模型.它包含一個外鍵,用來讓評論關(guān)聯(lián)到唯一一個帖子上。這是在Commnet數(shù)據(jù)模型中定義的一個多對一的數(shù)據(jù)關(guān)系,因為每個評論都是在帖子上,并且每個帖子都可能有多個評論。post的 related_name屬性,允許我們通過定義這個屬性來讓關(guān)聯(lián)的對象反向引用。在定義這個后,我們就可以通過使用comment.post獲取到當(dāng)前帖子的一條評論對象或者通過post.comments.all()獲取到當(dāng)前post的所有評論。如果你不定義related_name這個屬性,Django將使用 undercase name of 數(shù)據(jù)模型后面跟隨的_set連接器(在本例中是 comment_set)去命名關(guān)聯(lián)對象反向引用的管理器。

[翻譯不好,引用解釋:如果模型有一個ForeignKey,那么該ForeignKey 所指的模型實例可以通過一個管理器返回前一個模型的所有實例。默認情況下,這個管理器的名字為foo_set,其中foo是源模型的小寫名稱。該管理器返回的查詢集可以用上一節(jié)提到的方式進行過濾和操作。]

你可以在https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_one/學(xué)習(xí)到更多關(guān)于多對一關(guān)系的資料。中文版:http://python.usyiyi.cn/documents/django_182/topics/db/queries.html#backwards-related-objects

http://python.usyiyi.cn/translate/django_182/topics/db/queries.html#following-relationships-backward

[這個地方介紹的比較好

我們包含一個active的布爾類型字段,這個可以用來對comments進行手動的審核。我們用created字段對評論進行默認時間順序排序。

你剛才新創(chuàng)建的Comment模型還沒有同步到數(shù)據(jù)庫中。運行下面的命令,去生成一個新的遷移前準備。

python manage.py makemigrations blog

你將看到下面的輸出:

Migrations for 'blog':

0002_comment.py:

-create model comment

Django在blog程序的 migrations目錄下生成了一個0002_comment.py的文件,現(xiàn)在你需要去創(chuàng)建關(guān)聯(lián)數(shù)據(jù)庫模式并把更新應(yīng)用到數(shù)據(jù)庫上。運行下面的命令去應(yīng)用已存在的遷移:

python manage.py migrate

你在下面會看到如下的輸出信息:

Applying blog.0002_comment... OK

我們剛才創(chuàng)建的遷移就這樣被應(yīng)用了,在數(shù)據(jù)庫你就能看到一個新的blgo_comment的數(shù)據(jù)表存在。

現(xiàn)在我們可以把我們的新數(shù)據(jù)模型添加到管理后臺頁面中,通過簡單的交互界面去管理評論。打開blog程序里的admin.py文件,添加對Comment的調(diào)用和下面的ModelAdmin:

from .models import Post,Comment

class CommentAdmin(admin.ModelAdmin):

list_display =('name','email','post','created','active')#在管理后臺顯示的行

list_filter = ('active','created','updated') #在管理后臺,用戶可用的過濾字段

search_fields=('name','email','body')#在管理后臺,可以通過name、email、body進行搜索

admin.site.register(Comment,CommentAdmin)

用python manage.py runserver啟動開發(fā)環(huán)境的服務(wù)器,并用瀏覽器打開 http://127.0.0.1:8000/admin/,你將看到在blog的段落中,已經(jīng)包含了這個新的comment,如下面截圖所示:

XXXXXX

我們的數(shù)據(jù)模型現(xiàn)在被注冊進入到管理頁面,這讓我們可以通過簡單的交互界面去管理Comment實例。

從數(shù)據(jù)模型創(chuàng)建表單

我們還需要去創(chuàng)建一個表單讓我們用戶能在blog帖子里面進行評論。還記得之前我們說過,Django支持兩種基礎(chǔ)類去創(chuàng)建表單:From和ModelForm。在前面的例子中,我們已經(jīng)使用了第一種類去創(chuàng)建表單,讓用戶可以通過e-mail去分享帖子。在當(dāng)前的例子中,你將使用ModelForm類去創(chuàng)建表單,因為你必須從你的Comment數(shù)據(jù)模型中創(chuàng)建一個動態(tài)表態(tài)。編輯blog程序中的forms.py并添加下面的代碼:

from .models import Comment

class CommentForm(forms.ModelForm):

? class Meta:

model =Comment

fields =('name','email','body')

從數(shù)據(jù)模型中創(chuàng)建一個表單,我們只需要在Meta元組中去指明哪個模型??用來創(chuàng)建表單。Django 會自己反查數(shù)據(jù)模型并為我們創(chuàng)建一個動態(tài)的表單。每個表單的字段都有一個相應(yīng)的默認表單字段類型。我們定義我們模型字段的方法是考慮到表單驗證的。默認的,對于每個Django的form字段都在數(shù)據(jù)模型中包含了。然而,你可以通過使用fields_list方法來明確的告訴框架,那個字段是你想在form中使用的,或者使用exclude一個字段列表來定義表單會包含那些你想要包含的字段。在這個CommentForm中,我們只在表單中用了name,email,body字段,因為這些是我們的用戶可以去填寫的。

在views里面處理ModelForm

為了讓表單比較簡單,我們將使用帖子已有的detail視圖去實例化并處理表單。編輯models.py文件,添加Comment數(shù)據(jù)模型和CommentFrom表單的引用,并編輯post_detail視圖,像下面這樣:

from .models import Post,Comment

from .forms import EmailPostForm,CommentForm

def post_detail(request,year,month,day,post):

post = get_object_or_404(Post,slug=post,status='published',publish__year=year,publist__month=month,publish__day=day)

#List of active comments for this post

comments = post.comments.filter(active=True)

if request.method = 'POST':

#A comment was posted

comment_form = CommentForm(data = request.POST)

if comment_form.is_valid():

#create comment object bug don't save to database yet

new_comment = comment_form.save(commit=False)

#assign the current post to the comment

new_comment.post=post

#save the comment to the database

new_comment.save()

else:

comment_form =CommentFrom()

return render(request,'blog/post/detail.html',{'post':post,'comments':comments,'comment_form':comment_form})

我們回顧一下我們剛才添加到視圖中的信息。我們使用post_detail視圖去顯示帖子和他的評論,我們添加了一個QuerySet去檢索當(dāng)前帖子的所有活躍的評論

comments=post.comments.filter(active=True)

我們從post對象創(chuàng)建這個QuerySet。我們用之前在comment數(shù)據(jù)模型里面定義反向數(shù)據(jù)查詢管理器related_name=comments來通過post關(guān)系反向查詢comments。

在這個視圖中我們還需要去實現(xiàn)用戶提交新的評論功能。因此如果調(diào)用視圖的是一個GET請求,我們使用comment_form=CommentFrom()創(chuàng)建一個form實例,如果調(diào)用視圖的請求是一個POST請求,我們使用用戶提交的數(shù)據(jù)實例化表單,并用is_valid()方法來校驗數(shù)據(jù)有效性。如果表單是無效的,我們用校驗錯誤信息渲染模板,如果表單數(shù)據(jù)有效的,我們采取后續(xù)行動:

1、我們調(diào)用form.save()方法創(chuàng)建一個新的Comment對象,像下面這樣:

new_comment = comment_form.save(commit=False)

save()方法創(chuàng)建一個當(dāng)前表單所連接的模型的實例,并保存他到數(shù)據(jù)庫,假如你用commit=False調(diào)用他,你將創(chuàng)建模型的實例,但并沒有將它保存到數(shù)據(jù)庫。當(dāng)你在接下來的下一步,想在最終保存數(shù)據(jù)前,對數(shù)據(jù)進行一些修改,這是很方便的。save()方法對ModelForm是可用的,但對Form實例是不可用的,因為Form實例沒有關(guān)聯(lián)到任何的模型上。

2.我們把當(dāng)前的帖子指定到剛創(chuàng)建的comment上。

new_comment.post=post

通過這樣,我們清晰的描述了,新評論屬于給定的帖子。

3.最終,我們用下面的代碼將新評論保存到數(shù)據(jù)庫中。

new_comment.save()

現(xiàn)在我們的view已經(jīng)可以去處理和顯示新的評論了。

添加評論到帖子的詳情模板中

我們已經(jīng)創(chuàng)建帖子的評論管理功能。現(xiàn)在我們需要去讓我們的post_detail.html模板去適配做這些:

>顯示當(dāng)天帖子的評論總數(shù)

>顯示評論列表

>顯示用戶添加新評論的表單,讓用戶可以再帖子添加新評論

首先,我們將添加評論總數(shù)。打開blog_detail.html模板,并在content模塊里添加下面的代碼:

{% with comments.counts as total_comments %}

<h2>

{{total_comments}} ?comments {{total_comments|pluralize}}

</h2>

{% endwith %}

我們在模板中使用Django ORM,執(zhí)行查詢comments.count().記住django模板語言不用使用圓括號去調(diào)用方法。如上面我們使用的comments.counts.{% with %}標簽允許我們?nèi)ソo一個新的變量賦值,這個變量可以使用到 {% endwith %}標簽處。

注意:{% with %}模板標簽是一個需要規(guī)避多次訪問數(shù)據(jù)庫數(shù)據(jù)或使用比較耗時的方法時的有用方法。

我們使用pluralize模板過濾器,根據(jù)total_comments值去顯示comment的復(fù)數(shù)后綴.模板過濾器 用已被使用的變量的值做為輸入并返回一個計算過的值。在第三章《擴展Blog程序》我們將討論模板過濾器。

如果值是和1不同的,pluralize模板過濾器將顯示一個"s"。前面的文字將被渲染成 0 comments,1 comment, 或者N comments。Django包含豐富的模板標簽和過濾器去幫助你顯示信息,以你想要的方式。

現(xiàn)在,我們再來包含評論列表。在模板中,在之前代碼的后面添加下面行:

{% for comment in comments %}

<div class="comment">

<p class="info">

Comment {{ forloop.counter }} by{{ comment.name }}

{{comment.created}}

</p>

{comment.body|linebreaks}}

</div>

{% empty %}

<p>There are no comments yet.</p>

{% endfor %}

我們使用 {% for %}模板標簽去循環(huán)所有的評論。當(dāng)comments列表是空的時候,我們顯示一個默認的消息,告訴用戶當(dāng)前帖子還沒有評論。我們使用 {{forloop.counter}}去遍歷評論值,在每個迭代中都包含循環(huán)的計數(shù)。然后我們顯示提交評論的用戶名、日期、和評論的正文。

最終,當(dāng)評論提交成功后你需要去渲染表單或者顯示一條成功的信息。在上面的代碼下面添加下面的行:

{% if new_comment %}

<h2>Your comment has been added.</h2>

{% else %}

<h2>Add a new comment</h2>

<form action="." method ="post">

{{comment_form.as_p}}

{{% csrf_token %}}

<p><input type="submit" value="Add comment"></p>

</form>

{% endif %}

這個代碼是相當(dāng)簡單的:假如new_comment對象存在,我們顯示一個成功的信息,因為評論已被成功的創(chuàng)建了。否則,我們?yōu)槊總€字段使用一個段落元素去渲染表單,并且對于POST請求我們需要添加一個CSRF token的驗證碼。在瀏覽器打開http://127.0.0.1:8000/blog/,點擊帖子的標題查看詳情頁,如下圖所示:

XXXX

用表單添加幾個評論,他們應(yīng)該按照時間順序顯示在你的帖子后面。像下圖所示

XXXXXXX

在瀏覽器打開http://127.0.0.1:8000/admin/blog/comment/,在管理頁面,你將看到你創(chuàng)建的評論列表,選一個點擊并編輯,不選Active復(fù)選框,并點擊保存按鈕。我們將重新得到一個評論列表,并且在Active列上面,當(dāng)前評論將顯示一個不活躍的圖標。像下面的截圖中,第一個評論已經(jīng)被標記成inactive.

XXXX

這時候,假如你返回到帖子詳情頁,你將注意到,剛才被刪除的評論在任何地方都沒有再顯示。無論是創(chuàng)建的評論總數(shù)還是評論詳情。幸虧有active字段,你可以在你的帖子中設(shè)置一些評論無效并避免顯示他們。

添加標簽功能

在實現(xiàn)了我們的評論系統(tǒng)后,我們將開始去創(chuàng)建一個標記我們文章的方法,我們計劃在我們項目中整和一個第三方的Django 標簽程序。django-taggit 能夠給你提供一個TAG模型,并且是一個可重用的程序,讓你可以輕松地在任意一個模型中添加、管理標簽。你可以在https://github.com/alex/django-taggit里面查看他的源代碼。

首先,你需要通過pip來安裝django-taggit,運行下面命令安裝:

pip install django-taggit == 0.17.1

然后打開mysite項目的settings.py文件,添加taggit到你的installed_apps里面,添加后如下圖:

INSTALLED_APPS = (

#...

'blog',

'taggit',

)

打開blog程序的models.py文件,使用如下所示的命令,添加django-taggit提供的TabgableManager管理器到Post模型中

from taggit.managers import TaggableManager

class Post(models.Model):

#...

tags = TaggableManager()

tags管理器允許你從post對象中去添加、檢索、刪除標簽。運行下面的命令給模型的改變創(chuàng)建一個遷移事件。

python manage.py makemigrations blog

我們將得到如下的輸出:

Migrations for 'blog':

0003_post_tags.py

-Add field tags to post

現(xiàn)在,運行下面的命令,為django-taggit模型創(chuàng)建需要的數(shù)據(jù)庫表,并同步模型的變更到數(shù)據(jù)庫中。

python manage.py migrate

你將看到一個輸出指示當(dāng)前的遷移已經(jīng)被應(yīng)用了,如下圖所示:

Applying taggit.0001_initial.....ok

Applying taggit.0002_auto_20150616_2121.....ok

Applying blog.0003_post_tags....ok

現(xiàn)在數(shù)據(jù)庫已經(jīng)可以去試用django-taggit模型,試用python manage.py shell命令打開客戶端,我們來學(xué)學(xué)如何使用tags管理器。首先,我們檢索出來一個帖子,(用帖子ID)

>>>from blog.models import Post

>>> post = Post.objects.get(id=1)

然后我們在post對象里面添加一些標簽并將這些標簽再檢索出來,驗證這些標簽被成功添加了:

>>post.tags.add('music','jazz','django')

>>>post.tags.all()

[<tag:jazz>,<tag:django>,[tag:musci]]

最終,刪除一個標簽并再檢查標簽列表:

>>>post.tags.remove('django')

>>>post.tags.all()

[<Tag:jazz>,<Tag:music>]

很簡單,對不對?運行python manage.py runserver去啟動開發(fā)服務(wù)器,并在瀏覽器打開 http://127.0.0.1:8000/admin/taggit/tag/,你講看到在管理頁面有一個taggit應(yīng)用程序,里面有標簽對象的列表。

跳轉(zhuǎn)到http://127.0.0.1:8000/admin/blog/post/,點擊一條帖子并編輯他,你可以看到在帖子里面已經(jīng)包含了一個新的Tags的字段,像下圖所示,你可以很輕松的去編輯標簽信息。

XXXXXXXX

現(xiàn)在我們打算去編輯我們的blog帖子,并顯示這些標簽,打開blog/post/list.html模板,在html的帖子標題下面添加下面代碼:

<p class = "tags">TAGS :{{post.tags.all|join:","}}</p>

上面的 ?join模板過濾器的工作機制和python里面的 字符 join()方法相同,用來連接給定的字符元素。打開http://127.0.0.1:8000/bolg/,在每個帖子標題下面你就能看到一條標簽列表了,如下圖:

XXXXXXXX

現(xiàn)在,我們開始去編輯我們的post_list視圖,讓用戶可以通過一個給定的標簽去列出符合這個標簽的所有帖子。打開你的blog程序中的views.py文件,從django-taggit中加載Tag模板,像下面這樣把post_list視圖換成posts的tag選擇過濾器。

from taggit.models import tag

def post_list(request,tag_slug=None):

object_list = Post.published.all()

tag = None

if tag_slug:

? ?tag = get_object_or_404(Tag,slug=tag_slug)

object_list = object_list.filter(tags__in=[tag])

#...

視圖的工作流程如下:

1、在視圖我們?nèi)×艘粋€可選的tag_slug參數(shù),并賦值為None,這個參數(shù)將通過URL取得。

2.在view里面,我們創(chuàng)建最初的檢索,檢索出所有的已發(fā)表的帖子,假如當(dāng)前帖子有 tag_slug,我們通過已知的slug使用 get_object_or_404()快捷功能區(qū)獲取Tag對象。

3.然后我們根據(jù)帖子是否包含已給的tag來過濾出來帖子列表。因為這是一個多對多的關(guān)系,我們必須以 標簽包含在給定的標簽列表來過濾帖子。在我們的例子里面,只有一個元素符合。

請記住,Querysets是惰性的【像Entry.Objects.all(),這些操作返回的是一個QuerySet對象,這個對象比較特別,并不是執(zhí)行Objects.all(),或者filter之后就會與數(shù)據(jù)庫交互,而是執(zhí)行比如打印具體entry某個參數(shù),才會去進行數(shù)據(jù)庫查詢,這個就是django querysets惰性的解釋】,在渲染模板的時候當(dāng)我們循環(huán)完帖子列表Querysets才會去執(zhí)行檢索帖子的操作。

最后,修改view底部的render()功能,將tag變量值傳給模板,最終view視圖如下:

def post_list(request,tag_slug=None):

object_list = Post.published.all()

tag = None

if tag_slug:

tag = get_object_or_404(Tag,slug=tag_slug)

object_list =object_list.filter(tags__in=[tag])

paginator = Paginarot(object_list,3) #每頁顯示3條帖子

page = request.GET.get('page')

try :

? ? posts = paginator.page(page)

? except PageNotAnInteger:

? ?#if page is not an integer deliver the first page

? posts = paginator.page(1)

except Emptypage:

#if page is out of range deliver last page of results

posts = paginator.page(paginator.num_pages)

return render(request,'blog/post/list.html',{'page':page,'posts':posts,'tag':tag})

打開blog程序的urls.py文件,把PostListView的URL參數(shù)注釋掉,并把 post_list注釋去掉,如下所示:

url(r'^$' ,views.post_list,name='post_list'),

#url(r'^$',views.PostListView.as_view(),name='post_list'),

添加下面的新urlm模式去支持 通過標簽篩選帖子列表:

url(r'^tag/(?P<tag_slug>[-\w]+)/%$',views.post_list,name='post_list_by_tag'),

像看到那樣,兩個url的模式都是指向同一個view,但是我們把它命名不同的名字,第一個模式命名為post_list,沒有任何的參數(shù)項;第二個url模式,需要使用一個tag_slug參數(shù)來調(diào)用視圖。

以后我們用post_list視圖,我們?nèi)ゾ庉媌log/post/list.html模板,并編輯分頁使用posts對象:

{% include "pagination.html" with page=posts %}

在{% for %}循環(huán)上添加下面的行:

{% if tag %}

<h2>Posts tagged with "{{tag.name}}"</h2>

{% endif %}

加入用戶訪問泊客,他將看到帖子列,如果他通過一個給定的標簽去過濾帖子,他將看到這些信息。更改標簽的顯示方式:

<p class = "tags">

?Tags:

{% for tag in post.tags.all %}

? <a href = "{ % url "blog:post_list_by_tag" tag.slug %}">{{tag.name}}</a>

?{% if not forloop.last %},{% enfif %}

{% endfor %}

</p>

現(xiàn)在,我們循環(huán)通過帖子顯示的所有標簽,并通過tag過濾帖子。我們用 {%url "blog:post_list_by_tag" tag.slug%}動態(tài)創(chuàng)建url,并用url名字和標簽slug作為參數(shù),我們通過逗號分開這些標簽。

打開 http;//127.0.0.1:8000/blog/,點擊標簽連接,你可以看到通過標簽過濾出來的帖子列表,如下圖:

XXXXXXX


關(guān)聯(lián)檢索

現(xiàn)在,我們已經(jīng)為我們的博客貼上了標簽,做完這個,我們就可以圍繞標簽做很多有意思的事情。

通過標簽,我們可以把我們的博客進行很好的分類。類似主題的文章通常會有好幾個標簽,我們計劃去創(chuàng)建一個功能,顯示被分享的文章中,相關(guān)標簽的數(shù)量。這樣,當(dāng)一個用戶讀一個文章時,我們就可以建議他再讀其他的相關(guān)的文章。

為實現(xiàn)通過具體文章來進行關(guān)聯(lián)檢索,我們需要:

檢索當(dāng)前文章的所有標簽

獲取所有使用當(dāng)前文章標簽中任意一個標簽標記上的文章

通過分享文章中標簽的數(shù)量,對結(jié)果進行排序;

萬一有兩個或者多個文件,都有相同數(shù)量的標簽,那么推薦最近發(fā)布的。

將查詢限制為要推薦的文章數(shù)量

上面這些步驟被一個復(fù)雜的查詢數(shù)據(jù)集實現(xiàn),計劃在我們的post_view視圖包含這個查詢。打開blog程序的views.py文件,在文件頭部添加下面的import信息:

from django.db.models import Count

這是Django ORM 的Count聚合函數(shù),這個函數(shù)支持我們?nèi)?zhí)行記數(shù)匯總.在post_detail視圖的render()函數(shù)中前,添加下面的行:

#list of similar posts

post_tags_ids = post.tags.values_list('id',flat=True)

similar_posts = Post.published.filter(tags__inpost_tags_ids).exclude(id=post.id)

similar_posts=similar_posts.annotate(same_tags=Count('tags')).order_by('-same_tags','-publish')[:4]

我們來看下上述代碼的運行過程:

1.我們檢索當(dāng)前文章標簽ID的列表.values_list 查詢組將返回一個值的元組列表,values_list()查詢組將返回定值的字段的值的元組數(shù)據(jù)。我們通過傳遞flat=True去獲取一個平坦單調(diào)的列表,[1,2,3,....].

2.我們獲取所有包含任意一個上文標簽的文章并將當(dāng)前文章自己從列表中去掉;

3.我們使用Count聚合函數(shù)去生成一個統(tǒng)計字段same_tags,包含所有需要統(tǒng)計的標簽的總數(shù)

4.我們通過共享標簽的數(shù)量,對結(jié)果進行排序,并且當(dāng)不同的文章含有相同的標簽數(shù)量時,我們使用最近發(fā)布的文章,我們?nèi)‘?dāng)前結(jié)果最開頭的四篇文章作為和當(dāng)前文章關(guān)聯(lián)最緊密的文章去顯示;

添加sililar_post對象到render()函數(shù)的上下文字典字典中,如下圖:

return rendr(request,'blog/post/detail.html',{'post':post,'comments':comments,'comment_form':comment_form,'similar_posts':similar_posts})

現(xiàn)在 編輯 blog/post/detail.html模板,并在文章評論列前面添加下面的代碼:

<h2>Similar posts</h2>

{% for post in similar_posts %}

<p>

<a href = "{{post.get_absolute_url}}">{{post.title}}</a>

</p>

{% empty%}

There are no similar posts yet.

{% endfor %}

也建議 和我們在文章列表模板里面做的一樣 ,再添加標簽列表到你的文章詳情模板。現(xiàn)在你的文章詳情頁看起來應(yīng)該像下面這個樣子:


XXXXXX

現(xiàn)在你可以成功的推薦相關(guān)文章給你的用戶了,django-taggit同樣包含一個similar_objects()管理器,你可以使用這個管理器通過共享標簽檢索對象,你可以通過http://django-taggit.readthedocs.org/en/latest/api.html來查看全部的django-taggit相關(guān)技術(shù)知識。

總結(jié):

在本章節(jié),我們學(xué)習(xí)了如何使用Django 的froms和models froms。我們創(chuàng)建一個通過郵件分享網(wǎng)站內(nèi)容的系統(tǒng),我們給自己的博客創(chuàng)建了一個評論系統(tǒng),我們給我們的博客文章添加了標簽,整合一個第三方可重用的程序,讓我們可以創(chuàng)建關(guān)聯(lián)的查詢組,去查詢關(guān)聯(lián)對象。

在下一章,我們會學(xué)習(xí)如何創(chuàng)建一個 自定義的標簽和過濾器。我們還會去創(chuàng)建一個自定義站點地圖并填充到你的博客文章中,并在我們的程序中聚合一個高級搜索引擎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內(nèi)容