exam draft notification and submission

This commit is contained in:
root
2026-04-09 01:50:24 -04:00
parent 112d073235
commit 2543df3d33
4 changed files with 262 additions and 40 deletions
+137 -21
View File
@@ -26,6 +26,9 @@ class ExamDraftController extends BaseController
protected bool $hasAcceptanceTypeColumn = false;
protected bool $hasAdminIdColumn = false;
protected bool $hasReviewRevisionColumn = false;
protected bool $hasReviewerCommentColumn = false;
protected bool $hasReviewerCommentsColumn = false;
protected bool $hasAdminCommentsColumn = false;
protected string $authorIdColumn = 'teacher_id';
protected string $authorFileColumn = 'teacher_file';
protected string $authorFilenameColumn = 'teacher_filename';
@@ -65,6 +68,9 @@ class ExamDraftController extends BaseController
$this->hasAcceptanceTypeColumn = $this->schemaHasColumn('exam_drafts', 'acceptance_type');
$this->hasAdminIdColumn = $this->schemaHasColumn('exam_drafts', 'admin_id');
$this->hasReviewRevisionColumn = $this->schemaHasColumn('exam_drafts', 'review_revision');
$this->hasReviewerCommentColumn = $this->schemaHasColumn('exam_drafts', 'reviewer_comment');
$this->hasReviewerCommentsColumn = $this->schemaHasColumn('exam_drafts', 'reviewer_comments');
$this->hasAdminCommentsColumn = $this->schemaHasColumn('exam_drafts', 'admin_comments');
$this->authorIdColumn = $this->schemaHasColumn('exam_drafts', 'teacher_id') ? 'teacher_id' : 'author_id';
$this->authorFileColumn = $this->schemaHasColumn('exam_drafts', 'teacher_file') ? 'teacher_file' : 'author_file';
$this->authorFilenameColumn = $this->schemaHasColumn('exam_drafts', 'teacher_filename') ? 'teacher_filename' : 'author_filename';
@@ -73,7 +79,9 @@ class ExamDraftController extends BaseController
: ($this->schemaHasColumn('exam_drafts', 'reviewer_id') ? 'reviewer_id' : '');
$this->reviewerCommentColumn = $this->schemaHasColumn('exam_drafts', 'reviewer_comment')
? 'reviewer_comment'
: ($this->schemaHasColumn('exam_drafts', 'admin_comments') ? 'admin_comments' : '');
: ($this->schemaHasColumn('exam_drafts', 'reviewer_comments')
? 'reviewer_comments'
: ($this->schemaHasColumn('exam_drafts', 'admin_comments') ? 'admin_comments' : ''));
helper(['form', 'url', 'date']);
}
@@ -116,19 +124,22 @@ class ExamDraftController extends BaseController
// Keep all submissions visible; legacy ones are also surfaced in a separate tab
$drafts = $allDrafts;
$legacyQuery = $this->examDraftModel
->select($this->draftSelectColumns())
->select('cs.class_section_name')
->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left')
->where('exam_drafts.is_legacy', 1)
->where('exam_drafts.status', 'legacy')
->where('exam_drafts.final_file IS NOT NULL', null, false);
if (!empty($classSectionIds)) {
$legacyExams = $this->examDraftModel
->select($this->draftSelectColumns())
->select('cs.class_section_name')
->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left')
->whereIn('exam_drafts.class_section_id', $classSectionIds)
->where('exam_drafts.is_legacy', 1)
->where('exam_drafts.status', 'legacy')
->where('exam_drafts.final_file IS NOT NULL', null, false)
->orderBy('cs.class_section_name', 'ASC')
->orderBy('exam_drafts.created_at', 'DESC')
->findAll();
$legacyQuery = $legacyQuery->whereIn('exam_drafts.class_section_id', $classSectionIds);
}
$legacyExams = $legacyQuery
->orderBy('cs.class_section_name', 'ASC')
->orderBy('exam_drafts.created_at', 'DESC')
->findAll();
} else {
// Legacy column absent, show all drafts and skip legacy tab query
$drafts = $allDrafts;
@@ -283,11 +294,12 @@ class ExamDraftController extends BaseController
'status' => 'submitted',
];
$this->applyAuthorFilePayload($updateSubmit, $teacherFile, $teacherFilename);
if ($this->examDraftModel->update($draftRowId, $updateSubmit)) {
$saved = $this->ensureDraftStatus($draftRowId, 'submitted');
$this->notifyExamDraftEvent($saved, 'submitted');
return redirect()->to('/teacher/exam-drafts')->with('success', 'Exam draft submitted for review.');
}
if ($this->examDraftModel->update($draftRowId, $updateSubmit)) {
$saved = $this->ensureDraftStatus($draftRowId, 'submitted');
$this->notifyPrincipalExamDraftSubmitted($saved, $teacherId, $classSectionId);
$this->notifyExamDraftEvent($saved, 'submitted');
return redirect()->to('/teacher/exam-drafts')->with('success', 'Exam draft submitted for review.');
}
return redirect()->back()->withInput()->with('error', 'Unable to submit the exam draft.');
}
@@ -319,6 +331,7 @@ class ExamDraftController extends BaseController
if ($this->examDraftModel->insert($payload)) {
$newId = (int) $this->examDraftModel->getInsertID();
$saved = $newId > 0 ? $this->ensureDraftStatus($newId, 'submitted') : null;
$this->notifyPrincipalExamDraftSubmitted($saved, $teacherId, $classSectionId);
$this->notifyExamDraftEvent($saved, 'submitted');
return redirect()->to('/teacher/exam-drafts')->with('success', 'Exam draft submitted for review.');
}
@@ -556,8 +569,16 @@ class ExamDraftController extends BaseController
'status' => $status,
'reviewed_at' => utc_now(),
];
if ($this->reviewerCommentColumn !== '') {
$update[$this->reviewerCommentColumn] = $reviewerComment === '' ? null : $reviewerComment;
$existingComment = (string) ($draft['reviewer_comment'] ?? $draft['reviewer_comments'] ?? $draft['admin_comments'] ?? '');
$commentValue = $reviewerComment === '' ? null : $this->appendReviewerComment($existingComment, $reviewerComment);
if ($this->hasReviewerCommentColumn) {
$update['reviewer_comment'] = $commentValue;
}
if ($this->hasReviewerCommentsColumn) {
$update['reviewer_comments'] = $commentValue;
}
if ($this->hasAdminCommentsColumn) {
$update['admin_comments'] = $commentValue;
}
if ($this->reviewerIdColumn !== '') {
$update[$this->reviewerIdColumn] = (int) (session()->get('user_id') ?? 0);
@@ -589,8 +610,14 @@ class ExamDraftController extends BaseController
if ($this->hasReviewRevisionColumn) {
$newRow['review_revision'] = $reviewRevision;
}
if ($this->reviewerCommentColumn !== '') {
$newRow[$this->reviewerCommentColumn] = $update[$this->reviewerCommentColumn] ?? null;
if ($this->hasReviewerCommentColumn) {
$newRow['reviewer_comment'] = $commentValue;
}
if ($this->hasReviewerCommentsColumn) {
$newRow['reviewer_comments'] = $commentValue;
}
if ($this->hasAdminCommentsColumn) {
$newRow['admin_comments'] = $commentValue;
}
if ($this->reviewerIdColumn !== '') {
$newRow[$this->reviewerIdColumn] = $update[$this->reviewerIdColumn] ?? null;
@@ -831,6 +858,46 @@ class ExamDraftController extends BaseController
return $saved;
}
private function notifyPrincipalExamDraftSubmitted(?array $draft, int $teacherId, int $classSectionId): void
{
$principalEmail = trim((string) (env('PRINCIPAL_EMAIL') ?: ''));
if ($principalEmail === '' || !filter_var($principalEmail, FILTER_VALIDATE_EMAIL)) {
return;
}
if (empty($draft)) {
return;
}
try {
$teacher = $this->userModel->find($teacherId) ?? [];
$teacherName = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
if ($teacherName === '') {
$teacherName = 'Teacher #' . $teacherId;
}
$class = $this->classSectionModel->find($classSectionId) ?? [];
$className = $class['class_section_name'] ?? ('Class ' . $classSectionId);
$subject = 'Exam draft submitted: ' . $className;
$submittedAt = $draft['created_at'] ?? $draft['updated_at'] ?? '';
$body = '<p>A teacher has submitted a new exam draft.</p>'
. '<table style="border-collapse:collapse;">'
. '<tr><td style="padding:4px 8px;"><strong>Teacher</strong></td><td style="padding:4px 8px;">' . esc($teacherName) . '</td></tr>'
. '<tr><td style="padding:4px 8px;"><strong>Class</strong></td><td style="padding:4px 8px;">' . esc($className) . '</td></tr>'
. '<tr><td style="padding:4px 8px;"><strong>Exam type</strong></td><td style="padding:4px 8px;">' . esc($draft['exam_type'] ?? 'N/A') . '</td></tr>'
. '<tr><td style="padding:4px 8px;"><strong>Version</strong></td><td style="padding:4px 8px;">' . esc((string) ($draft['version'] ?? '')) . '</td></tr>'
. '<tr><td style="padding:4px 8px;"><strong>Status</strong></td><td style="padding:4px 8px;">' . esc((string) ($draft['status'] ?? 'submitted')) . '</td></tr>'
. '<tr><td style="padding:4px 8px;"><strong>Submitted at</strong></td><td style="padding:4px 8px;">' . esc((string) $submittedAt) . '</td></tr>'
. '</table>'
. '<p>Review drafts: <a href="' . esc(base_url('administrator/exam-drafts')) . '">Admin exam drafts</a></p>';
$mailer = \Config\Services::emailService();
$mailer->send($principalEmail, $subject, $body, 'notifications');
} catch (\Throwable $e) {
log_message('error', 'Failed to send exam draft notification: ' . $e->getMessage());
}
}
private function nextDraftVersion(array $draft): int
{
$query = $this->examDraftModel
@@ -887,6 +954,36 @@ class ExamDraftController extends BaseController
return $current + 1;
}
private function appendReviewerComment(string $existing, string $incoming): string
{
$existing = trim($existing);
$incoming = trim($incoming);
if ($incoming === '') {
return $existing;
}
if ($existing !== '' && str_starts_with($incoming, $existing)) {
$remainder = trim(substr($incoming, strlen($existing)));
if ($remainder === '') {
return $existing;
}
$incoming = $remainder;
}
$lines = $existing === '' ? [] : preg_split('/\R/', $existing);
$lastLine = $lines ? trim((string) end($lines)) : '';
if ($lastLine !== '') {
$lastBody = preg_replace('/^\d+\-\s*/', '', $lastLine);
if ($lastBody !== null && trim($lastBody) === $incoming) {
return $existing;
}
}
$nextIndex = count($lines) + 1;
$incoming = trim(preg_replace('/\R+/', ' ', $incoming) ?? $incoming);
$lines[] = $nextIndex . '- ' . $incoming;
return implode("\n", $lines);
}
private function statusOptions(): array
{
return ['submitted', 'accepted', 'review needed', 'rejected', 'canceled', 'under review', 'legacy'];
@@ -1001,6 +1098,7 @@ class ExamDraftController extends BaseController
$grouped[$key]['_is_review_row'] = true;
$grouped[$key]['review_files'] = [];
$grouped[$key]['final_version'] = null;
$grouped[$key]['_latest_review_comment'] = null;
}
$grouped[$key]['review_files'][] = [
'review_revision' => $reviewRev,
@@ -1008,6 +1106,16 @@ class ExamDraftController extends BaseController
'final_filename' => $row['final_filename'] ?? null,
'status' => $row['status'] ?? null,
];
$reviewComment = $row['reviewer_comment'] ?? $row['admin_comments'] ?? null;
if ($reviewComment !== null && $reviewComment !== '') {
$latestComment = $grouped[$key]['_latest_review_comment'] ?? null;
if ($latestComment === null || $reviewRev > (int) ($latestComment['review_revision'] ?? 0)) {
$grouped[$key]['_latest_review_comment'] = [
'review_revision' => $reviewRev,
'comment' => $reviewComment,
];
}
}
if (strtolower((string) ($row['status'] ?? '')) === 'accepted') {
$currentFinal = $grouped[$key]['final_version'] ?? null;
if ($currentFinal === null || $reviewRev > (int) ($currentFinal['review_revision'] ?? 0)) {
@@ -1029,8 +1137,12 @@ class ExamDraftController extends BaseController
if (!empty($grouped[$key]['_is_review_row'])) {
$reviewFiles = $grouped[$key]['review_files'] ?? [];
$latestReviewComment = $grouped[$key]['_latest_review_comment'] ?? null;
$grouped[$key] = $row;
$grouped[$key]['review_files'] = $reviewFiles;
if (!empty($latestReviewComment['comment'])) {
$grouped[$key]['reviewer_comment'] = $latestReviewComment['comment'];
}
unset($grouped[$key]['_is_review_row']);
}
}
@@ -1043,7 +1155,11 @@ class ExamDraftController extends BaseController
$row['final_file'] = $row['final_version']['final_file'] ?? $row['final_file'] ?? null;
$row['final_filename'] = $row['final_version']['final_filename'] ?? $row['final_filename'] ?? null;
}
if (empty($row['reviewer_comment']) && !empty($row['_latest_review_comment']['comment'])) {
$row['reviewer_comment'] = $row['_latest_review_comment']['comment'];
}
unset($row['_is_review_row']);
unset($row['_latest_review_comment']);
}
unset($row);