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

10 构建一个在线学习平台

10.5 创建内容管理系统

现在我们已经创建了一个万能的数据模型,接下来我们会创建一个内容管理系统(CMS)。CMS允许教师创建课程,并管理它们的内容。我们需要以下功能:

  • 登录到CMS
  • 教师创建的课程列表
  • 创建,编辑和删除课程
  • 添加单元到课程,并对它们重新排序
  • 添加不同类型的内容到每个单元中,并对它们重新排序

10.5.1 添加认证系统

我们将在平台中使用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/', admin.site.urls),
]

10.5.2 创建认证模板

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 %}</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>

这是基础模板,其它模板会从它扩展。在这个模板中,我们定义了以下块:

  • title:其它模块用来为每个页面添加自定义标题的块。
  • content:主要的内容块。所有扩展基础模板的模板必须在这个块中添加内容。
  • domready:位于jQuery的$(document).ready()函数内。允许我们在DOM完成加载时执行代码。

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

编辑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视图的标准登录模板。编辑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 %}

用户登出后会显示这个模板。执行python manage.py runserver命令启动开发服务器,然后在浏览器中打开http://127.0.0.1:8000/accounts/login/,你会看到以下登录页面:

10.5.3 创建基于类的视图

我们将构建用于创建,编辑和删除课程的视图。我们将使用基于类的视图。编辑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().get_queryset()
        return qs.filter(owner=self.request.user)

这是ManageCourseListView视图。它从Django的通用ListView继承。我们覆写了视图的get_queryset()方法,只检索当前用户创建的课程。要阻止用户编辑,更新或者删除不是他们创建的课程,我们还需要在创建,更新和删除视图中覆写get_queryset()方法。当你需要为数个基于类的视图提供特定行为,推荐方式是使用minxins

10.5.4 为基于类的视图使用mixins

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

  • 你想为一个类提供多个可选的特性
  • 你想在数个类中使用某个特性

你可以在这里阅读如何在基于类的视图中使用mixins的文档。

Django自带几个mixins,为基于类的视图提供额外的功能。你可以在这里找到所有mixins。

我们将创建包括一个常见功能的mixins类,并把它用于课程的视图。编辑courses应用的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')

在这段代码中,我们创建了OwnerMixinOwnerEditMixin两个mixins。我们与Django提供的ListViewCreateViewUpdateViewDeleteView视图一起使用这些mixins。OwnerMixin实现了以下方法:

  • get_queryset():视图用这个方法获得基本的QuerySet。我们的mixin会覆写这个方法,通过owner属性过滤对象,来检索属于当前用户的对象(request.user)。

OwnerEditMixin实现以下方法:

  • form_valid():使用Django的ModelFormMixin的视图会使用这个方法,比如,带表单或者模型表单的视图(比如CreateViewUpdateView)。当提交的表单有效时,会执行form_valid()。这个方法的默认行为是保存实例(对于模型表单),并重定向用户到success_url。我们覆写这个方法,在被保存对象的owner属性中自动设置当前用户。这样,当保存对象时,我们自动设置了对象的owner

我们的OwnerMixin类可用于与包括owner属性的任何模型交互的视图。

我们还定义了一个OwnerCourseMixin,它从OwnerMixin继承,并为子视图提供以下属性:

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

我们用以下属性定义了一个OwnerCourseEditMixin

  • fields:模型的这个字段构建了CreateViewUpdateView视图的模型表单。
  • success_url:当表单提交成功后,CreateViewUpdateView用它重定向用户。

最后,我们创建从OwnerCourseMixin继承的视图:

  • ManageCourseListView:列出用户创建的课程。它从OwnerCourseMixinListView继承。
  • CourseCreateView:用模型表单创建一个新的Course对象。它用在OwnerCourseEditMixin中定义的字段来构建模型表单,它还从CreateView继承。
  • CourseUpdateView:允许编辑一个已存在的Course对象。它从OwnerCourseEditMixinUpdateView继承。
  • CourseDeleteView:从OwnerCourseMixin和通用的DeleteView继承。定义了success_url,用于删除对象后重定向用户。

10.5.5 使用组和权限

我们已经创建了管理课程的基础视图。当前,任何用户都可以访问这些视图。我们想限制这些视图,只有教师有权限创建和管理课程。Django的认证框架包括一个权限系统,允许你给用户和组分配权限。我们将为教师用户创建一个组,并分配创建,更新和删除课程的权限。

