Django 1.10官方文檔的入門教程已經翻譯完畢,後續的部分將不會按照順序進行翻譯,而是挑重點的先翻譯。
有興趣的能夠關注個人博客。css
第一部分傳送門html
第二部分傳送門python
第四部分傳送門程序員
3.2 模型和數據庫Models and databasesweb
2.7 第一個Django app,Part 5:測試django
2.8 第一個Django app,Part 6:靜態文件編程
2.9 第一個Django app,Part 7:自定義admin站點瀏覽器
本章承上啓下,主要介紹自動化測試相關的內容。
什麼是自動化測試
測試是一種例行工做用於檢查你的代碼的行爲。
測試能夠劃分爲不一樣的級別。一些測試可能專一於小細節(某一個模型的方法是否會返回預期的值?), 一些測試則專一於檢查軟件的總體運行是否正常(用戶在對網站進行了一系列的輸入後,是否返回了指望的結果?)。這些其實和你早前在教程2中作的測試差很少,使用shell來檢測一個方法的行爲,或者運行程序並輸入數據來檢查它是怎麼執行的。
自動化測試的不一樣之處就在於這些測試會由系統來幫你完成。一旦你建立了一組測試程序,當你修改了你的應用,你就能夠用這組測試程序來檢查你的代碼是否仍然同預期的那樣運行,而無需執行耗時的手動測試。
爲何須要測試
那麼,爲何要進行測試?並且爲何是如今?
你可能以爲本身的Python/Django能力已經足夠,再去學習其餘的東西也許不是那麼的必要。 畢竟,咱們先前聯繫的投票應用已經表現得挺好了,將時間花在自動化測試上還不如用在改進咱們的應用上。 若是你學習Django就是爲了建立這麼一個簡單的投票應用,那麼進行自動化測試顯然沒有必要。 但若是不是這樣,那麼如今是一個很好的學習機會。
測試能夠節省你的時間
某種程度上,「檢查並發現工做正常」彷佛是種比較滿意的測試結果。但在一些複雜的應用中,你會發現組件之間存在各類各樣複雜的交互關係。
任何一個組件的改動,都有可能致使應用程序產生沒法預料的結果。得出‘彷佛工做正常’的結果,可能意味着你須要使用二十種不一樣的測試數據來測試你的代碼,而這僅僅是爲了確保你沒有搞砸某些事 ,很顯然,這種方法效率低下。然而,自動化測試只須要數秒就能夠完成以上的任務。若是出現了錯誤,還可以幫助找出引起這個異常行爲的代碼。
有時候你可能會以爲編寫測試程序相比起有價值的、創造性的編程工做顯得單調乏味、無趣,尤爲是當你的代碼工做正常時。然而,比起用幾個小時的時間來手動測試你的程序,或者試圖找出代碼中一個新生問題的緣由,編寫測試程序的性價比仍是很高的。
(譯者:下面都是些測試重要性的論述,看標題就行了)
編寫測試程序有不少種方法。一些程序員遵循一種叫作「測試驅動開發」的規則,他們在編寫代碼前會先編好測試程序。看起來彷佛有點反人類,但實際上這種方法與大多數人常常的作法很類似:先描述一個問題,而後編寫代碼來解決這個問題。測試驅動開發能夠簡單地用Python測試用例將問題格式化。
不少時候,剛接觸測試的人會先編寫一些代碼後才編寫測試程序。事實上,在以前就編寫一些測試會好一點,但無論怎麼說何時開始都不算晚。
有時候你很難決定從何時開始編寫測試。若是你已經編寫了數千行Python代碼,挑選它們中的一些來進行測試是不太容易的。這種狀況下,在下次你對代碼進行變動,添加一個新功能或者修復一個bug之時,編寫你的第一個測試,效果會很是好。
下面,讓咱們立刻來編寫一個測試。
發現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'...
這其中都發生了些什麼?:
這個測試通知咱們哪一個測試失敗了,錯誤出如今哪一行。
修復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()會按照預期的那樣工做。
這個投票應用沒有辨別能力:它將會發布任何的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'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,但普通訪問者則不行。最終要的是:不管添加什麼代碼來完成這個要求,都須要提供相應的測試代碼,無論你是先編寫測試程序而後讓這些代碼經過測試,仍是先用代碼解決其中的邏輯再編寫測試程序來檢驗它。
從某種程度上來講,你必定會查看你的測試代碼,而後想知道你的測試程序是否過於臃腫,咱們接着看下面的內容:
看起來咱們的測試代碼正在逐漸失去控制。以這樣的速度,測試的代碼量將很快超過咱們的實際應用程序代碼量,對比其它簡潔優雅的代碼,測試代碼既重複又毫無美感。
不要緊!隨它去!大多數狀況下,你能夠完一個測試程序,而後忘了它。當你繼續開發你的程序時,它將始終執行有效的測試功能。
有時,測試程序須要更新。假設咱們讓只有具備Choices的Questions纔會發佈,在這種狀況下,許多已經存在的測試都將失敗:這會告訴咱們哪些測試須要被修改,使得它們保持最新,因此從某種程度上講,測試能夠本身測試本身。
在最壞的狀況下,在你的開發過程當中,你會發現許多測試變得多餘。其實,這不是問題,對測試來講,冗餘是一件好事。
只要你的測試被合理地組織,它們就不會變得難以管理。 從經驗上來講,好的作法是:
本教程只介紹了一些基本的測試。還有不少你能夠作的工做,許多很是有用的工具可供你使用。
例如,雖然咱們的測試覆蓋了模型的內部邏輯和視圖發佈信息的方式,但你還可使用一個「基於瀏覽器」的框架例如Selenium來測試你的HTML文件真實渲染的樣子。這些工具不只可讓你檢查你的Django代碼的行爲,還可以檢查JavaScript的行爲。它會啓動一個瀏覽器,與你的網站進行交互,就像有一我的在操縱同樣!Django包含一個LiveServerTestCase來幫助與Selenium 這樣的工具集成。
若是你有一個複雜的應用,你可能爲了實現持續集成,想在每次提交代碼前對代碼進行自動化測試,讓代碼自動至少是部分自動地來控制它的質量。
發現你應用中未經測試的代碼的一個好方法是檢查代碼測試的覆蓋率。這也有助於識別脆弱的甚至殭屍代碼。若是你不能測試一段代碼,這一般意味着這些代碼須要被重構或者移除。 覆蓋率將幫助咱們識別殭屍代碼。查看3.9節《Testing in Django》來了解更多細節。
本節介紹了簡單的測試方法。下一節咱們將介紹靜態文件。
前面咱們編寫了一個通過測試的投票應用,如今讓咱們給它添加一張樣式表和一張圖片。
除了由服務器生成的HTML文件外,WEB應用通常須要提供一些其它的必要文件,好比圖片文件、JavaScript腳本和CSS樣式表等等,用來爲用戶呈現出一個完整的網頁。在Django中,咱們將這些文件稱爲「靜態文件」。
對於小項目,這些都不是大問題,你能夠將靜態文件放在任何你的web服務器可以找到的地方。可是對於大型項目,尤爲是那些包含多個app在內的項目,處理那些由app帶來的多套不一樣的靜態文件開始變得困難。
但這正是django.contrib.staticfiles的用途:它收集每一個應用(和任何你指定的地方)的靜態文件到一個單獨的地方,而且這個地方在線上能夠很容易維護。
首先在你的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風格!),這意味着你的樣式表被成功導入。
下面,咱們在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站點!
本節咱們主要介紹在第二部分簡要提到過的Django自動生成的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」字段前面(默認是在後面)。以下圖所示:
對於只有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)
字段集合中每個元組的第一個元素是該字段集合的標題。它讓咱們的頁面看起來像下面的樣子:
好了,咱們已經有了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」表單頁面看起來以下圖:
在這個表單中,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」頁面,應該看到以下圖所示:
它的工做機制是:這裏有3個插槽用於關聯Choices,並且每當你從新返回一個已經存在的對象的「Change」頁面,你又將得到3個新的額外的插槽可用。
在3個插槽的最後,還有一個「Add another Choice」連接。點擊它,又能夠得到一個新的插槽。若是你想刪除新增的插槽,點擊它右上方的X圖標便可。可是,默認的三個插槽不可刪除。下面是新增插槽的樣子:
這裏還有點小問題。上面頁面中插槽縱隊排列的方式須要佔據大塊的頁面空間,查看起來很不方便。爲此,Django提供了一種扁平化的顯示方式,你僅僅只須要修改一下ChoiceInline繼承的類爲admin.TabularInline替代先前的StackedInline:
polls/admin.py
class ChoiceInline(admin.TabularInline): #...
刷新一下頁面,你會看到相似表格的顯示方式:
注意「DELETE」列,它能夠刪除那些已有的Choice和新建的Choice。
Question的admin頁面咱們已經修改得差很少了,下面讓咱們來微調一下「change list」頁面,該頁面顯示了當前系統中全部的questions。
默認狀況下,該頁面看起來是這樣的:
一般,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()方法的結果也顯示出來。如今,頁面看起來會是下面的樣子:
你能夠點擊每一列的標題,來根據這列的內容進行排序。可是,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的過濾面板,以下圖所示:
根據你選擇的過濾條件的不一樣,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條。
很明顯,在每個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如何查找模板的信息。
默認狀況下,admin首頁顯示全部INSTALLED_APPS內並在admin應用中註冊過的app,以字母順序進行排序。
要定製admin首頁,你須要重寫admin/index.html模板,就像前面修改base_site.html模板的方法同樣,從源碼目錄拷貝到你指定的目錄內。編輯該文件,你會看到文件內使用了一個app_list模板變量。該變量包含了全部已經安裝的Django應用。你能夠硬編碼連接到指定對象的admin頁面,使用任何你認爲好的方法,用於替代這個app_list。
至此,新手教程已經結束了。此時,你也許想看看2.11節的《下一步幹什麼》。 或者你對Python包機制很熟悉,對如何將投票應用轉換成一個可重用的app感興趣,請看2.10節《高級教程:如何編寫可重用的apps》。