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)證框架的login
和logout
視圖:
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)建了OwnerMixin
和OwnerEditMixin
兩個(gè)mixins。我們與Django提供的ListView
,CreateView
,UpdateView
和DeleteView
視圖一起使用這些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è)方法,比如,帶表單或者模型表單的視圖(比如CreateView
和UpdateView
)。當(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)建了CreateView
和UpdateView
視圖的模型表單。 -
success_url
:當(dāng)表單提交成功后,CreateView
和UpdateView
用它重定向用戶。
最后,我們創(chuàng)建從OwnerCourseMixin
繼承的視圖:
-
ManageCourseListView
:列出用戶創(chuàng)建的課程。它從OwnerCourseMixin
和ListView
繼承。 -
CourseCreateView
:用模型表單創(chuàng)建一個(gè)新的Course
對(duì)象。它用在OwnerCourseEditMixin
中定義的字段來構(gòu)建模型表單,它還從CreateView
繼承。 -
CourseUpdateView
:允許編輯一個(gè)已存在的Course
對(duì)象。它從OwnerCourseEditMixin
和UpdateView
繼承。 -
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 add
,Can change
和Can 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
讓OwnerCourseMixin
從LoginRequiredMixin
繼承:
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
模板用于CourseCreateView
和CourseUpdateView
視圖。在這個(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è)確定的Form
或ModelForm
實(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í)行以下操作:
- 我們用提交的數(shù)據(jù)構(gòu)建一個(gè)
ModuleFormSet
實(shí)例。 - 我們執(zhí)行表單集的
is_valid()
方法,驗(yàn)證表單集的所有表單。 - 如果表單集有效,則調(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,Video
,Image
和File
模型構(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,Video
,Image
或者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_id
和model_name
參數(shù)。第一個(gè)參數(shù)允許我們把新內(nèi)容對(duì)象鏈接到給定的單元。第二個(gè)參數(shù)指定了構(gòu)建表單的內(nèi)容模型。 -
module_content_update
:用于更新已存在的文本,視圖,圖片或者文件對(duì)象。它包括module_id
和model_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è)上傳的File
和Image
內(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)的Text
,Video
,Image
或File
對(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)的Text
,Video
,Image
或File
對(duì)象。我們還包括一個(gè)用于創(chuàng)建新文本,視頻,圖片或文件內(nèi)容的鏈接。
我們想知道每個(gè)對(duì)象的item
對(duì)象的類型:Text
,Video
,Image
或File
。我們需要模型名構(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ù):
- 首先,我們?yōu)?code>modules元素定義了一個(gè)
sortable
元素。記住,因?yàn)閖Query選擇器使用CSS語法,所以我們使用了#modules
。 - 我們?yōu)?code>stop事件指定了一個(gè)函數(shù)。每次用戶完成對(duì)一個(gè)元素排序,會(huì)觸發(fā)這個(gè)事件。
- 我們創(chuàng)建了一個(gè)空的
modules_order
字典。這個(gè)字段的key
是單元的id
,值是分配給每個(gè)單元的序號(hào)。 - 我們迭代
#modules
的子元素。我們重新計(jì)算每個(gè)單元的顯示序號(hào),并獲得它的data-id
屬性,其中包括了單元的id
。我們添加id
為modules_order
字段的key
,單元的新索引作為值。 - 我們發(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的緩存框架。