pushy

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-28 11:29:07 +08:00
parent 463a8d3978
commit 89acdfc239
67 changed files with 2736 additions and 306 deletions
+74 -24
View File
@@ -875,7 +875,7 @@ exports.getQuiz = async (req, res) => {
exports.createQuiz = async (req, res) => {
try {
const { courseId, unitId } = req.params;
const { title, is_required, passing_score, max_questions, createdBy } = req.body;
const { title, is_required, passing_score, max_questions, shuffle_questions, createdBy } = req.body;
const unit = await Unit.findOne({ where: { unit_id: unitId, course_id: courseId, ...notDeleted } });
if (!unit) return R.error(res, "Unit not found.", 404);
@@ -889,6 +889,7 @@ exports.createQuiz = async (req, res) => {
is_required: is_required ?? false,
passing_score: passing_score ?? 70,
max_questions: max_questions ?? null,
shuffle_questions: shuffle_questions ?? false,
createdBy: createdBy ?? null,
});
@@ -903,15 +904,16 @@ exports.createQuiz = async (req, res) => {
exports.updateQuiz = async (req, res) => {
try {
const { courseId, unitId, quizId } = req.params;
const { title, is_required, passing_score, max_questions, updatedBy } = req.body;
const { title, is_required, passing_score, max_questions, shuffle_questions, updatedBy } = req.body;
const quiz = await UnitQuiz.findOne({ where: { quiz_id: quizId, unit_id: unitId, ...notDeleted } });
if (!quiz) return R.error(res, "Quiz not found.", 404);
if (title !== undefined) quiz.title = title;
if (is_required !== undefined) quiz.is_required = is_required;
if (passing_score !== undefined) quiz.passing_score = passing_score;
if (max_questions !== undefined) quiz.max_questions = max_questions;
if (title !== undefined) quiz.title = title;
if (is_required !== undefined) quiz.is_required = is_required;
if (passing_score !== undefined) quiz.passing_score = passing_score;
if (max_questions !== undefined) quiz.max_questions = max_questions;
if (shuffle_questions !== undefined) quiz.shuffle_questions = shuffle_questions;
quiz.updatedBy = updatedBy ?? null;
@@ -1244,7 +1246,7 @@ exports.getAssessment = async (req, res) => {
exports.createAssessment = async (req, res) => {
try {
const { courseId } = req.params;
const { title, is_required, passing_score, time_limit_minutes, max_questions, max_attempts, cooldown_hours, createdBy } = req.body;
const { title, is_required, passing_score, time_limit_minutes, max_questions, max_attempts, cooldown_hours, shuffle_questions, createdBy } = req.body;
const course = await Course.findOne({ where: { course_id: courseId, ...notDeleted } });
if (!course) return R.error(res, "Course not found.", 404);
@@ -1261,6 +1263,7 @@ exports.createAssessment = async (req, res) => {
max_questions: max_questions ?? null,
max_attempts: max_attempts ?? 3,
cooldown_hours: cooldown_hours ?? 24,
shuffle_questions: shuffle_questions ?? false,
createdBy: createdBy ?? null,
});
@@ -1275,20 +1278,21 @@ exports.createAssessment = async (req, res) => {
exports.updateAssessment = async (req, res) => {
try {
const { courseId, assessmentId } = req.params;
const { title, is_required, passing_score, time_limit_minutes, max_questions, max_attempts, cooldown_hours, updatedBy } = req.body;
const { title, is_required, passing_score, time_limit_minutes, max_questions, max_attempts, cooldown_hours, shuffle_questions, updatedBy } = req.body;
const assessment = await CourseAssessment.findOne({
where: { assessment_id: assessmentId, course_id: courseId, ...notDeleted },
});
if (!assessment) return R.error(res, "Assessment not found.", 404);
if (title !== undefined) assessment.title = title;
if (is_required !== undefined) assessment.is_required = is_required;
if (passing_score !== undefined) assessment.passing_score = passing_score;
if (title !== undefined) assessment.title = title;
if (is_required !== undefined) assessment.is_required = is_required;
if (passing_score !== undefined) assessment.passing_score = passing_score;
if (time_limit_minutes !== undefined) assessment.time_limit_minutes = time_limit_minutes;
if (max_questions !== undefined) assessment.max_questions = max_questions;
if (max_attempts !== undefined) assessment.max_attempts = max_attempts;
if (cooldown_hours !== undefined) assessment.cooldown_hours = cooldown_hours;
if (max_questions !== undefined) assessment.max_questions = max_questions;
if (max_attempts !== undefined) assessment.max_attempts = max_attempts;
if (cooldown_hours !== undefined) assessment.cooldown_hours = cooldown_hours;
if (shuffle_questions !== undefined) assessment.shuffle_questions = shuffle_questions;
assessment.updatedBy = updatedBy ?? null;
@@ -1423,6 +1427,23 @@ exports.getCoursesFlat = async (req, res) => {
}
};
exports.getCoursesBySubscription = async (req, res) => {
try {
const { slug } = req.query;
if (!slug) return R.error(res, 'slug query param is required.', 400);
const data = await Course.findAll({
where: { ...notDeleted, subscription: slug },
attributes: ['course_id', 'title', 'description', 'subscription'],
order: [['title', 'ASC']],
});
return R.success(res, 'Courses retrieved.', data);
} catch (err) {
console.error('[COURSE][BY SUBSCRIPTION]', err);
return R.error(res, 'Could not retrieve courses.', 500);
}
};
exports.getUnitsFlat = async (req, res) => {
try {
const rows = await Unit.findAll({
@@ -1565,11 +1586,12 @@ exports.syncInstructors = async (req, res) => {
// ─── COMPLETIONS HELPERS ──────────────────────────────────────────────────────
function extractUserInfo(user) {
if (!user) return { full_name: null, email: null, avatar_url: null };
if (!user) return { full_name: null, email: null, avatar_url: null, deleted: false };
return {
full_name: user.personal_info?.name?.full_name ?? null,
email: user.email ?? null,
avatar_url: user.personal_info?.avatar?.url ?? null,
deleted: !!user.deletedAt,
};
}
@@ -1578,12 +1600,13 @@ function groupByUser(attempts) {
for (const a of attempts) {
const uid = String(a.user_id);
if (!map.has(uid)) {
const { full_name, email, avatar_url } = extractUserInfo(a.user);
const { full_name, email, avatar_url, deleted } = extractUserInfo(a.user);
map.set(uid, {
user_id: a.user_id,
full_name,
email,
avatar_url,
deleted,
attempt_count: 0,
best_score: 0,
passed: false,
@@ -1602,10 +1625,36 @@ function groupByUser(attempts) {
}
function buildSummary(attempts) {
const takers = new Set(attempts.map((a) => String(a.user_id))).size;
const passed = attempts.filter((a) => a.passed).length;
const failed = attempts.length - passed;
const avg = attempts.length ? Math.round(attempts.reduce((s, a) => s + a.score, 0) / attempts.length) : 0;
const takers = new Set(attempts.map((a) => String(a.user_id))).size;
const passed = attempts.filter((a) => a.passed).length;
const failed = attempts.length - passed;
/*
* ┌─────────────────────────────────────────────────────────────────┐
* │ AVG SCORE formula │
* │ │
* │ avg = Σ( max(score) per user ) / unique_user_count │
* │ │
* │ Each student contributes exactly once — their personal best. │
* │ A student who retries 3× before passing counts once at peak │
* │ performance, not three times. Mirrors the "Best Score" column │
* │ shown per student in the completions table. │
* │ │
* │ NOTE: CockroachDB returns INT8 columns as strings via the pg │
* │ driver. Coerce with Number() before any arithmetic. │
* └─────────────────────────────────────────────────────────────────┘
*/
const bestByUser = new Map();
for (const a of attempts) {
const uid = String(a.user_id);
const s = Number(a.score);
if (!bestByUser.has(uid) || s > bestByUser.get(uid)) bestByUser.set(uid, s);
}
const bestScores = [...bestByUser.values()];
const avg = bestScores.length
? Math.round(bestScores.reduce((sum, s) => sum + s, 0) / bestScores.length)
: 0;
return {
total_takers: takers,
passed_count: passed,
@@ -1625,7 +1674,7 @@ exports.getQuizCompletions = async (req, res) => {
const attempts = await QuizAttempt.findAll({
where: { quiz_id: quizId },
attributes: ["attempt_id", "user_id", "attempt_number", "score", "earned_points", "total_points", "passed", "createdAt"],
include: [{ model: mdl_Users, as: "user", attributes: ["user_id", "email", "personal_info"] }],
include: [{ model: mdl_Users, as: "user", attributes: ["user_id", "email", "personal_info", "deletedAt"], paranoid: false }],
order: [["createdAt", "DESC"]],
});
@@ -1649,7 +1698,7 @@ exports.getAssessmentCompletions = async (req, res) => {
const attempts = await QuizAttempt.findAll({
where: { assessment_id: assessmentId },
attributes: ["attempt_id", "user_id", "attempt_number", "score", "earned_points", "total_points", "passed", "createdAt"],
include: [{ model: mdl_Users, as: "user", attributes: ["user_id", "email", "personal_info"] }],
include: [{ model: mdl_Users, as: "user", attributes: ["user_id", "email", "personal_info", "deletedAt"], paranoid: false }],
order: [["createdAt", "DESC"]],
});
@@ -1673,13 +1722,13 @@ exports.getAssessmentSessions = async (req, res) => {
const sessions = await AssessmentSession.findAll({
where: { assessment_id: assessmentId },
attributes: ["session_id", "user_id", "status", "started_at", "expires_at", "attempt_id", "createdAt", "updatedAt"],
include: [{ model: mdl_Users, as: "user", attributes: ["user_id", "email", "personal_info"] }],
include: [{ model: mdl_Users, as: "user", attributes: ["user_id", "email", "personal_info", "deletedAt"], paranoid: false }],
order: [["createdAt", "DESC"]],
});
const rows = sessions.map((s) => {
const j = s.toJSON();
const { full_name, email, avatar_url } = extractUserInfo(j.user);
const { full_name, email, avatar_url, deleted } = extractUserInfo(j.user);
const time_spent_seconds = j.status !== 'in_progress' && j.started_at
? Math.round((new Date(j.updatedAt) - new Date(j.started_at)) / 1000)
: null;
@@ -1689,6 +1738,7 @@ exports.getAssessmentSessions = async (req, res) => {
full_name,
email,
avatar_url,
deleted,
status: j.status,
started_at: j.started_at,
expires_at: j.expires_at,