使用python manage.py runserver命令启动开发服务器,并在浏览器中打开http://127.0.0.1:8000/admin/auth/group/add/,然后创建一个新的Group对象。添加组名为Instructors,并选择courses应用的所有权限,除了Subject模型的权限,如下图所示:

正如你所看到,每个模型有三个不同的权限:Can addCan changeCan delete。为这个组选择权限后,点击Save按钮。

Django自动为模型创建权限,但你也可以创建自定义权限。你可以在这里阅读更多关于添加自定义权限的信息。

打开http://127.0.0.1:8000/admin/auth/user/add/,然后添加一个新用户。编辑用户,并把它添加Instructors组,如下图所示:

用户从它所属的组中继承权限,但你也可以使用管理站点为单个用户添加独立权限。is_superuser设置为True的用户自动获得所有权限。

10.5.5.1 限制访问基于类的视图

我们将限制访问视图,只有拥有适当权限的用户才可以添加,修改或删除Course对象。认证框架包括一个permission_required装饰器来限制访问视图。Django 1.9将会包括基于类视图的权限mixins。但是Django 1.8不包括它们。因此,我们将使用第三方模块django-braces提供的权限mixins。

译者注:现在Django的最新版本是1.11.X。

Django-braces是一个第三方模块,其中包括一组通用的Django mixins。这些mixins为基于类的视图提供了额外的特性。你可以在这里查看django-braces提供的所有mixins。

使用pip命令安装django-braces:

pip install django-braces

我们将使用django-braces的两个mixins来限制访问视图:

  • LoginRequiredMixin:重复login_required装饰器的功能。
  • PermissionRequiredMixin:允许有特定权限的用户访问视图。记住,超级用户自动获得所有权限。

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

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

然后在创建,更新和删除视图中添加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属性中之指定的权限。现在只有合适权限的用户可以访问我们的视图。

让我们为这些视图创建URL。在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模式:

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/目录中创建以下目录和文件:

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视图的模板。在这个模板中,我们列出了当前用户创建的课程。我们包括了编辑或删除每个课程的链接,和一个创建新课程的链接。

使用python manage.py runserver命令启动开发服务器。在浏览器中打开http://127.0.0.1:8000/accounts/login/?next=/course/mine/,并用属于Instructors组的用户登录。登录后,你会重定向到http://127.0.0.1:8000/course/mine/,如下所示:

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

让我们创建模板,显示创建和更新课程视图的表单。编辑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视图。在这个模板中,我们检查上下文是否存在object变量。如果上下文中存在object,我们已经正在更新一个已存在课程,并在页面标题使用它。否则,我们创建一个新的Course对象。

在浏览器中打开http://127.0.0.1:8000/course/mine/,然后点击Create new course。你会看到以下页面:

填写表单,然后点击Save course按钮。课程会被保存,并且你会被重定向到课程列表页面,如下图所示:

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

最后,编辑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视图的模板。这个视图从Django提供的DeleteView视图继承,它希望用户确认是否删除一个对象。

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

点击CONFIRM按钮。课程会被删除,你会再次被重定向到课程列表页面。

现在教师可以创建,编辑和删除课程。下一步,我们将给教师提供一个内容管理系统,为课程添加单元和内容。我们从管理课程单元开始。

10.5.6 使用表单集

Django自带一个抽象层,可以在同一个页面使用多个表单。这些表单组称为表单集(formsets)。表单集管理多个确定的FormModelForm实例。所有表单会一次性提交,表单集会负责处理一些事情,比如显示的初始表单数量,限制最大的提交表单数量,以及验证所有表单。

表单集包括一个is_valide()方法,可以一次验证所有表单。你还可以为表单提供初始数据,并指定显示多少额外的空表单。

你可以在这里进一步学习表单集,以及在这里学习模型表单集。

10.5.6.1 管理课程单元

因为一个课程分为多个单元,所以这里可以使用表单集。在courses应用目录中创建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()函数构建它。内联表单集(inline formsets)是表单集之上的一个小抽象,可以简化关联对象的使用。这个函数允许我们动态构建一个模型表单集,把Module对象关联到一个Course对象。

