第一章 建立部落格應用

阿木古冷發表於2021-01-05

第一章 建立部落格應用

歡迎來到Django 2 by example的教程。你看到的是目前全網唯一翻譯該書的教程。

本書將介紹如何建立可以用於生產環境的完整Django專案。如果你還沒有安裝Django,本章在第一部分中將介紹如何安裝Django,之後的內容還包括建立一個簡單的部落格應用。本章的目的是讓讀者對Django的整體執行有一個概念,理解Django的各個元件如何互動運作,知道建立一個應用的基礎方法。本書將指導你建立一個個完整的專案,但不會對所有細節進行詳細闡述,Django各個元件的內容會在全書的各個部分內進行解釋。

本章的主要內容有:

  • 安裝Django並建立第一個專案
  • 設計資料模型和進行模型遷移(migrations)
  • 為資料模型建立管理後臺
  • 使用QuerySet和模型管理器
  • 建立檢視、模板和URLs
  • 給列表功能的檢視新增分頁功能
  • 使用基於類的檢視

1安裝Django

如果已經安裝了Django,可以跳過本部分到建立第一個Django專案小節。Django是Python的一個包(模組),所以可以安裝在任何Python環境。如果還沒有安裝Django,本節是一個用於本地開發的快速安裝Django指南。

Django 2.0需要Python直譯器的版本為3.4或更高。在本書中,採用Python 3.6.5版本,如果使用Linux或者macOS X,系統中也許已經安裝Python(部分Liunx發行版初始安裝Python2.7),對於Windows系統,從https://www.python.org/downloads/windows/下載Python安裝包。

譯者在此強烈建議使用基於UNIX的系統進行開發。

如果不確定系統中是否已經安裝了Python,可以嘗試在系統命令列中輸入python然後檢視輸出結果,如果看到類似下列資訊,則說明Python已經安裝:

Python 3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, 03:03:55)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

如果安裝的版本低於3.4,或者沒有安裝Python,從https://www.python.org/downloads/下載並安裝。

由於我們使用Python 3,所以暫時不需要安裝資料庫,因為Python 3自帶一個輕量級的SQLite3資料庫可以用於Django開發。如果打算在生產環境中部署Django專案,需要使用更高階的資料庫,比如PostgreSQL,MySQL或者Oracle資料庫。關於如何在Django中使用資料庫,可以看官方文件https://docs.djangoproject.com/en/2.0/topics/install/#database-installation。

譯者注:在翻譯本書和實驗程式碼的時候,譯者的開發環境是Centos 7.5 1804 + Python 3.7.0 + Django 2.1.0(最後一章升級到Django 2.1.2),除了後文會提到的一箇舊版本第三方庫外掛衝突的問題之外,未發現任何相容性問題。

1.1建立獨立的Python開發環境

推薦使用virtualenv建立獨立的開發環境,這樣可以對不同的專案應用不同版本的模組,比將這些模組直接安裝為系統Python的第三方庫要靈活很多。另一個使用virtualenv的優點是安裝Python模組的時候不需要任何管理員許可權。在系統命令列中輸入如下命令來安裝virtualenv

pip install virtualenv

在安裝完virtualenv之後,通過以下命令建立一個獨立環境:

virtualenv my_env

譯者注:需要將virtualenv的所在路徑新增到系統環境變數PATH中,對於Django也是如此,不然無法直接執行django-admin命令。

這個命令會在當前目錄下建立一個my_env/目錄,其中放著一個Python虛擬環境。在虛擬環境中安裝的Python包實際會被安裝到my_env/lib/python3.6/site-packages目錄中。

如果作業系統中安裝的是Python 2.X,必須再安裝Python 3.X,還需要設定virtualenv虛擬Python 3.X的環境。

可以通過如下命令查詢Python 3的安裝路徑,然後建立虛擬環境:

zenx$ which python3
/Library/Frameworks/Python.framework/Versions/3.6/bin/python3
zenx$ virtualenv my_env -p /Library/Frameworks/Python.framework/Versions/3.6/bin/python3

根據Linux發行版的不同,上邊的程式碼也會有所不同。在建立了虛擬環境對應的目錄之後,使用如下命令啟用虛擬環境:

source my_env/bin/activate

啟用之後,在命令列模式的提示符前會顯示括號包住該虛擬環境的名稱,如下所示:

(my_env)laptop:~ zenx$

開啟虛擬環境後,隨時可以通過在命令列中輸入deactivate來退出虛擬環境。

關於virtualenv的更多內容可以檢視https://virtualenv.pypa.io/en/latest/。

virtualenvwrapper這個工具可以方便的建立和管理系統中的所有虛擬環境,需要在系統中先安裝virtualenv,可以到https://virtualenvwrapper.readthedocs.io/en/latest/下載。

1.2使用PIP安裝Django

推薦使用pip包安裝Django。Python 3.6已經預裝了pip,也可以在https://pip.pypa.io/en/stable/installing/找到pip的安裝指南。

使用下邊的命令安裝Django:

pip install Django==2.0.5

譯者這裡安裝的是2.1版。

Django會被安裝到虛擬環境下的site-packages/目錄中。

現在可以檢查Django是否已經成功安裝,在系統命令列模式執行python,然後匯入Django,檢查版本,如下:

>>> import django
>>> django.get_version()
'2.0.5'

如果看到了這個輸出,就說明Django已經成功安裝了。

Django的其他安裝方式,可以檢視官方文件完整的安裝指南:https://docs.djangoproject.com/en/2.0/topics/install/。

2建立第一個Django專案

本書的第一個專案是建立一個完整的部落格專案。Django提供了一個建立專案並且初始化其中目錄結構和檔案的命令,在命令列模式中輸入:

django-admin startproject mysite

這會建立一個專案,名稱叫做mysite

避免使用Python或Django的內建名稱作為專案名稱。

