Files
shcool/college/models/assignment.py
T
2026-09-29 16:39:32 +08:00

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'