创建在线教育平台

Posted 一个处女座的测试

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了创建在线教育平台相关的知识,希望对你有一定的参考价值。

第十章 创建在线教育平台

在上一章,我们为电商网站项目添加了国际化功能,还创建了优惠码和商品推荐系统。在本章,会建立一个新的项目:一个在线教育平台,并创内容管理系统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 1
  Course 1
    Module 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 User

classSubject(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)

    classMeta:
        ordering = ['title']

    def__str__(self):
        return self.title

classCourse(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.title

classModule(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 models

classBaseContent(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 models

classBaseContent(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 timezone

classBaseContent(models.Model):
    title = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)

classOrderedContent(BaseContent):
    classMeta:
        proxy = True
        ordering = ['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.title

classText(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 ObjectDoesNotExist

classOrderField(models.PositiveIntegerField):

    def__init__(self, for_fields=None, *args, kwargs):
        self.for_fields = for_fields
        super(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 value
        else:
            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 OrderField

classModule(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_views

urlpatterns = [
    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.html
    registration/
        login.html
        logged_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 Course

classManageCourseListView(ListView):
    model = Course
    template_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, DeleteView

from .models import Course

classOwnerMixin:
    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.user
        returnsuper(OwnerEditMixin, self).form_valid(form)

classOwnerCourseMixin(OwnerMixin):
    model = Course

classOwnerCourseEditMixin(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 = Course
    fields = ['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 views

urlpatterns = [
    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_views

urlpatterns = [
    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.html
            form.html
            delete.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 new
                course</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类的实例组成。表单集内的所有表单在提交的时候会一并提交,表单集可以控制显示的表单数量,对提交的最大表单数量做限制,同时对其中的全部表单

以上是关于创建在线教育平台的主要内容,如果未能解决你的问题,请参考以下文章

自动化测试平台:开发用户认证接口

python自动化自动化测试平台开发:4.后端开发之用例的储存和查看

如何快速构建YApi平台完成接口测试中心

2019第一期《python测试开发》课程,10月13号开学

信息安全与安全测试实战课程要在上课了在线平台开课了

python自动化自动化测试平台开发:1.自动化测试平台简介