看一下專案目錄的結構:

mysite/
  manage.py
  mysite/
    __init__.py
    settings.py
    urls.py
    wsgi.py

這些檔案解釋如下:

  • manage.py:是一個命令列工具,可以通過這個檔案管理專案。其實是一個django-admin.py的包裝器,這個檔案在建立專案過程中不需要編輯。

  • mysite/
    

    :這是專案目錄,由以下檔案組成:

    • __init__.py:一個空檔案,告訴Python將mysite看成一個包。
    • settings.py:這是當前專案的設定檔案,包含一些初始設定
    • urls.py:這是URL patterns的所在地,其中的每一行URL,表示URL地址與檢視的一對一對映關係。
    • wsgi.py:這是自動生成的當前專案的WSGI程式,用於將專案作為一個WSGI程式啟動。

自動生成的settings.py是當前專案的配置檔案,包含一個用於使用SQLite 3 資料庫的設定,以及一個叫做INSTALLED_APPS的列表。INSTALLED_APPS包含Django預設新增到一個新專案中的所有應用。在之後的專案設定部分會接觸到這些應用。

為了完成專案建立,還必須在資料庫裡建立起INSTALLED_APPS中的應用所需的資料表,開啟系統命令列輸入下列命令:

cd mysite
python manage.py migrate

會看到如下輸出:

Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying sessions.0001_initial... OK

這些輸出表示Django剛剛執行的資料庫遷移(migrate)工作,在資料庫中建立了這些應用所需的資料表。在本章的建立和執行遷移部分會詳細介紹migrate命令。

2.1執行開發中的站點

Django提供了一個輕量級的Web服務程式,無需在生產環境即可快速測試開發中的站點。啟動這個服務之後,會檢查所有的程式碼是否正確,還可以在程式碼被修改之後,自動重新載入修改後的程式碼,但部分情況下比如向專案中加入了新的檔案,還需要手工關閉服務再重新啟動。

在命令列中輸入下列命令就可以啟動站點:

python manage.py runserver

應該會看到下列輸出:

Performing system checks...

System check identified no issues (0 silenced).
May 06, 2018 - 17:17:31
Django version 2.0.5, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

現在可以在瀏覽器中開啟http://127.0.0.1:8000/,會看到成功執行站點的頁面,如下圖所示:

img

能看到這個頁面,說明Django正在執行,如果此時看一下剛才啟動站點的命令列視窗,可以看到瀏覽器的GET請求:

[15/May/2018 17:20:30] "GET / HTTP/1.1" 200 16348

站點接受的每一個HTTP請求,都會顯示在命令列視窗中,如果站點發生錯誤,也會將錯誤顯示在該視窗中。

在啟動站點的時候,還可以指定具體的主機地址和埠,或者使用另外一個配置檔案,例如:

python manage.py runserver 127.0.0.1:8001 --settings=mysite.settings

如果站點需要在不同環境下執行,單獨為每個環境建立匹配的配置檔案。

當前這個站點只能用作開發測試,不能夠配置為生產用途。想要將Django配置到生產環境中,必須通過一個Web服務程式比如Apache,Gunicorn或者uWSGI,將Django作為一個WSGI程式執行。使用不同web服務程式部署Django請參考:https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/。本書的第十三章 上線會介紹如何配置生產環境。

2.2專案設定

開啟settings.py看一下專案設定,其中列出了一些設定,但這只是Django所有設定的一部分。可以在https://docs.djangoproject.com/en/2.0/ref/settings/檢視所有的設定和初始值。

檔案中的以下設定值得注意:

  • DEBUG是一個布林值,控制DEBUG模式的開啟或關閉。當設定為True時,Django會將所有的日誌和錯誤資訊都列印在視窗中。在生產環境中則必須設定為False,否則會導致資訊洩露。

  • ALLOWED_HOSTS在本地開發的時候,無需設定。在生產環境中,DEBUG設定為False時,必須將主機名/IP地址填入該列表中,以讓Django為該主機/IP提供服務。

  • INSTALLED_APPS列出了每個專案當前啟用的應用,Django預設包含下列應用:

    • django.contrib.admin:管理後臺應用
    • django.contrib.auth:使用者身份認證
    • django.contrib.contenttypes:追蹤ORM模型與應用的對應關係
    • django.contrib.sessions:session應用
    • django.contrib.messages:訊息應用
    • django.contrib.staticfiles:管理站點靜態檔案
  • MIDDLEWARE是中介軟體列表。

  • ROOT_URLCONF指定專案的根URL patterns配置檔案。

  • DATABASE是一個字典,包含不同名稱的資料庫及其具體設定,必須始終有一個名稱為default的資料庫,預設使用SQLite 3資料庫。

  • LANGUAGE_CODE站點預設的語言程式碼。

  • USE_TZ是否啟用時區支援,Django可以支援根據時區自動切換時間顯示。如果通過startproject命令建立站點,該項預設被設定為True

如果目前對這些設定不太理解也沒有關係,在之後的章節中這裡的設定都會使用到。

2.3專案(projects)與應用(applications)

在整本書中,這兩個詞會反覆出現。在Django中,像我們剛才那樣的一套目錄結構和其中的設定就是一個Django可識別的專案。應用指的就是一組Model(資料模型)、Views(檢視)、Templates(模板)和URLs的集合。Django框架通過使用應用,為站點提供各種功能,應用還可以被複用在不同的專案中。你可以將一個專案理解為一個站點,站點中包含很多功能,比如部落格,wiki,論壇,每一種功能都可以看作是一個應用。

2.4建立一個應用

我們將從頭開始建立一個部落格應用,進入專案根目錄(manage.py檔案所在的路徑),在系統命令列中輸入以下命令建立第一個Django應用:

python manage.py startapp blog

這條命令會在專案根目錄下建立一個如下結構的應用:

blog/
  __init__.py
  admin.py
  apps.py
  migrations/
    __init__.py
    models.py
    tests.py
    views.py

這些檔案的含義為:

  • admin.py:用於將模型註冊到管理後臺,以便在Django的管理後臺(Django administration site)檢視。管理後臺也是一個可選的應用。
  • apps.py:當前應用的主要配置檔案
  • migrations這個目錄包含應用的資料遷移記錄,用來追蹤資料模型的變化然後和資料庫同步。
  • models.py:當前應用的資料模型,所有的應用必須包含一個models.py檔案,但其中內容可以是空白。
  • test.py:為應用增加測試程式碼的檔案
  • views.py:應用的業務邏輯部分,每一個檢視接受一個HTTP請求,處理這個請求然後返回一個HTTP響應。

3設計部落格應用的資料架構(data schema)

schema是一個資料庫名詞,一般指的是資料在資料庫中的組織模式或者說架構。我們將通過在Django中定義資料模型來設計我們部落格應用在資料庫中的資料架構。一個資料模型,是指一個繼承了django.db.models.Model的Python 類。Django會為在models.py檔案中定義的每一個類,在資料庫中建立對應的資料表。Django為建立和運算元據模型提供了一系列便捷的API(Django ORM):

我們首先來定義一個Post類,在blog應用下的models.py檔案中新增下列程式碼:

from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User


class Post(models.Model):
    STATUS_CHOICES = (('draft', 'Draft'), ('published', 'Published'))
    title = models.CharField(max_length=250)
    slug = models.SlugField(max_length=250, unique_for_date='publish')
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
    body = models.TextField()
    publish = models.DateTimeField(default=timezone.now)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')


    class Meta:
        ordering = ('-publish',)


    def __str__(self):
        return self.title

這是我們為了部落格中每一篇文章定義的資料模型:

  • title:這是文章標題欄位。這個欄位被設定為Charfield型別,在SQL資料庫中對應VARCHAR資料型別
  • slug:該欄位通常在URL中使用。slug是一個短的字串,只能包含字母,數字,下劃線和減號。將使用slug欄位構成優美的URL,也方便搜尋引擎搜尋。其中的unique_for_date參數列示不允許兩條記錄的publish欄位日期和title欄位全都相同,這樣就可以使用文章釋出的日期與slug欄位共同生成一個唯一的URL標識該文章。
  • author:是一個外來鍵欄位。通過這個外來鍵,告訴Django一篇文章只有一個作者,一個作者可以寫多篇文章。對於這個欄位,Django會在資料庫中使用外來鍵關聯到相關資料表的主鍵上。在這個例子中,這個外來鍵關聯到Django內建使用者驗證模組的User資料模型上。on_delete參數列示刪除外來鍵關聯的內容時候的操作,這個並不是Django特有的定義,而是SQL 資料庫的標準操作;將其設定為CASCADE意味著如果刪除一個作者,將自動刪除所有與這個作者關聯的文章,對於該引數的設定,可以檢視https://docs.djangoproject.com/en/2.0/ref/models/fields/#django.db.models.ForeignKey.on_delete。related_name引數設定了從UserPost的反向關聯關係,用blog_posts為這個反向關聯關係命名,稍後會學習到該關係的使用。
  • body:是文章的正文部分。這個欄位是一個文字域,對應SQL資料庫的TEXT資料型別。
  • publish:文章釋出的時間。使用了django.utils.timezone.now作為預設值,這是一個包含時區的時間物件,可以將其認為是帶有時區功能的Python標準庫中的datetime.now方法。
  • created:表示建立該文章的時間。auto_now_add表示當建立一行資料的時候,自動用建立資料的時間填充。
  • updated:表示文章最後一次修改的時間,auto_now表示每次更新資料的時候,都會用當前的時間填充該欄位。
  • statues:這個欄位表示該文章的狀態,使用了一個choices引數,所以這個欄位的值只能為一系列選項中的值。

Django提供了很多不同型別的欄位可以用於資料模型,具體可以參考:https://docs.djangoproject.com/en/2.0/ref/models/fields/。

在資料模型中的Meta類表示存放模型的後設資料。通過定義ordering = ('-publish',),指定了Django在進行資料庫查詢的時候,預設按照發布時間的逆序將查詢結果排序。逆序通過加在欄位名前的減號表示。這樣最近釋出的文章就會排在前邊。

__str__()方法是Python類的功能,供顯示給人閱讀的資訊,這裡將其設定為文章的標題。Django在很多地方比如管理後臺中都呼叫該方法顯示物件資訊。

如果你之前使用的是Python 2.X,注意在Python 3中,所有的字串都已經是原生Unicode格式,所以只需要定義__str__()方法,__unicode__()方法已被廢棄。

3.1啟用應用

為了讓Django可以為應用中的資料模型建立資料表並追蹤資料模型的變化,必須在專案裡啟用應用。要啟用應用,編輯settings.py檔案,新增blog.apps.BlogConfigINSTALLED_APPS設定中:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog.apps.BlogConfig',
]

BlogConfig類是我們應用的配置類。現在Django就已經知道專案中包含了一個新應用,可以載入這個應用的資料模型了。

3.2建立和執行遷移

建立好了部落格文章的資料模型,之後需要將其變成資料庫中的資料表。Django提供資料遷移系統,用於追蹤資料模型的變動,然後將變化寫入到資料庫中。我們之前執行過的migrate命令會對INSTALLED_APPS中的所有應用進行掃描,根據資料模型和已經存在的遷移資料執行資料庫同步操作。

首先,我們需要來為Post模型建立遷移資料,進入專案根目錄,輸入下列命令:

python manage.py makemigrations blog

會看到如下輸出:

Migrations for 'blog':
  blog/migrations/0001_initial.py
    - Create model Post

