《Django By Example》第二章 中文 翻譯 (個人學習,渣翻)


書籍出處:https://www.packtpub.com/web-development/django-example
原作者:Antonio Melé

2016年12月13日發布(3天完成第二章的翻譯,但沒有進行校對,有很多錯別字以及模糊不清的語句,請大家見諒)

2017年2月17日校對完成(不是精校,希望大家多指出需要修改的地方)

2017年3月6日精校完成(感謝大牛 @kukoo 的精校!)

2017年3月21日再度精校(感謝大牛 @媽媽不在家 的精校!初版我已經不敢再看!)

(譯者注:翻譯完第一章后,發現翻譯第二章的速度上升了不少,難道這就是傳說中的經驗值提升了?)

第二章

用高級特性來增強你的blog

在上一章中,你創建了一個基礎的博客應用。現在你將利用一些高級的特性例如通過email來分享帖子,添加評論,給帖子打上tag,檢索出相似的帖子等將它改造成為一個功能更加齊全的博客。在本章中,你將會學習以下幾點:

  • 通過Django發送email
  • 在視圖(views)中創建並操作表單
  • 通過模型(models)創建表單
  • 集成第三方應用
  • 構建復雜的查詢集(QuerySets)

通過email分享帖子

首先,我們會允許用戶通過發送郵件來分享他們的帖子。讓我們花費一小會時間來想下,根據在上一章中學到的知識,你該如何使用views,urls和templates來創建這個功能。現在,核對一下你需要哪些才能允許你的用戶通過郵件來發送帖子。你需要做到以下幾點:

  • 給用戶創建一個表單來填寫他們的姓名,email,收件人以及評論,評論不是必選項。
  • views.py文件中創建一個視圖(view)來操作發布的數據和發送email
  • 在blog應用的urls.py中為新的視圖(view)添加一個URL模式
  • 創建一個模板(template)來展示這個表單

使用Django創建表單

讓我們開始創建一個表單來分享帖子。Django有一個內置的表單框架允許你通過簡單的方式來創建表單。這個表單框架允許你定義你的表單字段,指定這些字段必須展示的方式,以及指定這些字段如何驗證輸入的數據。Django表單框架還提供了一種靈活的方式來渲染表單以及操作數據。

Django提供了兩個可以創建表單的基本類:

  • Form: 允許你創建一個標准表單
  • ModelForm: 允許你創建一個可用於創建或者更新model實例的表單

首先,在你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中的展示形式。通過改變控件的屬性可以重寫默認的控件。在comment字段中,我們使用<textarea></textarea>HTML元素而不是使用默認的<input>元素來顯示它。

字段驗證取決於字段類型。例如,emailto字段是EmailField,這兩個字段都需要一個有效的email地址,否則字段驗證將會拋出一個forms.ValidationError異常導致表單驗證不通過。在表單驗證的時候其他的參數也會被考慮進來:我們將name字段定義為一個最大長度為25的字符串;通過設置required=False讓comments的字段可選。所有這些也會被考慮到字段驗證中去。目前我們在表單中使用的這些字段類型只是Django支持的表單字段的一部分。要查看更多可利用的表單字段,你可以訪問:https://docs.djangoproject.com/en/1.8/ref/forms/fields/

在視圖(views)中操作表單

當表單成功提交后你必須創建一個新的視圖(views)來操作表單和發送email。編輯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})

該視圖(view)完成了以下工作:

  • 我們定義了post_share視圖,參數為request對象和post_id
  • 我們使用get_object_or_404快捷方法通過ID獲取對應的帖子,並且確保獲取的帖子有一個published狀態。
  • 我們使用同一個視圖(view)來展示初始表單和處理提交后的數據。我們會區別被提交的表單和不基於這次請求方法的表單。我們將使用POST來提交表單。如果我們得到一個GET請求,一個空的表單必須顯示,而如果我們得到一個POST請求,則表單需要提交和處理。因此,我們使用request.method == 'POST'來區分這兩種場景。

