第十章 构建一个在线学习平台(上)

作者: lakerszhy | 来源:发表于2017-05-19 17:34 被阅读887次

    10 构建一个在线学习平台

    在上一章中,你为在线商店项目添加了国际化。你还构建了一个优惠券系统和一个商品推荐引擎。在本章中,你会创建一个新的项目。你会构建一个在线学习平台,这个平台会创建一个自定义的内容管理系统。

    在本章中,你会学习如何:

    • 为模型创建fixtures
    • 使用模型继承
    • 创建自定义O型字典
    • 使用基于类的视图和mixins
    • 构建表单集
    • 管理组和权限
    • 创建一个内容管理系统

    10.1 创建一个在线学习平台

    我们最后一个实战项目是一个在线学习平台。在本章中,我们会构建一个灵活的内容管理系统(CMS),允许教师创建课程和管理课程内容。

    首先,我们用以下命令为新项目创建一个虚拟环境,并激活它:

    mkdir env
    virtualenv env/educa
    source env/educa/bin/activate
    

    用以下命令在虚拟环境中安装Django:

    pip install Django
    

    我们将在项目中管理图片上传,所以我们还需要用以下命令安装Pillow:

    pip install Pillow
    

    使用以下命令创建一个新项目:

    django-admin startproject educa
    

    进入新的educa目录,并用以下命令创建一个新应用:

    cd educa
    django-admin startapp courses
    

    编辑educa项目的settings.py文件,把courses添加到INSTALLED_APPS设置中:

    INSTALLED_APPS = [
        'courses',
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
    ]
    

    现在courses应用已经在项目激活了。让我们为课程和课程内容定义模型。

    10.2 构建课程模型

    我们的在线学习平台会提供多种主题的课程。每个课程会划分为可配置的单元数量,而每个单元会包括可配置的内容数量。会有各种类型的内容:文本,文件,图片或者视频。下面这个例子展示了我们的课程目录的数据结构:

    Subject 1
        Course 1
            Module 1
                Content 1 (image)
                Content 3 (text)
            Module 2
                Content 4 (text)
                Content 5 (file)
                Content 6 (video)
                ...
    

    让我们构建课程模型。编辑courses应用的models.py文件,并添加以下代码:

    from django.db import models
    from django.contrib.auth.models import User
    
    class Subject(models.Model):
        title = models.CharField(max_length=200)
        slug = models.SlugField(max_length=200, unique=True)
    
        class Meta:
            ordering = ('title', )
    
        def __str__(self):
            return self.title
    
    class Course(models.Model):
        owner = models.ForeignKey(User, related_name='courses_created')
        subject = models.ForeignKey(Subject, related_name='courses')
        title = models.CharField(max_length=200)
        slug = models.SlugField(max_length=200, unique=True)
        overview = models.TextField()
        created = models.DateTimeField(auto_now_add=True)
    
        class Meta:
            ordering = ('-created',)
    
        def __str__(self):
            return self.title
    
    class Module(models.Model):
        course = models.ForeignKey(Course, related_name='modules')
        title = models.CharField(max_length=200)
        description = models.TextField(blank=True)
    
        def __str__(self):
            return self.title
    

    这些是初始的SubjectCourseModule模型。Course模型有以下字段:

    • owner:创建给课程的教师
    • subject:这个课程所属的主题。一个指向Subject模型的ForeignKey字段。
    • title:课程标题.
    • slug:课程别名,之后在URL中使用。
    • overview:一个TextField列,表示课程概述。
    • created:课程创建的日期和时间。因为设置了auto_now_add=True,所以创建新对象时,Django会自动设置这个字段。

    每个课程划分为数个单元。因此,Module模型包含一个指向Course模型的ForeignKey字段。

    打开终端执行以下命令,为应用创建初始的数据库迁移:

    python manage.py makemigrations
    

    你会看到以下输出:

    Migrations for 'courses':
      courses/migrations/0001_initial.py
        - Create model Course
        - Create model Module
        - Create model Subject
        - Add field subject to course
    

    然后执行以下命令,同步迁移到数据库中:

    python manage.py migrate
    

    你会看到一个输出,其中包括所有已经生效的数据库迁移,包括Django的数据库迁移。输出会包括这一行:

    Applying courses.0001_initial... OK
    

    这个告诉我们,courses应用的模型已经同步到数据库中。

    10.2.1 在管理站点注册模型

    我们将把课程模型添加到管理站点。编辑courses应用目录中的admin.py文件,并添加以下代码:

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

    现在courses应用的模型已经在管理站点注册。我们用@admin.register()装饰器代替admin.site.register()函数。它们的功能是一样的。

    10.2.2 为模型提供初始数据

    有时你可能希望用硬编码数据预填充数据库。这在项目创建时自动包括初始数据很有用,来替代手工添加数据。Django自带一种简单的方式,可以从数据库中加载和转储(dump)数据到fixtures文件中。

    Django支持JSON,XML或者YAML格式的fixtures。我们将创建一个fixture,其中包括一些项目的初始Subject对象。

    首先使用以下命令创建一个超级用户:

    python manage.py createsuperuser
    

    然后用以下命令启动开发服务器:

    python manage.py runserver
    

    现在在浏览器中打开http://127.0.0.1:8000/admin/courses/subject/。使用管理站点创建几个主题。列表显示页面如下图所示:

    在终端执行以下命令:

    python manage.py dumpdata courses --indent=2
    

    你会看到类似这样的输出:

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

    dumpdata命令从数据库中转储数据到标准输出,默认用JSON序列化。返回的数据结构包括模型和它的字段信息,Django可以把它加载到数据库中。

    你可以给这个命令提供应用的名称,或者用app.Model格式指定输出数据的模型。你还可以使用--format标签指定格式。默认情况下,dumpdata输出序列化的数据到标准输出。但是,你可以使用--output标签指定一个输出文件。--indent标签允许你指定缩进。关于更多dumpdata的参数信息,请执行python manage.py dumpdata --help命令。

    使用以下命令,把这个转储保存到courses应用的fixtures/目录中:

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

    使用管理站点移除你创建的主题。然后使用以下命令把fixture加载到数据库中:

    python manage.py loaddata subjects.json
    

    fixture中包括的所有Subject对象已经加载到数据库中。

    默认情况下,Django在每个应用的fixtures/目录中查找文件,但你也可以为loaddata命令指定fixture文件的完整路径。你还可以使用FIXTURE_DIRS设置告诉Django查找fixtures的额外目录。

    Fixtures不仅对初始数据有用,还可以为应用提供简单的数据,或者测试必需的数据。

    你可以在这里阅读如何在测试中使用fixtures。

    如果你想在模型迁移中加载fixtures,请阅读Django文档的数据迁移部分。记住,我们在第九章创建了自定义迁移,用于修改模型后迁移已存在的数据。你可以在这里阅读数据库迁移的文档。

    10.3 为不同的内容创建模型

    我们计划在课程模型中添加不同类型的内容,比如文本,图片,文件和视频。我们需要一个通用的数据模型,允许我们存储不同的内容。在第六章中,我们已经学习了使用通用关系创建指向任何模型对象的外键。我们将创建一个Content模型表示单元内容,并定义一个通过关系,关联到任何类型的内容。

    编辑courses应用的models.py文件,并添加以下导入:

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

    然后在文件结尾添加以下代码:

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

    这是Content模型。一个单元包括多个内容,所以我们定义了一个指向Module模型的外键。我们还建立了一个通用关系,从代表不同内容类型的不同模型关联到对象。记住,我们需要三个不同字段来设置一个通用关系。在Content模型中,它们分别是:

    • content_type:一个指向ContentType模型的ForeignKey字段。
    • object_id:这是一个PositiveIntegerField,存储关联对象的主键。
    • item:通过组合上面两个字段,指向关联对象的GenericForeignKey字段。

    在这个模型的数据库表中,只有content_typeobject_id字段有对应的列。item字段允许你直接检索或设置关联对象,它的功能建立在另外两个字段之上。

    我们将为每种内容类型使用不同的模型。我们的内容模型会有通用字段,但它们存储的实际内容会不同。

    10.3.1 使用模型继承

    Django支持模型继承,类似Python中标准类的继承。Django为使用模型继承提供了以下三个选择:

    • 抽象模型:当你想把一些通用信息放在几个模型时很有用。不会为抽象模型创建数据库表。
    • 多表模型继承:可用于层次中每个模型本身被认为是一个完整模型的情况下。为每个模型创建一张数据库表。
    • 代理模型:当你需要修改一个模型的行为时很有用。例如,包括额外的方法,修改默认管理器,或者使用不同的元选项。不会为代理模型创建数据库表。

    让我们近一步了解它们。

    10.3.1.1 抽象模型

    一个抽象模型是一个基类,其中定义了你想在所有子模型中包括的字段。Django不会为抽象模型创建任何数据库表。会为每个子模型创建一张数据库表,其中包括从抽象类继承的字段,和子模型中定义的字段。

    要标记一个抽象模型,你需要在它的Meta类中包括abstract=True。Django会认为它是一个抽象模型,并且不会为它创建数据库表。要创建子模型,你只需要从抽象模型继承。以下是一个Content抽象模型和Text子模型的例子:

    from django.db import models
    
    class BaseContent(models.Model):
        title = models.CharField(max_length=200)
        created = models.DateTimeField(auto_now_add=True)
        
        class Meta:
            abstract = True
    
    class Text(BaseContent):
        body = models.TextField()
    

    在这个例子中,Django只会为Text模型创建数据库表,其中包括titlecreatedbody字段。

    10.3.1.2 多表模型继承

    在多表继承中,每个模型都有一张相应的数据库表。Django会在子模型中创建指向父模型的OneToOneField字段。

    要使用多表继承,你必须从已存在模型中继承。Django会为原模型和子模型创建数据库表。下面是一个多表继承的例子:

    from django.db import models
    
    class BaseContent(models.Model):
        title = models.CharField(max_length=100)
        created = models.DateTimeField(auto_now_add=True)
        
    class Text(BaseContent):
        body = models.TextField()
    

    Django会在Text模型中包括一个自动生成的OneToOneField字段,并为每个模型创建一张数据库表。

    10.3.1.3 代理模型

    代理模型用于修改模型的行为,比如包括额外的方法或者不同的元选项。这两个模型都在原模型的数据库表上进行操作。在模型的Meta类中添加proxy=True来创建代理模型。

    下面这个例子展示了如何创建一个代理模型:

    from django.db import models
    from django.utils import timezone
    
    class BaseContent(models.Model):
        title = models.CharField(max_length=100)
        created = models.DateTimeField(auto_now_add=True)
        
    class OrderedContent(BaseContent):
        class Meta:
            proxy = True
            ordering = ['created']
            
        def create_delta(self):
            return timezone.now() - self.created
    

    我们在这里定义了一个OrderedContent模型,它是Content模型的代理模型。这个模型为QuerySet提供了默认排序和一个额外的created_delta()方法。ContentOrderedContent模型都在同一张数据库表上操作,并且可以用ORM通过任何一个模型访问对象。

    10.3.2 创建内容模型

    courses应用的Content模型包含一个通用关系来关联不同的内容类型。我们将为每种内容模型创建不用的模型。所有内容模型会有一些通用的字段,和一些额外字段存储自定义数据。我们将创建一个抽象模型,它会为所有内容模型提供通用字段。

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

    class ItemBase(models.Model):
        owner = models.ForeignKey(User, related_name='%(class)s_related')
        title = models.CharField(max_length=250)
        created = models.DateTimeField(auto_now_add=True)
        updated = models.DateTimeField(auto_now=True)
    
        class Meta:
            abstract = True
    
        def __str__(self):
            return self.title
    
    class Text(ItemBase):
        content = models.TextField()
    
    class File(ItemBase):
        file = models.FileField(upload_to='files')
    
    class Image(ItemBase):
        file = models.FileField(upload_to='images')
    
    class Video(ItemBase):
        url = models.URLField()
    

    在这段代码中,我们定义了一个ItemBase抽象模型。因此我们在Meta类中设置了abstract=True。在这个模型中,我们定义了ownertitlecreatedupdated字段。这些通用字段会用于所有内容类型。owner字段允许我们存储哪个用户创建了内容。因为这个字段在抽象类中定义,所以每个子模型需要不同的related_name。Django允许我们在related_name属性中为模型的类名指定占位符,比如%(class)s。这样,每个子模型的related_name会自动生成。因为我们使用%(class)s_related作为related_name,所以每个子模型对应的反向关系是text_relatedfile_relatedimage_relatedvideo_related

    我们定义了四个从ItemBase抽象模型继承的内容模型。分别是:

    • Text:存储文本内容。
    • File:存储文件,比如PDF。
    • Image:存储图片文件。
    • Video:存储视频。我们使用URLField字段来提供一个视频的URL,从而可以嵌入视频。

    除了自身的字段,每个子模型还包括ItemBase类中定义的字段。会为TextFileImageVideo模型创建对应的数据库表。因为ItemBase是一个抽象模型,所以它不会关联到数据库表。

    编辑你之前创建的Content模型,修改它的content_type字段:

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

    我们添加了limit_choices_to参数来限制ContentType对象可用于的通用关系。我们使用了model__in字段查找,来过滤ContentType对象的model属性为textvideoimage或者file

    让我们创建包括新模型的数据库迁移。在命令行中执行以下命令:

    python manage.py makemigrations
    

    你会看到以下输出:

    Migrations for 'courses':
      courses/migrations/0002_content_file_image_text_video.py
        - Create model Content
        - Create model File
        - Create model Image
        - Create model Text
        - Create model Video
    

    然后执行以下命令应用新的数据库迁移:

    python manage.py migrate
    

    你看到的输出的结尾是:

    Running migrations:
      Applying courses.0002_content_file_image_text_video... OK
    

    我们已经创建了模型,可以添加不同内容到课程单元中。但是我们的模型仍然缺少了一些东西。课程单元和内容应用遵循特定的顺序。我们需要一个字段对它们进行排序。

    10.4 创建自定义模板字段

    Django自带一组完整的模块字段,你可以用它们构建自己的模型。但是,你也可以创建自己的模型字段来存储自定义数据,或者修改已存在字段的行为。

    我们需要一个字段指定对象的顺序。如果你想用Django提供的字段,用一种简单的方式实现这个功能,你可能会想在模型中添加一个PositiveIntegerField。这是一个好的开始。我们可以创建一个从PositiveIntegerField继承的自定义字段,并提供额外的方法。

    我们会在排序字段中添加以下两个功能:

    • 没有提供特定序号时,自动分配一个序号。如果存储对象时没有提供序号,我们的字段会基于最后一个已存在的排序对象,自动分配下一个序号。如果两个对象的序号分别是1和2,保存第三个对象时,如果没有给定特定序号,我们应该自动分配为序号3。
    • 相对于其它字段排序对象。课程单元将会相对于它们所属的课程排序,而模块内容会相对于它们所属的单元排序。

    courses应用目录中创建一个fields.py文件,并添加以下代码:

    from django.db import models
    from django.core.exceptions import ObjectDoesNotExist
    
    class OrderField(models.PositiveIntegerField):
        def __init__(self, for_fields=None, *args, **kwargs):
            self.for_fields = for_fields
            super().__init__(*args, **kwargs)
    
        def pre_save(self, model_instance, add):
            if getattr(model_instance, self.attname) is None:
                # no current value
                try:
                    qs = self.model.objects.all()
                    if self.for_fields:
                        # filter by objects with the same field values
                        # for the fields in "for_fields"
                        query = {field: getattr(model_instance, field) for field in self.for_fields}
                        qs = qs.filter(**query)
                    # get the order of the last item
                    last_item = qs.latest(self.attname)
                    value = last_item.order + 1
                except ObjectDoesNotExist:
                    value = 0
                setattr(model_instance, self.attname, value)
                return value
            else:
                return super().pre_save(model_instance, add)
    

    这是我们自定义的OrderField。它从Django提供的PositiveIntegerField字段继承。我们的OrderField字段有一个可选的for_fields参数,允许我们指定序号相对于哪些字段计算。

    我们的字段覆写了PositiveIntegerField字段的pre_save()方法,它会在该字段保存到数据库中之前执行。我们在这个方法中执行以下操作:

    1. 我们检查模型实例中是否已经存在这个字段的值。我们使用self.attname,这是模型中指定的这个字段的属性名。如果属性的值不是None,我们如下计算序号:
    • 我们构建一个QuerySet检索这个字段模型所有对象。我们通过访问self.model检索字段所属的模型类。
    • 我们用定义在字段的for_fields参数中的模型字段(如果有的话)的当前值过滤QuerySet。这样,我们就能相对于给定字段计算序号。
    • 我们用last_item = qs.lastest(self.attname)从数据库中检索序号最大的对象。如果没有找到对象,我们假设它是第一个对象,并分配序号0。
    • 如果找到一个对象,我们在找到的最大序号上加1。
    • 我们用setattr()把计算的序号分配给模型实例中的字段值,并返回这个值。
    1. 如果模型实例有当前字段的值,则什么都不做。

    当你创建自定义模型字段时,让它们是通用的。避免分局特定模型或字段硬编码数据。你的字段应该可以用于所有模型。

    你可以在这里阅读更多关于编写自定义模型字段的信息。

    让我们在模型中添加新字段。编辑courses应用的models.py文件,并导入新的字段:

    from .fields import OrderField
    

    然后在Module模型中添加OrderField字段:

    order = OrderField(blank=True, for_fields=['course'])
    

    我们命名新字段为order,并通过设置for_fields=['course'],指定相对于课程计算序号。这意味着一个新单元会分配给同一个Course对象中最新的单元加1。现在编辑Module模型的__str__()方法,并如下引入它的序号:

    def __str__(self):
        return '{}. {}'.format(self.order, self.title)
    

    单元内容也需要遵循特定序号。在Content模型中添加一个OrderField字段:

    order = OrderField(blank=True, for_fields=['module'])
    

    这次我们指定序号相对于module字段计算。最后,让我们为两个模型添加默认排序。在ModuleContent模型中添加以下Meta类:

    class Meta:
        ordering = ['order']
    

    现在ModuleContent模型看起来是这样的:

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

    让我们创建反映新序号字段的模型迁移。打开终端,并执行以下命令:

    python manage.py makemigrations courses
    

    你会看到以下输出:

    You are trying to add a non-nullable field 'order' to content without a default; we can't do that (the database needs something to populate existing rows).
    Please select a fix:
     1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
     2) Quit, and let me add a default in models.py
    Select an option:
    

    Django告诉我们,因为我们在已存在的模型中添加了新字段,所以必须为数据库中已存在的行提供默认值。如果字段有null=True,则可以接受空值,并且Django创建迁移时不要求提供默认值。我们可以指定一个默认值,或者取消数据库迁移,并在创建迁移之前在models.py文件的order字段中添加default属性。

    输入1,然后按下Enter,为已存在的记录提供一个默认值。你会看到以下输出:

    Please enter the default value now, as valid Python
    The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
    Type 'exit' to exit this prompt
    >>>
    

    输入0作为已存在记录的默认值,然后按下Enter。Django还会要求你为Module模型提供默认值。选择第一个选项,然后再次输入0作为默认值。最后,你会看到类似这样的输出:

    Migrations for 'courses':
      courses/migrations/0003_auto_20170518_0743.py
        - Change Meta options on content
        - Change Meta options on module
        - Add field order to content
        - Add field order to module
    

    然后执行以下命令应用新的数据库迁移:

    python manage.py migrate
    

    这个命令的输出会告诉你迁移已经应用成功:

    Applying courses.0003_auto_20170518_0743... OK
    

    让我们测试新字段。使用python manage.py shell命令打开终端,并如下创建一个新课程:

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

    我们已经在数据库中创建了一个课程。现在,让我们添加一些单元到课程中,并查看单元序号是如何自动计算的。我们创建一个初始单元,并检查它的序号:

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

    OrderField设置它的值为0,因为这是给定课程的第一个Module对象。现在我们创建同一个课程的第二个单元:

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

    OrderField在已存在对象的最大序号上加1来计算下一个序号。让我们指定一个特定序号来创建第三个单元:

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

    如果我们指定了自定义序号,则OrderField字段不会介入,并且使用给定的order值。

    让我们添加第四个单元:

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

    这个单元的序号已经自动设置了。我们的OrderField字段不能保证连续的序号。但是它关注已存在的序号值,总是根据已存在的最大序号值分配下一个序号。

    让我们创建第二个课程,并添加一个单元:

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

    要计算新的单元序号,该字段只考虑属于同一个课程的已存在单元。因为这个第二个课程的第一个单元,所以序号为0。这是因为我们在Module模型的order字段中指定了for_fields=['course']

    恭喜你!你已经成功的创建了第一个自定义模型字段。

    相关文章

      网友评论

        本文标题:第十章 构建一个在线学习平台(上)

        本文链接:https://www.haomeiwen.com/subject/gjnyxxtx.html