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']
。
恭喜你!你已经成功的创建了第一个自定义模型字段。