2 為博客添加高級功能
上一章中,你創建了一個基礎的博客應用?,F在,利用一些高級特性,你要把它打造成一個功能完整的博客,比如通過郵件分享帖子,添加評論,為帖子打上標簽,以及通過相似度檢索帖子。在這一章中,你會學習以下主題:
- 使用Django發送郵件
- 在視圖中創建和處理表單
- 通過模型創建表單
- 集成第三方應用
- 構造復雜的
QuerySet
。
2.1 通過郵件分享帖子
首先,我們將會允許用戶通過郵件分享帖子?;ㄒ稽c時間想想,通過上一章學到的知識,你會如何使用視圖,URL和模板來完成這個功能?,F在核對一下,允許用戶通過郵件發送帖子需要完成哪些操作:
- 為用戶創建一個填寫名字,郵箱,收件人和評論(可選的)的表單
- 在
views.py
中創建一個視圖,用于處理post
數據和發送郵件 - 在
blog
應用的urls.py
文件中,為新視圖添加URL模式 - 創建一個顯示表單的模板
2.1.1 使用Django創建表單
讓我們從創建分享帖子的表單開始。Django有一個內置的表單框架,讓你很容易的創建表單。表單框架允許你定義表單的字段,指定它們的顯示方式,以及如何驗證輸入的數據。Django的表單框架還提供了一種靈活的方式,來渲染表單和處理數據。
Django有兩個創建表單的基礎類:
-
Form
:允許你創建標準的表單 -
ModelForm
:允許你通過創建表單來創建或更新模型實例
首先,在blog
應用目錄中創建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=forms.Textarea)
這是你的第一個Django表單。這段代碼通過繼承基類Form
創建了一個表單。我們使用不同的字段類型,Django可以相應的驗證字段。
表單可以放在Django項目的任何地方,但慣例是放在每個應用的
forms.py
文件中。
name
字段是一個CharField
。這種字段的類型渲染為<input type="text">
HTML元素。每種字段類型都有一個默認組件,決定了該字段如何在HTML中顯示??梢允褂?code>widget屬性覆蓋默認組件。在comments
字段中,我們使用Textarea
組件顯示為<textarea>
HTML元素,而不是默認的<input>
元素。
字段的驗證也依賴于字段類型。例如,email
和to
字段是EmailField
。這兩個字段都要求一個有效的郵箱地址,否則字段驗證會拋出forms.ValidationError
異常,導致表單無效。表單驗證時,還會考慮其它參數:我們定義name
字段的最大長度為25個字符,并使用required=False
讓comments
字段是可選的。字段驗證時,這些所有因素都會考慮進去。這個表單中使用的字段類型只是Django表單字段的一部分。在這里查看所有可用的表單字段列表。
2.1.2 在視圖中處理表單
你需要創建一個新視圖,用于處理表單,以及提交成功后發送一封郵件。編輯blog
應用的views.py
文件,添加以下代碼:
from .forms import EmailPostForm
def post_share(request, post_id):
# Retrieve post by 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
作為參數。 - 我們通過ID,使用
get_object_or_404()
快捷方法檢索狀態為published
的帖子。 - 我們使用同一個視圖=顯示初始表單和處理提交的數據。根據
request.method
區分表單是否提交。我們將使用POST
提交表單。如果我們獲得一個GET
請求,需要顯示一個空的表單;如果獲得一個POST
請求,表單會被提交,并且需要處理它。因此,我們使用request.method == 'POST'
來區分這兩種場景。
以下是顯示和處理表單的過程:
- 當使用
GET
請求初始加載視圖時,我們創建了一個新的表單實例,用于在模板中顯示空表單。
form = EmailPostForm()
- 用戶填寫表單,并通過
POST
提交。接著,我們使用提交的數據創建一個表單實例,提交的數據包括在request.POST
中:
if request.POST == 'POST':
# Form was submitted
form = EmailPostForm(request.POST)
- 接著,我們使用表單的
is_valid()
方法驗證提交的數據。該方法會驗證表單中的數據,如果所有字段都是有效數據,則返回True
。如果任何字段包含無效數據,則返回False
。你可以訪問form.errors
查看驗證錯誤列表。 - 如果表單無效,我們使用提交的數據在模板中再次渲染表單。我們將會在模板中顯示驗證錯誤。
- 如果表單有效,我們訪問
form.cleaned_data
獲得有效的數據。該屬性是表單字段和值的字典。
如果你的表單數據無效,
cleaned_data
只會包括有效的字段。
現在,你需要學習如何使用Django發送郵件,把所有功能串起來。
2.1.3 使用Django發送郵件
使用Django發送郵件非常簡單。首先,你需要一個本地SMTP服務,或者在項目的settings.py
文件中添加以下設置,定義一個外部SMTP服務的配置:
-
EMAIL_HOST
:SMTP服務器地址。默認是localhost
。 -
EMAIL_PORT
:SMTP服務器端口,默認25。 -
EMAIL_HOST_USER
:SMTP服務器的用戶名。 -
EMAIL_HOST_PASSWORD
:SMTP服務器的密碼。 -
EMAIL_USE_TLS
:是否使用TLS加密連接。 -
EMAIL_USE_SSL
:是否使用隱式TLS加密連接。
如果你沒有本地SMTP服務,可以使用你的郵箱提供商的SMTP服務。下面這個例子中的配置使用Google賬戶發送郵件:
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'your_account@gmail.com'
EMAIL_HOST_PASSWORD = 'your_password'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
運行python manage.py shell
命令打開Python終端,如下發送郵件:
>>> from django.core.mail import send_mail
>>> send_mail('Django mail', 'This e-mail was sent with Django',
'your_account@gmail.com', ['your_account@gmail.com'],
fail_silently=False)
send_mail()
的必填參數有:主題,內容,發送人,以及接收人列表。通過設置可選參數fail_silently=False
,如果郵件不能正確發送,就會拋出異常。如果看到輸出1
,則表示郵件發送成功。如果你使用前面配置的Gmail發送郵件,你可能需要在這里啟用低安全級別應用訪問權限。
現在,我們把它添加到視圖中。編輯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
form = EmailPostForm(request.POST)
if form.is_valid():
# Form fields passed validation
cd = form.cleaned_data
post_url = request.build_absolute_uri(post.get_absolute_url())
subject = '{} ({}) recommends you reading "{}"'.format(cd['name'], cd['email'], post.title)
message = 'Read "{}" at {}\n\n{}\'s comments: {}'.format(post.title, post_url, cd['name'], cd['comments'])
send_mail(subject, message, 'admin@blog.com', [cd['to']])
sent = True
else:
form = EmailPostForm()
return render(request,
'blog/post/share.html',
{'post': post, 'form': form, 'sent': sent})
注意,我們聲明了一個sent
變量,當帖子發送后,設置為True
。當表單提交成功后,我們用該變量在模板中顯示一條成功的消息。因為我們需要在郵件中包含帖子的鏈接,所以使用了get_absolute_url()
方法檢索帖子的絕對路徑。我們把這個路徑作為request.build_absolute_uri()
的輸入,構造一個包括HTTP模式(schema)和主機名的完整URL。我們使用驗證后的表單數據構造郵件的主題和內容,最后發送郵件到表單to
字段中的郵件地址。
現在,視圖的開發工作已經完成,記得為它添加新的URL模式。打開blog
應用的urls.py
文件,添加post_share
的URL模式:
urlpatterns = [
# ...
url(r'^(?P<post_id>\d+)/share/$', views.post_share, name='post_share'),
]
2.1.4 在模板中渲染表單
完成創建表單,編寫視圖和添加URL模式后,我們只缺少該視圖的模板了。在blog/templates/blog/post/
目錄中創建share.html
文件,添加以下代碼:
{% extends "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 }}
{% csrf_token %}
<input type="submit" value="Send e-mail">
</form>
{% endif %}
{% endblock %}
這個模板用于顯示表單,或者表單發送后的一條成功消息。正如你所看到的,我們創建了一個HTML表單元素,指定它需要使用POST
方法提交:
<form action="." method="post">
然后,我們包括了實際的表單實例。我們告訴Django使用as_p
方法,在HTML的<p>
元素中渲染表單的字段。我們也可以使用as_ul
把表單渲染為一個無序列表,或者使用as_table
渲染為HTML表格。如果你想渲染每一個字段,我們可以這樣迭代字段:
{% for field in form %}
<div>
{{ field.errors }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}
模板標簽{% csrf_token %}
使用自動生成的令牌引入一個隱藏字段,以避免跨站點請求偽造(CSRF)的攻擊。這些攻擊包含惡意網站或程序,對你網站上的用戶執行惡意操作。你可以在這里找到更多相關的信息。
上述標簽生成一個類似這樣的隱藏字段:
<input type="hidden" name="csrfmiddlewaretoken" value="26JjKo2lcEtYkGoV9z4XmJIEHLXN5LDR" />
默認情況下,Django會檢查所有
POST
請求中的CSRF令牌。記得在所有通過POST
提交的表單中包括csrf_token
標簽。
編輯blog/post/detail.html
模板,在{{ post.body|linebreaks }}
變量之后添加鏈接,用于分享帖子的URL:
<p>
<a href="{% url "blog:post_share" post.id %}">
Share this post
</a>
</p>
記住,我們使用Django提供的{% url %}
模板標簽,動態生成URL。我們使用名為blog
命名空間和名為post_share
的URL,并傳遞帖子ID作為參數來構造絕對路徑的URL。
現在,使用python manage.py runserver
命令啟動開發服務器,并在瀏覽器中打開http://127.0.0.1:8000/blog/
。點擊任何一篇帖子的標題,打開詳情頁面。在帖子正文下面,你會看到我們剛添加的鏈接,如下圖所示:
點擊Share this post
,你會看到一個包含表單的頁面,該頁面可以通過郵件分享帖子。如下圖所示:
該表單的CSS樣式在static/css/blog.css
文件中。當你點擊Send e-mail
按鈕時,該表單會被提交和驗證。如果所有字段都是有效數據,你會看到一條成功消息,如下圖所示:
如果你輸入了無效數據,會再次渲染表單,其中包括了所有驗證錯誤:
譯者注:不知道是因為瀏覽器不同,還是Django的版本不同,這里顯示的驗證錯誤跟原書中不一樣。我用的是Chrome瀏覽器。
2.2 創建評論系統
現在,我們開始為博客構建評論系統,讓用戶可以評論帖子。要構建評論系統,你需要完成以下工作:
- 創建一個保存評論的模型
- 創建一個提交表單和驗證輸入數據的表單
- 添加一個視圖,處理表單和保存新評論到數據庫中
- 編輯帖子詳情模板,顯示評論列表和添加新評論的表單
首先,我們創建一個模型存儲評論。打開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
模型。它包含一個外鍵,把評論與單篇帖子關聯在一起。這個多對一的關系在Comment
模型中定義,因為每條評論對應一篇帖子,而每篇帖子可能有多條評論。從關聯對象反向到該對象的關系由related_name
屬性命名。定義這個屬性后,我們可以使用comment.post
檢索評論對象的帖子,使用post.comments.all()
檢索帖子的所有評論。如果你沒有定義related_name
屬性,Django會使用模型名加_set
(即comment_set
)命名關聯對象反向到該對象的管理器。
你可以在這里學習更多關于多對一的關系。
我們使用了active
布爾字段,用于手動禁用不合適的評論。我們使用created
字段排序評論,默認按時間排序。
剛創建的Comment
模型還沒有同步到數據庫。運行以下命令,生成一個新的數據庫遷移,反射創建的新模型:
python manage.py makemigrations blog
你會看到以下輸出:
Migrations for 'blog'
0002_comment.py:
- Create model Comment
Django在blog
應用的migrations/
目錄中生成了0002_comment.py
文件。現在,你需要創建一個相關的數據庫架構,并把這些改變應用到數據庫中。運行以下命令,讓已存在的數據庫遷移生效:
python manage.py migrate
你會得到一個包括下面這一行的輸出:
Apply blog.0002_comment... OK
我們剛創建的數據庫遷移已經生效,數據庫中已經存在一張新的blog_comment
表。
現在我們可以添加新的模型到管理站點,以便通過簡單的界面管理評論。打開blog
應用的admin.py
文件,導入Comment
模型,并增加CommentAdmin
類:
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')
admin.site.register(Comment, CommentAdmin)
使用python manage.py runserver
命令啟動開發服務器,并在瀏覽器中打開http://127.0.0.1:8000/admin/
。你會在Blog
中看到新的模型,如下圖所示:
我們的模型已經在管理站點注冊,并且可以使用簡單的界面管理Comment
實例。
2.2.1 通過模型創建表單
我們仍然需要創建一個表單,讓用戶可以評論博客的帖子。記住,Django有兩個基礎類用來創建表單:Form
和ModelForm
。之前你使用了第一個,讓用戶可以通過郵件分享帖子。在這里,你需要使用ModelForm
,因為你需要從Comment
模型中動態的創建表單。編輯blog
應用的forms.py
文件,添加以下代碼:
from .models import Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ('name', 'email', 'body')
要通過模型創建表單,我們只需要在表單的Meta
類中指定,使用哪個模型構造表單。Django自省模型,并動態的為我們創建表單。每種模型字段類型都有相應的默認表單字段類型。我們定義模型字段的方式考慮了表單的驗證。默認情況下,Django為模型中的每個字段創建一個表單字段。但是,你可以使用fields
列表明確告訴框架,你想在表單中包含哪些字段,或者使用exclude
列表定義你想排除哪些字段。對應CommentForm
,我們只使用name
,email
,和body
字段,因為用戶只可能填寫這些字段。
2.2.2 在視圖中處理ModelForm
為了簡單,我們將會使用帖子詳情頁面實例化表單,并處理它。編輯views.py
文件,導入Comment
模型和CommentForm
表單,并修改post_detail
視圖,如下所示:
譯者注:原書中是編輯
models.py
文件,應該是作者的筆誤。
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,
publish__month=month,
publish__day=day)
# List of active comments for this post
comments = post.comments.filter(active=True)
new_comment = None
if request.method == 'POST':
# A comment was posted
comment_form = CommentForm(data=request.POST)
if comment_form.is_valid():
# Create Comment object but don't save to database yet
new_comment = comment_form.save(commit=False)
# Assign the current post to comment
new_comment.post = post
# Save the comment to the database
new_comment.save()
else:
comment_form = CommentForm()
return render(request,
'blog/post/detail.html',
{'post': post,
'comments': comments,
'new_comment': new_comment,
'comment_form': comment_form})
讓我們回顧一下,我們往視圖里添加了什么。我們使用post_detail
視圖顯示帖子和它的評論。我們添加了一個QuerySet
,用于檢索該帖子所有有效的評論:
comments = post.comments.filter(active=True)
我們從post
對象開始創建這個QuerySet
。我們在Comment
模型中使用related_name
屬性,定義了關聯對象的管理器為comments
。這里使用了這個管理器。
同時,我們使用同一個視圖讓用戶添加新評論。因此,如果視圖通過GET
調用,我們使用comment_form = CommentForm()
創建一個表單實例。如果是POST
請求,我們使用提交的數據實例化表單,并使用is_valid()
方法驗證。如果表單無效,我們渲染帶有驗證錯誤的模板。如果表單有效,我們完成以下操作:
- 通過調用表單的
save()
方法,我們創建一個新的Comment
對象:
new_comment = comment_form.save(commit=False)
save()
方法創建了一個鏈接到表單模型的實例,并把它存到數據庫中。如果使用commit=False
調用,則只會創建模型實例,而不會存到數據庫中。當你想在存儲之前修改對象的時候,會非常方便,之后我們就是這么做的。save()
只對ModelForm
實例有效,對Form
實例無效,因為它們沒有鏈接到任何模型。
- 我們把當前的帖子賦值給剛創建的評論:
new_comment.post = post
通過這個步驟,我們指定新評論屬于給定的帖子。
- 最后,使用下面的代碼,把新評論存到數據庫中:
new_comment.save()
現在,我們的視圖已經準備好了,可以顯示和處理新評論了。
2.2.3 在帖子詳情模板中添加評論
我們已經為帖子創建了管理評論的功能?,F在我們需要修改blog/post/detail.html
模板,完成以下工作:
- 為帖子顯示評論總數
- 顯示評論列表
- 顯示一個表單,用戶增加評論
首先,我們會添加總評論數。打開detail.html
模板,在content
塊中添加以下代碼:
{% with comments.count as total_comments %}
<h2>
{{ total_comments }} comment{{ total_comments|pluralize }}
</h2>
{% endwith %}
我們在模板中使用Django ORM執行comments.count()
這個QuerySet
。注意,Django模板語言調用方法時不帶括號。{% with %}
標簽允許我們把值賦給一個變量,我們可以在{% endwith %}
標簽之前一直使用它。
{% with %}
模板標簽非常有用,它可以避免直接操作數據庫,或者多次調用昂貴的方法。
我們使用了pluralize
模板過濾器,根據total_comments
的值決定是否顯示單詞comment
的復數形式。模板過濾器把它們起作用變量的值作為輸入,并返回一個計算后的值。我們會在第三章討論模板過濾器。
如果值不是1,pluralize
模板過濾器會顯示一個“s”。上面的文本會渲染為0 comments
,1 comment
,或者N comments
。Django包括大量的模板標簽和過濾器,可以幫助你以希望的方式顯示信息。
現在,讓我們添加評論列表。在上面代碼后面添加以下代碼:
{% 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 %}
模板標簽循環所有評論。如果comments
列表為空,顯示一個默認消息,告訴用戶該帖子還沒有評論。我們使用{{ forloop.counter }}
變量枚舉評論,它包括每次迭代中循環的次數。然后我們顯示提交評論的用戶名,日期和評論的內容。
最后,當表單成功提交后,我們需要渲染表單,或者顯示一條成功消息。在上面的代碼之后添加以下代碼:
{% 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 %}
代碼非常簡單:如果new_comment
對象存在,則顯示一條成功消息,因為已經創建評論成功。否則渲染表單,每個字段使用一個<p>
元素,以及POST
請求必需的CSRF令牌。在瀏覽器中打開http://127.0.0.1:8000/blog/
,點擊一條帖子標題,打開詳情頁面,如下圖所示:
使用表單添加兩條評論,它們會按時間順序顯示在帖子下方,如下圖所示:
在瀏覽器中打開http://127.0.0.1:8000/admin/blog/comment/
,你會看到帶有剛創建的評論列表的管理頁面。點擊某一條編輯,不選中Active
選擇框,然后點擊Save
按鈕。你會再次被重定向到評論列表,該評論的Active
列會顯示一個禁用圖標。類似下圖的第一條評論:
如果你回到帖子詳情頁面,會發現被刪除的評論沒有顯示;同時也沒有算在評論總數中。多虧了active
字段,你可以禁用不合適的評論,避免它們在帖子中顯示。
2.3 增加標簽功能
實現評論系統之后,我們準備為帖子添加標簽。我們通過在項目中集成一個第三方的Django標簽應用,來實現這個功能。django-taggit
是一個可復用的應用,主要提供了一個Tag
模型和一個管理器,可以很容易的為任何模型添加標簽。你可以在這里查看它的源碼。
首先,你需要通過pip
安裝django-taggit
,運行以下命令:
pip install django-taggit
然后打開mysite
項目的settings.py
文件,添加taggit
到INSTALLED_APPS
設置中:
INSTALLED_APPS = (
# ...
'blog',
'taggit',
)
打開blog
應用的models.py
文件,添加django-taggit
提供的TaggableManager
管理器到Post
模型:
from taggit.managers import TaggableManager
class Post(models.Model):
# ...
tags = TaggableManager()
tags
管理器允許你從Post
對象中添加,檢索和移除標簽。
運行以下命令,為模型改變創建一個數據庫遷移:
python manage.py makemigrations blog
你會看下以下輸出:
Migrations for 'blog'
0003_post_tags.py:
- Add field tags to post
現在,運行以下命令創建django-taggit
模型需要的數據庫表,并同步模型的變化:
python manage.py migrate
你會看到遷移數據庫生效的輸入,如下所示:
Applying taggit.0001_initial... OK
Applying taggit.0002_auto_20150616_2121... OK
Applying blog.0003_post_tags... OK
你的數據庫已經為使用django-taggit
模型做好準備了。使用python manage.py shell
打開終端,學習如何使用tags
管理器。
首先,我檢索其中一個帖子(ID為3的帖子):
>>> from blog.models import Post
>>> post = Post.objects.get(id=3)
接著給它添加標簽,并檢索它的標簽,檢查是否添加成功:
>>> post.tags.add('music', 'jazz', 'django')
>>> post.tags.all()
[<Tag: jazz>, <Tag: django>, <Tag: music>]
最后,移除一個標簽,并再次檢查標簽列表:
>>> post.tags.remove('django')
>>> post.tags.all()
[<Tag: jazz>, <Tag: music>]
這很容易,對吧?運行python manage.py runserver
,再次啟動開發服務器,并在瀏覽器中打開http://127.0.0.1:8000/admin/taggit/tag/
。你會看到taggit
應用管理站點,其中包括Tag
對象的列表:
導航到http://127.0.0.1:8000/admin/blog/post/
,點擊一條帖子編輯。你會看到,現在帖子包括一個新的Tags
字段,如下圖所示,你可以很方便的編輯標簽:
現在,我們將會編輯博客帖子,來顯示標簽。打開blog/post/list.html
模板,在帖子標題下面添加以下代碼:
<p class="tags">Tags: {{ post.tags.all|join:", " }}</p>
模板過濾器join
與Python字符串的join()
方法類似,用指定的字符串連接元素。在瀏覽器中打開http://127.0.0.1:8000/blog/
。你會看到每篇帖子標題下方有標簽列表:
現在,我們將要編輯post_list
視圖,為用戶列出具有指定標簽的所有帖子。打開blog
應用的views.py
文件,從django-taggit
導入Tag
模型,并修改post_list
視圖,可選的通過標簽過濾帖子:
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])
# ...
該視圖是這樣工作的:
- 該視圖接收一個默認值為
None
的可選參數tag_slug
。該參數會在URL中。 - 在視圖中,我們創建了初始的
QuerySet
,檢索所有已發布的帖子,如果給定了標簽別名,我們使用get_object_or_404()
快捷方法獲得給定別名的Tag
對象。 - 然后,我們過濾包括給定標簽的帖子列表。因為這是一個多對多的關系,所以我們需要把過濾的標簽放在指定列表中,在這個例子中只包含一個元素。
記住,QeurySet
是懶惰的。這個QuerySet
只有在渲染模板時,循環帖子列表時才會計算。
最后,修改視圖底部的render()
函數,傳遞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])
paginator = Paginator(object_list, 3)
page = request.GET.get('page')
try:
posts = paginator.page(page)
except PageNotAnInteger:
posts = paginator.page(1)
excpet EmptyPage:
posts = paginator.page(paginator.num_pages)
return render(request,
'blog/post/list.html',
{'page': page,
'posts': posts,
'tag': tag})
打開blog
應用的urls.py
文件,注釋掉基于類PostListView
的URL模式,取消post_list
視圖的注釋:
url(r'^$', views.post_list, name='post_list'),
# url(r'^$', views.PostListView.as_view(), name='post_list'),
添加以下URL模式,通過標簽列出帖子:
url(r'^tag/(?P<tag_slug>[-\w]+)/$', views.post_list,
name='post_list_by_tag'),
正如你所看到的,兩個模式指向同一個視圖,但是名稱不一樣。第一個模式不帶任何可選參數調用post_list
視圖,第二個模式使用tag_slug
參數調用視圖。
因為我們使用的是post_list
視圖,所以需要編輯blog/post/list.hmlt
模板,修改pagination
使用posts
參數:
{% include "pagination.html" with page=posts %}
在{% for %}
循環上面添加以下代碼:
{% if tag %}
<h2>Posts tagged with "{{ tag.name }}"</h2>
{% endif %}
如果用戶正在訪問博客,他會看到所有帖子列表。如果他通過指定標簽過濾帖子,就會看到這個信息?,F在,修改標簽的顯示方式:
<p class="tag">
Tags:
{% for tag in post.tags.all %}
<a href="{% url "blog:post_list_by_tag" tag.slug %}">
{{ tag.name }}
</a>
{% if not forloop.last %}, {% endif %}
{% endfof %}
</p>
現在,我們循環一篇帖子的所有標簽,顯示一個自定義鏈接到URL,以便使用該便簽過濾帖子。我們用{% url "blog:post_list_by_tag" tag.slug %}
構造URL,把URL名和標簽的別名作為參數。我們用逗號分隔標簽。
在瀏覽器中打開http://127.0.0.1:8000/blog/
,點擊某一個標簽鏈接。你會看到由該標簽過濾的帖子列表:
2.4 通過相似度檢索帖子
現在,我們已經為博客帖子添加了標簽,我們還可以用標簽做更多有趣的事。通過便簽,我們可以很好的把帖子分類。主題類似的帖子會有幾個共同的標簽。我們準備增加一個功能:通過帖子共享的標簽數量來顯示類似的帖子。在這種情況下,當用戶閱讀一篇帖子的時候,我們可以建議他閱讀其它相關帖子。
為某個帖子檢索相似的帖子,我們需要:
- 檢索當前帖子的所有標簽。
- 獲得所有帶這些便簽中任何一個的帖子。
- 從列表中排除當前帖子,避免推薦同一篇帖子。
- 通過和當前帖子共享的標簽數量來排序結果。
- 如果兩篇或以上的帖子有相同的標簽數量,推薦最近發布的帖子。
- 限制我們想要推薦的帖子數量。
這些步驟轉換為一個復雜的QuerySet
,我們需要在post_detail
視圖中包含它。打開blog
應用的views.py
文件,在頂部添加以下導入:
from django.db.models import Count
這是Django ORM的Count
匯總函數。此函數允許我們執行匯總計數。然后在post_detail
視圖的render()
函數之前添加以下代碼:
# List of similar posts
post_tags_ids = post.tags.values_list('id', flat=True)
similar_posts = Post.published.filter(tags__in=post_tags_ids)\
.exclude(id=post.id)
similar_posts = similar_posts.annotate(same_tags=Count('tags'))\
.order_by('-same_tags', '-publish')[:4]
這段代碼完成以下操作:
- 我們獲得一個包含當前帖子所有標簽的ID列表。
values_list()
這個QuerySet
返回指定字段值的元組。我們傳遞flat=True
給它,獲得一個[1, 2, 3, ...]
的列表。 - 我們獲得包含這些標簽中任何一個的所有帖子,除了當前帖子本身。
- 我們使用
Count
匯總函數生成一個計算后的字段same_tags
,它包含與所有查詢標簽共享的標簽數量。 - 我們通過共享的標簽數量排序結果(降序),共享的標簽數量相等時,用
publish
優先顯示最近發布的帖子。我們對結果進行切片,只獲取前四篇帖子。
為render()
函數添加similar_posts
對象到上下文字典中:
return render(request,
'blog/post/detail.html',
{'post': post,
'comments': comments,
'new_comment':new_comment,
'comment_form': comment_form,
'similar_posts': similar_posts})
現在,編輯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 post yet.
{% endfor %}
推薦你在帖子詳情模板中也添加標簽列表,就跟我們在帖子列表模板中所做的那樣?,F在,你的帖子詳情頁面應該看起來是這樣的:
譯者注:需要給其它帖子添加標簽,才能看到上圖所示的相似的帖子。
你已經成功的推薦了相似的帖子給用戶。django-taggit
也包含一個similar_objects()
管理器,可以用來檢索共享的標簽。你可以在這里查看所有django-taggit
管理器。
2.5 總結
在這一章中,你學習了如何使用Django表單和模型表單。你創建了一個可以通過郵件分享網站內容的系統,還為博客創建了評論系統。你為帖子添加了標簽,集成了一個可復用的應用,并創建了一個復雜的QuerySet
,通過相似度檢索對象。
下一章中,你會學習如何創建自定義模板標簽和過濾器。你還會構建一個自定義的站點地圖和帖子的RSS源,并在應用中集成一個高級的搜索引擎。