下面是展示和操作表單的過程:

  • 1.通過GET請求視圖(view)被初始加載后,我們創建一個新的表單實例,用來在模板(template)中顯示一個空的表單:

      form = EmailPostForm()
    
  • 2.當用戶填寫好了表單並通過POST提交表單。之后,我們會用保存在request.POST中提交的數據創建一個表單實例。

      if request.method == 'POST':
         # Form was submitted
         form = EmailPostForm(request.POST)
    
  • 3.在以上步驟之后,我們使用表單的is_valid()方法來驗證提交的數據。這個方法會驗證表單引進的數據,如果所有的字段都是有效數據,將會返回True。一旦有任何一個字段是無效的數據,is_valid()就會返回False。你可以通過訪問 form.errors來查看所有驗證錯誤的列表。

  • 4如果表單數據驗證沒有通過,我們會再次使用提交的數據在模板(template)中渲染表單。我們會在模板(template)中顯示驗證錯誤的提示。

  • 5.如果表單數據驗證通過,我們通過訪問form.cleaned_data獲取驗證過的數據。這個屬性是一個表單字段和值的字典。

如果你的表單數據沒有通過驗證,cleaned_data只會包含驗證通過的字段

現在,你需要學習如何使用Django來發送email,把所有的事情結合起來。

使用Django發送email

使用Django發送email非常簡單。首先,你需要有一個本地的SMTP服務或者通過在你項目的settings.py文件中添加以下設置去定義一個外部SMTP服務器的配置:

  • EMAIL_HOST: SMTP服務地址。默認本地。
  • EMAIL_POSR: SMATP服務端口,默認25。
  • EMAIL_HOST_USER: SMTP服務的用戶名。
  • EMAIL_HOST_PASSWORD: SMTP服務的密碼。
  • EMAIL_USE_TLS: 是否使用TLS加密連接。
  • EMAIL_USE_SSL: 是否使用隱式的SSL加密連接。

如果你沒有本地SMTP服務,你可以使用你的email服務供應商提供的SMTP服務。下面提供了一個簡單的例子展示如何通過使用Google賬戶的Gmail服務來發送email:

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 shell,發送一封email如下所示:

>>> 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,我們告訴這個方法如果email沒有發送成功那么需要拋出一個異常。如果你看到輸出是1,證明你的email發送成功了。如果你使用之前的配置用Gmail來發送郵件,你可能需要去 https://www.google.com/settings/security/lesssecureapps 去開通一下低安全級別應用的權限。(譯者注:練習時老老實實用QQ郵箱吧)

現在,我們要將以上代碼添加到我們的視圖(view)中。在blog應用下的views.py文件中編輯post_share視圖(view)如下所示:

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@myblog.com',[cd['to']])
            sent = True
    else:
        form = EmailPostForm()
        
    return render(request, 'blog/post/share.html', {'post': post,
                                                    'form': form,
                                                    'sent': sent})

請注意,我們聲明了一個sent變量並且當帖子被成功發送時賦予它True。當表單成功提交的時候,我們之后將在模板(template)中使用這個變量顯示一條成功提示。由於我們需要在email中包含帖子的超鏈接,所以我們通過使用post.get_absolute_url()方法來獲取到帖子的絕對路徑。我們將這個絕對路徑作為request.build_absolute_uri()的輸入值來構建一個完整的包含了HTTP schema和主機名的url。我們通過使用驗證過的表單數據來構建email的主題和消息內容並最終給表單to字段中包含的所有email地址發送email。

現在你的視圖(view)已經完成了,別忘記為它去添加一個新的URL模式。打開你的blog應用下的urls.py文件添加post_share的URL模式如下所示:

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

在模板(templates)中渲染表單

在通過創建表單,編寫視圖(view)以及添加URL模式后,我們就只剩下為這個視圖(view)添加模板(tempalte)了。在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 %}

這個模板(tempalte)專門用來顯示一個表單或一條成功提示信息。如你所見,我們創建的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 %}模板(tempalte)標簽(tag)引進了可以避開Cross-Site request forgery(CSRF)攻擊的自動生成的令牌,這是一個隱藏的字段。這些攻擊由惡意的站點或者可以在你的站點中為用戶執行惡意行為的程序組成。通過訪問 https://en.wikipedia.org/wiki/Cross-site_request_forgery你可以找到更多的信息

上述的標簽(tag)生成的隱藏字段就像下面一樣:

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

默認情況下,Django在所有的POST請求中都會檢查CSRF標記(token)。請記住要在所有使用POST方法提交的表單中包含csrf_token標簽。(譯者注:當然你也可以關閉這個檢查,注釋掉app_list中的csrf應用即可,我就是這么做的,因為我懶)

編輯你的blog/post/detail.html模板(template),在{{ post.body|linebreaks }}變量后面添加如下的鏈接來分享帖子的URL:

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

