From b072b7f973bb669fbf822d1e9bf0123d3bce9765 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?=
<9664676+pengyuthon@user.noreply.gitee.com>
Date: Fri, 21 Aug 2026 19:25:35 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=95=99=E8=82=B2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
college/__init__.py | 1 +
college/__manifest__.py | 8 +-
college/models/__init__.py | 2 +
college/models/chapter.py | 21 +++
college/models/course.py | 1 +
college/models/course_import_wizard.py | 163 +++++++++++++-----
college/security/groups.xml | 23 +++
college/security/ir.model.access.csv | 21 +++
college/static/src/img/shixun_module.png | Bin 0 -> 5526 bytes
college/static/src/scss/college_theme.scss | 1 +
.../static/src/scss/college_theme_dark.scss | 1 +
college/views/chapter_views.xml | 19 +-
college/views/course_views.xml | 8 +
college/views/menus.xml | 42 ++++-
14 files changed, 256 insertions(+), 55 deletions(-)
create mode 100644 college/static/src/img/shixun_module.png
diff --git a/college/__init__.py b/college/__init__.py
index 0650744..91c5580 100644
--- a/college/__init__.py
+++ b/college/__init__.py
@@ -1 +1,2 @@
+from . import controllers
from . import models
diff --git a/college/__manifest__.py b/college/__manifest__.py
index 304ef9f..d6660ab 100644
--- a/college/__manifest__.py
+++ b/college/__manifest__.py
@@ -1,6 +1,6 @@
{
'name': '教育',
- 'version': '18.0.3.0.0',
+ 'version': '18.0.3.0.4',
'summary': 'Odoo 18 教学管理平台 — 师生管理 + 课程管理 + 作业/进展(老师·学生双角色)',
'description': """
Odoo 18 教学管理平台(面向老师与学生,双角色权限)
@@ -40,11 +40,17 @@
'views/assignment_batch_wizard_views.xml',
'views/import_wizard_views.xml',
'views/homework_views.xml',
+ 'views/exam_views.xml',
+ 'views/exam_take_templates.xml',
'views/training_views.xml',
'views/course_import_wizard_views.xml',
'data/about_pages.xml',
'views/about_page_views.xml',
'views/menus.xml',
+ 'views/shixun_kanban_view.xml',
+ 'data/shixun_module_data.xml',
+ 'data/module_info_data.xml',
+ 'views/module_info_views.xml',
],
'assets': {
'web.assets_backend': [
diff --git a/college/models/__init__.py b/college/models/__init__.py
index 6d33cbb..9fa0adb 100644
--- a/college/models/__init__.py
+++ b/college/models/__init__.py
@@ -10,3 +10,5 @@ from . import import_wizard
from . import about_page
from . import training
from . import course_import_wizard
+from . import ir_module_module_info
+from . import exam
diff --git a/college/models/chapter.py b/college/models/chapter.py
index 0a2bee6..c29abaa 100644
--- a/college/models/chapter.py
+++ b/college/models/chapter.py
@@ -240,6 +240,21 @@ class CourseChapterExerciseAnswer(models.Model):
return super().write(vals)
+class CourseChapterTask(models.Model):
+ """章节任务清单——老师按章节维护的学习任务,学生端可见但不可编辑。"""
+
+ _name = 'yuthon.school.course.chapter.task'
+ _description = '章节任务'
+ _order = 'chapter_id, sequence, id'
+
+ chapter_id = fields.Many2one(
+ 'yuthon.school.course.chapter', string='章节',
+ required=True, ondelete='cascade', index=True)
+ sequence = fields.Integer(string='排序', default=10)
+ name = fields.Char(string='任务名称', required=True)
+ is_done = fields.Boolean(string='已完成', default=False)
+
+
class CourseChapter(models.Model):
"""课程章节——老师端在课程内容里维护,学生端作为学习目录。
@@ -256,6 +271,7 @@ class CourseChapter(models.Model):
name = fields.Char(string='章节标题', required=True, index=True)
sequence = fields.Integer(string='排序', default=10)
description = fields.Html(string='章节说明', sanitize=True)
+ class_hours = fields.Integer(string='课时', default=1, help='本章所占课时数')
# 视频与附件
video_file = fields.Binary(string='视频文件', attachment=True)
@@ -280,6 +296,11 @@ class CourseChapter(models.Model):
for chapter in self:
chapter.exercise_count = len(chapter.exercise_ids)
+ # 任务列表:老师按章节维护的任务清单
+ task_ids = fields.One2many(
+ 'yuthon.school.course.chapter.task', 'chapter_id',
+ string='任务列表')
+
# 随堂笔记:老师按章节记录的富文本笔记(与章节编辑表单共用同一字段)
notes = fields.Html(string='随堂笔记', sanitize=True)
diff --git a/college/models/course.py b/college/models/course.py
index ea5e44f..8ef6e9e 100644
--- a/college/models/course.py
+++ b/college/models/course.py
@@ -76,6 +76,7 @@ class SchoolCourse(models.Model):
objectives = fields.Html(string='课程目标', sanitize=True)
syllabus = fields.Html(string='课程大纲', sanitize=True)
features = fields.Html(string='课程特色', sanitize=True)
+ business_scenario = fields.Html(string='业务情景', sanitize=True)
# 学员分配
student_ids = fields.Many2many(
diff --git a/college/models/course_import_wizard.py b/college/models/course_import_wizard.py
index 268716c..9634616 100644
--- a/college/models/course_import_wizard.py
+++ b/college/models/course_import_wizard.py
@@ -1,7 +1,13 @@
-"""课程批量导入向导 —— CSV 模板下载 + 批量创建课程(导入大纲即可)。
+"""课程批量导入向导 —— CSV 模板下载 + 批量创建课程(可选连带章节与章节任务)。
-模板列:课程名称、课程编号、所属学院、学期、课程类型、课时、课程大纲。
-导入时直接创建 yuthon.school.course 记录。
+模板列(前 8 列为课程本体,后 5 列为可选章节/任务):
+课程名称、课程编号、所属学院、学期、课程类型、课时、课程大纲、业务情景、
+章节标题、章节说明、章节课时、任务名称、是否完成。
+
+- 仅填前 8 列:行为与旧版一致,只创建课程;
+- 填了「章节标题」:自动按 (课程, 章节标题) 找/建章节;
+- 填了「任务名称」:在对应章节下创建任务列表明细行(is_done 由「是否完成」决定)。
+同一课程 / 同一章节多行时自动复用,不重复创建。
"""
import base64
@@ -58,10 +64,13 @@ class CourseImportWizard(models.TransientModel):
'
点击下方「下载模板」按钮下载 CSV 模板;'
'用 Excel / WPS 打开模板,按示例填写数据:
'
'课程名称、课程编号 为必填;
'
- '所属学院、学期、课程类型、课时、课程大纲 为选填;'
+ '所属学院、学期、课程类型、课时、课程大纲、业务情景 为选填;'
'课程类型可选:理论 / 实践'
+ '章节 / 任务(可选):填「章节标题」会自动建/复用章节,'
+ '填「任务名称」会在该章节下建一条任务列表明细(是否完成填 是/否);'
+ '同一课程、同一章节多行会自动合并,不会重复创建;'
'保存为 CSV 后上传并点击「开始导入」;'
- '若课程编号已存在则跳过,不会重复创建。'
+ '若课程名称已存在则复用,不会重复创建课程。'
''
),
)
@@ -71,10 +80,15 @@ class CourseImportWizard(models.TransientModel):
def action_download_template(self):
"""生成 CSV 模板并提供下载。"""
header = ['课程名称', '课程编号', '所属学院', '学期',
- '课程类型(理论/实践)', '课时', '课程大纲']
+ '课程类型(理论/实践)', '课时', '课程大纲', '业务情景',
+ '章节标题', '章节说明', '章节课时',
+ '任务名称', '是否完成']
sample = ['Python 编程基础', 'PY101', '计算机学院',
'2025-2026第1学期', '理论', '48',
- 'Python基础语法、数据结构、面向对象编程']
+ 'Python基础语法、数据结构、面向对象编程',
+ '通过电商订单管理场景贯穿课程',
+ '第1章 环境搭建', '本章介绍开发环境', '4',
+ '安装Python', '否']
buf = io.StringIO()
writer = csv.writer(buf)
@@ -122,6 +136,10 @@ class CourseImportWizard(models.TransientModel):
col_map = self._normalize_columns(reader.fieldnames)
created = 0
+ reused_course = 0
+ created_chapter = 0
+ reused_chapter = 0
+ created_task = 0
skipped = 0
errors = []
@@ -135,46 +153,86 @@ class CourseImportWizard(models.TransientModel):
errors.append(_('第 %d 行:课程名称为必填,跳过。') % row_idx)
continue
- # 编号去重
- if code:
- existing = self.env['yuthon.school.course'].sudo().search(
- [('code', '=', code)], limit=1)
- if existing:
- skipped += 1
- continue
-
try:
- vals = {
- 'name': name,
- 'code': code,
- 'state': 'draft',
- }
+ # 课程:按名称优先复用,其次按编号;都不存在才新建
+ course = self.env['yuthon.school.course'].sudo().search(
+ [('name', '=', name)], limit=1)
+ if not course and code:
+ course = self.env['yuthon.school.course'].sudo().search(
+ [('code', '=', code)], limit=1)
- college_raw = row.get('college', '')
- if college_raw:
- vals['college'] = COLLEGE_NAME_MAP.get(college_raw, 'other')
+ if not course:
+ vals = {
+ 'name': name,
+ 'code': code,
+ 'state': 'draft',
+ }
+ college_raw = row.get('college', '')
+ if college_raw:
+ vals['college'] = COLLEGE_NAME_MAP.get(college_raw, 'other')
+ semester = row.get('semester', '')
+ if semester:
+ vals['semester'] = semester
+ course_type_raw = row.get('course_type', '')
+ if course_type_raw:
+ vals['course_type'] = COURSE_TYPE_MAP.get(
+ course_type_raw, 'theory')
+ hours_raw = row.get('hours', '')
+ if hours_raw:
+ try:
+ vals['hours'] = int(float(hours_raw))
+ except ValueError:
+ pass
+ syllabus = row.get('syllabus', '')
+ if syllabus:
+ vals['syllabus'] = syllabus
+ business_scenario = row.get('business_scenario', '')
+ if business_scenario:
+ vals['business_scenario'] = business_scenario
+ course = self.env['yuthon.school.course'].sudo().create(vals)
+ created += 1
+ else:
+ reused_course += 1
- semester = row.get('semester', '')
- if semester:
- vals['semester'] = semester
+ # 章节:填了章节标题才处理(按 课程+标题 复用)
+ chapter_title = row.get('chapter', '').strip()
+ chapter = None
+ if chapter_title:
+ chapter = self.env['yuthon.school.course.chapter'].sudo().search([
+ ('course_id', '=', course.id),
+ ('name', '=', chapter_title),
+ ], limit=1)
+ if not chapter:
+ chapter_vals = {'course_id': course.id, 'name': chapter_title}
+ chapter_desc = row.get('chapter_desc', '').strip()
+ if chapter_desc:
+ chapter_vals['description'] = chapter_desc
+ chapter_hours_raw = row.get('chapter_hours', '').strip()
+ if chapter_hours_raw:
+ try:
+ chapter_vals['class_hours'] = int(float(chapter_hours_raw))
+ except ValueError:
+ pass
+ chapter = self.env['yuthon.school.course.chapter'].sudo().create(
+ chapter_vals)
+ created_chapter += 1
+ else:
+ reused_chapter += 1
- course_type_raw = row.get('course_type', '')
- if course_type_raw:
- vals['course_type'] = COURSE_TYPE_MAP.get(course_type_raw, 'theory')
-
- hours_raw = row.get('hours', '')
- if hours_raw:
- try:
- vals['hours'] = float(hours_raw)
- except ValueError:
- pass
-
- syllabus = row.get('syllabus', '')
- if syllabus:
- vals['syllabus'] = syllabus
-
- self.env['yuthon.school.course'].sudo().create(vals)
- created += 1
+ # 任务列表:填了任务名称才在对应章节下创建明细行
+ task_name = row.get('task_name', '').strip()
+ if task_name and chapter:
+ is_done = row.get('is_done', '').strip() in (
+ '是', 'y', 'yes', 'true', '1', '完成')
+ task_vals = {
+ 'chapter_id': chapter.id,
+ 'name': task_name,
+ 'is_done': is_done,
+ 'sequence': 10,
+ }
+ self.env['yuthon.school.course.chapter.task'].sudo().create(
+ task_vals)
+ created_task += 1
except Exception as exc:
skipped += 1
@@ -183,9 +241,12 @@ class CourseImportWizard(models.TransientModel):
summary = _(
'导入完成!\n'
- '新建课程:%d\n'
+ '新建课程:%d(复用 %d)\n'
+ '新建章节:%d(复用 %d)\n'
+ '新建任务:%d\n'
'跳过:%d'
- ) % (created, skipped)
+ ) % (created, reused_course, created_chapter, reused_chapter,
+ created_task, skipped)
if errors:
summary += '\n' + _('注意事项:\n') + '\n'.join(errors[:10])
@@ -223,6 +284,18 @@ class CourseImportWizard(models.TransientModel):
mapping[f] = 'hours'
elif f_lower in ('课程大纲', 'syllabus', '大纲'):
mapping[f] = 'syllabus'
+ elif f_lower in ('业务情景', 'business_scenario', 'scenario'):
+ mapping[f] = 'business_scenario'
+ elif f_lower in ('章节标题', 'chapter', '章节', 'chapter_name'):
+ mapping[f] = 'chapter'
+ elif f_lower in ('章节说明', 'chapter_desc', 'chapter_description'):
+ mapping[f] = 'chapter_desc'
+ elif f_lower in ('章节课时', 'chapter_hours', 'chapter_class_hours'):
+ mapping[f] = 'chapter_hours'
+ elif f_lower in ('任务名称', 'task_name', '任务', 'task'):
+ mapping[f] = 'task_name'
+ elif f_lower in ('是否完成', 'is_done', '完成', 'done'):
+ mapping[f] = 'is_done'
else:
mapping[f] = f # 保留原名
return mapping
diff --git a/college/security/groups.xml b/college/security/groups.xml
index 7f0c04a..0961089 100644
--- a/college/security/groups.xml
+++ b/college/security/groups.xml
@@ -34,4 +34,27 @@
+
+
+
+ 考试答卷:学生仅能访问自己的答卷
+
+
+ [('user_id', '=', user.id)]
+
+
+
+
+
+
+
+ 考试答题:学生仅能访问自己的答题记录
+
+
+ [('paper_id.user_id', '=', user.id)]
+
+
+
+
+
diff --git a/college/security/ir.model.access.csv b/college/security/ir.model.access.csv
index c8f8acd..d9ab7a6 100644
--- a/college/security/ir.model.access.csv
+++ b/college/security/ir.model.access.csv
@@ -38,6 +38,9 @@ access_yuthon_school_course_import_wizard_teacher,yuthon.school.course.import.wi
access_yuthon_school_course_chapter_exercise_answer_admin,yuthon.school.course.chapter.exercise.answer.admin,model_yuthon_school_course_chapter_exercise_answer,base.group_system,1,1,1,1
access_yuthon_school_course_chapter_exercise_answer_teacher,yuthon.school.course.chapter.exercise.answer.teacher,model_yuthon_school_course_chapter_exercise_answer,college.group_teacher,1,0,0,0
access_yuthon_school_course_chapter_exercise_answer_student,yuthon.school.course.chapter.exercise.answer.student,model_yuthon_school_course_chapter_exercise_answer,college.group_student,1,1,1,0
+access_yuthon_school_course_chapter_task_admin,yuthon.school.course.chapter.task.admin,model_yuthon_school_course_chapter_task,base.group_system,1,1,1,1
+access_yuthon_school_course_chapter_task_teacher,yuthon.school.course.chapter.task.teacher,model_yuthon_school_course_chapter_task,college.group_teacher,1,1,1,1
+access_yuthon_school_course_chapter_task_student,yuthon.school.course.chapter.task.student,model_yuthon_school_course_chapter_task,college.group_student,1,0,0,0
access_yuthon_school_training_admin,yuthon.school.training.admin,model_yuthon_school_training,base.group_system,1,1,1,1
access_yuthon_school_training_teacher,yuthon.school.training.teacher,model_yuthon_school_training,college.group_teacher,1,1,1,1
access_yuthon_school_training_student,yuthon.school.training.student,model_yuthon_school_training,college.group_student,1,1,1,1
@@ -46,3 +49,21 @@ access_yuthon_about_page_teacher,yuthon.about.page.teacher,model_yuthon_about_pa
access_yuthon_about_page_student,yuthon.about.page.student,model_yuthon_about_page,college.group_student,1,0,0,0
access_ir_module_module_teacher,ir.module.module.teacher,base.model_ir_module_module,college.group_teacher,1,1,1,1
access_ir_module_module_student,ir.module.module.student.read,base.model_ir_module_module,college.group_student,1,0,0,0
+access_yuthon_school_exam_question_admin,yuthon.school.exam.question.admin,model_yuthon_school_exam_question,base.group_system,1,1,1,1
+access_yuthon_school_exam_question_teacher,yuthon.school.exam.question.teacher,model_yuthon_school_exam_question,college.group_teacher,1,1,1,1
+access_yuthon_school_exam_question_student,yuthon.school.exam.question.student,model_yuthon_school_exam_question,college.group_student,1,0,0,0
+access_yuthon_school_exam_question_option_admin,yuthon.school.exam.question.option.admin,model_yuthon_school_exam_question_option,base.group_system,1,1,1,1
+access_yuthon_school_exam_question_option_teacher,yuthon.school.exam.question.option.teacher,model_yuthon_school_exam_question_option,college.group_teacher,1,1,1,1
+access_yuthon_school_exam_question_option_student,yuthon.school.exam.question.option.student,model_yuthon_school_exam_question_option,college.group_student,1,0,0,0
+access_yuthon_school_exam_admin,yuthon.school.exam.admin,model_yuthon_school_exam,base.group_system,1,1,1,1
+access_yuthon_school_exam_teacher,yuthon.school.exam.teacher,model_yuthon_school_exam,college.group_teacher,1,1,1,1
+access_yuthon_school_exam_student,yuthon.school.exam.student,model_yuthon_school_exam,college.group_student,1,0,0,0
+access_yuthon_school_exam_line_admin,yuthon.school.exam.line.admin,model_yuthon_school_exam_line,base.group_system,1,1,1,1
+access_yuthon_school_exam_line_teacher,yuthon.school.exam.line.teacher,model_yuthon_school_exam_line,college.group_teacher,1,1,1,1
+access_yuthon_school_exam_line_student,yuthon.school.exam.line.student,model_yuthon_school_exam_line,college.group_student,1,0,0,0
+access_yuthon_school_exam_paper_admin,yuthon.school.exam.paper.admin,model_yuthon_school_exam_paper,base.group_system,1,1,1,1
+access_yuthon_school_exam_paper_teacher,yuthon.school.exam.paper.teacher,model_yuthon_school_exam_paper,college.group_teacher,1,1,1,1
+access_yuthon_school_exam_paper_student,yuthon.school.exam.paper.student,model_yuthon_school_exam_paper,college.group_student,1,1,1,0
+access_yuthon_school_exam_answer_admin,yuthon.school.exam.answer.admin,model_yuthon_school_exam_answer,base.group_system,1,1,1,1
+access_yuthon_school_exam_answer_teacher,yuthon.school.exam.answer.teacher,model_yuthon_school_exam_answer,college.group_teacher,1,1,1,1
+access_yuthon_school_exam_answer_student,yuthon.school.exam.answer.student,model_yuthon_school_exam_answer,college.group_student,1,1,1,0
diff --git a/college/static/src/img/shixun_module.png b/college/static/src/img/shixun_module.png
new file mode 100644
index 0000000000000000000000000000000000000000..be5ba8fc666a31e8cdc36e14c14990087b122ca7
GIT binary patch
literal 5526
zcmYLNc|256`=1#z%nYWnZ-c>2mJp&r7~7ap_CnUN@0pUdFoTRGnT8@;%96?+WeI~K
z6)6?k7$jvYQMUL^@8|b<-#_kh&V8QmdA`s6-gC}4OzXNpmnBKhm@px0;ME^d>$~h5&+iSazf}vlP;Wj!
z*53Quu79!H_?@h=%VajH`lf|r&xK~oo+hQ9G$PMSVI>H<2(ysm71XtDTIf``#`9)l
zx3z00ZS!SK-5T!>UQnSiT+_(7MnmUW(dM$$&dG|?wJQXkHzT+1kd5A#lTQiHw`e#&
z%o!gk$hN6GE8-m;jb|Lv)gyKqSEVcJ|LvouL=Vj6>n}<0@t|vN^os!x)cB3)8_>?*
z7D*!=#^NtZIO@#*BG)Pbbp1Xibso@kxFvq^qN4sY5cba33}i6P?3gyd@NF)$@Y(6~MqJZju9xwb7l
z)UCVaC2Jbr*cjwS?)}vS^5J^Tg%Xp6>K*|a681f*zFPcJGjtxYU0W6KM|G|}
zWXV7eOFV=*juPQBX#ra|YlK@O?yK`$Z21(HK!5KweRf2+tW}`WTq<%RPncncD<;B&
zkVJC5^!`Kdv-yy>zl=(@>)SPW$LdGiO*+5@9SnHbb=*p!KvS+Ky(d^B_XDY&_Tcx6
zn~TwvZKtkCq4M{`fg`~DhxZ0F0j@4xk)W(sk54NVN?McS*90CbN=4htXJp`;%(Nyw
zEwmnXogPZ_dB!k@0lFCV4;ym!mKv_@>(+I;j44p!*+eONIhct=&7}MtfRU7WZfDMS
zsG4u>`u7~Q+IVGt+c6bDV(+jiamm=GmD6p**F%Agu4)5^=37gP82|evC2H4>eD{ls
zoO|Z8`EAF)=h4Mrda{2*jbg%0;-@8V`QQ82N+f|r>TGhKKUcDg5-@Du$CWUaxy+b7
z@O&ZbNk`GblN`Bm11`+1i~qEQPRK{|&`&~(#eKW&LeA@ha)T8mi;Y4AiG8FSDt=54PB^{*o>l^?B+Yq&?Py{qfi
z&9IYCHxm{`0|FWW)$2tcg3s5kFXpiLkuEkYoLBahGS
zwn=8_?zBE$e_hY5ByMe*KwI*?=ywRKJVjv-BQoxTbp(+eR4R8yi|nVvEA!0xR_JaS
z>%azN{%1nas{mn)0+0eCA?Um=$_(6jhD$@RH$m*u!JBU0V;UiV+jkH3;sP|WZ1>k|
zDF%ju%e}(bu6+M`B&-u<)a1S^A4fr)n*EF(Yy&rXIW6x#plo7q~G+P&;S
z-n)OrZwd*HQ7PgnoNp-*!|v~4KT?qp9ETGZgB;jzLupWfkNGxut5n|zefw{WadU0w
z`Zj{l!;>~q_Vxt9kfd7;ez=fiXn96w;`Aj{H5f5WK>%R+-!9DwPv;$SnRasqy&UnYpBG`tZkM8pOLHZnNcH>-c-Hl&(jejW2oSjjBK2~XZP{Gb8)
z0?vq3wI9_)6wQg4K&l;!Gt6xMD3?&nv>$L{V2s{t2=){J)&j($mxhi_BxXMXndFE4
zgT3wSJ^DzL*sQ)>^0XctjF5I?L7AdgZ@lyweFPy2-x>Y-`Rm?0UUb28(L*%o*?1A%
zAzr$95pQXwG<}U5D&WssfQRUHB8%qk$o*VhCYASIHsc0EV#Xng47^Q!Z&jQrG2UzE
z;3AImEu<@P&;_4UaI@L`PSw#@8`^aivDE$$nuoY
zK`QI)e@V*-2!1pg-NwyM84`r7w^AmS+7n#7z7MQ_c{uQx)c}Uv_+Oyr#4Q>6VZFMc
zU^35(avH3|$2z>N>OKTg{y&?8#CRF{29Dq5cSOmhZBxT&{1nEGwIkOPGI31p2(I^x
z?vMgrYaR!+i4&$Kzj|E$txVA#MBD%0To+3)88c|v0bf%=el)KCze&9=$JR#$3(AQC
z_F$U!|I7shw`1$_*ZZWNPyu}`C0~sULCmhHgcWL=_{7|Y+^|_cgJB%4F;4s$yfCVW
zz#d9fBM-{a*1NMXt$Xiot$0NXlR$_o@q0ma`RKj_64-{5SQ>IWSJp^?Xrve{D1IrQR(!BtL-)hP^@N4a
zA)ZeQRfM(ClQ=911uypbuMmziSV0E2cG}wM;aPn-Hw!De#>sROMB=btHjU+RjI&T#E4_O1pkV>SJ!u>Dt?5>6hWI4IlSnCo5Ag}3pu}s$NSzV-#x)#
z5vjdrxWk?jnbK%DmVt+9Ns&aBYGsNkBW!ILF%txh&w&YAta)u{R}`qZvute+*K2ZP
zPKW!_E5z~G*d@kd{nXta4P(c{zC3^;=G=xK4fyf3WvKt@LV;iOkH&(#PDE2S9<=6^
zKp3(gu|(n;Wb$SiQ`Q@N47|el;2GjIVNJNi{^?fA)%R6oM^@00cS
z`hB}ksr=lkyDdt@O|i5;9GLr<b!AtMVeoAiHl9}
z$w_rlzL?6x#C|@W(=v~jWYomw++Fk+PB2pYeJwk>i;9&Kp=Z+aA*7%~zjx=iAKm}zd&nlmZ$x@KXoZlu5O!R#RnT$A?^$J{)WK-WMRhsk)wCHG?QL4cbEN#`SS5qA=lZ~*cVW=}fwWWw
z@dhCaO~d<4F4TAqQ%dl1>>7>B$eACGN&gSd4b|Uyfmnc(q{P|eR0Wv?HudFOF35q_
ze~~Z7pEAXAmb|aFAw=A?CKR<{fW%M1#ej-IcN3?Sh*P&OzeE$QlKa~7k<4NWYwQ&W
z5{Sc^7&ji1C0eESeG`%rbix(CGF-3+Lna5Hq(KTkG|z>R2jbb0lEzX(_!w_MJk11l
z+U%KZ*(Zn(LnjSwzVyn8p&e!OkwNTat_4{&EJd9k*z%nDO{t-f
zO-x%C0G(d;$GPOk>qjc)QaKhnSjr)FV7ue&ne#X?AoIkAZ@Zsd
z=^$?ExgUsj|0n}P=lWajmr{Ynm2p`ii9WqxSw$EegG#B8My=&AF-!=Xic->)%i!l(
zyn(?|;>CePN?B5#A48ar=Nvu*OhV$w=L7I$)6&=bn28iz&Mh+sFvf=0hUm}({&0X7
zUd#jF40(Hg{&lghSQZ7Z)Mj#%EGRe+J*>Te%@_Uc%WreJp~+ZUQlX#1cBu1vqjfq~
ziHqBl7Ww+;3$3kr6&D`-V#rs+$?=8HM$?_g<@n98_8DxdUoH<+9vdWXEgDeaaxmh}
z;OgAsV`|U!)DVNfxJUzLs_SPcJjqI9q+=xuJ9T85`ThtPpiS@da$X7U+U{*svCjJ&
zBay)#2sxZ>-+H0Umf
zITRgTMeFCAw!s=1vJ
z3^Tl_X)=95<&G%6p!|v;DzF_toi|kvV}J{F)i{a{%=*`^g`pJ7}Xu!JSuNtRYR_^h1c$M;|FTN2uU!^Icbv
zgm?;(veQJsZtk~uXAEV4R|nGit^*9*{?)e-qV1An5Br56XMng6a0Pvt^Mg=@0BkbW
zT0qlx;?!})agYp0`EWIA?0kt7D$qw9*!G;&AN1sz+=DStl~%OA>zuvn{?~6H#H``U
z9`I;6(;h!#ZB5RCC3rFFsyMlxmHq9Bw_R-|8I3(`
zW&tl#;*RQAV4eP-_`o*)r!BjUz3jEwR`Q43qeK87TGKZ<_L4@FF0J3lrlyf6sX
z62ZA^(M@k<*aY|9Ac3`XaPFRT)76!+w|;Wh`LK=`6)=JcUcClqOJpy#I6LQp{Jv9K
zkBe(Dl?oeoe(XEBb^T<>!-o@)fyg^s_+Sm$2oMNn{I3>pYtgpU!|UjZx9cQ{v
z==B6F7!z+$bQoh7(M*M7;1Q8vV1rheA8`gMUXbP$)mtP}biSa!*d8KJ+1Rgv>HnNLYrdoi7BbM9t3!_|2b)hly<#-u^;HQZ
zfa||e-FCD_22E3N{(juI^9lI1Wi+ik$Mtn|I^-<}oevT7(VyZt(`TITypK}=kQvLL
zw3|8qv88{h(+Us0+)C1{h@t)n=-MU}I+=g}yQ>?wE;FfikQ;ONhodx>Gk=`o#(_+8
z{izngTjUQqIx6SlPjV=I@xPLtA+7U1{GJFi1k^|?Sm|n}-;7#LoQsZD|0BLsN#5KW
zTGp6M3`wm(;-RQX;TIz|3!X1)LT`;0MnBBtF`p`Xl@O9#5yT1NRpZa+H(jyHqG;NT
zv%1J%I%(~t8UNwuqTL-Q0qlB@=3$HPUZ*=sCWV3>(>8DM8Y?QJ&@^9{TVGtC8Cf=!
z@EDR$Y5^8tdCIKSuZH#M<~Bv?wVn7o3}LQxmcX@&w*177opVPBoQ0p0yOPgG7i1NH
z39tZ8pLxG&!Z!s_j$3A6RL9%2Ao=2OJ0m;LMLBFy@Go)5?AN=zu{Z2;yor>^lcH`OQhWf3@J
z#ch)#V|!s4L#n70{jGFldX%Y5?h?FbEg%waYACbLZ;zAY1VMDU>-AuiW=G6(x?Q5`
z9fXFVX(@7ALi6%aru5RTjhui1$11ZSJ5bt8&i+FjQ&!9$_78o!YOTT;*phi!R?%>U
zpAZYkvpIizxi+&rr0?8pTOg;dFC4FBVL5N}jW=`si$mEFPM~#mgYo666hBxeXY9*LO1XquDyM&-`WL=9
z)Q-i=$)kzkb@z)9PT^|lWjEW>BB`eS^v_RYKWsRf@lvkz2u4mh$0XHgbSpbN+@7>M
zu_E$q-?Iw?YxE?i5?jRw9EInJo^2d(1_Q>4QO*L=ooX_pEuTN#l5V*m2?i(3?ln06
zQh6cr%3Un3Fq^4Y?u?J9Z@+Mk(cBUWoy}=mb~kYwh%RM$`!n`lt27w*+m&?KHdtL3
zs0tSjhME^8=$t#$3df{W=EBTG3jV&Wr+fHr6YyKLQx>7LnTyy}ok~b%v}KM#Vc3XT
z&Wa4|u}sq8^>;cRQ}YhQxdA8qogQPvpV!Uh)TwK}lB*q&_EWLJO&7#q?ZO4?7d$&R
zH-DE?$?Jvl+950Ql-OhY`DaubCPrfR_#br*gs;E(5e$WPd)`fdky*X1Ksgr*s4@E^}Y(K${&C!^1C1M_y$=Dya+jniX^J
z-ON=2eoEL0LWJiWHPTaTBo_o9cwVGU~BrKLS#1%sHxVT%vL92=Ynt&*iG9px&k@Vw#jSBsOVM-S|`Ilk$$I-nkE-26_z`
zfPXF#-)BI4PvAikz9*g>(1k(Ky2_R1nkrc8CC$6jLPZb=yeRLb0W)I8ZfcKHRNdPv
i^_frQ+;IP!O1VHum+3U>yqz$o2Sl>4F|RWvC;uN`3(GJ7
literal 0
HcmV?d00001
diff --git a/college/static/src/scss/college_theme.scss b/college/static/src/scss/college_theme.scss
index f05bbbf..ac999bf 100644
--- a/college/static/src/scss/college_theme.scss
+++ b/college/static/src/scss/college_theme.scss
@@ -266,3 +266,4 @@
h4 { font-size: 15px; margin: 16px 0 8px; }
p, li { color: $o-main-text-color; }
}
+
diff --git a/college/static/src/scss/college_theme_dark.scss b/college/static/src/scss/college_theme_dark.scss
index 231a017..1779238 100644
--- a/college/static/src/scss/college_theme_dark.scss
+++ b/college/static/src/scss/college_theme_dark.scss
@@ -299,3 +299,4 @@
h4 { font-size: 15px; margin: 16px 0 8px; color: #f8fafc; }
p, li { color: #cbd5e1; }
}
+
diff --git a/college/views/chapter_views.xml b/college/views/chapter_views.xml
index de0dd00..37772f1 100644
--- a/college/views/chapter_views.xml
+++ b/college/views/chapter_views.xml
@@ -8,9 +8,14 @@
+
+
+
+
+
+
+
+
diff --git a/college/views/course_views.xml b/college/views/course_views.xml
index 0bfef32..8320390 100644
--- a/college/views/course_views.xml
+++ b/college/views/course_views.xml
@@ -62,6 +62,7 @@
+