当前位置: 首页 > news >正文

测试开发之Django实战示例 第十章 创建在线教育平台

第十章 创建在线教育平台

在上一章,我们为电商网站项目添加了国际化功能,还创建了优惠码和商品推荐系统。在本章,会建立一个新的项目:一个在线教育平台,并创内容管理系统CMS(Content Management System)。

本章的具体内容有

  • 为模型建立fixtures

  • 使用模型的继承关系

  • 创建自定义模型字段

  • 使用CBV和mixin

  • 建立表单集formsets

  • 管理用户组与权限

  • 创建CMS

1创建在线教育平台项目

我们最后一个项目就是这个在线教育平台。在这个项目中,我们将建立一个灵活的CMS系统,让讲师可以创建课程并且管理课程的内容。

为本项目建立一个虚拟环境,在终端输入如下命令:

Copymkdirenv
virtualenv env/educa
sourceenv/educa/bin/activate

在虚拟环境中安装Django与Pillow:

Copypip install Django==2.0.5
pip install Pillow==5.1.0

之后新建项目educa:

Copydjango-admin startproject educa

进入educa目录然后新建名为courses的应用:

Copycd educa
django-admin startapp courses

编辑settings.py,将应用激活并且放在最上边一行:

CopyINSTALLED_APPS = ['courses.apps.CoursesConfig','django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles',
]

之后的第一步工作,依然是定义数据模型。

2创建课程模型

我们的在线教育平台会提供很多不同主题(subject)的课程,每一个课程会被划分为一定数量的课程章节(module),每个章节里边又有一定数量的内容(content)。对于一个课程来说,里边使用到的内容类型很多,包含文本,文件,图片甚至视频,下边的是一个课程的例子:

CopySubject 1Course 1Module 1Content1 (image)Content2 (text)Module 2Content3 (text)Content4 (file)Content5 (video)
......

来建立课程的数据模型,编辑courses应用下的models.py文件:

Copyfrom django.db import models
from django.contrib.auth.models import UserclassSubject(models.Model):title = models.CharField(max_length=200)slug = models.SlugField(max_length=200, unique=True)classMeta:ordering = ['title']def__str__(self):return self.titleclassCourse(models.Model):owner = models.ForeignKey(User, related_name='course_created', on_delete=models.CASCADE)subject = models.ForeignKey(Subject, related_name='courses', on_delete=models.CASCADE)title = models.CharField(max_length=200)slug = models.SlugField(max_length=200, unique=True)overview = models.TextField()created = models.DateTimeField(auto_now_add=True)classMeta:ordering = ['-created']def__str__(self):return self.titleclassModule(models.Model):course = models.ForeignKey(Course,related_name='modules',on_delete=models.CASCADE)title = models.CharField(max_length=200)description = models.TextField(blank=True)def__str__(self):return self.title

这是初始的Subject,Course和Module模型。Course模型的字段如下:

  1. owner: 课程讲师,也是课程创建者

  1. subject: 课程的主体,外键关联到Subject模型

  1. title: 课程名称

  1. slug: 课程slug名称,将来用在生成URL

  1. overview: 课程简介

  1. created: 课程建立时间,生成数据行时候自动填充

Module从属于一个具体的课程,所以Module模型中有一个外键连接到Course模型。

之后进行数据迁移,不再赘述。

2.1在管理后台注册上述模型

编辑course应用的admin.py文件,添加如下代码:

Copyfrom django.contrib import admin
from .models import Subject, Course, Module@admin.register(Subject)classSubjectAdmin(admin.ModelAdmin):list_display = ['title', 'slug']prepopulated_fields = {'slug': ('title',)}classModuleInline(admin.StackedInline):model = Module@admin.register(Course)classCourseAdmin(admin.ModelAdmin):list_display = ['title', 'subject', 'created']list_filter = ['created', 'subject']search_fields = ['title', 'overview']prepopulated_fields = {'slug': ('title',)}inlines = [ModuleInline]

这就注册好了应用里的全部模型,记住@admin.register()用于将模型注册到管理后台中。

2.2使用fixture为模型提供初始化数据

有些时候,需要使用原始数据来直接填充数据库,这比每次建立项目之后手工录入原始数据要方便很多。DJango提供了fixtures(可以理解为一个预先格式化好的数据文件)功能,可以方便的从数据库中读取数据到fixture中,或者把fixture中的数据导入至数据库。

Django支持使用JSON,XML或YAML等格式来使用fixture。来建立一个包含一些初始化的Subject对象的fixture:

首先创建超级用户:

Copypython manage.py createsuperuser

之后运行站点:

Copypython manage.py runserver

进入http://127.0.0.1:8000/admin/courses/subject/可以看到如下界面(需要先输入一些数据):

在shell中执行如下命令:

Copypython manage.py dumpdata courses --indent=2

可以看到如下输出:

Copy[{"model":"courses.subject","pk":1,"fields":{"title":"Mathematics","slug":"mathematics"}},{"model":"courses.subject","pk":2,"fields":{"title":"Music","slug":"music"}},{"model":"courses.subject","pk":3,"fields":{"title":"Physics","slug":"physics"}},{"model":"courses.subject","pk":4,"fields":{"title":"Programming","slug":"programming"}}]

dumpdata命令采取默认的JSON格式,将Course类中的数据序列化并且输出。JSON中包含了模型的名称,主键,字段与对应的值。设置了indent=2是表示每行的缩进。

可以通过向命令行提供应用名和模块名,例如app.Model,让数据直接输出到这个模型中;还可以通过--format参数控制输出的数据格式,默认是使用JSON格式。还可以通过--output参数指定输出到具体文件。

对于dumpdata的详细参数,可以使用命令python manage.py dumpdata --help查看。

使用如下命令把这个dump结果保存到courses应用的一个fixture/目录中:

Copymkdir courses/fixtures
python manage.py dumpdata courses --indent=2 --output=courses/fixtures/subjects.json

译者注,原书写成了在orders应用下的fixture/目录,显然是将应用名写错了。

现在进入管理后台,将Subject表中的数据全部删除,之后执行下列语句,从fixture中加载数据:

Copypython manage.py loaddata subjects.json

可以发现,所有删除的数据都都回来了。

默认情况下Django会到每个应用里的fixtures/目录内寻找指定的文件名,也可以在settings.py中设置 FIXTURE_DIRS来告诉Django到哪里寻找fixture。

fixture除了初始化数据库之外,还可以方便的为应用提供测试数据。

有关fixture的详情可以查看https://docs.djangoproject.com/en/2.0/topics/testing/tools/#fixture-loading。

如果在进行数据模型移植的时候就加载fixture生成初始数据,可以查看https://docs.djangoproject.com/en/2.0/topics/migrations/#data-migrations。

3创建不同类型内容的模型

在课程中会向用户提供不同类型的内容,包括文字,图片,文件和视频等。我们必须采用一个能够存储各种文件类型的通用模型。在第六章中,我们学会了使用通用关系来创建与项目内任何一个数据模型的关系。这里我们建立一个Content模型,用于存放章节中的内容,定义一个通用关系来连接任何类型的内容。

编辑courses应用的models.py文件,增加下列内容:

Copyfrom django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

之后在文件末尾添加下列内容:

CopyclassContent(models.Model):module = models.ForeignKey(Module, related_name='contents', on_delete=models.CASCADE)content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)object_id = models.PositiveIntegerField()item = GenericForeignKey('content_type', 'object_id')

这就是Content模型,设置外键关联到了Module模型,同时设置了与ContentType模型的通用关联关系,可以从获取任意模型的内容。复习一下创建通用关系的所需的三个字的:

  1. content_type:一个外键用于关联到ContentType模型。

  1. object_id: 对象的id,使用PositiveIntegerField字段。

  1. item: 通用关联关系字段,通过合并上两个字段来进行关联。

content_type, object_id两个字段会实际生成在数据库中,item字段的关系是ORM引擎构建的,不真正被写进数据库中。

下一步的工作是建立每种具体内容类型的数据库,这些数据库有一些相同的字段用于标识基本信息,也有不同的字段存放该模型独特的信息。

3.1模型的继承

Django支持数据模型之间的继承关系,这和Python程序的类继承关系很相似,Django提供了以下三种继承的方式:

  1. Abstarct model: 接口模型继承,用于方便的向不同的数据模型中添加相同的信息,这种继承方式中的基类不会在数据库中建立数据表,子类会建立数据表。

  1. Multi-table model inheritance: 多表模型继承,在继承关系中的每个表都被认为是一个完整的模型时采用此方法,继承关系中的每一个表都会实际在数据库中创建数据表。

  1. Proxy models:代理模型继承,在继承的时候需要改变模型的行为时使用,例如加入额外的方法,修改默认的模型管理器或使用新的Meta类设置,此种继承不会在数据库中创建数据表。

让我们详细看一下这三种方式。

3.1.1Abstract models 抽象基类继承

