238 lines
9.5 KiB
Python
238 lines
9.5 KiB
Python
from odoo import api, fields, models
|
|
|
|
|
|
class CourseChapterAttachment(models.Model):
|
|
"""课程章节附件(每个章节可挂多个文件:PDF / PPT / 资料等)。"""
|
|
|
|
_name = 'yuthon.school.course.chapter.attachment'
|
|
_description = '课程章节附件'
|
|
_order = 'sequence, id'
|
|
|
|
chapter_id = fields.Many2one(
|
|
'yuthon.school.course.chapter', string='章节', ondelete='cascade')
|
|
file = fields.Binary(string='文件', attachment=True)
|
|
name = fields.Char(string='文件名')
|
|
download_url = fields.Char(string='下载链接', compute='_compute_download_url')
|
|
download_link_html = fields.Html(
|
|
string='下载', compute='_compute_download_link_html', sanitize=False)
|
|
sequence = fields.Integer(string='排序', default=10)
|
|
|
|
@api.depends('file')
|
|
def _compute_download_url(self):
|
|
"""生成附件下载直链。"""
|
|
Attachment = self.env['ir.attachment'].sudo()
|
|
for rec in self:
|
|
if rec.file:
|
|
attachment = Attachment.search([
|
|
('res_model', '=', 'yuthon.school.course.chapter.attachment'),
|
|
('res_field', '=', 'file'),
|
|
('res_id', '=', rec.id)
|
|
], limit=1, order='id desc')
|
|
rec.download_url = (
|
|
f'/web/content/{attachment.id}?download=1'
|
|
if attachment else '')
|
|
else:
|
|
rec.download_url = ''
|
|
|
|
@api.depends('download_url', 'file')
|
|
def _compute_download_link_html(self):
|
|
for rec in self:
|
|
if rec.download_url:
|
|
rec.download_link_html = (
|
|
f'<a href="{rec.download_url}" target="_blank" '
|
|
f'class="btn btn-sm btn-primary">⬇ 下载</a>'
|
|
)
|
|
else:
|
|
rec.download_link_html = ''
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
return super().create(vals_list)
|
|
|
|
def write(self, vals):
|
|
return super().write(vals)
|
|
|
|
|
|
class CourseChapter(models.Model):
|
|
"""课程章节——老师端在课程内容里维护,学生端作为学习目录。
|
|
|
|
章节与课程同源:老师填写的视频、附件直接驱动学生端「课程学习」页面,
|
|
天然联动,无需额外同步。
|
|
"""
|
|
|
|
_name = 'yuthon.school.course.chapter'
|
|
_description = '课程章节'
|
|
_order = 'course_id, sequence, id'
|
|
|
|
course_id = fields.Many2one(
|
|
'yuthon.school.course', string='课程', required=True, ondelete='cascade')
|
|
name = fields.Char(string='章节标题', required=True, index=True)
|
|
sequence = fields.Integer(string='排序', default=10)
|
|
description = fields.Html(string='章节说明', sanitize=True)
|
|
|
|
# 视频与附件
|
|
video_file = fields.Binary(string='视频文件', attachment=True)
|
|
video_filename = fields.Char(string='视频文件名')
|
|
video_filename_display = fields.Char(
|
|
string='视频文件名(显示)', compute='_compute_video_filename_display',
|
|
store=True)
|
|
video_embed_html = fields.Html(
|
|
string='视频播放', compute='_compute_video_embed_html', sanitize=False)
|
|
attachment_ids = fields.One2many(
|
|
'yuthon.school.course.chapter.attachment', 'chapter_id', string='附件')
|
|
|
|
# 随堂笔记:老师按章节记录的富文本笔记(与章节编辑表单共用同一字段)
|
|
notes = fields.Html(string='随堂笔记', sanitize=True)
|
|
|
|
@api.depends('video_file', 'video_filename')
|
|
def _compute_video_filename_display(self):
|
|
"""显示用:从 ir.attachment 读取视频真实文件名并持久化。"""
|
|
for chapter in self:
|
|
chapter.video_filename_display = chapter.video_filename or ''
|
|
|
|
@api.depends('video_file')
|
|
def _compute_video_embed_html(self):
|
|
"""学生端:生成可直接播放的视频 HTML。"""
|
|
Attachment = self.env['ir.attachment'].sudo()
|
|
for chapter in self:
|
|
if not chapter.video_file:
|
|
chapter.video_embed_html = False
|
|
continue
|
|
attachment = Attachment.search([
|
|
('res_model', '=', 'yuthon.school.course.chapter'),
|
|
('res_field', '=', 'video_file'),
|
|
('res_id', '=', chapter.id)
|
|
], limit=1, order='id desc')
|
|
if attachment:
|
|
chapter.video_embed_html = (
|
|
f'<video controls style="width:100%;max-width:720px;" '
|
|
f'preload="metadata">'
|
|
f'<source src="/web/content/{attachment.id}" '
|
|
f'type="video/mp4">'
|
|
f'</video>'
|
|
)
|
|
else:
|
|
chapter.video_embed_html = False
|
|
|
|
# 已完成学员(标记学习完成的学生)
|
|
completed_student_ids = fields.Many2many(
|
|
'yuthon.school.student', string='已完成学员')
|
|
|
|
# 当前用户是否已完成(仅用于列表/表单展示,不入库)
|
|
is_done_for_me = fields.Boolean(
|
|
string='我已学完', compute='_compute_is_done_for_me', store=False)
|
|
|
|
@api.depends_context('uid')
|
|
def _compute_is_done_for_me(self):
|
|
student = self.env['yuthon.school.student'].sudo().search(
|
|
[('user_id', '=', self.env.uid)], limit=1)
|
|
for r in self:
|
|
r.is_done_for_me = bool(student and student in r.completed_student_ids)
|
|
|
|
def action_open_study(self):
|
|
"""学生端:弹窗打开当前章节的学习页(带视频播放/附件下载)。"""
|
|
self.ensure_one()
|
|
study_form = self.env.ref(
|
|
'college.yuthon_school_course_chapter_study_form')
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': '学习章节',
|
|
'res_model': 'yuthon.school.course.chapter',
|
|
'res_id': self.id,
|
|
'view_mode': 'form',
|
|
'views': [(study_form.id, 'form')],
|
|
'target': 'new',
|
|
}
|
|
|
|
def action_open_edit(self):
|
|
"""老师端:弹窗打开当前章节的编辑页。"""
|
|
self.ensure_one()
|
|
edit_form = self.env.ref(
|
|
'college.yuthon_school_course_chapter_form_view')
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': '编辑章节',
|
|
'res_model': 'yuthon.school.course.chapter',
|
|
'res_id': self.id,
|
|
'view_mode': 'form',
|
|
'view_id': edit_form.id,
|
|
'views': [(edit_form.id, 'form')],
|
|
'target': 'new',
|
|
}
|
|
|
|
def action_toggle_complete(self):
|
|
"""学生端:标记 / 取消标记本章节为已完成。"""
|
|
self.ensure_one()
|
|
student = self.env['yuthon.school.student'].sudo().search(
|
|
[('user_id', '=', self.env.uid)], limit=1)
|
|
if not student:
|
|
return {'type': 'ir.actions.act_window_close'}
|
|
if student in self.completed_student_ids:
|
|
self.sudo().write({'completed_student_ids': [(3, student.id)]})
|
|
else:
|
|
self.sudo().write({'completed_student_ids': [(4, student.id)]})
|
|
# 同步更新学习进展的最近学习日期与状态
|
|
progress = self.env['yuthon.school.progress'].sudo().search([
|
|
('course_id', '=', self.course_id.id),
|
|
('student_id', '=', student.id),
|
|
], limit=1)
|
|
if progress:
|
|
progress.sudo().write({'last_study_date': fields.Date.context_today(self)})
|
|
progress.sudo().recompute_status()
|
|
# 重新打开本章节表单,刷新已完成状态
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'yuthon.school.course.chapter',
|
|
'res_id': self.id,
|
|
'view_mode': 'form',
|
|
'target': 'current',
|
|
}
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
chapters = super().create(vals_list)
|
|
chapters._sync_video_filename()
|
|
chapters._refresh_course_progress()
|
|
return chapters
|
|
|
|
def write(self, vals):
|
|
res = super().write(vals)
|
|
if 'video_file' in vals:
|
|
self._sync_video_filename()
|
|
return res
|
|
|
|
def _sync_video_filename(self):
|
|
"""保持 video_filename 与 ir.attachment.name 一致:
|
|
- 视频被清空:清空 video_filename
|
|
- 已存在 attachment 且 chapter.video_filename 与之不一致:把附件名同步为 chapter.video_filename
|
|
"""
|
|
Attachment = self.env['ir.attachment'].sudo()
|
|
for chapter in self:
|
|
if not chapter.video_file:
|
|
# 视频被清空,连带清空文件名
|
|
if chapter.video_filename:
|
|
chapter.video_filename = ''
|
|
continue
|
|
attachment = Attachment.search([
|
|
('res_model', '=', 'yuthon.school.course.chapter'),
|
|
('res_field', '=', 'video_file'),
|
|
('res_id', '=', chapter.id)
|
|
], limit=1, order='id desc')
|
|
if attachment and chapter.video_filename \
|
|
and attachment.name != chapter.video_filename:
|
|
attachment.sudo().write({'name': chapter.video_filename})
|
|
|
|
def unlink(self):
|
|
courses = self.mapped('course_id')
|
|
res = super().unlink()
|
|
Progress = self.env['yuthon.school.progress'].sudo()
|
|
for course in courses:
|
|
Progress.search([('course_id', '=', course.id)]).recompute_status()
|
|
return res
|
|
|
|
def _refresh_course_progress(self):
|
|
"""章节创建后,刷新所属课程所有学员的进展状态。"""
|
|
Progress = self.env['yuthon.school.progress'].sudo()
|
|
for course in self.mapped('course_id'):
|
|
Progress.search([('course_id', '=', course.id)]).recompute_status()
|