該命令執行後會在blog應用下的migrations目錄裡新增一個0001_initial.py檔案,可以開啟該檔案看一下遷移資料是什麼樣子的。一個遷移資料檔案裡包含了與其他遷移資料的依賴關係,以及實際要對資料庫執行的操作。

為了瞭解Django實際執行的SQL語句,可以使用sqlmigrate加上遷移檔名,會列出要執行的SQL語句,但不會實際執行。在命令列中輸入下列命令然後觀察資料遷移的指令:

python manage.py sqlmigrate blog 0001

輸出應該如下所示:

BEGIN;
--
-- Create model Post
--
CREATE TABLE "blog_post" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" varchar(250) NOT NULL, "slug" varchar(250) NOT NULL, "body" text NOT
NULL, "publish" datetime NOT NULL, "created" datetime NOT NULL, "updated"
datetime NOT NULL, "status" varchar(10) NOT NULL, "author_id" integer NOT
NULL REFERENCES "auth_user" ("id"));
CREATE INDEX "blog_post_slug_b95473f2" ON "blog_post" ("slug");
CREATE INDEX "blog_post_author_id_dd7a8485" ON "blog_post" ("author_id");
COMMIT;

具體的輸出根據你使用的資料庫會有變化。上邊的輸出針對SQLite資料庫。可以看到表名被設定為應用名加上小寫的類名(blog_post)也可以通過在Meta類中使用db_table屬性設定表名。Django自動為每個模型建立了主鍵,也可以通過設定某個模型欄位引數primary_key=True來指定主鍵。預設的主鍵列名叫做id,和這個列同名的id欄位會自動新增到你的資料模型上。(即Post類被Django新增了Post.id屬性)。

然後來讓資料庫與新的資料模型進行同步,在命令列中輸入下列命令:

python manage.py migrate

會看到如下輸出:

Applying blog.0001_initial... OK

這樣就對INSTALLED_APPS中的所有應用執行完了資料遷移過程,包括我們的blog應用。在執行完遷移之後,資料庫中的資料表就反映了我們此時的資料模型。

如果之後又編輯了models.py檔案,對已經存在的資料模型進行了增刪改,或者又新增了新的資料模型,必須重新執行makemigrations建立新的資料遷移檔案然後執行migrate命令同步資料庫。

4為資料模型建立管理後臺站點(administration site)

定義了Post資料模型之後,可以為方便的管理其中的資料建立一個簡單的管理後臺。Django內建了一個管理後臺,這個管理後臺動態的讀入資料模型,然後建立一個完備的管理介面,從而可以方便的管理資料。這是一個可以“拿來就用”的方便工具。

管理後臺功能其實也是一個應用叫做django.contrib.admin,預設包含在INSTALLED_APPS設定中。

4.1建立超級使用者

要使用管理後臺,需要先註冊一個超級使用者,輸入下列命令:

python manage.py createsuperuser

會看到下列輸出,輸入使用者名稱、密碼和郵件:

Username (leave blank to use 'admin'): admin
Email address: admin@admin.com
Password: ********
Password (again): ********
Superuser created successfully.

4.2Django 管理後臺

使用python manage.py runserver啟動站點,然後開啟http://127.0.0.1:8000/admin/,可以看到如下的管理後臺登入頁面:

管理後臺登入介面

輸入剛才建立的超級使用者的使用者名稱和密碼,可以看到管理後臺首頁,如下所示:

預設介面

GroupUser已經存在於管理後臺中,這是因為設定中預設啟用了django.contrib.auth應用的原因。如果你點選Users,可以看到剛剛建立的超級使用者。還記得blog應用的Post模型與User模型通過author欄位產生外來鍵關聯嗎?

4.3向管理後臺內新增模型

我們把Post模型新增到管理後臺中,編輯blog應用的admin.py檔案為如下這樣:

from django.contrib import admin
from .models import Post

admin.site.register(Post)

之後重新整理管理後臺頁面,可以看到Post類出現在管理後臺中:

新增POST類後介面

看上去好像很簡單。每當在管理後臺中註冊一個模型,就能迅速在管理後臺中看到它,還可以對其進行增刪改查。

點選Posts右側的Add連結,可以看到Django根據模型的具體欄位動態的生成了新增頁面,如下所示:

img

Django對於每個欄位使用不同的表單外掛(form widgets,控制該欄位實際在頁面上對應的HTML元素)。即使是比較複雜的欄位比如DateTimeField,也會以簡單的介面顯示出來,類似於一個JavaScript的時間控制元件。

填寫完這個表單然後點選SAVE按鈕,被重定向到文章列表頁然後顯示一條成功資訊,像下面這樣:

img

可以再錄入一些文章資料,為之後資料庫相關操作做準備。

4.4自定義模型在管理後臺的顯示

現在我們來看一下如何自定義管理後臺,編輯blog應用的admin.py,修改成如下:

from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'slug', 'author', 'publish', 'status')

這段程式碼的意思是將我們的模型註冊到管理後臺中,並且建立了一個類繼承admin.ModelAdmin用於自定義模型的展示方式和行為。list_display屬性指定那些欄位在詳情頁中顯示出來。@admin.register()裝飾器的功能與之前的admin.site.register()一樣,用於將PostAdmin類註冊成Post的管理類。

再繼續新增一些自定義設定,如下所示:

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'slug', 'author', 'publish', 'status',)
    list_filter = ('status', 'created', 'publish', 'author',)
    search_fields = ('title', 'body',)
    prepopulated_fields = {'slug': ('title',)}
    raw_id_fields = ('author',)
    date_hierarchy = 'publish'
    ordering = ('status', 'publish',)

回到瀏覽器,重新整理一下posts的列表頁,會看到如下所示:

img

