Django2實戰示例 第二章 增強博客功能


目錄

Django2實戰示例 第一章 創建博客應用
Django2實戰示例 第二章 增強博客功能
Django2實戰示例 第三章 擴展博客功能
Django2實戰示例 第四章 創建社交網站
Django2實戰示例 第五章 內容分享功能
Django2實戰示例 第六章 追蹤用戶行為
Django2實戰示例 第七章 創建電商網站
Django2實戰示例 第八章 管理支付與訂單
Django2實戰示例 第九章 擴展商店功能
Django2實戰示例 第十章 創建在線教育平台
Django2實戰示例 第十一章 渲染和緩存課程內容
Django2實戰示例 第十二章 創建API
Django2實戰示例 第十三章 上線

第二章 增強博客功能

在之前的章節創建基礎的博客應用,現在可以變成具備通過郵件分享文章,帶有評論和標簽系統等功能的完整博客。在這一章會學習到如下內容:

  • 使用Django發送郵件
  • 創建表單並且通過視圖控制表單
  • 通過模型生成表單
  • 集成第三方應用
  • 更復雜的ORM查詢

1通過郵件分享文章

首先來制作允許用戶通過郵件分享文章鏈接的功能。在開始之前,先想一想你將如何使用視圖、URLs和模板來實現這個功能,然后看一看你需要做哪些事情:

  • 創建一個表單供用戶填寫名稱和電子郵件地址收件人,以及評論等。
  • views.py中創建一個視圖控制這個表單,處理接收到的數據然后發送電子郵件
  • 為新的視圖在urls.py中配置URL
  • 創建展示表單的模板

1.1使用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)

這是使用forms類創建的第一個標准表單,通過繼承內置Form類,然后設置字段為各種類型,用於驗證數據。

表單可以編寫在項目的任何位置,但通常將其編寫在對應應用的forms.py文件中。

name字段是Charfield類型,會被渲染為<input type="text">HTML標簽。每個字段都有一個默認的widget參數決定該字段被渲染成的HTML元素類型,可以通過widget參數改寫。在comments字段中,使用了widget=forms.Textarea令該字段被渲染為一個<textarea>元素,而不是默認的<input>元素。

字段驗證也依賴於字段屬性。例如:emailto字段都是EmailField類型,兩個字段都接受一個有效的電子郵件格式的字符串,否則這兩個字段會拋出forms.ValidationError錯誤。表單里還存在的驗證是:name字段的最大長度maxlength是25個字符,comments字段的required=False表示該字段可以沒有任何值。所有的這些設置都會影響到表單驗證。本表單只使用了很少一部分的字段類型,關於所有表單字段可以參考https://docs.djangoproject.com/en/2.0/ref/forms/fields/

1.2通過視圖控制表單

現在需要寫一個視圖,用於處理表單提交來的數據,當表單成功提交的時候發送電子郵件。編輯blog應用的views.py文件:

from .forms import EmailPostForm

def post_share(request, post_id):
    # 通過id 獲取 post 對象
    post = get_object_or_404(Post, id=post_id, status='published')
    if request.method == "POST":
        # 表單被提交
        form = EmailPostForm(request.POST)
        if form.is_valid():
            # 驗證表單數據
            cd = form.cleaned_data
            # 發送郵件......
    else:
        form = EmailPostForm()
    return render(request, 'blog/post/share.html', {'post': post, 'form': form})

這段代碼的邏輯如下:

  • 定義了post_share視圖,參數是request對象和post_id
  • 使用get_object_or_404()方法,通過ID和published取得所有已經發布的文章中對應ID的文章。
  • 這個視圖同時用於顯示空白表單和處理提交的表單數據。我們先通過request.method判斷當前請求是POST還是GET請求。如果是GET請求,展示一個空白表單;如果是POST請求,需要處理表單數據。

處理表單數據的過程如下:

  1. 視圖收到GET請求,通過form = EmailPostForm()創建一個空白的form對象,展示在頁面中是一個空白的表單供用戶填寫。
  2. 用戶填寫並通過POST請求提交表單,視圖使用request.POST中包含的表單數據創建一個表單對象:
if request.method == 'POST':
    # 表單被提交
    form = EmailPostForm(request.POST)
  1. 在上一步之后,調用表單對象的is_valid()方法。這個方法會驗證表單中所有的數據是否有效,如果全部通過驗證會返回True,任意一個字段未通過驗證,is_valid()就會返回False。如果返回False,此時可以在form.errors屬性中查看錯誤信息。
  2. 如果表單驗證失敗,我們將這個表單對象渲染回頁面,頁面中會顯示錯誤信息。
  3. 如果表單驗證成功,可以通過form.cleaned_data屬性訪問表單內所有通過驗證的數據,這個屬性類似於一個字典,包含字段名與值構成的鍵值對。

如果表單驗證失敗,form.cleaned_data只會包含通過驗證的數據。

現在就可以來學習如何使用Django發送郵件了。

1.3使用Django發送郵件

使用Django發送郵件比較簡單,需要一個本地或者外部的SMTP服務器,然后在settings.py文件中加入如下設置:

  • EMAIL_HOST:郵件主機,默認是localhost
  • EMAIL_PORT:SMTP服務端口,默認是25
  • EMAIL_HOST_USER:SMTP服務器的用戶名
  • EMAIL_HOST_PASSWORD:SMTP服務器的密碼
  • EMAIL_USE_TLS:是否使用TLS進行連接
  • EMAIL_USE_SSL:是否使用SSL進行連接

如果無法使用任何SMTP服務器,則可以將郵件打印在命令行窗口中,在settings.py中加入下列這行:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

這樣會把所有的郵件內容顯示在控制台,非常便於測試。

如果沒有本地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,在命令行環境中試驗一下發送郵件的指令:

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,就說明郵件成功發送。

如果采用以上設置無法成功使用Google的郵件服務,需要到https://myaccount.google.com/lesssecureapps,啟用“允許不夠安全的應用”,如下圖所示:

image

現在我們把發送郵件的功能加入到視圖中,編輯views.py中的post_share視圖函數:

def post_share(request, post_id):
    # 通過id 獲取 post 對象
    post = get_object_or_404(Post, id=post_id, status='published')
    sent = False

    if request.method == "POST":
        # 表單被提交
        form = EmailPostForm(request.POST)
        if form.is_valid():
            # 表單字段通過驗證
            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, 'lee0709@vip.sina.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()方法獲取被分享文章的URL,然后將其作為request.build_absolute_uri()的參數轉為完整的URL,再加上表單數據創建郵件正文,最后將郵件發送給to字段中的收件人。

還需要給視圖配置URL,打開blog應用中的urls.py,加一條post_share的URL pattern:

urlpatterns = [
    # ...
    path('<int:post_id>/share/', views.post_share, name='post_share'),
]

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 {{ form.cleaned_data.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">

之后渲染表單實例,通過使用as_p方法,將表單中的所有元素以

元素的方式展現出來。還可以使用as_ulas_table分別以列表和表格的形式顯示。如果想分別渲染每個表單元素,可以迭代表單對象中的每個元素,例如這樣:

{% for field in form %}
    <div>
        {{ field.errors }}
        {{ field.label_tag }} {{ field }}
    </div>
{% endfor %}

{% csrf_token %}在頁面中顯示為一個隱藏的input元素,是一個自動生成的防止跨站請求偽造(CSRF)攻擊的token。跨站請求偽造是一種冒充用戶在已經登錄的Web網站上執行非用戶本意操作的一種攻擊方式,可能由其他網站或一段程序發起。關於CRSF的更多信息可以參考https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)

例子生成的隱藏字段類似如下:

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

Django默認會對所有POST請求進行CSRF檢查,在所有POST方式提交的表單中,都要添加csrf_token

修改blog/post/detail.html將下列鏈接增加到{{ post.body|linebreaks }}之后:

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

這里的{% url %}標簽,其功能和在視圖中使用的reverse()方法類似,使用URL的命名空間blog和URL命名post_share,再傳入一個ID作為參數,就可以構建出一個URL。在頁面渲染時,{% url %}就會被渲染成反向解析出的URL。

現在使用python manage.py runserver啟動站點,打開http://127.0.0.1:8000/blog/,點擊任意文章查看詳情頁,在文章的正文下會出現分享鏈接,如下所示:

image

點擊 Share this post 鏈接,可以看到分享頁面:

image

這個表單的CSS樣式表文件位於static/css/blog.css。當你點擊SEND E-MAIL按鈕的時候,就會提交表單並驗證數據,如果有錯誤,可以看到頁面如下:

image

在某些現代瀏覽器上,很有可能瀏覽器會阻止你提交表單,提示必須完成某些字段,這是因為瀏覽器在提交根據表單的HTML元素屬性先進行了驗證。現在通過郵件分享鏈接的功能制作完成了,下一步是創建一個評論系統。

關閉瀏覽器驗證的方法是給表單添加novalidate屬性:<form action="" novalidate>

2創建評論系統

現在,我們要創建一個評論系統,讓用戶可以對文章發表評論。創建評論系統需要進行以下步驟:

  1. 創建一個模型用於存儲評論
  2. 創建一個表單用於提交評論和驗證數據
  3. 創建一個視圖用於處理表單和將表單數據存入數據庫
  4. 編輯文章詳情頁以展示評論和提供增加新評論的表單

首先來創建評論對應的數據模型,編輯blog應用的models.py文件,添加以下代碼:

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, 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模型包含一個外鍵(ForeignKey)用於將評論與一個文章聯系起來,定義了文章和評論的一對多關系,即一個文章下邊可以有多個評論;外鍵的related_name參數定義了在通過文章查找其評論的時候引用該關聯關系的名稱。這樣定義了該外鍵之后,可以通過comment.post獲得一條評論對應的文章,通過post.comments.all()獲得一個文章對應的所有評論。如果不定義related_name,Django會使用模型的小寫名加上_setcomment_set)來作為反向查詢的管理器名稱。

關於一對多關系的可以參考https://docs.djangoproject.com/en/2.0/topics/db/examples/many_to_one/

模型還包括一個active布爾類型字段,用於手工關閉不恰當的評論;還指定了排序方式為按照created字段進行排序。

新的Comment模型還沒有與數據庫同步,執行以下命令創建遷移文件:

python manage.py makemigrations blog

會看到如下輸出:

Migrations for 'blog':
  blog/migrations/0002_comment.py
    - Create model Comment

Django在migrations/目錄下創建了0002_comment.py文件,現在可以執行實際的遷移命令將模型寫入數據庫:

python manage.py migrate

會看到以下輸出:

Applying blog.0002_comment... OK

數據遷移的過程結束了,數據庫中新創建了名為blog_comment的數據表。

譯者注:數據遷移的部分在原書中重復次數太多,而且大部分無實際意義,如無需要特殊說明的地方,以下翻譯將略過類似的部分,以“執行數據遷移”或類似含義的字樣替代。

創建了模型之后,可以將其加入管理后台。打開blog應用中的admin.py文件,導入Comment模型,然后增加如下代碼:

from .models import Post, Comment

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ('name', 'email', 'post', 'created', 'active')
    list_filter = ('active', 'created', 'updated')
    search_fields = ('name', 'email', 'body')

啟動站點,到http://127.0.0.1:8000/admin/查看管理站點,會看到新的模型已經被加入到管理后台中:

image

2.1根據模型創建表單

在發送郵件的功能里,采用繼承forms.Form類的方式,自行編寫各個字段創建了一個表單。Django對於表單有兩個類:FormModelForm。這次我們使用ModelForm動態的根據Comment模型生成表單。編輯blog應用的forms.py文件:

from .models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('name', 'email', 'body')

依據模型創建表單,只需要在Meta類中指定基於哪個類即可。Django會自動內省該類然后創建對應的表單。我們對於模型字段的設置會影響到表單數據的驗證規則。默認情況下,Django對每一個模型字段都創建一個對應的表單元素。然而,可以顯示的通過Meta類中的fields屬性指定需要創建表單元素的字段,或者使用exclude屬性指定需要排除的字段。對於我們的CommentForm類,我們指定了表單只需要包含nameemailbody字段即可。