接口模型本质上是一个基类类,其中定义了所有需要包含在子模型中的字段。Django不会为接口模型创建任何数据库中的数据表。继承接口模型的子模型必须将这些字段完善,每一个子模型会创建数据表,表中的字段包括继承自接口模型的字段和子模型中自定义的字段。

为了标记一个模型为接口模型,在其Meta设置中,必须设置abstract = True,django就会认为该模型是一个接口模型,不会创建数据表。子模型只需要继承该模型即可。

下边的例子是如何建立一个接口模型Content和子模型Text:

Copyfrom django.db import modelsclassBaseContent(models.Model):title = models.CharField(max_length=100)created = models.DateTimeField(auto_now_add=True)classMeta:abstract = TrueclassText(BaseContent):body = models.TextField()

在这个例子中,实际在数据库中创建的是Text类对应的数据表,包含title,created和body字段。

3.1.2Multi-table model inheritance 多表继承

多表继承关系中的每一个表都是完整的数据模型。对于继承关系,Django会自动在子模型中创建一个一对一关系的外键连接到父模型。

要使用该种继承方式,必须继承一个已经存在的模型,django会把父模型和子模型都写入数据库,下边是一个例子:

Copyfrom django.db import modelsclassBaseContent(models.Model):title = models.CharField(max_length=100)created = models.DateTimeField(auto_now_add=True)classText(BaseContent):body = models.TextField()

Django会将两张表都写入数据库,Text表中除了body字段,还有一个一对一的外键关联到BaseContent表。

3.1.3Proxy models 代理模型

代理模型用于改变类的行为,例如增加额外的方法或者不同的Meta设置。父模型和子模型操作一张相同的数据表。Meta类中指定proxy=True 就可以建立一个代理模型。

下边是一个创建代理模型的例子:

Copyfrom django.db import models
from django.utils import timezoneclassBaseContent(models.Model):title = models.CharField(max_length=100)created = models.DateTimeField(auto_now_add=True)classOrderedContent(BaseContent):classMeta:proxy = Trueordering = ['created']defcreated_delta(self):return timezone.now() - self.created

这里我们定义了一个OrderedContent模型,作为BaseContent模型的一个代理模型。这个代理模型提供了排序设置和一个新方法created_delta()。OrderedContent和BaseContent都是操作由BaseContent模型生成的数据表,但新增的排序和方法,只有通过OrderedContent对象才能使用。

这种方法就类似于经典的Python类继承方式。

3.2创建内容的模型

courses应用中的Content模型现在有着通用关系,可以取得任何模型的数据。我们要为每种内容建立不同的模型。所有的内容模型都有相同的字段也有不同的字段,这里就采取接口模型继承的方式来建立内容模型:

编辑courses应用中的models.py文件,添加下列代码:

CopyclassItemBase(models.Model):owner = models.ForeignKey(User, related_name='%(class)s_related', on_delete=models.CASCADE)title = models.CharField(max_length=250)created = models.DateTimeField(auto_now_add=True)updated = models.DateTimeField(auto_now=True)classMeta:abstract = Truedef__str__(self):return self.titleclassText(ItemBase):content = models.TextField()classFile(ItemBase):file = models.FileField(upload_to='files')classImage(ItemBase):file = models.FileField(upload_to='images')classVideo(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',最后实际的名称是text_related, file_related, image_related 和 video_retaled。

我们定义了四种类型的内容模型,均继承ItemBase抽象基类:

  • Text: 存储教学文本

  • File: 存储分发给用户的文件,比如PDF文件等教学资料

  • Image: 存储图片

  • Video:存储视频,定义了一个URLField字段存储视频的路径。

每个子模型中都包含ItemBase中定义的字段。Django会针对四个子模型分别在数据库中创建数据表,但ItemBase类不会被写入数据库。

继续编辑courses应用的models.py文件,由于四个子模型的类名已经确定了,需要修改Content模型让其对应到这四个模型上,修改content_type字段如下:

CopyclassContent(models.Model):content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE,limit_choices_to={'model__in': ('text', 'file', 'image', 'video')})

这里使用了limit_choices_to属性,以使ContentType对象限于这四个模型中。如此定义之后,在查询数据库的时候还能够使用filter的参数例如model__in='text'来检索具体某个模型的对象。

建立好所有模型之后,执行数据迁移程序,不再赘述。

现在就已经建立了本项目所需要的基本数据表及其结构。然而我们的模型中还缺少一些内容:课程和课程的内容是按照一定顺序排列的,但用户建立课程和上传内容的时候未必是线性的,我们需要一个排序字段,通过字段可以把课程,章节和内容进行排序。

3.3创建自定义字段

Django内置了很完善的模型字段供方便快捷的建立数据模型。然而依然有无法满足用户需求的地方,我们也可以自定义模型字段,来存储个性化的内容,或者修改内置字段的行为。

我们需要一个字段存储课程和内容组织的顺序。通常用于确定顺序可以方便的采用内置的PositiveIntegerField字段,采用一个正整数就可以方便的标记数据的顺序。这里我们继承PositiveIntegerField字段,然后增加额外的行为来完成我们的自定义排序。

我们要给自定义字段增加增加如下两个功能:

  • 如果序号没有给出,则自动分配一个序号。当内容和课程表中存进一个新的数据对象的时候,如果用户给出了具体的序号,就将该序号存入到排序字段中。如果用户没有给出序号,应该自动按照最大的序号再加1。例如如果已经存在两个数据对象的序号是1和2,如果用户存入第三个数据但未给出序号,则应该自动给新数据对象分配序号3。

  • 根据其他相关的内容排序:章节应该按照课程排序,而内容应该按照章节排序

在courses应用下建立fields.py文件,添加如下代码:

Copyfrom django.db import models
from django.core.exceptions import ObjectDoesNotExistclassOrderField(models.PositiveIntegerField):def__init__(self, for_fields=None, *args, kwargs):self.for_fields = for_fieldssuper(OrderField, self).__init__(*args, kwargs)defpre_save(self, model_instance, add):ifgetattr(model_instance, self.attname) isNone:# 如果没有值,查询自己所在表的全部内容,找到最后一条字段,设置临时变量value = 最后字段的序号+1try:qs = self.model.objects.all()if self.for_fields:# 存在for_fields参数,通过该参数取对应的数据行query = {field: getattr(model_instance, field) for field in self.for_fields}qs = qs.filter(query)# 取最后一个数据对象的序号last_item = qs.latest(self.attname)value = last_item.order + 1except ObjectDoesNotExist:value = 0setattr(model_instance, self.attname, value)return valueelse:returnsuper(OrderField, self).pre_save(model_instance, add)

这是自定义的字段类OrderField,继承了内置的PositiveIntegerField类,还增加了额外的参数for_fields指定按照哪一个字段的顺序进行计算。

我们重写了pre_save()方法,这个方法是在将字段的值实际存入到数据库之前执行的。在这个方法里,执行了如下逻辑:

  1. 检查当前字段是否已经存在值,self.attname表示该字段对应的属性名,也就是字段属性。如果属性名是None,说明用户没有设置序号。则按照以下逻辑进行计算:

  1. 建立一个QuerySet,查询这个字段所在的模型的全部数据行。访问字段所在的模型使用了self.model

  1. 通过用户给出的for_fields参数,把上一步的QuerySet用其中的字段拆解之后过滤,这样就可以取得具体的用于计算序号的参考数据行。

  1. 然后从过滤过的QuerySet中使用last_item = qs.latest(self.attname)方法取出最新一行数据对应的序号。如果取不到,说明自己是第一行。就将临时变量设置为0

  1. 如果能够取到,就把取到的序号+1然后赋给value临时变量

  1. 然后通过setattr()将临时变量value添加为字段名属性对应的值

  1. 如果当前的字段已经有值,说明用户传入了序号,不需要做任何工作。

在自定义字段时,一定不要硬编码将内容写死,也需要像内置字段一样注意通用性。

关于自定义字段可以看https://docs.djangoproject.com/en/2.0/howto/custom-model-fields/。

3.4将自定义字段加入到模型中

建立好自定义的字段类之后,需要在各个模型中设置该字段,编辑courses应用的models.py文件,添加如下内容:

Copyfrom .fields import OrderFieldclassModule(models.Model):# ......order = OrderField(for_fields=['course'], blank=True)

我们给自定义的排序字段起名叫order,然后通过设置for_fields=['course'],让该字段按照课程来排序。这意味着如果最新的某个Course对象关联的module对象的序号是3,为该Course对象其新增一个关联的module对象的序号就是4。

然后编辑Module模型的__str__()方法:

CopyclassModule(models.Model):def__str__(self):return'{}. {}'.format(self.order, self.title)

章节对应的内容也必须有序号,现在为Content模型也增加上OrderField类型的字段:

CopyclassContent(models.Model):# ...order = OrderField(blank=True, for_fields=['module'])

这样就指定了Content对象的序号根据其对应的module字段来排序,最后为两个模型添加默认的排序,为两个模型添加如下Meta类:

CopyclassModule(models.Model):# ...classMeta:ordering = ['order']classContent(models.Model):# ...classMeta:ordering = ['order']