可以看到在該頁面上顯示的欄位就是list_display中的欄位。頁面出現了一個右側邊欄用於篩選結果,這個功能由list_filter屬性控制。頁面上方出現了一個搜尋欄,這是因為在search_fields中定義了可搜尋的欄位。在搜尋欄的下方,出現了時間層級導航條,這是在date_hierarchy中定義的。還可以看到文章預設通過Status和Publish欄位進行排序,這是由ordering屬性設定的。

這個時候點選Add Post,可以發現也有變化。當輸入文章標題時,slug欄位會根據標題自動填充,這是因為設定了prepopulated_fields屬性中slug欄位與title欄位的對應關係。現在author欄位旁邊出現了一個搜尋圖示,並且可以按照ID來查詢和顯示作者,如果在使用者數量很大的時候,這就方便太多了。

通過短短几行程式碼,就可以自定義模型在管理後臺中的顯示方法,還有很多自定義管理後臺和擴充套件管理後臺功能的方法,會在以後的各章中逐步遇到。

5使用QuerySet和模型管理器(managers)

現在我們有了一個功能齊備的管理後臺用於管理部落格的內容資料,現在可以來學習如何從資料庫中查詢資料並且對結果進行操作了。Django具有一套強大的API,可以供你輕鬆的實現增刪改查的功能,這就是Django Object-relational-mapper即Django ORM,可以相容MySQL,PostgreSQL,SQLite和Oracle,可以在settings.pyDATABASES中修改資料庫設定。可以通過編輯資料庫的路由設定讓Django同時使用多個資料庫。

一旦你建立好了資料模型,Django就提供了一套API供你運算元據模型,詳情可以參考https://docs.djangoproject.com/en/2.0/ref/models/。

5.1建立資料物件

開啟系統的終端視窗,執行如下命令:

python manage.py shell

然後錄入如下命令:

>>>from django.contrib.auth.models import User
>>>from blog.models import Post
>>>user = User.objects.get(username='admin')
>>>post = Post(title='Another post', slug='another-post', body='Post body', author = user)
>>>post.save()

讓我們來分析一下這段程式碼做的事情:我們先通過使用者名稱admin取得user物件,就是下邊這條命令:

user = User.objects.get(username='admin')

get()方法允許從資料庫中取出單獨一個資料物件。如果找不到對應資料,會丟擲DoseNotExist異常,如果結果超過一個,會丟擲MultipleObjectsReturn異常,這兩個異常都是被查詢的類的屬性。

然後我們通過下邊這條命令,使用了標題,簡稱和文章內容,以及指定author欄位為剛取得的User物件,新建了一個Post物件:

post = Post(title='Another post', slug='another-post', body='Post body', author = user)

這個物件暫時儲存在記憶體中,沒有被持久化(寫入)到資料庫中。

最後,我們通過save()方法將Post物件寫入到資料庫中:

post.save()

這條命令實際會轉化成一條INSERT SQL語句。現在我們已經知道了如何在記憶體中先建立一個資料物件然後將其寫入到資料庫中的方法,我們還可以使用create()方法一次性建立並寫入資料庫,像這樣:

Post.objects.create(title='One more post', slug='One more post', body='Post body', author=user)

5.2修改資料物件

現在,修改剛才的post物件的標題:

>>> post.title = 'New title'
>>> post.save()

這次save()方法實際轉化為一個UPDATESQL語句。

對資料物件做的修改直到呼叫save()方法才會被存入資料庫。

5.3查詢資料

Django ORM的全部使用都基於QuerySet(查詢結果集物件,由於該術語使用頻繁,因此在之後的文章中不再進行翻譯)。一個查詢結果集是一系列從資料庫中取得的資料物件,經過一系列的過濾條件,最終組合到一起構成的一個物件。

之前已經瞭解了使用Post.objects.get()方法從資料庫中取出一個單獨的資料物件,每個模型都有至少一個管理器,預設的管理器叫做objects。通過使用一個模型管理器,可以得到一個QuerySet,想得到一個資料表裡的所有資料物件,可以使用預設模型管理器的all()方法,像這樣:

>>> all_posts = Post.objects.all()

這樣就取得了一個包含資料庫中全部post的Queryset,值得注意的是,QuerySet還沒有被執行(即執行SQL語句),因為QuerySet是惰性求值的,只有在確實要對其進行表示式求值的時候,QuerySet才會被執行。惰性求值特性使得QuerySet非常有用。如果我們不是把QuerySet的結果賦值給一個變數,而是直接寫在Python命令列中,對應的SQL語句就會立刻被執行,因為會強制對其求值:

>>> Post.objects.all()

譯者注:原書一直沒有非常明確的指出這幾個概念,估計是因為本書不是面向Django初學者所致。這裡譯者總結一下:資料模型Model類=資料表,資料模型類的例項=資料表的一行資料(不一定是來自於資料庫的,也可能是記憶體中建立的),查詢結果集=包裝一系列資料模型類例項的物件。

5.3.1使用filter()方法

可以使用模型管理器的filter()過濾所需的資料,例如,可以過濾出所有2017年釋出的部落格文章:

Post.objects.filter(publish__year=2017)

還可以同時使用多個欄位過濾,比如選出所有admin作者於2017年釋出的文章:

Post.objects.filter(publish__year=2017, author__username='admin')

這和鏈式呼叫QuerySet的結果一樣:

Post.objects.filter(publish__year=2017).filter(author__username='admin')

QuerySet中使用的條件查詢採用雙下劃線寫法,比如例子中的publish__year,雙下劃線還一個用法是從關聯的模型中取其欄位,例如author__username

5.3.2使用exclude()方法

使用模型管理器的exclude()從結果集中去除符合條件的資料。例如選出2017年釋出的所有標題不以Why開頭的文章:

Post.objects.filter(publish__year=2017).exclude(title__startswith='Why')

5.3.3使用order_by()方法

對於查詢出的結果,可以使用order_by()方法按照不同的欄位進行排序。例如選出所有文章,使其按照title欄位排序:

Post.objects.order_by('title')

預設會採用升序排列,如果需要使用降序排列,在字串格式的欄位名前加一個減號:

Post.objects.order_by('-title')

譯者注:如果不指定order_by的排序方式,但在Meta中指定了順序,則預設會優先以Meta中的順序列出。

5.4刪除資料

如果想刪除一個資料,可以對一個資料物件直接呼叫delete()方法:

post = Post.objects.get(id=1)
post.delete()

當外來鍵中的on_delete引數被設定為CASCADE時,刪除一個物件會同時刪除所有對其有依賴關係的物件,比如刪除作者的時候該作者的文章會一併刪除。

譯者注:filter()exclude()all()這三個方法都返回一個QuerySet物件,所以可以任意鏈式呼叫。

5.5QuerySet何時會被求值

可以對一個QuerySet串聯任意多的過濾方法,但只有到該QuerySet實際被求值的時候,才會進行資料庫查詢。QuerySet僅在下列時候才被實際執行:

  • 第一次迭代QuerySet
  • 執行切片操作,例如Post.objects.all()[:3]
  • pickled或者快取QuerySet的時候
  • 呼叫QuerySet的repr()或者len()方法
  • 顯式對其呼叫list()方法將其轉換成列表
  • 將其用在邏輯判斷表示式中。比如bool()orandif

如果對結構化程式設計中的表示式求值有所瞭解的話,就可以知道只有表示式被實際求值的時候,QuerySet才會被執行。譯者在這裡推薦伯克利大學的CS 61A: Structure and Interpretation of Computer ProgramsPython教程。

5.6建立模型管理器

像之前提到的那樣,類名後的.objects就是預設的模型管理器,所有的ORM方法都通過模型管理器操作。除了預設的管理器之外,我們還可以自定義這個管理器。我們要建立一個管理器,用於獲取所有status欄位是published的文章。

自行編寫模型管理器有兩種方法:一是給預設的管理器增加新的方法,二是修改預設的管理器。第一種方法就像是給你提供了一個新的方法例如:Post.objects.my_manager(),第二種方法則是直接使用新的管理器例如:Post.my_manager.all()。我們想實現的方式是:Post.published.all()這樣的管理器。

blogmodels.py裡增加自定義的管理器:

class PublishedManager(models.Manager):
    def get_queryset(self):
        return super(PublishedManager, self).get_queryset().filter(status='published')

class Post(models.Model):
    # ......
    objects = models.Manager()  # 預設的管理器
    published = PublishedManager()  # 自定義管理器

模型管理器的get_queryset()方法返回後續方法要操作的QuerySet,我們重寫了該方法,以讓其返回所有過濾後的結果。現在我們已經自定義好了管理器並且將其新增到了Post模型中,現在可以使用這個管理器進行資料查詢,來測試一下:

啟動包含Django環境的Python命令列模式:

python manage.py shell

現在可以取得所有標題開頭是Who,而且已經發布的文章(實際的查詢結果根據具體資料而變):

Post.published.filter(title__startswith="Who")

6建立列表和詳情檢視函式

在瞭解了ORM的相關知識以後,就可以來建立檢視了。檢視是一個Python中的函式,接受一個HTTP請求作為引數,返回一個HTTP響應。所有返回HTTP響應的業務邏輯都在檢視中完成。

首先,我們會建立應用中的檢視,然後會為每個檢視定義一個匹配的URL路徑,最後,會建立HTML模板將檢視生成的結果展示出來。每一個檢視都會向模板傳遞引數並且渲染模板,然後返回一個包含最終渲染結果的HTTP響應。

6.1建立檢視函式

來建立一個檢視用於列出所有的文章。編輯blog應用的views.py檔案:

from django.shortcuts import render, get_object_or_404
from .models import Post

def post_list(request):
    posts = Post.published.all()
    return render(request, 'blog/post/list.html', {'posts': posts})

我們建立了第一個檢視函式--文章列表檢視。post_list目前只有一個引數request,這個引數對於所有的檢視都是必需的。在這個檢視中,取得了所有已經發布(使用了published管理器)的文章。

最後,使用由django.shortcuts提供的render()方法,使用一個HTML模板渲染結果。render()方法的引數分別是reqeust,HTML模板的位置,傳給模板的變數名與值。render()方法返回一個帶有渲染結果(HTML文字)的HttpResponse物件。render()方法還會將request物件攜帶的變數也傳給模板,在模板中可以訪問所有模板上下文管理器設定的變數。模板上下文管理器就是將變數設定到模板環境的可呼叫物件,會在第三章學習到。

再寫一個顯示單獨一篇文章的檢視,在views.py中新增下列函式:

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)
    return render(request, 'blog/post/detail.html', {'post': post})

這就是我們的文章詳情檢視。這個檢視需要yearmonthdaypost引數,用於獲取一個指定的日期和簡稱的文章。還記得之前建立模型時設定slug欄位的unique_for_date引數,這樣通過日期和簡稱可以找到唯一的一篇文章(或者找不到)。使用get_object_or_404()方法來獲取文章,這個方法返回匹配的一個資料物件,或者在找不到的情況下返回一個HTTP 404錯誤(not found)。最後使用render()方法通過一個模板渲染頁面。

6.2為檢視配置URL

URL pattern的作用是將URL對映到檢視上。一個URL pattern由一個正則字串,一個檢視和可選的名稱(該名稱必須唯一,可以在整個專案環境中使用)組成。Django接到對於某個URL的請求時,按照順序從上到下試圖匹配URL,停在第一個匹配成功的URL處,將HttpRequest類的一個例項和其他引數傳給對應的檢視並呼叫檢視處理本次請求。

blog應用下目錄下邊新建一個urls.py檔案,然後新增如下內容:

from django.urls import path
from . import views