請記住,我們通過使用Django提供的{% url %}模板(template)標簽(tag)來動態的生成URL。我們以blog為命名空間,以post_share為URL,同時傳遞帖子ID作為參數來構建絕對的URL。

現在,通過python manage.py runserver命令來啟動開發服務器,在瀏覽器中打開 http://127.0.0.1:8000/blog/ 。點擊任意一個帖子標題查看詳情頁面。在帖子內容的下方,你會看到我們剛剛添加的鏈接,如下所示:
django-2-1

點擊Share this post,你會看到包含通過email分享帖子的表單的頁面。看上去如下所示:
django-2-2

這個表單的CSS樣式被包含在示例代碼中的 static/css/blog.css文件中。當你點擊Send e-mail按鈕,這個表單會提交並驗證。如果所有的字段都通過了驗證,你會得到一條成功信息如下所示:
django-2-3

如果你輸入了無效數據,你會看到表單被再次渲染,並展示出驗證錯誤信息,如下所示:
django-2-4

創建一個評論系統

現在我們准備為blog創建一個評論系統,這樣用戶可以在帖子上進行評論。需要做到以下幾點來創建一個評論系統:

  • 創建一個模型(model)用來保存評論
  • 創建一個表單用來提交評論並且驗證輸入的數據
  • 添加一個視圖(view)來處理表單和保存新的評論到數據庫中
  • 編輯帖子詳情模板(template)來展示評論列表以及用來添加新評論的表單

首先,讓我們創建一個模型(model)來存儲評論。打開你的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模型(model)。它包含了一個外鍵將一個單獨的帖子和評論關聯起來。在Comment模型(model)中定義多對一(many-to-one)的關系是因為每一條評論只能在一個帖子下生成,而每一個帖子又可能包含多個評論。related_name屬性允許我們給這個屬性命名,這樣我們就可以利用這個關系從相關聯的對象反向定位到這個對象。定義好這個之后,我們通過使用 comment.post就可以從一條評論來取到對應的帖子,以及通過使用post.comments.all()來取回一個帖子所有的評論。如果你沒有定義related_name屬性,Django會使用這個模型(model)的名稱加上_set(在這里是:comment_set)來命名從相關聯的對象反向定位到這個對象的manager。

訪問https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_one/, 你可以學習更多關於多對一的關系。

我們用一個active布爾字段用來手動禁用那些不合適的評論。默認情況下,我們根據created字段,對評論按時間順序進行排序。

你剛創建的這個新的Comment模型(model)並沒有同步到數據庫中。運行以下命令生成一個新的反映了新模型(model)創建的數據遷移:

python manage.py makemigrations blog

你會看到如下輸出:

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

Django在blog應用下的migrations/目錄中生成了一個0002_comment.py文件。現在你需要創建一個關聯數據庫模式並且將這些改變應用到數據庫中。運行以下命令來執行已經存在的數據遷移:

python manage.py migrate

你會獲取以下輸出:

Applying blog.0002_comment... OK

我們剛剛創建的數據遷移已經被執行,現在一張blog_comment表已經存在數據庫中。

現在,我們可以添加我們新的模型(model)到管理站點中並通過簡單的接口來管理評論。打開blog應用下的admin.py文件,添加comment model的導入,添加如下內容:

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/ 。你會看到新的模型(model)在Blog區域中出現,如下所示:
django-2-5

我們的模型(model)現在已經被注冊到了管理站點,這樣我們就可以使用簡單的接口來管理評論實例。

通過模型(models)創建表單

我們仍然需要構建一個表單讓我們的用戶在blog帖子下進行評論。請記住,Django有兩個用來創建表單的基礎類:FormModelForm。你先前已經使用過第一個讓用戶通過email來分享帖子。在當前的例子中,你將需要使用ModelForm,因為你必須通過你的Comment模型(model)動態的創建表單。編輯blog應用下的forms.py,添加如下代碼:

from .models import Comment

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

根據模型(model)創建表單,我們只需要在這個表單的Meta類里表明使用哪個模型(model)來構建表單。Django將會解析model並為我們動態的創建表單。每一種模型(model)字段類型都有對應的默認表單字段類型。表單驗證時會考慮到我們定義模型(model)字段的方式。Django為模型(model)中包含的每個字段都創建了表單字段。然而,使用fields 列表你可以明確的告訴框架你想在你的表單中包含哪些字段,或者使用exclude 列表定義你想排除在外的那些字段。對於我們的CommentForm來說,我們在表單中只需要name,email,和body字段,因為我們只需要用到這3個字段讓我們的用戶來填寫。

