Files
shcool/college/models/chapter.py
T

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()