最终的Module和Content模型应该是这样:

CopyclassModule(models.Model):course = models.ForeignKey(Course, related_name='modules', on_delete=models.CASCADE)title = models.CharField(max_length=200)description = models.TextField(blank=True)order = OrderField(for_fields=['course'], blank=True)def__str__(self):return'{}. {}'.format(self.order, self.title)classMeta:ordering = ['order']classContent(models.Model):module = models.ForeignKey(Module, related_name='contents', on_delete=models.CASCADE)content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE,limit_choices_to={'model__in': ('text', 'video', 'image', 'file')})object_id = models.PositiveIntegerField()item = GenericForeignKey('content_type', 'object_id')order = OrderField(for_fields=['module'], blank=True)classMeta:ordering = ['order']

模型修改好了,执行迁移命令 python manage.py makemigrations courses,可以发现提示如下:

CopyTracking file by folder pattern:  migrations
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-offdefault now (will be seton all existing rows with a null value for this column)2) Quit, andletme add a defaultin models.py
Select an option:

这个提示的意思是说不能添加值为null的新字段order到数据表中,必须提供一个默认值。如果字段有null=True属性,就不会提示此问题。我们有两个选择,选项1是输入一个默认值,作为所有已经存在的数据行该字段的值,选项2是放弃这次操作,在模型中为该字段添加default=xx属性来设置默认值。

这里我们输入1并按回车键,看到如下提示:

CopyPlease 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'toexit this prompt

系统提示我们输入值,输入0然后按回车,之后Django又会对Module模型询问同样的问题,依然选择第一项然后输入0。之后可以看到:

CopyMigrations for'courses':courses\migrations\0003_auto_20181001_1344.py- Change Meta options on content- Change Meta options onmodule- Add field orderto content- Add field ordertomodule

表示成功,之后执行python manage.py migrate。然后我们来测试一下排序,打开系统命令行窗口:

Copypython manage.py shell

创建一个新课程:

Copy>>> from django.contrib.auth.models import User
>>> from courses.models import Subject, Course, Module
>>> user = User.objects.last()
>>> subject = Subject.objects.last()
>>> c1 = Course.objects.create(subject=subject, owner=user, title='Course 1', slug='course1')

添加了一个新课程,现在我们来为新课程添加对应的章节,来看看是如何自动排序的。

Copy>>. m1 = Module.objects.create(course=c1, title='Module 1')
>>> m1.order
0

可以看到m1对象的序号字段的值被设置为0,因为这是针对课程的第一个Module对象,下边再增加一个Module对象:

Copy m2 = Module.objects.create(course=c1, title='Module 2')
>>> m2.order
1

可以看到随后增加的Module对象的序号自动被设置成了1,这次我们创建第三个对象,指定序号为5:

Copy>>> m3 = Module.objects.create(course=c1, title='Module 3', order=5)
>>> m3.order
5

如果指定了序号,则序号就会是指定的数字。为了继续试验,再增加一个对象,不给出序号参数:

Copy>>> m4 = Module.objects.create(course=c1, title='Module 4')
>>> m4.order
6

可以看到,序号会根据最后保存的数据继续增加1。OrderField字段无法保证序号一定连续,但可以保证添加的内容的序号一定是从小到大排列的。

继续试验,我们再增加第二个课程,然后第二个课程添加一个Module对象:

Copy>>> c2 = Course.objects.create(subject=subject, title='Course 2', slug='course2', owner=user)
>>> m5 = Module.objects.create(course=c2, title='Module 1')
>>> m5.order
0

可以看到序号又从0开始,该字段在生成序号的时候只会考虑同属于同一个外键字段下边的对象,第二个课程的第一个Module对象的序号又从0开始,正是由于order字段设置了for_fields=['course']所致。

祝贺你成功创建了第一个自定义字段。

4创建内容管理系统CMS

在创建好了完整的数据模型之后,需要创建内容管理系统。内容管理系统能够让讲师创建课程然后管理课程资源。

我们的内容管理系统需要如下几个功能:

  • 登录功能

  • 列出讲师的全部课程

  • 新建,编辑和删除课程

  • 为课程增加章节

  • 为章节增加不同的内容

4.1为站点增加用户验证系统

这里我们使用Django内置验证模块为项目增加用户验证功能、所有的讲师和学生都是User模型的实例,都可以通过django.contrib.auth来管理用户。

编辑educa项目的根urls.py文件,添加连接到内置验证函数login和logout的路由:

Copyfrom django.contrib import admin
from django.urls import path
from django.contrib.auth import views as auth_viewsurlpatterns = [path('accounts/login/', auth_views.LoginView.as_view(), name='login'),path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),path('admin/', admin.site.urls),
]

4.2创建用户验证模板

在courses应用下建立如下目录和文件:

Copytemplates/base.htmlregistration/login.htmllogged_out.html

在编写登录登出和其他模板之前,先来编辑base.html作为母版,在其中添加如下内容:

Copy{% load staticfiles %}
<!DOCTYPE html><html><head><metacharset="utf-8"/><title>{% block title %}Educa{% endblock %}</title><linkhref="{% static "css/base.css" %}" rel="stylesheet"></head><body><divid="header"><ahref="/"class="logo">Educa</a><ulclass="menu">{% if request.user.is_authenticated %}<li><ahref="{% url "logout" %}">Sign out</a></li>{% else %}<li><ahref="{% url "login" %}">Sign in</a></li>{% endif %}</ul></div><divid="content">{% block content %}{% endblock %}
</div><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script><script>$(document).ready(function () {{% block domready %}{% endblock %}});
</script></body></html>

译者注:为了使用方便,这里将作者原书存放jQuery文件的的Google CDN换成了国内BootCDN的地址。下边很多地方都作类似处理。

在母版中,定义了几个块:

  1. title: 用于HEAD标签的TITLE标签使用

  1. content: 页面主体内容

  1. domready:包含jQuery的$document.ready()代码,为页面DOM加载完成后执行的JS代码

这里还用到了CSS文件,在courses应用中建立static/css/目录并将随书源代码中的CSS文件复制过来。

有了母版之后,编辑registration/login.html:

Copy{% extends "base.html" %}{% block title %}Log-in{% endblock %}{% block content %}<h1>Log-in</h1><divclass="module">{% if form.errors %}<p>Your username and password didn't match. Please try again.</p>{% else %}<p>Please, use the following form to log-in:</p>{% endif %}<divclass="login-form"><formaction="{% url 'login' %}"method="post">{{ form.as_p }}{% csrf_token %}<inputtype="hidden"name="next"value="{{ next }}"/><p><inputtype="submit"value="Log-in"></p></form></div></div>
{% endblock %}

这是Django标准的用于内置login视图的模板。继续编写同目录下的logged_out.html:

Copy{% extends "base.html" %}
{% block title %}Logged out{% endblock %}
{% block content %}<h1>Logged out</h1><divclass="module"><p>You have been successfully logged out.You can <ahref="{% url "login" %}">log-in again</a>.</p></div>
{% endblock %}

这是用户登出之后展示的页面。启动站点,到http://127.0.0.1:8000/accounts/login/ 查看,页面如下:

4.3创建CBV

我们将来创建增加,编辑和删除课程的功能。这次使用基于类的视图进行编写,编辑courses应用的views.py文件:

Copyfrom django.views.generic.listimport ListView
from .models import CourseclassManageCourseListView(ListView):model = Coursetemplate_name = 'courses/manage/course/list.html'defget_queryset(self):qs = super(ManageCourseListView, self).get_queryset()return qs.filter(owner=self.request.user)

这是ManageCourseListView视图,继承自内置的ListView视图。为了避免用户操作不属于该用户的内容,重写了get_queryset()方法以取得当前用户相关的课程,在其他增删改内容的视图中,我们同样需要重写get_queryset()方法。

如果想为一些CBV提供特定的功能和行为(而不是在每个类内重写某个方法),可以使用mixins

4.4在CBV中使用mixin

对类来说,Mixin是一种特殊的多继承方式。通过Mixin可以给类附加一系列功能,自定义类的行为。有两种情况一般都会使用mixins:

  • 给类提供一系列可选的特性

  • 在很多类中实现一种特定的功能

Django为CBV提供了一系列mixins用来增强CBV的功能,具体可以看https://docs.djangoproject.com/en/2.0/topics/class-based-views/mixins/。

我们准备创建一个mixin,包含一个通用的方法,用于我们与课程相关的CBV中。修改courses应用的views.py文件,修改成下面这样:

Copyfrom django.urls import reverse_lazy
from django.views.generic.listimport ListView
from django.views.generic.edit import CreateView, UpdateView, DeleteViewfrom .models import CourseclassOwnerMixin:defget_queryset(self):qs = super(OwnerMixin, self).get_queryset()return qs.filter(owner=self.request.user)classOwnerEditMixin:defform_valid(self, form):form.instance.owner = self.request.userreturnsuper(OwnerEditMixin, self).form_valid(form)classOwnerCourseMixin(OwnerMixin):model = CourseclassOwnerCourseEditMixin(OwnerCourseMixin, OwnerEditMixin):fields = ['subject', 'title', 'slug', 'overview']success_url = reverse_lazy('manage_course_list')template_name = 'courses/manage/course/form.html'classManageCourseListView(OwnerCourseMixin, ListView):template_name = 'courses/manage/course/list.html'classCourseCreateView(OwnerCourseEditMixin, CreateView):passclassCourseUpdateView(OwnerCourseEditMixin, UpdateView):passclassCourseDeleteView(OwnerCourseMixin, DeleteView):template_name = 'courses/manage/course/delete.html'success_url = reverse_lazy('manage_course_list')

在上述代码中,创建了两个mixin类OwnerMixin和OwnerEditMixin,将这些mixins和Django内置的ListView,CreateView,UpdateView,DeleteView一起使用。

这里创建的mixin类解释如下:

OwnerMixin实现了下列方法:

  • get_queryset():这个方法是内置视图用于获取QuerySet的方法,我们的mixin重写了该方法,让该方法只返回与当前用户request.user关联的查询结果。

OwnerEditMixin实现下列方法:

  • form_valid():所有使用了Django内置的ModelFormMixin的视图,都具有该方法。这个方法具体工作机制是:如CreateView和UpdateView这种需要处理表单数据的视图,当表单验证通过时,就会执行form_valid()方法。该方法的默认行为是保存数据对象,然后重定向到一个保存成功的URL。这里重写了该方法,自动给当前的数据对象设置上owner属性对应的用户对象,这样我们就在保存过程中自动附加上用户信息。

OwnerMixin可以用于任何带有owner字段的模型。

我们还定义了继承自OwnerMixin的OwnerCourseMixin,然后指定了下列参数:

  • model:进行查询的模型,可以被所有CBV使用。

定义了OwnerCourseEditMixin,具有下列属性:

  • fields:指定CreateView和UpdateView等处理表单的视图在建立表单对象的时候使用的字段。

  • success_url:CreateView和UpdateView视图在表单提交成功后的跳转地址,这里定义了一个URL名称manage_course_list,稍后会在路由中配置该名称

最后我们创建了如下几个OwnerCourseMixin的子类

  • ManageCourseListView:展示当前用户创建的课程,继承OwnerCourseMixin和ListView

  • CourseCreateView:使用一个模型表单创建一个新的Course对象,使用OwnerCourseEditMixin定义的字段,并且继承内置的CreateView

  • CourseUpdateView:允许编辑和修改已经存在的Course对象,继承OwnerCourseEditMixin和UpdateView

  • CourseDeleteView:继承OwnerCourseMixin和内置的DeleteView,定义了成功删除对象之后跳转的success_url

译者注:使用mixin时必须了解Python 3对于类继承的MRO查找顺序,想要确保mixin中重写的方法生效,必须在继承时把mixin放在内置CBV的左侧。对于刚开始使用mixin的读者,可以使用Pycharm 专业版点击右键--Diagrams--Show Diagrams--Python Class Diagram查看当前文件的类图来了解继承关系。

4.5使用用户组和权限

我们已经创建好了所有管理课程的视图。目前任何已登录用户都可以访问这些视图。但是我们要限制课程相关的内容只能由创建者进行操作,Django的内置用户验证模块提供了权限系统,用于向用户和用户组分派权限。我们准备针对讲师建立一个用户组,然后给这个用户组内用户授予增删改课程的权限。

启动站点,进入http://127.0.0.1:8000/admin/auth/group/add/ ,然后创建一个新的Group,名字叫做Instructors,然后为其选择除了Subject模型之外,所有与courses应用相关的权限。如下图所示:

可以看到,对于每个应用中的每个模型,都有三个权限can add, can change, can delete。选好之后,点击SAVE按钮保存。

译者住:如果读者使用2.1或者更新版本的Django,权限还包括can view

Django会为项目内的模型自动设置权限,如果需要的话,也可以编写自定义权限。具体可以查看https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#custom-permissions。

打开http://127.0.0.1:8000/admin/auth/user/add/添加一个新用户,然后设置其为Instructors用户组的成员,如下图所示:

默认情况下,用户会继承其用户组设置的权限,也可以自行选择任意的其他单独权限。如果用户的is_superuser属性被设置为True,则自动具有全部权限。

4.5.1限制访问CBV

我们将限制用户对于视图的访问,使具有对应权限的用户才能进行增删改Course对象的操作。这里使用两个django.contrib.auth提供的mixins来限制对视图的访问:

  1. LoginRequiredMixin: 与@login_required装饰器功能一样

  1. PermissionRequiredMixin: 允许具有特定权限的用户访问该视图,超级用户具备所有权限。

编辑courses应用的views.py文件,新增如下导入代码:

Copyfrom django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin

让OwnerCourseMixin类继承LoginRequiredMixin类,然后添加属性:

CopyclassOwnerCourseMixin(OwnerMixin, LoginRequiredMixin):model = Coursefields = ['subject', 'title', 'slug', 'overview']success_url = reverse_lazy('manage_course_list')

然后为几个视图都配置一个permission_required属性:

CopyclassCourseCreateView(PermissionRequiredMixin, OwnerCourseEditMixin, CreateView):permission_required = 'courses.add_course'classCourseUpdateView(PermissionRequiredMixin, OwnerCourseEditMixin, UpdateView):permission_required = 'courses.change_course'classCourseDeleteView(PermissionRequiredMixin, OwnerCourseMixin, DeleteView):template_name = 'courses/manage/course/delete.html'success_url = reverse_lazy('manage_course_list')permission_required = 'courses.delete_course'

PermissionRequiredMixin会检查用户是否具备在permission_required参数里指定的权限。现在视图就只能供指定权限的用户使用了。

视图编写完毕之后,为视图配置路由,先在courses应用中新建urls.py文件,添加下列代码:

Copyfrom django.urls import path
from . import viewsurlpatterns = [path('mine/', views.ManageCourseListView.as_view(), name='manage_course_list'),path('create/', views.CourseCreateView.as_view(), name='course_create'),path('<pk>/edit/', views.CourseUpdateView.as_view(), name='course_edit'),path('<pk>/delete/', views.CourseDeleteView.as_view(), name='course_delete'),
]

再来配置项目的根路由,将courses应用的路由作为二级路由:

Copyfrom django.urls import path, include
from django.contrib.auth import views as auth_viewsurlpatterns = [path('accounts/login/', auth_views.LoginView.as_view(), name='login'),path('accounts/logout/', auth_views.LogoutView.as_view(), name='logout'),path('admin/', admin.site.urls),path('course/', include('courses.urls')),
]

然后需要为视图创建模板,在courses应用的templates/目录下新建如下目录和文件:

Copycourses/manage/course/list.htmlform.htmldelete.html

编辑其中的courses/manage/course/list.html,添加下列代码:

Copy{% extends "base.html" %}
{% block title %}My courses{% endblock %}
{% block content %}<h1>My courses</h1><divclass="module">{% for course in object_list %}<divclass="course-info"><h3>{{ course.title }}</h3><p><ahref="{% url "course_edit" course.id %}">Edit</a><ahref="{% url "course_delete" course.id %}">Delete</a></p></div>{% empty %}<p>You haven't created any courses yet.</p>{% endfor %}<p><ahref="{% url "course_create" %}" class="button">Create newcourse</a></p></div>
{% endblock %}

这是供ManageCourseListView使用的视图。在这个视图里列出了所有的课程,然后生成对应的编辑和删除功能链接。

启动站点,到http://127.0.0.1:8000/accounts/login/?next=/course/mine/,用一个在Instructors用户组内的用户登录,可以看到如下界面:

这个页面会显示当前用户创建的所有课程。

现在来创建新增和修改课程需要的模板,编辑courses/manage/course/form.html,添加下列代码:

Copy{% extends "base.html" %}
{% block title %}{% if object %}Edit course "{{ object.title }}"{% else %}Create a new course{% endif %}
{% endblock %}
{% block content %}<h1>{% if object %}Edit course "{{ object.title }}"{% else %}Create a new course{% endif %}</h1><divclass="module"><h2>Course info</h2><formaction="."method="post">{{ form.as_p }}{% csrf_token %}<p><inputtype="submit"value="Save course"></p></form></div>
{% endblock %}

这个模板由CourseCreateView和CourseUpdateView进行操作。在模板内先检查object变量是否存在,如果存在则显示针对该对象的修改功能。如果不存在就建立一个新的Course对象。

浏览器中打开http://127.0.0.1:8000/course/mine/,点击CREATE NEW COURSE按钮,可以看到如下界面:

填写表单后后点击SAVE COURSE进行保存,课程会被保存,然后重定向到课程列表页,可以看到如下界面:

点击其中的Edit链接,可以在看到这个表单页面,但这次是修改已经存在的Course对象。

最后来编写courses/manage/course/delete.html,添加下列代码:

Copy{% extends "base.html" %}
{% block title %}Delete course{% endblock %}
{% block content %}<h1>Delete course "{{ object.title }}"</h1><divclass="module"><formaction=""method="post">{% csrf_token %}<p>Are you sure you want to delete "{{ object }}"?</p><inputtype="submit"class="button"value="Confirm"></form></div>
{% endblock %}

注意原书的代码在<input>元素的的class属性后边漏了一个"="号

这个模板由继承了DeleteView的CourseDeleteView视图操作,负责删除课程。

打开浏览器,点击刚才页面中的Delete链接,跳转到如下确认页面:

点击CONFIRM按钮,课程就会被删除,然后重定向至课程列表页。

讲师组用户现在可以增删改课程了。下边要做的是通过CMS让讲师组用户为课程添加章节和内容。

5管理章节与内容

这一节里来建立一个管理课程中章节和内容的系统,将为同时管理课程中的多个章节及其中不同的内容建立表单。章节和内容都需要按照特定的顺序记录在我们的CMS中。

5.1在课程模型中使用表单集(formsets)

Django通过一个抽象层控制页面中的所有表单对象。一组表单对象被称为表单集。表单集由多个Form类或者ModelForm类的实例组成。表单集内的所有表单在提交的时候会一并提交,表单集可以控制显示的表单数量,对提交的最大表单数量做限制,同时对其中的全部表单进行验证。

表单集包含一个is_valid()方法用于一次验证所有表单。可以给表单集初始数据,也可以控制表单集显示的空白表单数量。普通的表单集官方文档可以看https://docs.djangoproject.com/en/2.0/topics/forms/formsets/,由模型表单构成的model formset可以看https://docs.djangoproject.com/en/2.0/topics/forms/modelforms/#model-formsets。

由于一个课程由多个章节组成,方便运用表单集进行管理。在courses应用中建立forms.py文件,添加如下代码:

Copyfrom django import forms
from django.forms.models import inlineformset_factory
from .models import Course, ModuleModuleFormSet = inlineformset_factory(Course, Module, fields=['title', 'description'], extra=2, can_delete=True)

我们使用内置的inlineformset_factory()方法构建了表单集ModuleFormSet。内联表单工厂函数是在普通的表单集之上的一个抽象。这个函数允许我们动态的通过与Course模型关联的Module模型创建表单集。

对这个表单集我们应用了如下字段:

  • fields:表示表单集中每个表单的字段

  • extra:设置每次显示表单集时候的表单数量

  • can_delete:该项如果设置True,Django会在每个表单内包含一个布尔字段(被渲染成为一个CHECKBOX类型的INPUT元素),供用户选中需要删除的表单

编辑courses应用的views.py文件,增加下列代码:

Copyfrom django.shortcuts import redirect, get_object_or_404
from django.views.generic.base import TemplateResponseMixin, View
from .forms import ModuleFormSetclassCourseModuleUpdateView(TemplateResponseMixin, View):template_name = 'courses/manage/module/formset.html'course = Nonedefget_formset(self, data=None):return ModuleFormSet(instance=self.course, data=data)defdispatch(self, request, pk):self.course = get_object_or_404(Course, id=pk, owner=request.user)returnsuper(CourseModuleUpdateView, self).dispatch(request, pk)defget(self, request, *args, kwargs):formset = self.get_formset()return self.render_to_response({'course': self.course, 'formset': formset})defpost(self, request, *args, kwargs):formset = self.get_formset(data=request.POST)if formset.is_valid():formset.save()return redirect('manage_course_list')return self.render_to_response({'course': self.course, 'formset': formset})

CourseModuleUpdateView用于对一个课程的章节进行增删改。这个视图继承了以下的mixins和视图:

  • TemplateResponseMixin:这个mixin提供的功能是渲染模块并且返回HTTP响应,需要一个template_name属性用于指定模板位置,提供了一个render_to_response()方法给模板传入上下文并且渲染模板

  • View:基础的CBV视图,由Django内置提供。简单继承该类就可以得到一个基本的CBV。

在这个视图中,实现了如下的方法:

  1. get_formset():这个方法是创建formset对象的过程,为了避免重复编写所以写了一个方法。功能是根据获得的Course对象和可选的data参数来构建一个ModuleFormSet对象。

  1. dispatch():这个方法是View视图的方法,是一个分发器,HTTP请求进来之后,最先执行的是dispatch()方法。该方法把小写的HTTP请求的种类分发给同名方法:例如GET请求会被发送到get()方法进行处理,POST请求会被发送到post()方法进行处理。在这个方法里。使用get_object_or_404()加一个id参数,从Course类中获取对象。把这段代码包含在dispatch()方法中是因为无论GET还是POST请求,都会使用Course对象。在请求一进来的时候,就把Course对象存入self.course,供其他方法使用。

  1. get():处理GET请求。创建一个ModuleFormSet然后使用当前的Course对象渲染模板,使用了TemplateResponseMixin提供的render_to_response()方法

  1. post():处理POST请求,在这个方法中执行了如下动作:

  1. 使用请求附带的数据建立ModuleFormSet对象

  1. 执行is_valid()方法验证所有表单

  1. 验证通过则使用save()方法保存,这时增删改都会写入数据库。然后重定向到manage_course_list URL。如果未通过验证,就返回当前表单对象以显示错误信息。

编辑courses应用中的urls.py文件,为刚写的视图配置URL:

Copypath('<pk>/module/', views.CourseModuleUpdateView.as_view(), name='course_module_update'),

在模板目录courses/templates/下创建一个新目录,叫做module,然后创建templates/courses/manage/module/formset.html文件,添加下列代码:

Copy{% extends "base.html" %}
{% block title %}Edit "{{ course.title }}"
{% endblock %}
{% block content %}<h1>Edit "{{ course.title }}"</h1><divclass="module"><h2>Course modules</h2><formaction=""method="post">{{ formset }}{{ formset.management_form }}{% csrf_token %}<inputtype="submit"class="button"value="Save modules"></form></div>
{% endblock %}

在这个模板中,创建了一个表单元素<form>,其中包含了formset表单集,还包含了一个管理表单{{ formset.management_form }}。这个管理表单包含隐藏的字段用于控制显示起始,总计,最小和最大编号的表单。可以看到创建表单集很简单。

编辑courses/templates/course/list.html,把course_module_update的链接加在编辑和删除链接之下:

Copy<ahref="{% url "course_edit" course.id %}">Edit</a><ahref="{% url "course_delete" course.id %}">Delete</a><ahref="{% url "course_module_update" course.id %}">Edit modules</a>

现在模板中有了编辑课程中章节的链接,启动站点,到http://127.0.0.1:8000/course/mine/创建一个课程然后点击Edit modules链接,可以看到页面中的表单集如下:

这个表单集合包含了该课程中的每个Module对象,然后还多出来2个空白的表单可供填写,这是因为我们为ModuleFormSet设置了extra=2。输入两个新的章节内容,然后保存表单,再进编辑页面,可以看到又多出来了两个空白表单。

5.2向课程中添加内容

现在要为章节添加具体的内容。在之前我们定义了四种内容对应四个模型:文字,图片,文件和视频。可能会考虑建立四个不同的视图操作这四个不同的类,但这里我们采用更加通用的方式:建立一个视图来对这四个类进行增删改。

编辑courses应用中的views.py文件,添加如下代码:

Copyfrom django.forms.models import modelform_factory
from django.apps import apps
from .models import Module, ContentclassContentCreateUpdateView(TemplateResponseMixin, View):module = Nonemodel = Noneobj = Nonetemplate_name = 'courses/manage/content/form.html'defget_model(self, model_name):if model_name in ['text', 'video', 'image', 'file']:return apps.get_model(app_label='courses', model_name=model_name)returnNonedefget_form(self, model, *args, kwargs):Form = modelform_factory(model, exclude=['owner', 'order', 'created', 'updated'])return Form(*args, kwargs)defdispatch(self, request, module_id, model_name, id=None):self.module = get_object_or_404(Module, id=module_id, course__owner=request.user)self.model = self.get_model(model_name)ifid:self.obj = get_object_or_404(self.model, id=id, owner=request.user)returnsuper(ContentCreateUpdateView, self).dispatch(request, module_id, model_name, id)

这是ContentCreateUpdateView视图的第一部分。这个类用于建立和更新章节中的内容,这个类定义了如下方法:

  1. get_model():检查给出的名字是否在指定的四个类名中,然后用Django的apps模块,从courses应用中取出对应的模块,如果没有找到,就返回None

  1. get_form():使用内置的modelform_factory()方法建立表单集,去掉了四个指定的字段,使用剩下的字段建立。这么做,我们可以不考虑具体是哪个模型,只去掉通用的字段保留剩下的字段。

  1. dispatch():这个方法接收下列的URL参数,然后为当前对象设置module和model属性:

  • module_id:章节的id

  • model_name:内容模型的名称

  • id:要更新的内容的id,默认值为None表示新建。

然后来编写该视图的get()和post()方法:

Copydefget(self, request, module_id, model_name, id=None):form = self.get_form(self.model, instance=self.obj)return self.render_to_response({'form': form, 'object': self.obj})defpost(self, request, module_id, model_name, id=None):form = self.get_form(self.model, instance=self.obj, data=request.POST, files=request.FILES)if form.is_valid():obj = form.save(commit=False)obj.owner = request.userobj.save()ifnotid:# 新内容Content.objects.create(module=self.module, item=obj)return redirect('module_content_list', self.module.id)return self.render_to_response({'form': form, 'object': self.obj})

这两个方法解释如下:

  • get():处理GET请求。通过get_form()方法获取需要修改的四种内容之一生成的表单。如果没有id,前置的dispatch方法里不设置self.obj,所以instance=None,表示新建

  • post():处理POST请求。通过传入的所有数据创建表单集对象,然后进行验证。如果验证通过,给当前对象设置上user属性,然后保存。如果没有传入id,说明是新建内容,需要在Content中追加一条记录关联到module对象和新建的内容对象。

编辑courses应用的urls.py文件,为新视图配置URL:

Copy    path('module/<int:module_id>/content/<model_name>/create/', views.ContentCreateUpdateView.as_view(),name='module_content_create'),path('module/<int:module_id>/content/<model_name>/<id>/', views.ContentCreateUpdateView.as_view(),name='module_content_update'),

这两条路由解释如下:

  • module_content_create:用于建立新内容的URL,带有module_id和model_name两个参数,第一个是用来取得对应的module对象,第二个用来取得对应的内容数据模型。

  • module_content_update:用于修改原有内容的URL,除了带有module_id和model_name两个参数之外,还带有id用于确定具体修改哪一个内容对象。

在courses/manage/目录下创建一个新目录叫content,再创建courses/manage/content/form.html,添加下列代码:

Copy{% extends "base.html" %}
{% block title %}{% if object %}Edit content "{{ object.title }}"{% else %}Add a new content{% endif %}
{% endblock %}
{% block content %}<h1>{% if object %}Edit content "{{ object.title }}"{% else %}Add a new content{% endif %}</h1><divclass="module"><h2>Course info</h2><formaction=""method="post"enctype="multipart/form-data">{{ form.as_p }}{% csrf_token %}<p><inputtype="submit"value="Save content"></p></form></div>
{% endblock %}

这是视图ContentCreateUpdateView控制的模板。在这个模板里,使用了一个object变量,如果object变量不为None,说明在修改一个已经存在的内容,否则就是新建一个内容。

<form>标签中设置了属性enctype="multipart/form-data",因为File和Image模型中有文件字段。

启动站点,到http://127.0.0.1:8000/course/mine/,点击任何一个已经存在的课程的Edit modules链接,之后新建一个module。

然后打开带有当前Django环境的Python命令行,来进行一些测试,首先取到最后一个建立的module对象:

Copy>>> from courses.models import Module
>>> Module.objects.latest('id').id6

取到了这个id之后,打开http://127.0.0.1:8000/course/module/6/content/image/create/ ,把6替换成你实际取到的结果,可以看到创建Image对象的页面:

现在还不要提交表单,如果提交会报错,因为我们还没有定义module_content_list URL。

现在还需要一个视图用来删除内容。编辑courses应用的views.py文件:

CopyclassContentDeleteView(View):defpost(self, request, id):content = get_object_or_404(Content, id=id, module__course__owner=request.user)module = content.modulecontent.item.delete()content.delete()return redirect('module_content_list', module.id)

这个ContentDeleteView视图通过ID参数获取Content对象,然后删除相关的Text、Video、Image、或File对象,再把Content对象删除,之后重定向到module_content_list URL。

在就在courses应用的urls.py文件中设置该URL:

Copypath('content/<int:id>/delete/', views.ContentDeleteView.as_view(), name='module_content_delete'),

现在讲师用户就可以增删改内容了。

5.3管理章节与内容

在上一节里编写好了增删改的视图,现在需要一个视图将一个课程的全部章节和其中的内容展示出来的视图。

编辑courses应用的views.py文件,添加下列代码:

CopyclassModuleContentListView(TemplateResponseMixin, View):template_name = 'courses/manage/module/content_list.html'defget(self, request, module_id):module = get_object_or_404(Module,id=module_id,course__owner=request.user)return self.render_to_response({'module': module})

这个ModuleContentListView视图通过一个指定的Module对象的ID和当前用户,来获取Module对象,然后使用该对象渲染模板。

在courses应用的urls.py内加入该视图的路由:

Copypath('module/<int:module_id>/', views.ModuleContentListView.as_view(), name='module_content_list'),

在templates/courses/manage/module/目录中新建content_list.html,添加下列代码:

Copy{% extends "base.html" %}
{% block title %}Module {{ module.order|add:1 }}: {{ module.title }}
{% endblock %}
{% block content %}{% with course=module.course %}<h1>Course "{{ course.title }}"</h1><divclass="contents"><h3>Modules</h3><ulid="modules">{% for m in course.modules.all %}<lidata-id="{{ m.id }}" {% ifm == module %}class="selected"{% endif %}><ahref="{% url "module_content_list" m.id %}"><span>Module <spanclass="order">{{ m.order|add:1 }}</span></span><br>{{ m.title }}</a></li>{% empty %}<li>No modules yet.</li>{% endfor %}</ul><p><ahref="{% url "course_module_update" course.id %}">Edit modules</a></p></div><divclass="module"><h2>Module {{ module.order|add:1 }}: {{ module.title }}</h2><h3>Module contents:</h3><divid="module-contents">{% for content in module.contents.all %}<divdata-id="{{ content.id }}">{% with item=content.item %}<p>{{ item }}</p><ahref="#">Edit</a><formaction="{% url "module_content_delete" content.id %}"method="post"><inputtype="submit"value="Delete">{% csrf_token %}</form>{% endwith %}</div>{% empty %}<p>This module has no contents yet.</p>{% endfor %}</div><h3>Add new content:</h3><ulclass="content-types"><li><ahref="{% url "module_content_create" module.id "text" %}">Text</a></li><li><ahref="{% url "module_content_create" module.id "image" %}">Image</a></li><li><ahref="{% url "module_content_create" module.id "video" %}">Video</a></li><li><ahref="{% url "module_content_create" module.id "file" %}">File</a></li></ul></div>{% endwith %}
{% endblock %}

这是用来展示该课程中全部章节和内容的模板。迭代全部的章节显示在侧边栏中,然后针对每个章节的内容,通过content.item迭代其中的相关的所有内容进行展示,然后配上对应的链接。

我们想知道每个item对象究竟是text, video, image或者file的哪一种,因为我们需要模型的名称来创建修改数据的URL。此外还需要在模板中按照类别单独把每个内容展示出来。对于一个数据对象,可以通过_meta_属性获取该数据所属的模型类,但Django不允许在视图中使用以下划线开头的模板变量或者属性,以防访问到私有属性或方法。可以通过编写一个自定义的模板过滤器来解决。

在courses应用中建立如下目录和文件:

Copytemplatetags/__init__.pycourse.py

在其中的course.py中编写:

Copyfrom django import templateregister = template.Library()@register.filterdefmodel_name(obj):try:return obj._meta.model_nameexcept AttributeError:returnNone

这是model_name模板过滤器,在模板里可以通过object|model_name来获得一个数据对象所属的模型名称。

编辑刚才的templates/courses/manage/module/content_list.html,在{% extend %}的下一行添加:

Copy{% load course %}

然后找到下边两行:

Copy<p>{{ item }}</p><ahref="#">Edit</a>

替换成:

Copy<p>{{ item }} ({{ item|model_name }})</p><ahref="{% url "module_content_update" module.iditem|model_nameitem.id %}">Edit</a>

使用了自定义模板过滤器之后,我们在模板中显示内容对象时,就可以通过对象所属模型的名称来生成URL链接了。编辑courses/manage/course/list.html,添加一个列表页的链接:

Copy<ahref="{% url "course_module_update" course.id %}">Edit modules</a>
{% if course.modules.count > 0 %}<ahref="{% url "module_content_list" course.modules.first.id %}">Manage contents</a>
{% endif %}

这个新连接跳转到显示第一个章节的内容的页面。

打开http://127.0.0.1:8000/course/mine/,可以看到页面中多出来了Manage contents链接,点击该链接后如下图所示:

在左侧边栏点击一个章节时,该章节的内容就显示在右侧。这个页面还带了链接到添加四种类型内容的页面。实际添加一些内容然后看一下页面效果,内容也会展示出来:

5.4重新排列章节和内容的顺序

我们需要给用户提供一个简单的可以重新排序的方法。通过JavaScrip的拖动插件,让用户通过拖动就可以重新排列章节和内容的顺序。在用户结束拖动的时候,我们使用AJAX来记录当前的新顺序。

5.4.1使用django-braces模块中的mixins

django-braces是一个第三方模块,包含了一系列通用的Mixin,为CBV提供额外的功能。可以查看其官方文档:https://django-braces.readthedocs.io/en/latest/来获得完整的mixin列表。

