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

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

10.5 創(chuàng)建內(nèi)容管理系統(tǒng)

現(xiàn)在我們已經(jīng)創(chuàng)建了一個(gè)萬能的數(shù)據(jù)模型,接下來我們會(huì)創(chuàng)建一個(gè)內(nèi)容管理系統(tǒng)(CMS)。CMS允許教師創(chuàng)建課程,并管理它們的內(nèi)容。我們需要以下功能:

  • 登錄到CMS
  • 教師創(chuàng)建的課程列表
  • 創(chuàng)建,編輯和刪除課程
  • 添加單元到課程,并對(duì)它們重新排序
  • 添加不同類型的內(nèi)容到每個(gè)單元中,并對(duì)它們重新排序

10.5.1 添加認(rèn)證系統(tǒng)

我們將在平臺(tái)中使用Django的認(rèn)證框架。教師和學(xué)生都是Django的User模型的實(shí)例。因此,他們可以使用django.contrib.auth的認(rèn)證視圖登錄網(wǎng)站。

編輯educa項(xiàng)目的主urls.py文件,并引入Django認(rèn)證框架的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/', admin.site.urls),
]

10.5.2 創(chuàng)建認(rèn)證模板

courses應(yīng)用目錄中創(chuàng)建以下文件結(jié)構(gòu):

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

構(gòu)建認(rèn)證模板之前,我們需要為項(xiàng)目準(zhǔn)備基礎(chǔ)模板。編輯base.html模板,并添加以下內(nèi)容:

{% load staticfiles %}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>{% block title %}Educa{% endblock title %}</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 content %}
    </div>

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script>
        $(document).ready(function() {
            {% block domready %}
            {% endblock domready %}
        });
    </script>
</body>
</html>

這是基礎(chǔ)模板,其它模板會(huì)從它擴(kuò)展。在這個(gè)模板中,我們定義了以下塊:

  • title:其它模塊用來為每個(gè)頁面添加自定義標(biāo)題的塊。
  • content:主要的內(nèi)容塊。所有擴(kuò)展基礎(chǔ)模板的模板必須在這個(gè)塊中添加內(nèi)容。
  • domready:位于jQuery的$(document).ready()函數(shù)內(nèi)。允許我們?cè)贒OM完成加載時(shí)執(zhí)行代碼。

這個(gè)模板中使用的CSS樣式位于本章實(shí)例代碼的courses應(yīng)用的static/目錄中。你可以把它拷貝到項(xiàng)目的相同位置。

編輯registration/login.html模板,并添加以下代碼:

{% extends "base.html" %}

{% block title %}Log-in{% endblock title %}

{% 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, user 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 content %}

這是Django的login視圖的標(biāo)準(zhǔn)登錄模板。編輯registration/logged_out.html模板,并添加以下代碼:

{% extends "base.html" %}

{% block title %}Logged out{% endblock title %}

{% 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 content %}

用戶登出后會(huì)顯示這個(gè)模板。執(zhí)行python manage.py runserver命令啟動(dòng)開發(fā)服務(wù)器,然后在瀏覽器中打開http://127.0.0.1:8000/accounts/login/,你會(huì)看到以下登錄頁面:

10.5.3 創(chuàng)建基于類的視圖

我們將構(gòu)建用于創(chuàng)建,編輯和刪除課程的視圖。我們將使用基于類的視圖。編輯courses應(yīng)用的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().get_queryset()
        return qs.filter(owner=self.request.user)

這是ManageCourseListView視圖。它從Django的通用ListView繼承。我們覆寫了視圖的get_queryset()方法,只檢索當(dāng)前用戶創(chuàng)建的課程。要阻止用戶編輯,更新或者刪除不是他們創(chuàng)建的課程,我們還需要在創(chuàng)建,更新和刪除視圖中覆寫get_queryset()方法。當(dāng)你需要為數(shù)個(gè)基于類的視圖提供特定行為,推薦方式是使用minxins

10.5.4 為基于類的視圖使用mixins

Mixins是一個(gè)類的特殊的多重繼承。你可以用它們提供常見的離散功能,把它們添加到其它mixins中,允許你定義一個(gè)類的行為。有兩種主要場(chǎng)景下使用mixins:

  • 你想為一個(gè)類提供多個(gè)可選的特性
  • 你想在數(shù)個(gè)類中使用某個(gè)特性

