fix: throttle request issue per video,audio as indicated requirements in units and lessons

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-15 17:01:05 +08:00
parent 9c82b0de09
commit 1745990cda
2 changed files with 30 additions and 2 deletions
+20
View File
@@ -1350,6 +1350,13 @@ exports.getLessonsByUnitUuid = async (req, res) => {
const unitEvaluation = await evaluateEntity({ entityType: "unit", entityId: unit.unit_id, userId, courseId: null }); const unitEvaluation = await evaluateEntity({ entityType: "unit", entityId: unit.unit_id, userId, courseId: null });
const is_completed = unitEvaluation.status === "completed"; const is_completed = unitEvaluation.status === "completed";
// Admin-configured completion requirement for the unit itself (pass_quiz /
// manual_complete / read_all_content) — null when nothing's configured.
const unitRequirement = await CompletionRequirement.findOne({
where: { entity_type: "unit", entity_id: unit.unit_id },
attributes: ["type", "min_percent", "button_label"],
});
return R.success(res, "Unit lessons retrieved.", { return R.success(res, "Unit lessons retrieved.", {
unit_id: unit.unit_id, unit_id: unit.unit_id,
uuid: unit.uuid, uuid: unit.uuid,
@@ -1360,6 +1367,9 @@ exports.getLessonsByUnitUuid = async (req, res) => {
courses: plain.courses ?? [], courses: plain.courses ?? [],
quiz, quiz,
is_completed, is_completed,
completion: unitRequirement
? { type: unitRequirement.type, min_percent: unitRequirement.min_percent, button_label: unitRequirement.button_label }
: null,
lessons, lessons,
}); });
} catch (err) { } catch (err) {
@@ -1421,6 +1431,15 @@ exports.getLessonByUuid = async (req, res) => {
attributes: ["status", "completed_at"], attributes: ["status", "completed_at"],
}); });
// Admin-configured completion requirement (if any) — lets the standalone reader
// show what a learner must do to complete this lesson, same info the course-scoped
// reader gets via getCourse's per-lesson `completion` field. null when nothing's
// configured (default read_all_content behavior — nothing to display).
const requirement = await CompletionRequirement.findOne({
where: { entity_type: "lesson", entity_id: lesson.lesson_id },
attributes: ["type", "min_percent", "button_label"],
});
const plain = lesson.toJSON(); const plain = lesson.toJSON();
const firstUnit = plain.units?.[0] ?? null; const firstUnit = plain.units?.[0] ?? null;
const data = { const data = {
@@ -1433,6 +1452,7 @@ exports.getLessonByUuid = async (req, res) => {
objectives: plain.objectives ?? [], objectives: plain.objectives ?? [],
status: progress?.status ?? "not_started", status: progress?.status ?? "not_started",
completed_at: progress?.completed_at ?? null, completed_at: progress?.completed_at ?? null,
completion: requirement ? { type: requirement.type, min_percent: requirement.min_percent, button_label: requirement.button_label } : null,
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 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 ?? [], units: plain.units ?? [],
}; };
+10 -2
View File
@@ -219,11 +219,17 @@ async function recomputeCourseAfterAssessment(userId, courseId, t) {
* live block set on every write, not a snapshot taken when configured). * live block set on every write, not a snapshot taken when configured).
* No-ops (returns null) if the lesson has neither type configured. * No-ops (returns null) if the lesson has neither type configured.
*/ */
// text-video counts as a video block for watch_video purposes — same <video> element
// under the hood, just paired with text. A lesson mixing plain "video" and "text-video"
// blocks needs both kinds counted together when checking "every video block hit 100%".
const VIDEO_LIKE_BLOCK_TYPES = ['video', 'text-video'];
async function recordWatchProgress(userId, { async function recordWatchProgress(userId, {
lessonId, lessonUuid, unitId = null, unitUuid = null, courseId = null, courseUuid = null, lessonId, lessonUuid, unitId = null, unitUuid = null, courseId = null, courseUuid = null,
percent, blockId = null, blockType = null, percent, blockId = null, blockType = null,
}) { }) {
const blockRequirementType = blockType === 'video' ? 'watch_video' : blockType === 'audio' ? 'listen_audio' : null; const isVideoLikeBlock = VIDEO_LIKE_BLOCK_TYPES.includes(blockType);
const blockRequirementType = isVideoLikeBlock ? 'watch_video' : blockType === 'audio' ? 'listen_audio' : null;
const requirements = await CompletionRequirement.findAll({ const requirements = await CompletionRequirement.findAll({
where: { where: {
@@ -273,7 +279,9 @@ async function recordWatchProgress(userId, {
const nextBlockProgress = { ...prevBlockProgress, [blockId]: Math.max(prevBlockProgress[blockId] ?? 0, clampedPercent) }; const nextBlockProgress = { ...prevBlockProgress, [blockId]: Math.max(prevBlockProgress[blockId] ?? 0, clampedPercent) };
const page = await LessonPage.findOne({ where: { lesson_id: lessonId }, attributes: ['blocks'], transaction: t }); const page = await LessonPage.findOne({ where: { lesson_id: lessonId }, attributes: ['blocks'], transaction: t });
const matchingBlockIds = (page?.blocks ?? []).filter((b) => b.type === blockType).map((b) => b.id); const matchingBlockIds = (page?.blocks ?? [])
.filter((b) => isVideoLikeBlock ? VIDEO_LIKE_BLOCK_TYPES.includes(b.type) : b.type === blockType)
.map((b) => b.id);
const completed = matchingBlockIds.length > 0 && matchingBlockIds.every((id) => (nextBlockProgress[id] ?? 0) >= 100); const completed = matchingBlockIds.length > 0 && matchingBlockIds.every((id) => (nextBlockProgress[id] ?? 0) >= 100);
await CompletionRequirementProgress.upsert({ await CompletionRequirementProgress.upsert({