在視圖(views)中操作ModelForms

為了能更簡單的處理它,我們會使用帖子的詳情視圖(view)來實例化表單。編輯views.py文件(注: 原文此處有錯,應為 views.py),導入Comment模型(model)和CommentForm表單,並且修改post_detail視圖(view)如下所示:

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 the 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})

讓我們來回顧一下我們剛才對視圖(view)添加了哪些操作。我們使用post_detail視圖(view)來顯示帖子和該帖子的評論。我們添加了一個查詢集(QuerySet)來獲取這個帖子所有有效的評論:

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

我們從post對象開始構建這個查詢集(QuerySet)。我們使用關聯對象的manager,這個manager是我們在Comment 模型(model)中使用related_name關系屬性為comments定義的。
我們還在這個視圖(view)中讓我們的用戶添加一條新的評論。因此,如果這個視圖(view)是通過GET請求被加載的,那么我們用comment_fomr = commentForm()來創建一個表單實例。如果是通過POST請求,我們使用提交的數據並且用is_valid()方法驗證這些數據去實例化表單。如果這個表單是無效的,我們會用驗證錯誤信息渲染模板(template)。如果表單通過驗證,我們會做以下的操作:

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

new_comment = comment_form.save(commit=False)

Save()方法創建了一個表單鏈接的model的實例,並將它保存到數據庫中。如果你調用這個方法時設置commit=False,你創建的模型(model)實例不會即時保存到數據庫中。當你想在最終保存之前修改這個model對象會非常方便,我們接下來將做這一步驟。save()方法是給ModelForm用的,而不是給Form實例用的,因為Form實例沒有關聯上任何模型(model)。

  • 2.我們為我們剛創建的評論分配一個帖子:

      new_comment.post = post
    

    通過以上動作,我們指定新的評論是屬於這篇給定的帖子。

  • 3.最后,我們用下面的代碼將新的評論保存到數據庫中:

      new_comment.save()
    

我們的視圖(view)已經准備好顯示和處理新的評論了。

在帖子詳情模板(template)中添加評論

我們為帖子創建了一個管理評論的功能。現在我們需要修改我們的post_detail.html模板(template)來適應這個功能,通過做到以下步驟:

  • 顯示這篇帖子的評論總數
  • 顯示評論的列表
  • 顯示一個表單給用戶來添加新的評論

首先,我們來添加評論的總數。打開blog_detail.html模板(template)在content區塊中添加如下代碼:

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

在模板(template)中我們使用Django ORM執行comments.count() 查詢集(QuerySet)。注意,Django模板(template)語言中不使用圓括號來調用方法。{% with %} 標簽(tag)允許我們分配一個值給新的變量,這個變量可以一直使用直到遇到{% endwith %}標簽(tag)。

{% with %}模板(template)標簽(tag)是非常有用的,可以避免直接操作數據庫或避免多次調用花費較多的方法。

根據total_comments的值,我們使用pluralize 模板(template)過濾器(filter)為單詞comment顯示復數后綴。模板(Template)過濾器(filters)獲取到他們輸入的變量值,返回計算后的值。我們將會在第三章 擴展你的博客應用中討論更多的模板過濾器(tempalte filters)。

pluralize模板(template)過濾器(filter)在值不為1時,會在值的末尾顯示一個"s"。之前的文本將會被渲染成類似:0 comments, 1 comment 或者 N comments。Django內置大量的模板(template)標簽(tags)和過濾器(filters)來幫助你以你想要的方式來顯示信息。

現在,讓我們加入評論列表。在模板(template)中之前的代碼后面加入以下內容:

{% 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 %}模板(template)標簽(tag)來循環所有的評論。如果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/ 然后點擊任意一篇帖子的標題查看它的詳情頁面。你會看到如下頁面展示:

django-2-6

使用該表單添加數條評論。這些評論會在你的帖子下面根據時間排序來展示,類似下圖:
django-2-7

在你的瀏覽器中打開 http://127.0.0.1:8000/admin/blog/comment/ 。你會在管理頁面中看到你創建的評論列表。點擊其中一個進行編輯,取消選擇Active復選框,然后點擊Save按鈕。你會再次被重定向到評論列表頁面,剛才編輯的評論Save列將會顯示成一個沒有激活的圖標。類似下圖的第一個評論:
django-2-8

增加標簽(tagging)功能