你可以在這里閱讀如何在基于類的視圖中使用mixins的文檔。

Django自帶幾個(gè)mixins,為基于類的視圖提供額外的功能。你可以在這里找到所有mixins。

我們將創(chuàng)建包括一個(gè)常見功能的mixins類,并把它用于課程的視圖。編輯courses應(yīng)用的views.py文件,如下修改:

from django.core.urlresolvers import reverse_lazy
from django.views.generic.list import ListView
from django.views.generic.edit import CreateView
from django.views.generic.edit import UpdateView
from django.views.generic.edit import DeleteView
from .models import Course

class OwnerMixin:
    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(owner=self.request.user)

class OwnerEditMixin:
    def form_valid(self, form):
        form.instance.owner = self.request.user
        return super().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')

在這段代碼中,我們創(chuàng)建了OwnerMixinOwnerEditMixin兩個(gè)mixins。我們與Django提供的ListViewCreateViewUpdateViewDeleteView視圖一起使用這些mixins。OwnerMixin實(shí)現(xiàn)了以下方法:

  • get_queryset():視圖用這個(gè)方法獲得基本的QuerySet。我們的mixin會(huì)覆寫這個(gè)方法,通過owner屬性過濾對(duì)象,來檢索屬于當(dāng)前用戶的對(duì)象(request.user)。

OwnerEditMixin實(shí)現(xiàn)以下方法:

  • form_valid():使用Django的ModelFormMixin的視圖會(huì)使用這個(gè)方法,比如,帶表單或者模型表單的視圖(比如CreateViewUpdateView)。當(dāng)提交的表單有效時(shí),會(huì)執(zhí)行form_valid()。這個(gè)方法的默認(rèn)行為是保存實(shí)例(對(duì)于模型表單),并重定向用戶到success_url。我們覆寫這個(gè)方法,在被保存對(duì)象的owner屬性中自動(dòng)設(shè)置當(dāng)前用戶。這樣,當(dāng)保存對(duì)象時(shí),我們自動(dòng)設(shè)置了對(duì)象的owner

我們的OwnerMixin類可用于與包括owner屬性的任何模型交互的視圖。

我們還定義了一個(gè)OwnerCourseMixin,它從OwnerMixin繼承,并為子視圖提供以下屬性:

  • model:用于QuerySet的模型。可以被所有視圖使用。

我們用以下屬性定義了一個(gè)OwnerCourseEditMixin

  • fields:模型的這個(gè)字段構(gòu)建了CreateViewUpdateView視圖的模型表單。
  • success_url:當(dāng)表單提交成功后,CreateViewUpdateView用它重定向用戶。

最后,我們創(chuàng)建從OwnerCourseMixin繼承的視圖:

  • ManageCourseListView:列出用戶創(chuàng)建的課程。它從OwnerCourseMixinListView繼承。
  • CourseCreateView:用模型表單創(chuàng)建一個(gè)新的Course對(duì)象。它用在OwnerCourseEditMixin中定義的字段來構(gòu)建模型表單,它還從CreateView繼承。
  • CourseUpdateView:允許編輯一個(gè)已存在的Course對(duì)象。它從OwnerCourseEditMixinUpdateView繼承。
  • CourseDeleteView:從OwnerCourseMixin和通用的DeleteView繼承。定義了success_url,用于刪除對(duì)象后重定向用戶。

10.5.5 使用組和權(quán)限

我們已經(jīng)創(chuàng)建了管理課程的基礎(chǔ)視圖。當(dāng)前,任何用戶都可以訪問這些視圖。我們想限制這些視圖,只有教師有權(quán)限創(chuàng)建和管理課程。Django的認(rèn)證框架包括一個(gè)權(quán)限系統(tǒng),允許你給用戶和組分配權(quán)限。我們將為教師用戶創(chuàng)建一個(gè)組,并分配創(chuàng)建,更新和刪除課程的權(quán)限。

使用python manage.py runserver命令啟動(dòng)開發(fā)服務(wù)器,并在瀏覽器中打開http://127.0.0.1:8000/admin/auth/group/add/,然后創(chuàng)建一個(gè)新的Group對(duì)象。添加組名為Instructors,并選擇courses應(yīng)用的所有權(quán)限,除了Subject模型的權(quán)限,如下圖所示:

正如你所看到,每個(gè)模型有三個(gè)不同的權(quán)限:Can addCan changeCan delete。為這個(gè)組選擇權(quán)限后,點(diǎn)擊Save按鈕。

Django自動(dòng)為模型創(chuàng)建權(quán)限,但你也可以創(chuàng)建自定義權(quán)限。你可以在這里閱讀更多關(guān)于添加自定義權(quán)限的信息。

打開http://127.0.0.1:8000/admin/auth/user/add/,然后添加一個(gè)新用戶。編輯用戶,并把它添加Instructors組,如下圖所示:

用戶從它所屬的組中繼承權(quán)限,但你也可以使用管理站點(diǎn)為單個(gè)用戶添加獨(dú)立權(quán)限。is_superuser設(shè)置為True的用戶自動(dòng)獲得所有權(quán)限。

10.5.5.1 限制訪問基于類的視圖

我們將限制訪問視圖,只有擁有適當(dāng)權(quán)限的用戶才可以添加,修改或刪除Course對(duì)象。認(rèn)證框架包括一個(gè)permission_required裝飾器來限制訪問視圖。Django 1.9將會(huì)包括基于類視圖的權(quán)限mixins。但是Django 1.8不包括它們。因此,我們將使用第三方模塊django-braces提供的權(quán)限mixins。

譯者注:現(xiàn)在Django的最新版本是1.11.X。

Django-braces是一個(gè)第三方模塊,其中包括一組通用的Django mixins。這些mixins為基于類的視圖提供了額外的特性。你可以在這里查看django-braces提供的所有mixins。

使用pip命令安裝django-braces:

pip install django-braces

我們將使用django-braces的兩個(gè)mixins來限制訪問視圖:

  • LoginRequiredMixin:重復(fù)login_required裝飾器的功能。
  • PermissionRequiredMixin:允許有特定權(quán)限的用戶訪問視圖。記住,超級(jí)用戶自動(dòng)獲得所有權(quán)限。

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

from braces.views import LoginRequiredMixin
from braces.views import PermissionRequiredMixin

OwnerCourseMixinLoginRequiredMixin繼承:

class OwnerCourseMixin(OwnerMixin, LoginRequiredMixin):
    model = Course
    fields = ['subject', 'title', 'slug', 'overview']
    success_url = reverse_lazy('manage_course_list')

然后在創(chuàng)建,更新和刪除視圖中添加permission_required屬性:

class CourseCreateView(PermissionRequiredMixin,
                       OwnerCourseEditMixin, 
                       CreateView):
    permission_required = 'courses.add_course'

class CourseUpdateView(PermissionRequiredMixin,
                       OwnerCourseEditMixin, 
                       UpdateView):
    template_name = 'courses/manage/course/form.html'
    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'

PermissionRequiredMixin檢查訪問視圖的用戶是否有permission_required屬性中之指定的權(quán)限。現(xiàn)在只有合適權(quán)限的用戶可以訪問我們的視圖。

讓我們?yōu)檫@些視圖創(chuàng)建URL。在courses應(yīng)用目錄中創(chuàng)建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'),
]

這些是列出,創(chuàng)建,編輯和刪除課程視圖的URL模式。編輯educa項(xiàng)目的主urls.py文件,在其中包括courses應(yīng)用的URL模式:

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

我們需要為這些視圖創(chuàng)建模板。在courses應(yīng)用的templates/目錄中創(chuàng)建以下目錄和文件:

courses/
    manage/
        course/
            list.html
            form.html
            delete.html

編輯courses/manage/course/list.html模板,并添加以下代碼:

{% extends "base.html" %}

{% block title %}My courses{% endblock title %}

{% 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 content %}

這是ManageCourseListView視圖的模板。在這個(gè)模板中,我們列出了當(dāng)前用戶創(chuàng)建的課程。我們包括了編輯或刪除每個(gè)課程的鏈接,和一個(gè)創(chuàng)建新課程的鏈接。

使用python manage.py runserver命令啟動(dòng)開發(fā)服務(wù)器。在瀏覽器中打開http://127.0.0.1:8000/accounts/login/?next=/course/mine/,并用屬于Instructors組的用戶登錄。登錄后,你會(huì)重定向到http://127.0.0.1:8000/course/mine/,如下所示:

這個(gè)頁面會(huì)顯示當(dāng)前用戶創(chuàng)建的所有課程。

