196 lines
10 KiB
Python
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()
|