第二章 為博客添加高級功能

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>元素。

字段的驗證也依賴于字段類型。例如,emailto字段是EmailField。這兩個字段都要求一個有效的郵箱地址,否則字段驗證會拋出forms.ValidationError異常,導致表單無效。表單驗證時,還會考慮其它參數:我們定義name字段的最大長度為25個字符,并使用required=Falsecomments字段是可選的。字段驗證時,這些所有因素都會考慮進去。這個表單中使用的字段類型只是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'來區分這兩種場景。

以下是顯示和處理表單的過程:

  1. 當使用GET請求初始加載視圖時,我們創建了一個新的表單實例,用于在模板中顯示空表單。

form = EmailPostForm()

  1. 用戶填寫表單,并通過POST提交。接著,我們使用提交的數據創建一個表單實例,提交的數據包括在request.POST中:
if request.POST == 'POST':
    # Form was submitted
    form = EmailPostForm(request.POST)
  1. 接著,我們使用表單的is_valid()方法驗證提交的數據。該方法會驗證表單中的數據,如果所有字段都是有效數據,則返回True。如果任何字段包含無效數據,則返回False。你可以訪問form.errors查看驗證錯誤列表。
  2. 如果表單無效,我們使用提交的數據在模板中再次渲染表單。我們將會在模板中顯示驗證錯誤。
  3. 如果表單有效,我們訪問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有兩個基礎類用來創建表單: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為模型中的每個字段創建一個表單字段。但是,你可以使用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()方法驗證。如果表單無效,我們渲染帶有驗證錯誤的模板。如果表單有效,我們完成以下操作:

  1. 通過調用表單的save()方法,我們創建一個新的Comment對象:

new_comment = comment_form.save(commit=False)

save()方法創建了一個鏈接到表單模型的實例,并把它存到數據庫中。如果使用commit=False調用,則只會創建模型實例,而不會存到數據庫中。當你想在存儲之前修改對象的時候,會非常方便,之后我們就是這么做的。save()只對ModelForm實例有效,對Form實例無效,因為它們沒有鏈接到任何模型。

  1. 我們把當前的帖子賦值給剛創建的評論:

new_comment.post = post

通過這個步驟,我們指定新評論屬于給定的帖子。

  1. 最后,使用下面的代碼,把新評論存到數據庫中:

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文件,添加taggitINSTALLED_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])
        # ...

該視圖是這樣工作的:

  1. 該視圖接收一個默認值為None的可選參數tag_slug。該參數會在URL中。
  2. 在視圖中,我們創建了初始的QuerySet,檢索所有已發布的帖子,如果給定了標簽別名,我們使用get_object_or_404()快捷方法獲得給定別名的Tag對象。
  3. 然后,我們過濾包括給定標簽的帖子列表。因為這是一個多對多的關系,所以我們需要把過濾的標簽放在指定列表中,在這個例子中只包含一個元素。

記住,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]

這段代碼完成以下操作:

  1. 我們獲得一個包含當前帖子所有標簽的ID列表。values_list()這個QuerySet返回指定字段值的元組。我們傳遞flat=True給它,獲得一個[1, 2, 3, ...]的列表。
  2. 我們獲得包含這些標簽中任何一個的所有帖子,除了當前帖子本身。
  3. 我們使用Count匯總函數生成一個計算后的字段same_tags,它包含與所有查詢標簽共享的標簽數量。
  4. 我們通過共享的標簽數量排序結果(降序),共享的標簽數量相等時,用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源,并在應用中集成一個高級的搜索引擎。

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

推薦閱讀更多精彩內容