第十章 構(gòu)建一個(gè)在線學(xué)習(xí)平臺(tái)(上)

10 構(gòu)建一個(gè)在線學(xué)習(xí)平臺(tái)

在上一章中,你為在線商店項(xiàng)目添加了國(guó)際化。你還構(gòu)建了一個(gè)優(yōu)惠券系統(tǒng)和一個(gè)商品推薦引擎。在本章中,你會(huì)創(chuàng)建一個(gè)新的項(xiàng)目。你會(huì)構(gòu)建一個(gè)在線學(xué)習(xí)平臺(tái),這個(gè)平臺(tái)會(huì)創(chuàng)建一個(gè)自定義的內(nèi)容管理系統(tǒng)。

在本章中,你會(huì)學(xué)習(xí)如何:

  • 為模型創(chuàng)建fixtures
  • 使用模型繼承
  • 創(chuàng)建自定義O型字典
  • 使用基于類的視圖和mixins
  • 構(gòu)建表單集
  • 管理組和權(quán)限
  • 創(chuàng)建一個(gè)內(nèi)容管理系統(tǒng)

10.1 創(chuàng)建一個(gè)在線學(xué)習(xí)平臺(tái)

我們最后一個(gè)實(shí)戰(zhàn)項(xiàng)目是一個(gè)在線學(xué)習(xí)平臺(tái)。在本章中,我們會(huì)構(gòu)建一個(gè)靈活的內(nèi)容管理系統(tǒng)(CMS),允許教師創(chuàng)建課程和管理課程內(nèi)容。

首先,我們用以下命令為新項(xiàng)目創(chuàng)建一個(gè)虛擬環(huán)境,并激活它:

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

用以下命令在虛擬環(huán)境中安裝Django:

pip install Django

我們將在項(xiàng)目中管理圖片上傳,所以我們還需要用以下命令安裝Pillow:

pip install Pillow

使用以下命令創(chuàng)建一個(gè)新項(xiàng)目:

django-admin startproject educa

進(jìn)入新的educa目錄,并用以下命令創(chuàng)建一個(gè)新應(yīng)用:

cd educa
django-admin startapp courses

編輯educa項(xiàng)目的settings.py文件,把courses添加到INSTALLED_APPS設(shè)置中:

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

現(xiàn)在courses應(yīng)用已經(jīng)在項(xiàng)目激活了。讓我們?yōu)檎n程和課程內(nèi)容定義模型。

10.2 構(gòu)建課程模型

我們的在線學(xué)習(xí)平臺(tái)會(huì)提供多種主題的課程。每個(gè)課程會(huì)劃分為可配置的單元數(shù)量,而每個(gè)單元會(huì)包括可配置的內(nèi)容數(shù)量。會(huì)有各種類型的內(nèi)容:文本,文件,圖片或者視頻。下面這個(gè)例子展示了我們的課程目錄的數(shù)據(jù)結(jié)構(gòu):

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

讓我們構(gòu)建課程模型。編輯courses應(yīng)用的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:創(chuàng)建給課程的教師
  • subject:這個(gè)課程所屬的主題。一個(gè)指向Subject模型的ForeignKey字段。
  • title:課程標(biāo)題.
  • slug:課程別名,之后在URL中使用。
  • overview:一個(gè)TextField列,表示課程概述。
  • created:課程創(chuàng)建的日期和時(shí)間。因?yàn)樵O(shè)置了auto_now_add=True,所以創(chuàng)建新對(duì)象時(shí),Django會(huì)自動(dòng)設(shè)置這個(gè)字段。

每個(gè)課程劃分為數(shù)個(gè)單元。因此,Module模型包含一個(gè)指向Course模型的ForeignKey字段。

打開終端執(zhí)行以下命令,為應(yīng)用創(chuàng)建初始的數(shù)據(jù)庫(kù)遷移:

python manage.py makemigrations

你會(huì)看到以下輸出:

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

然后執(zhí)行以下命令,同步遷移到數(shù)據(jù)庫(kù)中:

python manage.py migrate

你會(huì)看到一個(gè)輸出,其中包括所有已經(jīng)生效的數(shù)據(jù)庫(kù)遷移,包括Django的數(shù)據(jù)庫(kù)遷移。輸出會(huì)包括這一行:

Applying courses.0001_initial... OK

這個(gè)告訴我們,courses應(yīng)用的模型已經(jīng)同步到數(shù)據(jù)庫(kù)中。

10.2.1 在管理站點(diǎn)注冊(cè)模型

我們將把課程模型添加到管理站點(diǎn)。編輯courses應(yīng)用目錄中的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]

現(xiàn)在courses應(yīng)用的模型已經(jīng)在管理站點(diǎn)注冊(cè)。我們用@admin.register()裝飾器代替admin.site.register()函數(shù)。它們的功能是一樣的。

10.2.2 為模型提供初始數(shù)據(jù)

有時(shí)你可能希望用硬編碼數(shù)據(jù)預(yù)填充數(shù)據(jù)庫(kù)。這在項(xiàng)目創(chuàng)建時(shí)自動(dòng)包括初始數(shù)據(jù)很有用,來(lái)替代手工添加數(shù)據(jù)。Django自帶一種簡(jiǎn)單的方式,可以從數(shù)據(jù)庫(kù)中加載和轉(zhuǎn)儲(chǔ)(dump)數(shù)據(jù)到fixtures文件中。

Django支持JSON,XML或者YAML格式的fixtures。我們將創(chuàng)建一個(gè)fixture,其中包括一些項(xiàng)目的初始Subject對(duì)象。

首先使用以下命令創(chuàng)建一個(gè)超級(jí)用戶:

python manage.py createsuperuser

然后用以下命令啟動(dòng)開發(fā)服務(wù)器:

python manage.py runserver

現(xiàn)在在瀏覽器中打開http://127.0.0.1:8000/admin/courses/subject/。使用管理站點(diǎn)創(chuàng)建幾個(gè)主題。列表顯示頁(yè)面如下圖所示:

在終端執(zhí)行以下命令:

python manage.py dumpdata courses --indent=2

你會(huì)看到類似這樣的輸出:

[
{
  "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命令從數(shù)據(jù)庫(kù)中轉(zhuǎn)儲(chǔ)數(shù)據(jù)到標(biāo)準(zhǔn)輸出,默認(rèn)用JSON序列化。返回的數(shù)據(jù)結(jié)構(gòu)包括模型和它的字段信息,Django可以把它加載到數(shù)據(jù)庫(kù)中。

你可以給這個(gè)命令提供應(yīng)用的名稱,或者用app.Model格式指定輸出數(shù)據(jù)的模型。你還可以使用--format標(biāo)簽指定格式。默認(rèn)情況下,dumpdata輸出序列化的數(shù)據(jù)到標(biāo)準(zhǔn)輸出。但是,你可以使用--output標(biāo)簽指定一個(gè)輸出文件。--indent標(biāo)簽允許你指定縮進(jìn)。關(guān)于更多dumpdata的參數(shù)信息,請(qǐng)執(zhí)行python manage.py dumpdata --help命令。

使用以下命令,把這個(gè)轉(zhuǎn)儲(chǔ)保存到courses應(yīng)用的fixtures/目錄中:

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

使用管理站點(diǎn)移除你創(chuàng)建的主題。然后使用以下命令把fixture加載到數(shù)據(jù)庫(kù)中:

python manage.py loaddata subjects.json

fixture中包括的所有Subject對(duì)象已經(jīng)加載到數(shù)據(jù)庫(kù)中。

默認(rèn)情況下,Django在每個(gè)應(yīng)用的fixtures/目錄中查找文件,但你也可以為loaddata命令指定fixture文件的完整路徑。你還可以使用FIXTURE_DIRS設(shè)置告訴Django查找fixtures的額外目錄。

Fixtures不僅對(duì)初始數(shù)據(jù)有用,還可以為應(yīng)用提供簡(jiǎn)單的數(shù)據(jù),或者測(cè)試必需的數(shù)據(jù)。

你可以在這里閱讀如何在測(cè)試中使用fixtures。

如果你想在模型遷移中加載fixtures,請(qǐng)閱讀Django文檔的數(shù)據(jù)遷移部分。記住,我們?cè)诘诰耪聞?chuàng)建了自定義遷移,用于修改模型后遷移已存在的數(shù)據(jù)。你可以在這里閱讀數(shù)據(jù)庫(kù)遷移的文檔。