讓我們創(chuàng)建模板,顯示創(chuàng)建和更新課程視圖的表單。編輯courses/manage/course/form.html模板,并添加以下代碼:

{% extends "base.html" %}

{% block title %}
    {% if object %}
        Edit course "{{ object.title }}"
    {% else %}
        Create a new course
    {% endif %}
{% endblock title %}

{% 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 content %}

form.html模板用于CourseCreateViewCourseUpdateView視圖。在這個(gè)模板中,我們檢查上下文是否存在object變量。如果上下文中存在object,我們已經(jīng)正在更新一個(gè)已存在課程,并在頁面標(biāo)題使用它。否則,我們創(chuàng)建一個(gè)新的Course對(duì)象。

在瀏覽器中打開http://127.0.0.1:8000/course/mine/,然后點(diǎn)擊Create new course。你會(huì)看到以下頁面:

填寫表單,然后點(diǎn)擊Save course按鈕。課程會(huì)被保存,并且你會(huì)被重定向到課程列表頁面,如下圖所示:

然后點(diǎn)擊你剛創(chuàng)建的課程的Edit鏈接。你會(huì)再次看到表單,但這次你在編輯已存在的Course對(duì)象,而不是創(chuàng)建一個(gè)新的。

最后,編輯courses/manage/course/delete.html模板,并添加以下代碼:

{% extends "base.html" %}

{% block title %}Delete course{% endblock title %}

{% 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 content %}

這是CourseDeleteView視圖的模板。這個(gè)視圖從Django提供的DeleteView視圖繼承,它希望用戶確認(rèn)是否刪除一個(gè)對(duì)象。

打開你的瀏覽器,并點(diǎn)擊課程的Delete鏈接。你會(huì)看到以下確認(rèn)頁面:

點(diǎn)擊CONFIRM按鈕。課程會(huì)被刪除,你會(huì)再次被重定向到課程列表頁面。

現(xiàn)在教師可以創(chuàng)建,編輯和刪除課程。下一步,我們將給教師提供一個(gè)內(nèi)容管理系統(tǒng),為課程添加單元和內(nèi)容。我們從管理課程單元開始。

10.5.6 使用表單集

Django自帶一個(gè)抽象層,可以在同一個(gè)頁面使用多個(gè)表單。這些表單組稱為表單集(formsets)。表單集管理多個(gè)確定的FormModelForm實(shí)例。所有表單會(huì)一次性提交,表單集會(huì)負(fù)責(zé)處理一些事情,比如顯示的初始表單數(shù)量,限制最大的提交表單數(shù)量,以及驗(yàn)證所有表單。

表單集包括一個(gè)is_valide()方法,可以一次驗(yàn)證所有表單。你還可以為表單提供初始數(shù)據(jù),并指定顯示多少額外的空表單。

你可以在這里進(jìn)一步學(xué)習(xí)表單集,以及在這里學(xué)習(xí)模型表單集。

10.5.6.1 管理課程單元

因?yàn)橐粋€(gè)課程分為多個(gè)單元,所以這里可以使用表單集。在courses應(yīng)用目錄中創(chuàng)建forms.py,并添加以下代碼:

from django import forms
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()函數(shù)構(gòu)建它。內(nèi)聯(lián)表單集(inline formsets)是表單集之上的一個(gè)小抽象,可以簡(jiǎn)化關(guān)聯(lián)對(duì)象的使用。這個(gè)函數(shù)允許我們動(dòng)態(tài)構(gòu)建一個(gè)模型表單集,把Module對(duì)象關(guān)聯(lián)到一個(gè)Course對(duì)象。

我們使用以下參數(shù)構(gòu)建表單集:

  • fields:在表單集的每個(gè)表單中包括的字段。
  • extra:允許我們?cè)诒韱渭性O(shè)置兩個(gè)額外的空表單。
  • can_delete:如果設(shè)置為True,Django會(huì)為每個(gè)表單包括一個(gè)布爾值字段,該字段渲染為一個(gè)復(fù)選框。它允許你標(biāo)記對(duì)象為刪除。

