第十章 構建一個線上學習平臺(上)
10 構建一個線上學習平臺
在上一章中,你為線上商店專案添加了國際化。你還構建了一個優惠券系統和一個商品推薦引擎。在本章中,你會建立一個新的專案。你會構建一個線上學習平臺,這個平臺會建立一個自定義的內容管理系統。
在本章中,你會學習如何:
- 為模型建立fixtures
- 使用模型繼承
- 建立自定義O型字典
- 使用基於類的檢視和mixins
- 構建表單集
- 管理組和許可權
- 建立一個內容管理系統
10.1 建立一個線上學習平臺
我們最後一個實戰專案是一個線上學習平臺。在本章中,我們會構建一個靈活的內容管理系統(CMS),允許教師建立課程和管理課程內容。
首先,我們用以下命令為新專案建立一個虛擬環境,並激活它:
mkdir env
virtualenv env/educa
source env/educa/bin/activate
用以下命令在虛擬環境中安裝Django:
pip install Django
我們將在專案中管理圖片上傳,所以我們還需要用以下命令安裝Pillow:
pip install Pillow
使用以下命令建立一個新專案:
django-admin startproject educa
進入新的educa
目錄,並用以下命令建立一個新應用:
cd educa
django-admin startapp courses
編輯educa
專案的settings.py
courses
新增到INSTALLED_APPS
設定中:
INSTALLED_APPS = [
'courses',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
現在courses
應用已經在專案激活了。讓我們為課程和課程內容定義模型。
10.2 構建課程模型
我們的線上學習平臺會提供多種主題的課程。每個課程會劃分為可配置的單元數量,而每個單元會包括可配置的內容數量。會有各種型別的內容:文字,檔案,圖片或者視訊。下面這個例子展示了我們的課程目錄的資料結構:
Subject 1
Course 1
Module 1
Content 1 (image)
Content 3 (text)
Module 2
Content 4 (text)
Content 5 (file)
Content 6 (video)
...
讓我們構建課程模型。編輯courses
應用的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
:建立給課程的教師subject
:這個課程所屬的主題。一個指向Subject
模型的ForeignKey
欄位。title
:課程標題.slug
:課程別名,之後在URL中使用。overview
:一個TextField
列,表示課程概述。created
:課程建立的日期和時間。因為設定了auto_now_add=True
,所以建立新物件時,Django會自動設定這個欄位。
每個課程劃分為數個單元。因此,Module
模型包含一個指向Course
模型的ForeignKey
欄位。
開啟終端執行以下命令,為應用建立初始的資料庫遷移:
python manage.py makemigrations
你會看到以下輸出:
Migrations for 'courses':
courses/migrations/0001_initial.py
- Create model Course
- Create model Module
- Create model Subject
- Add field subject to course
然後執行以下命令,同步遷移到資料庫中:
python manage.py migrate
你會看到一個輸出,其中包括所有已經生效的資料庫遷移,包括Django的資料庫遷移。輸出會包括這一行:
Applying courses.0001_initial... OK
這個告訴我們,courses
應用的模型已經同步到資料庫中。
10.2.1 在管理站點註冊模型
我們將把課程模型新增到管理站點。編輯courses
應用目錄中的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]
現在courses
應用的模型已經在管理站點註冊。我們用@admin.register()
裝飾器代替admin.site.register()
函式。它們的功能是一樣的。
10.2.2 為模型提供初始資料
有時你可能希望用硬編碼資料預填充資料庫。這在專案建立時自動包括初始資料很有用,來替代手工新增資料。Django自帶一種簡單的方式,可以從資料庫中載入和轉儲(dump)資料到fixtures檔案中。
Django支援JSON,XML或者YAML格式的fixtures。我們將建立一個fixture,其中包括一些專案的初始Subject
物件。
首先使用以下命令建立一個超級使用者:
python manage.py createsuperuser
然後用以下命令啟動開發伺服器:
python manage.py runserver
現在在瀏覽器中開啟http://127.0.0.1:8000/admin/courses/subject/
。使用管理站點建立幾個主題。列表顯示頁面如下圖所示:
在終端執行以下命令:
python manage.py dumpdata courses --indent=2
你會看到類似這樣的輸出:
[
{
"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
命令從資料庫中轉儲資料到標準輸出,預設用JSON序列化。返回的資料結構包括模型和它的欄位資訊,Django可以把它載入到資料庫中。
你可以給這個命令提供應用的名稱,或者用app.Model
格式指定輸出資料的模型。你還可以使用--format
標籤指定格式。預設情況下,dumpdata
輸出序列化的資料到標準輸出。但是,你可以使用--output
標籤指定一個輸出檔案。--indent
標籤允許你指定縮排。關於更多dumpdata
的引數資訊,請執行python manage.py dumpdata --help
命令。
使用以下命令,把這個轉儲儲存到courses
應用的fixtures/
目錄中:
mkdir courses/fixtures
python manage.py dumpdata courses --indent=2 --output=courses/fixtures/subjects.json
使用管理站點移除你建立的主題。然後使用以下命令把fixture載入到資料庫中:
python manage.py loaddata subjects.json
fixture中包括的所有Subject
物件已經載入到資料庫中。
預設情況下,Django在每個應用的fixtures/
目錄中查詢檔案,但你也可以為loaddata
命令指定fixture檔案的完整路徑。你還可以使用FIXTURE_DIRS
設定告訴Django查詢fixtures的額外目錄。
Fixtures不僅對初始資料有用,還可以為應用提供簡單的資料,或者測試必需的資料。
你可以在這裡閱讀如何在測試中使用fixtures。
如果你想在模型遷移中載入fixtures,請閱讀Django文件的資料遷移部分。記住,我們在第九章建立了自定義遷移,用於修改模型後遷移已存在的資料。你可以在這裡閱讀資料庫遷移的文件。
10.3 為不同的內容建立模型
我們計劃在課程模型中新增不同型別的內容,比如文字,圖片,檔案和視訊。我們需要一個通用的資料模型,允許我們儲存不同的內容。在第六章中,我們已經學習了使用通用關係建立指向任何模型物件的外來鍵。我們將建立一個Content
模型表示單元內容,並定義一個通過關係,關聯到任何型別的內容。
編輯courses
應用的models.py
檔案,並新增以下匯入:
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
然後在檔案結尾新增以下程式碼:
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
模型。一個單元包括多個內容,所以我們定義了一個指向Module
模型的外來鍵。我們還建立了一個通用關係,從代表不同內容型別的不同模型關聯到物件。記住,我們需要三個不同欄位來設定一個通用關係。在Content
模型中,它們分別是:
content_type
:一個指向ContentType
模型的ForeignKey
欄位。object_id
:這是一個PositiveIntegerField
,儲存關聯物件的主鍵。item
:通過組合上面兩個欄位,指向關聯物件的GenericForeignKey
欄位。
在這個模型的資料庫表中,只有content_type
和object_id
欄位有對應的列。item
欄位允許你直接檢索或設定關聯物件,它的功能建立在另外兩個欄位之上。
我們將為每種內容型別使用不同的模型。我們的內容模型會有通用欄位,但它們儲存的實際內容會不同。
10.3.1 使用模型繼承
Django支援模型繼承,類似Python中標準類的繼承。Django為使用模型繼承提供了以下三個選擇:
- 抽象模型:當你想把一些通用資訊放在幾個模型時很有用。不會為抽象模型建立資料庫表。
- 多表模型繼承:可用於層次中每個模型本身被認為是一個完整模型的情況下。為每個模型建立一張資料庫表。
- 代理模型:當你需要修改一個模型的行為時很有用。例如,包括額外的方法,修改預設管理器,或者使用不同的元選項。不會為代理模型建立資料庫表。
讓我們近一步瞭解它們。
10.3.1.1 抽象模型
一個抽象模型是一個基類,其中定義了你想在所有子模型中包括的欄位。Django不會為抽象模型建立任何資料庫表。會為每個子模型建立一張資料庫表,其中包括從抽象類繼承的欄位,和子模型中定義的欄位。
要標記一個抽象模型,你需要在它的Meta
類中包括abstract=True
。Django會認為它是一個抽象模型,並且不會為它建立資料庫表。要建立子模型,你只需要從抽象模型繼承。以下是一個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()
在這個例子中,Django只會為Text
模型建立資料庫表,其中包括title
,created
和body
欄位。
10.3.1.2 多表模型繼承
在多表繼承中,每個模型都有一張相應的資料庫表。Django會在子模型中建立指向父模型的OneToOneField
欄位。
要使用多表繼承,你必須從已存在模型中繼承。Django會為原模型和子模型建立資料庫表。下面是一個多表繼承的例子:
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會在Text
模型中包括一個自動生成的OneToOneField
欄位,併為每個模型建立一張資料庫表。
10.3.1.3 代理模型
代理模型用於修改模型的行為,比如包括額外的方法或者不同的元選項。這兩個模型都在原模型的資料庫表上進行操作。在模型的Meta
類中新增proxy=True
來建立代理模型。
下面這個例子展示瞭如何建立一個代理模型:
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
我們在這裡定義了一個OrderedContent
模型,它是Content
模型的代理模型。這個模型為QuerySet提供了預設排序和一個額外的created_delta()
方法。Content
和OrderedContent
模型都在同一張資料庫表上操作,並且可以用ORM通過任何一個模型訪問物件。
10.3.2 建立內容模型
courses
應用的Content
模型包含一個通用關係來關聯不同的內容型別。我們將為每種內容模型建立不用的模型。所有內容模型會有一些通用的欄位,和一些額外欄位儲存自定義資料。我們將建立一個抽象模型,它會為所有內容模型提供通用欄位。
編輯courses
應用的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()
在這段程式碼中,我們定義了一個ItemBase
抽象模型。因此我們在Meta
類中設定了abstract=True
。在這個模型中,我們定義了owner
,title
,created
和updated
欄位。這些通用欄位會用於所有內容型別。owner
欄位允許我們儲存哪個使用者建立了內容。因為這個欄位在抽象類中定義,所以每個子模型需要不同的related_name
。Django允許我們在related_name
屬性中為模型的類名指定佔位符,比如%(class)s
。這樣,每個子模型的related_name
會自動生成。因為我們使用%(class)s_related
作為related_name
,所以每個子模型對應的反向關係是text_related
,file_related
,image_related
和video_related
。
我們定義了四個從ItemBase
抽象模型繼承的內容模型。分別是:
Text
:儲存文字內容。File
:儲存檔案,比如PDF。Image
:儲存圖片檔案。Video
:儲存視訊。我們使用URLField
欄位來提供一個視訊的URL,從而可以嵌入視訊。
除了自身的欄位,每個子模型還包括ItemBase
類中定義的欄位。會為Text
,File
,Image
和Video
模型建立對應的資料庫表。因為ItemBase
是一個抽象模型,所以它不會關聯到資料庫表。
編輯你之前建立的Content
模型,修改它的content_type
欄位:
content_type = models.ForeignKey(
ContentType,
limit_choices_to = {
'model__in': ('text', 'video', 'image', 'file')
}
)
我們添加了limit_choices_to
引數來限制ContentType
物件可用於的通用關係。我們使用了model__in
欄位查詢,來過濾ContentType
物件的model
屬性為text
,video
,image
或者file
。
讓我們建立包括新模型的資料庫遷移。在命令列中執行以下命令:
python manage.py makemigrations
你會看到以下輸出:
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
然後執行以下命令應用新的資料庫遷移:
python manage.py migrate
你看到的輸出的結尾是:
Running migrations:
Applying courses.0002_content_file_image_text_video... OK
我們已經建立了模型,可以新增不同內容到課程單元中。但是我們的模型仍然缺少了一些東西。課程單元和內容應用遵循特定的順序。我們需要一個欄位對它們進行排序。
10.4 建立自定義模板欄位
Django自帶一組完整的模組欄位,你可以用它們構建自己的模型。但是,你也可以建立自己的模型欄位來儲存自定義資料,或者修改已存在欄位的行為。
我們需要一個欄位指定物件的順序。如果你想用Django提供的欄位,用一種簡單的方式實現這個功能,你可能會想在模型中新增一個PositiveIntegerField
。這是一個好的開始。我們可以建立一個從PositiveIntegerField
繼承的自定義欄位,並提供額外的方法。
我們會在排序欄位中新增以下兩個功能:
- 沒有提供特定序號時,自動分配一個序號。如果儲存物件時沒有提供序號,我們的欄位會基於最後一個已存在的排序物件,自動分配下一個序號。如果兩個物件的序號分別是1和2,儲存第三個物件時,如果沒有給定特定序號,我們應該自動分配為序號3。
- 相對於其它欄位排序物件。課程單元將會相對於它們所屬的課程排序,而模組內容會相對於它們所屬的單元排序。
在courses
應用目錄中建立一個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
欄位有一個可選的for_fields
引數,允許我們指定序號相對於哪些欄位計算。
我們的欄位覆寫了PositiveIntegerField
欄位的pre_save()
方法,它會在該欄位儲存到資料庫中之前執行。我們在這個方法中執行以下操作:
我們檢查模型例項中是否已經存在這個欄位的值。我們使用
self.attname
,這是模型中指定的這個欄位的屬性名。如果屬性的值不是None
,我們如下計算序號:- 我們構建一個
QuerySet
檢索這個欄位模型所有物件。我們通過訪問self.model
檢索欄位所屬的模型類。 - 我們用定義在欄位的
for_fields
引數中的模型欄位(如果有的話)的當前值過濾QuerySet
。這樣,我們就能相對於給定欄位計算序號。 - 我們用
last_item = qs.lastest(self.attname)
從資料庫中檢索序號最大的物件。如果沒有找到物件,我們假設它是第一個物件,並分配序號0。 - 如果找到一個物件,我們在找到的最大序號上加1。
- 我們用
setattr()
把計算的序號分配給模型例項中的欄位值,並返回這個值。
- 我們構建一個
如果模型例項有當前欄位的值,則什麼都不做。
當你建立自定義模型欄位時,讓它們是通用的。避免分局特定模型或欄位硬編碼資料。你的欄位應該可以用於所有模型。
你可以在這裡閱讀更多關於編寫自定義模型欄位的資訊。
讓我們在模型中新增新欄位。編輯courses
應用的models.py
檔案,並匯入新的欄位:
from .fields import OrderField
然後在Module
模型中新增OrderField
欄位:
order = OrderField(blank=True, for_fields=['course'])
我們命名新欄位為order
,並通過設定for_fields=['course']
,指定相對於課程計算序號。這意味著一個新單元會分配給同一個Course
物件中最新的單元加1。現在編輯Module
模型的__str__()
方法,並如下引入它的序號:
def __str__(self):
return '{}. {}'.format(self.order, self.title)
單元內容也需要遵循特定序號。在Content
模型中新增一個OrderField
欄位:
order = OrderField(blank=True, for_fields=['module'])
這次我們指定序號相對於module
欄位計算。最後,讓我們為兩個模型新增預設排序。在Module
和Content
模型中新增以下Meta
類:
class Meta:
ordering = ['order']
現在Module
和Content
模型看起來是這樣的:
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']
讓我們建立反映新序號欄位的模型遷移。開啟終端,並執行以下命令:
python manage.py makemigrations courses
你會看到以下輸出:
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告訴我們,因為我們在已存在的模型中添加了新欄位,所以必須為資料庫中已存在的行提供預設值。如果欄位有null=True
,則可以接受空值,並且Django建立遷移時不要求提供預設值。我們可以指定一個預設值,或者取消資料庫遷移,並在建立遷移之前在models.py
檔案的order
欄位中新增default
屬性。
輸入1
,然後按下Enter
,為已存在的記錄提供一個預設值。你會看到以下輸出:
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
作為已存在記錄的預設值,然後按下Enter
。Django還會要求你為Module
模型提供預設值。選擇第一個選項,然後再次輸入0
作為預設值。最後,你會看到類似這樣的輸出:
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
然後執行以下命令應用新的資料庫遷移:
python manage.py migrate
這個命令的輸出會告訴你遷移已經應用成功:
Applying courses.0003_auto_20170518_0743... OK
讓我們測試新欄位。使用python manage.py shell
命令開啟終端,並如下建立一個新課程:
>>> 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')
我們已經在資料庫中建立了一個課程。現在,讓我們新增一些單元到課程中,並檢視單元序號是如何自動計算的。我們建立一個初始單元,並檢查它的序號:
>>> m1 = Module.objects.create(course=c1, title='Module 1')
>>> m1.order
0
OrderField
設定它的值為0,因為這是給定課程的第一個Module
物件。現在我們建立同一個課程的第二個單元:
>>> m2 = Module.objects.create(course=c1, title='Module 2')
>>> m2.order
1
OrderField
在已存在物件的最大序號上加1來計算下一個序號。讓我們指定一個特定序號來建立第三個單元:
>>> m3 = Module.objects.create(course=c1, title='Module 3', order=5)
>>> m3.order
5
如果我們指定了自定義序號,則OrderField
欄位不會介入,並且使用給定的order
值。
讓我們新增第四個單元:
>>> m4 = Module.objects.create(course=c1, title='Module 4')
>>> m4.order
6
這個單元的序號已經自動設定了。我們的OrderField
欄位不能保證連續的序號。但是它關注已存在的序號值,總是根據已存在的最大序號值分配下一個序號。
讓我們建立第二個課程,並新增一個單元:
>>> 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
要計算新的單元序號,該欄位只考慮屬於同一個課程的已存在單元。因為這個第二個課程的第一個單元,所以序號為0。這是因為我們在Module
模型的order
欄位中指定了for_fields=['course']
。
恭喜你!你已經成功的建立了第一個自定義模型欄位。