我们使用以下参数构建表单集:

  • fields:在表单集的每个表单中包括的字段。
  • extra:允许我们在表单集中设置两个额外的空表单。
  • 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, 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视图处理表单集来添加,更新和删除指定课程的单元。这个视图从以下mixins和视图继承:

  • TemplateResponseMixin:这个mixin负责渲染模板,并返回一个HTTP响应。它需要一个template_name属性,指定被渲染的模板,并提供render_to_response()方法,传入上下文参数,并渲染模板。
  • View:Django提供的基础的基于类的视图。

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

  • get_formset():我们定义这个方法,避免构建表单集的重复代码。我们用可选的data为给定的Course对象创建ModuleFormSet对象。
  • dispatch():这个方法由View类提供。它接收一个HTTP请求作为参数,并尝试委托到与使用的HTTP方法匹配的小写方法:GET请求委托到get()方法,POST请求委托到post()方法。在这个方法中,我们用get_object_or_404()函数获得属于当前用户,并且ID等于id参数的Course对象。因为GET和POST请求都需要检索课程,所以我们在dispatch()方法中包括这段代码。我们把它保存在视图的course属性,让其它方法也可以访问。
  • get():GET请求时执行的方法。我们构建一个空的ModuleFormSet表单集,并使用TemplateResponseMixin提供的render_to_response()方法,把当前Course对象和表单集渲染到模板中。
  • post():POST请求时执行的方法。在这个方法中,我们执行以下操作:
  1. 我们用提交的数据构建一个ModuleFormSet实例。
  2. 我们执行表单集的is_valid()方法,验证表单集的所有表单。
  3. 如果表单集有效,则调用save()方法保存它。此时,添加,更新或者标记删除的单元等任何修改都会应用到数据库中。然后我们重定向用户到manage_course_list URL。如果表单集无效,则渲染显示错误的模板。

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

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

courses/manage/模板目录中创建module目录。创建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 %}

在这个模板中,我们创建了一个<form>元素,其中包括我们的表单集。我们还用{{ formset.management_form }}变量为表单集包括了管理表单。管理表单保存隐藏的字段,用于控制表单的初始数量,总数量,最小数量和最大数量。正如你所看到的,创建表单集很简单。

编辑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链接,你会看到如图所示的表单集:

表单集中包括课程中每个Module对象的表单。在这些表单之后,显示了两个额外的空表单,这是因为我们为ModuleFormSet设置了extra=2。当你保存表单集时,Django会包括另外两个额外字段来添加新单元。

10.5.7 添加内容到课程单元

现在我们需要一种添加内容到课程单元的方式。我们有四种不同类型的内容:文本,视频,图片和文件。我们可以考虑创建四个不同的视图,来为每种模型创建内容。但是我们会用更通用的方法:创建一个可以处理创建或更新任何内容模型对象的视图。

编辑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().dispatch(request, module_id, model_name, id)

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

  • get_model():在这里,我们检查给定的模型名称是否为四种内容模型之一:文本,视频,图片或文件。然后我们用Django的apps.get_model()获得给定模型名的实际类。如果给定的模型名不是四种之一,则返回None
  • get_form():我们用表单框架的modelform_factory()函数动态构建表单。因为我们要为TextVideoImageFile模型构建表单,所以我们使用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请求时执行。我们为被更新的TextVideoImage或者File实例构建模型表单。否则我们不会传递实例来创建新对象,因为如果没有提供id,则self.obj为None。
  • post():收到POST请求时执行。我们传递提交的所有数据和文件来构建模型表单。然后验证它。如果表单有效,我们创建一个新对象,并在保存到数据库之前把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_idmodel_name参数。第一个参数允许我们把新内容对象链接到给定的单元。第二个参数指定了构建表单的内容模型。
  • module_content_update:用于更新已存在的文本,视图,图片或者文件对象。它包括module_idmodel_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 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视图的模板。在这个模板中,我们检查上下文中是否存在object变量。如果存在,则表示正在更新一个已存在对象。否则,表示正在创建一个新对象。

因为表单中包含一个上传的FileImage内容模型文件,所以我们在<form>元素中包括了enctype="multipart/form-data

启动开发服务器。为已存在的课程创建一个单元,然后在浏览器中打开http://127.0.0.1:8000/course/module/6/content/image/create/。如果修改的话,请修改URL中的单元ID。你会看到创建一个Image对象的表单,如下图所示:

先不要提交表单。如果你这么做了,提交会失败,因为我们还没有定义module_content_list 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对象,它会删除关联的TextVideoImageFile对象,最后删除Content对象,然后重定向用户到module_content_list URL,列出单元剩余的内容。

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

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