編輯courses應(yīng)用的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, pk):
        self.course = get_object_or_404(
            Course, id=pk, owner=request.user
        )
        return super().dispatch(request, pk)

    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視圖處理表單集來添加,更新和刪除指定課程的單元。這個(gè)視圖從以下mixins和視圖繼承:

  • TemplateResponseMixin:這個(gè)mixin負(fù)責(zé)渲染模板,并返回一個(gè)HTTP響應(yīng)。它需要一個(gè)template_name屬性,指定被渲染的模板,并提供render_to_response()方法,傳入上下文參數(shù),并渲染模板。
  • View:Django提供的基礎(chǔ)的基于類的視圖。

在這個(gè)視圖中,我們實(shí)現(xiàn)了以下方法:

  • get_formset():我們定義這個(gè)方法,避免構(gòu)建表單集的重復(fù)代碼。我們用可選的data為給定的Course對(duì)象創(chuàng)建ModuleFormSet對(duì)象。
  • dispatch():這個(gè)方法由View類提供。它接收一個(gè)HTTP請(qǐng)求作為參數(shù),并嘗試委托到與使用的HTTP方法匹配的小寫方法:GET請(qǐng)求委托到get()方法,POST請(qǐng)求委托到post()方法。在這個(gè)方法中,我們用get_object_or_404()函數(shù)獲得屬于當(dāng)前用戶,并且ID等于id參數(shù)的Course對(duì)象。因?yàn)镚ET和POST請(qǐng)求都需要檢索課程,所以我們?cè)?code>dispatch()方法中包括這段代碼。我們把它保存在視圖的course屬性,讓其它方法也可以訪問。
  • get():GET請(qǐng)求時(shí)執(zhí)行的方法。我們構(gòu)建一個(gè)空的ModuleFormSet表單集,并使用TemplateResponseMixin提供的render_to_response()方法,把當(dāng)前Course對(duì)象和表單集渲染到模板中。
  • post():POST請(qǐng)求時(shí)執(zhí)行的方法。在這個(gè)方法中,我們執(zhí)行以下操作:
  1. 我們用提交的數(shù)據(jù)構(gòu)建一個(gè)ModuleFormSet實(shí)例。
  2. 我們執(zhí)行表單集的is_valid()方法,驗(yàn)證表單集的所有表單。
  3. 如果表單集有效,則調(diào)用save()方法保存它。此時(shí),添加,更新或者標(biāo)記刪除的單元等任何修改都會(huì)應(yīng)用到數(shù)據(jù)庫中。然后我們重定向用戶到manage_course_list URL。如果表單集無效,則渲染顯示錯(cuò)誤的模板。

編輯courses應(yīng)用的urls.py文件,并添加以下URL模式:

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

courses/manage/模板目錄中創(chuàng)建module目錄。創(chuàng)建courses/manage/module/formset.html模板,并添加以下代碼:

{% extends "base.html" %}

{% block title %}
    Edit "{{ course.title }}"
{% endblock title %}

{% 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 content %}

在這個(gè)模板中,我們創(chuàng)建了一個(gè)<form>元素,其中包括我們的表單集。我們還用{{ formset.management_form }}變量為表單集包括了管理表單。管理表單保存隱藏的字段,用于控制表單的初始數(shù)量,總數(shù)量,最小數(shù)量和最大數(shù)量。正如你所看到的,創(chuàng)建表單集很簡(jiǎn)單。

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

我們已經(jīng)包括了編輯課程單元的鏈接。在瀏覽器中打開http://127.0.0.1:8000/course/mine/,然后點(diǎn)擊一個(gè)課程的Edit modules鏈接,你會(huì)看到如圖所示的表單集:

表單集中包括課程中每個(gè)Module對(duì)象的表單。在這些表單之后,顯示了兩個(gè)額外的空表單,這是因?yàn)槲覀優(yōu)?code>ModuleFormSet設(shè)置了extra=2。當(dāng)你保存表單集時(shí),Django會(huì)包括另外兩個(gè)額外字段來添加新單元。

10.5.7 添加內(nèi)容到課程單元

現(xiàn)在我們需要一種添加內(nèi)容到課程單元的方式。我們有四種不同類型的內(nèi)容:文本,視頻,圖片和文件。我們可以考慮創(chuàng)建四個(gè)不同的視圖,來為每種模型創(chuàng)建內(nèi)容。但是我們會(huì)用更通用的方法:創(chuàng)建一個(gè)可以處理創(chuàng)建或更新任何內(nèi)容模型對(duì)象的視圖。