我们要使用django-braces中下列mixin:

  • CsrfExemptMixin:在POST请求中不检查CSRF,无需生成csrf_token

  • JsonRequestResponseMixin:以JSON字符串形式解析请求中的数据,并且序列化响应数据为JSON格式,带有application/json头部信息

通过pip安装django-braces:

Copypip install django-braces==1.13.0

我们需要一个视图,能够接受JSON格式的新的模块顺序。编辑courses应用的views.py文件,添加下列代码:

Copyfrom braces.views import CsrfExemptMixin, JsonRequestResponseMixinclassModuleOrderView(CsrfExemptMixin, JsonRequestResponseMixin, View):defpost(self, request):forid, order in self.request_json.items():Module.objects.filter(id=id, course__owner=request.user).update(order=order)return self.render_json_response({'saved': 'OK'})

这个ModuleOrderView视图的逻辑是拿到JSON数据后,对于其中的每一条记录,更新module对象的order字段。

基于类似的逻辑,来编写章节内容的重新排列视图,继续在views.py中追加代码:

CopyclassContentOrderView(CsrfExemptMixin, JsonRequestResponseMixin, View):defpost(self, request):forid, order in self.request_json.items():Content.objects.filter(id=id, module__course__owner=request.user).update(order=order)return self.render_json_response({'saved': 'OK'})

然后编辑courses应用的urls.py,为这两个视图配置URL:

Copy    path('module/order/', views.ModuleOrderView.as_view(), name='module_order'),path('content/order/', views.ContentOrderView.as_view(), name='content_order'),

最后,需要在模板中实现拖动功能。使用jQuery UI库来完成这个功能。jQuery UI基于jQuery,提个了一系列的界面互动操作,效果和插件。我们使用其中的sortable元素。首先,需要把jQuery加载到母版中。打开base.html,在加载jQuery的script标签之后加入jQuery UI。

Copy<scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>

这里使用了国内的CDN。由于jQueryUI依赖于jQuery,所以要在其后载入。之后编辑courses/manage/module/content_list.html,在底部添加如下代码:

Copy{% block domready %}
$('#modules').sortable({stop: function (event, ui) {let modules_order = {};$('#modules').children().each(function () {$(this).find('.order').text($(this).index() + 1);modules_order[$(this).data('id')] = $(this).index();});$.ajax({type: 'POST',url: '{% url "module_order" %}',contentType: 'application/json; charset=utf-8',dataType: 'json',data: JSON.stringify(modules_order)});}
});$('#module-contents').sortable({stop: function (event, ui) {let contents_order = {};$('#module-contents').children().each(function () {contents_order[$(this).data('id')] = $(this).index();});$.ajax({type: 'POST',url: '{% url "content_order" %}',contentType: 'application/json; charset=utf-8',dataType: 'json',data: JSON.stringify(contents_order),});}
});
{% endblock %}

译者注:这里对原书的代码增加了let声明。

这段代码加载在{% domready %}块中,会在页面DOM加载完成后立刻执行。在代码中为所有的侧边栏中的章节列表定义了一个sortable方法,为内容也定义了一个同样功能的方法。这段代码做了下列工作:

  1. 使用#modules选择器,为modules的HTML元素定义了sortable元素

  1. 定义了一个stop事件处理函数,用户停止拖动后触发该事件

  1. 建立了一个空字典modules_order(JS里叫做对象),其中的键是module的ID(LI元素的data-id属性的值),值是重新排列后的顺序。

  1. 遍历拖动后的#module的子元素,取得此时每个元素的data-id和此时在列表中的索引,用此时的id作为键,其顺序作为值,更新modules_order字典。

  1. 通过AJAX发送POST请求到content_order URL进行处理,请求中带有modules_order JSON字符串,交给ModuleOrderView进行处理。

用于排序内容部分的sortable元素与上述这个相似。启动站点,重新加载编辑内容的页面,现在可以通过拖动重新排列章节和内容的顺序,如下图所示:

现在我们就实现了拖动排序功能。

总结

这一章学习了如何建立一个CMS。使用了模型继承和创建自定义字段,同时使用了基于类的视图和mixins。还使用了表单集和实现了一个管理不同的内容的系统。

如有不懂还要咨询下方小卡片,博主也希望和志同道合的测试人员一起学习进步

在适当的年龄,选择适当的岗位,尽量去发挥好自己的优势。

我的自动化测试开发之路,一路走来都离不每个阶段的计划,因为自己喜欢规划和总结,

测试开发视频教程、学习笔记领取传送门!!!

相关文章:

测试开发之Django实战示例 第十章 创建在线教育平台

第十章 创建在线教育平台在上一章&#xff0c;我们为电商网站项目添加了国际化功能&#xff0c;还创建了优惠码和商品推荐系统。在本章&#xff0c;会建立一个新的项目&#xff1a;一个在线教育平台&#xff0c;并创内容管理系统CMS&#xff08;Content Management System&…...

Hadoop高可用搭建(二)

目录 解压Hadoop 改名 更改配置文件 workers hdfs-site.xml core-site.xml hadoop-env.sh mapred-site.xml yarn-site.xml 设置环境变量 启动集群 启动zk集群 启动journalnode服务 格式化hfds namenode 启动namenode 同步namenode信息 查看namenode节点状态 …...

如何用企微SCRM管理系统发掘老客户的新增长点?

如何用企微SCRM管理系统发掘老客户的新增长点&#xff1f; 一直做投放拉新&#xff0c;很快营销成本会难以支撑&#xff0c;如果在私域运营中始终留不下老用户&#xff0c;那么运营也是失败的。 开发老客户的成本只需新客户成本的1/6&#xff0c;但很多企业对老客户都忽视了&…...

我用python疯狂爬取公司数据

我是半路从一个纯小白学过来的&#xff0c;学习途中也掉过许多坑&#xff0c;在这里建议新手要先把基础打扎实&#xff0c;然后再去学习自己需要的内容&#xff0c;不要想着全部学完再用&#xff0c;那样你是永远学不完的&#xff0c;用哪方面就学习哪方面的内容&#xff0c;不…...

EMR集群运行TPC-DS在云盘和OSS中的对比

1.简介 TPC-DS是大数据领域最为知名的Benchmark标准。本文介绍使用阿里云EMR集群运行TPC-DS在云盘和OSS中的表现对比。 2.环境准备 1.创建EEMR-5.10.1集群 1个master,2个core,3台机器都s是4c16g。 2.安装Git和Maven sudo yum install -y git maven3.下载TPC-DS Benchmark工…...

菜鸟在 windows 下 python 中安装 jupyter 踩坑要点 、被神化的 VsCode

我平时用不到 python &#xff0c;更没用过 jupyter &#xff0c;因此我的 python知识仅限于知道有 python 这么个编程语言&#xff0c;会写个 print("Hello World!!!") 而已&#xff0c;完全没听过 jupyter &#xff0c;因为某些原因今天需要安装下 jupyter 看看&am…...

k8s简单搭建

前言 最近学习k8s&#xff0c;跟着网上各种教程搭建了简单的版本&#xff0c;一个master节点&#xff0c;两个node节点&#xff0c;这里记录下防止以后忘记。 具体步骤 准备环境 用Oracle VM VirtualBox虚拟机软件安装3台虚拟机&#xff0c;一台master节点&#xff0c;两台…...

计算机SCI期刊审稿人,一般关注论文的那些问题? - 易智编译EaseEditing

编辑主要关心&#xff1a; &#xff08;1&#xff09;文章内容是否具有足够的创新性&#xff1f; &#xff08;2&#xff09;文章主题是否符合期刊的受众读者&#xff1f; &#xff08;3&#xff09;文章方法学是否合理&#xff0c;数据处理是否充分&#xff1f; &#xff08;…...

Docker迁移以及环境变量问题

问题一描述将docker容器通过docker export命令打包&#xff0c;传输到另外的服务器&#xff0c;再通过docker import命令导入后&#xff0c;发现原来docker容器中的环境变量失效了。解决方案1. 【无效方案】直接在docker容器中通过export命令设置环境变量。export LD_LIBRARY_P…...

Sphinx文档生成工具(二)

rst语法 官方的语法手册 行内的样式&#xff1a; #斜体 *message* #粗体 **message** #等宽 不能有换行 message标题 一级标题 ^^^^^^^^ 二级标题 --------- 三级标题 >>>>>>>>> 四级标题 ::::::::: 五级标题六级标题 """"…...

Python快速上手系列--JSON--入门篇

本章我们来看看json的一些应用。简单易懂还实用。一起来看看数据类型以及一些语法规则吧1、数字&#xff08;整数或浮点数&#xff09; 如&#xff1a;{"age":18, "score":70.5} 注意&#xff0c;数字直接写&#xff0c;不需要带任何符号2、字符串&#xf…...

axios中的GET POST PUT PATCH,发送请求时params和data的区别

axios 中 get/post请求方式 1. 前言 最近突然发现post请求可以使用params方式传值&#xff0c;然后想总结一下其中的用法。 2.1 分类 经过查阅资料&#xff0c;get请求是可以通过body传输数据的&#xff0c;但是许多工具类并不支持此功能。 在postman中&#xff0c;选择get请…...

