《Django By Example》第十章 上 中文 翻译 (个人学习,渣翻)

全文链接

第一章 创建一个blog应用
第二章 使用高级特性来增强你的blog
第三章 扩展你的blog应用
第四章上 创建一个社交网站
第四章下 创建一个社交网站
第五章 在你的网站中分享内容
第六章 跟踪用户动作
第七章 建立一个在线商店
第八章 管理付款和订单
第九章上 扩展你的商店
第九章下 扩展你的商店
第十章上 创建一个在线学习平台
第十章下 创建一个在线学习平台
第十一章 缓存内容
第十二章 构建一个API

书籍出处:https://www.packtpub.com/web-development/django-example
原作者:Antonio Melé

(译者注:翻译本章过程中几次想放弃,但是既然都到第十章了,怎么能放弃!)

第十章

创建一个在线学习平台(e-Learning Platform)

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

在这章中,你会学习以下操作:

  • 创建fixtures给你的模型
  • 使用模型继承
  • 创建定制模型字段
  • 使用基于类的视图和mixins
  • 构建formsets
  • 管理组合权限
  • 创建一个内容管理系统

创建一个在线学习平台

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

首先,创建一个虚拟环境给你的新项目并且激活它通过以下命令:

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

安装Django到你的虚拟环境中通过以下命令:

pip install Django==1.8.6

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

pip install Pillow==2.9.0

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

django-admin startproject educa

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

cd educa
django-admin startapp courses

编辑educa项目的settings.py文件并且添加coursesINSTALLED_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 (images)
            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

这些是最初的Subject,Course,以及Module模型。Course模型字段如下所示:

  • owner:创建这个课程的教师。
  • subject:这个课程属于的主题。这是一个ForeingnKey字段指向Subject模型。
  • title:课程标题。
  • slug:课程的slug。之后它将会被用在URLs中。
  • overview:这是一个TextFied列用来包含一个关于课程的概述。
  • created:课程被创建的日期和时间。它将会被Django自动设置当创建一个新的对象,因为auto_now_add=True

每一个课程都被划分为多个模块。因此,Module模型包含一个ForeignKey字段用来指向Course模型。

打开shell并且运行一下命令来给这个应用创建最初的迁移:

python manange.py makemigrations

你将会看到以下输出:

Migrations for 'courses':
     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引用模型已经同步到了数据库中。

注册模型到管理平台中

我们将要添加课程模型到管理平台中。编辑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]

课程应用的模型现在已经在管理平台中注册。我们使用@admin.register()装饰器替代了admin.site.register()方法。它们都提供了相同的功能。

提供最初数据给模型

有时候你可能想要预装你的数据库通过使用硬编码数据。这是很有用的,当自动包含最初数据在项目设置中用来替代手工去添加数据。Django自带一个简单的方法来加载以及转储数据库中的数据到字段中,这被称为fixtures。

Django支持fixtures在JSON,XML,或者YAML格式中。我们将要创建一个fixture用来包含一些最初的Subject对象给我们的项目。

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

python manage.py createsuperuser

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

python manage.py runserver

现在,打开 http://127.0.0.1:8000/admin/courses/subject/ 在你的浏览器中。创建一些主题通过使用管理平台。列页面看上去如下所示:

django-10-1

运行一下命令在shell中:

python manage.py dumpdata courese --indent=2

你会看到类似以下的输出:

[
{
  "fileld:": {
    "title": "Programming",
    "slug": "programming"
  },
  "model": "courses.subject",
  "pk": 1
},
{
"fields": {
    "title": "Mathematics",
    "slug": "mathematics"
  },
  "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
} 
]

dumpdata命令从数据库中转储数据到标准输出中,默认序列化为JSON。这串数据结构包含的信息关于这个模型以及它的字段将会被Django用来加载它到数据库中。

你可以提供应用名给这命令或者指定模型给输出数据使用app.Model格式。你还可以指定格式通过使用--format标记。默认的,dumpdata输出序列化数据给标准输出。当然,你可以表明一个输出文件通过使用--output标记。--indent标记允许你指定缩进。更多信息关于udmpdata参数,运行python manage.py dumpdata --help

保存这个转储为一个fixtures文件到orders应用的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/目录下的文件,但是你可以指定fixture文件的完整路径给loaddata命令。你还可以使用FIXTURE_DIRS设置来告诉Django去额外的目录寻找fixtures。

Fixtures并不只对初始化数据有用,还可以提供简单的数据给你的应用或者数据请求给你的测试用例。

你可以找到更多关于如何使用fixtures在测试中,通过访问 https://docs.djangoproject.com/en/1.8/topics/testing/tools/#topics-testing-fixtures

