“全能”選手—Django 1.10文檔中文版Part3


歡迎大家訪問我的個人網站《劉江的博客和教程》:www.liujiangblog.com

主要分享Python 及Django教程以及相關的博客


Django 1.10官方文檔的入門教程已經翻譯完畢,后續的部分將不會按照順序進行翻譯,而是挑重點的先翻譯。
有興趣的可以關注我的博客。

第一部分傳送門

第二部分傳送門

第四部分傳送門

3.2 模型和數據庫Models and databases

3.2.2 查詢操作making queries

3.3.8 會話sessions

目錄

2.7 第一個Django app,Part 5:測試

  • 2.7.1 自動化測試介紹
  • 2.7.2 基本的測試策略
  • 2.7.3 編寫我們的第一個測試程序
  • 2.7.4 測試一個視圖
  • 2.7.5 測試越多越好
  • 2.7.6 進一步測試

2.8 第一個Django app,Part 6:靜態文件

  • 2.8.1 自定義app的外觀
  • 2.8.2 添加背景圖片

2.9 第一個Django app,Part 7:自定義admin站點

  • 2.9.1 自定義admin表單
  • 2.9.2 添加關系對象
  • 2.9.3 自定義admin change list
  • 2.9.4 定制admin外觀
  • 2.9.5 定制admin首頁
  • 2.9.6 接下來學習什么?

2.7 第一個Django app,Part 5:測試

本章承上啟下,主要介紹自動化測試相關的內容。

2.7.1 自動化測試介紹

什么是自動化測試

測試是一種例行工作用於檢查你的代碼的行為。

測試可以划分為不同的級別。一些測試可能專注於小細節(某一個模型的方法是否會返回預期的值?), 一些測試則專注於檢查軟件的整體運行是否正常(用戶在對網站進行了一系列的輸入后,是否返回了期望的結果?)。這些其實和你早前在教程2中做的測試差不多,使用shell來檢測一個方法的行為,或者運行程序並輸入數據來檢查它是怎么執行的。

自動化測試的不同之處就在於這些測試會由系統來幫你完成。一旦你創建了一組測試程序,當你修改了你的應用,你就可以用這組測試程序來檢查你的代碼是否仍然同預期的那樣運行,而無需執行耗時的手動測試。

為什么需要測試

那么,為什么要進行測試?而且為什么是現在?

你可能覺得自己的Python/Django能力已經足夠,再去學習其他的東西也許不是那么的必要。 畢竟,我們先前聯系的投票應用已經表現得挺好了,將時間花在自動化測試上還不如用在改進我們的應用上。 如果你學習Django就是為了創建這么一個簡單的投票應用,那么進行自動化測試顯然沒有必要。 但如果不是這樣,那么現在是一個很好的學習機會。

測試可以節省你的時間

某種程度上,“檢查並發現工作正常”似乎是種比較滿意的測試結果。但在一些復雜的應用中,你會發現組件之間存在各種各樣復雜的交互關系。

任何一個組件的改動,都有可能導致應用程序產生無法預料的結果。得出‘似乎工作正常’的結果,可能意味着你需要使用二十種不同的測試數據來測試你的代碼,而這僅僅是為了確保你沒有搞砸某些事 ,很顯然,這種方法效率低下。然而,自動化測試只需要數秒就可以完成以上的任務。如果出現了錯誤,還能夠幫助找出引發這個異常行為的代碼。

有時候你可能會覺得編寫測試程序相比起有價值的、創造性的編程工作顯得單調乏味、無趣,尤其是當你的代碼工作正常時。然而,比起用幾個小時的時間來手動測試你的程序,或者試圖找出代碼中一個新生問題的原因,編寫測試程序的性價比還是很高的。

(譯者:下面都是些測試重要性的論述,看標題就好了)

  • 測試不僅僅可以發現問題,它們還能防止問題
  • 測試使你的代碼更受歡迎
  • 測試有助於團隊合作

2.7.2 基本的測試策略