在實現了我們的評論系統之后,我們准備創創建一個方法來給我們的帖子添加標簽。我們將通過在我們的項目中集成第三方的Django標簽應用來完成這個功能。django-taggit是一個可復用的應用,它會提供給你一個Tag模型(model)和一個管理器(manager)來方便的給任何模型(model)添加標簽。你可以在 https://github.com/alex/django-taggit 看到它的源碼。

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

pip install django-taggit==0.17.1**(譯者注:根據@孤獨狂飲 驗證,直接 `pip install django-taggit` 安裝最新版即可,原作者提供的版本過舊會有問題,感謝@孤獨狂飲)**

之后打開mysite項目下的settings.py文件,在INSTALLED_APPS設置中設置如下:

INSTALLED_APPS = (
    # ...
    'blog',
    'taggit',
)

打開你的blog應用下的model.py文件,給Post模型(model)添加django-taggit提供的TaggableManager管理器(manager),使用如下代碼:

from taggit.managers import TaggableManager
class Post(models.Model):
    # ...
    tags = TaggableManager()

這個tags管理器(manager)允許你給Post對象添加,獲取以及移除標簽。

運行以下命令為你的模型(model)改變創建一個數據庫遷移:

python manage.py makemigrations blog

你會看到如下輸出:

Migrations for 'blog':
  0003_post_tags.py:
    - Add field tags to post

現在,運行以下代碼在數據庫中生成django-taggit模型(model)對應的表以及同步你的模型(model)的改變:

python manage.py migrate

你會看到以下輸出,:

Applying taggit.0001_initial... OK
Applying taggit.0002_auto_20150616_2121... OK
Applying blog.0003_post_tags... OK

你的數據庫現在已經可以使用django-taggit模型(model)。打開終端運行命令 python manage.py shell來學習如何使用tags管理器(manager)。首先,我們取回我們的其中一篇帖子(該帖子的ID為1):

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

之后為它添加一些標簽並且取回它的標簽來檢查標簽是否添加成功:

>>> 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對象列表:

django-2-9

轉到 http://127.0.0.1:8000/admin/blog/post/ 並點擊一篇帖子進行編輯。你會看到帖子中包含了一個新的Tags字段如下所示,你可以非常容易的編輯它:
django-2-10

現在,我們准備編輯我們的blog帖子來顯示這些標簽。打開blog/post/list.html 模板(template)在帖子標題下方添加如下HTML代碼:

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

join模板(template)過濾器(filter)的功能類似python字符串的join()方法,將給定的字符串連接起來。在瀏覽器中打開 http://127.0.0.1:8000/blog/ 。 你會看到每一個帖子的標題下面的標簽列表:

django-2-11

現在,讓我們來編輯我們的post_list視圖(view)讓用戶可以列出打上了特定標簽的所有帖子。打開blog應用下的views.py文件,從django-taggit中導入Tag模型(model),然后修改post_list視圖(view)讓它可以通過標簽選擇性的過濾,如下所示:

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]) 
        # ...

這個視圖(view)做了以下工作:

  • 1.視圖(view)帶有一個可選的tag_slug參數,默認是一個None值。這個參數會帶進URL中。
  • 2.視圖(view)的內部,我們構建了初始的查詢集(QuerySet),取回所有發布狀態的帖子,假如給予一個標簽 slug,我們通過get_object_or_404()用給定的slug來獲取標簽對象。
  • 3.之后我們過濾所有帖子只留下包含給定標簽的帖子。因為有一個多對多(many-to-many)的關系,我們必須通過給定的標簽列表來過濾,在我們的例子中標簽列表只包含一個元素。

要記住查詢集(QuerySets)是惰性的。這個查詢集(QuerySets)只有當我們在模板(template)中循環渲染帖子列表時才會被執行。