如果你想要加载fixturres在模型迁移中,去看下Django的文档关于数据迁移。请记住,我们创建了一个定制迁移在第九章,扩展你的商店来迁移存在的数据在修改给翻译的模型之后。你可以找到迁移数据的文档,通过访问 https://docs.djangoproject.com/en/1.8/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模型。一个模块包含多种内容,所有我们定义了一个ForeignKey字段给module模型。我们还设置了一个通用关系来连接对象从不同的模型中相当于不同的内容类型。请记住,我们需要三种不同的字段来设置一个通用关系。在我们的Content模型中,它们是:

  • content_type:一个ForeignKey字段指向ContentType模型
  • object_id:这是PositiveIntegerField用来存储有关联对象的关键字
  • item:一个GenericForeignKey字段指向被关联的对象通过结合前两个字段

只有content_typeobject_id字段有一个对应列在这个模型的数据库表中。item字段允许你去检索或者直接设置关联对象,并且它的功能是简历在其他两个字段之上。

我们将要使用一个不同的模型给每一种内容。我们的内容模型将会有很多共有的字段,但是它们将会有不同之处在它们存储的真实内容中。

使用模型继承

Django支持模型继承。类似与Python中的标准类继承。Django提供以下三种方式来使用模型继承:

  • Abstract models:非常有用当你想要安置一些公用信息到多个模型中。没有数据库表会被创建给抽象模型。
  • Multi-table model inheritance:可适当的利用当每个模型经过慎重考虑都是一个它自身的完整的模型。每个模型都会创建一个数据库表。
  • Proxy models:非常有用当你需要改变一个模型行为,比如说,包含额外的方法,修改默认管理器,或者使用不同的元选项。没有数据表会被创建给代理模型。

让我们对以上三者都来一次近距离的实践。

抽象模型

一个抽象模型就是一个基础类,你定义在其中的字段就是你想要包含到所有子模型中的字段。Djnago不会创建任何数据库表给抽象模型。每个子模型都会创建一张数据库表,包含有继承自抽象类的字段以及在子模型中自己定义的字段。

为了抽象一个模型,你需要在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字段给子模型创建关系指向它的父模型。

为了使用多表继承,你必须基于一个存在的模型。djnago将会创建一张数据表给每个源头模型以及子模型。以下例子展示多表继承:

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将会包含一个自动生成的OneToOneField字段在Text模型中并且给每个模型创建一张数据库表。

代理模型

代理模型被用于改变一个模型的行为,举个例子,包含额外的方法或者不同的元选项。每个模型对源头模型的数据库表起作用。为了创建一个代理模型,在这个模型的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模型这是一个代理模型给Content模型使用。这个模型提供了一个默认的排序给查询集并且一个额外的create_delta()方法。这两个模型,ContentOrderedContent,对同一个数据库表起作用,并且通过任一一个模型都能通过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允许我们去指定一个占位符给model类名在related_name属性类似%(class)s。为了做到这些,related_name对每个子模型都会自动生成。因为我们使用%(class)s_related作为related_name,给子模型的相对关系将各自是text_related,file_related,image_related,以及vide0_related

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

  • Text:用来存储文本内容。
  • File:用来存储文件,例如PDF。
  • Image:用来存储图片文件。
  • Video:用来存储视频。我们使用一个URLField字段来提供一个视频URL为了嵌入该视频。

每个子模型包含定义在ItemBase类中的字段以及它自己的字段。text_related,file_related,image_related,以及vide0_related都会各自创建一张数据库表。不会有数据库表连接到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属性就像'text','video','image',或者'file'。

让我们创建一个迁移来包含这些新的模型我们之前添加的。运行以下命令:

python manage.py makemigrations

你会看到以下输出:

Migrations for 'courses':
     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:
     Rendering model states... DONE
     Applying courses.0002_content_file_image_text_video... OK

我们之前创建的模型对于添加不同的内容给课程模块是很合适的。但是,仍然有一些东西是被遗漏的在我们的模型中。课程模块和内容应当跟随一个特定的顺序。我们需要一个字段,这个字段允许我们简单的排序它们。

创建定制模型字段

Django自带一个完整的模型字段采集能让你用来构建你的模型。当然,你也可以创建你自己的模型字段来存储定制数据或者改变现有字段的行为。

我们需要一个字段允许我们给对象们定义次序。如果你想通过Djanog提供的一个字段来方便的处理这点,你大概会想到添加一个PositiveIntegerField给你的模型。这是一个好的起点。我们可以创建一个定制字段,该字段继承自PositiveIntegerField并且提供额外的行为。