編輯courses應(yīng)用的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().dispatch(request, module_id, model_name, id)

這是ContentCreateUpdateView的第一部分。它允許我們創(chuàng)建和更新不同模型的內(nèi)容。這個(gè)視圖定義了以下方法:

  • get_model():在這里,我們檢查給定的模型名稱是否為四種內(nèi)容模型之一:文本,視頻,圖片或文件。然后我們用Django的apps.get_model()獲得給定模型名的實(shí)際類。如果給定的模型名不是四種之一,則返回None
  • get_form():我們用表單框架的modelform_factory()函數(shù)動(dòng)態(tài)構(gòu)建表單。因?yàn)槲覀円獮?code>Text,VideoImageFile模型構(gòu)建表單,所以我們使用exclude參數(shù)指定要從表單中排出的字段,而讓剩下的所有字段自動(dòng)包括在表單中。這樣我們不用根據(jù)模型來包括字段。
  • dispatch():它接收以下URL參數(shù),并用類屬性存儲(chǔ)相應(yīng)的單元,模型和內(nèi)容對(duì)象:
  • module_id:內(nèi)容會(huì)關(guān)聯(lián)的單元的ID。
  • model_name:內(nèi)容創(chuàng)建或更新的模型名。
  • id:被更新的對(duì)象的ID。創(chuàng)建新對(duì)象時(shí)為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請(qǐng)求時(shí)執(zhí)行。我們?yōu)楸桓碌?code>Text,VideoImage或者File實(shí)例構(gòu)建模型表單。否則我們不會(huì)傳遞實(shí)例來創(chuàng)建新對(duì)象,因?yàn)槿绻麤]有提供id,則self.obj為None。
  • post():收到POST請(qǐng)求時(shí)執(zhí)行。我們傳遞提交的所有數(shù)據(jù)和文件來構(gòu)建模型表單。然后驗(yàn)證它。如果表單有效,我們創(chuàng)建一個(gè)新對(duì)象,并在保存到數(shù)據(jù)庫之前把request.user作為它的所有者。我們檢查id參數(shù)。如果沒有提供id,我們知道用戶正在創(chuàng)建新對(duì)象,而不是更新已存在的對(duì)象。如果這是一個(gè)新對(duì)象,我們?yōu)榻o定的單元?jiǎng)?chuàng)建一個(gè)Content對(duì)象,并把它關(guān)聯(lián)到新的內(nèi)容。

編輯courses應(yīng)用的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:用于創(chuàng)建文本,視頻,圖片或者文件對(duì)象,并把它們添加到一個(gè)單元。它包括module_idmodel_name參數(shù)。第一個(gè)參數(shù)允許我們把新內(nèi)容對(duì)象鏈接到給定的單元。第二個(gè)參數(shù)指定了構(gòu)建表單的內(nèi)容模型。
  • module_content_update:用于更新已存在的文本,視圖,圖片或者文件對(duì)象。它包括module_idmodel_name參數(shù),以及被更新的內(nèi)容的id參數(shù)。

courses/manage/模板目錄中創(chuàng)建content目錄。創(chuàng)建courses/manage/content/form.html模板,并添加以下內(nèi)容:

{% extends "base.html" %}

{% block title %}   
    {% if object %}
        Edit content "{{ object.title }}"
    {% else %}
        Add a new content
    {% endif %}
{% endblock title %}     

{% 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 content %}   

這是ContentCreateUpdateView視圖的模板。在這個(gè)模板中,我們檢查上下文中是否存在object變量。如果存在,則表示正在更新一個(gè)已存在對(duì)象。否則,表示正在創(chuàng)建一個(gè)新對(duì)象。

因?yàn)楸韱沃邪粋€(gè)上傳的FileImage內(nèi)容模型文件,所以我們?cè)?code><form>元素中包括了enctype="multipart/form-data

啟動(dòng)開發(fā)服務(wù)器。為已存在的課程創(chuàng)建一個(gè)單元,然后在瀏覽器中打開http://127.0.0.1:8000/course/module/6/content/image/create/。如果修改的話,請(qǐng)修改URL中的單元ID。你會(huì)看到創(chuàng)建一個(gè)Image對(duì)象的表單,如下圖所示:

先不要提交表單。如果你這么做了,提交會(huì)失敗,因?yàn)槲覀冞€沒有定義module_content_list URL。我們一會(huì)創(chuàng)建它。

