完善教育
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user