Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39ab0d113e | |||
| ce56c96cdd | |||
| b4a06903e2 | |||
| 0654668530 |
@@ -538,6 +538,9 @@ $routes->get('grading/(:segment)/(:num)/(:num)', 'View\GradingController::show/$
|
||||
$routes->post('grading/update', 'View\GradingController::update');
|
||||
$routes->get('grading', 'View\GradingController::grading');
|
||||
$routes->post('grading/toggle-score-lock', 'View\GradingController::toggleScoreLock', ['filter' => 'auth:read']);
|
||||
$routes->get('grading/toggle-score-lock', static function () {
|
||||
return 'GET route hit. Something is calling this route incorrectly.';
|
||||
});
|
||||
$routes->post('grading/lock-all-scores', 'View\GradingController::lockAllScores', ['filter' => 'auth:read']);
|
||||
$routes->post('grading/release-scores', 'View\GradingController::toggleParentScoresRelease', ['filter' => 'auth:read']);
|
||||
$routes->post('grading/refresh-semester-scores', 'View\GradingController::refreshSemesterScores', ['filter' => 'auth:read']);
|
||||
|
||||
@@ -10,8 +10,13 @@ class ProofreadController extends ResourceController
|
||||
{
|
||||
// Basic per-IP throttling: 10 requests per minute
|
||||
$throttler = service('throttler');
|
||||
$key = 'proofread-' . $this->request->getIPAddress();
|
||||
if (!$throttler->check($key, 10, MINUTE)) {
|
||||
|
||||
// Cache/throttler keys cannot contain reserved characters.
|
||||
// Raw IPv6 addresses contain ":", so hash the IP first.
|
||||
$ip = $this->request->getIPAddress();
|
||||
$key = 'proofread-' . hash('sha256', $ip);
|
||||
|
||||
if (! $throttler->check($key, 10, MINUTE)) {
|
||||
return $this->respond([
|
||||
'ok' => false,
|
||||
'error' => 'Too many requests. Try again in a minute.',
|
||||
@@ -20,8 +25,9 @@ class ProofreadController extends ResourceController
|
||||
}
|
||||
|
||||
// Accept form-urlencoded payload to play nicely with CSRF protection
|
||||
$validation = service('validation');
|
||||
$payload = sanitize_request_value($this->request->getPost(['text']));
|
||||
|
||||
$validation = service('validation');
|
||||
$validation->setRules([
|
||||
'text' => 'required|min_length[1]|max_length[20000]',
|
||||
]);
|
||||
@@ -36,20 +42,34 @@ class ProofreadController extends ResourceController
|
||||
|
||||
$text = (string) $payload['text'];
|
||||
|
||||
$client = \Config\Services::curlrequest(['timeout' => 10]);
|
||||
$client = \Config\Services::curlrequest([
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
try {
|
||||
$resp = $client->post('https://api.languagetool.org/v2/check', [
|
||||
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/x-www-form-urlencoded',
|
||||
],
|
||||
'form_params' => [
|
||||
'text' => $text,
|
||||
'language' => 'en-US',
|
||||
],
|
||||
]);
|
||||
|
||||
$result = json_decode($resp->getBody(), true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return $this->respond([
|
||||
'ok' => false,
|
||||
'error' => 'Invalid response from proofread service.',
|
||||
'csrfHash' => csrf_hash(),
|
||||
], 502);
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
'ok' => true,
|
||||
'result' => json_decode($resp->getBody(), true),
|
||||
'result' => $result,
|
||||
'csrfHash' => csrf_hash(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
@@ -60,4 +80,4 @@ class ProofreadController extends ResourceController
|
||||
], 502);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -346,10 +346,10 @@ class CertificateController extends BaseController
|
||||
'student_id' => $sid,
|
||||
'student_name' => $student['firstname'] . ' ' . $student['lastname'],
|
||||
'grade' => $this->formatGrade($student['grade'] ?? ''),
|
||||
'cert_date' => $certDateDb,
|
||||
//'cert_date' => $certDateDb,
|
||||
'school_year' => $schoolYear,
|
||||
'class_section_id' => $classSectionId ?: null,
|
||||
'issued_by' => $issuedBy,
|
||||
//'issued_by' => $issuedBy,
|
||||
'issued_at' => $issuedAt,
|
||||
]);
|
||||
$student['cert_number'] = $certNumber;
|
||||
|
||||
@@ -1584,13 +1584,18 @@ $scoresEndY = $pdf->GetY();
|
||||
$firstSemesterScore = $secondSemesterScore;
|
||||
}
|
||||
|
||||
$rankingScore = $secondSemesterScore;
|
||||
if ($normSemester === 'spring' && is_numeric($firstSemesterScore) && is_numeric($secondSemesterScore)) {
|
||||
$rankingScore = round(((float)$firstSemesterScore + (float)$secondSemesterScore) / 2, 1);
|
||||
}
|
||||
|
||||
$termRanking = $this->calculateTermRanking(
|
||||
$studentId,
|
||||
$sectionCode,
|
||||
$sectionId,
|
||||
$refYear,
|
||||
$refSemester !== '' ? $refSemester : (string)($score['semester'] ?? ''),
|
||||
$secondSemesterScore
|
||||
$rankingScore
|
||||
);
|
||||
|
||||
// Total semester days from attendance records (max total_attendance within same term)
|
||||
@@ -1748,8 +1753,11 @@ $scoresEndY = $pdf->GetY();
|
||||
return null;
|
||||
}
|
||||
|
||||
$semesterForRank = trim((string)$semester);
|
||||
$rankByFinalScore = $this->normalizeSemester($semesterForRank) === 'spring';
|
||||
|
||||
$builder = $this->db->table('semester_scores ss')
|
||||
->select('ss.student_id, ss.semester_score, ss.updated_at, ss.id, s.firstname, s.lastname')
|
||||
->select('ss.student_id, ss.semester, ss.semester_score, ss.updated_at, ss.id, s.firstname, s.lastname')
|
||||
->join('students s', 's.id = ss.student_id', 'inner')
|
||||
->where('s.is_active', 1)
|
||||
->where('ss.school_year', $schoolYear)
|
||||
@@ -1757,8 +1765,8 @@ $scoresEndY = $pdf->GetY();
|
||||
->orderBy('ss.updated_at', 'DESC')
|
||||
->orderBy('ss.id', 'DESC');
|
||||
|
||||
if (trim((string)$semester) !== '') {
|
||||
$this->applySemesterFilter($builder, (string)$semester, 'ss.semester');
|
||||
if ($semesterForRank !== '') {
|
||||
$this->applySemesterFilter($builder, $semesterForRank, 'ss.semester');
|
||||
}
|
||||
|
||||
$rows = $builder->get()->getResultArray();
|
||||
@@ -1767,6 +1775,7 @@ $scoresEndY = $pdf->GetY();
|
||||
}
|
||||
|
||||
$scoresByStudent = [];
|
||||
$studentIds = [];
|
||||
foreach ($rows as $row) {
|
||||
$sid = (int)($row['student_id'] ?? 0);
|
||||
if ($sid <= 0 || isset($scoresByStudent[$sid])) {
|
||||
@@ -1784,12 +1793,59 @@ $scoresEndY = $pdf->GetY();
|
||||
'firstname' => trim((string)($row['firstname'] ?? '')),
|
||||
'lastname' => trim((string)($row['lastname'] ?? '')),
|
||||
];
|
||||
$studentIds[] = $sid;
|
||||
}
|
||||
|
||||
if (empty($scoresByStudent) || !isset($scoresByStudent[$studentId])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($rankByFinalScore) {
|
||||
$firstRowsBuilder = $this->db->table('semester_scores ss')
|
||||
->select('ss.student_id, ss.semester_score, ss.updated_at, ss.id')
|
||||
->where('ss.school_year', $schoolYear)
|
||||
->whereIn('ss.class_section_id', $sectionIds)
|
||||
->whereIn('ss.student_id', $studentIds)
|
||||
->orderBy('ss.updated_at', 'DESC')
|
||||
->orderBy('ss.id', 'DESC');
|
||||
|
||||
if ($semesterForRank !== '') {
|
||||
$this->applySemesterExclusion($firstRowsBuilder, $semesterForRank, 'ss.semester');
|
||||
}
|
||||
|
||||
$firstRows = $firstRowsBuilder->get()->getResultArray();
|
||||
$firstScoresByStudent = [];
|
||||
foreach ($firstRows as $row) {
|
||||
$sid = (int)($row['student_id'] ?? 0);
|
||||
if ($sid <= 0 || isset($firstScoresByStudent[$sid])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$scoreVal = $row['semester_score'] ?? null;
|
||||
if (!is_numeric($scoreVal)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$firstScoresByStudent[$sid] = (float)$scoreVal;
|
||||
}
|
||||
|
||||
foreach ($scoresByStudent as $sid => &$rankRow) {
|
||||
if (!isset($firstScoresByStudent[$sid])) {
|
||||
unset($scoresByStudent[$sid]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$rankRow['score'] = round(((float)$firstScoresByStudent[$sid] + (float)$rankRow['score']) / 2, 4);
|
||||
}
|
||||
unset($rankRow);
|
||||
|
||||
if (empty($scoresByStudent) || !isset($scoresByStudent[$studentId])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$scoresByStudent[$studentId]['score'] = round((float)$studentScore, 4);
|
||||
}
|
||||
|
||||
$rankable = array_values($scoresByStudent);
|
||||
usort($rankable, static function (array $a, array $b): int {
|
||||
$scoreCmp = $b['score'] <=> $a['score'];
|
||||
|
||||
@@ -231,18 +231,36 @@ class ScoreCommentController extends BaseController
|
||||
$rawCommentsBuilder->groupEnd();
|
||||
}
|
||||
|
||||
if (!$isAllSections && $classSectionId > 0) {
|
||||
// Prefer the exact class-section row over legacy NULL class-section rows.
|
||||
// Without this, duplicate rows can overwrite each other unpredictably.
|
||||
$rawCommentsBuilder
|
||||
->orderBy("CASE WHEN class_section_id = {$classSectionId} THEN 0 ELSE 1 END", '', false)
|
||||
->orderBy('id', 'DESC');
|
||||
} else {
|
||||
$rawCommentsBuilder->orderBy('id', 'DESC');
|
||||
}
|
||||
|
||||
$rawComments = $rawCommentsBuilder->findAll();
|
||||
|
||||
$existingAttendance = [];
|
||||
foreach ($rawComments as $c) {
|
||||
$sid = (int)$c['student_id'];
|
||||
$typ = (string)$c['score_type'];
|
||||
$sid = (int) $c['student_id'];
|
||||
$typ = strtolower(trim((string) $c['score_type']));
|
||||
|
||||
// The query is ordered with the best row first. Do not let legacy/older
|
||||
// duplicate rows overwrite the selected comment/review.
|
||||
if (isset($comments[$sid][$typ])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$comments[$sid][$typ] = [
|
||||
'comment' => $c['comment'] ?? null,
|
||||
'comment_review' => $c['comment_review'] ?? null,
|
||||
'commented_by' => $c['commented_by'] ?? null,
|
||||
'reviewed_by' => $c['reviewed_by'] ?? null,
|
||||
];
|
||||
|
||||
if ($typ === 'attendance') {
|
||||
$existingAttendance[$sid] = $c;
|
||||
}
|
||||
@@ -435,6 +453,16 @@ class ScoreCommentController extends BaseController
|
||||
$existingQuery->groupEnd();
|
||||
}
|
||||
|
||||
if (is_int($classSectionId) && $classSectionId > 0) {
|
||||
// Prefer updating the real class-section row. If only a legacy NULL row
|
||||
// exists, it will be picked next and migrated to the class section below.
|
||||
$existingQuery
|
||||
->orderBy("CASE WHEN class_section_id = {$classSectionId} THEN 0 ELSE 1 END", '', false)
|
||||
->orderBy('id', 'DESC');
|
||||
} else {
|
||||
$existingQuery->orderBy('id', 'DESC');
|
||||
}
|
||||
|
||||
$existing = $existingQuery->first();
|
||||
|
||||
$data = [
|
||||
|
||||
@@ -64,9 +64,9 @@
|
||||
<th>Certificate #</th>
|
||||
<th>Student</th>
|
||||
<th>Grade</th>
|
||||
<th>Cert Date</th>
|
||||
<!--th>Cert Date</th-->
|
||||
<th>School Year</th>
|
||||
<th>Issued By</th>
|
||||
<!--th>Issued By</th-->
|
||||
<th>Issued At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -51,19 +51,7 @@
|
||||
<div class="col-6">
|
||||
<div class="field-label">School Year</div>
|
||||
<div class="field-value"><?= esc($record['school_year'] ?? '—') ?></div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="field-label">Certificate Date</div>
|
||||
<div class="field-value">
|
||||
<?= $record['cert_date'] ? esc(date('F j, Y', strtotime($record['cert_date']))) : '—' ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="field-label">Issued By</div>
|
||||
<div class="field-value">
|
||||
<?= esc(trim(($record['admin_firstname'] ?? '') . ' ' . ($record['admin_lastname'] ?? '')) ?: '—') ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="field-label">Issued On</div>
|
||||
<div class="field-value">
|
||||
|
||||
@@ -32,7 +32,7 @@ $lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex justify-content-between mt-4 flex-wrap gap-2">
|
||||
<button type="submit" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<button type="submit" form="commentsForm" class="btn btn-success" <?= $lockAttr ?>>
|
||||
<i class="bi bi-save"></i> Save All Changes
|
||||
</button>
|
||||
<a href="<?= base_url('/grading') ?>" class="btn btn-secondary">
|
||||
@@ -177,7 +177,10 @@ $lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
</td>
|
||||
<?php elseif ($semester === 'Spring'): ?>
|
||||
<td>
|
||||
<?php $finalCommentId = 'final-comment-' . $studentId; ?>
|
||||
<?php $finalReviewId = 'final-review-' . $studentId; ?>
|
||||
<textarea
|
||||
id="<?= esc($finalCommentId) ?>"
|
||||
name="comments[<?= $studentId ?>][final]"
|
||||
rows="2"
|
||||
class="form-control"
|
||||
@@ -186,7 +189,6 @@ $lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
maxlength="350"
|
||||
placeholder="Enter Final comment" <?= $lockAttr ?>><?= esc($comments[$studentId]['final']['comment'] ?? '') ?></textarea>
|
||||
</td>
|
||||
<?php $finalReviewId = 'final-review-' . $studentId; ?>
|
||||
<td>
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<textarea
|
||||
@@ -196,6 +198,13 @@ $lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
class="form-control review-field"
|
||||
placeholder="Enter Final review" <?= $lockAttr ?>><?= esc($comments[$studentId]['final']['comment_review'] ?? '') ?></textarea>
|
||||
<div class="d-flex flex-column gap-1 align-items-stretch">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-sm copy-final-btn"
|
||||
data-copy-source="#<?= esc($finalCommentId) ?>"
|
||||
data-copy-target="#<?= esc($finalReviewId) ?>" <?= $lockAttr ?>>
|
||||
Paste
|
||||
</button>
|
||||
<div class="small text-muted proofread-status" data-status-for="<?= esc($finalReviewId) ?>"></div>
|
||||
<ul class="proofread-list small mb-0" data-list-for="<?= esc($finalReviewId) ?>"></ul>
|
||||
</div>
|
||||
@@ -474,18 +483,9 @@ $lockAttr = $scoresLocked ? 'disabled' : '';
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll('.copy-ptap-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const source = document.querySelector(this.dataset.copySource || '');
|
||||
const target = document.querySelector(this.dataset.copyTarget || '');
|
||||
if (!source || !target) return;
|
||||
target.value = source.value;
|
||||
target.style.height = 'auto';
|
||||
target.style.height = (target.scrollHeight) + 'px';
|
||||
target.focus();
|
||||
});
|
||||
});
|
||||
wireCopyButtons('.copy-ptap-btn');
|
||||
wireCopyButtons('.copy-midterm-btn');
|
||||
wireCopyButtons('.copy-final-btn');
|
||||
});
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
Reference in New Issue
Block a user