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 @@
    - - - + + + + + + + + @@ -29,6 +34,14 @@ title="查看本题所有学员的回答"/> + + + + + + + + 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 @@ +