編寫測試程序有很多種方法。一些程序員遵循一種叫做“測試驅動開發”的規則,他們在編寫代碼前會先編好測試程序。看起來似乎有點反人類,但實際上這種方法與大多數人經常的做法很相似:先描述一個問題,然后編寫代碼來解決這個問題。測試驅動開發可以簡單地用Python測試用例將問題格式化。

很多時候,剛接觸測試的人會先編寫一些代碼后才編寫測試程序。事實上,在之前就編寫一些測試會好一點,但不管怎么說什么時候開始都不算晚。

有時候你很難決定從什么時候開始編寫測試。如果你已經編寫了數千行Python代碼,挑選它們中的一些來進行測試是不太容易的。這種情況下,在下次你對代碼進行變更,添加一個新功能或者修復一個bug之時,編寫你的第一個測試,效果會非常好。

下面,讓我們馬上來編寫一個測試。

2.7.3 編寫我們的第一個測試程序

發現BUG

很巧,在我們的投票應用中有一個小bug需要修改:在Question.was_published_recently()方法的返回值中,當Qeustion在最近的一天發布的時候返回True(這是正確的),然而當Question在未來的日期內發布的時候也返回True(這是錯誤的)。

我們可以在admin后台創建一個發布日期在未來的Question,然后在shell中驗證這個bug:

>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> # 創建一個發布日期在30天后的問卷
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> # 測試一下返回值
>>> future_question.was_published_recently()
True

由於“將來”不等於“最近”,因此這顯然是個bug。

創建一個測試來暴露這個bug

剛才我們是在shell中測試了這個bug,那如何通過自動化測試來發現這個bug呢?

通常,我們會把測試代碼放在應用的tests.py文件中;測試系統將自動地從任何名字以test開頭的文件中查找測試程序。

將下面的代碼輸入投票應用的tests.py文件中:

polls/tests.py

import datetime
from django.utils import timezone
from django.test import TestCase
from .models import Question

class QuestionMethodTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        """
 在將來發布的問卷應該返回False
 """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

我們在這里創建了一個django.test.TestCase的子類,它具有一個方法,該方法創建一個pub_date在未來的Question實例。最后我們檢查was_published_recently()的輸出,它應該是 False。

運行測試程序

在終端中,運行下面的命令,

$ python manage.py test polls

你將看到結果如下:

Creating test database for alias 'default'...
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question
self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...

這其中都發生了些什么?:

  • python manage.py test polls命令會查找所有投票應用中的測試程序
  • 發現一個django.test.TestCase的子類
  • 為測試創建一個專用的數據庫
  • 查找函數名以test開頭的測試方法
  • 在test_was_published_recently_with_future_question方法中,創建一個Question實例,該實例的pub_data字段的值是30天后的未來日期。
  • 然后利用assertIs()方法,它發現was_published_recently()返回了True,而不是我們希望的False。

這個測試通知我們哪個測試失敗了,錯誤出現在哪一行。

修復bug

我們已經知道了問題所在,現在可以去修復bug了。具體如下:
polls/models.py

def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now

再次運行測試程序:

Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...

更加全面的測試

我們可以使was_published_recently()方法更加可靠,事實上,在修復一個錯誤的同時又引入一個新的錯誤將是一件很令人尷尬的事。

下面,我們在同一個測試類中再額外添加兩個其它的方法,來更加全面地進行測試:
polls/tests.py

def test_was_published_recently_with_old_question(self):
    """
 日期超過1天的將返回False。這里創建了一個30天前發布的實例。
 """
    time = timezone.now() - datetime.timedelta(days=30)
    old_question = Question(pub_date=time)
    self.assertIs(old_question.was_published_recently(), False)
    
    
def test_was_published_recently_with_recent_question(self):
    """
 最近一天內的將返回True。這里創建了一個1小時內發布的實例。
 """
    time = timezone.now() - datetime.timedelta(hours=1)
    recent_question = Question(pub_date=time)
    self.assertIs(recent_question.was_published_recently(), True)

現在我們有三個測試來保證無論發布時間是在過去、現在還是未來Question.was_published_recently()都將返回正確的結果。

