from odoo import models, fields, api from odoo.exceptions import ValidationError, AccessError class EducationLMSSubmission(models.Model): _name = 'education.lms.submission' _description = 'LMS Submission' assignment_id = fields.Many2one('education.lms.assignment', string='Assignment', required=True, ondelete='cascade') student_id = fields.Many2one('education.student', string='Student', required=True) submission_date = fields.Datetime(string='Submission Date', readonly=True) attachment_ids = fields.Many2many('ir.attachment', string='Attachments') text_submission = fields.Html(string='Text Submission') status = fields.Selection([ ('draft', 'Draft'), ('submitted', 'Submitted'), ('evaluated', 'Evaluated'), ('late', 'Late') ], string='Status', default='draft', required=True) marks = fields.Float(string='Marks') feedback = fields.Html(string='Feedback') evaluation_date = fields.Datetime(string='Evaluation Date') evaluated_by = fields.Many2one('res.users', string='Evaluated By', readonly=True) is_late = fields.Boolean(string='Is Late', compute='_compute_is_late', store=True) _unique_assignment_student = models.Constraint('unique(assignment_id, student_id)', 'A student can only have one submission per assignment.') @api.depends('submission_date', 'assignment_id.due_date') def _compute_is_late(self): for record in self: if record.submission_date and record.assignment_id.due_date: # Compare Datetime to Date (convert due_date to end of day conceptually) record.is_late = record.submission_date.date() > record.assignment_id.due_date else: record.is_late = False def action_submit(self): for record in self: if record.status != 'draft': raise ValidationError("Only draft submissions can be submitted.") # validation for content if not record.attachment_ids and not record.text_submission: raise ValidationError("You must provide either a text submission or an attachment to submit.") record.write({'status': 'submitted'}) def action_evaluate(self): for record in self: if record.status not in ['submitted', 'late', 'evaluated']: raise ValidationError("Can only evaluate submitted or late assignments.") record.write({'status': 'evaluated'}) @api.constrains('status', 'attachment_ids', 'text_submission') def _check_content(self): for record in self: if record.status in ['submitted', 'late', 'evaluated'] and not record.attachment_ids and not record.text_submission: raise ValidationError("You must provide either a text submission or an attachment to submit.") @api.constrains('marks', 'status') def _check_marks(self): for record in self: if record.status == 'evaluated': # To distinguish empty marks from 0.0, we can check if it's False, but Odoo Float often returns 0.0. # However, if we literally need to reject "empty" marks, we check if it is literally False. # In Odoo, Float field without default=0.0 will return 0.0. If we must reject empty, how? # Actually, in Odoo 17, if a Float field is null in the database, it is 0.0 in ORM. # But let's check if we can verify it's provided in vals when evaluated. pass if record.status == 'evaluated': if record.marks < 0: raise ValidationError("Marks cannot be negative.") if record.marks > record.assignment_id.maximum_marks: raise ValidationError(f"Marks cannot exceed the maximum allowed ({record.assignment_id.maximum_marks}).") @api.constrains('student_id', 'assignment_id') def _check_eligibility(self): for record in self: student = record.student_id.sudo() course = record.assignment_id.course_id if course.program_id != student.program_id: raise ValidationError("Student program does not match the course program.") if course.academic_year_id != student.academic_year_id: raise ValidationError("Student academic year does not match the course academic year.") if course.term_id and course.term_id != student.academic_term_id: raise ValidationError("Student term does not match the course term.") if course.batch_id and course.batch_id != student.batch_id: raise ValidationError("Student batch does not match the course batch.") @api.constrains('student_id') def _check_student_ownership(self): if self.env.su or self.env.user.has_group('education_lms.group_education_lms_faculty'): return if self.env.user.has_group('education_lms.group_education_lms_student'): for record in self: if record.student_id.sudo().portal_user_id.id != self.env.user.id: raise AccessError("You can only create or edit submissions for yourself.") @api.constrains('assignment_id') def _check_assignment_closed(self): for record in self: if record.assignment_id.state == 'closed': raise ValidationError("Cannot create or modify submissions for a closed assignment.") @api.model_create_multi def create(self, vals_list): is_faculty = self.env.user.has_group('education_lms.group_education_lms_faculty') is_admin = self.env.su or self.env.user.has_group('education_lms.group_education_lms_admin') is_student = self.env.user.has_group('education_lms.group_education_lms_student') if is_student and not (is_admin or is_faculty): for vals in vals_list: if 'marks' in vals or 'feedback' in vals: raise AccessError("Students cannot set marks or feedback.") if 'status' in vals and vals['status'] not in ['draft', 'submitted']: raise AccessError("Students cannot set submission status to this value.") if 'evaluated_by' in vals or 'evaluation_date' in vals: raise AccessError("Students cannot set evaluation metadata.") return super().create(vals_list) def write(self, vals): is_faculty = self.env.user.has_group('education_lms.group_education_lms_faculty') is_admin = self.env.su or self.env.user.has_group('education_lms.group_education_lms_admin') is_student = self.env.user.has_group('education_lms.group_education_lms_student') for record in self: if 'student_id' in vals and vals['student_id'] != record.student_id.id: raise AccessError("Cannot change the student of an existing submission.") if 'assignment_id' in vals and vals['assignment_id'] != record.assignment_id.id: raise AccessError("Cannot change the assignment of an existing submission.") # Lifecycle block: cannot modify if closed if record.assignment_id.state == 'closed' and not is_admin: raise AccessError("Closed assignments cannot be modified.") if is_faculty and not is_admin: if record.assignment_id.course_id.faculty_id.id != self.env.user.id: raise AccessError("You can only evaluate or modify submissions for assignments in your courses.") if 'marks' in vals and vals['marks'] is False: # If faculty explicitly clears marks while status is evaluated if vals.get('status', record.status) == 'evaluated': raise ValidationError("Marks cannot be empty for an evaluated submission.") if is_student and not (is_admin or is_faculty): if record.status not in ['draft']: raise AccessError("You cannot modify a submission once it has been submitted or evaluated.") if 'marks' in vals or 'feedback' in vals: raise AccessError("Students cannot modify marks or feedback.") if 'status' in vals and vals['status'] not in ['draft', 'submitted']: raise AccessError("Students cannot modify submission status directly.") if 'evaluated_by' in vals or 'evaluation_date' in vals: raise AccessError("Students cannot modify evaluation metadata.") # Check if status changes to evaluated and marks is empty/False for record in self: new_status = vals.get('status', record.status) if new_status == 'evaluated': new_marks = vals.get('marks', record.marks) # In python, 0.0 is falsy, but we accept 0.0. We reject False or None. if new_marks is False or new_marks is None: raise ValidationError("Marks cannot be empty for an evaluated submission.") res = super().write(vals) for record in self: updates = {} if record.status == 'evaluated' and not record.evaluation_date: updates.update({'evaluation_date': fields.Datetime.now(), 'evaluated_by': self.env.user.id}) elif record.status == 'submitted' and not record.submission_date: # Calculate if late is_late = fields.Datetime.now().date() > record.assignment_id.due_date updates.update({'submission_date': fields.Datetime.now(), 'status': 'late' if is_late else 'submitted'}) if updates: record.sudo().write(updates) return res def unlink(self): is_faculty = self.env.user.has_group('education_lms.group_education_lms_faculty') is_admin = self.env.su or self.env.user.has_group('education_lms.group_education_lms_admin') is_student = self.env.user.has_group('education_lms.group_education_lms_student') for record in self: if is_faculty and not is_admin: if record.assignment_id.course_id.faculty_id.id != self.env.user.id: raise AccessError("You can only delete submissions for assignments in your courses.") if is_student and not (is_admin or is_faculty): if record.status != 'draft': raise AccessError("You can only delete submissions while they are in Draft state.") return super().unlink()