10.3 為不同的內(nèi)容創(chuàng)建模型

我們計(jì)劃在課程模型中添加不同類型的內(nèi)容,比如文本,圖片,文件和視頻。我們需要一個(gè)通用的數(shù)據(jù)模型,允許我們存儲(chǔ)不同的內(nèi)容。在第六章中,我們已經(jīng)學(xué)習(xí)了使用通用關(guān)系創(chuàng)建指向任何模型對(duì)象的外鍵。我們將創(chuàng)建一個(gè)Content模型表示單元內(nèi)容,并定義一個(gè)通過(guò)關(guān)系,關(guān)聯(lián)到任何類型的內(nèi)容。

編輯courses應(yīng)用的models.py文件,并添加以下導(dǎo)入:

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

然后在文件結(jié)尾添加以下代碼:

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模型。一個(gè)單元包括多個(gè)內(nèi)容,所以我們定義了一個(gè)指向Module模型的外鍵。我們還建立了一個(gè)通用關(guān)系,從代表不同內(nèi)容類型的不同模型關(guān)聯(lián)到對(duì)象。記住,我們需要三個(gè)不同字段來(lái)設(shè)置一個(gè)通用關(guān)系。在Content模型中,它們分別是:

  • content_type:一個(gè)指向ContentType模型的ForeignKey字段。
  • object_id:這是一個(gè)PositiveIntegerField,存儲(chǔ)關(guān)聯(lián)對(duì)象的主鍵。
  • item:通過(guò)組合上面兩個(gè)字段,指向關(guān)聯(lián)對(duì)象的GenericForeignKey字段。

在這個(gè)模型的數(shù)據(jù)庫(kù)表中,只有content_typeobject_id字段有對(duì)應(yīng)的列。item字段允許你直接檢索或設(shè)置關(guān)聯(lián)對(duì)象,它的功能建立在另外兩個(gè)字段之上。

我們將為每種內(nèi)容類型使用不同的模型。我們的內(nèi)容模型會(huì)有通用字段,但它們存儲(chǔ)的實(shí)際內(nèi)容會(huì)不同。

10.3.1 使用模型繼承

Django支持模型繼承,類似Python中標(biāo)準(zhǔn)類的繼承。Django為使用模型繼承提供了以下三個(gè)選擇:

  • 抽象模型:當(dāng)你想把一些通用信息放在幾個(gè)模型時(shí)很有用。不會(huì)為抽象模型創(chuàng)建數(shù)據(jù)庫(kù)表。
  • 多表模型繼承:可用于層次中每個(gè)模型本身被認(rèn)為是一個(gè)完整模型的情況下。為每個(gè)模型創(chuàng)建一張數(shù)據(jù)庫(kù)表。
  • 代理模型:當(dāng)你需要修改一個(gè)模型的行為時(shí)很有用。例如,包括額外的方法,修改默認(rèn)管理器,或者使用不同的元選項(xiàng)。不會(huì)為代理模型創(chuàng)建數(shù)據(jù)庫(kù)表。

讓我們近一步了解它們。

10.3.1.1 抽象模型

一個(gè)抽象模型是一個(gè)基類,其中定義了你想在所有子模型中包括的字段。Django不會(huì)為抽象模型創(chuàng)建任何數(shù)據(jù)庫(kù)表。會(huì)為每個(gè)子模型創(chuàng)建一張數(shù)據(jù)庫(kù)表,其中包括從抽象類繼承的字段,和子模型中定義的字段。

要標(biāo)記一個(gè)抽象模型,你需要在它的Meta類中包括abstract=True。Django會(huì)認(rèn)為它是一個(gè)抽象模型,并且不會(huì)為它創(chuàng)建數(shù)據(jù)庫(kù)表。要?jiǎng)?chuàng)建子模型,你只需要從抽象模型繼承。以下是一個(gè)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()

在這個(gè)例子中,Django只會(huì)為Text模型創(chuàng)建數(shù)據(jù)庫(kù)表,其中包括titlecreatedbody字段。

10.3.1.2 多表模型繼承

在多表繼承中,每個(gè)模型都有一張相應(yīng)的數(shù)據(jù)庫(kù)表。Django會(huì)在子模型中創(chuàng)建指向父模型的OneToOneField字段。