最后,polls 應用雖然簡單,但是無論它今后會變得多么復雜以及會和多少其它的應用產生相互作用,我們都能保證Question.was_published_recently()會按照預期的那樣工作。

2.7.4 測試一個視圖

這個投票應用沒有辨別能力:它將會發布任何的Question,包括pub_date字段是未來的。我們應該改進這一點。讓pub_date是將來時間的Question應該在未來發布,但是一直不可見,直到那個時間點才會變得可見。

在我們嘗試修復任何事情之前,讓我們先看一下可用的工具。

Django測試用客戶端

Django提供了一個測試客戶端用來模擬用戶和代碼的交互。我們可以在tests.py甚至shell 中使用它。

先介紹使用shell的情況,這種方式下,需要做很多在tests.py中不必做的事。首先是設置測試環境:

>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()

setup_test_environment()會安裝一個模板渲染器,它使我們可以檢查一些額外的屬性比如response.context,這些屬性通常情況下是訪問不到的。請注意,這種方法不會建立一個測試數據庫,所以以下命令將運行在現有的數據庫上,輸出的內容也會根據你已經創建的Question的不同而稍有不同。如果你當前settings.py中的的TIME_ZONE不正確,那么你或許得不到預期的結果。在進行下一步之前,請確保時區設置正確。

下面我們需要導入測試客戶端類(在之后的tests.py中,我們將使用django.test.TestCase類,它具有自己的客戶端,不需要導入這個類):

>>> from django.test import Client
>>> # 創建一個實例
>>> client = Client()

下面是具體的一些使用操作:

>>> # 從'/'獲取響應
>>> response = client.get('/')
>>> # 這個地址應該返回的是404頁面
>>> response.status_code
404
>>> # 另一方面我們希望在'/polls/'獲取一些內容
>>> # 通過使用'reverse()'方法,而不是URL硬編碼
>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code
200
>>> response.content
b'\n <ul>\n \n <li><a href="/polls/1/">What&#39;s up?</a></li>\n \n </ul>\n\n'
>>> # 如果下面的操作沒有正常執行,有可能是你前面忘了安裝測試環境--setup_test_environment() 
>>> response.context['latest_question_list']
<QuerySet [<Question: What's up?>]>

改進我們的視圖

投票的列表會顯示還沒有發布的問卷(即pub_date在未來的問卷)。讓我們來修復它。
在教程 4中,我們介紹了一個繼承ListView的基類視圖:
polls/views.py

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]

我們需要在get_queryset()方法中對比timezone.now()。首先導入timezone模塊,然后修改
get_queryset()方法,如下:
polls/views.py

from django.utils import timezone

def get_queryset(self):
    """
 Return the last five published questions (not including those set to be
 published in the future).
 """
    return Question.objects.filter(
    pub_date__lte=timezone.now()
    ).order_by('-pub_date')[:5]

filter()方法,確保了查詢的結果是在當前時間之前,而不包含將來的日期。

測試新視圖

對於沒有測試概念的程序員,啟動服務器、在瀏覽器中載入站點、創建一些發布時間在過去和將來的Questions,然后檢驗是否只有已經發布的Question才會展示出來,整個過程耗費大量的時間。對於有測試理念的程序員,不會每次修改與這相關的代碼時都重復上述步驟,編寫一測試程序是必然的。下面,讓我們基於以上shell會話中的內容,再編寫一個測試。

將下面的代碼添加到polls/tests.py:
首先導入reverse方法:

from django.urls import reverse

創建一個快捷函數來創建Question,同時創建一個新的測試類:

def create_question(question_text, days):
    """
 2個參數,一個是問卷的文本內容,另外一個是當前時間的偏移天數,負值表示發布日期在過去,正值表示發布日期在將來。
 """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)
    
    