app_name = 'blog'
urlpatterns = [
    # post views
    path('', views.post_list, name='post_list'),
    path('<int:year>/<int:month>/<int:day>/<slug:post>/', views.post_detail, name='post_detail'),
]

上邊的程式碼中,通過app_name定義了一個名稱空間,方便以應用為中心組織URL並且通過名稱對應到URL上。然後使用path()設定了兩條具體的URL pattern。第一條沒有任何的引數,對應post_list檢視。第二條需要如下四個引數並且對應到post_detail檢視:

  • year:需要匹配一個整數
  • month:需要匹配一個整數
  • day:需要匹配一個整數
  • post:需要匹配一個slug形式的字串

我們使用了一對尖括號從URL中獲取這些引數。任何URL中匹配上這些內容的文字都會被捕捉為這個引數的對應的型別值。例如<int:year>會匹配到一個整數形式的字串然後會給模板傳遞名稱為int的變數,其值為捕捉到的字串轉換為整數後的值。而<slug:post>則會被轉換成一個名稱為post,值為slug型別(僅有ASCII字元或數字,減號,下劃線組成的字串)的變數傳給檢視。

對於URL匹配的型別,可以參考https://docs.djangoproject.com/en/2.0/topics/http/urls/#path-converters

如果使用path()無法滿足需求,則可以使用re_path(),通過Python正規表示式匹配複雜的URL。參考https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.re_path瞭解re_path()的使用方法,參考https://docs.python.org/3/howto/regex.html瞭解Python中如何使用正規表示式。

為每個檢視建立單獨的urls.py檔案是保持應用可被其他專案重用的最好方式。

現在我們必須把blog應用的URL包含在整個專案的URL中,到mysite目錄下編輯urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls', namespace='blog')),
]

這行新的URL使用include方法匯入了blog應用的所有URL,使其位於blog/URL路徑下,還指定了名稱空間blog。URL名稱空間必須在整個專案中唯一。之後我們方便的通過使用名稱空間來快速指向具體的URL,例如blog:post_listblog:post_detail。關於URL名稱空間可以參考https://docs.djangoproject.com/en/2.0/topics/http/urls/#url-namespaces。

6.3規範模型的URL

可以使用在上一節建立的post_detail URL來為Post模型的每一個資料物件建立規範化的URL。通常的做法是給模型新增一個get_absolute_url()方法,該方法返回物件的URL。我們將使用reverse()方法通過名稱和其他引數來構建URL。編輯models.py檔案

from django.urls import reverse

class Post(models.Model):
    # ......
    def get_absolute_url(self):
        return reverse('blog:post_detail', args=[self.publish.year, self.publish.month, self.publish.day, self.slug])

之後在模板中,就可以使用get_absolute_url()建立超連結到具體資料物件。

譯者注:原書這裡寫得很簡略,實際上反向解析URL是建立結構化站點非常重要的內容,可以參考Django 1.11版本的Django進階-路由系統瞭解原理,Django 2.0此部分變化較大,需研讀官方文件。

7為檢視建立模板

已經為blog應用配置好了URL pattern,現在需要將內容通過模板展示出來。

blog應用下建立如下目錄:

templates/
    blog/
        base.html
        post/
            list.html
            detail.html

這就是模板的目錄結構。base.html包含頁面主要的HTML結構,並且將結構分為主體內容和側邊欄兩部分。list.htmldetail.html會分表繼承base.html並渲染各自的內容。

Django提供了強大的模板語言用於控制資料渲染,由模板標籤(template tags)模板變數(template variables)模板過濾器(template filters)組成:

  • template tags:進行渲染控制,類似{% tag %}
  • template variables:可認為是模板標籤的一種特殊形式,即只是一個變數,渲染的時候只替換內容,類似{{ variable }}
  • template filters:附加在模板變數上改變變數最終顯示結果,類似{{ variable|filter }}

所有內建的模板標籤和過濾器可以參考https://docs.djangoproject.com/en/2.0/ref/templates/builtins/。

編輯base.html,新增下列內容:

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>{% block title %}{% endblock %}</title>
    <link rel="stylesheet" href="{% static "css/blog.css" %}">
</head>
<body>
    <div id="content">
        {% block content %}
        {% endblock %}
    </div>
    <div id="sidebar">
        <h2>My blog</h2>
        <p>This is my blog.</p>
    </div>
</body>
</html>

{% load static %} 表示匯入由django.contrib.staticfiles應用提供的static模板標籤,匯入之後,在整個當前模板中都可以使用{% static %}標籤從而匯入靜態檔案例如blog.css(可在本書配套原始碼blog應用的static/目錄下找到,將其拷貝到你的專案的相同位置)。

還可以看到有兩個{% block %}表示這個標籤的開始與結束部分定義了一個塊,繼承該模板的模板將用具體內容替換這兩個塊。這兩個塊的名稱是titlecontent

編輯post/list.html

{% extends "blog/base.html" %}
{% block title %}My Blog{% endblock %}
{% block content %}
    <h1>My Blog</h1>
    {% for post in posts %}
        <h2>
            <a href="{{ post.get_absolute_url }}">
                {{ post.title }}
            </a>
        </h2>
        <p class="date">
        Published {{ post.publish }} by {{ post.author }}
        </p>
        {{ post.body|truncatewords:30|linebreaks }}
    {% endfor %}
{% endblock %}

通過使用{% extends %},讓該模板繼承了母版blog/base.html,然後用實際內容填充了titlecontent塊。通過迭代所有的文章,展示文章標題,釋出日期,作者、正文及一個連結到文章的規範化URL。在正文部分使用了兩個filter:truncatewords用來截斷指定數量的文字,linebreaks將結果帶上一個HTML換行。filter可以任意連用,每個都在上一個的結果上生效。

