美文网首页django by example 实践
django by example 实践 educa 项目(一)

django by example 实践 educa 项目(一)

作者: 学以致用123 | 来源:发表于2018-04-18 13:09 被阅读123次

    点我查看本文集的说明及目录。


    本项目相关内容包括:

    实现过程

    CH10 创建一个在线学习平台

    CH11 缓存内容

    CH12 创建API


    CH10 创建一个在线学习平台


    上一章,我们为在线商店项目添加了国际化,还创建了折扣系统和商品推荐工程。本章,我们将新建一个在线学习平台项目,实现自定义内容管理系统。

    本章,我们将学习:

    • 为模型创建 fixtures
    • 使用模型继承
    • 创建自定义模型字段
    • 使用类视图和 mixins
    • 创建 formsets
    • 管理组和权限
    • 创建内容管理系统

    创建一个在线学习平台


    我们的最后一个练习项目是在线学习平台。本章,我们将创建一个灵活的 内容管理系统( CMS ),帮助老师创建课程和管理课程内容。

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

    makedir env
    virtualenv env/educa
    source env/educa/bin/active
    

    笔者注:

    可以将虚拟环境放到前两个项目 ( blog、bookmarks、myshop ) 虚拟环境所在的文件夹下,这时, mkdir env 改为 cd env

    笔者这里仍然使用第一章中用到的 PyCharm 创建虚拟环境。

    在虚拟环境中使用以下命令安装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 应用已经激活了。我们来为课程和课程内容创建模型。

    创建课程模型


    在线学习平台将提供多个主题的课程。每个课程由可配置数量的模块组成,每个模块将包含可配置数量的内容。内容为下面的几种类型:文本、文件、图像或者视频。课程目录的数据格式看起来是这样的:

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

    我们来创建课程目录,编辑 courses 应用并添加以下代码:

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

    上面是 Subject、Course 和 Module 模型。Course 模型包括以下字段:

    • owner : 创建课程的老师;

    • subject: 课程所属的主题, Subject 模型的外键;

    • title: 课程名称;

    • slug:课程 slug ,用于后面的 URLs;

    • overview : 课程简介,TextField 类型;

    • created:创建课程的日期和时间,auto_now_add=True 表示 django 自动设置。

    每个课程分为几个模块,因此,Module 模型包括指向 Course 模型的外键。

    打开 shell 运行以下命令创建应用的初始迁移文件:

    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
    

    你将看到下面这样的输出:

    Operations to perform:
      Apply all migrations: admin, auth, contenttypes, courses, sessions
    Running migrations:
      Applying contenttypes.0001_initial... OK
      Applying auth.0001_initial... OK
      Applying admin.0001_initial... OK
      Applying admin.0002_logentry_remove_auto_add... OK
      Applying contenttypes.0002_remove_content_type_name... OK
      Applying auth.0002_alter_permission_name_max_length... OK
      Applying auth.0003_alter_user_email_max_length... OK
      Applying auth.0004_alter_user_username_opts... OK
      Applying auth.0005_alter_user_last_login_null... OK
      Applying auth.0006_require_contenttypes_0002... OK
      Applying auth.0007_alter_validators_add_error_messages... OK
      Applying auth.0008_alter_user_username_max_length... OK
      Applying courses.0001_initial... OK
      Applying sessions.0001_initial... OK
    
    

    这表示项目的模型已经同步到数据库中了。

    将模型注册到 admin网站


    我们将课程模型添加到 admin网站中。编辑 courses 应用的 admin.py 文件并添加以下代码:

    from django.contrib import admin
    
    from .models import Subject, Course, Module
    
    
    # Register your models here.
    @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]
       
    

    课程应用的模型现在已经注册到 admin网站了。我们使用 admin.register() 装饰器代替 admin.site.register() 函数,两者提供相同的功能。

    为模型提供初始数据


    有时候我们可能希望使用硬编码数据预填充数据库。 这样项目启动时可以自动包含初始数据,从而不必手动添加。 Django 提供一种将数据库中的数据加载并转储到 fixture 文件中的简单方法 。

    Django 支持 JSON、XML 或 YAML 格式的 fixture 。我们将为项目创建包含初始 Subject 对象的 fixture 。

    首先,使用以下命令创建一个超级管理员:

    python manage.py createsuperuser
    

    然后使用以下命令运行开发服务器:

    python manage.py runserver
    

    现在,在浏览器中打开 http://127.0.0.1:8000/admin/courses/subject/ 。使用 admin网站创建几个主题,主题列表显示的页面看起来是这样的:

    subject_data.png

    在 shell 中运行以下命令:

    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 manage.py dumpdata —help 获取 dumpdata 参数的更多信息。

    使用以下命令将转存数据保存到 courses 应用的 fixtures/ 目录:

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

    使用 admin 网站删除创建的 subject 数据。然后使用以下命令将 fixture 加载到数据库:

    python manage.py loaddata subjects.json
    

    fixtures 中的所有 Subject 对象都已加载到了数据库中。

    默认情况下,Django 搜索每个应用 fixtures 目录中的文件,但是可以为 loaddata 命令指定具体目录。还可以设置 FIXTURES_DIRS 告诉 Django 额外的路径。

    注意:

    Fixtures 不仅可以用于初始化数据,还可以用于为应用提供示例数据或测试数据。

    阅读 https://docs.djangoproject.com/en/1.11/topics/testing/tools/#topics-testing-fixtures 可以了解如何使用 fixtures 进行测试。

    如果希望在数据迁移时加载 fixture ,请查看 Django 关于数据迁移的文档。 我们在第九章中使用自定义迁移在更改模型后迁移存在的数据,https://docs.djangoproject.com/en/1.11/topics/migrations/#data-migrations 包含迁移数据的文档。

    为不同内容创建模型


    我们将为课程模块添加不同类型的内容,比如:文本、图片、文件、视频。我们需要一个多类数据模型存储不同内容。在第六章我们学习了使用通用关系创建外键(可以指定任意模型对象)。我们将创建一个 Content 模型来表示模块内容并定义一个连接各种类型内容的关系。

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

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

    然后,在文件的末尾添加以下代码:

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

    这是 Content 模型。一个模块包括多种内容,因此了定义一个指向 Module 模型的外键,我们还建立了通用关系来连接表示不同类型内容的不同模型的对象。我们需要三个字段设置通用关系,在 Content 模型中,这三个字段是:

    • content_type :指向 ContentType 模型的 ForeignKey ;

    • object_id :保存相关对象主键的 PositiveIntegerField ;

    • item :结合前面两个字段指向相关对象的 GenericForeignKey ;

    只有 content_type 和 object_id 字段在数据库中有对应的列。item 字段可以直接获取或者设置相关对象,它的功能基于前面两个字段。

    我们将为每类内容使用不同的模型,不同内容模型包括几个通用字段,它们的区别在于存储的实际内容不同。

    使用模型继承


    Django 支持模型继承,它的工作方式与 Python 的标准类继承相似。Django 为模型继承提供下面三种选项:

    • 抽象模型:通用信息存储到多个模型中时使用。抽象模型不会创建数据库表。

    • 多表模型继承:继承中的每个模型都可以看做是一个完整的模型。每个模型都会创建一个数据库表。

    • 代理模型:更改模型的行为时使用,例如,包含额外的方法、更改默认管理器或者使用不同的 meta 选项,代理模型不创建数据库表。

    我们来详细了解一下。

    抽象模型


    抽象模型是一个基类,我们可以在抽象模型中定义所有子模型都包含的字段。Django 不为抽象模型创建任何数据库表。它将为每个子模型创建数据库表,数据库表中的字段包括从抽象模型继承的字段和子模型定义的字段。

    我们需要在模型的 Meta 类中设置 abstract=True 来标记抽象模型。 Django 将其识别为抽象模型,不为它生成数据库表。创建子模型时,只需要继承抽象模型,下面的例子是一个抽象 Content 模型和一个 Text 子模型:

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

    这种情况下,Django将只为 Text 模型创建一个数据库表,这个数据库表包括 title、created 和 body 字段。

    多表模型继承


    在多表继承中每个模型对应一个数据库表,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 字段,并为每个模型创建一个数据库表。

    代理模型


    代理模型用于更改模型的行为,例如,添加额外的方法或设置不同的 meta 选项。两个模型都操作源模型的数据库表。在模型的 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 created_delta(self):
            return timezone.now() - self.created
    

    这里,我们定义了一个 OrderedContent 模型,它是 BaseContent 模型的代理模型。这个模型为 QuerySets 提供了默认排序以及额外的 created_delta() 方法。BaseContent 模型和 OrderedContent 模型操作相同的数据库表,两个模型的 ORM 都可以访问对象。

    创建内容模型


    courses 应用的 Content 模型包含一个通用关系(用于连接不同类型的内容)。我们将为不同内容的创建不同的模型。所有内容模型将具有一些相同字段以及存储自定义数据的额外字段。我们将创建一个抽象模型来为所有内容模型提供相同的字段。

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

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

    代码定义了 ItemBase 抽象模型。模型 Meta 类中设置了 abstract = True。 在这个模型中我们定义了 owner、title 、created 和 updated 字段。所有类型的内容都将使用这几个字段。 owner 字段保存创建内容的用户。由于这些字段是在抽象模型中定义的,我们需要为每个子模型自定不同的related_name,Django 可以在 related_name 属性中使用 %(class)s 表示 model 类的名称。这样将为每个子模型自动生成 related_name 。由于我们使用 '%(class)s_related' 作为 related_name ,子模型的反向关系将分别是 text_related,file_related,image_related 和 video_related。

    我们已经定义了四种不同内容的模型,它们都继承 ItemBase 抽象模型,分别是:

    • Text:保存文本内容;

    • File:保存文件,比如 PDF;

    • Image:保存图片文件;

    • Video:保存视频。我们使用URLField 字段提供视频的 URL 来嵌入视频。

    每个子模型除了自己的字段还包括 ItemBase 中定义的字段。数据库将分别为 Text、File、Image、Video 创建数据库表。ItemBase 模型由于是抽象模型,没有对应的数据库表。

    编辑我们前面创建的 Content 模型,将它的 content_type 字段修改为:

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

    我们为 ContentType 对象设置了 limit_choices_to 参数来使用通过关系。我们使用 model__in 字段查询来过滤 model 属性为 ‘text'、’video‘、’image‘、’file‘ 的对象。

    我们来添加新的模型,运行以下命令:

    python manage.py makemigrations
    

    你应该可以看到下面的输出:

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

    然后,运行以下命令应用迁移文件:

    python manage.py migrate
    

    应该可以看到这样的输出:

    Operations to perform:
      Apply all migrations: admin, auth, contenttypes, courses, sessions
    Running migrations:
      Applying courses.0002_content_file_image_text_video... OK
    

    我们已经创建了可以用于多种类型内容的课程模块。然而,模型中还少一些东西。课程模块和内容应该遵循一定的顺序,我们需要一个实现排序的字段。

    创建自定义模型字段


    Django 内置完备的创建模型所用的字段集合。但是,我们还可以创建自己的模型字段来存储自定义数据或者更改已经存在的字段的行为。

    我们需要一个字段指定对象的顺序。如果使用 Django 提供的字段,你很可能会想到为模型添加 PositiveIntegerField 字段,这是一个很好的起点。我们可以创建继承 PositiveIntegerField 的自定义字段来提供额外的行为。

    我们要创建的顺序字段要有这两个相关功能:

    • 如果没有指定顺序,则自动配置顺序。如果存储对象没有提供顺序,字段应该基于上一个存在的对象自动配置序号,如果存在顺序分别为 1 和 2 的两个对象,当保存第 3 个对象时,如果没有指定顺序,应该自动将第 3 个对象的顺序配置为 3。

    • 相对于其它字段对对象进行排序。课程模块按照所属的课程进行排序,模块内容按照所属的模块进行排序。

    在 courses 应用目录下新建 fields.py 的文件并添加以下代码:

    from django.core.exceptions import ObjectDoesNotExist
    from django.db import models
    
    
    class OrderField(models.PositiveIntegerField):
        def __init__(self, for_fields=None, *args, **kwargs):
            self.for_fields = for_fields
            super(OrderField, self).__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(OrderField, self).pre_save(model_instance, add)
    
    

    这是我们自定义的 OrderField , 它继承 Django 提供的 PositiveIntegerField 字段。 OrderField 字段接受可选的 for_field 参数(用于指定排序使用的字段)。

    OrderField 重写了 PositiveIntegerField 的 pre_save() 方法,该方法在保存到数据库之前执行。这个方法实现了以下动作:

    1. 检查模型实例中该字段是否已经赋值,使用模型设置字段名称的属性 self.attrname 进行检查。如果属性的值不为 None,我们将按照下面的方法计算顺序:

      1. 创建一个获取字段模型所有对象的 Queryset ,这里通过 self.model 获得字段所属的模型类;
      2. 如果定义了 for_fields 参数,通过 for_fields 定义的字段的当前值对 QuerySet 进行过滤。这样,我们可以根据对应字段计算排序;
      3. 使用 last_item = qs.latest(self.attname) 从数据库中获取序号最大的对象。如果没有找到对象,则假设这个对象是第一个,将其序号配置为 0;
      4. 如果找到了对象,取序号最大值加 1;
      5. 使用 setattr() 为模型实例该字段配置计算的序号并返回;
    2. 如果模型实例中该字段已经赋值,则不进行任何操作。

    注意:

    创建自定义模型字段时,要确保字段通用。避免基于特定模型或字段的硬编码。字段应该可以在任何模型中使用。

    我们可以在 https://docs.djangoproject.com/en/1.11/howto/custom-model-fields/ 找到更多关于自定义模型字段的信息。

    我们来为模型添加新的字段,编辑 courses 应用的 models.py 文件并添加以下代码:

    from .fields import OrderField
    

    然后,向 Module 模型添加 OrderField 字段:

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

    将新字段命名为 order,并通过设置 for_fields=['course'] 指定该字段根据课程进行排序;这意味着新的模块的序号为相同课程对象已有模块最大序号加 1 。现在,我们可以编辑 Module 模型的 __str__方法来使用排序:

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

    模块内容也需要排序,在 Content 模型中添加 OrderField 字段:

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

    这次使用 module 字段进行排序。最后为模型添加排序方式。在 Module 和 Content 模型添加下面的 Meta 类:

    class Meta:
        ordering = ['order']
    

    现在 Module 和 Content 模型看起来是这样的:

    class Module(models.Model):
        course = models.ForeignKey(Course, related_name='modules')
        title = models.CharField(max_length=200)
        description = models.TextField(blank=True)
        order = OrderField(blank=True, for_fields=['course'])
    
        def __str__(self):
            return '{}. {}'.format(self.order, self.title)
    
        class Meta:
            ordering = ['order']
    
    
    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']
    

    下面创建新的模型迁移来增加新的排序字段,打开 shell 运行以下字段:

    python manage.py makemigrations
    

    你将会看到下面的输出:

    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: 
    

    由于为存在的模型添加了新的字段,我们需要为数据库中已有的记录提供默认值。如果字段设置了 null = True,它将接收空值,Django 迁移时将不会询问默认值。我们可以指定默认值或者取消迁移,并在 models.py 文件中为 order 属性添加一个 default 属性。

    输入 1 并回车为存在的记录提供默认值,你将看到下面的输出:

    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 设置已经存在记录的值并回车, Django 还需要为 Module 模型设置默认值。选择选项 1 并输入 0 作为默认值。最后,你将看到类似下面的输出:

    Migrations for 'courses':
          courses/migrations/0003_auto_20180227_0314.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
    

    输出下面的内容表示迁移成功:

    Operations to perform:
      Apply all migrations: admin, auth, contenttypes, courses, sessions
    Running migrations:
      Applying courses.0003_auto_20180227_0314... OK
    

    我们来测试一下新的字段,使用python manage.py shell 打开 shell 并创建一个新的课程:

    In [1]: from django.contrib.auth.models import User
    
    In [2]: from courses.models import Subject, Course, Module
    
    In [3]: user = User.objects.latest('id')
    
    In [4]: subject = Subject.objects.latest('id')
    
    In [5]: c1 = Course.objects.create(subject=subject,owner = user,title='Course 1'
       ...: ,slug='course1')
    

    我们已经在数据库中创建了一个课程,现在,我们来为课程添加模块并查看模块如何自动计算排序,我们创建一个初始模块并检查他的排序:

    In [10]: m1 = Module.objects.create(course=c1,title='Module 1')
    In [12]: m1.order
    Out[12]: 0
    

    因为这是该课程的第一个 Module 对象,OrderField 将值设为 0 ,现在,我们为同一个课程创建第二个模块:

    In [13]: m2 = Module.objects.create(course=c1,title='Module 2')
    In [14]: m2.order
    Out[14]: 1
    

    OrderField 将已有对象的最大序号加 1 得到下一个序号。我们通过设置固定值创建第三个模块:

    In [15]: m3 = Module.objects.create(course=c1,title='Module 3',order=5)
    
    In [16]: m3.order
    Out[16]: 5
    

    如果设置一个自定义序号,OrderField 字段直接使用给定的序号。

    我们来添加第四个模块:

    In [17]: m4 = Module.objects.create(course=c1,title='Module 4')
    In [18]: m4.order
    Out[18]: 6
    

    这个模块的序号是自动设置的。 OrderField 不能保证序号值是连续的。它基于已有的序号的最大值设置下一个序号值。

    我们来创建第二个课程并为这个课程添加一个模块:

    In [19]: c2 = Course.objects.create(subject=subject,title='Course 2',slug='cours
        ...: e2',owner=user)
    
    In [20]: m5 = Module.objects.create(course=c2,title='Module 1')
    
    In [21]: m5.order
    Out[21]: 0
    

    计算模块顺序时,字段仅考虑同一课程中存在的模块,由于这是第二个课程的第一个模块,序号值为 0 。这是由于 Module 模型的 order 字段指定了 for_fields=['course']

    祝贺你,你已经成功的创建了第一个自定义模型字段。

    创建内容管理系统


    我们已经创建了可以存储多种数据的模型。现在需要创建一个内容管理系统(CMS)。CMS 将帮助老师创建课程并且管理内容。我们需要提供以下功能:

    • 登录 CMS;
    • 列出老师创建的课程;
    • 创建、编辑、删除课程;
    • 向课程添加模块以及对模块进行重新排序;
    • 向每个模块添加不同类型的内容以及对内容进行重新排序。

    添加权限系统


    我们将在这个平台上使用 Django 的权限框架。老师和学生都是 Django User 模型的一个实例。这样,他们都可以使用 django.contrib.auth 的权限视图进行登录。

    编辑 educa 项目的 urls.py 文件,在 URL 模式中加入 Django 权限框架的 login 和 logout 视图:

    from django.conf.urls import url
    from django.contrib import admin
    from django.contrib.auth import views as auth_views
    
    urlpatterns = [
        url(r'^accounts/login/$', auth_views.LoginView.as_view(), name='login'),
        url(r'^accounts/logout/$', auth_views.LogoutView.as_view(), name='logout'),
        url(r'^admin/', admin.site.urls), ]
    

    笔者注:

    这里使用 LoginView、LogoutView 类视图代替即将废弃的 login、logout 函数。

    创建权限模板

    在 courses 应用目录下创建下面的文件结构:

    CH10-1.png

    创建权限模板之前,我们需要为项目准备基础模板,编辑 base.html 模板文件并添加下面的内容:

    {% load static %}
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8"/>
        <title>{% block title %}Educa{% endblock %}</title>
        <link href="{% static "css/base.css" %}" rel="stylesheet">
    </head>
    <body>
    <div id="header">
        <a href="/" class="logo">Educa</a>
        <ul class="menu">
            {% if request.user.is_authenticated %}
                <li><a href="{% url "logout" %}">Sign out</a></li>
            {% else %}
                <li><a href="{% url "login" %}">Sign in</a></li>
            {% endif %}
        </ul>
    </div>
    <div id="content">
        {% block content %}
        {% endblock %}
    </div>
    
    <script src="https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
    {#<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>#}
        $(document).ready(function () {
            {% block domready %}
            {% endblock %}
        });
    </script>
    </body>
    </html>
    

    笔者注:

    这里使用 <script src="https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script> 代替 <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>,解决无法访问 google 的问题。

    这是其它模板使用的基础模板,在这个模板中,我们定义了以下模块:

    • title:其它模板可以使用这个模块添加自定义标题;

    • content: 内容主模块,所有扩展基础模板的模板都应在这一模块中添加内容。

    • domready: 位于 jQuery 的 $.document.ready()函数中,DOM 加载完成后可以执行这些代码。

    模板使用的 CSS 文件在本章 courses 应用的 static/ 目录下,你可以将 static/目录拷贝到你的项目中。

    笔者注:

    static 目录下 css/base.css 的第一行为@import url(http://fonts.googleapis.com/css?family=Roboto:500,300,400);由于国内无法登录google,这样可能会导致 css 文件无法加载,因此暂时将本行注释掉。这样可能会导致项目页面字体与书中页面效果字体有些差异。

    编辑 registration/login.html 模板并添加以下代码:

    {% extends "base.html" %}
    
    {% block title %}Log-in{% endblock %}
    
    {% block content %}
        <h1>Log-in</h1>
        <div class="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 %}
            <div class="login-form">
                <form action="{% url 'login' %}" method="post">
                    {{ form.as_p }}
                    {% csrf_token %}
                    <input type="hidden" name="next" value="{{ next }}"/>
                    <p><input type="submit" value="Log-in"></p>
                </form>
            </div>
        </div>
    {% endblock %}
    

    这是一个标准的 Django login 视图模板。编辑 registration/logged_out.html 模板并添加以下代码:

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

    这是用户退出登录后的模板。使用 python manage.py runserver 运行开发服务器,在浏览器中打开 http://127.0.0.1:8000/accounts/login/ ,应该可以看到这样的登录页面:

    login.png

    创建类视图


    我们将实现创建、编辑和删除课程的视图。这里我们将使用类视图。编辑 courses 应用的 views.py 文件并添加以下代码:

    from django.views.generic.list import ListView
    
    from .models import Course
    
    
    # Create your views here.
    class ManageCourseListView(ListView):
        model = Course
        template_name = 'courses/manage/course/list.html'
    
        def get_queryset(self):
            qs = super(ManageCourseListView, self).get_queryset()
            return qs.filter(owner=self.request.user)
    
    

    这是 ManageCourseListView 视图,它继承 Django 通用 ListView 视图。我们重写视图的 get_queryset() 方法来获得当前用户创建的课程。为了防止用户编辑、更改或者删除别人的课程,我们还需要重写创建、更改和删除视图的 get_queryset() 方法。如果需要为几个类函数提供同一个行为,推荐使用 mixins。

    使用Mixin


    Mixins 是一些特殊类型的多重继承。我们可以用它来提供通用的函数(添加到其它mixin中)来定义类的行为,通常在两种情况下使用 mixins :

    • 希望为一个类提供多个可选特性;

    • 希望在几个类中使用一个特定特性;

    我们可以在 https://docs.djangoproject.com/en/1.11/topics/class-based-views/mixins/ 了解更多类视图如何使用mixin的内容。

    Django 内置一些 mixins 来帮助类函数实现额外功能,详见 https://docs.djangoproject.com/en/1.11/ref/class-based-views/mixins/

    我们将创建一个mixin 类来包含通用行为并在课程视图中使用。编辑 courses 应用的 views.py 文件进行如下修改:

    from django.core.urlresolvers import reverse_lazy
    from django.views.generic.edit import CreateView, UpdateView, DeleteView
    from django.views.generic.list import ListView
    
    from .models import Course
    
    
    class OwnerMixin(object):
        def get_queryset(self):
            qs = super(OwnerMixin, self).get_queryset()
            return qs.filter(owner=self.request.user)
    
    
    class OwnerEditMixin(object):
        def form_valid(self, form):
            form.instance.owner = self.request.user
            return super(OwnerEditMixin, self).form_valid(form)
    
    
    class OwnerCourseMixin(OwnerMixin):
        model = Course
    
    
    class OwnerCourseEditMixin(OwnerCourseMixin, OwnerEditMixin):
        fields = ['subject', 'title', 'slug', 'overview']
        success_url = reverse_lazy('manage_course_list')
        template_name = 'courses/manage/course/form.html'
    
    
    class ManageCourseListView(OwnerCourseMixin, ListView):
        template_name = 'courses/manage/course/list.html'
    
    
    class CourseCreateView(OwnerCourseEditMixin, CreateView):
        pass
    
    
    class CourseUpdateView(OwnerCourseEditMixin, UpdateView):
        pass
    
    
    class CourseDeleteView(OwnerCourseMixin, DeleteView):
        template_name = 'courses/manage/course/delete.html'
        success_url = reverse_lazy('manage_course_list')
    
    

    代码中,我们创建了 OwnerMixin 和 OwnerEditMixin ,这些 mixins 将与 Django 的 ListView、CreateView、UpdateView、DeleteView 组合使用。OwnerMixin 实现下面的方法:

    • get_queryset() : 视图使用这个方法获得基本 QuerySet 。OwnerMixin 将这个方法重写为通过 owner 属性过滤属于当前用户(request.user) 的 QuerySet ;

    OwnerEditMixin实现以下方法:

    • form_valid() :继承 ModelFormMixin 的视图(使用 forms 或者 modelforms 的 CreateView、UpdateView 等视图)使用这个方法,表单有效时将执行 form_valid() 。这个方法的默认行为是保存实例( modelforms )并将用户重定向到 successful_url 。我们重写这个方法为要保存对象的 owner 属性自动设置为当前用户。这样,保存对象时我们可以自动设置对象的用户。

    OwnerMixin 类可以用于与任何包含 owner 属性的模型进行交互的视图。

    我们还定义了继承 OwnerMixin 的 OwnerCourseMixin 子类,并为子类提供了以下属性:

    • model: QuerySet 使用的模型,可以供所有视图使用。

    OwnerCourseEditMixin 定义了以下属性:

    • fields:CreateView 和 UpdateView 创建模型表单的模型字段。
    • success_url:CreateView 和 UpdateView 的表单成功提交时重定向到的 URL 。这里我们使用稍后创建的 manage_course_list 视图。

    最后创建了 OwnerCourseMixin 的子视图:

    • MangeCourseListView: 用户创建的课程列表。它继承 OwnerCourseMixin 和 ListView。
    • CourseCreateView:使用 modelform 创建新的 Course 对象,使用 OwnerCourseEditMixin 定义的字段来创建模型表单,并且继承 CreateView 。
    • CourseUpdateView:帮助用户编辑存在的 Course 对象。继承 OwnerCourseEditMixin 和 UpdateView 。
    • CourseDeleteView:继承 OwnerCourseEditMixin 和通用的 DeleteView。 定义对象成功删除后重定向的 successful_url 。

    组和权限


    我们已经创建了管理课程的基本视图,到目前为止,任何用户都可以访问这些视图。但是我们希望只有老师才能创建和管理课程。Django 权限框架内置权限系统来为用户和群组配置权限。我们将为老师创建一个组并为他们配置创建、更新和删除课程的权限。

    使用 python manage.py runserver 运行开发服务器并在浏览器中打开 http://127.0.0.1:8000/admin/auth/group/add/ 并创建一个新的 Group 对象。添加 Instructors 并选择 courses 应用中除了 Subject 模型相关权限之外的所有权限:

    permission.png

    你可以看到,每个模型有 can add,can change,can delete 三种不同权限。为这个组选择完权限后点击 Save 按钮。

    Django 可以为模型创建权限,用户也可以创建自定义权限。https://docs.djangoproject.com/en/1.11/topics/auth/customizing/#custom-permissions 中有如何添加自定义权限的介绍。

    打开 http://127.0.0.1:8000/admin/auth/user/add/ 并创建新用户,编辑用户并添加 Instructors 组:

    CH10-2.png

    用户继承所属群组的权限,我们还也可以使用 admin 网站为单个用户设置个人权限。is_superuser 设置为 True 的用户自动具有所有权限。

    限制访问类视图


    我们将设置类视图的访问权限,只有有权限的用户才能添加、更改、删除 Course 对象。权限框架内置 permission_required 装饰器来限制视图的访问。这里我们使用 Django 提供的权限 mixin。

    笔者注:

    Django1.9 将包含类视图的权限 mixin,Django1.8不包含这些 mixin。原文使用的是 Django1.8 ,这里使用的是 Django 1.11。因此,原文使用第三方模块 django-braces 的权限 mixin,这里则直接导入。

    我们将使用下面两个mixins 来限制对视图的访问:

    • LoginRequiredMixin :实现 login_required 装饰器的功能。
    • PermissionRequiredMixin: 将访问视图的权限限制为具有特定权限的用户。超级用户具有所有权限。

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

    from django.contrib.auth.mixins import LoginRequiredMixin, \
        PermissionRequiredMixin
    

    OwnerCourseMixin 继承 LoginRequiredMixin:

    class OwnerCourseMixin(OwnerMixin, LoginRequiredMixin):
        model = Course
    

    然后,为创建、更新、删除视图增加 permission_required 属性:

    class CourseCreateView(PermissionRequiredMixin, OwnerCourseEditMixin,
                           CreateView):
        permission_required = 'courses.add_course'
    
    
    class CourseUpdateView(PermissionRequiredMixin, OwnerCourseEditMixin,
                           UpdateView):
        permission_required = 'courses.change_course'
    
    
    class CourseDeleteView(PermissionRequiredMixin, OwnerCourseMixin, DeleteView):
        template_name = 'courses/manage/course/delete.html'
        success_url = reverse_lazy('manage_course_list')
        permission_required = 'courses.delete_course'
    

    笔者注:

    permission_required 的格式为 "appname:action_modelname"的小写形式。

    PermissionRequiredMixin 将检查访问视图的用户是否具有 permission_required 设置的权限。我们的视图现在只有有权限的用户才能访问了。

    我们来为这些视图创建 URLs 。在 courses 应用下新建 urls.py 的文件。添加以下代码:

    from django.conf.urls import url
    
    from . import views
    
    urlpatterns = [
        url(r'^mine/$', views.ManageCourseListView.as_view(),
                       name='manage_course_list'),
        url(r'^create/$', views.CourseCreateView.as_view(), name='course_create'),
        url(r'^(?P<pk>\d+)/edit/$', views.CourseUpdateView.as_view(),
            name='course_edit'),
        url(r'^(?P<pk>\d+)/delete/$', views.CourseDeleteView.as_view(),
            name='course_delete'), ]
    

    这些是列表、增加、编辑、删除课程视图的 URL 模式。编辑 educa 项目的 urls.py 文件写入 courses 应用的 URL 模式:

    from django.conf.urls import url, include
    from django.contrib import admin
    from django.contrib.auth import views as auth_views
    
    urlpatterns = [url(r'^accounts/login/$', auth_views.login, name='login'),
                   url(r'^accounts/logout/$', auth_views.logout, name='logout'),
                   url(r'^admin/', admin.site.urls),
                   url(r'course/', include('courses.urls'))]
    

    我们需要为这些视图创建模板。在 courses 应用的 templates 目录下创建这样的文件结构:


    course_structure.png

    编辑 courses/manage/course/list.html 模板并添加以下代码:

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

    这是 ManageCourseListView 视图的模板。列出当前用户创建的课程,并包含编辑、删除每个课程的链接,以及创建新课程的链接。

    使用 python manage.py runserver 运行开发服务器,在浏览器中打开 http://127.0.0.1:8000/course/mine/ ,由于该视图需要登录,因此页面会跳转到 http://127.0.0.1:8000/accounts/login/?next=/course/mine/ ,使用 Instructors 组的用户登录,登录后会重定向到 http://127.0.0.1:8000/course/mine/ ,你将看到下面的页面:

    course_list.png

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

    我们来新建创建及修改课程的表单所用的模板,编辑 courses/manage/course/form.html 模板并添加以下代码:

    {% 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>
        <div class="module">
            <h2>Course info</h2>
            <form action="." method="post">
                {{ form.as_p }}
                {% csrf_token %}
                <p><input type="submit" value="Save course"></p>
            </form>
        </div>
    {% endblock %}
    
    

    form.html 模板用于 CourseCreateView 和 CourseUpdateView 视图。在这个模板中,我们检查内容中是否存在 object 变量。如果存在 object 对象,模板将用来更新已存在的课程,使用对象的名称作为页面标题,否则将新建 Course 对象。

    create_course.png

    在浏览器中打开 http://127.0.0.1:8000/course/mine/ 并点击 Create new course 按钮,我们将看到下面的页面:

    edit_course.png

    然后,点击刚刚创建的课程的 Edit 链接,你将会再次看到表单,但是这次是编辑已经存在的 Course 对象,而不是新建一个。

    最后,编辑 courses/manage/course/delete.html 模板并添加以下代码:

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

    这是 CourseDeleteView 的视图。这个视图继承 Django 提供的 DeleteView,使用这个视图删除对象时首先需要用户进行确认。

    打开浏览器并点击课程的 Delete 链接,你将会看到下面的确认页面:

    delete_course.png

    点击 CONFIRM 按钮,课程将会被删除,用户被重定向到课程列表页面。

    Instructors 现在可以创建、编辑、删除课程了,下一步,我们需要为他们提供课程管理系统来为课程添加模块和内容,我们将从管理课程模块开始。

    使用 formsets


    Django 内置同一个页面中管理多个表单的抽象层。多个表单被称为 formset 。Formset 管理 Form 或者 ModelForm 创建的多个实例。所有表单都将同时提交,formset 管理要展示的初始表单数量、可以提交的最大表单数量以对所有表单进行验证。

    Formsets 包含 is_valid() 方法来同时验证所有表单。你可以为表单提供初始数据并设置可以展示多少个额外的空表单。

    我们可以从 https://docs.djangoproject.com/en/1.11/topics/forms/formsets/ 了解 formset,从 https://docs.djangoproject.com/en/1.11/topics/forms/modelforms/#model-formsets 了解模型 formset 。

    管理课程模块


    由于一个课程可以分为多个模块,我们这里可以使用 formset 。在 courses 应用目录下新建 forms.py 文件并添加以下代码:

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

    这是 ModuleFormSet 。我们使用 Django 提供的 inlineformset_factory() 方法创建这个 formset。内联 formset 是在 formset 之上的抽象,以便与相关对象交互。这个函数帮助我们自动创建一个 Course 对象的相关 Module 对象。

    我们使用以下的参数创建 formset :

    • fields:formset 中每个 form 包含的字段 ;
    • extra:设置 formset 中要展示的额外的空表单的数量;
    • can_delete:如果将其设置为 True ,Django将为每个表单增加一个渲染为选择框的布尔字段,它允许用户标记希望删除的对象。

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

    from django.shortcuts import redirect, get_object_or_404
    from django.views.generic.base import TemplateResponseMixin, View
    from .forms import ModuleFormSet
    
    
    class CourseModuleUpdateView(TemplateResponseMixin, View):
        template_name = 'courses/manage/module/formset.html'
        course = None
    
        def get_formset(self, data=None):
            return ModuleFormSet(instance=self.course, data=data)
         
            
       def dispatch(self, request, *args, **kwargs):
            pk = kwargs.get('pk', None)
            self.course = get_object_or_404(Course, id=pk, owner=request.user)
            return super(CourseModuleUpdateView, self).dispatch(request, *args,
                                                                **kwargs)
    
        def get(self, request, *args, **kwargs):
            formset = self.get_formset()
            return self.render_to_response(
                {'course': self.course, 'formset': formset})
    
        def post(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 视图用来实现为特定视图添加、更新和删除模块,这个视图继承这些 mixin 和视图:

    • TemplateResponseMixin:负责渲染模板和返回 HTTP 响应的 mixin 。需要设置表示渲染的模板的 template_name 属性,并提供 render_to_response() 方法来传输内容并渲染模板。
    • View : Django 提供的基类视图。

    在这个视图中,我们实现了以下方法:

    • get_formset() :这个方法用来避免创建 formset 时重复编码。这里使用可选数据为给定课程创建了一个 ModuleFormSet 对象。

    • dispatch():这个方法由 View 类提供。它接收一个 HTTP 请求以及请求参数并运行 HTTP 方法的小写形式的方法: GET 请求将运行 get() 方法,POST 请求将运行 post() 方法。在这个方法中,我们使用快捷函数 get_object_or_404() 根据给定的 id 获得当前用户的 Course 对象。由于 GET 和 POST 请求都需要获取该课程,因此我们将其放在 dispatch() 方法中。我们将其保存在视图的 course 对象中以便其他方法进行访问。

      笔者注:

      原文的代码是:

      def dispatch(self, request, pk):    
          self.course = get_object_or_404(Course, id=pk, owner=request.user)    
          return super(CourseModuleUpdateView, self).dispatch(request, pk)
      

      由于 django 1.11 中类视图 View 的dispatch 的输入参数为request, args,*kwargs,因此进行了修改。

    • get():响应 GET 请求,创建空的 ModuleFormSet formset ,然后使用 TemplateResponseMixin 提供的 render_to_response() 方法用创建的 formset 和 Course 对象渲染模板。

    • post():响应 POST 请求,在这个方法中,我们实现了下面三个动作:

      1. 使用提交的数据创建 ModuleFormSet 实例;
      2. 执行 formset 的 is_valid() 方法验证所有表单;
      3. 如果 formset 有效,调用 save() 方法进行保存。这时,增加、更新、标记删除等动作都将应用到数据库,然后将用户重定向到 manage_course_list 对应的 URL 。如果 formset 无效,将渲染模板展示错误。

    编辑 courses 应用的 urls.py 文件并添加下面的 URL 模式:

    url(r'^(?P<pk>\d+)/module/$', views.CourseModuleUpdateView.as_view(),
        name='course_module_update'), 
    

    在 courses/manage/ 模板目录下新建名为 module 的新目录,新建 courses/manage/course/module/formset.html 模板并添加以下代码:

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

    这个模板中创建 formset 所在的 <form> 元素,并使用 {{ formset.management_form }} 变量设置了 formset 的管理表单。管理表单内置控制表单初始值、最小数量、最大数量等信息的隐藏字段。我们可以看到,创建 formset 非常简单。

    编辑 courses/manage/course/list.html 模板并在编辑、删除链接之后添加 course_module_update URL链接:

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

    我们已经引入了编辑课程模块的链接,在浏览器中打开 http://127.0.0.1:8000/course/mine/ 并点击一个课程的 Edit modules 链接,我们将看到下面的 formset:

    edit_module.png

    formset 为课程中的每个 Module 对象内置一个表单,除此之外,由于 ModuleFormSet 设置了 extra=2 ,还包括了两个空的表单。保存 formset 时,Django 将包含两个额外字段来添加新模块。

    向课程模块添加内容


    现在,我们需要向课程模块添加内容,我们有四种类型的内容:文本、音频、图像和文件。我们可以考虑创建四个不同的视图,每个视图对应一个模型。但是这里我们将使用更通用的方法,创建一个视图来处理任何内容模型的创建和修改。

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

    from django.forms.models import modelform_factory
    from django.apps import apps
    from .models import Module, Content
    
    
    class ContentCreateUpdateView(TemplateResponseMixin, View):
        module = None
        model = None
        obj = None
        template_name = 'courses/manage/content/form.html'
    
        def get_model(self, model_name):
            if model_name in ['text', 'video', 'image', 'file']:
                return apps.get_model(app_label='courses', model_name=model_name)
            return None
    
        def get_form(self, model, *args, **kwargs):
            Form = modelform_factory(model, exclude=['owner', 'order', 'created',
                                                     'updated'])
            return Form(*args, **kwargs)
    
        def dispatch(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)
            if id:
                self.obj = get_object_or_404(self.model, id=id, owner=request.user)
            return super(ContentCreateUpdateView, self).dispatch(request, module_id,
                                                                 model_name, id)
    
    

    这是 ContentCreateUpdateView 视图的第一部分,我们可以用它创建和更新不同模型的内容,这个视图定义了以下方法:

    • get_model() :检查模型名称是否为内容模型:文件、音频、图像和文件。然后使用 Django apps 的 get_model 方法获得给定模型名称对应的实际模型,如果模型名称无效,则返回 None ;
    • get_form() :使用表单框架的 modelform_factory() 动态生成表单,由于要为 Text、Video、Image、File 模型创建表单,这里使用 exclude 参数排除表单不用显示的通用字段,从而自动包含其它字段。这样,不需要根据模型设置不同的字段。
    • dispatch():获得下面的 URL 参数并将参数以类属性的形式保存到相应的模块、模型和内容对象中。
      • module_id : 内容所属模块的 id;
      • model_name:要创建/更改的模型名称;
      • id:要更改的对象的 id ,创建对象时为 None。

    向 ContentCreateUpdateView 视图添加下面的 get() 和 post() 方法:

    def get(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})
    
    def post(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.user
            obj.save()
            if not id:
                # new content
                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 请求,我们为需要更新的 Text、Video、Image、File 模型创建表单,如果没有提供 id ,那么 self.obj 将为 None,这时将创建一个新的对象;

    • post():响应 POST 请求,创建模型表单并传入数据或者文件,然后验证模型表单。如果模型表单有效,使用 commitFalse 的 save() 方法将模型对象保存到 obj 变量中,但是不写入数据库,将 obj 变量的 owner 属性设置 request.user,然后将对象保存到数据库。我们还要检查 id,如果没有定义 id 则说明用户需要创建一个新的对象。如果这是一个新对象,我们将为给定模块创建新的 Content 对象并配置新的内容。

    编辑 courses 应用的 urls.py 文件并添加以下 URL 模式:

    url(
        r'^module/(?P<module_id>\d+)/content/(?P<model_name>\w+)/create/$',
        views.ContentCreateUpdateView.as_view(), 
        name='module_content_create'),
    url(
        r'^module/(?P<module_id>\d+)/content/(?P<model_name>\w+)/(?P<id>\d+)/$',
        views.ContentCreateUpdateView.as_view(),
        name='module_content_update'),
    

    新的 URL 模式为:

    • module_content_create:创建新的文本、音频、图像或者文件对象并添加到模块中;它包含 module_id 和 module_name 参数,第一个参数将新的内容对象连接到给定的模块,第二个参数指定表单对应的内容模型。

    • module_content_update:更新已经存在的文本、音频、图像或者文件对象,它包含 module_id 和 module_name 参数,并包括 id 参数来指定要更新的内容对象。

    在模板目录 courses/manage 目录下新建 content 的目录,新建 courses/manage/content/form.html 模板并添加以下代码:

    {% 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>
        <div class="module">
            <h2>Course info</h2>
            <form action="" method="post" enctype="multipart/form-data">
                {{ form.as_p }}
                {% csrf_token %}
                <p><input type="submit" value="Save content"></p>
            </form>
        </div>
    {% endblock %}
    

    这个模板用于 ContentCreateUpdateView 视图,模板中,我们检查内容中是否包含 object 变量,如果存在,则修改已经存在的对象,否则,我们将创建一个新的对象。

    在 HTML <form> 元素中设置 enctype="multipart/form-data“,以便于表单为 File 和 Image 模型上传内容。

    运行开发服务器,为已经存在的课程新建一个模块,并在浏览器中打开 http://127.0.0.1:8000/course/module/6/content/image/create/( 如果需要可以更改 URL 中的模块 id )。你将看到创建 Image 对象的表单:

    create_image.png

    先不要提交表单,因为我们还没有创建 module_content_list URL,稍后将创建这个 URL 。

    我们还需要一个删除内容的视图,编辑 courses 应用的 views.py 文件并添加以下代码 :

    class ContentDeleteView(View):
        def post(self, request, id):
            content = get_object_or_404(Content, id=id,
                                        module__course__owner=request.user)
            module = content.module
            content.item.delete()
            content.delete()
            return redirect('module_content_list', module.id)
    

    ContentDeleteView 视图通过给定的 id 获取 Content 对象,它将删除对应的的 文本、音频、图像或者文件对象,最终删除 Content 对象,并重定向到 module_content_list URL。

    编辑 courses 应用的 urls.py 文件并添加以下 URL 模式:

    url(r'^content/(?P<id>\d+)/delete/$', 
        views.ContentDeleteView.as_view(),
        name='module_content_delete'),
    

    现在,老师可以轻松的创建、更改和删除内容了。

    管理模块和内容


    我们已经创建了 新建、编辑和删除课程模块和内容的视图。现在,我们需要一个视图来展示一个课程的所有模块并为特定模块列出所有内容。

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

    class ModuleContentListView(TemplateResponseMixin, View):
        template_name = 'courses/manage/module/content_list.html'
    
        def get(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 视图,这个视图将获得当前用户特定 id 的 Module 对象,并使用 Module 对象渲染模板。

    编辑 courses 应用的 urls.py 文件并添加以下 URL 模式:

    url(r'^module/(?P<module_id>\d+)/$', 
        views.ModuleContentListView.as_view(),    
        name='module_content_list'), 
    

    在 templates/courses/manage/module/ 目录下新建名为 content_list.html 的模板,添加以下代码:

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

    这是展示某个课程所有模块和选中模块对应所有内容的模板。我们在边栏中遍历课程模块,还将遍历模块内容并访问 content.item 来获得对应的 Text、Video、Image、或 File 对象。此外还包括创建新 Text、Video、Image、或 File 的链接。

    我们希望知道每个 item 对象的类型,需要模型名称来创建编辑对象的 URL 。除此之外,还要根据 item 对象的类型在模板中使用不同的方式进行展示。我们可以通过访问对象的 _meta 属性从模型的 Meta 类中得到对象的模型。然而, Django 不允许在模板中访问下划线开头的变量,这样做是为了防止获取私有属性或者调用私有方法。我们可以通过自定义模板过滤器解决这个问题。

    在 courses 应用目录下创建下面的文件结构:

    custom tag.png

    编辑 course.py 模块并添加以下代码:

    from django import template
    
    register = template.Library()
    
    
    @register.filter
    def model_name(obj):
        try:
            return obj._meta.model_name
        except AttributeError:
            return None
    
    

    这是 model_name 模板过滤器。我们将使用 object|model_name 的形式来获得对象的模型名称。
    编辑 templates/courses/manage/module/content_list.html 模板并在{% extends %} 模板标签之后添加下面的内容:

    {% load course %}
    

    笔者注:

    需要重启开发服务器才能加载 course 标签。

    它将导入 course 模板标签。然后,找到下面的内容:

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

    将其替换为:

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

    现在,我们在模板中展示了内容模型并通过模型名称构建了编辑对象的链接,编辑 courses/manage/course/list.html 模板并添加跳转到 module_content_list URL 的链接:

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

    如果课程包含模块,用户可以通过新的链接访问课程第一个模块的内容。

    打开 http://127.0.0.1:8000/course/mine/,并点击至少包含一个模块的课程的 Manage contents 链接,你将会看到下面的页面:

    content_list_1.png

    点击左侧边栏的模块时,相应模块的内容将展示在主区域。模板还包括为展示的模块添加新的文本、音频、图像或者文件内容的链接。为一个模块添加不同类型的内容看一下结果。内容将在 Module contents 后面进行展示,如下图所示:

    content_list_2.png

    对模块和内容进行重新排序


    我们需要提供对课程模块和内容重新排序的简便方法。这里将使用 JavaScript drag-n-drop 组件实现拖动重新排序。当用户拖动完一个模块后,将加载异步请求(AJAX ) 来保存新的模块顺序。

    这里需要一个接收模型 id 新排序( JSON 编码)的视图,编辑 courses 应用的 views.py 文件,并添加以下代码:

    from braces.views import CsrfExemptMixin, JsonRequestResponseMixin
    
    
    class ModuleOrderView(CsrfExemptMixin, JsonRequestResponseMixin, View):
        def post(self, request):
            for id, 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'})
    

    笔者注:

    这里,我们需要安装 组和权限 一节中由于 Django 1.11 已经内置需要的 Mixin 而没有安装的 django-brace,我们通过以下命令安装 django-braces:

    pip install django-braces
    

    笔者注:

    由于视图中对数据库进行操作,不建议使用 CsrfExemptMixin ,建议在 Ajax post 请求中加入 csrf token。

    这是 ModuleOrderView 视图,我们使用 django-braces 的 mixins:

    • CsrfExemptMixin:避免检查 POST 请求的 CSRF 令牌。要在不生成 csrf_token 的情况下响应 AJAX POST 请求。

    • JsonRequestResponseMixin:以 JSON 格式解析请求数据并将响应序列化为 JSON 格式,使用 application/json 内容格式返回 HTTP 响应。

    我们可以创建一个类似的视图来对模块内容进行排序,在 views.py 文件中添加以下代码:

    class ContentOrderView(CsrfExemptMixin, JsonRequestResponseMixin, View):
        def post(self, request):
            for id, 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 模式:

    url(r'^module/order/$',views.ModuleOrderView.as_view(),
        name='module_order'),
    url(r'^content/order/$',views.ContentOrderView.as_view(),
        name='content_order'),
    

    最后,我们需要在模板中实现 drag-n-drop 功能。实现这个功能需要使用 jQuery UI 库, jQuery UI 库基于 jQuery 并提供一套用户界面交互、特效、小部件集合。我们将使用其中的 sortable 元素,首先,需要在基础模板中加载 jQuery UI ,打开 courses 应用 templates/ 目录下的 base.html 文件,加载 jQuery 后加载 jQuery UI :

    <script src="https://cdn.staticfile.org/jquery/2.1.4/jquery.min.js"></script>
    {#<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>#}
    <script src="{% static 'js/jquery-ui.min.js' %}"></script>
    

    笔者注:

    jQuery UI 了解及下载参考:http://www.runoob.com/jqueryui/jqueryui-download.html

    这里下载了 jquery-ui.min.js 文件并保存到 static/js/ 文件夹中。

    在 jQuery 框架之后加载了 jQuery UI 库。现在,编辑 courses/manage/module/content_list.html 模板并在模板尾部添加以下代码:

    {% block domready %}
        $('#modules').sortable({
            stop: function(event, ui) {
                modules_order = {};
                $('#modules').children().each(function(){
                    // update the order field
                    $(this).find('.order').text($(this).index() + 1);
                    // associate the module's id with its order
                    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) {
                contents_order = {};
                $('#module-contents').children().each(function(){
                    // associate the module's id with its order
                    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 %} 
    

    JavaScript 代码位于 {% block domready %} 块中,因此在 base.html 中定义的 jQuery 的 $(doucument).ready()事件中。这将保证加载页面后执行 JavaScript 代码。我们为边栏的模块列表定义一个 sortable 元素,为模块的内容列表定义另一个 sortable 元素。它们的工作方式相似,在这段代码中,我们完成以下任务:

    1. 首先,为 id 为 modules 的HTML 元素定义了一个 sortable 元素,请注意我们使用 #modules ,因为jQuery对选择器使用 CSS 符号。
    2. 为 stop 事件指定了函数,用户每次完成一个元素排序都会触发 stop 事件。
    3. 创建一个空的 modules_order 字典,这个字典的主键为模块的 id,值为每个模块配置的序号。
    4. 遍历 #module 子元素,重新计算每个模块的展示顺序并且获取其 data-id 属性,该属性包含模块 id 。我们将 id 、新的排序组成的键值对添加到 modules_order 字典中。
    5. 加载 content_order 的 AJAX POST 请求,在请求中包含 modules_order 的序列化 JSON 数据,对应的 ModuleOrderView 视图负责更新模块顺序。

    这里的 sortable 元素非常简单。回到浏览器并重新加载页面,现在,你可以点击并拖动模块和内容了,向下面的例子中这样对它们进行重新排序:

    CH10-3.png

    非常棒,现在可以对课程模块以及模块内容进行排序了。

    总结


    本章,我们学习了创建多种内容管理系统,使用了模型继承并创建了自定义模型字段,还使用了类视图和 mixin ,创建了 formset 以及管理不同类型内容的系统。

    下一章,我们将创建学生注册系统,将渲染不同种类的内容,学习如何使用 Django 缓存框架。

    相关文章

      网友评论

        本文标题:django by example 实践 educa 项目(一)

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