mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user