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='只能选择自己已发布的课程。') 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' 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'