最后,修改視圖(view)最底部的render()函數來,傳遞tag變量給模板(template)。這個視圖(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 = Paginator(object_list, 3) # 3 posts in each page
   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應用下的url.py文件,注釋基於類的PostListView URL模式,然后取消post_list視圖(view)的注釋,如下所示:

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

添加下面額外的URL pattern到通過標簽過濾過的帖子列表中:

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

如你所見,兩個模式都指向了相同的視圖(view),但是我們可以給它們不同的命名。第一個模式會調用post_list視圖(view)並且不帶上任何可選參數。然而第二個模式會調用這個視圖(view)帶上tag_slug參數。

因為我們要使用post_list視圖(view),編輯blog/post/list.html 模板(template),使用posts對象修改pagination,如下所示:

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

{% for %}循環上方添加如下代碼:

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

如果用戶正在訪問blog,他會看到所有帖子列表。如果他指定一個標簽來過濾所有的帖子,他就會看到以上的信息。現在,修改標簽的顯示方式,如下所示:

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

現在,我們循環一個帖子的所有標簽,通過某一標簽來顯示一個自定義的鏈接URL。我們通過{% url "blog:post_list_by_tag" tag.slug %},用URL的名稱以及標簽 slug作為參數來構建URL。我們使用逗號分隔這些標簽。

在瀏覽器中打開 http://127.0.0.1:8000/blog/ 然后點擊任意的標簽鏈接,你會看到通過該標簽過濾過的帖子列表,如下所示:

django-2-12

檢索類似的帖子

如今,我們已經可以給我們的blog帖子加上標簽,我們可以通過它們做更多有意思的事情。通過使用標簽,我們能夠很好的分類我們的blog帖子。擁有類似主題的帖子一般會有幾個共同的標簽。我們准備創建一個功能:通過帖子共享的標簽數量來顯示類似的帖子。這樣的話,當一個用戶閱讀一個帖子,我們可以建議他們去讀其他有關聯的帖子。

為了通過一個特定的帖子檢索到類似的帖子,我們需要做到以下幾點:

  • 返回當前帖子的所有標簽。
  • 返回所有帶有這些標簽的帖子。
  • 在返回的帖子列表中排除當前的帖子,避免推薦相同的帖子。
  • 通過和當前帖子共享的標簽數量來排序所有的返回結果。
  • 假設有兩個或多個帖子擁有相同數量的標簽,推薦最近的帖子。
  • 限制我們想要推薦的帖子數量。

這些步驟可以轉換成一個復雜的查詢集(QuerySet),該查詢集(QuerySet)我們需要包含在我們的post_detail視圖(view)中。打開blog應用中的view.py文件,在頂部添加如下導入:

from django.db.models import Count

這是Django ORM的Count聚合函數。這個函數允許我們處理聚合計算。然后在post_detail視圖(view)的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的Python列表。values_list() 查詢集(QuerySet)返回包含給定的字段值的元祖。我們傳給元祖flat=True來獲取一個簡單的列表類似[1,2,3,...]
  • 2.我們獲取所有包含這些標簽的帖子排除了當前的帖子。
  • 3.我們使用Count聚合函數來生成一個計算字段same_tags,該字段包含與查詢到的所有 標簽共享的標簽數量。
  • 4.我們通過共享的標簽數量來排序(降序)結果並且通過publish字段來挑選擁有相同共享標簽數量的帖子中的最近的一篇帖子。我們對返回的結果進行切片只保留最前面的4篇帖子。

render()函數中給上下文字典增加similar_posts對象,如下所示:

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

現在,編輯blog/post/detail.html模板(template)在帖子評論列表前添加如下代碼:

<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 %}

推薦你在你的帖子詳情模板中添加標簽列表,就像我們在帖子列表模板所做的一樣。現在,你的帖子詳情頁面看上去如下所示:
django-2-13

你已經成功的為你的用戶推薦了類似的帖子。django-taggit還內置了一個similar_objects() 管理器(manager)使你可以通過共享的標簽返回所有對象。你可以通過訪問 http://django-taggit.readthedocs.org/en/latest/api.html 看到所有django-taggit管理器。

總結

在本章中,你學習了如何使用Django的表單和模型(model)表單。你創建了一個通過email分享你的站點內容的系統,還為你的博客創建了一個評論系統。通過集成一個可復用的應用,你為你的帖子增加了打標簽的功能。同時,你還構建了一個復雜的查詢集(QuerySets)用來返回類似的對象。

在下一章中,你會學習到如何創建自定義的模板(temaplate)標簽(tags)和過濾器(filters)。你還會為你的博客應用構建一個自定義的站點地圖,集成一個高級的搜索引擎。

書籍出處:https://www.packtpub.com/web-development/django-example
原作者:Antonio Melé

2016年12月13日發布(3天完成第二章的翻譯,但沒有進行校對,有很多錯別字以及模糊不清的語句,請大家見諒)

2017年2月17日校對完成(不是精校,希望大家多指出需要修改的地方)

2017年3月6日精校完成(感謝感謝大牛 @kukoo 的精校!)

2017年3月21日再度精校(感謝大牛 @媽媽不在家 的精校!初版我已經不敢再看!)


免責聲明!

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



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