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 0000000..be5ba8f
Binary files /dev/null and b/college/static/src/img/shixun_module.png differ
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 @@
+