我們還需要一個(gè)視圖來刪除內(nèi)容。編輯courses應(yīng)用的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對(duì)象,它會(huì)刪除關(guān)聯(lián)的TextVideoImageFile對(duì)象,最后刪除Content對(duì)象,然后重定向用戶到module_content_list URL,列出單元剩余的內(nèi)容。

編輯courses應(yīng)用的urls.py文件,并添加以下URL模式:

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

現(xiàn)在,教師可以很容易的創(chuàng)建,更新和刪除內(nèi)容。

10.5.8 管理單元和內(nèi)容

我們已經(jīng)構(gòu)建創(chuàng)建,編輯,刪除課程單元和內(nèi)容的視圖。現(xiàn)在,我們需要一個(gè)顯示某個(gè)課程所有單元和列出特定單元所有內(nèi)容的視圖。

編輯courses應(yīng)用的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視圖。這個(gè)視圖用給定的id獲得屬于當(dāng)前用戶的Module對(duì)象,并用給定的單元渲染模板。

編輯courses應(yīng)用的urls.py文件,并添加以下URL模式:

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

templates/courses/manage/module/目錄中創(chuàng)建content_list.html模板,并添加以下代碼:

{% extends "base.html" %}

{% block title %}
    Module {{ module.order|add:1 }}: {{ module.title }}
{% endblock title %}

{% 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 {{ moudle.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 %}

這個(gè)模板用于顯示某個(gè)課程的所有單元,以及選定單元的內(nèi)容。我們迭代課程單元,并在側(cè)邊欄顯示它們。我們還迭代單元的內(nèi)容,并訪問content.item獲得關(guān)聯(lián)的TextVideoImageFile對(duì)象。我們還包括一個(gè)用于創(chuàng)建新文本,視頻,圖片或文件內(nèi)容的鏈接。

我們想知道每個(gè)對(duì)象的item對(duì)象的類型:TextVideoImageFile。我們需要模型名構(gòu)建編輯對(duì)象的URL。除了這個(gè),我們還根據(jù)內(nèi)容的類型,在模板中顯示每個(gè)不同的item。我們可以從模型的Meta類獲得一個(gè)對(duì)象的模型(通過訪問對(duì)象的_meta屬性)。然而,Django不允許在模板中訪問下劃線開頭的變量或?qū)傩裕瑏碜柚乖L問私有數(shù)據(jù)或調(diào)到私有方法。我們可以編寫一個(gè)自定義模板過濾器來解決這個(gè)問題。

courses應(yīng)用目錄中創(chuàng)建以下文件結(jié)構(gòu):

templatetags/
    __init__.py
    course.py

編輯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模板過濾器。我們?cè)谀0逯杏?code>object|model_name獲得一個(gè)對(duì)象的模型名。

編輯templates/courses/manage/module/content_list.html模板,并在{% extends %}模板標(biāo)簽之后添加這一行代碼:

{% load course %}

這會(huì)加載coursse模板標(biāo)簽。然后找到以下代碼:

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

替換為以下代碼:

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

現(xiàn)在我們?cè)谀0逯酗@示item模型,并用模型名構(gòu)建鏈接來編輯對(duì)象。編輯courses/manage/course/list.html模板,并添加一個(gè)到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 %}

新鏈接允許用戶訪問課程第一個(gè)單元的內(nèi)容(如果存在的話)。

在瀏覽器中打開http://127.0.0.1:8000/course/mine/,并點(diǎn)擊至少包括一個(gè)單元的課程的Manage contents鏈接。你會(huì)看到如圖所示的頁面:

當(dāng)你點(diǎn)擊左邊欄的單元,則會(huì)在主區(qū)域顯示它的內(nèi)容。模板還包括鏈接,用于添加文本,視頻,圖片或文件內(nèi)容到顯示的單元。添加一組不同的內(nèi)容到單元中,并看一下眼結(jié)果。內(nèi)容會(huì)在Module contents下面顯示,如下圖所示:

10.5.9 重新排序單元和內(nèi)容

我們需要提供一種簡(jiǎn)單的方式對(duì)課程單元和它們的內(nèi)容重新排序。我們將使用一個(gè)JavaScript拖放組件,讓用戶通過拖拽對(duì)課程的單元進(jìn)行重新排序。當(dāng)用戶完成拖拽一個(gè)單元,我們會(huì)發(fā)起一個(gè)異步請(qǐng)求(AJAX)來存儲(chǔ)新的單元序號(hào)。