class QuestionViewTests(TestCase):
    def test_index_view_with_no_questions(self):
        """
 如果問卷不存在,給出相應的提示。
 """
        response = self.client.get(reverse('polls:index'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_index_view_with_a_past_question(self):
        """
 發布日期在過去的問卷將在index頁面顯示。
 """
        create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
        response.context['latest_question_list'],
        ['<Question: Past question.>']
        )
        
    def test_index_view_with_a_future_question(self):
        """
 發布日期在將來的問卷不會在index頁面顯示
 """
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_index_view_with_future_question_and_past_question(self):
        """
 即使同時存在過去和將來的問卷,也只有過去的問卷會被顯示。
 """
        create_question(question_text="Past question.", days=-30)
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
        response.context['latest_question_list'],
        ['<Question: Past question.>']
        )

    def test_index_view_with_two_past_questions(self):
        """
 index頁面可以同時顯示多個問卷。
 """
        create_question(question_text="Past question 1.", days=-30)
        create_question(question_text="Past question 2.", days=-5)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
        response.context['latest_question_list'],
        ['<Question: Past question 2.>', '<Question: Past question 1.>']
        )

看一下具體的解釋:

create_question是一個創建Question對象的函數。

test_index_view_with_no_questions不創建任何Question,但會檢查消息“No polls are available.” 並驗證latest_question_list為空。注意django.test.TestCase類提供一些額外的斷言方法。在這些例子中,我們使用了assertContains() 和assertQuerysetEqual()。

在test_index_view_with_a_past_question中,我們創建一個Question並驗證它是否出現在列表中。

在test_index_view_with_a_future_question中,我們創建一個pub_date在未來的Question。數據庫會為每一個測試方法進行重置,所以第一個Question已經不在那里,因此index頁面里不應該有任何Question。

諸如此類,事實上,我們是在用測試,模擬站點上的管理員輸入和用戶體驗,檢查系統的每一個狀態變化,發布的是預期的結果。

測試 DetailView視圖

然而,即使未來發布的Question不會出現在index中,如果用戶知道或者猜出正確的URL依然可以訪問它們。所以我們需要給DetailView視圖添加一個這樣的約束:

polls/views.py

class DetailView(generic.DetailView):
    ...
    def get_queryset(self):
        return Question.objects.filter(pub_date__lte=timezone.now())

同樣,我們將增加一些測試來檢驗pub_date在過去的Question可以顯示出來,而pub_date在未來的不可以。

