10 構(gòu)建一個(gè)在線學(xué)習(xí)平臺(tái)
在上一章中,你為在線商店項(xiàng)目添加了國(guó)際化。你還構(gòu)建了一個(gè)優(yōu)惠券系統(tǒng)和一個(gè)商品推薦引擎。在本章中,你會(huì)創(chuàng)建一個(gè)新的項(xiàng)目。你會(huì)構(gòu)建一個(gè)在線學(xué)習(xí)平臺(tái),這個(gè)平臺(tái)會(huì)創(chuàng)建一個(gè)自定義的內(nèi)容管理系統(tǒng)。
在本章中,你會(huì)學(xué)習(xí)如何:
- 為模型創(chuàng)建fixtures
- 使用模型繼承
- 創(chuàng)建自定義O型字典
- 使用基于類的視圖和mixins
- 構(gòu)建表單集
- 管理組和權(quán)限
- 創(chuàng)建一個(gè)內(nèi)容管理系統(tǒng)
10.1 創(chuàng)建一個(gè)在線學(xué)習(xí)平臺(tái)
我們最后一個(gè)實(shí)戰(zhàn)項(xiàng)目是一個(gè)在線學(xué)習(xí)平臺(tái)。在本章中,我們會(huì)構(gòu)建一個(gè)靈活的內(nèi)容管理系統(tǒng)(CMS),允許教師創(chuàng)建課程和管理課程內(nèi)容。
首先,我們用以下命令為新項(xiàng)目創(chuàng)建一個(gè)虛擬環(huán)境,并激活它:
mkdir env
virtualenv env/educa
source env/educa/bin/activate
用以下命令在虛擬環(huán)境中安裝Django:
pip install Django
我們將在項(xiàng)目中管理圖片上傳,所以我們還需要用以下命令安裝Pillow:
pip install Pillow
使用以下命令創(chuàng)建一個(gè)新項(xiàng)目:
django-admin startproject educa
進(jìn)入新的educa
目錄,并用以下命令創(chuàng)建一個(gè)新應(yīng)用:
cd educa
django-admin startapp courses
編輯educa
項(xiàng)目的settings.py
文件,把courses
添加到INSTALLED_APPS
設(shè)置中:
INSTALLED_APPS = [
'courses',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
現(xiàn)在courses
應(yīng)用已經(jīng)在項(xiàng)目激活了。讓我們?yōu)檎n程和課程內(nèi)容定義模型。
10.2 構(gòu)建課程模型
我們的在線學(xué)習(xí)平臺(tái)會(huì)提供多種主題的課程。每個(gè)課程會(huì)劃分為可配置的單元數(shù)量,而每個(gè)單元會(huì)包括可配置的內(nèi)容數(shù)量。會(huì)有各種類型的內(nèi)容:文本,文件,圖片或者視頻。下面這個(gè)例子展示了我們的課程目錄的數(shù)據(jù)結(jié)構(gòu):
Subject 1
Course 1
Module 1
Content 1 (image)
Content 3 (text)
Module 2
Content 4 (text)
Content 5 (file)
Content 6 (video)
...
讓我們構(gòu)建課程模型。編輯courses
應(yīng)用的models.py
文件,并添加以下代碼:
from django.db import models
from django.contrib.auth.models import User
class Subject(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
class Meta:
ordering = ('title', )
def __str__(self):
return self.title
class Course(models.Model):
owner = models.ForeignKey(User, related_name='courses_created')
subject = models.ForeignKey(Subject, related_name='courses')
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
overview = models.TextField()
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ('-created',)
def __str__(self):
return self.title
class Module(models.Model):
course = models.ForeignKey(Course, related_name='modules')
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
def __str__(self):
return self.title
這些是初始的Subject
,Course
和Module
模型。Course
模型有以下字段:
-
owner
:創(chuàng)建給課程的教師 -
subject
:這個(gè)課程所屬的主題。一個(gè)指向Subject
模型的ForeignKey
字段。 -
title
:課程標(biāo)題. -
slug
:課程別名,之后在URL中使用。 -
overview
:一個(gè)TextField
列,表示課程概述。 -
created
:課程創(chuàng)建的日期和時(shí)間。因?yàn)樵O(shè)置了auto_now_add=True
,所以創(chuàng)建新對(duì)象時(shí),Django會(huì)自動(dòng)設(shè)置這個(gè)字段。
每個(gè)課程劃分為數(shù)個(gè)單元。因此,Module
模型包含一個(gè)指向Course
模型的ForeignKey
字段。
打開終端執(zhí)行以下命令,為應(yīng)用創(chuàng)建初始的數(shù)據(jù)庫(kù)遷移:
python manage.py makemigrations
你會(huì)看到以下輸出:
Migrations for 'courses':
courses/migrations/0001_initial.py
- Create model Course
- Create model Module
- Create model Subject
- Add field subject to course
然后執(zhí)行以下命令,同步遷移到數(shù)據(jù)庫(kù)中:
python manage.py migrate
你會(huì)看到一個(gè)輸出,其中包括所有已經(jīng)生效的數(shù)據(jù)庫(kù)遷移,包括Django的數(shù)據(jù)庫(kù)遷移。輸出會(huì)包括這一行:
Applying courses.0001_initial... OK
這個(gè)告訴我們,courses
應(yīng)用的模型已經(jīng)同步到數(shù)據(jù)庫(kù)中。
10.2.1 在管理站點(diǎn)注冊(cè)模型
我們將把課程模型添加到管理站點(diǎn)。編輯courses
應(yīng)用目錄中的admin.py
文件,并添加以下代碼:
from django.contrib import admin
from .models import Subject, Course, Module
@admin.register(Subject)
class SubjectAdmin(admin.ModelAdmin):
list_display = ['title', 'slug']
prepopulated_fields = {'slug': ('title', )}
class ModuleInline(admin.StackedInline):
model = Module
@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
list_display = ['title', 'subject', 'created']
list_filter = ['created', 'subject']
search_fields = ['title', 'overview']
prepopulated_fields = {'slug': ('title', )}
inlines = [ModuleInline]
現(xiàn)在courses
應(yīng)用的模型已經(jīng)在管理站點(diǎn)注冊(cè)。我們用@admin.register()
裝飾器代替admin.site.register()
函數(shù)。它們的功能是一樣的。
10.2.2 為模型提供初始數(shù)據(jù)
有時(shí)你可能希望用硬編碼數(shù)據(jù)預(yù)填充數(shù)據(jù)庫(kù)。這在項(xiàng)目創(chuàng)建時(shí)自動(dòng)包括初始數(shù)據(jù)很有用,來(lái)替代手工添加數(shù)據(jù)。Django自帶一種簡(jiǎn)單的方式,可以從數(shù)據(jù)庫(kù)中加載和轉(zhuǎn)儲(chǔ)(dump)數(shù)據(jù)到fixtures文件中。
Django支持JSON,XML或者YAML格式的fixtures。我們將創(chuàng)建一個(gè)fixture,其中包括一些項(xiàng)目的初始Subject
對(duì)象。
首先使用以下命令創(chuàng)建一個(gè)超級(jí)用戶:
python manage.py createsuperuser
然后用以下命令啟動(dòng)開發(fā)服務(wù)器:
python manage.py runserver
現(xiàn)在在瀏覽器中打開http://127.0.0.1:8000/admin/courses/subject/
。使用管理站點(diǎn)創(chuàng)建幾個(gè)主題。列表顯示頁(yè)面如下圖所示:
在終端執(zhí)行以下命令:
python manage.py dumpdata courses --indent=2
你會(huì)看到類似這樣的輸出:
[
{
"model": "courses.subject",
"pk": 1,
"fields": {
"title": "Programming",
"slug": "programming"
}
},
{
"model": "courses.subject",
"pk": 2,
"fields": {
"title": "Physics",
"slug": "physics"
}
},
{
"model": "courses.subject",
"pk": 3,
"fields": {
"title": "Music",
"slug": "music"
}
},
{
"model": "courses.subject",
"pk": 4,
"fields": {
"title": "Mathematics",
"slug": "mathematics"
}
}
]
dumpdata
命令從數(shù)據(jù)庫(kù)中轉(zhuǎn)儲(chǔ)數(shù)據(jù)到標(biāo)準(zhǔn)輸出,默認(rèn)用JSON序列化。返回的數(shù)據(jù)結(jié)構(gòu)包括模型和它的字段信息,Django可以把它加載到數(shù)據(jù)庫(kù)中。
你可以給這個(gè)命令提供應(yīng)用的名稱,或者用app.Model
格式指定輸出數(shù)據(jù)的模型。你還可以使用--format
標(biāo)簽指定格式。默認(rèn)情況下,dumpdata
輸出序列化的數(shù)據(jù)到標(biāo)準(zhǔn)輸出。但是,你可以使用--output
標(biāo)簽指定一個(gè)輸出文件。--indent
標(biāo)簽允許你指定縮進(jìn)。關(guān)于更多dumpdata
的參數(shù)信息,請(qǐng)執(zhí)行python manage.py dumpdata --help
命令。
使用以下命令,把這個(gè)轉(zhuǎn)儲(chǔ)保存到courses
應(yīng)用的fixtures/
目錄中:
mkdir courses/fixtures
python manage.py dumpdata courses --indent=2 --output=courses/fixtures/subjects.json
使用管理站點(diǎn)移除你創(chuàng)建的主題。然后使用以下命令把fixture加載到數(shù)據(jù)庫(kù)中:
python manage.py loaddata subjects.json
fixture中包括的所有Subject
對(duì)象已經(jīng)加載到數(shù)據(jù)庫(kù)中。
默認(rèn)情況下,Django在每個(gè)應(yīng)用的fixtures/
目錄中查找文件,但你也可以為loaddata
命令指定fixture文件的完整路徑。你還可以使用FIXTURE_DIRS
設(shè)置告訴Django查找fixtures的額外目錄。
Fixtures不僅對(duì)初始數(shù)據(jù)有用,還可以為應(yīng)用提供簡(jiǎn)單的數(shù)據(jù),或者測(cè)試必需的數(shù)據(jù)。
你可以在這里閱讀如何在測(cè)試中使用fixtures。
如果你想在模型遷移中加載fixtures,請(qǐng)閱讀Django文檔的數(shù)據(jù)遷移部分。記住,我們?cè)诘诰耪聞?chuàng)建了自定義遷移,用于修改模型后遷移已存在的數(shù)據(jù)。你可以在這里閱讀數(shù)據(jù)庫(kù)遷移的文檔。
10.3 為不同的內(nèi)容創(chuàng)建模型
我們計(jì)劃在課程模型中添加不同類型的內(nèi)容,比如文本,圖片,文件和視頻。我們需要一個(gè)通用的數(shù)據(jù)模型,允許我們存儲(chǔ)不同的內(nèi)容。在第六章中,我們已經(jīng)學(xué)習(xí)了使用通用關(guān)系創(chuàng)建指向任何模型對(duì)象的外鍵。我們將創(chuàng)建一個(gè)Content
模型表示單元內(nèi)容,并定義一個(gè)通過(guò)關(guān)系,關(guān)聯(lián)到任何類型的內(nèi)容。
編輯courses
應(yīng)用的models.py
文件,并添加以下導(dǎo)入:
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
然后在文件結(jié)尾添加以下代碼:
class Content(models.Model):
module = models.ForeignKey(Module, related_name='contents')
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
item = GenericForeignKey('content_type', 'object_id')
這是Content
模型。一個(gè)單元包括多個(gè)內(nèi)容,所以我們定義了一個(gè)指向Module
模型的外鍵。我們還建立了一個(gè)通用關(guān)系,從代表不同內(nèi)容類型的不同模型關(guān)聯(lián)到對(duì)象。記住,我們需要三個(gè)不同字段來(lái)設(shè)置一個(gè)通用關(guān)系。在Content
模型中,它們分別是:
-
content_type
:一個(gè)指向ContentType
模型的ForeignKey
字段。 -
object_id
:這是一個(gè)PositiveIntegerField
,存儲(chǔ)關(guān)聯(lián)對(duì)象的主鍵。 -
item
:通過(guò)組合上面兩個(gè)字段,指向關(guān)聯(lián)對(duì)象的GenericForeignKey
字段。
在這個(gè)模型的數(shù)據(jù)庫(kù)表中,只有content_type
和object_id
字段有對(duì)應(yīng)的列。item
字段允許你直接檢索或設(shè)置關(guān)聯(lián)對(duì)象,它的功能建立在另外兩個(gè)字段之上。
我們將為每種內(nèi)容類型使用不同的模型。我們的內(nèi)容模型會(huì)有通用字段,但它們存儲(chǔ)的實(shí)際內(nèi)容會(huì)不同。
10.3.1 使用模型繼承
Django支持模型繼承,類似Python中標(biāo)準(zhǔn)類的繼承。Django為使用模型繼承提供了以下三個(gè)選擇:
- 抽象模型:當(dāng)你想把一些通用信息放在幾個(gè)模型時(shí)很有用。不會(huì)為抽象模型創(chuàng)建數(shù)據(jù)庫(kù)表。
- 多表模型繼承:可用于層次中每個(gè)模型本身被認(rèn)為是一個(gè)完整模型的情況下。為每個(gè)模型創(chuàng)建一張數(shù)據(jù)庫(kù)表。
- 代理模型:當(dāng)你需要修改一個(gè)模型的行為時(shí)很有用。例如,包括額外的方法,修改默認(rèn)管理器,或者使用不同的元選項(xiàng)。不會(huì)為代理模型創(chuàng)建數(shù)據(jù)庫(kù)表。
讓我們近一步了解它們。
10.3.1.1 抽象模型
一個(gè)抽象模型是一個(gè)基類,其中定義了你想在所有子模型中包括的字段。Django不會(huì)為抽象模型創(chuàng)建任何數(shù)據(jù)庫(kù)表。會(huì)為每個(gè)子模型創(chuàng)建一張數(shù)據(jù)庫(kù)表,其中包括從抽象類繼承的字段,和子模型中定義的字段。
要標(biāo)記一個(gè)抽象模型,你需要在它的Meta
類中包括abstract=True
。Django會(huì)認(rèn)為它是一個(gè)抽象模型,并且不會(huì)為它創(chuàng)建數(shù)據(jù)庫(kù)表。要?jiǎng)?chuàng)建子模型,你只需要從抽象模型繼承。以下是一個(gè)Content
抽象模型和Text
子模型的例子:
from django.db import models
class BaseContent(models.Model):
title = models.CharField(max_length=200)
created = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
class Text(BaseContent):
body = models.TextField()
在這個(gè)例子中,Django只會(huì)為Text
模型創(chuàng)建數(shù)據(jù)庫(kù)表,其中包括title
,created
和body
字段。
10.3.1.2 多表模型繼承
在多表繼承中,每個(gè)模型都有一張相應(yīng)的數(shù)據(jù)庫(kù)表。Django會(huì)在子模型中創(chuàng)建指向父模型的OneToOneField
字段。
要使用多表繼承,你必須從已存在模型中繼承。Django會(huì)為原模型和子模型創(chuàng)建數(shù)據(jù)庫(kù)表。下面是一個(gè)多表繼承的例子:
from django.db import models
class BaseContent(models.Model):
title = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
class Text(BaseContent):
body = models.TextField()
Django會(huì)在Text
模型中包括一個(gè)自動(dòng)生成的OneToOneField
字段,并為每個(gè)模型創(chuàng)建一張數(shù)據(jù)庫(kù)表。
10.3.1.3 代理模型
代理模型用于修改模型的行為,比如包括額外的方法或者不同的元選項(xiàng)。這兩個(gè)模型都在原模型的數(shù)據(jù)庫(kù)表上進(jìn)行操作。在模型的Meta
類中添加proxy=True
來(lái)創(chuàng)建代理模型。
下面這個(gè)例子展示了如何創(chuàng)建一個(gè)代理模型:
from django.db import models
from django.utils import timezone
class BaseContent(models.Model):
title = models.CharField(max_length=100)
created = models.DateTimeField(auto_now_add=True)
class OrderedContent(BaseContent):
class Meta:
proxy = True
ordering = ['created']
def create_delta(self):
return timezone.now() - self.created
我們?cè)谶@里定義了一個(gè)OrderedContent
模型,它是Content
模型的代理模型。這個(gè)模型為QuerySet提供了默認(rèn)排序和一個(gè)額外的created_delta()
方法。Content
和OrderedContent
模型都在同一張數(shù)據(jù)庫(kù)表上操作,并且可以用ORM通過(guò)任何一個(gè)模型訪問(wèn)對(duì)象。
10.3.2 創(chuàng)建內(nèi)容模型
courses
應(yīng)用的Content
模型包含一個(gè)通用關(guān)系來(lái)關(guān)聯(lián)不同的內(nèi)容類型。我們將為每種內(nèi)容模型創(chuàng)建不用的模型。所有內(nèi)容模型會(huì)有一些通用的字段,和一些額外字段存儲(chǔ)自定義數(shù)據(jù)。我們將創(chuàng)建一個(gè)抽象模型,它會(huì)為所有內(nèi)容模型提供通用字段。
編輯courses
應(yīng)用的models.py
文件,并添加以下代碼:
class ItemBase(models.Model):
owner = models.ForeignKey(User, related_name='%(class)s_related')
title = models.CharField(max_length=250)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
def __str__(self):
return self.title
class Text(ItemBase):
content = models.TextField()
class File(ItemBase):
file = models.FileField(upload_to='files')
class Image(ItemBase):
file = models.FileField(upload_to='images')
class Video(ItemBase):
url = models.URLField()
在這段代碼中,我們定義了一個(gè)ItemBase
抽象模型。因此我們?cè)?code>Meta類中設(shè)置了abstract=True
。在這個(gè)模型中,我們定義了owner
,title
,created
和updated
字段。這些通用字段會(huì)用于所有內(nèi)容類型。owner
字段允許我們存儲(chǔ)哪個(gè)用戶創(chuàng)建了內(nèi)容。因?yàn)檫@個(gè)字段在抽象類中定義,所以每個(gè)子模型需要不同的related_name
。Django允許我們?cè)?code>related_name屬性中為模型的類名指定占位符,比如%(class)s
。這樣,每個(gè)子模型的related_name
會(huì)自動(dòng)生成。因?yàn)槲覀兪褂?code>%(class)s_related作為related_name
,所以每個(gè)子模型對(duì)應(yīng)的反向關(guān)系是text_related
,file_related
,image_related
和video_related
。
我們定義了四個(gè)從ItemBase
抽象模型繼承的內(nèi)容模型。分別是:
-
Text
:存儲(chǔ)文本內(nèi)容。 -
File
:存儲(chǔ)文件,比如PDF。 -
Image
:存儲(chǔ)圖片文件。 -
Video
:存儲(chǔ)視頻。我們使用URLField
字段來(lái)提供一個(gè)視頻的URL,從而可以嵌入視頻。
除了自身的字段,每個(gè)子模型還包括ItemBase
類中定義的字段。會(huì)為Text
,File
,Image
和Video
模型創(chuàng)建對(duì)應(yīng)的數(shù)據(jù)庫(kù)表。因?yàn)?code>ItemBase是一個(gè)抽象模型,所以它不會(huì)關(guān)聯(lián)到數(shù)據(jù)庫(kù)表。
編輯你之前創(chuàng)建的Content
模型,修改它的content_type
字段:
content_type = models.ForeignKey(
ContentType,
limit_choices_to = {
'model__in': ('text', 'video', 'image', 'file')
}
)
我們添加了limit_choices_to
參數(shù)來(lái)限制ContentType
對(duì)象可用于的通用關(guān)系。我們使用了model__in
字段查找,來(lái)過(guò)濾ContentType
對(duì)象的model
屬性為text
,video
,image
或者file
。
讓我們創(chuàng)建包括新模型的數(shù)據(jù)庫(kù)遷移。在命令行中執(zhí)行以下命令:
python manage.py makemigrations
你會(huì)看到以下輸出:
Migrations for 'courses':
courses/migrations/0002_content_file_image_text_video.py
- Create model Content
- Create model File
- Create model Image
- Create model Text
- Create model Video
然后執(zhí)行以下命令應(yīng)用新的數(shù)據(jù)庫(kù)遷移:
python manage.py migrate
你看到的輸出的結(jié)尾是:
Running migrations:
Applying courses.0002_content_file_image_text_video... OK
我們已經(jīng)創(chuàng)建了模型,可以添加不同內(nèi)容到課程單元中。但是我們的模型仍然缺少了一些東西。課程單元和內(nèi)容應(yīng)用遵循特定的順序。我們需要一個(gè)字段對(duì)它們進(jìn)行排序。
10.4 創(chuàng)建自定義模板字段
Django自帶一組完整的模塊字段,你可以用它們構(gòu)建自己的模型。但是,你也可以創(chuàng)建自己的模型字段來(lái)存儲(chǔ)自定義數(shù)據(jù),或者修改已存在字段的行為。
我們需要一個(gè)字段指定對(duì)象的順序。如果你想用Django提供的字段,用一種簡(jiǎn)單的方式實(shí)現(xiàn)這個(gè)功能,你可能會(huì)想在模型中添加一個(gè)PositiveIntegerField
。這是一個(gè)好的開始。我們可以創(chuàng)建一個(gè)從PositiveIntegerField
繼承的自定義字段,并提供額外的方法。
我們會(huì)在排序字段中添加以下兩個(gè)功能:
- 沒(méi)有提供特定序號(hào)時(shí),自動(dòng)分配一個(gè)序號(hào)。如果存儲(chǔ)對(duì)象時(shí)沒(méi)有提供序號(hào),我們的字段會(huì)基于最后一個(gè)已存在的排序?qū)ο螅詣?dòng)分配下一個(gè)序號(hào)。如果兩個(gè)對(duì)象的序號(hào)分別是1和2,保存第三個(gè)對(duì)象時(shí),如果沒(méi)有給定特定序號(hào),我們應(yīng)該自動(dòng)分配為序號(hào)3。
- 相對(duì)于其它字段排序?qū)ο蟆Un程單元將會(huì)相對(duì)于它們所屬的課程排序,而模塊內(nèi)容會(huì)相對(duì)于它們所屬的單元排序。
在courses
應(yīng)用目錄中創(chuàng)建一個(gè)fields.py
文件,并添加以下代碼:
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
class OrderField(models.PositiveIntegerField):
def __init__(self, for_fields=None, *args, **kwargs):
self.for_fields = for_fields
super().__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
if getattr(model_instance, self.attname) is None:
# no current value
try:
qs = self.model.objects.all()
if self.for_fields:
# filter by objects with the same field values
# for the fields in "for_fields"
query = {field: getattr(model_instance, field) for field in self.for_fields}
qs = qs.filter(**query)
# get the order of the last item
last_item = qs.latest(self.attname)
value = last_item.order + 1
except ObjectDoesNotExist:
value = 0
setattr(model_instance, self.attname, value)
return value
else:
return super().pre_save(model_instance, add)
這是我們自定義的OrderField
。它從Django提供的PositiveIntegerField
字段繼承。我們的OrderField
字段有一個(gè)可選的for_fields
參數(shù),允許我們指定序號(hào)相對(duì)于哪些字段計(jì)算。
我們的字段覆寫了PositiveIntegerField
字段的pre_save()
方法,它會(huì)在該字段保存到數(shù)據(jù)庫(kù)中之前執(zhí)行。我們?cè)谶@個(gè)方法中執(zhí)行以下操作:
- 我們檢查模型實(shí)例中是否已經(jīng)存在這個(gè)字段的值。我們使用
self.attname
,這是模型中指定的這個(gè)字段的屬性名。如果屬性的值不是None
,我們?nèi)缦掠?jì)算序號(hào):
- 我們構(gòu)建一個(gè)
QuerySet
檢索這個(gè)字段模型所有對(duì)象。我們通過(guò)訪問(wèn)self.model
檢索字段所屬的模型類。 - 我們用定義在字段的
for_fields
參數(shù)中的模型字段(如果有的話)的當(dāng)前值過(guò)濾QuerySet
。這樣,我們就能相對(duì)于給定字段計(jì)算序號(hào)。 - 我們用
last_item = qs.lastest(self.attname)
從數(shù)據(jù)庫(kù)中檢索序號(hào)最大的對(duì)象。如果沒(méi)有找到對(duì)象,我們假設(shè)它是第一個(gè)對(duì)象,并分配序號(hào)0。 - 如果找到一個(gè)對(duì)象,我們?cè)谡业降淖畲笮蛱?hào)上加1。
- 我們用
setattr()
把計(jì)算的序號(hào)分配給模型實(shí)例中的字段值,并返回這個(gè)值。
- 如果模型實(shí)例有當(dāng)前字段的值,則什么都不做。
當(dāng)你創(chuàng)建自定義模型字段時(shí),讓它們是通用的。避免分局特定模型或字段硬編碼數(shù)據(jù)。你的字段應(yīng)該可以用于所有模型。
你可以在這里閱讀更多關(guān)于編寫自定義模型字段的信息。
讓我們?cè)谀P椭刑砑有伦侄巍>庉?code>courses應(yīng)用的models.py
文件,并導(dǎo)入新的字段:
from .fields import OrderField
然后在Module
模型中添加OrderField
字段:
order = OrderField(blank=True, for_fields=['course'])
我們命名新字段為order
,并通過(guò)設(shè)置for_fields=['course']
,指定相對(duì)于課程計(jì)算序號(hào)。這意味著一個(gè)新單元會(huì)分配給同一個(gè)Course
對(duì)象中最新的單元加1。現(xiàn)在編輯Module
模型的__str__()
方法,并如下引入它的序號(hào):
def __str__(self):
return '{}. {}'.format(self.order, self.title)
單元內(nèi)容也需要遵循特定序號(hào)。在Content
模型中添加一個(gè)OrderField
字段:
order = OrderField(blank=True, for_fields=['module'])
這次我們指定序號(hào)相對(duì)于module
字段計(jì)算。最后,讓我們?yōu)閮蓚€(gè)模型添加默認(rèn)排序。在Module
和Content
模型中添加以下Meta
類:
class Meta:
ordering = ['order']
現(xiàn)在Module
和Content
模型看起來(lái)是這樣的:
class Module(models.Model):
course = models.ForeignKey(Course, related_name='modules')
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
order = OrderField(blank=True, for_fields=['course'])
class Meta:
ordering = ['order']
def __str__(self):
return '{}. {}'.format(self.order, self.title)
class Content(models.Model):
module = models.ForeignKey(Module, related_name='contents')
content_type = models.ForeignKey(
ContentType,
limit_choices_to = {
'model__in': ('text', 'video', 'image', 'file')
}
)
object_id = models.PositiveIntegerField()
item = GenericForeignKey('content_type', 'object_id')
order = OrderField(blank=True, for_fields=['module'])
class Meta:
ordering = ['order']
讓我們創(chuàng)建反映新序號(hào)字段的模型遷移。打開終端,并執(zhí)行以下命令:
python manage.py makemigrations courses
你會(huì)看到以下輸出:
You are trying to add a non-nullable field 'order' to content without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit, and let me add a default in models.py
Select an option:
Django告訴我們,因?yàn)槲覀冊(cè)谝汛嬖诘哪P椭刑砑恿诵伦侄危员仨殲閿?shù)據(jù)庫(kù)中已存在的行提供默認(rèn)值。如果字段有null=True
,則可以接受空值,并且Django創(chuàng)建遷移時(shí)不要求提供默認(rèn)值。我們可以指定一個(gè)默認(rèn)值,或者取消數(shù)據(jù)庫(kù)遷移,并在創(chuàng)建遷移之前在models.py
文件的order
字段中添加default
屬性。
輸入1
,然后按下Enter
,為已存在的記錄提供一個(gè)默認(rèn)值。你會(huì)看到以下輸出:
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>>
輸入0
作為已存在記錄的默認(rèn)值,然后按下Enter
。Django還會(huì)要求你為Module
模型提供默認(rèn)值。選擇第一個(gè)選項(xiàng),然后再次輸入0
作為默認(rèn)值。最后,你會(huì)看到類似這樣的輸出:
Migrations for 'courses':
courses/migrations/0003_auto_20170518_0743.py
- Change Meta options on content
- Change Meta options on module
- Add field order to content
- Add field order to module
然后執(zhí)行以下命令應(yīng)用新的數(shù)據(jù)庫(kù)遷移:
python manage.py migrate
這個(gè)命令的輸出會(huì)告訴你遷移已經(jīng)應(yīng)用成功:
Applying courses.0003_auto_20170518_0743... OK
讓我們測(cè)試新字段。使用python manage.py shell
命令打開終端,并如下創(chuàng)建一個(gè)新課程:
>>> from django.contrib.auth.models import User
>>> from courses.models import Subject, Course, Module
>>> user = User.objects.latest('id')
>>> subject = Subject.objects.latest('id')
>>> c1 = Course.objects.create(subject=subject, owner=user, title='Course 1', slug='course1')
我們已經(jīng)在數(shù)據(jù)庫(kù)中創(chuàng)建了一個(gè)課程。現(xiàn)在,讓我們添加一些單元到課程中,并查看單元序號(hào)是如何自動(dòng)計(jì)算的。我們創(chuàng)建一個(gè)初始單元,并檢查它的序號(hào):
>>> m1 = Module.objects.create(course=c1, title='Module 1')
>>> m1.order
0
OrderField
設(shè)置它的值為0,因?yàn)檫@是給定課程的第一個(gè)Module
對(duì)象。現(xiàn)在我們創(chuàng)建同一個(gè)課程的第二個(gè)單元:
>>> m2 = Module.objects.create(course=c1, title='Module 2')
>>> m2.order
1
OrderField
在已存在對(duì)象的最大序號(hào)上加1來(lái)計(jì)算下一個(gè)序號(hào)。讓我們指定一個(gè)特定序號(hào)來(lái)創(chuàng)建第三個(gè)單元:
>>> m3 = Module.objects.create(course=c1, title='Module 3', order=5)
>>> m3.order
5
如果我們指定了自定義序號(hào),則OrderField
字段不會(huì)介入,并且使用給定的order
值。
讓我們添加第四個(gè)單元:
>>> m4 = Module.objects.create(course=c1, title='Module 4')
>>> m4.order
6
這個(gè)單元的序號(hào)已經(jīng)自動(dòng)設(shè)置了。我們的OrderField
字段不能保證連續(xù)的序號(hào)。但是它關(guān)注已存在的序號(hào)值,總是根據(jù)已存在的最大序號(hào)值分配下一個(gè)序號(hào)。
讓我們創(chuàng)建第二個(gè)課程,并添加一個(gè)單元:
>>> c2 = Course.objects.create(subject=subject, owner=user, title='Course 2', slug='course2')
>>> m5 = Module.objects.create(course=c2, title='Module 1')
>>> m5.order
0
要計(jì)算新的單元序號(hào),該字段只考慮屬于同一個(gè)課程的已存在單元。因?yàn)檫@個(gè)第二個(gè)課程的第一個(gè)單元,所以序號(hào)為0。這是因?yàn)槲覀冊(cè)?code>Module模型的order
字段中指定了for_fields=['course']
。
恭喜你!你已經(jīng)成功的創(chuàng)建了第一個(gè)自定義模型字段。