我們需要一個(gè)視圖接收用JSON編碼的單元id的新順序。編輯courses應(yīng)用的views.py文件,并添加以下代碼:

from braces.views import CsrfExemptMixin
from braces.views import 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'
        })

這是ModuleOrderView視圖。我們使用了django-braces的以下mixins:

  • CsrfExemptMixin:避免在POST請(qǐng)求中檢查CSRF令牌。我們需要它執(zhí)行AJAX POST請(qǐng)求,而不用生成csrf_token
  • JsonRequestResponseMixin:解析數(shù)據(jù)為JSON格式,并序列化響應(yīng)為JSON,同時(shí)返回帶application/json內(nèi)容類型的HTTP響應(yīng)。

我們可以構(gòu)建一個(gè)類似的視圖來排序單元的內(nèi)容。在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'
        })

現(xiàn)在編輯courses應(yīng)用的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'),

最后,我們需要在模板中實(shí)現(xiàn)拖放功能。我們將使用jQuery UI庫實(shí)現(xiàn)這個(gè)功能。jQuery UI構(gòu)建在jQuery之上,它提供了一組界面交互,效果和組件。我們將使用它的sortable元素。首先,我們需要在基礎(chǔ)模板中加載jQuery UI。打開courses應(yīng)用中templates目錄的base.html文件,在加載jQuery下面加載jQuery UI:

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js"></script>

我們?cè)趈Query框架之后加載jQuery UI庫。現(xiàn)在編輯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(content_order),
            });
        }
    });
{% endblock domready %}

這段JavaScript代碼在{% block domready %}塊中,因此它會(huì)包括在jQuery的$(document).ready()事件中,這個(gè)事件在base.html模板中定義。這確保了一旦頁面加載完成,就會(huì)執(zhí)行我們的JavaScript代碼。我們?yōu)閭?cè)邊欄中的單元列表和單元的內(nèi)容列表定義了兩個(gè)不同的sortable元素。它們以同樣的方式工作。在這段代碼中,我們執(zhí)行了以下任務(wù):

  1. 首先,我們?yōu)?code>modules元素定義了一個(gè)sortable元素。記住,因?yàn)閖Query選擇器使用CSS語法,所以我們使用了#modules
  2. 我們?yōu)?code>stop事件指定了一個(gè)函數(shù)。每次用戶完成對(duì)一個(gè)元素排序,會(huì)觸發(fā)這個(gè)事件。
  3. 我們創(chuàng)建了一個(gè)空的modules_order字典。這個(gè)字段的key是單元的id,值是分配給每個(gè)單元的序號(hào)。
  4. 我們迭代#modules的子元素。我們重新計(jì)算每個(gè)單元的顯示序號(hào),并獲得它的data-id屬性,其中包括了單元的id。我們添加idmodules_order字段的key,單元的新索引作為值。
  5. 我們發(fā)起一個(gè)AJAX POST請(qǐng)求到content_order URL,在請(qǐng)求中包括modules_order序列化后的JSON數(shù)據(jù)。相應(yīng)的ModuleOrderView負(fù)責(zé)更新單元序號(hào)。

對(duì)內(nèi)容進(jìn)行排序的sortable元素跟它很類似。回到瀏覽器中,重新加載頁面。現(xiàn)在你可以點(diǎn)擊和拖拽單元和內(nèi)容,對(duì)它們進(jìn)行排序,如下圖所示:

非常棒!你現(xiàn)在可以對(duì)課程單元和單元內(nèi)容重新排序了。

10.6 總結(jié)

在本章中,你學(xué)習(xí)了如果創(chuàng)建一個(gè)多功能的內(nèi)容管理系統(tǒng)。你使用了模型繼承,并創(chuàng)建自定義模型字段。你還使用了基于類的視圖和mixins。你創(chuàng)建了表單集和一個(gè)系統(tǒng),來管理不同類型的內(nèi)容。

下一章中,你會(huì)創(chuàng)建一個(gè)學(xué)生注冊(cè)系統(tǒng)。你還會(huì)渲染不同類型的內(nèi)容,并學(xué)習(xí)如何使用Django的緩存框架。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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