有两种相关的功能我们将构建到我们的次序字段中:

  • 自动分配一个次序值当没有指定的次序被提供的时候。当没有次数被提供的时候存储一个对象,我们的字段将自动分配下一个次序,该次序基于最后存在次序的对象。如果有两个对象,分别是次序1和次序2,当保存第三个对象的时候,我们会自动分配次序3给第三个对象如果没有给予指定的次序。
  • 次序对象关于其他的字段。课程模块将按照它们所属的课程和相关模块的内容进行排序。

创建一个新的fields.py文件到courses应用目录下,然后添加以下代码:

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(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_fields参数,这个参数允许我们表明次序根据这些字段进行计算。

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

  • 1 我们检查在模型实例中的字段是否已有一个值。我们是self.attname,它是在这个模型中给予这个字段的属性名。如果在这个属性的值不同于None,我们就会进行如下操作来计算出一个次序给它:

    • 1 我们构建一个查询集去检索所有对象给这个字段的模型。我们通过访问self.model来检索该字段所属的模型类。
    • 2 我们通过模型字段中的那些被定义在for_fields参数中的字段的当前值来过滤这个查询集(如果有的话)。为了做到这点,我们通过给予的字段来计算次序。
    • 3 我们从数据库中使用最高的次序来检索对象通过是用last_item = qs.latest(self.attname)。如果没有找到对象,我们假定这个对象是第一个并且分配次序0给它。
    • 4 如果找到一个对象,我们给找到的最高次序增加1。
    • 5 我们分配计算过的次序给在模型实例中的字段的值通过使用setattr()并且返回它。
  • 2 如果这个模型实例有一个值给当前的字段,我们不需要做任何事情。

当你创建定制模型字段,使它们通过。避免硬编码数据被依赖一个指定模型或者字段。你的字段才能在任意模型中起效。

你可以找到更多的信息关于编写定制模型字段,通过访问 https://docs.djangoproject.com/en/1.8/howto/custom-model-fields/

让我们添加新的字段给我们的模型。编辑courses应用的models.py文件,导入新的字段如下所示:

from .fields import OrderField

之后,添加以下OrderField字段给Module模型:

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

我们命名新的字段为order,并且我们指定该字段的次序根据课程计算通过设置for_fields=['course']。这意味着新的模块的次序将会是最后的同样的Course对象模块的次序增加1。现在你可以编辑Module模型的__str__()方法来包含它的次序如下所示:

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

模块内容也需要跟随一个特定的次序。添加一个OrderField字段给Content模型如下所示:

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

这一次,我们指定这个次序根据moduel字段进行计算。最后,让我们给这两个模型都添加一个默认的序列。添加如下Meta类给ModuleContent模型:

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',
                                                      'file')})
    item = GenericForeignKey('content_type', 'object_id')
    order = OrderField(blank=True, for_fields=['module'])
    class Meta:
        ordering = ['order']        

让我们创建一个新模型迁移来体现新的次序字段。打开shell并且运行如下命令:

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)
 2) Quit, and let me add a default in models.py
Select an option:

Django正在告诉我们由于我们添加了一个新的字段给已经存在的模型,我们必须提供一个默认值给数据库中已经存在的各行记录。如果这个字段有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()
>>>

输入 0 作为给已经存在的记录的默认值然后按下回车。Djanog将会询问你还需要一个默认值给Module模型。选择第一个选项然后再次输入 0 作为默认值。最后,你将会看到如下类似的输入:

Migrations for 'courses':
 0003_auto_20150701_1851.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_20150701_1851... OK

让我们测试我们新的字段。打开shell使用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, title='Course 2', slug='course2', owner=user)
>>> m5 = Module.objects.create(course=c2, title='Module 1')
>>> m5.order
0

为了计算这个新模块的次序,该字段只需要考虑基于同一课程的已经存在的模块。由于这是第二个课程的第一个模块,次序的结果值就是 0 。这是因为我们指定for_fields=['course']Module模型的order字段中。

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

创建一个内容管理系统

到现在我们已经创建了一个多功能数据模型,我们将要构建一个内容管理系统(CMS)。这个CMS将允许教师去创建课程以及管理课程的内容。我们需要提供以下功能:

  • 登录CMS。
  • 排列教师创建的课程。
  • 创建,编辑以及删除课程。
  • 添加模块到一个课程中并且重新排序它们。
  • 添加不同类型的内容给每个模块并且重新排序内容。

添加认证系统

我们将要使用Django的认证框架到我们的平台中。教师和学生都将会是Django User模型的一个实例。从而,他们将能够登录这个站点通过使用django.contrib.auth的认证视图。

编辑educa项目的主urls.py文件然后包含Django认证框架的loginlogout视图:

from django.conf.urls import include, url
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/', include(admin.site.urls)),
]

创建认证模板

courses应用目录下创建如下文件结构:

templates/
    base.html
    registration/
        login.html
        logged_out.html

在构建认证模板之前,我们需要给我们的项目准备好基础模板。编辑base.html模板文件然后添加以下内容:

{% load staticfiles %}
<!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://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
     <script>
       $(document).ready(function() {
         {% block domready %}
         {% endblock %}
       });
     </script>
   </body>
</html>

这个基础模板将会被其他的模板扩展。在这个模板中,我们定义了以下区块:

  • title:这个区块是给别的模板用来给每个页面添加定制的标题。
  • content:这个是内容的主区块。所有扩展基础模板的模板都可以添加各自的内容到这个区块。
  • domready:位于jQuery的$document.ready()方法里面。它允许我们执行代码当DOM完成加载的时候。

这个模板使用的CSS样式位于本章实例代码的courses应用下的static/目录下。你可以拷贝static/目录到你的项目的相同目录下来使用它们。

编辑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/ 。你会看到如下登录页面:

django-10-2

创建基于类的视图

我们将要构建一些视图用来创建,编辑,以及删除课程。为了这个目的我们将会使用基于类的视图。编辑courses应用的views.py文件并且添加如下代码:

from django.views.generic.list import ListView
from .models import Course

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_queryse()方法。当你需要去提供一个指定行为给多个基于类的视图,推荐你使用mixins

对基于类的视图使用mixins

mixins是一种特殊的用于一个类的多重继承。你可以使用它们来提供常见的离散功能,添加到其他的mixins,允许你去定义一个类的行为。有两种场景要使用mixins:

  • 你想要提供多个可选的特性给一个类
  • 你想要使用一个特定的特性在多个类上

你可以找到关于如何在基于类的视图上使用mixins的文档,通过访问 https://docs.djangoproject.com/en/1.8/topics/class-based-views/mixins/

Django自带多个mixins用来提供额外的功能给你的基于类的视图。你可以找到所有的mixins在 https://docs.djangoproject.com/en/1.8/ref/class-based-views/mixins/

我们将要创建一个mixin类来包含一个公用的行为并且将它给课程的视图使用。编辑courses应用的views.py文件,把它修改成如下所示:

from django.core.urlresolvers import reverse_lazy
from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, UpdateView, \
                                         DeleteView
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')

在上述代码中,我们创建了OwnerMixinOwnerEditMixin这两个mixin。我们将要使用这些mixins与Django提供的ListViewCreateViewUpdateView以及DeleteView视图结合。Ownermixin导入了以下方法。

  • get_queryset():这个方法被视图用来获取基础查询集。我们的mixin将会重写这个方法使用owner属性对对象进行过滤来检索属于当前用户的对象(request.user)。

OwnerEditMixin导入了以下方法:

  • form_valid():这个方法被视图用来使用Django的ModelFormMixin mixin,也就是说,带有表单的视图或模型表单的视图比如CreateviewUpdateView.form_valid()当提交的表单是有效的时候就会被执行。这个方法默认的行为是保存实例(对于模型表单)以及重定向用户到success_url。我们重写了这个方法来自动设置当前的用户到本次会被保存的对象的owner属性中。为了做到前面所说的,我们设置自动分配一个拥有者给该对象,当该对象被保存的时候。

我们的OwnerMixin类能够被视图用来和任意模型进行交互使模型包含一个owner属性。

我们还定义了一个OwnercourseMixin类,该类继承OwnerMixin并且提供以下属性给子视图:

  • model:这个模型给查询集使用。被所有视图使用。

我们定义一个OwnerCourseEditMixin mixin通过以下属性:

  • fields:这些模型字段用来从CreateViewUpdateView视图中构建模型。
  • success_url:被CreateViewUpdateView使用来在表单成功提交之后重定向用户。我们之后将会创建一个名为manage_course_list的URL来使用。

最后,我们创建以下视图,这些视图都是基于OwnerCourseMixin的子类:

  • MangeCourselISTvIEW:排序用户创建的课程。它从OwnerCourseMixinListView继承而来。
  • CoursecreateView:使用模型表单来创建一个新的Course对象。它使用定义在OwnerCourseEditMixin中的字段来构建一个表单模型并且也是CreateView的子类。
  • CourseUpdateView:允许编辑一个现有的Course对象。它从OwnerCourseMixinUpdateView继承而来。
  • CourseDeleteView:从OwnerCourseMixin和通用的DeleteView继承而来。定义success_url在对象被删除的时候重定向用户。

(译者注:上半章结束)

推荐阅读更多精彩内容