Django2實戰示例 第一章 創建博客應用


目錄

Django2實戰示例 第一章 創建博客應用
Django2實戰示例 第二章 增強博客功能
Django2實戰示例 第三章 擴展博客功能
Django2實戰示例 第四章 創建社交網站
Django2實戰示例 第五章 內容分享功能
Django2實戰示例 第六章 追蹤用戶行為
Django2實戰示例 第七章 創建電商網站
Django2實戰示例 第八章 管理支付與訂單
Django2實戰示例 第九章 擴展商店功能
Django2實戰示例 第十章 創建在線教育平台
Django2實戰示例 第十一章 渲染和緩存課程內容
Django2實戰示例 第十二章 創建API
Django2實戰示例 第十三章 上線

第一章 創建博客應用

歡迎來到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/,會看到成功運行站點的頁面,如下圖所示:

image

能看到這個頁面,說明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_deleterelated_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根據模型的具體字段動態的生成了添加頁面,如下所示:

image

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

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

image

可以再錄入一些文章數據,為之后數據庫相關操作做准備。

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的列表頁,會看到如下所示:

image

可以看到在該頁面上顯示的字段就是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/,可以看到如下頁面(如果沒有文章,通過管理后台添加一些):

image

然后編輯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 %}

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

image

看一下此時的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,添加如下代碼:


    <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/,可以看到頁面如下:

image

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,還學習了分頁功能。下一章將學習給博客增加評論系統和標簽分類功能,以及通過郵件分享文章鏈接的功能。


免責聲明!

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



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