class QuestionIndexDetailTests(TestCase):
    def test_detail_view_with_a_future_question(self):
        """
 訪問發布時間在將來的detail頁面將返回404.
 """
        future_question = create_question(question_text='Future question.', days=5)
        url = reverse('polls:detail', args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)
    
    def test_detail_view_with_a_past_question(self):
        """
 訪問發布時間在過去的detail頁面將返回詳細問卷內容。
 """
        past_question = create_question(question_text='Past Question.', days=-5)
        url = reverse('polls:detail', args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

更多的測試設計

我們應該添加一個類似get_queryset的方法到ResultsView並為該視圖創建一個新的類。這將與我們上面的范例非常類似,實際上也有許多重復。

我們還可以在其它方面改進我們的應用,並隨之不斷地增加測試。例如,發布一個沒有Choices的Questions就顯得極不合理。所以,我們的視圖應該檢查這點並排除這些Questions。我們的測試會創建一個不帶Choices的Question然后測試它不會發布出來,同時創建一個類似的帶有Choices的Question並確保它會發布出來。

也許登陸的管理員用戶應該被允許查看還沒發布的Questions,但普通訪問者則不行。最終要的是:無論添加什么代碼來完成這個要求,都需要提供相應的測試代碼,不管你是先編寫測試程序然后讓這些代碼通過測試,還是先用代碼解決其中的邏輯再編寫測試程序來檢驗它。

從某種程度上來說,你一定會查看你的測試代碼,然后想知道你的測試程序是否過於臃腫,我們接着看下面的內容:

2.7.5 測試越多越好

看起來我們的測試代碼正在逐漸失去控制。以這樣的速度,測試的代碼量將很快超過我們的實際應用程序代碼量,對比其它簡潔優雅的代碼,測試代碼既重復又毫無美感。

沒關系!隨它去!大多數情況下,你可以完一個測試程序,然后忘了它。當你繼續開發你的程序時,它將始終執行有效的測試功能。

有時,測試程序需要更新。假設我們讓只有具有Choices的Questions才會發布,在這種情況下,許多已經存在的測試都將失敗:這會告訴我們哪些測試需要被修改,使得它們保持最新,所以從某種程度上講,測試可以自己測試自己。

在最壞的情況下,在你的開發過程中,你會發現許多測試變得多余。其實,這不是問題,對測試來說,冗余是一件好事。

只要你的測試被合理地組織,它們就不會變得難以管理。 從經驗上來說,好的做法是:

  • 為每個模型或視圖創建一個專屬的TestClass
  • 為你想測試的每一種情況建立一個單獨的測試方法
  • 為測試方法命名時最好從字面上能大概看出它們的功能

2.7.6 進一步測試

本教程只介紹了一些基本的測試。還有很多你可以做的工作,許多非常有用的工具可供你使用。

例如,雖然我們的測試覆蓋了模型的內部邏輯和視圖發布信息的方式,但你還可以使用一個“基於瀏覽器”的框架例如Selenium來測試你的HTML文件真實渲染的樣子。這些工具不僅可以讓你檢查你的Django代碼的行為,還能夠檢查JavaScript的行為。它會啟動一個瀏覽器,與你的網站進行交互,就像有一個人在操縱一樣!Django包含一個LiveServerTestCase來幫助與Selenium 這樣的工具集成。

如果你有一個復雜的應用,你可能為了實現持續集成,想在每次提交代碼前對代碼進行自動化測試,讓代碼自動至少是部分自動地來控制它的質量。

發現你應用中未經測試的代碼的一個好方法是檢查代碼測試的覆蓋率。這也有助於識別脆弱的甚至僵屍代碼。如果你不能測試一段代碼,這通常意味着這些代碼需要被重構或者移除。 覆蓋率將幫助我們識別僵屍代碼。查看3.9節《Testing in Django》來了解更多細節。

本節介紹了簡單的測試方法。下一節我們將介紹靜態文件。

2.8 第一個Django app,Part 6:靜態文件

前面我們編寫了一個經過測試的投票應用,現在讓我們給它添加一張樣式表和一張圖片。

除了由服務器生成的HTML文件外,WEB應用一般需要提供一些其它的必要文件,比如圖片文件、JavaScript腳本和CSS樣式表等等,用來為用戶呈現出一個完整的網頁。在Django中,我們將這些文件稱為“靜態文件”。

對於小項目,這些都不是大問題,你可以將靜態文件放在任何你的web服務器能夠找到的地方。但是對於大型項目,尤其是那些包含多個app在內的項目,處理那些由app帶來的多套不同的靜態文件開始變得困難。

但這正是django.contrib.staticfiles的用途:它收集每個應用(和任何你指定的地方)的靜態文件到一個單獨的地方,並且這個地方在線上可以很容易維護。

2.8.1 自定義app的外觀

首先在你的polls目錄中創建一個static目錄。Django將在那里查找靜態文件,這與Django在polls/templates/中尋找對應的模板文件的方式是一致的。

Django的STATICFILES_FINDERS設置項中包含一個查找器列表,它們知道如何從各種源中找到靜態文件。 其中一個默認的查找器是AppDirectoriesFinder,它在每個INSTALLED_APPS下查找“static”子目錄,例如我們剛創建的那個“static”目錄。admin管理站點也為它的靜態文件使用相同的目錄結構。

在剛才的static中新建一個polls子目錄,再在該子目錄中創建一個style.css文件。換句話說,這個css樣式文件應該是polls/static/polls/style.css。你可以通過書寫polls/style.css在Django中訪問這個靜態文件,與你如何訪問模板的路徑類似。

靜態文件的命名空間:
與模板類似,我們可以將靜態文件直接放在polls/static(而不是創建另外一個polls 子目錄),但實際上這是一個壞主意。Django將使用它所找到的第一個匹配到的靜態文件,如果在你的不同應用中存在兩個同名的靜態文件,Django將無法區分它們。我們需要告訴Django該使用其中的哪一個,最簡單的方法就是為它們添加命名空間。也就是說,將這些靜態文件放進以它們所在的應用的名字同名的另外一個子目錄下(白話講:多建一層與應用同名的子目錄)。
譯者:良好的目錄結構是每個應用都應該創建自己的urls、views、models、templates和static,每個templates包含一個與應用同名的子目錄,每個static也包含一個與應用同名的子目錄。

將下面的代碼寫入樣式文件:
polls/static/polls/style.css

li a {
    color: green;
}

接下來在模板文件的頭部加入下面的代碼:
polls/templates/polls/index.html

{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}" />

{% static %}模板標簽會生成靜態文件的絕對URL路徑。

重新加載http://localhost:8000/polls/,你會看到Question的超鏈接變成了綠色(Django風格!),這意味着你的樣式表被成功導入。

2.8.2 添加背景圖片

下面,我們在polls/static/polls/目錄下創建一個用於存放圖片的images子目錄,在這個子目錄里放入background.gif文件。換句話說,這個文件的路徑是polls/static/polls/images/background.gif。

修改你的css樣式文件:
polls/static/polls/style.css

body {
    background: white url("images/background.gif") no-repeat right bottom;
}

重新加載http://localhost:8000/polls/,你會在屏幕的右下方看到載入的背景圖片。

警告:
顯然,{% static %}模板標簽不能用在靜態文件,比如樣式表中,因為他們不是由Django生成的。 你應該使用相對路徑來相互鏈接靜態文件,因為這樣你可以改變STATIC_URL ( static模板標簽用它來生成URLs)而不用同時修改一大堆靜態文件中路徑相關的部分。

以上介紹的都是基礎中的基礎。更多的內容請查看4.15節《Managing static files》和6.5.12節《The staticfiles app》。4.16節《Deploying static files》討論了更多關於如何在真實服務器上部署靜態文件。

本節內容較少,下一節我們將介紹自定義Django的admin站點!

2.9 第一個Django app,Part 7:自定義admin站點

本節我們主要介紹在第二部分簡要提到過的Django自動生成的admin站點。

2.9.1 自定義admin表單

通過admin.site.register(Question)語句,我們在admin站點中注冊了Question模型。Django會自動生成一個該模型的默認表單頁面。如果你想自定義該頁面的外觀和工作方式,可以在注冊對象的時候告訴Django你的選項。

下面是一個修改admin表單默認排序方式的例子:
首先修改admin.py的代碼:
polls/admin.py

from django.contrib import admin
from .models import Question


class QuestionAdmin(admin.ModelAdmin):
    fields = ['pub_date', 'question_text']
    
admin.site.register(Question, QuestionAdmin)

一般步驟是:創建一個模型管理類,將它作為第二個參數傳遞給admin.site.register(),隨時隨地修改模型的admin選項。

上面的修改,讓“Publication date”字段顯示在“Question”字段前面(默認是在后面)。如下圖所示:
1.png-8.9kB

對於只有2個字段的情況,效果看起來還不是很明顯,但是,如果,你有一打的字段,選擇一種直觀符合人類習慣的排序方式是一種重要的有用的細節處理。

同時,談及包含大量字段的表單,你也許想將表單划分為一些字段集合。
polls/admin.py

from django.contrib import admin
from .models import Question

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
    (None, {'fields': ['question_text']}),
    ('Date information', {'fields': ['pub_date']}),
    ]
