import base64 from odoo import api, fields, models from odoo.exceptions import ValidationError class CommunitySchoolAssignment(models.Model): _name = 'community.school.assignment' _description = 'School Assignment' _order = 'due_date asc, id desc' _inherit = ['mail.thread', 'mail.activity.mixin'] name = fields.Char(string='Title', required=True, tracking=True) class_id = fields.Many2one( 'community.school.class', string='Class', required=True, ondelete='cascade', ) term_id = fields.Many2one(related='class_id.term_id', store=True) teacher_id = fields.Many2one(related='class_id.teacher_id', store=True) description = fields.Html(string='Instructions') due_date = fields.Datetime(string='Due Date', required=True, tracking=True) max_score = fields.Integer(string='Max Score', default=100) state = fields.Selection( [('draft', 'Draft'), ('published', 'Published'), ('closed', 'Closed')], default='draft', required=True, tracking=True, ) submission_ids = fields.One2many('community.school.submission', 'assignment_id') submission_count = fields.Integer(compute='_compute_submission_count', store=True) @api.depends('submission_ids') def _compute_submission_count(self): for assignment in self: assignment.submission_count = len(assignment.submission_ids) def action_publish(self): for rec in self: rec.state = 'published' def action_close(self): for rec in self: rec.state = 'closed' def action_reset_draft(self): for rec in self: rec.state = 'draft' def action_view_submissions(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': f'Submissions – {self.name}', 'res_model': 'community.school.submission', 'view_mode': 'list,form', 'domain': [('assignment_id', '=', self.id)], 'context': {'default_assignment_id': self.id}, } class CommunitySchoolSubmission(models.Model): _name = 'community.school.submission' _description = 'Assignment Submission' _order = 'submitted_on desc, id desc' assignment_id = fields.Many2one( 'community.school.assignment', required=True, ondelete='cascade', ) enrollment_id = fields.Many2one( 'community.school.enrollment', string='Student Enrollment', required=True, ondelete='cascade', ) student_id = fields.Many2one(related='enrollment_id.student_id', store=True) class_id = fields.Many2one(related='assignment_id.class_id', store=True) submitted_on = fields.Datetime(string='Submitted On', readonly=True) notes = fields.Text(string='Student Notes') # File attachment stored as binary attachment_name = fields.Char(string='File Name') attachment_data = fields.Binary(string='Uploaded File', attachment=True) # Grading score = fields.Integer(string='Score') feedback = fields.Text(string='Teacher Feedback') state = fields.Selection( [('submitted', 'Submitted'), ('graded', 'Graded')], default='submitted', required=True, ) is_late = fields.Boolean(compute='_compute_is_late', store=True) _assignment_enrollment_uniq = models.Constraint( 'unique(assignment_id, enrollment_id)', 'A submission for this assignment by this student already exists.', ) @api.depends('submitted_on', 'assignment_id.due_date') def _compute_is_late(self): for sub in self: if sub.submitted_on and sub.assignment_id.due_date: sub.is_late = sub.submitted_on > sub.assignment_id.due_date else: sub.is_late = False @api.constrains('score', 'assignment_id') def _check_score(self): for sub in self: if sub.score < 0: raise ValidationError('Score cannot be negative.') if sub.assignment_id and sub.score > sub.assignment_id.max_score: raise ValidationError( f'Score ({sub.score}) cannot exceed max score ({sub.assignment_id.max_score}).' ) def action_grade(self): for sub in self: sub.state = 'graded'