要使用多表繼承,你必須從已存在模型中繼承。Django會(huì)為原模型和子模型創(chuàng)建數(shù)據(jù)庫(kù)表。下面是一個(gè)多表繼承的例子:

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會(huì)在Text模型中包括一個(gè)自動(dòng)生成的OneToOneField字段,并為每個(gè)模型創(chuàng)建一張數(shù)據(jù)庫(kù)表。

10.3.1.3 代理模型

代理模型用于修改模型的行為,比如包括額外的方法或者不同的元選項(xiàng)。這兩個(gè)模型都在原模型的數(shù)據(jù)庫(kù)表上進(jìn)行操作。在模型的Meta類中添加proxy=True來(lái)創(chuàng)建代理模型。

下面這個(gè)例子展示了如何創(chuàng)建一個(gè)代理模型:

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

我們?cè)谶@里定義了一個(gè)OrderedContent模型,它是Content模型的代理模型。這個(gè)模型為QuerySet提供了默認(rèn)排序和一個(gè)額外的created_delta()方法。ContentOrderedContent模型都在同一張數(shù)據(jù)庫(kù)表上操作,并且可以用ORM通過(guò)任何一個(gè)模型訪問(wèn)對(duì)象。

10.3.2 創(chuàng)建內(nèi)容模型

courses應(yīng)用的Content模型包含一個(gè)通用關(guān)系來(lái)關(guān)聯(lián)不同的內(nèi)容類型。我們將為每種內(nèi)容模型創(chuàng)建不用的模型。所有內(nèi)容模型會(huì)有一些通用的字段,和一些額外字段存儲(chǔ)自定義數(shù)據(jù)。我們將創(chuàng)建一個(gè)抽象模型,它會(huì)為所有內(nèi)容模型提供通用字段。

編輯courses應(yīng)用的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()

在這段代碼中,我們定義了一個(gè)ItemBase抽象模型。因此我們?cè)?code>Meta類中設(shè)置了abstract=True。在這個(gè)模型中,我們定義了ownertitlecreatedupdated字段。這些通用字段會(huì)用于所有內(nèi)容類型。owner字段允許我們存儲(chǔ)哪個(gè)用戶創(chuàng)建了內(nèi)容。因?yàn)檫@個(gè)字段在抽象類中定義,所以每個(gè)子模型需要不同的related_name。Django允許我們?cè)?code>related_name屬性中為模型的類名指定占位符,比如%(class)s。這樣,每個(gè)子模型的related_name會(huì)自動(dòng)生成。因?yàn)槲覀兪褂?code>%(class)s_related作為related_name,所以每個(gè)子模型對(duì)應(yīng)的反向關(guān)系是text_relatedfile_relatedimage_relatedvideo_related

我們定義了四個(gè)從ItemBase抽象模型繼承的內(nèi)容模型。分別是:

  • Text:存儲(chǔ)文本內(nèi)容。
  • File:存儲(chǔ)文件,比如PDF。
  • Image:存儲(chǔ)圖片文件。
  • Video:存儲(chǔ)視頻。我們使用URLField字段來(lái)提供一個(gè)視頻的URL,從而可以嵌入視頻。

除了自身的字段,每個(gè)子模型還包括ItemBase類中定義的字段。會(huì)為TextFileImageVideo模型創(chuàng)建對(duì)應(yīng)的數(shù)據(jù)庫(kù)表。因?yàn)?code>ItemBase是一個(gè)抽象模型,所以它不會(huì)關(guān)聯(lián)到數(shù)據(jù)庫(kù)表。

編輯你之前創(chuàng)建的Content模型,修改它的content_type字段:

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

我們添加了limit_choices_to參數(shù)來(lái)限制ContentType對(duì)象可用于的通用關(guān)系。我們使用了model__in字段查找,來(lái)過(guò)濾ContentType對(duì)象的model屬性為textvideoimage或者file

讓我們創(chuàng)建包括新模型的數(shù)據(jù)庫(kù)遷移。在命令行中執(zhí)行以下命令:

python manage.py makemigrations

你會(huì)看到以下輸出:

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

然后執(zhí)行以下命令應(yīng)用新的數(shù)據(jù)庫(kù)遷移:

python manage.py migrate

你看到的輸出的結(jié)尾是:

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