開啟系統命令列輸入python manage.py runserver啟動站點,然後在瀏覽器中訪問http://127.0.0.1:8000/blog/,可以看到如下頁面(如果沒有文章,通過管理後臺新增一些):

img

然後編輯post/detail.html

{% extends 'blog/base.html' %}
{% block title %}
{{ post.title }}
{% endblock %}

{% block content %}
    <h1>{{ post.title }}</h1>
    <p class="date">
    Published {{ post.publish }} by {{ post.author }}
    </p>
    {{ post.body|linebreaks }}
{% endblock %}

現在可以回到剛才的頁面,點選任何一篇文章可以看到詳情頁:

img

看一下此時的URL,應該類似/blog/2017/12/14/who-was-djangoreinhardt/。這就是我們生成的規範化的URL。

8新增分頁功能

當輸入一些文章後,你會很快意識到需要將所有的文章分頁進行顯示。Django自帶了一個分頁器可以方便地進行分頁。

編輯blog應用的views.py檔案,修改post_list檢視:

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

def post_list(request):
    object_list = Post.published.all()
    paginator = Paginator(object_list, 3)  # 每頁顯示3篇文章
    page = request.GET.get('page')
    try:
        posts = paginator.page(page)
    except PageNotAnInteger:
        # 如果page引數不是一個整數就返回第一頁
        posts = paginator.page(1)
    except EmptyPage:
        # 如果頁數超出總頁數就返回最後一頁
        posts = paginator.page(paginator.num_pages)
    return render(request, 'blog/post/list.html', {'page': page, 'posts': posts})

分頁器相關程式碼解釋如下:

  1. 使用要分頁的內容和每頁展示的內容數量,例項化Paginator類得到paginator物件
  2. 通過get()方法獲取page變數,表示當前的頁碼
  3. 呼叫paginator.page()方法獲取要展示的資料
  4. 如果page引數不是一個整數就返回第一頁,如果頁數超出總頁數就返回最後一頁
  5. 把頁碼和要展示的內容傳給頁面。

現在需要為分頁功能建立一個單獨的模板,以讓該模板可以包含在任何使用分頁功能的頁面中,在blog應用的templates/目錄中新建pagination.html,新增如下程式碼:

<div class="pagination">
    <span class="step-links">
        {% if page.has_previous %}
        <a href="?page={{ page.previous_page_number }}">Previous</a>
        {% endif %}
    <span class="current">
        Page {{ page.number }} of {{ page.paginator.num_pages }}.
    </span>
    {% if page.has_next %}
        <a href="?page={{ page.next_page_number }}">Next</a>
    {% endif %}
    </span>
</div>

這個用於分頁的模板接受一個名稱為Page的物件,然後顯示前一頁,後一頁和總頁數。為此,回到blog/post/list.html檔案,在{% content %}中的最下邊增加一行:

{% block content %}
    # ......
    {% include 'pagination.html' with page=posts %}
{% endblock %}

由於檢視傳遞給列表頁的Page物件的名稱叫做posts,所以通過with重新指定了變數名稱以讓分頁模板也能正確接收到該物件。

開啟瀏覽器到http://127.0.0.1:8000/blog/,可以看到頁面如下:

img

9使用基於類的檢視

Python中類可以取代函式,檢視是一個接受HTTP請求並返回HTTP響應的可呼叫物件,所以基於函式的檢視(FBV)也可以通過基於類的檢視(CBV)來實現。Django為CBV提供了基類View,包含請求分發功能和其他一些基礎功能。

CBV相比FBV有如下優點

  • 可編寫單獨的方法對應不同的HTTP請求型別如GET,POST,PUT等請求,不像FBV一樣需要使用分支
  • 使用多繼承建立可複用的類模組(也叫做mixins

可以看一下關於CBV的介紹:https://docs.djangoproject.com/en/2.0/topics/class-based-views/intro/。

我們用Django的內建CBV類ListView來改寫post_list檢視,ListView的作用是列出任意型別的資料。編輯blog應用的views.py檔案,新增下列程式碼:

from django.views.generic import ListView
class PostListView(ListView):
    queryset = Post.published.all()
    context_object_name = 'posts'
    paginate_by = 3
    template_name = 'blog/post/list.html'

這個CBV和post_list檢視函式的功能類似,在上邊的程式碼裡做了以下工作:

  • 使用queryset變數查詢所有已釋出的文章。實際上,可以不使用這個變數,通過指定model = Post,這個CBV就會去進行Post.objects.all()查詢獲得全部文章。
  • 設定posts為模板變數的名稱,如果不設定context_object_name引數,預設的變數名稱是object_list
  • 設定paginate_by為每頁顯示3篇文章
  • 通過template_name指定需要渲染的模板,如果不指定,預設使用blog/post_list.html

開啟blog應用的urls.py檔案,註釋掉剛才的post_list URL pattern,為PostListView類增加一行:

urlpatterns = [
    # post views
    # path('', views.post_list, name='post_list'),
    path('',views.PostListView.as_view(),name='post_list'),
    path('<int:year>/<int:month>/<int:day>/<slug:post>/', views.post_detail, name='post_detail'),
]

為了正常使用分頁功能,需要使用正確的變數名稱,Django內建的ListView返回的變數名稱叫做page_obj,所以必須修改post/list.html中匯入分頁模板的那行程式碼:

{% include 'pagination.html' with page=page_obj %}

在瀏覽器中開啟http://127.0.0.1:8000/blog/,看一下是否和原來使用post_list的結果一樣。這是一個簡單的CBV示例,會在第十章更加深入的瞭解CBV的使用。

總結

這一章通過建立一個簡單的部落格應用,學習了基礎的Django框架使用方法:設計了資料模型並且進行了資料模型遷移,建立了檢視,模板和URLs,還學習了分頁功能。下一章將學習給部落格增加評論系統和標籤分類功能,以及通過郵件分享文章連結的功能。

相關文章