洛阳 网站建设 大师字画,从零开始学微信公众号运营推广,中英双语网站建设合同,实验室规划设计厂商第十章 创建在线教育平台在上一章#xff0c;我们为电商网站项目添加了国际化功能#xff0c;还创建了优惠码和商品推荐系统。在本章#xff0c;会建立一个新的项目#xff1a;一个在线教育平台#xff0c;并创内容管理系统CMS#xff08;Content Management System…第十章 创建在线教育平台在上一章我们为电商网站项目添加了国际化功能还创建了优惠码和商品推荐系统。在本章会建立一个新的项目一个在线教育平台并创内容管理系统CMSContent Management System。本章的具体内容有为模型建立fixtures使用模型的继承关系创建自定义模型字段使用CBV和mixin建立表单集formsets管理用户组与权限创建CMS1创建在线教育平台项目我们最后一个项目就是这个在线教育平台。在这个项目中我们将建立一个灵活的CMS系统让讲师可以创建课程并且管理课程的内容。为本项目建立一个虚拟环境在终端输入如下命令Copymkdirenv
virtualenv env/educa
sourceenv/educa/bin/activate在虚拟环境中安装Django与PillowCopypip install Django2.0.5
pip install Pillow5.1.0之后新建项目educaCopydjango-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_length200)slug models.SlugField(max_length200, uniqueTrue)classMeta:ordering [title]def__str__(self):return self.titleclassCourse(models.Model):owner models.ForeignKey(User, related_namecourse_created, on_deletemodels.CASCADE)subject models.ForeignKey(Subject, related_namecourses, on_deletemodels.CASCADE)title models.CharField(max_length200)slug models.SlugField(max_length200, uniqueTrue)overview models.TextField()created models.DateTimeField(auto_now_addTrue)classMeta:ordering [-created]def__str__(self):return self.titleclassModule(models.Model):course models.ForeignKey(Course,related_namemodules,on_deletemodels.CASCADE)title models.CharField(max_length200)description models.TextField(blankTrue)def__str__(self):return self.title这是初始的SubjectCourse和Module模型。Course模型的字段如下owner 课程讲师也是课程创建者subject 课程的主体外键关联到Subject模型title 课程名称slug 课程slug名称将来用在生成URLoverview 课程简介created 课程建立时间生成数据行时候自动填充Module从属于一个具体的课程所以Module模型中有一个外键连接到Course模型。之后进行数据迁移不再赘述。2.1在管理后台注册上述模型编辑course应用的admin.py文件添加如下代码Copyfrom django.contrib import admin
from .models import Subject, Course, Moduleadmin.register(Subject)classSubjectAdmin(admin.ModelAdmin):list_display [title, slug]prepopulated_fields {slug: (title,)}classModuleInline(admin.StackedInline):model Moduleadmin.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支持使用JSONXML或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 --indent2可以看到如下输出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中包含了模型的名称主键字段与对应的值。设置了indent2是表示每行的缩进。可以通过向命令行提供应用名和模块名例如app.Model让数据直接输出到这个模型中还可以通过--format参数控制输出的数据格式默认是使用JSON格式。还可以通过--output参数指定输出到具体文件。对于dumpdata的详细参数可以使用命令python manage.py dumpdata --help查看。使用如下命令把这个dump结果保存到courses应用的一个fixture/目录中Copymkdir courses/fixtures
python manage.py dumpdata courses --indent2 --outputcourses/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_namecontents, on_deletemodels.CASCADE)content_type models.ForeignKey(ContentType, on_deletemodels.CASCADE)object_id models.PositiveIntegerField()item GenericForeignKey(content_type, object_id)这就是Content模型设置外键关联到了Module模型同时设置了与ContentType模型的通用关联关系可以从获取任意模型的内容。复习一下创建通用关系的所需的三个字的content_type一个外键用于关联到ContentType模型。object_id 对象的id使用PositiveIntegerField字段。item: 通用关联关系字段通过合并上两个字段来进行关联。content_type, object_id两个字段会实际生成在数据库中item字段的关系是ORM引擎构建的不真正被写进数据库中。下一步的工作是建立每种具体内容类型的数据库这些数据库有一些相同的字段用于标识基本信息也有不同的字段存放该模型独特的信息。3.1模型的继承Django支持数据模型之间的继承关系这和Python程序的类继承关系很相似Django提供了以下三种继承的方式Abstarct model 接口模型继承用于方便的向不同的数据模型中添加相同的信息这种继承方式中的基类不会在数据库中建立数据表子类会建立数据表。Multi-table model inheritance 多表模型继承在继承关系中的每个表都被认为是一个完整的模型时采用此方法继承关系中的每一个表都会实际在数据库中创建数据表。Proxy models代理模型继承在继承的时候需要改变模型的行为时使用例如加入额外的方法修改默认的模型管理器或使用新的Meta类设置此种继承不会在数据库中创建数据表。让我们详细看一下这三种方式。3.1.1Abstract models 抽象基类继承接口模型本质上是一个基类类其中定义了所有需要包含在子模型中的字段。Django不会为接口模型创建任何数据库中的数据表。继承接口模型的子模型必须将这些字段完善每一个子模型会创建数据表表中的字段包括继承自接口模型的字段和子模型中自定义的字段。为了标记一个模型为接口模型在其Meta设置中必须设置abstract Truedjango就会认为该模型是一个接口模型不会创建数据表。子模型只需要继承该模型即可。下边的例子是如何建立一个接口模型Content和子模型TextCopyfrom django.db import modelsclassBaseContent(models.Model):title models.CharField(max_length100)created models.DateTimeField(auto_now_addTrue)classMeta:abstract TrueclassText(BaseContent):body models.TextField()在这个例子中实际在数据库中创建的是Text类对应的数据表包含titlecreated和body字段。3.1.2Multi-table model inheritance 多表继承多表继承关系中的每一个表都是完整的数据模型。对于继承关系Django会自动在子模型中创建一个一对一关系的外键连接到父模型。要使用该种继承方式必须继承一个已经存在的模型django会把父模型和子模型都写入数据库下边是一个例子Copyfrom django.db import modelsclassBaseContent(models.Model):title models.CharField(max_length100)created models.DateTimeField(auto_now_addTrue)classText(BaseContent):body models.TextField()Django会将两张表都写入数据库Text表中除了body字段还有一个一对一的外键关联到BaseContent表。3.1.3Proxy models 代理模型代理模型用于改变类的行为例如增加额外的方法或者不同的Meta设置。父模型和子模型操作一张相同的数据表。Meta类中指定proxyTrue 就可以建立一个代理模型。下边是一个创建代理模型的例子Copyfrom django.db import models
from django.utils import timezoneclassBaseContent(models.Model):title models.CharField(max_length100)created models.DateTimeField(auto_now_addTrue)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_deletemodels.CASCADE)title models.CharField(max_length250)created models.DateTimeField(auto_now_addTrue)updated models.DateTimeField(auto_nowTrue)classMeta:abstract Truedef__str__(self):return self.titleclassText(ItemBase):content models.TextField()classFile(ItemBase):file models.FileField(upload_tofiles)classImage(ItemBase):file models.FileField(upload_toimages)classVideo(ItemBase):url models.URLField()在这段代码中首先建立了一个接口模型ItemBase其中有四个字段然后在Meta中设置了abstractTrue以使该类为接口类。该类中定义了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_deletemodels.CASCADE,limit_choices_to{model__in: (text, file, image, video)})这里使用了limit_choices_to属性以使ContentType对象限于这四个模型中。如此定义之后在查询数据库的时候还能够使用filter的参数例如model__intext来检索具体某个模型的对象。建立好所有模型之后执行数据迁移程序不再赘述。现在就已经建立了本项目所需要的基本数据表及其结构。然而我们的模型中还缺少一些内容课程和课程的内容是按照一定顺序排列的但用户建立课程和上传内容的时候未必是线性的我们需要一个排序字段通过字段可以把课程章节和内容进行排序。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_fieldsNone, *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()方法这个方法是在将字段的值实际存入到数据库之前执行的。在这个方法里执行了如下逻辑检查当前字段是否已经存在值self.attname表示该字段对应的属性名也就是字段属性。如果属性名是None说明用户没有设置序号。则按照以下逻辑进行计算建立一个QuerySet查询这个字段所在的模型的全部数据行。访问字段所在的模型使用了self.model通过用户给出的for_fields参数把上一步的QuerySet用其中的字段拆解之后过滤这样就可以取得具体的用于计算序号的参考数据行。然后从过滤过的QuerySet中使用last_item qs.latest(self.attname)方法取出最新一行数据对应的序号。如果取不到说明自己是第一行。就将临时变量设置为0如果能够取到就把取到的序号1然后赋给value临时变量然后通过setattr()将临时变量value添加为字段名属性对应的值如果当前的字段已经有值说明用户传入了序号不需要做任何工作。在自定义字段时一定不要硬编码将内容写死也需要像内置字段一样注意通用性。关于自定义字段可以看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], blankTrue)我们给自定义的排序字段起名叫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(blankTrue, 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_namemodules, on_deletemodels.CASCADE)title models.CharField(max_length200)description models.TextField(blankTrue)order OrderField(for_fields[course], blankTrue)def__str__(self):return{}. {}.format(self.order, self.title)classMeta:ordering [order]classContent(models.Model):module models.ForeignKey(Module, related_namecontents, on_deletemodels.CASCADE)content_type models.ForeignKey(ContentType, on_deletemodels.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], blankTrue)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 cant 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到数据表中必须提供一个默认值。如果字段有nullTrue属性就不会提示此问题。我们有两个选择选项1是输入一个默认值作为所有已经存在的数据行该字段的值选项2是放弃这次操作在模型中为该字段添加defaultxx属性来设置默认值。这里我们输入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
Typeexittoexit this prompt系统提示我们输入值输入0然后按回车之后Django又会对Module模型询问同样的问题依然选择第一项然后输入0。之后可以看到CopyMigrations forcourses: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 Userfrom courses.models import Subject, Course, Moduleuser User.objects.last()subject Subject.objects.last()c1 Course.objects.create(subjectsubject, owneruser, titleCourse 1, slugcourse1)添加了一个新课程现在我们来为新课程添加对应的章节来看看是如何自动排序的。Copy. m1 Module.objects.create(coursec1, titleModule 1)m1.order
0可以看到m1对象的序号字段的值被设置为0因为这是针对课程的第一个Module对象下边再增加一个Module对象Copy m2 Module.objects.create(coursec1, titleModule 2)m2.order
1可以看到随后增加的Module对象的序号自动被设置成了1这次我们创建第三个对象指定序号为5Copy m3 Module.objects.create(coursec1, titleModule 3, order5)m3.order
5如果指定了序号则序号就会是指定的数字。为了继续试验再增加一个对象不给出序号参数Copy m4 Module.objects.create(coursec1, titleModule 4)m4.order
6可以看到序号会根据最后保存的数据继续增加1。OrderField字段无法保证序号一定连续但可以保证添加的内容的序号一定是从小到大排列的。继续试验我们再增加第二个课程然后第二个课程添加一个Module对象Copy c2 Course.objects.create(subjectsubject, titleCourse 2, slugcourse2, owneruser)m5 Module.objects.create(coursec2, titleModule 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(), namelogin),path(accounts/logout/, auth_views.LogoutView.as_view(), namelogout),path(admin/, admin.site.urls),
]4.2创建用户验证模板在courses应用下建立如下目录和文件Copytemplates/base.htmlregistration/login.htmllogged_out.html在编写登录登出和其他模板之前先来编辑base.html作为母版在其中添加如下内容Copy{% load staticfiles %}
!DOCTYPE htmlhtmlheadmetacharsetutf-8/title{% block title %}Educa{% endblock %}/titlelinkhref{% static css/base.css %} relstylesheet/headbodydividheaderahref/classlogoEduca/aulclassmenu{% if request.user.is_authenticated %}liahref{% url logout %}Sign out/a/li{% else %}liahref{% url login %}Sign in/a/li{% endif %}/ul/divdividcontent{% block content %}{% endblock %}
/divscriptsrchttps://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js/scriptscript$(document).ready(function () {{% block domready %}{% endblock %}});
/script/body/html译者注为了使用方便这里将作者原书存放jQuery文件的的Google CDN换成了国内BootCDN的地址。下边很多地方都作类似处理。在母版中定义了几个块title: 用于HEAD标签的TITLE标签使用content: 页面主体内容domready包含jQuery的$document.ready()代码为页面DOM加载完成后执行的JS代码这里还用到了CSS文件在courses应用中建立static/css/目录并将随书源代码中的CSS文件复制过来。有了母版之后编辑registration/login.htmlCopy{% extends base.html %}{% block title %}Log-in{% endblock %}{% block content %}h1Log-in/h1divclassmodule{% if form.errors %}pYour username and password didnt match. Please try again./p{% else %}pPlease, use the following form to log-in:/p{% endif %}divclasslogin-formformaction{% url login %}methodpost{{ form.as_p }}{% csrf_token %}inputtypehiddennamenextvalue{{ next }}/pinputtypesubmitvalueLog-in/p/form/div/div
{% endblock %}这是Django标准的用于内置login视图的模板。继续编写同目录下的logged_out.htmlCopy{% extends base.html %}
{% block title %}Logged out{% endblock %}
{% block content %}h1Logged out/h1divclassmodulepYou 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.htmldefget_queryset(self):qs super(ManageCourseListView, self).get_queryset()return qs.filter(ownerself.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(ownerself.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.htmlclassManageCourseListView(OwnerCourseMixin, ListView):template_name courses/manage/course/list.htmlclassCourseCreateView(OwnerCourseEditMixin, CreateView):passclassCourseUpdateView(OwnerCourseEditMixin, UpdateView):passclassCourseDeleteView(OwnerCourseMixin, DeleteView):template_name courses/manage/course/delete.htmlsuccess_url reverse_lazy(manage_course_list)
在上述代码中创建了两个mixin类OwnerMixin和OwnerEditMixin将这些mixins和Django内置的ListViewCreateViewUpdateViewDeleteView一起使用。这里创建的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_urlCreateView和UpdateView视图在表单提交成功后的跳转地址这里定义了一个URL名称manage_course_list稍后会在路由中配置该名称最后我们创建了如下几个OwnerCourseMixin的子类ManageCourseListView展示当前用户创建的课程继承OwnerCourseMixin和ListViewCourseCreateView使用一个模型表单创建一个新的Course对象使用OwnerCourseEditMixin定义的字段并且继承内置的CreateViewCourseUpdateView允许编辑和修改已经存在的Course对象继承OwnerCourseEditMixin和UpdateViewCourseDeleteView继承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来限制对视图的访问LoginRequiredMixin: 与login_required装饰器功能一样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_courseclassCourseUpdateView(PermissionRequiredMixin, OwnerCourseEditMixin, UpdateView):permission_required courses.change_courseclassCourseDeleteView(PermissionRequiredMixin, OwnerCourseMixin, DeleteView):template_name courses/manage/course/delete.htmlsuccess_url reverse_lazy(manage_course_list)permission_required courses.delete_coursePermissionRequiredMixin会检查用户是否具备在permission_required参数里指定的权限。现在视图就只能供指定权限的用户使用了。视图编写完毕之后为视图配置路由先在courses应用中新建urls.py文件添加下列代码Copyfrom django.urls import path
from . import viewsurlpatterns [path(mine/, views.ManageCourseListView.as_view(), namemanage_course_list),path(create/, views.CourseCreateView.as_view(), namecourse_create),path(pk/edit/, views.CourseUpdateView.as_view(), namecourse_edit),path(pk/delete/, views.CourseDeleteView.as_view(), namecourse_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(), namelogin),path(accounts/logout/, auth_views.LogoutView.as_view(), namelogout),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 %}h1My courses/h1divclassmodule{% for course in object_list %}divclasscourse-infoh3{{ course.title }}/h3pahref{% url course_edit course.id %}Edit/aahref{% url course_delete course.id %}Delete/a/p/div{% empty %}pYou havent created any courses yet./p{% endfor %}pahref{% url course_create %} classbuttonCreate 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 %}/h1divclassmoduleh2Course info/h2formaction.methodpost{{ form.as_p }}{% csrf_token %}pinputtypesubmitvalueSave 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 %}h1Delete course {{ object.title }}/h1divclassmoduleformactionmethodpost{% csrf_token %}pAre you sure you want to delete {{ object }}?/pinputtypesubmitclassbuttonvalueConfirm/form/div
{% endblock %}注意原书的代码在input元素的的class属性后边漏了一个号这个模板由继承了DeleteView的CourseDeleteView视图操作负责删除课程。打开浏览器点击刚才页面中的Delete链接跳转到如下确认页面点击CONFIRM按钮课程就会被删除然后重定向至课程列表页。讲师组用户现在可以增删改课程了。下边要做的是通过CMS让讲师组用户为课程添加章节和内容。5管理章节与内容这一节里来建立一个管理课程中章节和内容的系统将为同时管理课程中的多个章节及其中不同的内容建立表单。章节和内容都需要按照特定的顺序记录在我们的CMS中。5.1在课程模型中使用表单集formsetsDjango通过一个抽象层控制页面中的所有表单对象。一组表单对象被称为表单集。表单集由多个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], extra2, can_deleteTrue)我们使用内置的inlineformset_factory()方法构建了表单集ModuleFormSet。内联表单工厂函数是在普通的表单集之上的一个抽象。这个函数允许我们动态的通过与Course模型关联的Module模型创建表单集。对这个表单集我们应用了如下字段fields表示表单集中每个表单的字段extra设置每次显示表单集时候的表单数量can_delete该项如果设置TrueDjango会在每个表单内包含一个布尔字段被渲染成为一个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.htmlcourse Nonedefget_formset(self, dataNone):return ModuleFormSet(instanceself.course, datadata)defdispatch(self, request, pk):self.course get_object_or_404(Course, idpk, ownerrequest.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(datarequest.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。在这个视图中实现了如下的方法get_formset()这个方法是创建formset对象的过程为了避免重复编写所以写了一个方法。功能是根据获得的Course对象和可选的data参数来构建一个ModuleFormSet对象。dispatch()这个方法是View视图的方法是一个分发器HTTP请求进来之后最先执行的是dispatch()方法。该方法把小写的HTTP请求的种类分发给同名方法例如GET请求会被发送到get()方法进行处理POST请求会被发送到post()方法进行处理。在这个方法里。使用get_object_or_404()加一个id参数从Course类中获取对象。把这段代码包含在dispatch()方法中是因为无论GET还是POST请求都会使用Course对象。在请求一进来的时候就把Course对象存入self.course供其他方法使用。get()处理GET请求。创建一个ModuleFormSet然后使用当前的Course对象渲染模板使用了TemplateResponseMixin提供的render_to_response()方法post()处理POST请求在这个方法中执行了如下动作使用请求附带的数据建立ModuleFormSet对象执行is_valid()方法验证所有表单验证通过则使用save()方法保存这时增删改都会写入数据库。然后重定向到manage_course_list URL。如果未通过验证就返回当前表单对象以显示错误信息。编辑courses应用中的urls.py文件为刚写的视图配置URLCopypath(pk/module/, views.CourseModuleUpdateView.as_view(), namecourse_module_update),在模板目录courses/templates/下创建一个新目录叫做module然后创建templates/courses/manage/module/formset.html文件添加下列代码Copy{% extends base.html %}
{% block title %}Edit {{ course.title }}
{% endblock %}
{% block content %}h1Edit {{ course.title }}/h1divclassmoduleh2Course modules/h2formactionmethodpost{{ formset }}{{ formset.management_form }}{% csrf_token %}inputtypesubmitclassbuttonvalueSave modules/form/div
{% endblock %}在这个模板中创建了一个表单元素form其中包含了formset表单集还包含了一个管理表单{{ formset.management_form }}。这个管理表单包含隐藏的字段用于控制显示起始总计最小和最大编号的表单。可以看到创建表单集很简单。编辑courses/templates/course/list.html把course_module_update的链接加在编辑和删除链接之下Copyahref{% url course_edit course.id %}Edit/aahref{% url course_delete course.id %}Delete/aahref{% url course_module_update course.id %}Edit modules/a现在模板中有了编辑课程中章节的链接启动站点到http://127.0.0.1:8000/course/mine/创建一个课程然后点击Edit modules链接可以看到页面中的表单集如下这个表单集合包含了该课程中的每个Module对象然后还多出来2个空白的表单可供填写这是因为我们为ModuleFormSet设置了extra2。输入两个新的章节内容然后保存表单再进编辑页面可以看到又多出来了两个空白表单。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.htmldefget_model(self, model_name):if model_name in [text, video, image, file]:return apps.get_model(app_labelcourses, model_namemodel_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, idNone):self.module get_object_or_404(Module, idmodule_id, course__ownerrequest.user)self.model self.get_model(model_name)ifid:self.obj get_object_or_404(self.model, idid, ownerrequest.user)returnsuper(ContentCreateUpdateView, self).dispatch(request, module_id, model_name, id)这是ContentCreateUpdateView视图的第一部分。这个类用于建立和更新章节中的内容这个类定义了如下方法get_model()检查给出的名字是否在指定的四个类名中然后用Django的apps模块从courses应用中取出对应的模块如果没有找到就返回Noneget_form()使用内置的modelform_factory()方法建立表单集去掉了四个指定的字段使用剩下的字段建立。这么做我们可以不考虑具体是哪个模型只去掉通用的字段保留剩下的字段。dispatch()这个方法接收下列的URL参数然后为当前对象设置module和model属性module_id章节的idmodel_name内容模型的名称id要更新的内容的id默认值为None表示新建。然后来编写该视图的get()和post()方法Copydefget(self, request, module_id, model_name, idNone):form self.get_form(self.model, instanceself.obj)return self.render_to_response({form: form, object: self.obj})defpost(self, request, module_id, model_name, idNone):form self.get_form(self.model, instanceself.obj, datarequest.POST, filesrequest.FILES)if form.is_valid():obj form.save(commitFalse)obj.owner request.userobj.save()ifnotid:# 新内容Content.objects.create(moduleself.module, itemobj)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所以instanceNone表示新建post()处理POST请求。通过传入的所有数据创建表单集对象然后进行验证。如果验证通过给当前对象设置上user属性然后保存。如果没有传入id说明是新建内容需要在Content中追加一条记录关联到module对象和新建的内容对象。编辑courses应用的urls.py文件为新视图配置URLCopy path(module/int:module_id/content/model_name/create/, views.ContentCreateUpdateView.as_view(),namemodule_content_create),path(module/int:module_id/content/model_name/id/, views.ContentCreateUpdateView.as_view(),namemodule_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 %}/h1divclassmoduleh2Course info/h2formactionmethodpostenctypemultipart/form-data{{ form.as_p }}{% csrf_token %}pinputtypesubmitvalueSave content/p/form/div
{% endblock %}这是视图ContentCreateUpdateView控制的模板。在这个模板里使用了一个object变量如果object变量不为None说明在修改一个已经存在的内容否则就是新建一个内容。form标签中设置了属性enctypemultipart/form-data因为File和Image模型中有文件字段。启动站点到http://127.0.0.1:8000/course/mine/点击任何一个已经存在的课程的Edit modules链接之后新建一个module。然后打开带有当前Django环境的Python命令行来进行一些测试首先取到最后一个建立的module对象Copy from courses.models import ModuleModule.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, idid, module__course__ownerrequest.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(), namemodule_content_delete),现在讲师用户就可以增删改内容了。5.3管理章节与内容在上一节里编写好了增删改的视图现在需要一个视图将一个课程的全部章节和其中的内容展示出来的视图。编辑courses应用的views.py文件添加下列代码CopyclassModuleContentListView(TemplateResponseMixin, View):template_name courses/manage/module/content_list.htmldefget(self, request, module_id):module get_object_or_404(Module,idmodule_id,course__ownerrequest.user)return self.render_to_response({module: module})这个ModuleContentListView视图通过一个指定的Module对象的ID和当前用户来获取Module对象然后使用该对象渲染模板。在courses应用的urls.py内加入该视图的路由Copypath(module/int:module_id/, views.ModuleContentListView.as_view(), namemodule_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 coursemodule.course %}h1Course {{ course.title }}/h1divclasscontentsh3Modules/h3ulidmodules{% for m in course.modules.all %}lidata-id{{ m.id }} {% ifm module %}classselected{% endif %}ahref{% url module_content_list m.id %}spanModule spanclassorder{{ m.order|add:1 }}/span/spanbr{{ m.title }}/a/li{% empty %}liNo modules yet./li{% endfor %}/ulpahref{% url course_module_update course.id %}Edit modules/a/p/divdivclassmoduleh2Module {{ module.order|add:1 }}: {{ module.title }}/h2h3Module contents:/h3dividmodule-contents{% for content in module.contents.all %}divdata-id{{ content.id }}{% with itemcontent.item %}p{{ item }}/pahref#Edit/aformaction{% url module_content_delete content.id %}methodpostinputtypesubmitvalueDelete{% csrf_token %}/form{% endwith %}/div{% empty %}pThis module has no contents yet./p{% endfor %}/divh3Add new content:/h3ulclasscontent-typesliahref{% url module_content_create module.id text %}Text/a/liliahref{% url module_content_create module.id image %}Image/a/liliahref{% url module_content_create module.id video %}Video/a/liliahref{% 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 %}然后找到下边两行Copyp{{ item }}/pahref#Edit/a替换成Copyp{{ item }} ({{ item|model_name }})/pahref{% url module_content_update module.iditem|model_nameitem.id %}Edit/a使用了自定义模板过滤器之后我们在模板中显示内容对象时就可以通过对象所属模型的名称来生成URL链接了。编辑courses/manage/course/list.html添加一个列表页的链接Copyahref{% 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模块中的mixinsdjango-braces是一个第三方模块包含了一系列通用的Mixin为CBV提供额外的功能。可以查看其官方文档https://django-braces.readthedocs.io/en/latest/来获得完整的mixin列表。我们要使用django-braces中下列mixinCsrfExemptMixin在POST请求中不检查CSRF无需生成csrf_tokenJsonRequestResponseMixin以JSON字符串形式解析请求中的数据并且序列化响应数据为JSON格式带有application/json头部信息通过pip安装django-bracesCopypip install django-braces1.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(idid, course__ownerrequest.user).update(orderorder)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(idid, module__course__ownerrequest.user).update(orderorder)return self.render_json_response({saved: OK})然后编辑courses应用的urls.py为这两个视图配置URLCopy path(module/order/, views.ModuleOrderView.as_view(), namemodule_order),path(content/order/, views.ContentOrderView.as_view(), namecontent_order),最后需要在模板中实现拖动功能。使用jQuery UI库来完成这个功能。jQuery UI基于jQuery提个了一系列的界面互动操作效果和插件。我们使用其中的sortable元素。首先需要把jQuery加载到母版中。打开base.html在加载jQuery的script标签之后加入jQuery UI。Copyscriptsrchttps://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js/scriptscriptsrchttps://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; charsetutf-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; charsetutf-8,dataType: json,data: JSON.stringify(contents_order),});}
});
{% endblock %}译者注这里对原书的代码增加了let声明。这段代码加载在{% domready %}块中会在页面DOM加载完成后立刻执行。在代码中为所有的侧边栏中的章节列表定义了一个sortable方法为内容也定义了一个同样功能的方法。这段代码做了下列工作使用#modules选择器为modules的HTML元素定义了sortable元素定义了一个stop事件处理函数用户停止拖动后触发该事件建立了一个空字典modules_orderJS里叫做对象其中的键是module的IDLI元素的data-id属性的值值是重新排列后的顺序。遍历拖动后的#module的子元素取得此时每个元素的data-id和此时在列表中的索引用此时的id作为键其顺序作为值更新modules_order字典。通过AJAX发送POST请求到content_order URL进行处理请求中带有modules_order JSON字符串交给ModuleOrderView进行处理。用于排序内容部分的sortable元素与上述这个相似。启动站点重新加载编辑内容的页面现在可以通过拖动重新排列章节和内容的顺序如下图所示现在我们就实现了拖动排序功能。总结这一章学习了如何建立一个CMS。使用了模型继承和创建自定义字段同时使用了基于类的视图和mixins。还使用了表单集和实现了一个管理不同的内容的系统。如有不懂还要咨询下方小卡片博主也希望和志同道合的测试人员一起学习进步在适当的年龄选择适当的岗位尽量去发挥好自己的优势。我的自动化测试开发之路一路走来都离不每个阶段的计划因为自己喜欢规划和总结测试开发视频教程、学习笔记领取传送门