我們已經(jīng)創(chuàng)建了模型,可以添加不同內(nèi)容到課程單元中。但是我們的模型仍然缺少了一些東西。課程單元和內(nèi)容應(yīng)用遵循特定的順序。我們需要一個(gè)字段對(duì)它們進(jìn)行排序。

10.4 創(chuàng)建自定義模板字段

Django自帶一組完整的模塊字段,你可以用它們構(gòu)建自己的模型。但是,你也可以創(chuàng)建自己的模型字段來(lái)存儲(chǔ)自定義數(shù)據(jù),或者修改已存在字段的行為。

我們需要一個(gè)字段指定對(duì)象的順序。如果你想用Django提供的字段,用一種簡(jiǎn)單的方式實(shí)現(xiàn)這個(gè)功能,你可能會(huì)想在模型中添加一個(gè)PositiveIntegerField。這是一個(gè)好的開始。我們可以創(chuàng)建一個(gè)從PositiveIntegerField繼承的自定義字段,并提供額外的方法。

我們會(huì)在排序字段中添加以下兩個(gè)功能:

  • 沒(méi)有提供特定序號(hào)時(shí),自動(dòng)分配一個(gè)序號(hào)。如果存儲(chǔ)對(duì)象時(shí)沒(méi)有提供序號(hào),我們的字段會(huì)基于最后一個(gè)已存在的排序?qū)ο螅詣?dòng)分配下一個(gè)序號(hào)。如果兩個(gè)對(duì)象的序號(hào)分別是1和2,保存第三個(gè)對(duì)象時(shí),如果沒(méi)有給定特定序號(hào),我們應(yīng)該自動(dòng)分配為序號(hào)3。
  • 相對(duì)于其它字段排序?qū)ο蟆Un程單元將會(huì)相對(duì)于它們所屬的課程排序,而模塊內(nèi)容會(huì)相對(duì)于它們所屬的單元排序。

courses應(yīng)用目錄中創(chuàng)建一個(gè)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字段有一個(gè)可選的for_fields參數(shù),允許我們指定序號(hào)相對(duì)于哪些字段計(jì)算。

我們的字段覆寫了PositiveIntegerField字段的pre_save()方法,它會(huì)在該字段保存到數(shù)據(jù)庫(kù)中之前執(zhí)行。我們?cè)谶@個(gè)方法中執(zhí)行以下操作:

  1. 我們檢查模型實(shí)例中是否已經(jīng)存在這個(gè)字段的值。我們使用self.attname,這是模型中指定的這個(gè)字段的屬性名。如果屬性的值不是None,我們?nèi)缦掠?jì)算序號(hào):
  • 我們構(gòu)建一個(gè)QuerySet檢索這個(gè)字段模型所有對(duì)象。我們通過(guò)訪問(wèn)self.model檢索字段所屬的模型類。
  • 我們用定義在字段的for_fields參數(shù)中的模型字段(如果有的話)的當(dāng)前值過(guò)濾QuerySet。這樣,我們就能相對(duì)于給定字段計(jì)算序號(hào)。
  • 我們用last_item = qs.lastest(self.attname)從數(shù)據(jù)庫(kù)中檢索序號(hào)最大的對(duì)象。如果沒(méi)有找到對(duì)象,我們假設(shè)它是第一個(gè)對(duì)象,并分配序號(hào)0。
  • 如果找到一個(gè)對(duì)象,我們?cè)谡业降淖畲笮蛱?hào)上加1。
  • 我們用setattr()把計(jì)算的序號(hào)分配給模型實(shí)例中的字段值,并返回這個(gè)值。
  1. 如果模型實(shí)例有當(dāng)前字段的值,則什么都不做。

當(dāng)你創(chuàng)建自定義模型字段時(shí),讓它們是通用的。避免分局特定模型或字段硬編碼數(shù)據(jù)。你的字段應(yīng)該可以用于所有模型。

你可以在這里閱讀更多關(guān)于編寫自定義模型字段的信息。

讓我們?cè)谀P椭刑砑有伦侄巍>庉?code>courses應(yīng)用的models.py文件,并導(dǎo)入新的字段:

from .fields import OrderField

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

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

