262 lines
11 KiB
Python
262 lines
11 KiB
Python
from odoo import fields, models, api, _
|
|
from odoo.exceptions import UserError
|
|
import io
|
|
import zipfile
|
|
import base64
|
|
|
|
|
|
class SchoolAssignment(models.Model):
|
|
"""作业习题——教师布置给某门课程的学习任务。"""
|
|
|
|
_name = 'yuthon.school.assignment'
|
|
_description = '作业习题'
|
|
_order = 'due_date, id'
|
|
|
|
name = fields.Char(string='作业标题', required=True)
|
|
course_id = fields.Many2one(
|
|
'yuthon.school.course',
|
|
string='所属课程',
|
|
required=True,
|
|
ondelete='cascade',
|
|
domain="[('state', '=', 'published'), ('teacher_id', '=', uid)]",
|
|
help='只能选择自己已发布的课程。')
|
|
chapter_id = fields.Many2one(
|
|
'yuthon.school.course.chapter',
|
|
string='对应章节',
|
|
domain="[('course_id', '=', course_id)]",
|
|
ondelete='set null',
|
|
index=True,
|
|
help='该作业所属的课程章节,不选则代表整门课程作业。')
|
|
teacher_id = fields.Many2one(
|
|
'res.users', string='布置教师',
|
|
default=lambda self: self.env.user, required=True)
|
|
description = fields.Html(string='作业说明', sanitize=True)
|
|
due_date = fields.Date(string='截止日期')
|
|
attachment = fields.Binary(string='附件', attachment=True)
|
|
attachment_filename = fields.Char(string='附件名')
|
|
state = fields.Selection(
|
|
[('draft', '草稿'), ('published', '已发布'), ('closed', '已截止')],
|
|
string='状态', default='draft', index=True)
|
|
submission_ids = fields.One2many(
|
|
'yuthon.school.assignment.submission', 'assignment_id', string='提交记录')
|
|
submission_count = fields.Integer(
|
|
string='提交数', compute='_compute_submission_count', store=True)
|
|
active = fields.Boolean(string='启用', default=True)
|
|
|
|
@api.depends('submission_ids.state')
|
|
def _compute_submission_count(self):
|
|
for r in self:
|
|
r.submission_count = len(r.submission_ids.filtered(
|
|
lambda s: s.state in ('submitted', 'graded')))
|
|
|
|
def action_publish(self):
|
|
"""发布作业,仅草稿状态可发布;发布后自动为选课学员创建待提交记录。"""
|
|
for record in self:
|
|
if record.state != 'draft':
|
|
raise UserError(_('作业「%s」当前状态为 %s,请勿重复发布。') % (record.name or '', record.state))
|
|
self.write({'state': 'published'})
|
|
self._create_pending_submissions()
|
|
return True
|
|
|
|
def action_publish_batch(self):
|
|
"""批量发布:仅处理草稿状态的作业。"""
|
|
for record in self:
|
|
record.action_publish()
|
|
|
|
def _create_pending_submissions(self):
|
|
"""为课程的每个选课学员创建一条待提交记录(去重)。"""
|
|
submission_env = self.env['yuthon.school.assignment.submission'].sudo()
|
|
for assignment in self:
|
|
course = assignment.course_id
|
|
if not course or not course.student_ids:
|
|
continue
|
|
existing_students = set(
|
|
submission_env.search([
|
|
('assignment_id', '=', assignment.id),
|
|
]).mapped('student_id').ids
|
|
)
|
|
for student in course.student_ids:
|
|
if student.id in existing_students:
|
|
continue
|
|
submission_env.create({
|
|
'assignment_id': assignment.id,
|
|
'student_id': student.id,
|
|
'state': 'pending',
|
|
})
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
assignments = super().create(vals_list)
|
|
assignments._check_overdue()
|
|
return assignments
|
|
|
|
def write(self, vals):
|
|
res = super().write(vals)
|
|
if any(k in vals for k in ('due_date', 'state')):
|
|
self._check_overdue()
|
|
return res
|
|
|
|
def _check_overdue(self):
|
|
"""根据截止日期自动调整状态(仅对已发布/已截止生效):
|
|
- 截止日期早于今天 → 已截止
|
|
- 截止日期为今天或以后 → 已发布(支持把过期作业改回未来日期恢复发布)
|
|
草稿状态不受截止日期影响;未填写截止日期则保持已发布。
|
|
"""
|
|
today = fields.Date.context_today(self)
|
|
for record in self:
|
|
if record.state in ('published', 'closed') and record.due_date:
|
|
target = 'closed' if record.due_date < today else 'published'
|
|
if record.state != target:
|
|
record.write({'state': target})
|
|
|
|
|
|
class SchoolAssignmentSubmission(models.Model):
|
|
"""作业提交——学员针对某次作业的作答与教师批改记录。"""
|
|
|
|
_name = 'yuthon.school.assignment.submission'
|
|
_description = '作业提交'
|
|
_order = 'submit_time desc, id'
|
|
_rec_name = 'name'
|
|
|
|
chapter_id = fields.Many2one(
|
|
'yuthon.school.course.chapter',
|
|
string='章节',
|
|
related='assignment_id.chapter_id',
|
|
store=True, index=True,
|
|
)
|
|
|
|
|
|
|
|
name = fields.Char(string='提交标题', readonly=True, copy=False)
|
|
assignment_id = fields.Many2one(
|
|
'yuthon.school.assignment', string='作业', required=True, ondelete='cascade')
|
|
student_id = fields.Many2one(
|
|
'yuthon.school.student', string='学员', required=True, ondelete='cascade',
|
|
default=lambda self: self._default_student())
|
|
|
|
@api.model
|
|
def _default_student(self):
|
|
return self.env['yuthon.school.student'].search(
|
|
[('user_id', '=', self.env.uid)], limit=1)
|
|
content = fields.Html(string='作答内容', sanitize=True)
|
|
attachment = fields.Binary(string='提交文件', attachment=True)
|
|
attachment_filename = fields.Char(string='文件名')
|
|
assignment_attachment = fields.Binary(
|
|
string='老师布置���附件', related='assignment_id.attachment',
|
|
readonly=True)
|
|
assignment_attachment_filename = fields.Char(
|
|
related='assignment_id.attachment_filename',
|
|
readonly=True)
|
|
submit_time = fields.Datetime(string='提交时间')
|
|
score = fields.Float(string='得分')
|
|
feedback = fields.Text(string='评语')
|
|
due_date = fields.Date(
|
|
string='截止日期',
|
|
related='assignment_id.due_date',
|
|
readonly=True, store=True)
|
|
state = fields.Selection(
|
|
[('pending', '待提交'), ('submitted', '待批改'), ('graded', '已批改')],
|
|
string='状态', default='pending', index=True)
|
|
teacher_id = fields.Many2one(
|
|
'res.users', string='批改人',
|
|
related='assignment_id.teacher_id',
|
|
readonly=True, store=False)
|
|
active = fields.Boolean(string='启用', default=True)
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for vals in vals_list:
|
|
if not vals.get('name'):
|
|
assignment = self.env['yuthon.school.assignment'].browse(
|
|
vals.get('assignment_id'))
|
|
student = self.env['yuthon.school.student'].browse(
|
|
vals.get('student_id'))
|
|
a_name = assignment.name or '作业'
|
|
s_name = student.name or '学员'
|
|
vals['name'] = f'{a_name} - {s_name}的提交'
|
|
return super().create(vals_list)
|
|
|
|
@api.onchange('assignment_id', 'student_id')
|
|
def _onchange_submission_name(self):
|
|
if self.assignment_id and self.student_id and not self.name:
|
|
self.name = f'{self.assignment_id.name} - {self.student_id.name}的提交'
|
|
|
|
def action_submit(self):
|
|
"""学生点击提交:待提交 → 待批改,并记录提交时间。"""
|
|
for record in self:
|
|
if record.state != 'pending':
|
|
raise UserError(_('只有待提交状态的作业可以执行提交。'))
|
|
self.write({
|
|
'state': 'submitted',
|
|
'submit_time': fields.Datetime.now(),
|
|
})
|
|
|
|
def action_open_batch_wizard(self):
|
|
"""老师批量批改:打开批量打分向导。"""
|
|
if not self.env.user.has_group('college.group_teacher'):
|
|
raise UserError(_('只有老师可以进行批量批改。'))
|
|
return {
|
|
'name': _('批量批改'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'yuthon.school.assignment.submission.batch.wizard',
|
|
'view_mode': 'form',
|
|
'target': 'new',
|
|
'context': self.env.context,
|
|
}
|
|
|
|
def action_download_attachments(self):
|
|
"""老师批量下载提交文件:把选中的提交记录的附件打包为 zip,
|
|
zip 内文件名格式为「学员姓名 + 原文件名」,并触发下载。"""
|
|
if not self.env.user.has_group('college.group_teacher'):
|
|
raise UserError(_('只有老师可以批量下载提交文件。'))
|
|
submissions = self.filtered(lambda s: s.attachment)
|
|
if not submissions:
|
|
raise UserError(_('选中的提交记录中没有可下载的附件。'))
|
|
# 收集每个 zip 内的文件名,防止重复(同一学员多个提交同名文件)
|
|
used_names = set()
|
|
zip_buffer = io.BytesIO()
|
|
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
|
|
for sub in submissions:
|
|
student_name = sub.student_id.name or '未命名学员'
|
|
original_filename = sub.attachment_filename or '附件'
|
|
zip_name = f'{student_name}_{original_filename}'
|
|
# 防止 zip 内部重名
|
|
if zip_name in used_names:
|
|
counter = 2
|
|
while f'{student_name}_{counter}_{original_filename}' in used_names:
|
|
counter += 1
|
|
zip_name = f'{student_name}_{counter}_{original_filename}'
|
|
used_names.add(zip_name)
|
|
zf.writestr(zip_name, base64.b64decode(sub.attachment))
|
|
zip_buffer.seek(0)
|
|
zip_b64 = base64.b64encode(zip_buffer.read()).decode('ascii')
|
|
# 临时附件存下载 zip
|
|
attachment = self.env['ir.attachment'].sudo().create({
|
|
'name': '提交文件打包.zip',
|
|
'datas': zip_b64,
|
|
'res_model': 'yuthon.school.assignment.submission',
|
|
'res_id': self[0].id if self else 0,
|
|
'type': 'binary',
|
|
})
|
|
return {
|
|
'type': 'ir.actions.act_url',
|
|
'url': f'/web/content/{attachment.id}?download=true',
|
|
'target': 'self',
|
|
}
|
|
|
|
def write(self, vals):
|
|
"""老师填写或修改分数时,自动标记为已批改。"""
|
|
if 'score' in vals and 'state' not in vals:
|
|
vals = dict(vals)
|
|
# 仅对待批改状态的记录,填分即自动变已批改
|
|
if any(r.state == 'submitted' for r in self):
|
|
vals['state'] = 'graded'
|
|
return super().write(vals)
|
|
|
|
@api.onchange('score')
|
|
def _onchange_score(self):
|
|
"""表单中填写分数后,未保存前状态栏即时显示已批改。"""
|
|
if self.state == 'submitted' and self.score:
|
|
self.state = 'graded'
|
|
|