hume项目k8s的改造

hume项目k8s的改造 一、修改构建目录结构 1、在根目录下添加build-work文件夹 目录结构如下 [rootk8s-worker-01 build-work]# tree . . ├── Dockerfile ├── hume │ └── start.sh └── Jenkinsfile2、每个文件内容如下 Dockerfile FROM ccr.ccs.tencentyun…...

MACD红二波选股公式,选出MACD二次翻红的标的

经过一段上涨行情之后&#xff0c;市场出现了时间稍长或者幅度稍大的调整&#xff0c;MACD指标的DIF、DEA会出现死叉&#xff0c;柱线由红色转变为绿色。 而调整时间较短或者幅度较小&#xff0c;MACD红柱会缩短&#xff0c;但不出现绿柱&#xff0c;之后红柱开始变长&#xff…...

mac上安装mysql

mac上安装mysql1. 关于Linux上安装mysql2. 下载安装2.1 下载2.2 安装3. 客户端连接mysql3.1 先查看mysql服务3.2 连接mysql客户端3.2.1 终端使用命令连接3.2.2 可视化工具连接3.3 其他简单操作&#xff08;启动服务等&#xff09;3.3.1 可视化界面操作4. 配置环境变量4.1 配置环…...

Django 模型继承问题

文章目录Django 模型继承问题继承出现的情况Meta 和多表继承Meta 和多表继承继承与反向关系指定父类连接字段代理模型QuerySet 仍会返回请求的模型基类约束代理模型管理器代理继承和未托管的模型间的区别多重继承不能用字段名 "hiding"在一个包中管理模型Django 模型…...

Vue3篇.01-简介及基本使用,项目创建方式, 模板语法, 事件监听, 修饰符

一.简介1.概念Vue 是一款用于构建用户界面的 JS框架&#xff0c; 基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c; 高效地开发用户界面。渐进式框架&#xff0c; 适应不同需求进行开发。两个核心功能&#xff1a;声明式…...

别学英语了,真的

文 / 王不留&#xff08;微信公众号&#xff1a;王不留&#xff09; 这两年&#xff0c;很多朋友加我微信后&#xff0c;第一句常是&#xff0c;学英语有什么用啊&#xff1f; 我会统一给出真诚答复&#xff1a;没用&#xff0c;真的。 看新闻&#xff0c;中文海量信息已经严重…...

CRM系统五大技巧集成Excel为销售流程赋能

销售过程中有很多情况会降低团队的效率。通过正确的实施CRM客户管理系统&#xff0c;可以帮助您的企业自动执行手动任务、减少错误并专注于完成交易。这里有5个技巧&#xff0c;可以帮助您的销售人员通过CRM集成Excel为销售流程赋能并提高他们的整体效率。 技巧1&#xff1a;将…...

交通部互通互联码的根证书规则

引言 为了更好的服务交通互通互联码而更新这篇文章。 中金根证书其实是可以自己生成的。 代码内调整 中心公钥索引要保证自己的唯一性。 此处的唯一&#xff0c;是要保证在机具侧的唯一&#xff0c;因为他要根据这个索引去查找证书以及公钥。 提供根公钥给机具侧 生成的公钥…...

Map和Set(Java详解)

在开始详解之前&#xff0c;先来看看集合的框架&#xff1a; 可以看到Set实现了Collection接口&#xff0c;而Map又是一个单独存在的接口。 而最下面又分别各有两个类&#xff0c;分别是TreeSet&#xff08;Map&#xff09;和 HashSet&#xff08;Map&#xff09;。 TreeSet&…...

Vue 3的响应式机制

什么是响应式 Js代码是自上而下执行的&#xff0c;结合下面代码看&#xff0c;代码执行后&#xff0c;会打印两次double的结果&#xff0c;结果也都是2&#xff0c;即使修改了代码中count的值后&#xff0c;double的值也不会发生任何改变。 let count 1 let double count * …...

30岁了,说几句大实话

是的&#xff0c;我 30 岁了&#xff0c;还是周岁。 就在这上个月末&#xff0c;我度过了自己 30 岁的生日。 都说三十而立&#xff0c;要对自己有一个正确的认识&#xff0c;明确自己以后想做什么&#xff0c;能做什么。 想想时间&#xff0c;过得真快。 过五关斩六将&…...

AsyncTask使用及源码查看Android P

AsyncTask AsyncTask用于处理耗时任务&#xff0c;可以即时通知进度&#xff0c;最终返回结果。可以用于下载等处理。 使用 实现类继承三个方法 1. doInBackground后台执行&#xff0c;在此方法中进行延时操作 /*** Override this method to perform a computation on a back…...

花2个月面过华为测开岗,拿个30K不过分吧?

背景介绍 美本计算机专业&#xff0c;代码能力一般&#xff0c;之前有过两段实习以及一个学校项目经历。第一份实习是大二暑期在深圳的一家互联网公司做前端开发&#xff0c;第二份实习由于大三暑假回国的时间比较短&#xff08;小于两个月&#xff09;&#xff0c;于是找的实…...

JAVA练习51-最大子数组和

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 一、题目-最大子数组和 1.题目描述 2.思路与代码 2.1 思路 2.2 代码 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 2月15日练…...

Inception Transformer

paper链接: https://arxiv.org/abs/2205.12956v2 code链接: https://github.com/sail-sg/iFormer Inception Transformer一、引言二、实现细节三、实验一、分类二、检测三、分割四、消融实验一、引言 最近的研究表明&#xff0c;Transformer具有很强的建立远程依赖关系的能力…...

10分钟学会数据库压力测试,你敢信?

目录 前言 查看数据库版本 下载驱动&#xff1a; 菜单路径 配置 Variable Name Bound to Pool模块配置 Connection pool configuration模块配置 Database Connection Configuration模块配置 菜单路径 Variable Name Bound to Pool 脚本结构 脚本&#xff08;执行查询…...

论文阅读 | Video Super-Resolution Transformer

引言&#xff1a;2021年用Transformer实现视频超分VSR的文章&#xff0c;改进了SA并在FFN中加入了光流引导 论文&#xff1a;【here】 代码&#xff1a;【here】 Video Super-Resolution Transformer 引言 视频超分中有一组待超分的图片&#xff0c;因此视频超分也经常被看做…...

7-6 带头节点的双向循环链表操作

本题目要求读入一系列整数&#xff0c;依次插入到双向循环链表的头部和尾部&#xff0c;然后顺序和逆序输出链表。 链表节点类型可以定义为 typedef int DataType; typedef struct LinkedNode{DataType data;struct LinkedNode *prev;struct LinkedNode *next; }LinkedNode;链…...

建站平台取名字/杭州网站优化公司哪家好

在使用Canvas的drawImage绘制图片时&#xff0c;却发现绘制不出图片&#xff0c;原因是图片是异步加载&#xff0c;图片加载完再绘制。 //html <img src"1.png" /> <canvas id"draw"></canvas>//js var Image document.images[0];var d…...

如何自做网站/新闻头条最新消息

TC130&#xff1a;看懂蒙特卡洛积分(一) 概率分布变换与随机采样​zhuanlan.zhihu.comTC130&#xff1a;游戏渲染进阶​zhuanlan.zhihu.com概率论背景(1) 数学期望设 是随机变量, 是 的函数, .A. 如果 是离散型随机变量, 其分布列为 .随机变量 的数学期望定义为 .的数学期望定义…...

榆垡网站建设/搜一搜百度

这个教程主要是分享如何快速组建WAMP开发环境&#xff0c;对于软件的详细配置&#xff0c;自行参考文档或搜索。 Visual C Redistributable for Visual Studio 2015 下载地址&#xff1a;https://www.microsoft.com/zh-CN/download/details.aspx?id48145 (个人测试window10下不…...

安陆网站建设/网站建设品牌公司

【SymPy】&#xff08;一&#xff09;SymPy简介 【SymPy】&#xff08;二&#xff09;使用SymPy需要避开的坑 【SymPy】&#xff08;三&#xff09;基本操作&#xff08;四&#xff09;打印 简化 文章目录简化1 simplify2 多项式/有理函数简化2.1 expand2.2 factor2.2.3 colle…...

网址大全2345色综合导航/最专业的seo公司

urllib-Python3文档链接致谢python修行路 1.初识urllib urllib库包含以下模块&#xff1a; urllib.request——打开和读取 URLsurllib.error——urllib.request异常处理urllib.parse——解码URLsurllib.robotparser——解码robots.txt 2.urllib爬虫 2.1 简单的get方法 简…...

做系统那个网站好/域名

前几天和一个在读的本科生聊天&#xff0c;他一直在抱怨学校学习的理论知识太多&#xff0c;实践的机会太少。担心自己因此毕业后可能难以找到工作。我认为一个人要是想投入开发&#xff0c;他总是可以找到项目的。与其把自己的时间浪费在抱怨和指责上&#xff0c;为什么不现在…...