2.2在視圖中處理表單

由於提交評論的動作在文章詳情頁發生,所以把處理表單的功能整合進文章詳情視圖中會讓代碼更簡潔。編輯views.py文件,導入Comment模型然后修改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, publish__month=month, publish__day=day)
    # 列出文章對應的所有活動的評論
    comments = post.comments.filter(active=True)

    new_comment = None

    if request.method == "POST":
        comment_form = CommentForm(data=request.POST)
        if comment_form.is_valid():
            # 通過表單直接創建新數據對象,但是不要保存到數據庫中
            new_comment = comment_form.save(commit=False)
            # 設置外鍵為當前文章
            new_comment.post = post
            # 將評論數據對象寫入數據庫
            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)

Comments類中定義的外鍵的related_name屬性的名稱作為管理器,對post對象執行查詢從而得到了所需的QuerySet。

同時還為這個視圖增加了新增評論的功能。初始化了一個new_comment變量為None,用於標記一個新評論是否被創建。如果是GET請求,使用comment_form = CommentForm()創建空白表單;如果是POST請求,使用提交的數據生成表單對象並調用is_valid()方法進行驗證。如果表單未通過驗證,使用當前表單渲染頁面以提供錯誤信息。如果表單通過驗證,則進行如下工作:

  1. 調用當前表單的save()方法生成一個Comment實例並且賦給new_comment變量,就是下邊這一行:
new_comment = comment_form.save(commit=False)
表單對象的`save()`方法會返回一個由當前數據構成的,表單關聯的數據類的對象,並且會將這個對象寫入數據庫。如果指定`commit=False`,則數據對象會被創建但不會被寫入數據庫,便於在保存到數據庫之前對對象進行一些操作。
  1. comment對象的外鍵關聯指定為當前文章:new_comment.post = post,這樣就明確了當前的評論是屬於這篇文章的。
  2. 最后,調用save()方法將新的評論對象寫入數據庫:new_comment.save()

save()方法僅對ModelForm生效,因為Form類沒有關聯到任何數據模型。

2.3為文章詳情頁面添加評論

已經創建了用於管理一個文章的評論的視圖,現在需要修改post/detail.html來做以下的事情:

  • 展示當前文章的評論總數
  • 列出所有評論
  • 展示表單供用戶添加新評論

首先要增加評論總數,編輯post/detail.html,將下列內容追加在content塊內的底部:

{% with comments.count as total_comments %}
    <h2>
        {{ total_comments }} comment{{ total_comments|pluralize }}
    </h2>
{% endwith %}

我們在模板里使用了Django ORM,執行了comments.count()。在模板中執行一個對象的方法時,不需要加括號;也正因為如此,不能夠執行必須帶有參數的方法。{% with %}標簽表示在{% endwith %}結束之前,都可以使用一個變量來代替另外一個變量或者值。

{% with %}標簽經常用於避免反復對數據庫進行查詢和向模板傳入過多變量。

這里使用了pluralize模板過濾器,用於根據total_comments的值顯示復數詞尾。將在下一章詳細討論模板過濾器。

如果值大於1,pluralize過濾器會返回一個帶復數詞尾"s"的字符串,實際渲染出的字符串會是0 comments1 comment2 comments或者N comments

然后來增加評論列表的部分,在post/detail.html中上述代碼之后繼續追加:

{% 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對象存在,顯示一條成功信息,其他情況下則用as_p方法渲染整個表單以及CSRF token。在瀏覽器中打開http://127.0.0.1:8000/blog/,可以看到如下頁面:

image

使用表單添加一些評論,然后刷新頁面,應該可以看到評論以發布的時間排序:

image

在瀏覽器中打開http://127.0.0.1:8000/admin/blog/comment/,可以在管理后台中看到所有評論,點擊其中的一個進行編輯,取消掉Active字段的勾,然后點擊SAVE按鈕。然后會跳回評論列表,Acitve欄會顯示一個紅色叉號表示該評論未被激活,如下圖所示:

image

此時返回文章詳情頁,可以看到被設置為未激活的評論不會顯示出來,也不會被統計到評論總數中。由於有了這個active字段,可以非常方便的控制評論顯示與否而不需要實際刪除。

3添加標簽功能

在完成了評論系統之后,我們將來給文章加上標簽系統。標簽系統通過集成django-taggit第三方應用模塊到我們的Django項目來實現,django-taggit提供一個Tag數據模型和一個管理器,可以方便的給任何模型加上標簽。django-taggit的源代碼位於:https://github.com/alex/django-taggit

通過pip安裝django-taggit

pip install django_taggit==0.22.2

譯者注:如果安裝了django 2.1或更新版本,請下載最新版 django-taggit。原書的0.22.2版只能和Django 2.0.5版搭配使用。新版使用方法與0.22.2版沒有任何區別。

之后在setting.py里的INSTALLED_APPS設置中增加taggit以激活該應用:

INSTALLED_APPS = [
    # ...
    'blog.apps.BlogConfig',
    'taggit',
]

打開blog應用下的models.py文件,將django-taggit提供的TaggableMananger模型管理器加入到Post模型中:

from taggit.managers import TaggableManager

class Post(models.Model):
    # ......
    tags=TaggableManager()

這個管理器可以對Post對象的標簽進行增刪改查。然后執行數據遷移。

現在數據庫也已經同步完成了,先學習一下如何使用django-taggit模塊和其tags管理器。使用python manage.py shell進入Python命令行然后輸入下列命令:

之后來看如何使用,先到命令行里:

>>> from blog.models import Post
>>> post = Post.objects.get(id=1)

然后給這個文章增加一些標簽,然后再獲取這些標簽看一下是否添加成功:

>>> post.tags.add('music', 'jazz', 'django')
>>> post.tags.all()
<QuerySet [<Tag: jazz>, <Tag: music>, <Tag: django>]>

刪除一個標簽再檢查標簽列表:

>>> post.tags.remove('django')
>>> post.tags.all()
<QuerySet [<Tag: jazz>, <Tag: music>]>

操作很簡單。啟動站點然后到http://127.0.0.1:8000/admin/taggit/tag/,可以看到列出taggit應用中Tag對象的管理頁面:

image

http://127.0.0.1:8000/admin/blog/post/點擊一篇文章進行修改,可以看到文章現在包含了一個標簽字段,如下所示:

image

現在還需要在頁面上展示標簽,編輯blog/post/list.html,在顯示文章的標題下邊添加:

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

join過濾器的功能和Python字符串的join()方法很類似,打開http://127.0.0.1:8000/blog/,就可以看到在每個文章的標題下方列出了標簽:

image

現在來編輯post_list視圖,讓用戶可以根據一個標簽列出具備該標簽的所有文章,打開blog應用的views.py文件,從django-taggit中導入Tag模型,然后修改post_list視圖,讓其可以額外的通過標簽來過濾文章:

from taggit.models import Tag

def post_list(request, tag_slug=None):
    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) # 3 posts in each page
    # ......

post_list視圖現在工作如下:

  1. 多接收一個tag_slug參數,默認值為None。這個參數將通過URL傳入
  2. 在視圖中,創建了初始的QuerySet用於獲取所有的已發布的文章,然后判斷如果傳入了tag_slug,就通過get_object_or_404()方法獲取對應的Tag對象
  3. 然后過濾初始的QuerySet,條件為文章的標簽中包含選出的Tag對象,由於這是一個多對多關系,所以將Tag對象放入一個列表內選擇。

QuerySet是惰性的,直到模板渲染過程中迭代posts對象列表的時候,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) # 3 posts in each page
    page = request.GET.get('page')
    try:
        posts = paginator.page(page)
    except PageNotAnInteger:
        posts = paginator.page(1)
    except EmptyPage:
        posts = paginator.page(paginator.num_pages)

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

打開blog應用的urls.py文件,注釋掉PostListView那一行,取消post_list視圖的注釋,像下邊這樣:

path('', views.post_list, name='post_list'),
# path('', views.PostListView.as_view(), name='post_list'),

再增加一行通過標簽顯示文章的URL:

path('tag/<slug:tag_slug>/', views.post_list, name='post_list_by_tag'),

可以看到,兩個URL指向了同一個視圖,但命名不同。第一個URL不帶任何參數去調用post_list視圖,第二個URL則會帶上tag_slug參數調用post_list視圖。使用了一個<slug:tag_slug>獲取參數。

由於我們將CBV改回為FBV,所以在blog/post/list.html里將include語句的變量改回FBV的posts

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

再增加顯示文章標簽的{% for %}循環的代碼:

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

如果用戶訪問博客,可以看到全部的文章列表;如果用戶點擊某個具體標簽,就可以看到具備該標簽的文章。現在還需改變一下標簽的顯示方式:

<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 %}
    {% endfor %}
</p>

我們通過迭代所有標簽,將標簽設置為一個鏈接,指向通過該標簽對應的所有文章。通過{% url "blog:post_list_by_tag" tag.slug %}反向解析出了鏈接。

現在到http://127.0.0.1:8000/blog/,然后點擊任何標簽,就可以看到該標簽對應的文章列表:

image

4通過相似性獲取文章

在為博客添加了標簽功能之后,可以使用標簽來做一些有趣的事情。一些相同主題的文章會具有相同的標簽,可以創建一個功能給用戶按照共同標簽數量的多少推薦文章。

為了實現該功能,需要如下幾步:

  1. 獲得當前文章的所有標簽
  2. 拿到所有具備這些標簽的文章
  3. 把當前文章從這個文章列表里去掉以避免重復顯示
  4. 按照具有相同標簽的多少來排列
  5. 如果文章具有相同數量的標簽,按照時間來排列
  6. 限制總推薦文章數目

這幾個步驟會使用到更復雜的QuerySet,打開blog應用的views.py文件,在最上邊增加一行:

from django.db.models import Count

這是從Django ORM中導入的Count聚合函數,這個函數可以按照分組統計某個字段的數量,django.db.models還包含下列聚合函數:

  • Avg:計算平均值
  • Max:取最大值
  • Min:取最小值
  • Count:計數
  • Sum:求和

譯者注:作者在此處有所保留,沒有寫Sum函數,此外還有Q查詢

聚合查詢的官方文檔在https://docs.djangoproject.com/en/2.0/topics/db/aggregation/

修改post_detail視圖,在render()上邊加上這一段內容,縮進與render()行同級:

def post_detail(request, year, month, day, post):
    # ......
    # 顯示相近Tag的文章列表
    post_tags_ids = post.tags.values_list('id',flat=True)
    similar_tags = Post.published.filter(tags__in=post_tags_ids).exclude(id=post.id)
    similar_posts = similar_tags.annotate(same_tags=Count('tags')).order_by('-same_tags','-publish')[:4]
    return render(......)

以上代碼解釋如下:

  1. values_list方法返回指定的字段的值構成的元組,通過指定flat=True,讓其結果變成一個列表比如[1, 2, 3, ...]
  2. 選出所有包含上述標簽的文章並且排除當前文章
  3. 使用Count對每個文章按照標簽計數,並生成一個新字段same_tags用於存放計數的結果
  4. 按照相同標簽的數量,降序排列結果,然后截取前四個結果作為最終傳入模板的數據對象。

最后修改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 posts yet.
{% endfor %}

現在的文章詳情頁面示例如下:

image

現在已經實現了該功能。django-taggit模塊包含一個similar_objects()模型管理器也可以實現這個功能,可以在https://django-taggit.readthedocs.io/en/latest/api.html查看django-taggit的所有模型管理器使用方法。

還可以用同樣的方法在文章詳情頁為文章添加標簽顯示。

總結

在這一章里了解了如何使用Django的表單和模型表單,為博客添加了通過郵件分享文章的功能和評論系統。第一次使用了Django的第三方應用,通過django-taggit為博客增添了基於標簽的功能。最后進行復雜的聚合查詢實現了通過標簽相似性推薦文章。

下一章會學習如何創建自定義的模板標簽和模板過濾器,創建站點地圖和RSS feed,以及對博客文章實現全文檢索功能。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM