682 lines
33 KiB
PHP
682 lines
33 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\View;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Models\AssessmentFormModel;
|
|
use App\Models\AssessmentQuestionModel;
|
|
use App\Models\QuestionPoolModel;
|
|
use App\Models\StudentAssessmentModel;
|
|
use App\Models\StudentModel;
|
|
use CodeIgniter\Exceptions\PageNotFoundException;
|
|
|
|
class AssessmentController extends BaseController
|
|
{
|
|
private $db;
|
|
private QuestionPoolModel $poolModel;
|
|
private AssessmentQuestionModel $questionModel;
|
|
private AssessmentFormModel $formModel;
|
|
private StudentAssessmentModel $assessmentModel;
|
|
private StudentModel $studentModel;
|
|
|
|
public function __construct()
|
|
{
|
|
helper(['form']);
|
|
$this->db = db_connect();
|
|
$this->poolModel = new QuestionPoolModel();
|
|
$this->questionModel = new AssessmentQuestionModel();
|
|
$this->formModel = new AssessmentFormModel();
|
|
$this->assessmentModel = new StudentAssessmentModel();
|
|
$this->studentModel = new StudentModel();
|
|
}
|
|
|
|
public function pools()
|
|
{
|
|
$pool = $this->primaryPool();
|
|
if (! $pool) {
|
|
throw new PageNotFoundException('The New Student Assessment pool is not installed. Run the database migrations first.');
|
|
}
|
|
$questions = $this->questionModel->where('pool_id', $pool['id'])->orderBy('order_index', 'ASC')->orderBy('id', 'ASC')->findAll();
|
|
return view('assessments/pool', ['pool' => $pool, 'questions' => $questions]);
|
|
}
|
|
|
|
public function pool(int $id)
|
|
{
|
|
$pool = $this->primaryPool();
|
|
if (! $pool || (int) $pool['id'] !== $id) {
|
|
return redirect()->to('administrator/assessments/pools');
|
|
}
|
|
$questions = $this->questionModel->where('pool_id', $id)->orderBy('order_index', 'ASC')->orderBy('id', 'ASC')->findAll();
|
|
return view('assessments/pool', ['pool' => $pool, 'questions' => $questions]);
|
|
}
|
|
|
|
public function storeQuestion(int $poolId)
|
|
{
|
|
$this->requirePool($poolId);
|
|
if (! $this->validateQuestion()) {
|
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
|
}
|
|
$max = $this->db->table('assessment_questions')->selectMax('order_index')->where('pool_id', $poolId)->get()->getRowArray();
|
|
$this->questionModel->insert($this->questionPayload($poolId, ((int) ($max['order_index'] ?? 0)) + 1));
|
|
return redirect()->to('administrator/assessments/pools')->with('success', 'Question added.');
|
|
}
|
|
|
|
public function updateQuestion(int $id)
|
|
{
|
|
$question = $this->requireQuestion($id);
|
|
if (! $this->validateQuestion()) {
|
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
|
}
|
|
$this->questionModel->update($id, $this->questionPayload((int) $question['pool_id'], (int) $question['order_index']));
|
|
return redirect()->to('administrator/assessments/pools')->with('success', 'Question updated.');
|
|
}
|
|
|
|
public function deleteQuestion(int $id)
|
|
{
|
|
$question = $this->requireQuestion($id);
|
|
if ($this->db->table('assessment_form_questions')->where('question_id', $id)->countAllResults() > 0) {
|
|
return redirect()->back()->with('error', 'This question is used by a form and cannot be deleted.');
|
|
}
|
|
$this->questionModel->delete($id);
|
|
$this->normalizeQuestionOrder((int) $question['pool_id']);
|
|
return redirect()->to('administrator/assessments/pools')->with('success', 'Question deleted.');
|
|
}
|
|
|
|
public function moveQuestion(int $id)
|
|
{
|
|
$question = $this->requireQuestion($id);
|
|
$direction = (string) $this->request->getPost('direction');
|
|
$operator = $direction === 'up' ? '<' : '>';
|
|
$sort = $direction === 'up' ? 'DESC' : 'ASC';
|
|
$other = $this->db->table('assessment_questions')
|
|
->where('pool_id', $question['pool_id'])->where('order_index ' . $operator, $question['order_index'])
|
|
->orderBy('order_index', $sort)->limit(1)->get()->getRowArray();
|
|
if ($other) {
|
|
$this->db->transStart();
|
|
$this->questionModel->update($id, ['order_index' => $other['order_index']]);
|
|
$this->questionModel->update((int) $other['id'], ['order_index' => $question['order_index']]);
|
|
$this->db->transComplete();
|
|
}
|
|
return redirect()->to('administrator/assessments/pools');
|
|
}
|
|
|
|
public function forms()
|
|
{
|
|
$forms = $this->db->table('assessment_forms f')
|
|
->select('f.*, p.name AS pool_name, COUNT(DISTINCT fq.question_id) AS question_count, COUNT(DISTINCT sa.id) AS assignment_count')
|
|
->join('question_pools p', 'p.id = f.pool_id')
|
|
->join('assessment_form_questions fq', 'fq.form_id = f.id', 'left')
|
|
->join('student_assessments sa', 'sa.form_id = f.id', 'left')
|
|
->groupBy('f.id')->orderBy('f.created_at', 'DESC')->get()->getResultArray();
|
|
return view('assessments/forms', ['forms' => $forms]);
|
|
}
|
|
|
|
public function newForm()
|
|
{
|
|
$pool = $this->primaryPool();
|
|
$selectedSchoolYear = $this->currentSchoolYearName((string) session()->get('school_year'));
|
|
return view('assessments/form_builder', [
|
|
'form' => null,
|
|
'pools' => $pool ? [$pool] : [],
|
|
'questions' => $pool ? $this->questionModel->where('pool_id', $pool['id'])->orderBy('order_index', 'ASC')->findAll() : [],
|
|
'selectedIds' => [], 'locked' => false, 'schoolYears' => $this->schoolYearOptions(),
|
|
'selectedSchoolYear' => $selectedSchoolYear,
|
|
]);
|
|
}
|
|
|
|
public function storeForm()
|
|
{
|
|
return $this->saveForm(null);
|
|
}
|
|
|
|
public function editForm(int $id)
|
|
{
|
|
$form = $this->requireForm($id);
|
|
$questions = $this->questionModel->where('pool_id', $form['pool_id'])->orderBy('order_index', 'ASC')->findAll();
|
|
$selectedRows = $this->db->table('assessment_form_questions')->where('form_id', $id)->orderBy('order_index', 'ASC')->get()->getResultArray();
|
|
return view('assessments/form_builder', [
|
|
'form' => $form,
|
|
'pools' => $this->poolModel->orderBy('name', 'ASC')->findAll(),
|
|
'questions' => $questions,
|
|
'selectedIds' => array_map('intval', array_column($selectedRows, 'question_id')),
|
|
'locked' => $this->db->table('student_assessments')->where('form_id', $id)->countAllResults() > 0,
|
|
'schoolYears' => $this->schoolYearOptions(),
|
|
'selectedSchoolYear' => (string) ($form['school_year'] ?? ''),
|
|
]);
|
|
}
|
|
|
|
public function updateForm(int $id)
|
|
{
|
|
$this->requireForm($id);
|
|
return $this->saveForm($id);
|
|
}
|
|
|
|
public function publishForm(int $id)
|
|
{
|
|
$form = $this->requireForm($id);
|
|
$questionCount = $this->db->table('assessment_form_questions')->where('form_id', $id)->countAllResults();
|
|
if ($questionCount === 0) {
|
|
return redirect()->to('administrator/assessments/forms')->with('error', 'Add at least one question before publishing this form.');
|
|
}
|
|
if (! preg_match('/^\d{4}-\d{4}$/', (string) ($form['school_year'] ?? ''))) {
|
|
return redirect()->to('administrator/assessments/forms/' . $id . '/edit')
|
|
->with('error', 'Select a school year before publishing this form.');
|
|
}
|
|
if ($form['status'] !== 'published') {
|
|
$this->formModel->update($id, ['status' => 'published']);
|
|
}
|
|
return redirect()->to('administrator/assessments/forms')->with('success', 'Assessment form published.');
|
|
}
|
|
|
|
public function preview(int $id)
|
|
{
|
|
$data = $this->formWithQuestions($id);
|
|
$data['preview'] = true;
|
|
return view('assessments/take', $data);
|
|
}
|
|
|
|
public function newStudentRoster()
|
|
{
|
|
$schoolYear = $this->currentSchoolYearName((string) session()->get('school_year'));
|
|
$data = service('enrollmentWithdrawal')->newStudents($schoolYear);
|
|
$data['schoolYear'] = $schoolYear;
|
|
$data['assessmentByStudent'] = $this->assessmentActionsForStudents(array_column($data['new_students'] ?? [], 'id'), $schoolYear);
|
|
return view('assessments/new_students', $data);
|
|
}
|
|
|
|
public function studentAssignments(int $studentId)
|
|
{
|
|
$student = $this->studentModel->find($studentId);
|
|
if (! $student) {
|
|
throw new PageNotFoundException('Student not found.');
|
|
}
|
|
$schoolYear = $this->currentSchoolYearName((string) session()->get('school_year'));
|
|
$active = $this->activeAssessmentForStudent($studentId, $schoolYear);
|
|
if ($active) {
|
|
return view('assessments/admin_take', $this->assessmentDetails((int) $active['id']));
|
|
}
|
|
$forms = $this->db->table('assessment_forms f')
|
|
->select('f.*, p.name AS pool_name, COUNT(fq.question_id) AS question_count')
|
|
->join('question_pools p', 'p.id = f.pool_id')
|
|
->join('assessment_form_questions fq', 'fq.form_id = f.id', 'left')
|
|
->where('f.status', 'published')->where('f.school_year', $schoolYear)
|
|
->groupBy('f.id')->orderBy('f.name', 'ASC')->get()->getResultArray();
|
|
$assignments = $this->db->table('student_assessments sa')
|
|
->select('sa.*, f.name AS form_name, f.school_year')->join('assessment_forms f', 'f.id = sa.form_id')
|
|
->where('sa.student_id', $studentId)->orderBy('sa.assigned_at', 'DESC')->get()->getResultArray();
|
|
return view('assessments/student_assignments', ['student' => $student, 'forms' => $forms, 'assignments' => $assignments]);
|
|
}
|
|
|
|
public function startInterview(int $studentId)
|
|
{
|
|
if (! $this->studentModel->find($studentId)) {
|
|
throw new PageNotFoundException('Student not found.');
|
|
}
|
|
$schoolYear = $this->currentSchoolYearName((string) session()->get('school_year'));
|
|
$active = $this->activeAssessmentForStudent($studentId, $schoolYear);
|
|
if ($active) return redirect()->to("administrator/assessments/students/{$studentId}");
|
|
|
|
$assignedFormIds = array_map('intval', array_column(
|
|
$this->assessmentModel->select('form_id')->where('student_id', $studentId)->findAll(),
|
|
'form_id'
|
|
));
|
|
$formBuilder = $this->formModel->where('status', 'published')->where('school_year', $schoolYear)
|
|
->orderBy('created_at', 'DESC')->orderBy('id', 'DESC');
|
|
if ($assignedFormIds !== []) $formBuilder->whereNotIn('id', $assignedFormIds);
|
|
$form = $formBuilder->first();
|
|
if (! $form) {
|
|
return redirect()->to('administrator/assessments/students/' . $studentId)
|
|
->with('error', 'No unassigned published assessment form is available for this student.');
|
|
}
|
|
$this->assessmentModel->insert([
|
|
'form_id' => (int) $form['id'], 'student_id' => $studentId, 'status' => 'not_started',
|
|
'assigned_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
return redirect()->to("administrator/assessments/students/{$studentId}");
|
|
}
|
|
|
|
public function assign(int $studentId)
|
|
{
|
|
if (! $this->studentModel->find($studentId)) {
|
|
throw new PageNotFoundException('Student not found.');
|
|
}
|
|
$formId = (int) $this->request->getPost('form_id');
|
|
$schoolYear = $this->currentSchoolYearName((string) session()->get('school_year'));
|
|
$form = $this->formModel->where('id', $formId)->where('status', 'published')->where('school_year', $schoolYear)->first();
|
|
if (! $form) {
|
|
return redirect()->back()->with('error', 'Choose a published assessment form.');
|
|
}
|
|
if ($this->assessmentModel->where(['form_id' => $formId, 'student_id' => $studentId])->first()) {
|
|
return redirect()->back()->with('error', 'This form is already assigned to this student.');
|
|
}
|
|
try {
|
|
$inserted = $this->assessmentModel->insert([
|
|
'form_id' => $formId, 'student_id' => $studentId, 'status' => 'not_started',
|
|
'assigned_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('warning', 'Assessment assignment rejected: ' . $e->getMessage());
|
|
$inserted = false;
|
|
}
|
|
if ($inserted === false) {
|
|
return redirect()->back()->with('error', 'This form is already assigned to this student.');
|
|
}
|
|
return redirect()->to("administrator/assessments/students/{$studentId}")->with('success', 'Assessment assigned.');
|
|
}
|
|
|
|
public function grade(int $assessmentId)
|
|
{
|
|
$data = $this->assessmentDetails($assessmentId);
|
|
if (! in_array($data['assessment']['status'], ['completed', 'graded'], true)) {
|
|
return redirect()->back()->with('error', 'The student must complete the assessment before it can be reviewed.');
|
|
}
|
|
return view('assessments/grade', $data);
|
|
}
|
|
|
|
public function saveGrade(int $assessmentId)
|
|
{
|
|
$data = $this->assessmentDetails($assessmentId);
|
|
if (! in_array($data['assessment']['status'], ['completed', 'graded'], true)) {
|
|
return redirect()->back()->with('error', 'The assessment is not ready for review.');
|
|
}
|
|
$this->persistEducationCommitteeNote($assessmentId);
|
|
$this->db->transStart();
|
|
$this->db->table('student_answers')->where('student_assessment_id', $assessmentId)->update([
|
|
'is_correct' => null, 'points_awarded' => null, 'updated_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
$this->assessmentModel->update($assessmentId, [
|
|
'status' => 'graded', 'score' => null, 'graded_at' => date('Y-m-d H:i:s'),
|
|
'graded_by' => (int) session()->get('user_id') ?: null,
|
|
]);
|
|
$this->db->transComplete();
|
|
return redirect()->to("administrator/assessments/results/{$assessmentId}")->with('success', 'Assessment review completed.');
|
|
}
|
|
|
|
public function results(int $assessmentId)
|
|
{
|
|
$data = $this->assessmentDetails($assessmentId);
|
|
$data['adminView'] = true;
|
|
return view('assessments/results', $data);
|
|
}
|
|
|
|
public function saveAdminProgress(int $assessmentId)
|
|
{
|
|
$assessment = $this->assessmentModel->find($assessmentId);
|
|
if (! $assessment) throw new PageNotFoundException('Assessment not found.');
|
|
if (! in_array($assessment['status'], ['not_started', 'in_progress'], true)) {
|
|
return $this->response->setStatusCode(409)->setJSON(['status' => false, 'message' => 'This assessment can no longer be changed.']);
|
|
}
|
|
$this->persistEducationCommitteeNote($assessmentId);
|
|
$this->persistAnswers($assessment, (array) $this->request->getPost('answers'));
|
|
return $this->response->setJSON([
|
|
'status' => true, 'message' => 'Saved', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
public function saveAdminNote(int $assessmentId)
|
|
{
|
|
if (! $this->assessmentModel->find($assessmentId)) {
|
|
throw new PageNotFoundException('Assessment not found.');
|
|
}
|
|
$this->persistEducationCommitteeNote($assessmentId);
|
|
return $this->response->setJSON([
|
|
'status' => true, 'message' => 'Note saved', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
public function completeAdminInterview(int $assessmentId)
|
|
{
|
|
$assessment = $this->assessmentModel->find($assessmentId);
|
|
if (! $assessment) throw new PageNotFoundException('Assessment not found.');
|
|
if (! in_array($assessment['status'], ['not_started', 'in_progress'], true)) {
|
|
return redirect()->to('administrator/assessments/students/' . $assessment['student_id'])
|
|
->with('error', 'This assessment has already been completed.');
|
|
}
|
|
$this->persistEducationCommitteeNote($assessmentId);
|
|
$this->persistAnswers($assessment, (array) $this->request->getPost('answers'));
|
|
$this->assessmentModel->update($assessmentId, ['status' => 'completed', 'submitted_at' => date('Y-m-d H:i:s')]);
|
|
return redirect()->to("administrator/assessments/review/{$assessmentId}")
|
|
->with('success', 'Assessment completed. Review the responses below.');
|
|
}
|
|
|
|
public function myAssessments()
|
|
{
|
|
$studentIds = $this->accessibleStudentIds();
|
|
$assignments = [];
|
|
if ($studentIds) {
|
|
$assignments = $this->db->table('student_assessments sa')
|
|
->select('sa.*, f.name AS form_name, f.school_year, s.firstname, s.lastname')
|
|
->join('assessment_forms f', 'f.id = sa.form_id')
|
|
->join('students s', 's.id = sa.student_id')
|
|
->whereIn('sa.student_id', $studentIds)->orderBy('sa.assigned_at', 'DESC')->get()->getResultArray();
|
|
}
|
|
return view('assessments/my_assessments', ['assignments' => $assignments]);
|
|
}
|
|
|
|
public function take(int $assessmentId)
|
|
{
|
|
$assessment = $this->requireAccessibleAssessment($assessmentId);
|
|
if ($assessment['status'] === 'graded') {
|
|
return redirect()->to("student/assessments/{$assessmentId}/results");
|
|
}
|
|
if ($assessment['status'] === 'completed') {
|
|
return redirect()->to('student/assessments')->with('success', 'This assessment has already been submitted and is awaiting review.');
|
|
}
|
|
$data = $this->formWithQuestions((int) $assessment['form_id']);
|
|
$answerRows = $this->db->table('student_answers')->where('student_assessment_id', $assessmentId)->get()->getResultArray();
|
|
$data['answers'] = array_column($answerRows, 'answer_value', 'question_id');
|
|
$data['assessment'] = $assessment;
|
|
$data['preview'] = false;
|
|
return view('assessments/take', $data);
|
|
}
|
|
|
|
public function saveProgress(int $assessmentId)
|
|
{
|
|
$assessment = $this->requireAccessibleAssessment($assessmentId);
|
|
if (! in_array($assessment['status'], ['not_started', 'in_progress'], true)) {
|
|
return $this->response->setStatusCode(409)->setJSON(['status' => false, 'message' => 'This assessment can no longer be changed.']);
|
|
}
|
|
$answers = (array) $this->request->getPost('answers');
|
|
$this->persistAnswers($assessment, $answers);
|
|
if ($this->request->isAJAX()) {
|
|
return $this->response->setJSON([
|
|
'status' => true, 'message' => 'Progress saved.',
|
|
'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
return redirect()->back()->with('success', 'Progress saved.');
|
|
}
|
|
|
|
public function submit(int $assessmentId)
|
|
{
|
|
$assessment = $this->requireAccessibleAssessment($assessmentId);
|
|
if (! in_array($assessment['status'], ['not_started', 'in_progress'], true)) {
|
|
return redirect()->to('student/assessments')->with('error', 'This assessment was already submitted.');
|
|
}
|
|
$this->persistAnswers($assessment, (array) $this->request->getPost('answers'));
|
|
$this->assessmentModel->update($assessmentId, ['status' => 'completed', 'submitted_at' => date('Y-m-d H:i:s')]);
|
|
return redirect()->to('student/assessments')->with('success', 'Assessment submitted. Your responses will appear after review.');
|
|
}
|
|
|
|
public function myResults(int $assessmentId)
|
|
{
|
|
$assessment = $this->requireAccessibleAssessment($assessmentId);
|
|
if ($assessment['status'] !== 'graded') {
|
|
return redirect()->to('student/assessments')->with('error', 'Results are not available yet.');
|
|
}
|
|
$data = $this->assessmentDetails($assessmentId);
|
|
$data['adminView'] = false;
|
|
return view('assessments/results', $data);
|
|
}
|
|
|
|
private function saveForm(?int $id)
|
|
{
|
|
$rules = [
|
|
'name' => 'required|string|max_length[150]', 'pool_id' => 'required|is_natural_no_zero',
|
|
'school_year' => 'required|regex_match[/^\d{4}-\d{4}$/]',
|
|
'status' => 'required|in_list[draft,published,archived]',
|
|
'education_committee_note' => 'permit_empty|string|max_length[10000]',
|
|
];
|
|
if (! $this->validate($rules)) {
|
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
|
}
|
|
$existingForm = $id !== null ? $this->requireForm($id) : null;
|
|
$submittedSchoolYear = trim((string) $this->request->getPost('school_year'));
|
|
if ($this->db->tableExists('school_years')
|
|
&& $this->db->table('school_years')->where('name', $submittedSchoolYear)->countAllResults() === 0) {
|
|
return redirect()->back()->withInput()->with('error', 'Select a valid school year.');
|
|
}
|
|
$poolId = $existingForm ? (int) $existingForm['pool_id'] : (int) $this->request->getPost('pool_id');
|
|
if (! $this->poolModel->find($poolId)) {
|
|
return redirect()->back()->withInput()->with('error', 'Question pool not found.');
|
|
}
|
|
$locked = $id !== null && $this->db->table('student_assessments')->where('form_id', $id)->countAllResults() > 0;
|
|
$selected = array_values(array_unique(array_map('intval', (array) $this->request->getPost('question_ids'))));
|
|
if ($this->request->getPost('use_all')) {
|
|
$selected = array_map('intval', array_column($this->questionModel->where('pool_id', $poolId)->orderBy('order_index', 'ASC')->findAll(), 'id'));
|
|
}
|
|
if (! $locked) {
|
|
$validRows = empty($selected) ? [] : $this->questionModel->where('pool_id', $poolId)->whereIn('id', $selected)->findAll();
|
|
$validIds = array_map('intval', array_column($validRows, 'id'));
|
|
$selected = array_values(array_filter($selected, static fn ($qid) => in_array($qid, $validIds, true)));
|
|
usort($selected, function ($a, $b) {
|
|
return ((int) $this->request->getPost('order_' . $a)) <=> ((int) $this->request->getPost('order_' . $b));
|
|
});
|
|
if (empty($selected)) {
|
|
return redirect()->back()->withInput()->with('error', 'Select at least one question.');
|
|
}
|
|
}
|
|
$payload = [
|
|
'name' => trim((string) $this->request->getPost('name')),
|
|
'school_year' => $locked ? (string) $existingForm['school_year'] : $submittedSchoolYear,
|
|
'status' => $this->request->getPost('status'),
|
|
'education_committee_note' => $this->nullableString($this->request->getPost('education_committee_note')),
|
|
];
|
|
if ($id === null) $payload['pool_id'] = $poolId;
|
|
$this->db->transStart();
|
|
if ($id === null) {
|
|
$payload['created_by'] = (int) session()->get('user_id') ?: null;
|
|
$id = (int) $this->formModel->insert($payload, true);
|
|
} else {
|
|
$this->formModel->update($id, $payload);
|
|
}
|
|
if (! $locked) {
|
|
$this->db->table('assessment_form_questions')->where('form_id', $id)->delete();
|
|
foreach ($selected as $index => $questionId) {
|
|
$this->db->table('assessment_form_questions')->insert(['form_id' => $id, 'question_id' => $questionId, 'order_index' => $index + 1]);
|
|
}
|
|
}
|
|
$this->db->transComplete();
|
|
return redirect()->to('administrator/assessments/forms')->with('success', 'Assessment form saved.');
|
|
}
|
|
|
|
private function validateQuestion(): bool
|
|
{
|
|
$valid = $this->validate([
|
|
'type' => 'required|in_list[multiple_choice,short_answer,true_false,essay]',
|
|
'text' => 'required|string|max_length[10000]', 'options_text' => 'permit_empty|string|max_length[10000]',
|
|
'correct_answer' => 'permit_empty|string|max_length[10000]',
|
|
]);
|
|
if ($valid && $this->request->getPost('type') === 'multiple_choice') {
|
|
$choices = preg_split('/\r\n|\r|\n/', trim((string) $this->request->getPost('options_text'))) ?: [];
|
|
$choices = array_filter(array_map('trim', $choices), static fn ($choice) => $choice !== '');
|
|
if (count($choices) < 2) {
|
|
$this->validator->setError('options_text', 'Multiple-choice questions need at least two choices.');
|
|
return false;
|
|
}
|
|
}
|
|
return $valid;
|
|
}
|
|
|
|
private function questionPayload(int $poolId, int $order): array
|
|
{
|
|
$type = (string) $this->request->getPost('type');
|
|
$options = null;
|
|
if ($type === 'true_false') {
|
|
$options = json_encode(['True', 'False']);
|
|
} elseif ($type === 'multiple_choice') {
|
|
$values = preg_split('/\r\n|\r|\n/', trim((string) $this->request->getPost('options_text')));
|
|
$values = array_values(array_filter(array_map('trim', $values ?: []), static fn ($v) => $v !== ''));
|
|
$options = $values ? json_encode($values, JSON_UNESCAPED_UNICODE) : null;
|
|
}
|
|
return [
|
|
'pool_id' => $poolId, 'type' => $type, 'text' => trim((string) $this->request->getPost('text')),
|
|
'options' => $options, 'correct_answer' => $this->nullableString($this->request->getPost('correct_answer')),
|
|
'points' => 0, 'order_index' => $order,
|
|
];
|
|
}
|
|
|
|
private function formWithQuestions(int $id): array
|
|
{
|
|
$form = $this->requireForm($id);
|
|
$questions = $this->db->table('assessment_form_questions fq')
|
|
->select('q.*, fq.order_index AS form_order')->join('assessment_questions q', 'q.id = fq.question_id')
|
|
->where('fq.form_id', $id)->orderBy('fq.order_index', 'ASC')->get()->getResultArray();
|
|
return ['form' => $form, 'questions' => $questions, 'answers' => []];
|
|
}
|
|
|
|
private function assessmentDetails(int $id): array
|
|
{
|
|
$assessment = $this->db->table('student_assessments sa')
|
|
->select('sa.*, f.name AS form_name, f.id AS assessment_form_id, f.school_year, f.education_committee_note AS form_education_committee_note, s.firstname, s.lastname, s.school_id')
|
|
->join('assessment_forms f', 'f.id = sa.form_id')->join('students s', 's.id = sa.student_id')
|
|
->where('sa.id', $id)->get()->getRowArray();
|
|
if (! $assessment) {
|
|
throw new PageNotFoundException('Assessment not found.');
|
|
}
|
|
$questions = $this->db->table('assessment_form_questions fq')
|
|
->select('q.*, fq.order_index AS form_order, a.answer_value')
|
|
->join('assessment_questions q', 'q.id = fq.question_id')
|
|
->join('student_answers a', 'a.question_id = q.id AND a.student_assessment_id = ' . (int) $id, 'left')
|
|
->where('fq.form_id', $assessment['form_id'])->orderBy('fq.order_index', 'ASC')->get()->getResultArray();
|
|
return ['assessment' => $assessment, 'questions' => $questions];
|
|
}
|
|
|
|
private function requirePool(int $id): array
|
|
{
|
|
$row = $this->poolModel->find($id);
|
|
if (! $row) throw new PageNotFoundException('Question pool not found.');
|
|
return $row;
|
|
}
|
|
|
|
private function primaryPool(): ?array
|
|
{
|
|
$pool = $this->poolModel->where('name', 'New Student Assessment')->first();
|
|
if ($pool) return $pool;
|
|
|
|
return $this->poolModel->orderBy('id', 'ASC')->first() ?: null;
|
|
}
|
|
|
|
private function schoolYearOptions(): array
|
|
{
|
|
if (! $this->db->tableExists('school_years')) return [];
|
|
return $this->db->table('school_years')->select('name, status')->orderBy('name', 'DESC')->get()->getResultArray();
|
|
}
|
|
|
|
private function requireQuestion(int $id): array
|
|
{
|
|
$row = $this->questionModel->find($id);
|
|
if (! $row) throw new PageNotFoundException('Question not found.');
|
|
return $row;
|
|
}
|
|
|
|
private function requireForm(int $id): array
|
|
{
|
|
$row = $this->formModel->find($id);
|
|
if (! $row) throw new PageNotFoundException('Assessment form not found.');
|
|
return $row;
|
|
}
|
|
|
|
private function normalizeQuestionOrder(int $poolId): void
|
|
{
|
|
$rows = $this->questionModel->where('pool_id', $poolId)->orderBy('order_index', 'ASC')->orderBy('id', 'ASC')->findAll();
|
|
foreach ($rows as $index => $row) $this->questionModel->update($row['id'], ['order_index' => $index + 1]);
|
|
}
|
|
|
|
private function nullableString($value): ?string
|
|
{
|
|
$value = trim((string) $value);
|
|
return $value === '' ? null : $value;
|
|
}
|
|
|
|
private function assessmentActionsForStudents(array $studentIds, string $schoolYear): array
|
|
{
|
|
$studentIds = array_values(array_unique(array_filter(array_map('intval', $studentIds))));
|
|
if ($studentIds === []) return [];
|
|
$rows = $this->db->table('student_assessments sa')
|
|
->select('sa.*, f.name AS form_name')->join('assessment_forms f', 'f.id = sa.form_id')
|
|
->where('f.school_year', $schoolYear)
|
|
->whereIn('sa.student_id', $studentIds)->orderBy('sa.assigned_at', 'DESC')->orderBy('sa.id', 'DESC')
|
|
->get()->getResultArray();
|
|
$map = [];
|
|
foreach ($rows as $row) {
|
|
$studentId = (int) $row['student_id'];
|
|
$priority = match ((string) $row['status']) {
|
|
'completed' => 3, 'not_started', 'in_progress' => 2, 'graded' => 1, default => 0,
|
|
};
|
|
$currentStatus = $map[$studentId]['status'] ?? null;
|
|
$currentPriority = match ($currentStatus) {
|
|
'completed' => 3, 'not_started', 'in_progress' => 2, 'graded' => 1, default => -1,
|
|
};
|
|
if ($priority > $currentPriority) $map[$studentId] = $row;
|
|
}
|
|
return $map;
|
|
}
|
|
|
|
private function activeAssessmentForStudent(int $studentId, string $schoolYear): ?array
|
|
{
|
|
return $this->db->table('student_assessments sa')
|
|
->select('sa.*')->join('assessment_forms f', 'f.id = sa.form_id')
|
|
->where('sa.student_id', $studentId)->where('f.school_year', $schoolYear)
|
|
->whereIn('sa.status', ['not_started', 'in_progress'])
|
|
->orderBy('sa.assigned_at', 'DESC')->orderBy('sa.id', 'DESC')->get()->getRowArray() ?: null;
|
|
}
|
|
|
|
private function persistAnswers(array $assessment, array $answers): void
|
|
{
|
|
$questionIds = array_map('intval', array_column(
|
|
$this->db->table('assessment_form_questions')->select('question_id')->where('form_id', $assessment['form_id'])->get()->getResultArray(),
|
|
'question_id'
|
|
));
|
|
$now = date('Y-m-d H:i:s');
|
|
$this->db->transStart();
|
|
foreach ($questionIds as $questionId) {
|
|
if (! array_key_exists($questionId, $answers)) continue;
|
|
$value = is_array($answers[$questionId]) ? implode(', ', $answers[$questionId]) : trim((string) $answers[$questionId]);
|
|
$existing = $this->db->table('student_answers')->where([
|
|
'student_assessment_id' => $assessment['id'], 'question_id' => $questionId,
|
|
])->get()->getRowArray();
|
|
$payload = ['answer_value' => $value, 'updated_at' => $now];
|
|
if ($existing) {
|
|
$this->db->table('student_answers')->where('id', $existing['id'])->update($payload);
|
|
} else {
|
|
$payload += ['student_assessment_id' => $assessment['id'], 'question_id' => $questionId, 'created_at' => $now];
|
|
$this->db->table('student_answers')->insert($payload);
|
|
}
|
|
}
|
|
if ($assessment['status'] === 'not_started') {
|
|
$this->assessmentModel->update($assessment['id'], ['status' => 'in_progress', 'started_at' => $now]);
|
|
}
|
|
$this->db->transComplete();
|
|
}
|
|
|
|
private function persistEducationCommitteeNote(int $assessmentId): void
|
|
{
|
|
$note = trim((string) $this->request->getPost('education_committee_note'));
|
|
if (mb_strlen($note) > 10000) $note = mb_substr($note, 0, 10000);
|
|
$this->assessmentModel->update($assessmentId, [
|
|
'education_committee_note' => $note === '' ? null : $note,
|
|
]);
|
|
}
|
|
|
|
private function requireAccessibleAssessment(int $id): array
|
|
{
|
|
$assessment = $this->assessmentModel->find($id);
|
|
if (! $assessment || ! in_array((int) $assessment['student_id'], $this->accessibleStudentIds(), true)) {
|
|
throw new PageNotFoundException('Assessment not found.');
|
|
}
|
|
return $assessment;
|
|
}
|
|
|
|
private function accessibleStudentIds(): array
|
|
{
|
|
$userId = (int) session()->get('user_id');
|
|
if ($userId <= 0) return [];
|
|
$ids = [];
|
|
$user = $this->db->table('users')->select('school_id')->where('id', $userId)->get()->getRowArray();
|
|
$schoolId = trim((string) ($user['school_id'] ?? ''));
|
|
if ($schoolId !== '') {
|
|
$row = $this->db->table('students')->select('id')->where('school_id', $schoolId)->get()->getRowArray();
|
|
if ($row) $ids[] = (int) $row['id'];
|
|
}
|
|
$parentRows = $this->db->table('students')->select('id')->where('parent_id', $userId)->get()->getResultArray();
|
|
$ids = array_merge($ids, array_map('intval', array_column($parentRows, 'id')));
|
|
if ($this->db->tableExists('family_guardians') && $this->db->tableExists('family_students')) {
|
|
$familyRows = $this->db->table('family_guardians fg')->select('fs.student_id')
|
|
->join('family_students fs', 'fs.family_id = fg.family_id')->where('fg.user_id', $userId)->get()->getResultArray();
|
|
$ids = array_merge($ids, array_map('intval', array_column($familyRows, 'student_id')));
|
|
}
|
|
return array_values(array_unique(array_filter($ids)));
|
|
}
|
|
}
|