Metatron_Student_ERP/addons/education_lms/models/education_lms_submission.py

196 lines
10 KiB
Python

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()