完善教育

This commit is contained in:
李鹏宇
2026-08-21 19:25:35 +08:00
parent 87f2003caa
commit b072b7f973
14 changed files with 256 additions and 55 deletions
+118 -45
View File
@@ -1,7 +1,13 @@
"""课程批量导入向导 —— CSV 模板下载 + 批量创建课程(导入大纲即可)。
"""课程批量导入向导 —— CSV 模板下载 + 批量创建课程(可选连带章节与章节任务)。
模板列:课程名称、课程编号、所属学院、学期、课程类型、课时、课程大纲。
导入时直接创建 yuthon.school.course 记录。
模板列(前 8 列为课程本体,后 5 列为可选章节/任务):
课程名称、课程编号、所属学院、学期、课程类型、课时、课程大纲、业务情景、
章节标题、章节说明、章节课时、任务名称、是否完成。
- 仅填前 8 列:行为与旧版一致,只创建课程;
- 填了「章节标题」:自动按 (课程, 章节标题) 找/建章节;
- 填了「任务名称」:在对应章节下创建任务列表明细行(is_done 由「是否完成」决定)。
同一课程 / 同一章节多行时自动复用,不重复创建。
"""
import base64
@@ -58,10 +64,13 @@ class CourseImportWizard(models.TransientModel):
'<li>点击下方「下载模板」按钮下载 CSV 模板;</li>'
'<li>用 Excel / WPS 打开模板,按示例填写数据:<br/>'
'<b>课程名称</b>、<b>课程编号</b> 为必填;<br/>'
'<b>所属学院、学期、课程类型、课时、课程大纲</b> 为选填;</li>'
'<b>所属学院、学期、课程类型、课时、课程大纲、业务情景</b> 为选填;</li>'
'<li>课程类型可选:理论 / 实践</li>'
'<li><b>章节 / 任务(可选)</b>:填「章节标题」会自动建/复用章节,'
'填「任务名称」会在该章节下建一条任务列表明细(是否完成填 是/否);'
'同一课程、同一章节多行会自动合并,不会重复创建;</li>'
'<li>保存为 CSV 后上传并点击「开始导入」;</li>'
'<li>若课程编号已存在则跳过,不会重复创建。</li>'
'<li>若课程名称已存在则复用,不会重复创建课程。</li>'
'</ol>'
),
)
@@ -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