现在,教师可以很容易的创建,更新和删除内容。

10.5.8 管理单元和内容

我们已经构建创建,编辑,删除课程单元和内容的视图。现在,我们需要一个显示某个课程所有单元和列出特定单元所有内容的视图。

编辑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对象,并用给定的单元渲染模板。

编辑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 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 %}

这个模板用于显示某个课程的所有单元,以及选定单元的内容。我们迭代课程单元,并在侧边栏显示它们。我们还迭代单元的内容,并访问content.item获得关联的TextVideoImageFile对象。我们还包括一个用于创建新文本,视频,图片或文件内容的链接。

我们想知道每个对象的item对象的类型:TextVideoImageFile。我们需要模型名构建编辑对象的URL。除了这个,我们还根据内容的类型,在模板中显示每个不同的item。我们可以从模型的Meta类获得一个对象的模型(通过访问对象的_meta属性)。然而,Django不允许在模板中访问下划线开头的变量或属性,来阻止访问私有数据或调到私有方法。我们可以编写一个自定义模板过滤器来解决这个问题。

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

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模板过滤器。我们在模板中用object|model_name获得一个对象的模型名。

编辑templates/courses/manage/module/content_list.html模板,并在{% extends %}模板标签之后添加这一行代码:

{% load course %}

这会加载coursse模板标签。然后找到以下代码:

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

现在我们在模板中显示item模型,并用模型名构建链接来编辑对象。编辑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链接。你会看到如图所示的页面:

当你点击左边栏的单元,则会在主区域显示它的内容。模板还包括链接,用于添加文本,视频,图片或文件内容到显示的单元。添加一组不同的内容到单元中,并看一下眼结果。内容会在Module contents下面显示,如下图所示:

10.5.9 重新排序单元和内容

我们需要提供一种简单的方式对课程单元和它们的内容重新排序。我们将使用一个JavaScript拖放组件,让用户通过拖拽对课程的单元进行重新排序。当用户完成拖拽一个单元,我们会发起一个异步请求(AJAX)来存储新的单元序号。

我们需要一个视图接收用JSON编码的单元id的新顺序。编辑courses应用的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请求中检查CSRF令牌。我们需要它执行AJAX POST请求,而不用生成csrf_token
  • 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'),

最后,我们需要在模板中实现拖放功能。我们将使用jQuery UI库实现这个功能。jQuery UI构建在jQuery之上,它提供了一组界面交互,效果和组件。我们将使用它的sortable元素。首先,我们需要在基础模板中加载jQuery UI。打开courses应用中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>

我们在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(content_order),
            });
        }
    });
{% endblock domready %}

这段JavaScript代码在{% block domready %}块中,因此它会包括在jQuery的$(document).ready()事件中,这个事件在base.html模板中定义。这确保了一旦页面加载完成,就会执行我们的JavaScript代码。我们为侧边栏中的单元列表和单元的内容列表定义了两个不同的sortable元素。它们以同样的方式工作。在这段代码中,我们执行了以下任务:

  1. 首先,我们为modules元素定义了一个sortable元素。记住,因为jQuery选择器使用CSS语法,所以我们使用了#modules
  2. 我们为stop事件指定了一个函数。每次用户完成对一个元素排序,会触发这个事件。
  3. 我们创建了一个空的modules_order字典。这个字段的key是单元的id,值是分配给每个单元的序号。
  4. 我们迭代#modules的子元素。我们重新计算每个单元的显示序号,并获得它的data-id属性,其中包括了单元的id。我们添加idmodules_order字段的key,单元的新索引作为值。
  5. 我们发起一个AJAX POST请求到content_order URL,在请求中包括modules_order序列化后的JSON数据。相应的ModuleOrderView负责更新单元序号。

对内容进行排序的sortable元素跟它很类似。回到浏览器中,重新加载页面。现在你可以点击和拖拽单元和内容,对它们进行排序,如下图所示:

非常棒!你现在可以对课程单元和单元内容重新排序了。

10.6 总结

在本章中,你学习了如果创建一个多功能的内容管理系统。你使用了模型继承,并创建自定义模型字段。你还使用了基于类的视图和mixins。你创建了表单集和一个系统,来管理不同类型的内容。

下一章中,你会创建一个学生注册系统。你还会渲染不同类型的内容,并学习如何使用Django的缓存框架。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容