admin.site.register(Question, QuestionAdmin)

字段集合中每一個元組的第一個元素是該字段集合的標題。它讓我們的頁面看起來像下面的樣子:
2.png-10kB

2.9.2 添加關系對象

好了,我們已經有了Question的admin頁面,一個Question有多個CHoices,但是我們還沒有顯示Choices的admin頁面。有兩個辦法可以解決這個問題。第一個是像Question一樣將Choice注冊到admin站點,這很容易:
polls/admin.py

from django.contrib import admin
from .models import Choice, Question

# ...
admin.site.register(Choice)

現在訪問admin頁面,就可以看到Choice了,其“Add Choice”表單頁面看起來如下圖:

3.png-6.6kB

在這個表單中,Question字段是一個select選擇框,包含了當前數據庫中所有的Question實例。Django在admin站點中,自動地將所有的外鍵關系展示為一個select框。在我們的例子中,目前只有一個question對象存在。

請注意圖中的綠色加號,它連接到Question模型。每一個包含外鍵關系的對象都會有這個綠色加號。點擊它,會彈出一個新增Question的表單,類似Question自己的添加表單。填入相關信息點擊保存后,Django自動將該Question保存在數據庫,並作為當前Choice的關聯外鍵對象。白話講就是,新建一個Question並作為當前Choice的外鍵。

