外观
Django框架-在线学习平台内容管理
约 17004 字大约 57 分钟
2026-03-24
在上一节中,我们创建了在线学习平台的数据模型,并学习了如何创建和执行数据库迁移。
在本节中,我们将继续构建教师创建课程、管理课程内容等功能。
本节将学习如何:
为内容管理系统构建身份验证视图
使用基于类的视图和 mixins 创建内容管理系统(CMS)
构建表单集(formset)和模型表单集,用于编辑课程模块和模块内容
管理用户组和权限
实现拖放功能,以便重新排列模块和内容。
创建用于显示课程信息的公开视图
建立学生注册系统
管理学生课程注册
为课程模块提供多样化的内容
项目代码:educa3.zip
1、 添加身份验证视图
既然已经创建了一个多态数据模型,接下来我们将构建一个内容管理系统(CMS)来管理课程及其内容。第一步是为 CMS 添加身份验证系统。
1.1 添加身份验证系统
我们将使用 Django 自带的身份验证框架来实现用户登录功能。在之前的”构建社交网站”项目中,我们已经学过如何使用 Django 的身份验证视图。
教师和学生都是 Django User 模型的实例,因此他们都可以通过 django.contrib.auth 提供的身份验证视图来登录网站。
编辑项目 educa 下的 urls.py 文件,添加 Django 身份验证框架的 login 和 logout 视图:
from django.conf import settings # 导入设置
from django.conf.urls.static import static # 导入静态文件处理模块
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import path
urlpatterns = [
path( # 登录视图
'accounts/login/',
auth_views.LoginView.as_view(),
name='login'
),
path( # 登出视图
'accounts/logout/',
auth_views.LogoutView.as_view(),
name='logout',
),
path('admin/', admin.site.urls),
]
# 静态文件处理
if settings.DEBUG:
urlpatterns += static(
settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
)接下来,我们来创建身份验证所需的模板。
1.2 创建身份验证模板
在 courses 应用的 templates 目录下创建如下文件结构:
templates/
base.html
registration/
login.html
logged_out.html在构建身份验证模板之前,需要先准备项目的基础模板。编辑 base.html 模板文件,添加以下代码:
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>{% block title %}在线学习平台{% endblock %}</title>
<link href="{% static "css/base.css" %}" rel="stylesheet">
</head>
<body>
<div id="header">
<a href="/" class="logo">在线学习平台</a>
<ul class="menu">
{% if request.user.is_authenticated %}
<li>
<form action="{% url "logout" %}" method="post">
<button type="submit">退出</button>
{% csrf_token %}
</form>
</li>
{% else %}
<li><a href="{% url "login" %}">登录</a></li>
{% endif %}
</ul>
</div>
<div id="content">
{% block content %}
{% endblock %}
</div>
{% block include_js %}
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', (event) => {
// DOM loaded
{% block domready %}
{% endblock %}
})
</script>
</body>
</html>这是基础模板,其余模板都会继承它。在这个模板中,我们定义了以下几个 block:
title:其他模板可以通过这个 block 为每个页面设置自定义标题。content:主要内容区域。所有继承基础模板的子模板都应该在这个 block 中添加内容。domready位于事件的 JavaScript 事件监听器内部。这允许在文档对象模型( DOM ) 完成操作后执行代码。
编辑registration/login.html模板并向其中添加以下代码:
{% extends "base.html" %}
{% block title %}登录界面{% endblock %}
{% block content %}
<h1>登录界面</h1>
<div class="module">
{% if form.errors %}
<p>的用户名和密码不匹配。请重试。</p>
{% else %}
<p>请使用以下表单登录:</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="登录"></p>
</form>
</div>
</div>
{% endblock %}这是 Django 视图的标准登录模板login。 编辑registration/logged_out.html模板并添加以下代码:
{% extends "base.html" %}
{% block title %}退出登录{% endblock %}
{% block content %}
<h1>退出登录</h1>
<div class="module">
<p>
已成功注销。
可以<a href="{% url "login" %}">再次登录</a>.
</p>
</div>
{% endblock %}这是模板,用户登出后将显示此信息。使用以下命令运行开发服务器:
python manage.py runserver在浏览器中打开http://127.0.0.1:8000/accounts/login/。应该会看到登录页面: 
使用超级用户凭据登录。将被重定向到 URL http://127.0.0.1:8000/accounts/profile/,这是模块的默认重定向 URL 。由于指定的 URL 尚不存在,auth将收到 HTTP响应。成功登录后重定向用户的 URL 在设置中定义。现在,还可以看到“退出登录”按钮。在页面顶部。点击“注销”按钮。现在应该看到“已注销”页面,如图所示:

账户已注销页面,已成功为内容管理系统创建了身份验证系统。
2、 创建内容管理系统 (CMS)
既然已经创建了一个功能全面的数据模型,接下来将构建内容管理系统 (CMS)。该 CMS 将允许教师创建课程并管理课程内容。需要提供以下功能:
列出该教师开设的课程。
创建、编辑和删除课程
向课程中添加模块并重新排序
为每个模块添加不同类型的内容
重新排列课程模块和内容
接下来从基本的 CRUD 视图开始。
2.1 创建基于类的视图
构建视图创建、编辑和删除课程。将使用基于类的视图来实现这些功能。
编辑课程 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)基于类的视图 (CBV) 标准化机甲舱
FBV (Function) vs CBV (Class-Based) Architecture
手摇式脚踏车:FBV 函数视图
⚙️
def view(request):if req == 'GET': # 手写路由...⚙️
耗时较长、重复造轮子...
军工业自动化机甲舱:CBV 类视图
get_queryset(): 自动化抓取模型数据... get_context_data(): 上下文装配完毕... render_to_response(): 模板对接,输出响应! class ListView(View):在早期的 Django 开发中,我们习惯于编写**基于函数的视图 (FBV)**。它就像一辆简易的手摇脚踏车,你必须亲手处理每一个 if request.method == 'GET',编写重复的对象提取逻辑与模板渲染器,耗心耗力。而 django.views.generic 提供的**基于类的视图 (CBV)** 提供了一整套高度标准化的“模块化机甲驾驶舱”。如 ListView 或 CreateView,它们内部预装了全套的智能微控制器(如自动调用 get_queryset() 获取数据字典,自动分发 dispatch() 路由 HTTP 动词)。你只需填入两个参数(如 model = Course),剩下的脏活累活机制全部由自动化仪表盘透明处理结束!
In nascent Django engineering, we frequently crafted **Function-Based Views (FBV)**. Comparable to manually pedaling a rudimentary bicycle, you are forced to literally write `if request.method == 'GET'`, redundantly query objects, and write identical rendering subroutines over and over. However, `django.views.generic` exposes **Class-Based Views (CBV)**, representing a fully standardized modular Mech Cockpit. Highly-evolved archetypes like `ListView` strictly enforce pre-installed architectural avionics (such as natively firing `get_queryset()` to funnel dictionary objects and piping through `dispatch()` for HTTP verb routing). By simply declaring parameters like `model=Course`, the heavy-lifting logic is subliminally handled by the mechanized automation pipeline.
这是ManageCourseListView视图。它继承自 Django 的通用类ListView。需要重写get_queryset()视图的 create 方法,以便仅检索当前用户创建的课程。为了防止用户编辑、更新或删除他们未创建的课程,还需要重写get_queryset()创建、更新和删除视图中的相应方法。当需要为多个基于类的视图提供特定行为时,建议使用mixin。
2.2 使用 mixin 实现基于类的视图
Mixins 是特别的类的多重继承。如果是 Python 中混入(mixin)的新手,只需要理解混入是一种专门为其他类提供方法的类,但它本身并不适合独立使用。这使得可以开发共享功能,并通过让这些类继承混入类,以模块化的方式将它们集成到不同的类中。混入的概念类似于基类,但可以使用多个混入来扩展给定类的功能。
使用 mixin 主要有两种情况:
想为一个类提供多个可选功能
想在多个类中使用某个特定功能
Django 提供了一些 mixin,可以为基于类的视图提供额外的功能。可以在https://docs.djangoproject.com/en/5.2/topics/class-based-views/mixins/了解更多关于 mixin 的信息。
将为 mixin 类中的多个视图实现一个通用的行为,并将其用于……课程浏览量。编辑views.py找到应用程序文件并按courses如下方式修改:
from django.urls import reverse_lazy # 导入反向URL解析函数
# 创建视图类、删除视图类、更新视图类
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.list import ListView # 列表视图类
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 # 课程模型
fields = ['subject', 'title', 'slug', 'overview'] # 课程字段
success_url = reverse_lazy('manage_course_list') # 成功重定向URL
# 课程所有者编辑混入类
class OwnerCourseEditMixin(OwnerCourseMixin, OwnerEditMixin):
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'外骨骼多重继承:Mixin 混入接驳台
Class Inheritance: Injecting behavior via OwnerMixin & OwnerEditMixin
OwnerMixin
👁️ 专属雷达
qs.filter(owner=user) class CourseUpdateView(...)
UpdateView
model=Coursefields=[...]OwnerEditMixin
⚙️ 机动臂烙印
form.instance.owner = user仅仅依靠一套原厂标准的 CBV 机甲是不够的,我们需要定制化!在面向对象编程的世界中,**混入(Mixin)**就等同于“即插即用的机甲外置战术背包”。在这个装配台上,我们的核心躯干是一台原生的 UpdateView(它只懂得通用的对象更新保存)。当我们通过多重继承,从左侧为其挂载上 OwnerMixin 装甲时,它瞬间获得了一枚专属雷达(重写了 get_queryset),视野内只能看到属于该用户的课程;当我们从右侧挂载上 OwnerEditMixin 机械组时,它又具备了在表单落地前自动把所属人刻录为当前用户的能力(重载了 form_valid)。通过模块化拼装,我们就诞生了一台高度特化的 CourseUpdateView 定制机!
Relying purely on factory-standard CBV mechs is insufficient; we demand tactical customization! In object-oriented paradigms, **Mixins** act as "Plug-and-play External Armor Configurations". Within this assembly bay, the central torso operates as a generic `UpdateView` (merely capable of blind saving). By engaging multiple inheritance, dragging the `OwnerMixin` plate onto the left socket equips a proprietary targeting radar (overriding `get_queryset`), restricting its vision solely to the user's native records. Slapping the `OwnerEditMixin` module onto the right instantly retrofits an automated searing arm (via overidden `form_valid`), which forcibly stamps the `owner=user` credential right before database commit. Through pure modular assembly, we effortlessly architect a hyper-specialized `CourseUpdateView` operational suit!
在这段代码中,将创建 MyQuerySetOwnerMixin和 OwnerEditMixinMyQuerySet mixin。装甲拼接后,将把这些 mixin 与Django 提供的MyQuerySetListView、CreateView MyQuerySetUpdateView、MyQuerySet 和MyQuerySet 视图一起使用。MyQuerySet实现了getQuerySet 方法,视图使用该方法获取基础查询集。的 mixin 将重写此方法,以按属性过滤对象,从而检索属于当前用户的对象。DeleteViewOwnerMixinget_queryset()ownerrequest.userOwnerEditMixin实现该form_valid()方法,该方法由使用 Django mixin 的视图使用ModelFormMixin——即带有表单或模型表单的视图,例如CreateView——并且UpdateView。form_valid()`当提交的表单有效时执行。
此方法的默认行为是保存实例(对于模型表单)并将用户重定向到页面success_url。可以重写此方法,以便在保存对象的属性中自动设置当前用户owner。这样,就可以在保存对象时自动设置其所有者。
我们OwnerMixin班能是用于与包含owner属性的任何模型交互的视图。
还可以定义一个OwnerCourseMixin继承自父类OwnerMixin并为子视图提供以下属性的类:
model:用于 QuerySets 的模型;所有视图都使用它。fieldsCreateView:用于构建模型表单和视图的模型字段UpdateView。success_url:用于在表单成功提交或对象删除后重定向用户。可以使用名为<url_name>的 URLCreateView,该 URL 稍后会创建。UpdateViewDeleteViewmanage_course_list
OwnerCourseEditMixin可以使用以下属性定义mixin:
template_nameCreateView:将用于视图的模板UpdateView。
最后,创建以下子类视图OwnerCourseMixin:
ManageCourseListView:列出用户创建的课程。它继承自OwnerCourseMixin和。它为模板定义了一个用于列出课程的ListView特定属性。template_nameCourseCreateView:使用模型表单创建新Course对象。它使用在 中定义的字段OwnerCourseMixin来构建模型表单及其子类CreateView。它使用 中定义的模板OwnerCourseEditMixin。CourseUpdateView允许编辑现有Course对象。它使用 中定义的字段OwnerCourseMixin来构建模型表单及其子类UpdateView。它使用 中定义的模板OwnerCourseEditMixin。CourseDeleteView继承自OwnerCourseMixin通用类DeleteView。它为模板定义了一个特定template_name属性,用于确认课程删除。
已创建了用于管理课程的基本视图。虽然已自行实现了 CRUD 视图,但第三方应用程序 Neapolitan 允许在单个视图中实现标准的列表、详细信息、创建和删除视图。可以学习更多的关于Neapolitan 位于https://github.com/carltongibson/neapolitan。
接下来,将使用 Django 身份验证组和权限来限制对这些视图的访问。
2.3 与群组和权限相关的工作
目前,任何用户可以访问课程管理视图。希望限制这些视图的访问权限,以便只有教师才能创建和管理课程。
Django 的身份验证框架包含一个权限系统。默认情况下,Django 会为已安装应用程序中的每个模型生成四个权限: created add、view viewd、editdchange和deletedelete。这些权限分别对应于创建新实例、查看现有实例、编辑实例和删除实例的操作。
权限可以直接分配给单个用户或用户组。这种方法通过对权限进行分组简化了用户管理,并增强了应用程序的安全性。
将创建一个教师用户组,并分配创建、更新和删除课程的权限。
使用以下命令运行开发服务器:
python manage.py runserverhttp://127.0.0.1:8000/admin/auth/group/add/在浏览器中打开以创建新Group对象。添加名称 师 并选择 学习平台管理 应用程序的所有权限,但以下权限除外:该 学科 模型的具体内容如下: 
如图所示,每个模型都有四种不同的权限:查看、添加、修改和删除。选择此组的权限后,单击“保存”按钮。
Django 会自动为模型创建权限,但也可以创建自定义权限。将在第 15 章 “构建 API”中学习如何创建自定义权限。可以在https://docs.djangoproject.com/en/5.2/topics/auth/customizing/#custom-permissions阅读更多关于添加自定义权限的信息。
打开http://127.0.0.1:8000/admin/auth/user/add/并创建一个新用户。编辑该用户并将其添加到“教师”组,具体操作如下:
图 :用户组选择
用户继承其所属组的权限,但也可以添加单独的权限。仅限使用管理站点的单个用户。已is_superuser设置为True自动拥有所有权限的用户。
接下来,将通过将权限纳入我们的观点中来实际应用权限。
2.4 限制对基于类的视图的访问
前往限制对视图的访问,以便只有拥有相应权限的用户才能添加、更改或删除Course对象。将使用以下两个 mixin 来django.contrib.auth限制对视图的访问:
LoginRequiredMixin:复制login_required装饰器的功能。PermissionRequiredMixin:授予具有特定权限的用户查看视图的权限。请注意,超级用户自动拥有所有权限。
编辑views.py应用程序文件courses,并添加以下导入语句:
from django.contrib.auth.mixins import (
LoginRequiredMixin, # 登录权限混入类
PermissionRequiredMixin,# 权限混入类
)使其OwnerCourseMixin继承LoginRequiredMixin并PermissionRequiredMixin像这样:
# 课程所有者混入类
class OwnerCourseMixin(
OwnerMixin, LoginRequiredMixin, PermissionRequiredMixin
):
model = Course
fields = ['subject', 'title', 'slug', 'overview']
success_url = reverse_lazy('manage_course_list')然后,添加属性permission_required课程观点如下:
# 课程管理列表视图
class ManageCourseListView(OwnerCourseMixin, ListView):
template_name = 'courses/manage/course/list.html' # 课程列表模板
permission_required = 'courses.view_course' # 查看课程权限
# 课程创建视图
class CourseCreateView(OwnerCourseEditMixin, CreateView):
permission_required = 'courses.add_course' # 添加课程权限
# 课程编辑视图
class CourseUpdateView(OwnerCourseEditMixin, UpdateView):
permission_required = 'courses.change_course' # 更改课程权限
# 课程删除视图
class CourseDeleteView(OwnerCourseMixin, DeleteView):
template_name = 'courses/manage/course/delete.html'
permission_required = 'courses.delete_course'身份验证双重联合权限屏障
Access Control: LoginRequiredMixin & PermissionRequiredMixin
👨💻
游客 (未登录)
LoginRequiredMixin(要求在场)
PermissionRequiredMixin需要 courses.add_course 指令
ManageCourseListView机密后端数据系统
模拟终端日志:等待用户发起模拟访问...
一个坚如磐石的系统由层层安防拦截网构成。在这个模块化拼装机甲中,我们不仅拼装了读写功能的 Mixin,还插入了强大的**鉴权立场发生器**。访问 ManageCourseListView 页面,必须经过双重硬核扫描:第一道蓝色力场 LoginRequiredMixin 犹如门禁,它会无情弹飞所有匿名游客,确保你必须先持有基础的平台登录绿卡;接着进入红色红外基因扫描网 PermissionRequiredMixin 的射程,此时哪怕是已满权限卡池的学生如果未携带 courses.view_course 专属生物密钥,仍会被拒绝访问!唯有指定的 Instructor 身份才能穿透力场,直达核心视图系统。
A fortress-grade system is heavily layered with intersecting security interception nets. Beyond just plugging in functional read/write Mixins into our modular framework, we simultaneously interface sheer **Authentication Forcefield Generators**. Penetrating the `ManageCourseListView` corridor mandates weathering a hardcore dual-scan clearance: The frontline blue pulsefield, `LoginRequiredMixin`, acts as a rudimentary checkpoint, indiscriminately ricocheting unbound anonymous tourists to enforce basic logged-in green-card ownership; Surviving that places you immediately inside the hostile red biometric perimeter of `PermissionRequiredMixin`. Merely possessing a platform identity (like a standard student) without harboring the esoteric `courses.view_course` molecular cryptography will trigger an instantaneous bounceback! Solely designated Instructors can bridge these violent energy barriers to finally access the core view subroutines securely.
PermissionRequiredMixin检查访问视图的用户是否拥有permission_required属性中指定的权限。现在,只有拥有相应权限的用户才能访问的视图。 让我们为这些视图创建 URL。在courses应用程序目录下创建一个名为 .url 的新文件urls.py。将以下代码添加到该文件中:
from django.urls import path
from . import views
urlpatterns = [
path( # 我的课程管理视图
'mine/',
views.ManageCourseListView.as_view(),
name='manage_course_list',
),
path( # 创建课程视图
'create/',
views.CourseCreateView.as_view(),
name='course_create',
),
path( # 编辑课程视图
'<pk>/edit/',
views.CourseUpdateView.as_view(),
name='course_edit',
),
path( # 删除课程视图
'<pk>/delete/',
views.CourseDeleteView.as_view(),
name='course_delete',
),
]这些是课程列表、创建、编辑和删除视图的 URL 模式。pk参数指的是主键字段。请记住,pk 是主键的缩写。每个 Django模型它有一个用作主键的字段。默认情况下,主键是自动生成的id字段。Django 的单个对象通用视图通过该字段检索对象。编辑项目的pk主文件,并按如下方式添加应用程序的 URL 模式。urls.pyeducacourses
- 项目 educa 目录下的 urls.py 配置文件代码如下:
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import include, path
urlpatterns = [
path(
'accounts/login/', auth_views.LoginView.as_view(), name='login'
),
path(
'accounts/logout/',
auth_views.LogoutView.as_view(),
name='logout'
),
path('admin/', admin.site.urls),
path('course/', include('courses.urls')), # 课程列表
]
if settings.DEBUG:
urlpatterns += static(
settings.MEDIA_URL, document_root=settings.MEDIA_ROOT
)需要创造templates/这些视图的模板。在应用程序目录下创建以下目录和文件courses:
courses/
manage/
course/
list.html
form.html
delete.html编辑courses/manage/course/list.html模板并添加以下代码:
{% extends "base.html" %}
{% block title %}我的课程{% endblock %}
{% block content %}
<h1>我的课程</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 %}">编辑</a>
<a href="{% url "course_delete" course.id %}">删除</a>
</p>
</div>
{% empty %}
<p>还没有创建任何课程。</p>
{% endfor %}
<p>
<a href="{% url "course_create" %}" class="button">创建新课程</a>
</p>
</div>
{% endblock %}这是模板此视图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 %}
编辑课程 "{{ object.title }}"
{% else %}
创建新课程
{% endif %}
{% endblock %}
{% block content %}
<h1>
{% if object %}
编辑课程 "{{ object.title }}"
{% else %}
创建新课程
{% endif %}
</h1>
<div class="module">
<h2>课程信息</h2>
<form method="post">
{{ form.as_p }}
{% csrf_token %}
<p><input type="submit" value="保存课程"></p>
</form>
</div>
{% endblock %}模板form.html是此模板同时用于页面视图CourseCreateView和CourseUpdateView文章视图。在此模板中,需要检查object变量是否存在于上下文中。如果变量object存在于上下文中,则表示正在更新现有课程,并将其用于页面标题。否则,将创建一个新Course对象。 http://127.0.0.1:8000/course/mine/在浏览器中打开网站,然后点击“创建新课程”按钮。将看到以下页面:
填写填写表格并点击“保存课程”按钮。课程将被保存,将被重定向到课程列表页面。页面应如下所示:
图:包含一门课程的教师课程页面
然后,点击“编辑”链接。刚刚创建的课程。将再次看到该表单,但这次是编辑现有Course对象,而不是创建新对象。
最后,编辑courses/manage/course/delete.html模板并添加以下代码:
{% extends "base.html" %}
{% block title %}删除课程{% endblock %}
{% block content %}
<h1>删除课程 "{{ object.title }}"</h1>
<div class="module">
<form action="" method="post">
{% csrf_token %}
<p>确定要删除 "{{ object }}" 吗?</p>
<input type="submit" value="确认删除">
</form>
</div>
{% endblock %}这是视图的模板CourseDeleteView。此视图继承DeleteView自 Django 提供的视图,该视图需要用户确认才能删除对象。 在浏览器中打开课程列表,然后点击课程的“删除”链接。应该会看到以下确认页面:
图 :删除课程确认页面
点击“确认”按钮。课程将被删除,将被重新重定向到课程列表页面。
教师可以现在创建、编辑和删除课程。接下来,需要为他们提供一个内容管理系统 (CMS),以便添加课程模块及其内容。将从管理课程模块开始。
3、 管理课程模块及其内容
将要构建一个用于管理课程模块的系统以及它们的内容。需要创建表单,用于管理每门课程的多个模块以及每个模块的不同类型的内容。模块及其内容都必须遵循特定的顺序,并且应该可以重新订购它们使用内容管理系统。
3.1 使用表单集进行课程模块
Django 自带用于在同一页面上处理多个表单的抽象层。这些表单组被称为表单集。表单集管理特定Form表单的多个实例ModelForm。所有表单一次性提交,表单集负责设置初始显示的表单数量、限制可提交表单的最大数量并验证所有表单。
表单集包含一个is_valid()可以一次性验证所有表单的方法。还可以为表单提供初始数据,并指定要显示的额外空白表单的数量。可以访问https://docs.djangoproject.com/en/5.2/topics/forms/formsets/了解更多关于表单集的信息,访问https://docs.djangoproject.com/en/5.2/topics/forms/modelforms/#model-formsets了解更多关于模型表单集的信息。
由于课程被划分为数量不定的模块,因此使用表单集来管理这些模块是合理的。forms.py在应用程序目录中创建一个文件courses,并将以下代码添加到其中:
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, # 允许删除模块
)内联表单集分身克隆矩阵
inlineformset_factory(Course, Module, extra=2)
🧬 母体 Course
PK: 5
PK: 5
management_form 控制台 TOTAL_FORMS: 0 | INITIAL_FORMS: 0 | MAX_NUM: 1000
Form #0 (已存在数据)
id: [ 10 ]
course_id: [ 5 ] 🔒
title: [ "Intro" ]
Form #1 (Extra 空白表单)
id: [ -- ]
course_id: [ 5 ] 🔒
title: [ "" ]
Form #2 (Extra 空白表单)
id: [ -- ]
course_id: [ 5 ] 🔒
title: [ "" ]
在单一页面内管理批量数据对象是一项繁琐的工程。Django 给出的军工厂级解决方案是**内联表单集(Inline Formset)**。把它想象成一个“批量分身克隆矩阵库”。我们只要给工厂提供一个母体 Course 的 ID 参数,并设定 extra=2,按下机器按钮!工厂底层便会拉起 management_form(负责计数统治)。接着,克隆流水线不仅会顺藤摸瓜检索出已经存在的模块表单(Form #0),还会根据 extra 参数凭空制造出长得一模一样但一片空白的备胎表单(Form #1、#2)。最变态的是,它强行利用引力纽带(暗中锁死外键 course_id = 5),将所有这些克隆的子表单和 Course 母体死死拴在一起,无需你在界面中手动选择关联母体!
Marshalling bulk data objects within a unified page traditionally demands tedious scaffolding. Django's military-grade solution is the **Inline Formset**. Conceptualize it as a "Batch Cloning Matrix". We merely supply the factory with a parent `Course` ID and an operational parameter `extra=2`, then engage the heavy machinery! Deep within, the matrix auto-generates the `management_form` (a strict numeric overseer HUD). Subsequently, the cloning pipeline not only regurgitates pre-existing module data forms (Form #0), but magically stamps out identical yet thoroughly blank standby auxiliary forms (Form #1, #2). Most ingeniously, it deploys invisible gravitational tethers (imperatively locking the `course_id` foreign key down to 5), ensuring every single cloned progeny remains inextricably tethered to the parent `Course` matrix without any manual dropdown intervention in the UI!
这是ModuleFormSet表单集。可以使用 Django 提供的函数来构建它。内联表单集是对表单集的简单抽象,它简化了对相关对象的操作。此函数允许为与某个对象相关的对象inlineformset_factory()动态构建模型表单集。ModuleCourse 可以使用以下参数来构建表单集:
fields:表单集中每个表单将包含的字段。extra允许设置要在表单集中显示的空额外表单的数量。can_delete如果启用此选项True,Django 将在每个表单中包含一个布尔字段,该字段将呈现为复选框输入框。可以使用它来标记要删除的对象。
编辑views.py文件在courses应用程序中添加以下代码:
from django.shortcuts import get_object_or_404, redirect
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): # 处理GET请求
formset = self.get_formset() # 获取模块表单集
return self.render_to_response( # 渲染响应
{'course': self.course, 'formset': formset} #渲染课程和表单集
)
def post(self, request, *args, **kwargs): # 处理POST请求
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视图处理表单集,用于添加、更新和删除特定课程的模块。此视图继承自以下 mixin 和视图:
TemplateResponseMixin这个 mixin 负责渲染模板并返回 HTTP 响应。它需要一个template_name属性来指定要渲染的模板,并提供一个render_to_response()方法来传递上下文并渲染模板。View:Django 提供的基本基于类的视图。
在这种观点下,实现以下方法:
get_formset()定义此方法是为了避免重复编写构建表单集的代码。可以ModuleFormSet为给定Course对象创建一个包含可选数据的对象。dispatch()此方法由类提供View。它接收一个 HTTP 请求及其参数,并尝试将其委托给与所用 HTTP 方法匹配的小写方法。GET请求分别委托给<method> get()方法和POST<request>方法。在此方法中,可以使用快捷函数获取当前用户对应的给定参数的对象。之所以将此代码包含在post()<method>方法中,是因为需要检索<request>和<request>请求的课程。将其保存到视图的<request>属性中,以便其他方法可以访问它。get_object_or_404()Courseiddispatch()GETPOSTcourseget():针对请求执行GET。构建一个空表单集,并使用提供的方法ModuleFormSet将其与当前对象一起渲染到模板中。Courserender_to_response()TemplateResponseMixinpost():针对POST请求执行。在此方法中,将执行以下操作:ModuleFormSet使用提交的数据构建一个实例。需要执行
is_valid()表单集的方法来验证其所有表单。如果表单集有效,则调用该
save()方法保存它。此时,所做的任何更改(例如添加、更新或标记模块以进行删除)都会应用到数据库。然后,将用户重定向到 URLmanage_course_list。如果表单集无效,则渲染模板以显示任何错误。
编辑urls.py应用程序文件courses,并向其中添加以下 URL 模式:
path( # 课程模块更新视图
'<pk>/module/',
views.CourseModuleUpdateView.as_view(),
name='course_module_update'
),在模板目录下创建一个新目录courses/manage/,并将其命名为module.Create创建一个courses/manage/module/formset.html模板,并在其中添加以下代码:
{% extends "base.html" %}
{% block title %}
编辑 "{{ course.title }}"
{% endblock %}
{% block content %}
<h1>编辑 "{{ course.title }}"</h1>
<div class="module">
<h2>课程模块</h2>
<form method="post">
{{ formset }}
{{ formset.management_form }}
{% csrf_token %}
<input type="submit" value="保存模块">
</form>
</div>
{% endblock %}在此模板中,需要创建一个<form>HTML 元素,并在其中包含 <formset> 标签formset。还需要使用变量 <formset> 包含表单集的管理表单{{ formset.management_form }}。管理表单包含隐藏字段,用于控制表单的初始数量、总数、最小数量和最大数量。 编辑模板,并在课程“编辑”和“删除”courses/manage/course/list.html链接下方的URL 中添加以下链接:course_module_update
<a href="{% url "course_edit" course.id %}">编辑</a>
<a href="{% url "course_delete" course.id %}">删除</a>
<a href="{% url "course_module_update" course.id %}">编辑模块</a>已添加了编辑课程模块的链接。 在浏览器中打开http://127.0.0.1:8000/course/mine/。创建课程,然后点击“编辑模块”链接。为此,应该会看到如下所示的表单集:
图:课程编辑页面,包括课程模块的表单集
表单集Module其中包含课程中每个对象的表单。之后,由于进行了extra=2设置,还会显示两个额外的空白表单ModuleFormSet。保存表单集时,Django 将添加另外两个字段以添加新模块。
可以看出,表单集对于管理单个页面上的多个表单实例非常有用。表单集简化了从一组相似表单中高效收集和验证数据的过程。
了解之后了解表单集的工作原理,将通过动态创建表单来探索高级表单功能,这些表单可以适应添加到课程模块中的各种类型的内容。
3.2 向课程模块添加内容
现在需要一个向课程模块添加内容的方法。有四种不同类型的内容:文本、视频、图像和文件。可以考虑创建四个不同的视图来创建内容,每个模型对应一个表单。但是,将采用更灵活的方法,创建一个视图来处理任何内容模型对象的创建或更新。将根据教师想要添加到课程中的内容类型(文本Text、Video视频、Image图像或File文件)动态构建此视图的表单。
编辑views.py应用程序文件courses,并将以下代码添加到其中:
from django.apps import apps
from django.forms.models import modelform_factory
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()在这里,需要检查给定的模型名称是否为以下四个内容模型之一:Text <model_name>、Video<model_name>Image、<model_name>或<model_name>File。然后,使用 Django 的apps模块获取给定模型名称对应的实际类。如果给定的模型名称不是有效名称之一,则返回nullNone。get_form()modelform_factory()可以使用表单框架的功能构建动态表单。由于要为<model>、<model>、Text <model>和<model>模型构建表单,因此可以使用参数指定要从表单中排除的公共字段,并让所有其他属性自动包含。这样,就不必根据模型来了解要包含哪些字段。VideoImageFileexcludedispatch()此函数接收以下 URL 参数,并将相应的模块、模型和内容对象存储为类属性:module_id:内容所属/将要所属模块的 ID。model_name要创建/更新的内容的模型名称。id:正在更新的对象的 ID。用于None创建新对象。
添加以下内容get()和post()方法ContentCreateUpdateView:
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:当收到请求时执行。需要为正在更新的实例构建模型表单。否则,由于未提供 ID,将不会传递任何实例来创建新对象Text。VideoImageFileself.objNonepost()当收到请求时执行POST。构建模型表单,并将提交的数据和文件传递给它。然后,验证表单。如果表单有效,则创建一个新对象,并将request.user其所有者指定为新对象,然后再将其保存到数据库。检查参数id。如果未提供 ID,则说明用户正在创建新对象而不是更新现有对象。如果是新对象,则为content给定模块创建一个对象,并将新内容与其关联。
编辑urls.py应用程序文件courses,并向其中添加以下 URL 模式:
path(
'module/<int:module_id>/content/<model_name>/create/',
views.ContentCreateUpdateView.as_view(),
name='module_content_create'
),
path(
'module/<int:module_id>/content/<model_name>/<id>/',
views.ContentCreateUpdateView.as_view(),
name='module_content_update'
),新的URL格式如下:
module_content_create用于创建新的文本、视频、图像或文件对象并将其添加到模块中。它包含contentmodule_id和model_namemodule参数。第一个参数允许将新内容对象链接到给定的模块。第二个参数指定要为其构建表单的内容模型。module_content_update用于更新现有的文本、视频、图像或文件对象。它包含module_idandmodel_name参数和一个id用于标识要更新的内容的参数。
在模板目录下创建一个新目录courses/manage/,并将其命名为content.Createcourses/manage/content/form.html将以下代码添加到模板中:
{% extends "base.html" %}
{% block title %}
{% if object %}
Edit content "{{ object.title }}"
{% else %}
Add new content
{% endif %}
{% endblock %}
{% block content %}
<h1>
{% if object %}
Edit content "{{ object.title }}"
{% else %}
Add 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 %}这是视图的模板ContentCreateUpdateView。在这个模板中,需要检查一个object变量是否存在于上下文中。如果变量object存在于上下文中,则更新现有对象;否则,创建一个新对象。 需要enctype="multipart/form-data"在<form>HTML 元素中包含它,因为表单包含文件上传功能,用于File内容Image模型。
运行打开开发服务器,http://127.0.0.1:8000/course/mine/点击现有课程的“编辑模块”,创建一个模块。
然后,使用以下命令打开 Python shell:
python manage.py shell获取最新创建的模块的 ID,方法如下:
>>> from courses.models import Module
>>> Module.objects.latest('id').id
6Django 5.2 新增功能:shell 现在会自动从的项目中导入所有模型INSTALLED_APPS。Module导入操作会在 shell 启动时自动完成。 运行开发服务器,并在浏览器中打开,将模块 ID 替换为之前获取的 ID。将看到如下所示的http://127.0.0.1:8000/course/module/6/content/image/create/创建对象表单:Image
选择添加文本内容
图:课程“添加新内容”表单
不要提交表单尚未创建。如果尝试这样做,将会失败,因为尚未定义module_content_listURL。稍后将创建它。
还需要一个用于删除内容的视图。编辑应用views.py程序文件courses并添加以下代码:
# 课程内容删除视图
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对象。它会删除相关的Text、、或对象。最后,它会删除该对象并将用户重定向到URL 以列出模块的其他内容。VideoImageFilecontentmodule_content_list 编辑urls.py应用程序文件courses,并向其中添加以下 URL 模式:
path( # 课程内容删除视图
'content/<int:id>/delete/',
views.ContentDeleteView.as_view(),
name='module_content_delete'
),现在,教师可以轻松创建、更新和删除内容。本节所学的方法对于以通用方式管理包含各种数据的表单非常有用。该方法也可应用于其他需要灵活解决方案来处理数据输入的场景。 下一节,我们将创建用于显示课程模块和内容的视图和模板。
3.3 管理模块及其内容
已经建立了需要创建、编辑和删除课程模块及其内容的视图。接下来,还需要一个视图来显示课程的所有模块,并列出特定模块的内容。
编辑views.py应用程序文件courses,并将以下代码添加到其中:
# 课程模块内容列表视图
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视图。此视图获取Module具有给定 ID 的当前用户对象,并使用给定模块渲染模板。 编辑urls.py应用程序文件courses,并向其中添加以下 URL 模式:
path( # 课程内容列表视图
'module/<int:module_id>/',
views.ModuleContentListView.as_view(),
name='module_content_list'
),在目录内创建一个新模板templates/courses/manage/module/并将其命名为content_list.html. 添加将其替换为以下代码:
{% extends "base.html" %}
{% load course %}
{% block title %}
模块 {{ module.order|add:1 }}: {{ module.title }}
{% endblock %}
{% block content %}
{% with course=module.course %}
<h1>课程 "{{ course.title }}"</h1>
<div class="contents">
<h3>模块</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>
模块 <span class="order">{{ m.order|add:1 }}</span>
</span>
<br>
{{ m.title }}
</a>
</li>
{% empty %}
<li>没有模块。</li>
{% endfor %}
</ul>
<p><a href="{% url "course_module_update" course.id %}">
编辑模块</a></p>
</div>
<div class="module">
<h2>模块 {{ module.order|add:1 }}: {{ module.title }}</h2>
<h3>模块内容:</h3>
<div id="module-contents">
{% for content in module.contents.all %}
<div data-id="{{ content.id }}">
{% with item=content.item %}
<p>{{ item }} ({{ item|model_name }})</p>
<a href="{% url "module_content_update" module.id item|model_name item.id %}">
编辑
</a>
<form action="{% url "module_content_delete" content.id %}" method="post">
<input type="submit" value="删除">
{% csrf_token %}
</form>
{% endwith %}
</div>
{% empty %}
<p>此模块暂无内容。</p>
{% endfor %}
</div>
<h3>添加新内容:</h3>
<ul class="content-types">
<li>
<a href="{% url "module_content_create" module.id "text" %}">文本</a>
</li>
<li>
<a href="{% url "module_content_create" module.id "image" %}">图片</a>
</li>
<li>
<a href="{% url "module_content_create" module.id "video" %}">视频</a>
</li>
<li>
<a href="{% url "module_content_create" module.id "file" %}">文件</a>
</li>
</ul>
</div>
{% endwith %}
{% endblock %}请确保模板标签不要跨越多行;Django 模板引擎要求标签清晰定义且不间断。 此模板用于显示课程的所有模块以及所选模块的内容。可以遍历课程模块,将其显示在侧边栏中。还可以遍历模块的内容和访问权限content.item,以获取相关的文本Text、视频Video、图像Image或File文件内容。此外,还可以添加链接以创建新的文本、视频、图像或文件内容。
想知道每个对象是什么类型item: <object> Text、Video<object> Image、<object> 或 File<object>。需要模型名称来构建用于编辑对象的 URL。此外,可以根据内容类型在模板中以不同的方式显示每个项目。可以Meta通过访问对象的 <model>_meta属性从模型的类中获取对象的模型名称。但是,Django 不允许访问以 <model> 开头的变量或属性。模板中的下划线会阻止对私有属性的检索或对私有方法的调用。可以通过编写自定义模板过滤器来解决此问题。
在应用程序目录下创建以下文件结构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 # 如果没有模型名称则返回None这是model_name模板过滤器。可以在模板中应用它,以object|model_name获取对象的模型名称。 编辑templates/courses/manage/module/content_list.html模板,并在模板标签下方添加以下代码行{% extends %}:
{% load course %}这将加载course模板标签。然后,找到以下几行:
<p>{{ item }}</p>
<a href="#">编辑</a>请将它们替换为以下内容:
<p>{{ item }} ({{ item|model_name }})</p>
<a href="{% url "module_content_update" module.id item|model_name item.id %}">
编辑
</a>在前面代码中,可以在模板中显示项目模型名称,并且还可以使用该模型名称来构建编辑对象的链接。 编辑courses/manage/course/list.html模板并添加指向 URL 的链接module_content_list,如下所示:
<a href="{% url "course_module_update" course.id %}">编辑模块</a>
{% if course.modules.count > 0 %}
<a href="{% url "module_content_list" course.modules.first.id %}">
管理内容</a>
{% endif %}新链接允许用户访问课程第一个模块的内容(如果有的话)。 停止开发服务器,然后使用以下命令重新启动它:
python manage.py runserver通过停止并运行开发服务器,可以确保course模板标签文件被加载。 打开http://127.0.0.1:8000/course/mine/并点击包含至少一个模块的课程的“管理内容”链接。将看到类似如下的页面:
图 :管理课程模块内容的页面
当点击在左侧边栏的模块中,其内容会显示在主区域中。该模板还包含用于为当前显示的模块添加新文本、视频、图像或文件内容的链接。
向模块中添加几种不同类型的内容,然后查看结果。模块内容将显示在下方:模块内容:
图:管理不同的模块内容
接下来,我们将允许课程教师通过简单的拖放功能重新排列模块和模块内容。
4、 重新排列模块及其内容
我们将实施一项新增的 JavaScript 拖放功能允许课程教师通过拖动来重新排列课程模块。拖放功能增强了用户界面,提供了一种比使用数字或点击按钮更直观、更自然的元素排序方式。它还能为课程教师节省时间,使他们能够轻松地重新组织课程模块及其内容。
为了实现此功能,我们将使用 HTML5 Sortable 库,该库简化了使用原生 HTML5 拖放 API 创建可排序列表的过程。
当用户完成模块拖动后,将使用 JavaScript Fetch API 向存储新模块顺序的服务器发送异步 HTTP 请求。
可以在https://www.w3schools.com/html/html5_draganddrop.asp阅读更多关于 HTML5 拖放 API 的信息。还可以找到使用 HTML5 构建的示例。HTML5 可排序器库位于https://lukasoppermann.github.io/html5sortable/。文档HTML5 Sortable 库可在https://github.com/lukasoppermann/html5sortable获取。
让我们实现视图来更新课程模块和模块内容的顺序。
4.1 使用来自 django-braces 的 mixins
django-braces是一个第三方模块那包含一系列用于 Django 的通用 mixin。这些 mixin 为基于类的视图提供了额外的功能,适用于各种常见场景。可以django-braces在https://django-braces.readthedocs.io/查看所有 mixin 的列表。
将使用以下 mixin django-braces:
CsrfExemptMixin用于避免检查跨站请求伪造( CSRF ) 令牌在POST请求中。需要这样做才能执行 AJAXPOST请求而无需传递参数csrf_token。JsonRequestResponseMixin:将请求数据解析为 JSON,并将响应序列化为 JSON,并返回具有指定application/json内容类型的 HTTP 响应。
请使用以下命令django-braces进行安装:pip
python -m pip install django-braces==1.17.0需要一个视图,用于接收以 JSON 编码的模块 ID 新顺序,并据此更新顺序。编辑应用views.py程序文件courses,并将以下代码添加到其中:
# 导入CSRF免疫混入类、JSON请求响应混入类
from braces.views import CsrfExemptMixin, 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) # 更新排序
# 返回JSON响应
return self.render_json_response({'saved': 'OK'})此ModuleOrderView视图允许更新课程模块的顺序。 可以建造一个与订单类似的观点一个模块内容。将以下代码添加到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) # 更新排序
# 返回JSON响应
return self.render_json_response({'saved': 'OK'})现在,编辑应用urls.py程序文件courses,并将以下 URL 模式添加到其中:
path( # 课程模块排序视图
'module/order/',
views.ModuleOrderView.as_view(),
name='module_order'
),
path( # 课程内容排序视图
'content/order/',
views.ContentOrderView.as_view(),
name='content_order'
),最后,需要在模板中实现拖放功能。我们将使用 HTML5 Sortable 库,它简化了使用标准 HTML 拖放 API 创建可排序元素的过程。虽然还有其他 JavaScript 库可以实现相同的功能,但我们选择 HTML5 Sortable 是因为它轻量级且利用了原生 HTML5 拖放 API。 编辑base.html位于templates/应用程序目录中的模板courses,添加以下内容堵塞粗体突出显示:
{% load static %}
<!DOCTYPE html>
<html>
<head>
# ...
</head>
<body>
<div id="header">
# ...
</div>
<div id="content">
{% block content %}
{% endblock %}
</div>
{% block include_js %}
{% endblock %}
<script>
document.addEventListener('DOMContentLoaded', (event) => {
// DOM loaded
{% block domready %}
{% endblock %}
})
</script>
</body>
</html>这个名为“新块”的功能include_js允许将 JavaScript 文件插入到任何扩展自该base.html模板的模板中。 接下来,编辑courses/manage/module/content_list.html模板,并将以下以粗体突出显示的代码添加到模板底部:
# ...
{% block content %}
# ...
{% endblock %}
{% block include_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.13.3/html5sortable.min.js"></script>
{% endblock %}这段代码从公共内容分发网络( CDN )加载 HTML5 Sortable 库。请记住加载了一个JavaScript在第 6 章“ 在的网站上共享内容”中,我们提到了从 CDN 获取库。 现在将以下domready以粗体突出显示的代码块添加到courses/manage/module/content_list.html模板中:
# ...
{% block content %}
# ...
{% endblock %}
{% block include_js %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/html5sortable/0.13.3/html5sortable.min.js"></script>
{% endblock %}
{% block domready %}
var options = {
method: 'POST',
mode: 'same-origin'
}
const moduleOrderUrl = '{% url "module_order" %}';
{% endblock %}在这些新增代码行中,需要将 JavaScript 代码添加到模板中{% block domready %}事件监听器定义的代码块中。这可以确保的 JavaScript 代码在页面加载完成后执行。通过这段代码,可以定义用于重新排序模块的 HTTP 请求选项,这些模块将在接下来实现。将使用 Fetch API 发送请求来更新模块顺序。URL路径已构建并存储在 JavaScript 常量中。DOMContentLoadedbase.htmlPOSTmodule_ordermoduleOrderUrl 将以下以粗体突出显示的代码添加到domready代码块中:
{% block domready %}
var options = {
method: 'POST',
mode: 'same-origin'
}
const moduleOrderUrl = '{% url "module_order" %}';
sortable('#modules', {
forcePlaceholderSize: true,
placeholderClass: 'placeholder'
});
{% endblock %}在新代码中,可以使用 sortable<div> 为 HTML 元素定义一个元素id="modules",该元素模块列表这侧边栏。请记住,可以使用 CSS 选择器#来选择具有指定属性的元素id。当开始拖动项目时,HTML5 Sortable 库会创建一个占位符项目,以便轻松查看元素将放置的位置。 可以将选项设置forcePlacehoderSize为truetrue,强制占位符元素具有高度,并使用 classplaceholderClass定义占位符元素的 CSS 类。使用在模板加载的静态文件placeholder中定义的名为 class 的类。css/base.cssbase.html
在浏览器中打开http://127.0.0.1:8000/course/mine/并点击任意课程的“管理内容”。现在,可以将课程模块拖放到左侧边栏中,如图13.12所示:

图 13.12:使用拖放功能重新排列模块
当拖动时该元素,将看Sortable 库创建的占位符元素带有虚线边框。该占位符元素用于指定拖动元素的放置位置。
当将模块拖动到不同位置时,需要向服务器发送 HTTP 请求以保存新的排序结果。这可以通过将事件处理程序附加到可排序元素并使用 JavaScript Fetch API 向服务器发送请求来实现。
编辑模板domready中的相应代码块courses/manage/module/content_list.html,并添加以下以粗体突出显示的代码:
{% block domready %}
var options = {
method: 'POST',
mode: 'same-origin'
}
const moduleOrderUrl = '{% url "module_order" %}';
sortable('#modules', {
forcePlaceholderSize: true,
placeholderClass: 'placeholder'
})[0].addEventListener('sortupdate', function(e) {
modulesOrder = {};
var modules = document.querySelectorAll('#modules li');
modules.forEach(function (module, index) {
// update module index
modulesOrder[module.dataset.id] = index;
// update index in HTML element
module.querySelector('.order').innerHTML = index + 1;
});
// add new order to the HTTP request options
options['body'] = JSON.stringify(modulesOrder);
// send HTTP request
fetch(moduleOrderUrl, options)
});
{% endblock %}在新代码中,事件监听器是此事件是为sortupdate可排序元素的事件创建的。sortupdate当元素被拖放到不同位置时,该事件会被触发。事件函数中执行以下任务:
创建一个空
modulesOrder字典。该字典的键是模块 ID,值包含每个模块的索引。使用 CSS 选择器,
#modules通过 来选择 HTML 元素的列表元素。document.querySelectorAll()#modules liforEach()用于遍历列表中的每个元素。每个模块的新索引都存储在
modulesOrder字典中。每个模块的 ID 可以通过data-id访问 HTML 属性来获取module.dataset.id。字典的键是 IDmodulesOrder,值是模块的新索引。通过选择带有 CSS 类的元素来更新每个模块的显示顺序
order。由于索引是从零开始的,而我们希望显示从一开始的索引,因此我们将其添加1到index。一个名为
order的键body被添加到options字典中,其中包含新的订单信息modulesOrder。该JSON.stringify()方法将 JavaScript 对象转换为 JSON 字符串。这是用于更新模块订单的 HTTP 请求的正文。Fetch API 通过创建
fetch()HTTP 请求来更新模块顺序。ModuleOrderView与该 URL 对应的视图module_order负责更新模块顺序。
现在可以拖放模块了。拖动模块完成后,系统会向 URL 发送 HTTP 请求module_order以更新模块顺序。如果刷新页面,系统将更新模块顺序。这页面,最新模块顺序将被保留,因为它已在数据库中更新。图 13.13显示了使用拖放功能对模块进行排序后,侧边栏中模块的不同顺序:
图 13.13:使用拖放功能重新排序模块后的新顺序
如果遇到任何问题,请记住使用浏览器的开发者工具来调试 JavaScript 和HTTP 请求。通常情况下,可以在网站的任何位置点击鼠标右键打开上下文菜单,然后单击“检查”或“检查元素”以访问浏览器的 Web 开发者工具。
让我们添加同样的拖放功能,以便课程教师也能对模块内容进行排序。
编辑模板domready中的相应代码块courses/manage/module/content_list.html,并添加以下以粗体突出显示的代码:
{% block domready %}
// ...
const contentOrderUrl = '{% url "content_order" %}';
sortable('#module-contents', {
forcePlaceholderSize: true,
placeholderClass: 'placeholder'
})[0].addEventListener('sortupdate', function(e) {
contentOrder = {};
var contents = document.querySelectorAll('#module-contents div');
contents.forEach(function (content, index) {
// update content index
contentOrder[content.dataset.id] = index;
});
// add new order to the HTTP request options
options['body'] = JSON.stringify(contentOrder);
// send HTTP request
fetch(contentOrderUrl, options)
});
{% endblock %}AJAX 磁悬浮物流传输阵列
HTML5 Sortable & Fetch API (Asynchronous sorting updates)
前端界面: HTML5 Sortable
≡模块 ID: 12
order: 0
≡模块 ID: 45
order: 1
≡模块 ID: 89
order: 2
{} ⚡
调用
sortupdate 事件触发调用
fetch() API 后端服务:
ModuleOrderView🛡️
CsrfExemptMixin 放行 > 解析 JsonRequest...
> for id, order in json.items():
> Module.update(order=order)
> return {'saved': 'OK'}
用户对于顺序的排列极其敏感。传统的表单提交不仅会导致整个页面刷新中断思路,而且体验极其割裂。这就需要前端拖拽与后端 AJAX 的联合军工阵列。在左侧的前端磁悬浮阵列中,你依靠 html5sortable 库将模块上下拖动。松手的瞬间,立刻触发原生的 sortupdate 电流事件,它会立刻呼叫一架 Fetch API 匿踪无人机。这架飞机打包携带了一个极其紧凑的 JSON 数据包装载舱(包含“模块ID:新的索引位置”键值对),悄无声息地直飞后端服务器。因为后端配置了坚固的 CsrfExemptMixin 盾牌特许放行,接收器直接解析载荷并在后台循环更新数据库 `order` 字段。整个过程无需页面重启,如丝般顺滑!
Users are exceptionally sensitive to sequence restructuring. Traditional full-page form submissions brutally shatter the cognitive flow. This mandates the deployment of a joint Front-to-Back AJAX militarized apparatus. On the left frontend magnetic-levitation grid, you reorder modules empowered by the `html5sortable` library. The precise millisecond you release the grip, it triggers a raw `sortupdate` electrical event, subsequently scrambling a stealth `Fetch API` delivery drone. This drone compresses a highly compact JSON payload packet (containing the map of `{Module_ID: New_Index}` pairs) and silently rockets into the backend cyberspace server. Because the server rack is explicitly armored with a permissive `CsrfExemptMixin` deflector shield, the receiver digests the payload immediately, executing database `order` updates in the shadows. The entire logistical pipeline operates flawlessly without ever restarting the DOM timeline!
在这种情况下,使用content_orderURL而是module_order构建sortable功能在具有指定 ID 的 HTML 元素上进行操作module-contents。其功能与课程模块排序基本相同。在这种情况下,无需更新内容编号,因为它们不包含任何可见的索引。 现在,可以拖放模块和模块内容,如图13.14所示:
图 13.14:使用拖放功能重新排列模块内容
到目前为止我们建造了一个对于课程讲师来说,这是一个功能非常全面的内容管理系统。
5、显示课程目录
可能是我们迫不及待地想要开始渲染和缓存,但在那之前还有一些设置工作需要完成。让我们从课程目录开始。的课程目录需要构建以下功能:
列出所有可选课程,可按科目筛选
显示单个课程概述
这样一来,学生就能看到平台上所有可用的课程,并报名参加他们感兴趣的课程。编辑应用程序views.py文件courses并添加以下代码:
from django.db.models import Count
from .models import Subject
# 课程列表视图
class CourseListView(TemplateResponseMixin, View):
model = Course # 课程模型
template_name = 'courses/course/list.html' # 模板名称
# 获取课程列表
def get(self, request, subject=None):
# annotate用于添加聚合计数
subjects = Subject.objects.annotate(
total_courses=Count('courses') # 计算每个学科的课程数量
)
# 获取所有课程并注释模块数量
all_courses = Course.objects.annotate(
total_modules=Count('modules') # 计算每个课程的模块数量
)
if subject: # 如果有学科参数则过滤课程
# 获取学科课程
subject = get_object_or_404(Subject, slug=subject)
# 获取学科课程
courses = all_courses.filter(subject=subject)
else:
courses = all_courses # 赋值所有课程
return self.render_to_response(
{
'subjects': subjects,
'subject': subject,
'courses': courses,
}
)这是CourseListView视图。它继承自TemplateResponseMixin和View。在此视图中,执行以下操作:
使用 ORM 的
annotate()方法,通过Count()聚合函数检索所有科目,包括每个科目的课程总数。检索所有可用课程,包括每门课程中包含的模块总数。
如果提供了主题别名 URL 参数,则检索相应的
subject对象并将查询限制为属于给定主题的课程。使用
render_to_response()提供的方法TemplateResponseMixin将对象渲染到模板并返回 HTTP 响应。
让我们创建一个用于显示单个课程概览的详细视图。请将以下代码添加到views.py文件中:
from django.views.generic.detail import DetailView
# 课程详情视图
class CourseDetailView(DetailView):
model = Course # 课程模型
template_name = 'courses/course/detail.html'DetailView此视图继承自Django 提供的通用视图。需要指定 <model>model和 template_name<model> 属性。Django 的 <model>DetailView需要一个主键(<key> pk)或 slug URL 参数来检索给定模型的单个对象。该视图会渲染 <model> 中指定的模板template_name,包括Course模板上下文变量中的对象object。 urls.py编辑项目主文件educa,并向其中添加以下 URL 模式:
from courses.views import CourseListView
urlpatterns = [
# ...
path('', CourseListView.as_view(), name='course_list'), # 课程列表
]将course_listURL 模式添加到项目的主urls.py文件中,因为希望在 URL 中显示课程列表http://127.0.0.1:8000/,并且应用程序的所有其他 URL 都courses具有/course/前缀。 编辑urls.py文件在应用程序中courses添加以下 URL 模式:
path( # 课程学科列表视图
'subject/<slug:subject>/',
views.CourseListView.as_view(),
name='course_list_subject',
),
path( # 课程详情视图
'<slug:slug>/',
views.CourseDetailView.as_view(),
name='course_detail',
),可以定义以下 URL 模式:
course_list_subject:用于显示某个科目的所有课程course_detail用于显示单个课程概述
让我们为视图构建CourseListView模板CourseDetailView。
templates/courses/在应用程序目录下创建以下文件结构courses:
course/
list.html
detail.html编辑courses/course/list.html申请模板courses并填写以下内容代码:
{% extends "base.html" %}
{% block title %}
{% if subject %}
{{ subject.title }} 课程
{% else %}
所有课程
{% endif %}
{% endblock %}
{% block content %}
<h1>
{% if subject %}
{{ subject.title }} 课程
{% else %}
所有课程
{% endif %}
</h1>
<div class="contents">
<h3>学生列表</h3>
<ul id="modules">
<li {% if not subject %}class="selected"{% endif %}>
<a href="{% url "course_list" %}">所有</a>
</li>
{% for s in subjects %}
<li {% if subject == s %}class="selected"{% endif %}>
<a href="{% url "course_list_subject" s.slug %}">
{{ s.title }}
<br>
<span>{{ s.total_courses }} 课程 {{ s.total_courses|pluralize }}</span>
</a>
</li>
{% endfor %}
</ul>
</div>
<div class="module">
{% for course in courses %}
{% with subject=course.subject %}
<h3>
<a href="{% url "course_detail" course.slug %}">
{{ course.title }}
</a>
</h3>
<p>
<a href="{% url "course_list_subject" subject.slug %}">{{ subject }}</a>.
{{ course.total_modules }} 模块.
导师: {{ course.owner.get_full_name }}
</p>
{% endwith %}
{% endfor %}
</div>
{% endblock %}确保没有模板标签被拆分成多行。 这是列出可用课程的模板。需要创建一个 HTML 列表来显示所有Subject对象,并为每个对象生成一个指向其 URL 的链接course_list_subject。还需要包含每个科目的课程总数,并使用模板过滤器在课程数量不为 1 时为“课程pluralize”一词添加复数后缀,例如显示0 门课程、1 门课程、2 门课程等等。如果选中了某个科目,则添加一个HTML 类来高亮显示该科目。需要遍历每个对象,显示模块总数和教师姓名。1selectedCourse
运行开发服务器并http://127.0.0.1:8000/在浏览器中打开。应该会看到类似以下页面:
图:课程列表页
左侧边栏包含所有学科,以及每个学科的课程总数。可以点击任意学科来筛选显示的课程。
编辑courses/course/detail.html模板并添加以下代码:
{% extends "base.html" %}
{% block title %}
{{ object.title }}
{% endblock %}
{% block content %}
{% with subject=object.subject %}
<h1>
{{ object.title }}
</h1>
<div class="module">
<h2>概述</h2>
<p>
<a href="{% url "course_list_subject" subject.slug %}">
{{ subject.title }}</a>.
{{ object.modules.count }} 模块.
导师: {{ object.owner.get_full_name }}
</p>
{{ object.overview|linebreaks }}
{% if request.user.is_authenticated %}
<form action="{% url "student_enroll_course" %}" method="post">
{{ enroll_form }}
{% csrf_token %}
<input type="submit" value="立即报名">
</form>
{% else %}
<a href="{% url "student_registration" %}" class="button">
前往注册
</a>
{% endif %}
</div>
{% endwith %}
{% endblock %}此模板显示单个课程的概述和详细信息。http://127.0.0.1:8000/在浏览器中打开并点击其中一个课程。应该会看到一个页面,其中包含……以下结构:
图:课程概览页
已创建一个用于展示课程的公共区域。接下来,需要允许用户注册成为学生并选修课程。
6、添加学生注册
我们需要实施学生注册功能用于选课和访问学习内容。请使用以下命令创建新应用程序:
python manage.py startapp students编辑settings.py项目文件educa,并将新应用程序添加到INSTALLED_APPS设置中,如下所示:
INSTALLED_APPS = [
# ...
'students.apps.StudentsConfig', # 添加自定义学生students应用
]6.1创建学生注册视图
编辑views.py文件在students应用程序中编写以下代码:
from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.views.generic.edit import CreateView
# 学生注册视图
class StudentRegistrationView(CreateView):
template_name = 'students/student/registration.html' # 模板名称
form_class = UserCreationForm # 表单类
success_url = reverse_lazy('student_course_list') # 成功URL
# 表单验证方法
def form_valid(self, form):
result = super().form_valid(form) # 调用父类表单验证
cd = form.cleaned_data # 获取清理后的数据
user = authenticate( # 验证用户
username=cd['username'], password=cd['password1']
)
login(self.request, user) # 登录用户
return result # 返回结果此视图允许学生在的网站上注册。可以使用通用的 <model> 组件CreateView,它提供了创建模型对象的功能。此视图需要以下属性:
template_name:用于渲染此视图的模板路径。form_class创建对象的表单必须是Django 的注册表单ModelForm。可以使用 Django 的UserCreationForm注册表单来创建User对象。success_url:表单成功提交后用户将被重定向到的 URL。为此,需要反转名为<url>的 URLstudent_course_list,该 URL 将在我们“访问课程内容”部分中创建,用于列出学生已注册的课程。
当提交了有效的表单数据后,将执行此form_valid()方法。该方法必须返回 HTTP 响应。可以重写此方法,以便在用户成功注册后登录。
创建一个新文件在应用程序目录下students创建一个名为 .app 的文件,并urls.py在其中添加以下代码:
from django.urls import path
from . import views
urlpatterns = [
path( # 注册视图
'register/',
views.StudentRegistrationView.as_view(),
name='student_registration'
),
]然后,编辑项目主文件urls.py,并在 URL 配置中添加以下模式,以educa包含应用程序的 URL :students
urlpatterns = [
# ...
path('students/', include('students.urls')),
]在应用程序目录下创建以下文件结构students:
templates/
students/
student/
registration.html编辑students/student/registration.html模板并添加以下代码:
{% extends "base.html" %}
{% block title %}
Sign up
{% endblock %}
{% block content %}
<h1>
Sign up
</h1>
<div class="module">
<p>Enter your details to create an account:</p>
<form method="post">
{{ form.as_p }}
{% csrf_token %}
<p><input type="submit" value="Create my account"></p>
</form>
</div>
{% endblock %}运行请在浏览器中打开开发服务器http://127.0.0.1:8000/students/register/。应该会看到类似这样的注册表单:
图:学生注册表
注意视图属性student_course_list中指定的 URL尚不存在。如果提交表单,Django 将找不到注册成功后要重定向到的 URL。success_urlStudentRegistrationView如前所述,将在“访问课程内容”部分创建此 URL。
6.2 报名参加课程
用户之后Course用户创建账户后即可报名课程。要存储报名信息,需要在账户和模型之间建立多对多关系User。
编辑models.py应用程序文件courses,并在模型中添加以下字段Course:
students = models.ManyToManyField(
User,
related_name='courses_joined',
blank=True
)在 shell 中执行以下命令,为该更改创建迁移:
python manage.py makemigrations会看到类似这样的输出:
Migrations for 'courses':
courses/migrations/0004_course_students.py
- Add field students to course然后,执行以下命令以应用待处理的迁移:
python manage.py migrate应该会看到一些输出,其中以以下这行结尾:
Applying courses.0004_course_students... OK现在可以将学生与他们选修的课程关联起来。接下来,我们将创建学生选课的功能。 在应用程序目录下创建一个新文件,students并将其命名为forms.py。将以下代码添加到该文件中:
from django import forms # 引入表单类
from courses.models import Course # 引入课程模型
# 课程报名表单
class CourseEnrollForm(forms.Form):
# 课程字段
course = forms.ModelChoiceField(
queryset=Course.objects.none(), # 课程查询集
widget=forms.HiddenInput # 隐藏输入小部件
)
# 初始化方法,设置课程查询集
def __init__(self, *args, **kwargs):
# 获取课程查询集
super(CourseEnrollForm, self).__init__(*args, **kwargs)
# 设置课程查询集
self.fields['course'].queryset = Course.objects.all()此表单用于学生注册课程。该course字段用于填写用户将要注册的课程;因此,它是空的ModelChoiceField。使用HiddenInput控件是因为此字段不打算对用户可见。初始时,将 QuerySet 定义为空Course.objects.none()。使用none()空 QuerySet 会创建一个不返回任何对象且不会查询数据库的空 QuerySet。这避免了在表单初始化期间不必要的数据库加载。可以在__init__()表单的 getQuerySet() 方法中填充实际的 QuerySet。这种动态设置允许根据不同的情况调整表单,例如根据用户注册的 QuerySet 筛选可用课程。具体标准。总的来说,这种方法让在管理表单数据方面拥有更大的灵活性,确保根据表单的使用上下文获取数据。这种方法也符合 Django 中高效处理表单查询集的最佳实践。 将在CourseDetailView视图中使用此表单来显示一个注册按钮。编辑views.py应用程序文件students并添加以下代码:
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import FormView
from .forms import CourseEnrollForm
# 课程报名视图
class StudentEnrollCourseView(LoginRequiredMixin, FormView):
course = None # 课程对象
form_class = CourseEnrollForm # 表单类
# 模板名称
def form_valid(self, form):
self.course = form.cleaned_data['course'] # 获取课程对象
self.course.students.add(self.request.user) # 添加学生到课程
return super().form_valid(form) # 调用父类表单验证
# 成功URL方法
def get_success_url(self):
return reverse_lazy(
'student_course_detail', args=[self.course.id] # 课程ID这是StudentEnrollCourseView视图,用于处理学生选课。该视图继承自 mixin,LoginRequiredMixin因此只有登录用户才能访问。它也继承自 Django 的FormView视图,因为它处理表单提交。可以使用CourseEnrollForm表单的form_class属性,并定义一个course属性来存储提交的Course对象。表单验证通过后,当前用户将被添加到已选课程的学生列表中。 该get_success_url()方法返回表单成功提交后用户将被重定向到的 URL。此方法等效于该success_url属性。然后,需要反转名为 . 的 URL student_course_detail。
编辑urls.py文件在students应用程序中添加以下 URL 模式:
path(
'enroll-course/',
views.StudentEnrollCourseView.as_view(),
name='student_enroll_course'
),让我们在课程概览页面添加报名按钮表单。编辑views.py应用程序文件courses,并修改CourseDetailView使其如下所示:
from students.forms import CourseEnrollForm
# 课程详情视图
class CourseDetailView(DetailView):
model = Course # 课程模型
template_name = 'courses/course/detail.html'
# 获取课程详情
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) # 获取上下文数据
context['enroll_form'] = CourseEnrollForm( # 创建课程注册表单
initial={'course': self.object} # 初始化表单数据
)
return context # 返回上下文数据可以使用此get_context_data()方法将注册表单包含在模板渲染的上下文中。需要使用course当前对象初始化表单的隐藏字段,Course以便可以直接提交表单。 编辑courses/course/detail.html模板并找到以下行:
{{ object.overview|linebreaks }}请将其替换为以下代码:
{{ object.overview|linebreaks }}
{% if request.user.is_authenticated %}
<form action="{% url "student_enroll_course" %}" method="post">
{{ enroll_form }}
{% csrf_token %}
<input type="submit" value="立即报名">
</form>
{% else %}
<a href="{% url "student_registration" %}" class="button">
前往注册
</a>
{% endif %}这是按钮用于报名课程。如果用户已通过身份验证,则会显示报名按钮,其中包含指向student_enroll_course网址的隐藏表单。如果用户未通过身份验证,则会显示平台注册链接。 请确保开发服务器正在运行,http://127.0.0.1:8000/在浏览器中打开,然后点击课程。如果已登录,应该会在课程概览下方看到“立即报名”按钮,如下所示:
图:课程概览页面,包含“立即报名”按钮
如果是如果未登录,将看到“注册报名”按钮。
7、渲染课程内容
一旦学生学生注册课程后,需要一个集中访问所有已注册课程的平台。我们需要整理学生注册的课程列表,并提供每门课程的内容访问权限。然后,我们需要实现一个系统来呈现构成课程模块的各种类型的内容,例如文本、图像、视频和文档。接下来,我们将构建必要的视图和模板,方便用户访问课程内容。
7.1 访问课程内容
需要视野用于显示学生已选课程以及访问课程内容的视图。编辑应用views.py程序文件students并添加以下代码:
from django.views.generic.list import ListView
from courses.models import Course
# 课程列表视图
class StudentCourseListView(LoginRequiredMixin, ListView):
model = Course # 课程模型
template_name = 'students/course/list.html' # 模板名称
# 查询集方法
def get_queryset(self):
qs = super().get_queryset() # 获取查询集
return qs.filter(students__in=[self.request.user]) # 过滤学生此视图用于显示学生已选修的课程。它继承自某个类,LoginRequiredMixin以确保只有登录用户才能访问此视图。它还继承自ListView用于显示对象列表的通用类Course。可以重写该方法,仅检索学生已选修的课程;为此,get_queryset()需要按学生的字段筛选查询集。ManyToManyField 然后,将以下代码添加到应用程序views.py文件中students:
from django.views.generic.detail import DetailView
# 课程详情视图
class StudentCourseDetailView(LoginRequiredMixin, DetailView):
model = Course # 课程模型
template_name = 'students/course/detail.html' # 模板名称
# 查询集方法
def get_queryset(self):
qs = super().get_queryset() # 获取查询集
return qs.filter(students__in=[self.request.user]) # 过滤学生
# 上下文数据方法
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) # 获取上下文数据
# get course object
course = self.get_object() # 获取课程对象
if 'module_id' in self.kwargs: # 检查模块ID
# 获取当前模块
context['module'] = course.modules.get(
id=self.kwargs['module_id'] # 模块ID
)
else:
# 获取第一个模块
context['module'] = course.modules.all()[0] # 第一个模块
return context # 返回上下文数据这是StudentCourseDetailView视图。需要重写该get_queryset()方法,将基础查询集限制为学生已注册的课程。还需要重写该get_context_data()方法。如果提供了 URL 参数,则在上下文中设置课程模块module_id。否则,设置课程的第一个模块。这样,注册学生就可以在课程内浏览各个模块。 编辑urls.py应用程序文件students,并向其中添加以下 URL 模式:
path( # 课程列表视图
'courses/',
views.StudentCourseListView.as_view(),
name='student_course_list',
),
path( # 课程详情视图
'course/<pk>/',
views.StudentCourseDetailView.as_view(),
name='student_course_detail',
),
path( # 课程详情视图 带模块ID
'course/<pk>/<module_id>/',
views.StudentCourseDetailView.as_view(),
name='student_course_detail_module',
),templates/students/在应用程序目录下创建以下文件结构students:
course/
detail.html
list.html编辑students/course/list.html将以下代码添加到模板中:
{% extends "base.html" %}
{% block title %}我的课程{% endblock %}
{% block content %}
<h1>我的课程</h1>
<div class="module">
{% for course in object_list %}
<div class="course-info">
<h3>{{ course.title }}</h3>
<p><a href="{% url "student_course_detail" course.id %}">
访问内容</a></p>
</div>
{% empty %}
<p>
您尚未注册任何课程。
<a href="{% url "course_list" %}">浏览课程</a>
来注册课程。
</p>
{% endfor %}
</div>
{% endblock %}此模板显示学生已注册的课程。请记住,新学生成功注册平台后,将被重定向到此student_course_listURL。我们也需要在学生登录平台时将其重定向到此 URL。 编辑settings.py项目文件educa,并将以下代码添加到其中:
from django.urls import reverse_lazy
LOGIN_REDIRECT_URL = reverse_lazy('student_course_list')这是模块用于auth在学生成功登录后,如果请求中没有参数,则重定向学生的设置next。登录成功后,学生将被重定向到指定student_course_list的 URL,以查看他们已注册的课程。 编辑students/course/detail.html将以下代码添加到模板中:
{% extends "base.html" %}
{% block title %}
{{ object.title }}
{% endblock %}
{% block content %}
<h1>
{{ module.title }}
</h1>
<div class="contents">
<h3>模块内容</h3>
<ul id="modules">
{% for m in object.modules.all %}
<li data-id="{{ m.id }}" {% if m == module %}class="selected"{% endif %}>
<a href="{% url "student_course_detail_module" object.id m.id %}">
<span>
模块 <span class="order">{{ m.order|add:1 }}</span>
</span>
<br>
{{ m.title }}
</a>
</li>
{% empty %}
<li>还没有模块。</li>
{% endfor %}
</ul>
</div>
<div class="module">
{% for content in module.contents.all %}
{% with item=content.item %}
<h2>{{ item.title }}</h2>
{{ item.render }}
{% endwith %}
{% endfor %}
</div>
{% endblock %}请确保所有模板标签均未跨越多行。这是供已注册学生访问课程内容的模板。首先,需要创建一个包含所有课程模块并高亮显示当前模块的 HTML 列表。然后,遍历当前模块的内容,并使用 . 方法访问每个内容项并将其显示出来{{ item.render }}。接下来,需要将 .render()方法添加到内容模型中。此方法将负责正确渲染内容。 现在可以访问http://127.0.0.1:8000/students/register/、注册新学生账号并报名参加任何课程。
7.2 渲染不同类型的内容
为了显示课程内容,需要渲染创建的不同内容类型:文本、图像、视频和文件。
编辑models.py应用程序文件courses,并将以下render()方法添加到ItemBase模型中:
from django.template.loader import render_to_string
class ItemBase(models.Model):
# ...
# 渲染内容的方法
def render(self):
# 使用模板渲染内容
return render_to_string(
f'courses/content/{self._meta.model_name}.html',
{'item': self}, # 传递内容对象给模板
)多态渲染模板输送分拣机
Polymorphic Rendering: ItemBase.render(self) & Dynamic Routing
方法:
识别参数: .html
render(self)识别参数: .html
读取
self._meta.model_name模板:
text.html (格式文本流)模板:
video.html (django-embed-video) ▶️ [外部视频播放器全息图]
在展现多态内容时,在模板里面写大量的 if-else 判别极其丑陋且难以维护。在高级设计模式中,我们会利用流水线**分拣机扫描引擎**!我们在抽象基类 ItemBase 中植入了一个原生的 render(self) 扫描探头方法。当从数据库拉出这批形状各异的盲盒数据送上渲染传送带时,扫描机瞬间读取数据箱子底部的隐形二维码(self._meta.model_name)。此时,扫描机如同智能铁轨分拨器,立刻动态拼接模板名称:如果盒子里是文本,它被抛掷给 text.html 解析处理;如果是视频流,则抛掷给 video.html 执行高级第三方插件 django-embed-video 渲染。底层调用极其优雅统一!
Deploying brutalist `if-else` blockages across templates to unmask polymorphic content is architecturally hideous and unmaintainable. Advanced design paradigms dictate a logistical **Sorter Engine** structure! We surgically embed a native `render(self)` scanning probe method straight into the `ItemBase` abstract mother class. When arrays of heterogeneous blind-box data are yanked from the database and thrust onto the rendering conveyor belt, the scanning archway instantly triggers to scan the invisible QR signature located on the box's chassis (`self._meta.model_name`). Functioning identically to intelligent railway track switchers, the scanner dynamically constructs the target template string filename: If the box dictates text payload, it's mercilessly routed into `text.html`. Conversely, raw video streams are switched onto `video.html`, instigating the powerful 3rd-party `django-embed-video` holoprojector.
此方法使用一个render_to_string()函数来渲染模板,并将渲染后的内容作为字符串返回。每种类型的内容都使用一个以其内容模型命名的模板进行渲染。self._meta.model_name该方法用于动态地为每个内容模型生成相应的模板名称。它render()提供了一个用于渲染各种内容的通用接口。 templates/courses/在应用程序目录下创建以下文件结构courses:
content/
text.html
file.html
image.html
video.html编辑courses/content/text.html模板并写入以下代码:
{{ item.content|linebreaks }}这是用于渲染文本内容的模板。该linebreaks模板过滤器会将纯文本中的换行符替换为 HTML 换行符。 编辑courses/content/file.html模板并添加以下内容:
<p>
<a href="{{ item.file.url }}" class="button">下载文件</a>
</p>这是用于渲染文件的模板。它会生成一个文件下载链接。 编辑courses/content/image.html模板并写入:
<p>
<img src="{{ item.file.url }}" alt="{{ item.title }}">
</p>这是用于渲染图像的模板。 还需要创建一个用于渲染Video对象的模板。将使用它django-embed-video来嵌入视频内容。django-embed-video是一个第三方 Django 应用程序,它允许通过提供视频的公共 URL,将来自 YouTube 或 Vimeo 等来源的视频嵌入到的模板中。
使用以下命令安装软件包:
python -m pip install django-embed-video==1.4.10编辑settings.py项目文件,并将应用程序添加到INSTALLED_APPS设置中,如下所示:
INSTALLED_APPS = [
# ...
'embed_video',
]可以在https://django-embed-video找到该django-embed-video应用程序的文档。.readthedocs.io/en/latest/。 编辑courses/content/video.html模板并写入以下代码:
{% load embed_video_tags %}
{% video item.url "small" %}这是用于渲染视频的模板。 现在,运行开发服务器并http://127.0.0.1:8000/course/mine/在浏览器中访问。使用属于该Instructors组的用户帐户访问网站,并向课程添加多个内容。要添加视频内容,只需复制任何 YouTube URL(例如https://www.youtube.com/watch?v=bgV39DlmZ2U),并将其粘贴到url表单的相应字段中即可。
将内容添加到课程后,打开课程页面http://127.0.0.1:8000/,点击课程,然后点击“立即注册”按钮。应该已经注册了该课程并被重定向到student_course_detail相应的 URL。图 14.6显示了一个课程内容页面示例:
图 :课程目录页
到目前为止我们创建了一个通用界面,用于渲染具有不同类型内容的课程。