我們命名新字段為order,并通過(guò)設(shè)置for_fields=['course'],指定相對(duì)于課程計(jì)算序號(hào)。這意味著一個(gè)新單元會(huì)分配給同一個(gè)Course對(duì)象中最新的單元加1。現(xiàn)在編輯Module模型的__str__()方法,并如下引入它的序號(hào):

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

單元內(nèi)容也需要遵循特定序號(hào)。在Content模型中添加一個(gè)OrderField字段:

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

這次我們指定序號(hào)相對(duì)于module字段計(jì)算。最后,讓我們?yōu)閮蓚€(gè)模型添加默認(rèn)排序。在ModuleContent模型中添加以下Meta類:

class Meta:
    ordering = ['order']

現(xiàn)在ModuleContent模型看起來(lái)是這樣的:

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']

讓我們創(chuàng)建反映新序號(hào)字段的模型遷移。打開終端,并執(zhí)行以下命令:

python manage.py makemigrations courses

你會(huì)看到以下輸出:

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告訴我們,因?yàn)槲覀冊(cè)谝汛嬖诘哪P椭刑砑恿诵伦侄危员仨殲閿?shù)據(jù)庫(kù)中已存在的行提供默認(rèn)值。如果字段有null=True,則可以接受空值,并且Django創(chuàng)建遷移時(shí)不要求提供默認(rèn)值。我們可以指定一個(gè)默認(rèn)值,或者取消數(shù)據(jù)庫(kù)遷移,并在創(chuàng)建遷移之前在models.py文件的order字段中添加default屬性。

輸入1,然后按下Enter,為已存在的記錄提供一個(gè)默認(rèn)值。你會(huì)看到以下輸出:

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作為已存在記錄的默認(rèn)值,然后按下Enter。Django還會(huì)要求你為Module模型提供默認(rèn)值。選擇第一個(gè)選項(xiàng),然后再次輸入0作為默認(rèn)值。最后,你會(huì)看到類似這樣的輸出:

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

然后執(zhí)行以下命令應(yīng)用新的數(shù)據(jù)庫(kù)遷移:

python manage.py migrate

這個(gè)命令的輸出會(huì)告訴你遷移已經(jīng)應(yīng)用成功:

Applying courses.0003_auto_20170518_0743... OK

讓我們測(cè)試新字段。使用python manage.py shell命令打開終端,并如下創(chuàng)建一個(gè)新課程:

>>> 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')

我們已經(jīng)在數(shù)據(jù)庫(kù)中創(chuàng)建了一個(gè)課程。現(xiàn)在,讓我們添加一些單元到課程中,并查看單元序號(hào)是如何自動(dòng)計(jì)算的。我們創(chuàng)建一個(gè)初始單元,并檢查它的序號(hào):

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

OrderField設(shè)置它的值為0,因?yàn)檫@是給定課程的第一個(gè)Module對(duì)象。現(xiàn)在我們創(chuàng)建同一個(gè)課程的第二個(gè)單元:

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

OrderField在已存在對(duì)象的最大序號(hào)上加1來(lái)計(jì)算下一個(gè)序號(hào)。讓我們指定一個(gè)特定序號(hào)來(lái)創(chuàng)建第三個(gè)單元:

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

如果我們指定了自定義序號(hào),則OrderField字段不會(huì)介入,并且使用給定的order值。

讓我們添加第四個(gè)單元:

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

這個(gè)單元的序號(hào)已經(jīng)自動(dòng)設(shè)置了。我們的OrderField字段不能保證連續(xù)的序號(hào)。但是它關(guān)注已存在的序號(hào)值,總是根據(jù)已存在的最大序號(hào)值分配下一個(gè)序號(hào)。

讓我們創(chuàng)建第二個(gè)課程,并添加一個(gè)單元:

>>> 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

要計(jì)算新的單元序號(hào),該字段只考慮屬于同一個(gè)課程的已存在單元。因?yàn)檫@個(gè)第二個(gè)課程的第一個(gè)單元,所以序號(hào)為0。這是因?yàn)槲覀冊(cè)?code>Module模型的order字段中指定了for_fields=['course']

恭喜你!你已經(jīng)成功的創(chuàng)建了第一個(gè)自定義模型字段。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,034評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,337評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,883評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,114評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,625評(píng)論 1 332
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,555評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,737評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,973評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,343評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,699評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容