mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
course,tasklist,task and completed validation
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -28,11 +28,12 @@ const {
|
||||
Unit, Lesson, LessonPage,
|
||||
CourseUnit, UnitLesson,
|
||||
CourseObjective, LessonObjective,
|
||||
CoursePrerequisite, CourseAssessment,
|
||||
CoursePrerequisite, CourseRole, CourseAssessment,
|
||||
UnitQuiz, QuizQuestion, QuizOption, QuizAttempt,
|
||||
AssessmentSession, QuizSession, LessonReadingProgress,
|
||||
AssessmentSession, QuizSession, LessonReadingProgress, UnitReadingProgress,
|
||||
} = require("../../models/courses/courses.associations");
|
||||
const { flattenUnits, flattenLessons } = require("../../utils/courses/hierarchy.util");
|
||||
const { resolvePrerequisiteTitles, resolvePrerequisiteCompletion } = require("../../utils/courses/resolvePrerequisiteTitles.util");
|
||||
const mdl_TierCategories = require("../../models/tiers/tier_categories.mdl");
|
||||
const { gradeSubmission } = require("../../utils/courses/grading.util");
|
||||
const { shuffleOptions, shuffleQuestions, getAttemptStatus } = require("../../utils/courses/quiz_security.util");
|
||||
@@ -42,6 +43,7 @@ const {
|
||||
recomputeUnitAfterQuiz,
|
||||
recomputeCourseAfterAssessment,
|
||||
} = require('../../services/completion_requirements.service');
|
||||
const { getPlaybackPositions } = require('../../services/playback_position.service');
|
||||
const CompletionRequirement = require('../../models/courses/completion_requirement.mdl');
|
||||
const PendingCertificate = require('../../models/courses/pending_certificate.mdl');
|
||||
const Certificate = require('../../models/courses/certificate.mdl');
|
||||
@@ -359,6 +361,11 @@ exports.getCourse = async (req, res) => {
|
||||
required: false,
|
||||
attributes: ["prereq_id", "ref_type", "ref_id", "order_index"],
|
||||
},
|
||||
{
|
||||
model: CourseRole, as: "roles",
|
||||
required: false,
|
||||
attributes: ["role_id", "text", "order_index"],
|
||||
},
|
||||
{
|
||||
model: CourseAssessment, as: "assessment",
|
||||
required: false,
|
||||
@@ -384,6 +391,7 @@ exports.getCourse = async (req, res) => {
|
||||
order: [
|
||||
[{ model: CourseObjective, as: "objectives" }, "order_index", "ASC"],
|
||||
[{ model: CoursePrerequisite, as: "prerequisites" }, "order_index", "ASC"],
|
||||
[{ model: CourseRole, as: "roles" }, "order_index", "ASC"],
|
||||
],
|
||||
});
|
||||
|
||||
@@ -391,6 +399,12 @@ exports.getCourse = async (req, res) => {
|
||||
|
||||
const plain = course.toJSON();
|
||||
plain.units = flattenUnits(plain.units); // junction order_index → flat field, sorted
|
||||
plain.prerequisites = await resolvePrerequisiteTitles(plain.prerequisites, { Course, Unit, Lesson });
|
||||
plain.prerequisites = await resolvePrerequisiteCompletion(
|
||||
plain.prerequisites,
|
||||
{ Certificate, UnitReadingProgress, LessonReadingProgress },
|
||||
req.user.user_id,
|
||||
);
|
||||
|
||||
// Attach has_passed to each unit's quiz in one query
|
||||
const quizIds = plain.units
|
||||
@@ -587,7 +601,23 @@ exports.getLesson = async (req, res) => {
|
||||
});
|
||||
|
||||
if (!lesson) return R.error(res, "Lesson not found.", 404);
|
||||
return R.success(res, "Lesson retrieved.", { ...lesson.toJSON(), unit_id: Number(unitId), order_index: lessonLink.order_index });
|
||||
|
||||
// completion tells the client whether this lesson's video/audio blocks are gated by a
|
||||
// watch-type requirement (anti-skip seek-cap should only apply then) — resume_positions
|
||||
// is unconditional, tracked for every video/audio block regardless of completion type.
|
||||
const [requirement, resumePositions] = await Promise.all([
|
||||
CompletionRequirement.findOne({
|
||||
where: { entity_type: "lesson", entity_id: lessonId },
|
||||
attributes: ["type", "min_percent", "button_label"],
|
||||
}),
|
||||
getPlaybackPositions(req.user.user_id, lessonId),
|
||||
]);
|
||||
|
||||
return R.success(res, "Lesson retrieved.", {
|
||||
...lesson.toJSON(), unit_id: Number(unitId), order_index: lessonLink.order_index,
|
||||
completion: requirement ? { type: requirement.type, min_percent: requirement.min_percent, button_label: requirement.button_label } : null,
|
||||
resume_positions: resumePositions,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[CLIENT][LESSON][GET ONE]", err);
|
||||
return R.error(res, "Could not retrieve lesson.", 500);
|
||||
@@ -1440,6 +1470,10 @@ exports.getLessonByUuid = async (req, res) => {
|
||||
attributes: ["type", "min_percent", "button_label"],
|
||||
});
|
||||
|
||||
// Unconditional — tracked for every video/audio block regardless of whether `requirement`
|
||||
// above is watch-type or configured at all (resume is a UX convenience, not a gate).
|
||||
const resumePositions = await getPlaybackPositions(req.user.user_id, lesson.lesson_id);
|
||||
|
||||
const plain = lesson.toJSON();
|
||||
const firstUnit = plain.units?.[0] ?? null;
|
||||
const data = {
|
||||
@@ -1453,6 +1487,7 @@ exports.getLessonByUuid = async (req, res) => {
|
||||
status: progress?.status ?? "not_started",
|
||||
completed_at: progress?.completed_at ?? null,
|
||||
completion: requirement ? { type: requirement.type, min_percent: requirement.min_percent, button_label: requirement.button_label } : null,
|
||||
resume_positions: resumePositions,
|
||||
unit: firstUnit ? { unit_id: firstUnit.unit_id, uuid: firstUnit.uuid, title: firstUnit.title, duration_seconds: firstUnit.duration_seconds, course: firstUnit.courses?.[0] ?? null } : null, // back-compat singular field
|
||||
units: plain.units ?? [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user