但是,實話說,這種創建方式的效率不怎么樣。如果在創建Question對象的時候就可以直接添加一些Choice,那會更好。讓我們來動手試試。

刪除Choice模型對register()方法的調用。然后,編輯Question的注冊代碼如下:
polls/admin.py

from django.contrib import admin
from .models import Choice, Question


class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3
    
    
class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
    (None, {'fields': ['question_text']}),
    ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    inlines = [ChoiceInline]
    
admin.site.register(Question, QuestionAdmin)

上面的代碼告訴Django:Choice對象將在Question管理頁面進行編輯,默認情況,請提供3個Choice對象的編輯區域。

加載“Add question”頁面,應該看到如下圖所示:

4.png-20.9kB

它的工作機制是:這里有3個插槽用於關聯Choices,而且每當你重新返回一個已經存在的對象的“Change”頁面,你又將獲得3個新的額外的插槽可用。

在3個插槽的最后,還有一個“Add another Choice”鏈接。點擊它,又可以獲得一個新的插槽。如果你想刪除新增的插槽,點擊它右上方的X圖標即可。但是,默認的三個插槽不可刪除。下面是新增插槽的樣子:

5.png-11.3kB

這里還有點小問題。上面頁面中插槽縱隊排列的方式需要占據大塊的頁面空間,查看起來很不方便。為此,Django提供了一種扁平化的顯示方式,你僅僅只需要修改一下ChoiceInline繼承的類為admin.TabularInline替代先前的StackedInline:
polls/admin.py

class ChoiceInline(admin.TabularInline):
    #...

刷新一下頁面,你會看到類似表格的顯示方式:

6.png-8.8kB

注意“DELETE”列,它可以刪除那些已有的Choice和新建的Choice。

2.9.3 自定義admin change list

Question的admin頁面我們已經修改得差不多了,下面讓我們來微調一下“change list”頁面,該頁面顯示了當前系統中所有的questions。

默認情況下,該頁面看起來是這樣的:

7.png-9.4kB

通常,Django只顯示str()方法指定的內容。但是有時候,我們可能會想要同時顯示一些別的內容。要實現這一目的,可以使用list_display屬性,它是一個由字段組成的元組,其中的每一個字段都會按順序顯示在“change list”頁面上,代碼如下:
polls/admin.py

class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ('question_text', 'pub_date', 'was_published_recently')

額外的,我們把was_published_recently()方法的結果也顯示出來。現在,頁面看起來會是下面的樣子:

8.png-11.9kB

你可以點擊每一列的標題,來根據這列的內容進行排序。但是,was_published_recently這一列除外,不支持這種根據函數輸出結果進行排序的方式。同時請注意,was_published_recently這一列的列標題默認是方法的名字,內容則是輸出的字符串表示形式。

可以通過給方法提供一些屬性來改進輸出的樣式,就如下面所示:
polls/models.py

class Question(models.Model):
    # ...
    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now
    was_published_recently.admin_order_field = 'pub_date'
    was_published_recently.boolean = True
    was_published_recently.short_description = 'Published recently?'

想要了解更多關於這些方法屬性的信息,請參考6.5節《list_displasy》。

我們還可以對顯示結果進行過濾,通過使用list_filter屬性。在QuestionAdmin中添加下面的代碼:

list_filter = ['pub_date']

再次刷新change list頁面,你會看到在頁面右邊多出了一個基於pub_date的過濾面板,如下圖所示:

9.png-16.6kB

根據你選擇的過濾條件的不同,Django會在面板中添加不容的過濾選項。由於pub_date是一個DateTimeField,因此,Django自動添加了這些選項:“Any date”, “Today”, “Past 7 days”, “This month”, “This year”。

順理成章的,讓我們添加一些搜索的能力:

search_fields = ['question_text']

這會在頁面的頂部增加一個搜索框。當輸入搜索關鍵字后,Django會在question_text字段內進行搜索。只要你願意,你可以使用任意多個搜索字段,Django在后台使用的都是SQL查詢語句的LIKE語法,但是,有限制的搜索字段有助於后台的數據庫查詢效率。

也許你注意到了,頁面還提供分頁功能,默認每頁顯示100條。

2.9.4 定制admin外觀

很明顯,在每一個admin頁面頂端都顯示“Django administration”是很可笑的,它僅僅是個占位文本。利用Django的模板系統,很容易修改它。

定制你的項目模板

在manage.py文件同級下創建一個templates目錄。打開你的設置文件mysite/settings.py,在TEMPLATES條目中添加一個DIRS選項:
mysite/settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

DIRS是一個文件系統目錄的列表,是搜索路徑。當加載Django模板時,會在DIRS中進行查找。

模板的組織方式:
就像靜態文件一樣,我們可以把所有的模板都放在一起,形成一個大大的模板文件夾,並且工作正常。但是我們不建議這樣!我們建議每一個模板都應該存放在它所屬應用的模板目錄內(例如polls/templates)而不是整個項目的模板目錄(templates),因為這樣每個應用才可以被方便和正確的重用。請參考2.10節《如何重用apps》。

接下來,在剛才創建的templates中創建一個admin目錄,將admin/base_site.html模板文件拷貝到該目錄內。這個html文件來自Django源碼,它位於django/contrib/admin/templates目錄內。

Django的源代碼在哪里?
如果你無法找到Django的源代碼文件的存放位置,你可以使用下面的命令:
$ python -c "import django; print(django.__path__)"

編輯該文件,用你喜歡的站點名字替換掉{{ site_header|default:_(’Django administration’) }}(包括兩個大括號一起),看起來像下面這樣:

{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1>
{% endblock %}

在這里,我們使用這個方法教會你如何重寫模板。但是在實際的項目中,你可以使用django.contrib.admin.AdminSite.site_header屬性(詳見6.5節),方便的對這個頁面title進行自定義。

請注意,所有Django默認的admin模板都可以被重寫。類似剛才重寫base_site.html模板的方法一樣,從源代碼目錄將html文件拷貝至你自定義的目錄內,然后修改文件。

定制你的應用模板

聰明的讀者可能會問:但是DIRS默認是空的,Django是如何找到默認的admin模板呢?回答是,由於APP_DIRS被設置為True,Django將自動查找每一個應用包內的templates/子目錄(不要忘了django.contrib.admin也是一個應用)。

我們的投票應用不太復雜,因此不需要自定義admin模板。但是如果它變得越來越復雜,因為某些功能而需要修改Django的標准admin模板,那么修改app的模板就比修改項目的模板更加明智。這樣的話,你可以將投票應用加入到任何新的項目中,並且保證能夠找到它所需要的自定義模板。

查看3.5節《template loading documentation》獲取更多關於Django如何查找模板的信息。

2.9.5 定制admin首頁

默認情況下,admin首頁顯示所有INSTALLED_APPS內並在admin應用中注冊過的app,以字母順序進行排序。

要定制admin首頁,你需要重寫admin/index.html模板,就像前面修改base_site.html模板的方法一樣,從源碼目錄拷貝到你指定的目錄內。編輯該文件,你會看到文件內使用了一個app_list模板變量。該變量包含了所有已經安裝的Django應用。你可以硬編碼鏈接到指定對象的admin頁面,使用任何你認為好的方法,用於替代這個app_list。

2.9.6 接下來學習什么?

至此,新手教程已經結束了。此時,你也許想看看2.11節的《下一步干什么》。
或者你對Python包機制很熟悉,對如何將投票應用轉換成一個可重用的app感興趣,請看2.10節《高級教程:如何編寫可重用的apps》。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com