Compare commits
No commits in common. "10954d88b087a5e6a98299dd291669c2701c3766" and "5b53929d92101a2ccfbccea2fd37c9a36008863c" have entirely different histories.
10954d88b0
...
5b53929d92
|
|
@ -24,7 +24,6 @@ import (
|
||||||
practicesservice "Yimaru-Backend/internal/services/practices"
|
practicesservice "Yimaru-Backend/internal/services/practices"
|
||||||
programsservice "Yimaru-Backend/internal/services/programs"
|
programsservice "Yimaru-Backend/internal/services/programs"
|
||||||
"Yimaru-Backend/internal/services/questions"
|
"Yimaru-Backend/internal/services/questions"
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
|
||||||
"Yimaru-Backend/internal/services/recommendation"
|
"Yimaru-Backend/internal/services/recommendation"
|
||||||
"Yimaru-Backend/internal/services/settings"
|
"Yimaru-Backend/internal/services/settings"
|
||||||
"Yimaru-Backend/internal/services/subscriptions"
|
"Yimaru-Backend/internal/services/subscriptions"
|
||||||
|
|
@ -393,7 +392,6 @@ func main() {
|
||||||
|
|
||||||
// Questions service (unified questions system)
|
// Questions service (unified questions system)
|
||||||
questionsSvc := questions.NewService(store)
|
questionsSvc := questions.NewService(store)
|
||||||
examPrepSvc := examprep.NewService(store)
|
|
||||||
|
|
||||||
// LMS programs (top-level hierarchy)
|
// LMS programs (top-level hierarchy)
|
||||||
programSvc := programsservice.NewService(store)
|
programSvc := programsservice.NewService(store)
|
||||||
|
|
@ -453,7 +451,6 @@ func main() {
|
||||||
app := httpserver.NewApp(
|
app := httpserver.NewApp(
|
||||||
assessmentSvc,
|
assessmentSvc,
|
||||||
questionsSvc,
|
questionsSvc,
|
||||||
examPrepSvc,
|
|
||||||
programSvc,
|
programSvc,
|
||||||
courseSvc,
|
courseSvc,
|
||||||
moduleSvc,
|
moduleSvc,
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
DROP TABLE IF EXISTS lms_practices;
|
DROP TABLE IF EXISTS lms_practices;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
-- Default CEFR-style courses per seeded program: Beginner→A1,A2; Intermediate→B1,B2; Advanced→C1,C2.
|
-- Default CEFR-style course names per program (custom courses can still be created via the API with any name).
|
||||||
-- Custom courses can still be created via the API with any name.
|
-- Matches hierarchy note on courses: CEFR labels A1..C2, plus ad-hoc names allowed.
|
||||||
INSERT INTO courses (program_id, name, description, thumbnail)
|
INSERT INTO courses (program_id, name, description, thumbnail)
|
||||||
SELECT
|
SELECT
|
||||||
p.id,
|
p.id,
|
||||||
|
|
@ -7,13 +7,12 @@ SELECT
|
||||||
'Default CEFR level course (system seed).',
|
'Default CEFR level course (system seed).',
|
||||||
NULL
|
NULL
|
||||||
FROM programs AS p
|
FROM programs AS p
|
||||||
INNER JOIN (
|
CROSS JOIN (
|
||||||
VALUES
|
VALUES
|
||||||
('Beginner', 'A1'),
|
('A1'),
|
||||||
('Beginner', 'A2'),
|
('A2'),
|
||||||
('Intermediate', 'B1'),
|
('B1'),
|
||||||
('Intermediate', 'B2'),
|
('B2'),
|
||||||
('Advanced', 'C1'),
|
('C1'),
|
||||||
('Advanced', 'C2')
|
('C2')
|
||||||
) AS v (program_name, name)
|
) AS v (name);
|
||||||
ON p.name = v.program_name;
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
-- Data cleanup is not reversed; restoring the old cross-product seed would be ambiguous.
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
-- Align default seeded courses with program: Beginner→A1,A2; Intermediate→B1,B2; Advanced→C1,C2.
|
|
||||||
-- Only touches rows with the system seed description; custom courses are unchanged.
|
|
||||||
-- Removing a course cascades to modules, lessons, and related LMS progress (see FKs on those tables).
|
|
||||||
|
|
||||||
DELETE FROM courses AS c
|
|
||||||
USING programs AS p
|
|
||||||
WHERE c.program_id = p.id
|
|
||||||
AND c.description = 'Default CEFR level course (system seed).'
|
|
||||||
AND (
|
|
||||||
(
|
|
||||||
p.name = 'Beginner'
|
|
||||||
AND c.name IN ('B1', 'B2', 'C1', 'C2')
|
|
||||||
)
|
|
||||||
OR (
|
|
||||||
p.name = 'Intermediate'
|
|
||||||
AND c.name IN ('A1', 'A2', 'C1', 'C2')
|
|
||||||
)
|
|
||||||
OR (
|
|
||||||
p.name = 'Advanced'
|
|
||||||
AND c.name IN ('A1', 'A2', 'B1', 'B2')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO courses (program_id, name, description, thumbnail)
|
|
||||||
SELECT
|
|
||||||
p.id,
|
|
||||||
v.name,
|
|
||||||
'Default CEFR level course (system seed).',
|
|
||||||
NULL
|
|
||||||
FROM programs AS p
|
|
||||||
INNER JOIN (
|
|
||||||
VALUES
|
|
||||||
('Beginner', 'A1'),
|
|
||||||
('Beginner', 'A2'),
|
|
||||||
('Intermediate', 'B1'),
|
|
||||||
('Intermediate', 'B2'),
|
|
||||||
('Advanced', 'C1'),
|
|
||||||
('Advanced', 'C2')
|
|
||||||
) AS v (program_name, name)
|
|
||||||
ON p.name = v.program_name
|
|
||||||
WHERE
|
|
||||||
NOT EXISTS (
|
|
||||||
SELECT 1 FROM courses AS e
|
|
||||||
WHERE e.program_id = p.id AND e.name = v.name
|
|
||||||
);
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
DROP TABLE IF EXISTS exam_prep.catalog_courses;
|
|
||||||
DROP SCHEMA IF EXISTS exam_prep;
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
-- Standalone exam-prep content hierarchy (DET, IELTS, TOEFL, etc.) — isolated from LMS Learn English tables.
|
|
||||||
CREATE SCHEMA IF NOT EXISTS exam_prep;
|
|
||||||
|
|
||||||
-- Top-level catalog "course" (e.g. Duolingo English Test, IELTS); admin-configurable labels.
|
|
||||||
CREATE TABLE exam_prep.catalog_courses (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
thumbnail TEXT,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_exam_prep_catalog_courses_sort ON exam_prep.catalog_courses (sort_order, id);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE IF EXISTS exam_prep.units;
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
-- Units under an exam-prep catalog course (e.g. "Introduction to the DET English Test").
|
|
||||||
CREATE TABLE exam_prep.units (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
catalog_course_id BIGINT NOT NULL REFERENCES exam_prep.catalog_courses (id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
thumbnail TEXT,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_exam_prep_units_catalog_course_id ON exam_prep.units (catalog_course_id);
|
|
||||||
CREATE INDEX idx_exam_prep_units_catalog_sort ON exam_prep.units (catalog_course_id, sort_order, id);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE IF EXISTS exam_prep.unit_modules;
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
-- Modules under an exam-prep unit (table name unit_modules avoids sqlc/LMS collision with public.modules).
|
|
||||||
CREATE TABLE exam_prep.unit_modules (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
unit_id BIGINT NOT NULL REFERENCES exam_prep.units (id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
thumbnail TEXT,
|
|
||||||
icon TEXT,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_exam_prep_unit_modules_unit_id ON exam_prep.unit_modules (unit_id);
|
|
||||||
CREATE INDEX idx_exam_prep_unit_modules_unit_sort ON exam_prep.unit_modules (unit_id, sort_order, id);
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
DROP INDEX IF EXISTS uq_exam_prep_unit_module_lessons_sort;
|
|
||||||
DROP TABLE IF EXISTS exam_prep.unit_module_lessons;
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
-- Lessons under an exam-prep unit module (mirrors LMS lessons under modules; avoids collision with public.lessons / sqlc).
|
|
||||||
CREATE TABLE exam_prep.unit_module_lessons (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
unit_module_id BIGINT NOT NULL REFERENCES exam_prep.unit_modules (id) ON DELETE CASCADE,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
video_url TEXT,
|
|
||||||
thumbnail TEXT,
|
|
||||||
description TEXT,
|
|
||||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX uq_exam_prep_unit_module_lessons_sort ON exam_prep.unit_module_lessons (unit_module_id, sort_order);
|
|
||||||
|
|
||||||
CREATE INDEX idx_exam_prep_unit_module_lessons_module_id ON exam_prep.unit_module_lessons (unit_module_id);
|
|
||||||
CREATE INDEX idx_exam_prep_unit_module_lessons_module_created ON exam_prep.unit_module_lessons (unit_module_id, created_at DESC);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
DROP TABLE IF EXISTS exam_prep.lesson_practices;
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
-- Exam-prep practices: one row per practice, attached to an exam-prep lesson only; reuses public.question_sets / questions.
|
|
||||||
CREATE TABLE exam_prep.lesson_practices (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
unit_module_lesson_id BIGINT NOT NULL REFERENCES exam_prep.unit_module_lessons (id) ON DELETE CASCADE,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
story_description TEXT,
|
|
||||||
story_image TEXT,
|
|
||||||
persona_id BIGINT REFERENCES users (id) ON DELETE SET NULL,
|
|
||||||
question_set_id BIGINT NOT NULL REFERENCES question_sets (id) ON DELETE RESTRICT,
|
|
||||||
quick_tips TEXT,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX idx_exam_prep_lesson_practices_lesson_id ON exam_prep.lesson_practices (unit_module_lesson_id);
|
|
||||||
CREATE INDEX idx_exam_prep_lesson_practices_question_set_id ON exam_prep.lesson_practices (question_set_id);
|
|
||||||
CREATE INDEX idx_exam_prep_lesson_practices_lesson_created ON exam_prep.lesson_practices (unit_module_lesson_id, created_at DESC);
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
-- name: ExamPrepCreateCatalogCourse :one
|
|
||||||
INSERT INTO exam_prep.catalog_courses (name, description, thumbnail, sort_order)
|
|
||||||
SELECT
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
coalesce((
|
|
||||||
SELECT
|
|
||||||
max(c.sort_order)
|
|
||||||
FROM exam_prep.catalog_courses AS c), 0) + 1
|
|
||||||
RETURNING
|
|
||||||
*;
|
|
||||||
|
|
||||||
-- name: ExamPrepGetCatalogCourseByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM exam_prep.catalog_courses
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ExamPrepListCatalogCourses :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
c.id,
|
|
||||||
c.name,
|
|
||||||
c.description,
|
|
||||||
c.thumbnail,
|
|
||||||
c.sort_order,
|
|
||||||
c.created_at,
|
|
||||||
c.updated_at
|
|
||||||
FROM exam_prep.catalog_courses c
|
|
||||||
ORDER BY c.sort_order ASC, c.id ASC
|
|
||||||
LIMIT $1 OFFSET $2;
|
|
||||||
|
|
||||||
-- name: ExamPrepListAllCatalogCourseIDs :many
|
|
||||||
SELECT
|
|
||||||
id
|
|
||||||
FROM exam_prep.catalog_courses
|
|
||||||
ORDER BY id;
|
|
||||||
|
|
||||||
-- name: ExamPrepUpdateCatalogCourse :one
|
|
||||||
UPDATE exam_prep.catalog_courses
|
|
||||||
SET
|
|
||||||
name = coalesce(sqlc.narg('name')::varchar, name),
|
|
||||||
description = coalesce(sqlc.narg('description')::text, description),
|
|
||||||
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
|
|
||||||
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = sqlc.arg('id')
|
|
||||||
RETURNING
|
|
||||||
*;
|
|
||||||
|
|
||||||
-- name: ExamPrepDeleteCatalogCourse :exec
|
|
||||||
DELETE FROM exam_prep.catalog_courses
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
-- name: ExamPrepCreateLessonPractice :one
|
|
||||||
INSERT INTO exam_prep.lesson_practices (
|
|
||||||
unit_module_lesson_id,
|
|
||||||
title,
|
|
||||||
story_description,
|
|
||||||
story_image,
|
|
||||||
persona_id,
|
|
||||||
question_set_id,
|
|
||||||
quick_tips
|
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: ExamPrepGetLessonPracticeByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM exam_prep.lesson_practices
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ExamPrepListLessonPracticesByLessonID :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
p.id,
|
|
||||||
p.unit_module_lesson_id,
|
|
||||||
p.title,
|
|
||||||
p.story_description,
|
|
||||||
p.story_image,
|
|
||||||
p.persona_id,
|
|
||||||
p.question_set_id,
|
|
||||||
p.quick_tips,
|
|
||||||
p.created_at,
|
|
||||||
p.updated_at
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
WHERE p.unit_module_lesson_id = $1
|
|
||||||
ORDER BY p.created_at DESC
|
|
||||||
LIMIT $2
|
|
||||||
OFFSET $3;
|
|
||||||
|
|
||||||
-- name: ExamPrepUpdateLessonPractice :one
|
|
||||||
UPDATE exam_prep.lesson_practices
|
|
||||||
SET
|
|
||||||
title = coalesce(sqlc.narg('title')::varchar, title),
|
|
||||||
story_description = coalesce(sqlc.narg('story_description')::text, story_description),
|
|
||||||
story_image = coalesce(sqlc.narg('story_image')::text, story_image),
|
|
||||||
persona_id = coalesce(sqlc.narg('persona_id')::bigint, persona_id),
|
|
||||||
question_set_id = coalesce(sqlc.narg('question_set_id')::bigint, question_set_id),
|
|
||||||
quick_tips = coalesce(sqlc.narg('quick_tips')::text, quick_tips),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = sqlc.arg('id')
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: ExamPrepDeleteLessonPractice :exec
|
|
||||||
DELETE FROM exam_prep.lesson_practices
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
-- name: ExamPrepCreateUnitModuleLesson :one
|
|
||||||
INSERT INTO exam_prep.unit_module_lessons (unit_module_id, title, video_url, thumbnail, description, sort_order)
|
|
||||||
SELECT
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
coalesce((
|
|
||||||
SELECT
|
|
||||||
max(l.sort_order)
|
|
||||||
FROM exam_prep.unit_module_lessons l
|
|
||||||
WHERE
|
|
||||||
l.unit_module_id = $1), 0) + 1
|
|
||||||
RETURNING
|
|
||||||
*;
|
|
||||||
|
|
||||||
-- name: ExamPrepGetUnitModuleLessonByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM exam_prep.unit_module_lessons
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ExamPrepListUnitModuleLessonIDsByUnitModule :many
|
|
||||||
SELECT
|
|
||||||
l.id
|
|
||||||
FROM exam_prep.unit_module_lessons l
|
|
||||||
WHERE
|
|
||||||
l.unit_module_id = $1
|
|
||||||
ORDER BY
|
|
||||||
l.id;
|
|
||||||
|
|
||||||
-- name: ExamPrepListUnitModuleLessonsByUnitModuleID :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
l.id,
|
|
||||||
l.unit_module_id,
|
|
||||||
l.title,
|
|
||||||
l.video_url,
|
|
||||||
l.thumbnail,
|
|
||||||
l.description,
|
|
||||||
l.sort_order,
|
|
||||||
l.created_at,
|
|
||||||
l.updated_at
|
|
||||||
FROM exam_prep.unit_module_lessons l
|
|
||||||
WHERE
|
|
||||||
l.unit_module_id = $1
|
|
||||||
ORDER BY
|
|
||||||
l.sort_order ASC,
|
|
||||||
l.id ASC
|
|
||||||
LIMIT $2
|
|
||||||
OFFSET $3;
|
|
||||||
|
|
||||||
-- name: ExamPrepUpdateUnitModuleLesson :one
|
|
||||||
UPDATE exam_prep.unit_module_lessons
|
|
||||||
SET
|
|
||||||
title = coalesce(sqlc.narg('title')::varchar, title),
|
|
||||||
video_url = coalesce(sqlc.narg('video_url')::text, video_url),
|
|
||||||
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
|
|
||||||
description = coalesce(sqlc.narg('description')::text, description),
|
|
||||||
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = sqlc.arg('id')
|
|
||||||
RETURNING
|
|
||||||
*;
|
|
||||||
|
|
||||||
-- name: ExamPrepDeleteUnitModuleLesson :exec
|
|
||||||
DELETE FROM exam_prep.unit_module_lessons
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
-- name: ExamPrepCreateUnitModule :one
|
|
||||||
INSERT INTO exam_prep.unit_modules (unit_id, name, description, thumbnail, icon, sort_order)
|
|
||||||
SELECT
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
coalesce((
|
|
||||||
SELECT
|
|
||||||
max(m.sort_order)
|
|
||||||
FROM exam_prep.unit_modules m
|
|
||||||
WHERE
|
|
||||||
m.unit_id = $1), 0) + 1
|
|
||||||
RETURNING
|
|
||||||
*;
|
|
||||||
|
|
||||||
-- name: ExamPrepGetUnitModuleByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM exam_prep.unit_modules
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ExamPrepListUnitModuleIDsByUnit :many
|
|
||||||
SELECT
|
|
||||||
m.id
|
|
||||||
FROM exam_prep.unit_modules m
|
|
||||||
WHERE
|
|
||||||
m.unit_id = $1
|
|
||||||
ORDER BY
|
|
||||||
m.id;
|
|
||||||
|
|
||||||
-- name: ExamPrepListUnitModulesByUnit :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
m.id,
|
|
||||||
m.unit_id,
|
|
||||||
m.name,
|
|
||||||
m.description,
|
|
||||||
m.thumbnail,
|
|
||||||
m.icon,
|
|
||||||
m.sort_order,
|
|
||||||
m.created_at,
|
|
||||||
m.updated_at
|
|
||||||
FROM exam_prep.unit_modules m
|
|
||||||
WHERE
|
|
||||||
m.unit_id = $1
|
|
||||||
ORDER BY
|
|
||||||
m.sort_order ASC,
|
|
||||||
m.id ASC
|
|
||||||
LIMIT $2
|
|
||||||
OFFSET $3;
|
|
||||||
|
|
||||||
-- name: ExamPrepUpdateUnitModule :one
|
|
||||||
UPDATE exam_prep.unit_modules
|
|
||||||
SET
|
|
||||||
name = coalesce(sqlc.narg('name')::varchar, name),
|
|
||||||
description = coalesce(sqlc.narg('description')::text, description),
|
|
||||||
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
|
|
||||||
icon = coalesce(sqlc.narg('icon')::text, icon),
|
|
||||||
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = sqlc.arg('id')
|
|
||||||
RETURNING
|
|
||||||
*;
|
|
||||||
|
|
||||||
-- name: ExamPrepDeleteUnitModule :exec
|
|
||||||
DELETE FROM exam_prep.unit_modules
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
-- name: ExamPrepCreateUnit :one
|
|
||||||
INSERT INTO exam_prep.units (catalog_course_id, name, description, thumbnail, sort_order)
|
|
||||||
SELECT
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
coalesce((
|
|
||||||
SELECT
|
|
||||||
max(u.sort_order)
|
|
||||||
FROM exam_prep.units u
|
|
||||||
WHERE
|
|
||||||
u.catalog_course_id = $1), 0) + 1
|
|
||||||
RETURNING
|
|
||||||
*;
|
|
||||||
|
|
||||||
-- name: ExamPrepGetUnitByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM exam_prep.units
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ExamPrepListUnitIDsByCatalogCourse :many
|
|
||||||
SELECT
|
|
||||||
u.id
|
|
||||||
FROM exam_prep.units u
|
|
||||||
WHERE
|
|
||||||
u.catalog_course_id = $1
|
|
||||||
ORDER BY
|
|
||||||
u.id;
|
|
||||||
|
|
||||||
-- name: ExamPrepListUnitsByCatalogCourse :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
u.id,
|
|
||||||
u.catalog_course_id,
|
|
||||||
u.name,
|
|
||||||
u.description,
|
|
||||||
u.thumbnail,
|
|
||||||
u.sort_order,
|
|
||||||
u.created_at,
|
|
||||||
u.updated_at
|
|
||||||
FROM exam_prep.units u
|
|
||||||
WHERE
|
|
||||||
u.catalog_course_id = $1
|
|
||||||
ORDER BY
|
|
||||||
u.sort_order ASC,
|
|
||||||
u.id ASC
|
|
||||||
LIMIT $2
|
|
||||||
OFFSET $3;
|
|
||||||
|
|
||||||
-- name: ExamPrepUpdateUnit :one
|
|
||||||
UPDATE exam_prep.units
|
|
||||||
SET
|
|
||||||
name = coalesce(sqlc.narg('name')::varchar, name),
|
|
||||||
description = coalesce(sqlc.narg('description')::text, description),
|
|
||||||
thumbnail = coalesce(sqlc.narg('thumbnail')::text, thumbnail),
|
|
||||||
sort_order = coalesce(sqlc.narg('sort_order')::int, sort_order),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = sqlc.arg('id')
|
|
||||||
RETURNING
|
|
||||||
*;
|
|
||||||
|
|
||||||
-- name: ExamPrepDeleteUnit :exec
|
|
||||||
DELETE FROM exam_prep.units
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
@ -39,33 +39,7 @@ SELECT
|
||||||
c.thumbnail,
|
c.thumbnail,
|
||||||
c.sort_order,
|
c.sort_order,
|
||||||
c.created_at,
|
c.created_at,
|
||||||
c.updated_at,
|
c.updated_at
|
||||||
(
|
|
||||||
SELECT
|
|
||||||
COUNT(*)::bigint
|
|
||||||
FROM
|
|
||||||
modules m
|
|
||||||
WHERE
|
|
||||||
m.course_id = c.id) AS module_count,
|
|
||||||
(
|
|
||||||
SELECT
|
|
||||||
COUNT(*)::bigint
|
|
||||||
FROM
|
|
||||||
lessons l
|
|
||||||
INNER JOIN modules m ON l.module_id = m.id
|
|
||||||
WHERE
|
|
||||||
m.course_id = c.id) AS lesson_count,
|
|
||||||
-- Practices whose parent is the course only (lms_practices.course_id). Excludes
|
|
||||||
-- practices linked via module_id or lesson_id, even for modules/lessons in this course.
|
|
||||||
(
|
|
||||||
SELECT
|
|
||||||
COUNT(*)::bigint
|
|
||||||
FROM
|
|
||||||
lms_practices p
|
|
||||||
WHERE
|
|
||||||
p.course_id = c.id
|
|
||||||
AND p.module_id IS NULL
|
|
||||||
AND p.lesson_id IS NULL) AS practice_count
|
|
||||||
FROM
|
FROM
|
||||||
courses c
|
courses c
|
||||||
WHERE
|
WHERE
|
||||||
|
|
|
||||||
|
|
@ -246,95 +246,3 @@ FROM
|
||||||
WHERE
|
WHERE
|
||||||
c.program_id = $1
|
c.program_id = $1
|
||||||
AND ulp.user_id = $2;
|
AND ulp.user_id = $2;
|
||||||
|
|
||||||
-- Published practices in a module (module-level and lesson-level practices should carry module_id).
|
|
||||||
-- name: CountPublishedPracticesInModule :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
lp.module_id = $1
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED';
|
|
||||||
|
|
||||||
-- name: CountUserCompletedPublishedPracticesInModule :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
lp.module_id = $1
|
|
||||||
AND upp.user_id = $2
|
|
||||||
AND upp.completed_at IS NOT NULL
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED';
|
|
||||||
|
|
||||||
-- name: CountPublishedPracticesInCourse :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
lp.course_id = $1
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED';
|
|
||||||
|
|
||||||
-- name: CountUserCompletedPublishedPracticesInCourse :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
lp.course_id = $1
|
|
||||||
AND upp.user_id = $2
|
|
||||||
AND upp.completed_at IS NOT NULL
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED';
|
|
||||||
|
|
||||||
-- name: CountPublishedPracticesInProgram :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN courses c ON c.id = lp.course_id
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
c.program_id = $1
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED';
|
|
||||||
|
|
||||||
-- name: CountUserCompletedPublishedPracticesInProgram :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN courses c ON c.id = lp.course_id
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
c.program_id = $1
|
|
||||||
AND upp.user_id = $2
|
|
||||||
AND upp.completed_at IS NOT NULL
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED';
|
|
||||||
|
|
||||||
-- name: GetPracticeScopeByQuestionSetID :one
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
course_id,
|
|
||||||
module_id,
|
|
||||||
lesson_id
|
|
||||||
FROM
|
|
||||||
lms_practices
|
|
||||||
WHERE
|
|
||||||
question_set_id = $1
|
|
||||||
ORDER BY
|
|
||||||
id DESC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,7 @@ VALUES ($1, $2, $3, $4, $5, $6);
|
||||||
SELECT id, user_id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
|
SELECT id, user_id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
|
||||||
FROM otps
|
FROM otps
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
ORDER BY id DESC
|
ORDER BY created_at DESC LIMIT 1;
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- name: GetOtpByCode :one
|
|
||||||
SELECT id, user_id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
|
|
||||||
FROM otps
|
|
||||||
WHERE user_id = $1
|
|
||||||
AND otp = $2
|
|
||||||
ORDER BY id DESC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- name: MarkOtpAsUsed :exec
|
-- name: MarkOtpAsUsed :exec
|
||||||
UPDATE otps
|
UPDATE otps
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,10 @@ SELECT
|
||||||
q.explanation,
|
q.explanation,
|
||||||
q.tips,
|
q.tips,
|
||||||
q.voice_prompt,
|
q.voice_prompt,
|
||||||
q.sample_answer_voice_prompt,
|
|
||||||
q.image_url,
|
q.image_url,
|
||||||
q.status as question_status,
|
q.status as question_status
|
||||||
qaa.correct_answer_text AS audio_correct_answer_text
|
|
||||||
FROM question_set_items qsi
|
FROM question_set_items qsi
|
||||||
JOIN questions q ON q.id = qsi.question_id
|
JOIN questions q ON q.id = qsi.question_id
|
||||||
LEFT JOIN question_audio_answers qaa ON qaa.question_id = q.id
|
|
||||||
WHERE qsi.set_id = $1
|
WHERE qsi.set_id = $1
|
||||||
AND q.status != 'ARCHIVED'
|
AND q.status != 'ARCHIVED'
|
||||||
ORDER BY qsi.display_order;
|
ORDER BY qsi.display_order;
|
||||||
|
|
@ -73,12 +70,9 @@ SELECT
|
||||||
q.explanation,
|
q.explanation,
|
||||||
q.tips,
|
q.tips,
|
||||||
q.voice_prompt,
|
q.voice_prompt,
|
||||||
q.sample_answer_voice_prompt,
|
q.image_url
|
||||||
q.image_url,
|
|
||||||
qaa.correct_answer_text AS audio_correct_answer_text
|
|
||||||
FROM question_set_items qsi
|
FROM question_set_items qsi
|
||||||
JOIN questions q ON q.id = qsi.question_id
|
JOIN questions q ON q.id = qsi.question_id
|
||||||
LEFT JOIN question_audio_answers qaa ON qaa.question_id = q.id
|
|
||||||
WHERE qsi.set_id = $1
|
WHERE qsi.set_id = $1
|
||||||
AND q.status = 'PUBLISHED'
|
AND q.status = 'PUBLISHED'
|
||||||
ORDER BY qsi.display_order;
|
ORDER BY qsi.display_order;
|
||||||
|
|
|
||||||
933
docs/docs.go
933
docs/docs.go
|
|
@ -951,663 +951,6 @@ const docTemplate = `{
|
||||||
"responses": {}
|
"responses": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/exam-prep/catalog-courses": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "List exam-prep catalog courses",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 20,
|
|
||||||
"description": "Page size",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"description": "Offset",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"description": "Top-level exam track (DET, IELTS, …) in schema exam_prep — separate from LMS programs/courses",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Create exam-prep catalog course",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Catalog course",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.CreateExamPrepCatalogCourseInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Created",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/catalog-courses/reorder": {
|
|
||||||
"put": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Reorder all exam-prep catalog courses",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "ordered_ids: every catalog course id exactly once",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ReorderIDsRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/catalog-courses/{catalogCourseId}/units": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "List exam-prep units for a catalog course",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "catalogCourseId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 20,
|
|
||||||
"description": "Page size",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"description": "Offset",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"description": "Unit under a catalog course (e.g. chapter title)",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Create exam-prep unit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "catalogCourseId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Unit",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.CreateExamPrepUnitInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Created",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/catalog-courses/{catalogCourseId}/units/reorder": {
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Reorder units within a catalog course",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "catalogCourseId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "ordered_ids: every unit id in this catalog course, new order",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ReorderIDsRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/catalog-courses/{id}": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Get exam-prep catalog course by ID",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Update exam-prep catalog course",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Fields to update",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.UpdateExamPrepCatalogCourseInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Delete exam-prep catalog course",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/lessons/{id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Get exam-prep lesson by ID",
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Update exam-prep lesson",
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Delete exam-prep lesson",
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/lessons/{lessonId}/practices": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "List exam-prep practices for a lesson",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep lesson ID",
|
|
||||||
"name": "lessonId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 20,
|
|
||||||
"description": "Page size",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"description": "Offset",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Create exam-prep practice (under a lesson; uses shared question_sets)",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep lesson ID (unit_module_lessons.id)",
|
|
||||||
"name": "lessonId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Practice",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.CreateExamPrepPracticeInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/modules/{id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Get exam-prep module by ID",
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Update exam-prep module",
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Delete exam-prep module",
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/modules/{moduleId}/lessons": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "List exam-prep lessons for a unit module",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep unit module ID",
|
|
||||||
"name": "moduleId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 20,
|
|
||||||
"description": "Page size",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"description": "Offset",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Create exam-prep lesson (under a unit module)",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep unit module ID",
|
|
||||||
"name": "moduleId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Lesson",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.CreateExamPrepLessonInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/modules/{moduleId}/lessons/reorder": {
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Reorder lessons within an exam-prep unit module",
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/practices/{id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Get exam-prep practice by ID",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep practice ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Update exam-prep practice",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep practice ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Fields to update",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.UpdateExamPrepPracticeInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Delete exam-prep practice",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep practice ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/units/{id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Get exam-prep unit by ID",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Update exam-prep unit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Fields to update",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.UpdateExamPrepUnitInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Delete exam-prep unit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/units/{unitId}/modules": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "List exam-prep modules for a unit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "unitId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Create exam-prep module",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "unitId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Module",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.CreateExamPrepModuleInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/units/{unitId}/modules/reorder": {
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Reorder modules within a unit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "unitId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "ordered_ids",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ReorderIDsRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/files/audio": {
|
"/api/v1/files/audio": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
|
@ -1636,39 +979,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/files/refresh-url": {
|
|
||||||
"post": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"files"
|
|
||||||
],
|
|
||||||
"summary": "Refresh presigned URL for a file",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "reference (object key, minio://..., or existing presigned URL)",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handlers.refreshFileURLReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/files/upload": {
|
"/api/v1/files/upload": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
|
@ -4646,26 +3956,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/questions/component-catalog": {
|
|
||||||
"get": {
|
|
||||||
"description": "Valid stimulus and response component kind codes for dynamic question-type definitions",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"questions"
|
|
||||||
],
|
|
||||||
"summary": "Question-type builder component catalog",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/questions/search": {
|
"/api/v1/questions/search": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Search questions by text",
|
"description": "Search questions by text",
|
||||||
|
|
@ -4721,46 +4011,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/questions/validate-question-type-definition": {
|
|
||||||
"post": {
|
|
||||||
"description": "Validates selected stimulus and response component kinds for temporary question-type definitions",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"questions"
|
|
||||||
],
|
|
||||||
"summary": "Validate dynamic question-type definition",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Stimulus and response component kinds",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handlers.validateQuestionTypeDefinitionReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/questions/{id}": {
|
"/api/v1/questions/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a question with its options/short answers",
|
"description": "Returns a question with its options/short answers",
|
||||||
|
|
@ -8916,107 +8166,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.CreateExamPrepCatalogCourseInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateExamPrepLessonInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"title"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"video_url": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateExamPrepModuleInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateExamPrepPracticeInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"question_set_id",
|
|
||||||
"title"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"persona_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"question_set_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"quick_tips": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"story_description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"story_image": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateExamPrepUnitInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateLessonInput": {
|
"domain.CreateLessonInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -9771,63 +8920,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.UpdateExamPrepCatalogCourseInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"sort_order": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.UpdateExamPrepPracticeInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"persona_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"question_set_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"quick_tips": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"story_description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"story_image": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.UpdateExamPrepUnitInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"sort_order": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.UpdateKnowledgeLevelReq": {
|
"domain.UpdateKnowledgeLevelReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -10954,14 +10046,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers.refreshFileURLReq": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"reference": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.refreshToken": {
|
"handlers.refreshToken": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11237,23 +10321,6 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers.validateQuestionTypeDefinitionReq": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"response_component_kinds": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stimulus_component_kinds": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.verifyOTPReq": {
|
"handlers.verifyOTPReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -943,663 +943,6 @@
|
||||||
"responses": {}
|
"responses": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/exam-prep/catalog-courses": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "List exam-prep catalog courses",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 20,
|
|
||||||
"description": "Page size",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"description": "Offset",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"description": "Top-level exam track (DET, IELTS, …) in schema exam_prep — separate from LMS programs/courses",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Create exam-prep catalog course",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Catalog course",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.CreateExamPrepCatalogCourseInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Created",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/catalog-courses/reorder": {
|
|
||||||
"put": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Reorder all exam-prep catalog courses",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "ordered_ids: every catalog course id exactly once",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ReorderIDsRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/catalog-courses/{catalogCourseId}/units": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "List exam-prep units for a catalog course",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "catalogCourseId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 20,
|
|
||||||
"description": "Page size",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"description": "Offset",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"description": "Unit under a catalog course (e.g. chapter title)",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Create exam-prep unit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "catalogCourseId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Unit",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.CreateExamPrepUnitInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Created",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/catalog-courses/{catalogCourseId}/units/reorder": {
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Reorder units within a catalog course",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "catalogCourseId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "ordered_ids: every unit id in this catalog course, new order",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ReorderIDsRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/catalog-courses/{id}": {
|
|
||||||
"get": {
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Get exam-prep catalog course by ID",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Update exam-prep catalog course",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Fields to update",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.UpdateExamPrepCatalogCourseInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Delete exam-prep catalog course",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Catalog course ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/lessons/{id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Get exam-prep lesson by ID",
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Update exam-prep lesson",
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Delete exam-prep lesson",
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/lessons/{lessonId}/practices": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "List exam-prep practices for a lesson",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep lesson ID",
|
|
||||||
"name": "lessonId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 20,
|
|
||||||
"description": "Page size",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"description": "Offset",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Create exam-prep practice (under a lesson; uses shared question_sets)",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep lesson ID (unit_module_lessons.id)",
|
|
||||||
"name": "lessonId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Practice",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.CreateExamPrepPracticeInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/modules/{id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Get exam-prep module by ID",
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Update exam-prep module",
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Delete exam-prep module",
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/modules/{moduleId}/lessons": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "List exam-prep lessons for a unit module",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep unit module ID",
|
|
||||||
"name": "moduleId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 20,
|
|
||||||
"description": "Page size",
|
|
||||||
"name": "limit",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"default": 0,
|
|
||||||
"description": "Offset",
|
|
||||||
"name": "offset",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Create exam-prep lesson (under a unit module)",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep unit module ID",
|
|
||||||
"name": "moduleId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Lesson",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.CreateExamPrepLessonInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/modules/{moduleId}/lessons/reorder": {
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Reorder lessons within an exam-prep unit module",
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/practices/{id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Get exam-prep practice by ID",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep practice ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Update exam-prep practice",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep practice ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Fields to update",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.UpdateExamPrepPracticeInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Delete exam-prep practice",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Exam prep practice ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/units/{id}": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Get exam-prep unit by ID",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Update exam-prep unit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Fields to update",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.UpdateExamPrepUnitInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Delete exam-prep unit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "id",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/units/{unitId}/modules": {
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "List exam-prep modules for a unit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "unitId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Create exam-prep module",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "unitId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Module",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.CreateExamPrepModuleInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/exam-prep/units/{unitId}/modules/reorder": {
|
|
||||||
"put": {
|
|
||||||
"tags": [
|
|
||||||
"exam-prep"
|
|
||||||
],
|
|
||||||
"summary": "Reorder modules within a unit",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Unit ID",
|
|
||||||
"name": "unitId",
|
|
||||||
"in": "path",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "ordered_ids",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ReorderIDsRequest"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/files/audio": {
|
"/api/v1/files/audio": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
|
@ -1628,39 +971,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/files/refresh-url": {
|
|
||||||
"post": {
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"files"
|
|
||||||
],
|
|
||||||
"summary": "Refresh presigned URL for a file",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "reference (object key, minio://..., or existing presigned URL)",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handlers.refreshFileURLReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/files/upload": {
|
"/api/v1/files/upload": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
|
@ -4638,26 +3948,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/questions/component-catalog": {
|
|
||||||
"get": {
|
|
||||||
"description": "Valid stimulus and response component kind codes for dynamic question-type definitions",
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"questions"
|
|
||||||
],
|
|
||||||
"summary": "Question-type builder component catalog",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/questions/search": {
|
"/api/v1/questions/search": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Search questions by text",
|
"description": "Search questions by text",
|
||||||
|
|
@ -4713,46 +4003,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/questions/validate-question-type-definition": {
|
|
||||||
"post": {
|
|
||||||
"description": "Validates selected stimulus and response component kinds for temporary question-type definitions",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"questions"
|
|
||||||
],
|
|
||||||
"summary": "Validate dynamic question-type definition",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"description": "Stimulus and response component kinds",
|
|
||||||
"name": "body",
|
|
||||||
"in": "body",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handlers.validateQuestionTypeDefinitionReq"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.Response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/domain.ErrorResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/questions/{id}": {
|
"/api/v1/questions/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a question with its options/short answers",
|
"description": "Returns a question with its options/short answers",
|
||||||
|
|
@ -8908,107 +8158,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.CreateExamPrepCatalogCourseInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateExamPrepLessonInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"title"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"video_url": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateExamPrepModuleInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"icon": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateExamPrepPracticeInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"question_set_id",
|
|
||||||
"title"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"persona_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"question_set_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"quick_tips": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"story_description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"story_image": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateExamPrepUnitInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.CreateLessonInput": {
|
"domain.CreateLessonInput": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -9763,63 +8912,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain.UpdateExamPrepCatalogCourseInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"sort_order": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.UpdateExamPrepPracticeInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"persona_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"question_set_id": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"quick_tips": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"story_description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"story_image": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.UpdateExamPrepUnitInput": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"description": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"sort_order": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"thumbnail": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domain.UpdateKnowledgeLevelReq": {
|
"domain.UpdateKnowledgeLevelReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
@ -10946,14 +10038,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers.refreshFileURLReq": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"reference": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.refreshToken": {
|
"handlers.refreshToken": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -11229,23 +10313,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers.validateQuestionTypeDefinitionReq": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"response_component_kinds": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stimulus_component_kinds": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handlers.verifyOTPReq": {
|
"handlers.verifyOTPReq": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
||||||
|
|
@ -28,72 +28,6 @@ definitions:
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
type: object
|
type: object
|
||||||
domain.CreateExamPrepCatalogCourseInput:
|
|
||||||
properties:
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
thumbnail:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
type: object
|
|
||||||
domain.CreateExamPrepLessonInput:
|
|
||||||
properties:
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
thumbnail:
|
|
||||||
type: string
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
video_url:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- title
|
|
||||||
type: object
|
|
||||||
domain.CreateExamPrepModuleInput:
|
|
||||||
properties:
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
icon:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
thumbnail:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
type: object
|
|
||||||
domain.CreateExamPrepPracticeInput:
|
|
||||||
properties:
|
|
||||||
persona_id:
|
|
||||||
type: integer
|
|
||||||
question_set_id:
|
|
||||||
type: integer
|
|
||||||
quick_tips:
|
|
||||||
type: string
|
|
||||||
story_description:
|
|
||||||
type: string
|
|
||||||
story_image:
|
|
||||||
type: string
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- question_set_id
|
|
||||||
- title
|
|
||||||
type: object
|
|
||||||
domain.CreateExamPrepUnitInput:
|
|
||||||
properties:
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
thumbnail:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- name
|
|
||||||
type: object
|
|
||||||
domain.CreateLessonInput:
|
domain.CreateLessonInput:
|
||||||
properties:
|
properties:
|
||||||
description:
|
description:
|
||||||
|
|
@ -608,43 +542,6 @@ definitions:
|
||||||
thumbnail:
|
thumbnail:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
domain.UpdateExamPrepCatalogCourseInput:
|
|
||||||
properties:
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
sort_order:
|
|
||||||
type: integer
|
|
||||||
thumbnail:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
domain.UpdateExamPrepPracticeInput:
|
|
||||||
properties:
|
|
||||||
persona_id:
|
|
||||||
type: integer
|
|
||||||
question_set_id:
|
|
||||||
type: integer
|
|
||||||
quick_tips:
|
|
||||||
type: string
|
|
||||||
story_description:
|
|
||||||
type: string
|
|
||||||
story_image:
|
|
||||||
type: string
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
domain.UpdateExamPrepUnitInput:
|
|
||||||
properties:
|
|
||||||
description:
|
|
||||||
type: string
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
sort_order:
|
|
||||||
type: integer
|
|
||||||
thumbnail:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
domain.UpdateKnowledgeLevelReq:
|
domain.UpdateKnowledgeLevelReq:
|
||||||
properties:
|
properties:
|
||||||
knowledge_level:
|
knowledge_level:
|
||||||
|
|
@ -1404,11 +1301,6 @@ definitions:
|
||||||
required:
|
required:
|
||||||
- option_text
|
- option_text
|
||||||
type: object
|
type: object
|
||||||
handlers.refreshFileURLReq:
|
|
||||||
properties:
|
|
||||||
reference:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
handlers.refreshToken:
|
handlers.refreshToken:
|
||||||
properties:
|
properties:
|
||||||
access_token:
|
access_token:
|
||||||
|
|
@ -1594,17 +1486,6 @@ definitions:
|
||||||
title:
|
title:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
handlers.validateQuestionTypeDefinitionReq:
|
|
||||||
properties:
|
|
||||||
response_component_kinds:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
stimulus_component_kinds:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
type: object
|
|
||||||
handlers.verifyOTPReq:
|
handlers.verifyOTPReq:
|
||||||
properties:
|
properties:
|
||||||
otp:
|
otp:
|
||||||
|
|
@ -2640,447 +2521,6 @@ paths:
|
||||||
responses: {}
|
responses: {}
|
||||||
tags:
|
tags:
|
||||||
- practices
|
- practices
|
||||||
/api/v1/exam-prep/catalog-courses:
|
|
||||||
get:
|
|
||||||
parameters:
|
|
||||||
- default: 20
|
|
||||||
description: Page size
|
|
||||||
in: query
|
|
||||||
name: limit
|
|
||||||
type: integer
|
|
||||||
- default: 0
|
|
||||||
description: Offset
|
|
||||||
in: query
|
|
||||||
name: offset
|
|
||||||
type: integer
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: List exam-prep catalog courses
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Top-level exam track (DET, IELTS, …) in schema exam_prep — separate
|
|
||||||
from LMS programs/courses
|
|
||||||
parameters:
|
|
||||||
- description: Catalog course
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.CreateExamPrepCatalogCourseInput'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"201":
|
|
||||||
description: Created
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
summary: Create exam-prep catalog course
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/catalog-courses/{catalogCourseId}/units:
|
|
||||||
get:
|
|
||||||
parameters:
|
|
||||||
- description: Catalog course ID
|
|
||||||
in: path
|
|
||||||
name: catalogCourseId
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- default: 20
|
|
||||||
description: Page size
|
|
||||||
in: query
|
|
||||||
name: limit
|
|
||||||
type: integer
|
|
||||||
- default: 0
|
|
||||||
description: Offset
|
|
||||||
in: query
|
|
||||||
name: offset
|
|
||||||
type: integer
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: List exam-prep units for a catalog course
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Unit under a catalog course (e.g. chapter title)
|
|
||||||
parameters:
|
|
||||||
- description: Catalog course ID
|
|
||||||
in: path
|
|
||||||
name: catalogCourseId
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: Unit
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.CreateExamPrepUnitInput'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"201":
|
|
||||||
description: Created
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: Create exam-prep unit
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/catalog-courses/{catalogCourseId}/units/reorder:
|
|
||||||
put:
|
|
||||||
parameters:
|
|
||||||
- description: Catalog course ID
|
|
||||||
in: path
|
|
||||||
name: catalogCourseId
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: 'ordered_ids: every unit id in this catalog course, new order'
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ReorderIDsRequest'
|
|
||||||
responses: {}
|
|
||||||
summary: Reorder units within a catalog course
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/catalog-courses/{id}:
|
|
||||||
delete:
|
|
||||||
parameters:
|
|
||||||
- description: Catalog course ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: Delete exam-prep catalog course
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
get:
|
|
||||||
parameters:
|
|
||||||
- description: Catalog course ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: Get exam-prep catalog course by ID
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
put:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
parameters:
|
|
||||||
- description: Catalog course ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: Fields to update
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.UpdateExamPrepCatalogCourseInput'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: Update exam-prep catalog course
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/catalog-courses/reorder:
|
|
||||||
put:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
parameters:
|
|
||||||
- description: 'ordered_ids: every catalog course id exactly once'
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ReorderIDsRequest'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: Reorder all exam-prep catalog courses
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/lessons/{id}:
|
|
||||||
delete:
|
|
||||||
responses: {}
|
|
||||||
summary: Delete exam-prep lesson
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
get:
|
|
||||||
responses: {}
|
|
||||||
summary: Get exam-prep lesson by ID
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
put:
|
|
||||||
responses: {}
|
|
||||||
summary: Update exam-prep lesson
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/lessons/{lessonId}/practices:
|
|
||||||
get:
|
|
||||||
parameters:
|
|
||||||
- description: Exam prep lesson ID
|
|
||||||
in: path
|
|
||||||
name: lessonId
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- default: 20
|
|
||||||
description: Page size
|
|
||||||
in: query
|
|
||||||
name: limit
|
|
||||||
type: integer
|
|
||||||
- default: 0
|
|
||||||
description: Offset
|
|
||||||
in: query
|
|
||||||
name: offset
|
|
||||||
type: integer
|
|
||||||
responses: {}
|
|
||||||
summary: List exam-prep practices for a lesson
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
post:
|
|
||||||
parameters:
|
|
||||||
- description: Exam prep lesson ID (unit_module_lessons.id)
|
|
||||||
in: path
|
|
||||||
name: lessonId
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: Practice
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.CreateExamPrepPracticeInput'
|
|
||||||
responses: {}
|
|
||||||
summary: Create exam-prep practice (under a lesson; uses shared question_sets)
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/modules/{id}:
|
|
||||||
delete:
|
|
||||||
responses: {}
|
|
||||||
summary: Delete exam-prep module
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
get:
|
|
||||||
responses: {}
|
|
||||||
summary: Get exam-prep module by ID
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
put:
|
|
||||||
responses: {}
|
|
||||||
summary: Update exam-prep module
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/modules/{moduleId}/lessons:
|
|
||||||
get:
|
|
||||||
parameters:
|
|
||||||
- description: Exam prep unit module ID
|
|
||||||
in: path
|
|
||||||
name: moduleId
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- default: 20
|
|
||||||
description: Page size
|
|
||||||
in: query
|
|
||||||
name: limit
|
|
||||||
type: integer
|
|
||||||
- default: 0
|
|
||||||
description: Offset
|
|
||||||
in: query
|
|
||||||
name: offset
|
|
||||||
type: integer
|
|
||||||
responses: {}
|
|
||||||
summary: List exam-prep lessons for a unit module
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
post:
|
|
||||||
parameters:
|
|
||||||
- description: Exam prep unit module ID
|
|
||||||
in: path
|
|
||||||
name: moduleId
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: Lesson
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.CreateExamPrepLessonInput'
|
|
||||||
responses: {}
|
|
||||||
summary: Create exam-prep lesson (under a unit module)
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/modules/{moduleId}/lessons/reorder:
|
|
||||||
put:
|
|
||||||
responses: {}
|
|
||||||
summary: Reorder lessons within an exam-prep unit module
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/practices/{id}:
|
|
||||||
delete:
|
|
||||||
parameters:
|
|
||||||
- description: Exam prep practice ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
responses: {}
|
|
||||||
summary: Delete exam-prep practice
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
get:
|
|
||||||
parameters:
|
|
||||||
- description: Exam prep practice ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
responses: {}
|
|
||||||
summary: Get exam-prep practice by ID
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
put:
|
|
||||||
parameters:
|
|
||||||
- description: Exam prep practice ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: Fields to update
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.UpdateExamPrepPracticeInput'
|
|
||||||
responses: {}
|
|
||||||
summary: Update exam-prep practice
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/units/{id}:
|
|
||||||
delete:
|
|
||||||
parameters:
|
|
||||||
- description: Unit ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
responses: {}
|
|
||||||
summary: Delete exam-prep unit
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
get:
|
|
||||||
parameters:
|
|
||||||
- description: Unit ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
responses: {}
|
|
||||||
summary: Get exam-prep unit by ID
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
put:
|
|
||||||
parameters:
|
|
||||||
- description: Unit ID
|
|
||||||
in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: Fields to update
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.UpdateExamPrepUnitInput'
|
|
||||||
responses: {}
|
|
||||||
summary: Update exam-prep unit
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/units/{unitId}/modules:
|
|
||||||
get:
|
|
||||||
parameters:
|
|
||||||
- description: Unit ID
|
|
||||||
in: path
|
|
||||||
name: unitId
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
responses: {}
|
|
||||||
summary: List exam-prep modules for a unit
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
post:
|
|
||||||
parameters:
|
|
||||||
- description: Unit ID
|
|
||||||
in: path
|
|
||||||
name: unitId
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: Module
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.CreateExamPrepModuleInput'
|
|
||||||
responses: {}
|
|
||||||
summary: Create exam-prep module
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/exam-prep/units/{unitId}/modules/reorder:
|
|
||||||
put:
|
|
||||||
parameters:
|
|
||||||
- description: Unit ID
|
|
||||||
in: path
|
|
||||||
name: unitId
|
|
||||||
required: true
|
|
||||||
type: integer
|
|
||||||
- description: ordered_ids
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ReorderIDsRequest'
|
|
||||||
responses: {}
|
|
||||||
summary: Reorder modules within a unit
|
|
||||||
tags:
|
|
||||||
- exam-prep
|
|
||||||
/api/v1/files/audio:
|
/api/v1/files/audio:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -3099,27 +2539,6 @@ paths:
|
||||||
summary: Upload an audio file
|
summary: Upload an audio file
|
||||||
tags:
|
tags:
|
||||||
- files
|
- files
|
||||||
/api/v1/files/refresh-url:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
parameters:
|
|
||||||
- description: reference (object key, minio://..., or existing presigned URL)
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handlers.refreshFileURLReq'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: Refresh presigned URL for a file
|
|
||||||
tags:
|
|
||||||
- files
|
|
||||||
/api/v1/files/upload:
|
/api/v1/files/upload:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
@ -5166,20 +4585,6 @@ paths:
|
||||||
summary: Submit audio answer for a question
|
summary: Submit audio answer for a question
|
||||||
tags:
|
tags:
|
||||||
- questions
|
- questions
|
||||||
/api/v1/questions/component-catalog:
|
|
||||||
get:
|
|
||||||
description: Valid stimulus and response component kind codes for dynamic question-type
|
|
||||||
definitions
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
summary: Question-type builder component catalog
|
|
||||||
tags:
|
|
||||||
- questions
|
|
||||||
/api/v1/questions/search:
|
/api/v1/questions/search:
|
||||||
get:
|
get:
|
||||||
description: Search questions by text
|
description: Search questions by text
|
||||||
|
|
@ -5217,33 +4622,6 @@ paths:
|
||||||
summary: Search questions
|
summary: Search questions
|
||||||
tags:
|
tags:
|
||||||
- questions
|
- questions
|
||||||
/api/v1/questions/validate-question-type-definition:
|
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: Validates selected stimulus and response component kinds for temporary
|
|
||||||
question-type definitions
|
|
||||||
parameters:
|
|
||||||
- description: Stimulus and response component kinds
|
|
||||||
in: body
|
|
||||||
name: body
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handlers.validateQuestionTypeDefinitionReq'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.Response'
|
|
||||||
"400":
|
|
||||||
description: Bad Request
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/domain.ErrorResponse'
|
|
||||||
summary: Validate dynamic question-type definition
|
|
||||||
tags:
|
|
||||||
- questions
|
|
||||||
/api/v1/ratings:
|
/api/v1/ratings:
|
||||||
get:
|
get:
|
||||||
description: Returns paginated ratings for a specific target
|
description: Returns paginated ratings for a specific target
|
||||||
|
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.30.0
|
|
||||||
// source: exam_prep_catalog_courses.sql
|
|
||||||
|
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ExamPrepCreateCatalogCourse = `-- name: ExamPrepCreateCatalogCourse :one
|
|
||||||
INSERT INTO exam_prep.catalog_courses (name, description, thumbnail, sort_order)
|
|
||||||
SELECT
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
coalesce((
|
|
||||||
SELECT
|
|
||||||
max(c.sort_order)
|
|
||||||
FROM exam_prep.catalog_courses AS c), 0) + 1
|
|
||||||
RETURNING
|
|
||||||
id, name, description, thumbnail, sort_order, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepCreateCatalogCourseParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepCreateCatalogCourse(ctx context.Context, arg ExamPrepCreateCatalogCourseParams) (ExamPrepCatalogCourse, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepCreateCatalogCourse, arg.Name, arg.Description, arg.Thumbnail)
|
|
||||||
var i ExamPrepCatalogCourse
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepDeleteCatalogCourse = `-- name: ExamPrepDeleteCatalogCourse :exec
|
|
||||||
DELETE FROM exam_prep.catalog_courses
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepDeleteCatalogCourse(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, ExamPrepDeleteCatalogCourse, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepGetCatalogCourseByID = `-- name: ExamPrepGetCatalogCourseByID :one
|
|
||||||
SELECT id, name, description, thumbnail, sort_order, created_at, updated_at
|
|
||||||
FROM exam_prep.catalog_courses
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepGetCatalogCourseByID(ctx context.Context, id int64) (ExamPrepCatalogCourse, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepGetCatalogCourseByID, id)
|
|
||||||
var i ExamPrepCatalogCourse
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepListAllCatalogCourseIDs = `-- name: ExamPrepListAllCatalogCourseIDs :many
|
|
||||||
SELECT
|
|
||||||
id
|
|
||||||
FROM exam_prep.catalog_courses
|
|
||||||
ORDER BY id
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepListAllCatalogCourseIDs(ctx context.Context) ([]int64, error) {
|
|
||||||
rows, err := q.db.Query(ctx, ExamPrepListAllCatalogCourseIDs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []int64
|
|
||||||
for rows.Next() {
|
|
||||||
var id int64
|
|
||||||
if err := rows.Scan(&id); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, id)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepListCatalogCourses = `-- name: ExamPrepListCatalogCourses :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
c.id,
|
|
||||||
c.name,
|
|
||||||
c.description,
|
|
||||||
c.thumbnail,
|
|
||||||
c.sort_order,
|
|
||||||
c.created_at,
|
|
||||||
c.updated_at
|
|
||||||
FROM exam_prep.catalog_courses c
|
|
||||||
ORDER BY c.sort_order ASC, c.id ASC
|
|
||||||
LIMIT $1 OFFSET $2
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepListCatalogCoursesParams struct {
|
|
||||||
Limit int32 `json:"limit"`
|
|
||||||
Offset int32 `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExamPrepListCatalogCoursesRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepListCatalogCourses(ctx context.Context, arg ExamPrepListCatalogCoursesParams) ([]ExamPrepListCatalogCoursesRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, ExamPrepListCatalogCourses, arg.Limit, arg.Offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []ExamPrepListCatalogCoursesRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i ExamPrepListCatalogCoursesRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepUpdateCatalogCourse = `-- name: ExamPrepUpdateCatalogCourse :one
|
|
||||||
UPDATE exam_prep.catalog_courses
|
|
||||||
SET
|
|
||||||
name = coalesce($1::varchar, name),
|
|
||||||
description = coalesce($2::text, description),
|
|
||||||
thumbnail = coalesce($3::text, thumbnail),
|
|
||||||
sort_order = coalesce($4::int, sort_order),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $5
|
|
||||||
RETURNING
|
|
||||||
id, name, description, thumbnail, sort_order, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepUpdateCatalogCourseParams struct {
|
|
||||||
Name pgtype.Text `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
SortOrder pgtype.Int4 `json:"sort_order"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepUpdateCatalogCourse(ctx context.Context, arg ExamPrepUpdateCatalogCourseParams) (ExamPrepCatalogCourse, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepUpdateCatalogCourse,
|
|
||||||
arg.Name,
|
|
||||||
arg.Description,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.SortOrder,
|
|
||||||
arg.ID,
|
|
||||||
)
|
|
||||||
var i ExamPrepCatalogCourse
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.30.0
|
|
||||||
// source: exam_prep_lesson_practices.sql
|
|
||||||
|
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ExamPrepCreateLessonPractice = `-- name: ExamPrepCreateLessonPractice :one
|
|
||||||
INSERT INTO exam_prep.lesson_practices (
|
|
||||||
unit_module_lesson_id,
|
|
||||||
title,
|
|
||||||
story_description,
|
|
||||||
story_image,
|
|
||||||
persona_id,
|
|
||||||
question_set_id,
|
|
||||||
quick_tips
|
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
||||||
RETURNING id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepCreateLessonPracticeParams struct {
|
|
||||||
UnitModuleLessonID int64 `json:"unit_module_lesson_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
StoryDescription pgtype.Text `json:"story_description"`
|
|
||||||
StoryImage pgtype.Text `json:"story_image"`
|
|
||||||
PersonaID pgtype.Int8 `json:"persona_id"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
QuickTips pgtype.Text `json:"quick_tips"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepCreateLessonPractice(ctx context.Context, arg ExamPrepCreateLessonPracticeParams) (ExamPrepLessonPractice, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepCreateLessonPractice,
|
|
||||||
arg.UnitModuleLessonID,
|
|
||||||
arg.Title,
|
|
||||||
arg.StoryDescription,
|
|
||||||
arg.StoryImage,
|
|
||||||
arg.PersonaID,
|
|
||||||
arg.QuestionSetID,
|
|
||||||
arg.QuickTips,
|
|
||||||
)
|
|
||||||
var i ExamPrepLessonPractice
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitModuleLessonID,
|
|
||||||
&i.Title,
|
|
||||||
&i.StoryDescription,
|
|
||||||
&i.StoryImage,
|
|
||||||
&i.PersonaID,
|
|
||||||
&i.QuestionSetID,
|
|
||||||
&i.QuickTips,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepDeleteLessonPractice = `-- name: ExamPrepDeleteLessonPractice :exec
|
|
||||||
DELETE FROM exam_prep.lesson_practices
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepDeleteLessonPractice(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, ExamPrepDeleteLessonPractice, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepGetLessonPracticeByID = `-- name: ExamPrepGetLessonPracticeByID :one
|
|
||||||
SELECT id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
|
||||||
FROM exam_prep.lesson_practices
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepGetLessonPracticeByID(ctx context.Context, id int64) (ExamPrepLessonPractice, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepGetLessonPracticeByID, id)
|
|
||||||
var i ExamPrepLessonPractice
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitModuleLessonID,
|
|
||||||
&i.Title,
|
|
||||||
&i.StoryDescription,
|
|
||||||
&i.StoryImage,
|
|
||||||
&i.PersonaID,
|
|
||||||
&i.QuestionSetID,
|
|
||||||
&i.QuickTips,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepListLessonPracticesByLessonID = `-- name: ExamPrepListLessonPracticesByLessonID :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
p.id,
|
|
||||||
p.unit_module_lesson_id,
|
|
||||||
p.title,
|
|
||||||
p.story_description,
|
|
||||||
p.story_image,
|
|
||||||
p.persona_id,
|
|
||||||
p.question_set_id,
|
|
||||||
p.quick_tips,
|
|
||||||
p.created_at,
|
|
||||||
p.updated_at
|
|
||||||
FROM exam_prep.lesson_practices p
|
|
||||||
WHERE p.unit_module_lesson_id = $1
|
|
||||||
ORDER BY p.created_at DESC
|
|
||||||
LIMIT $2
|
|
||||||
OFFSET $3
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepListLessonPracticesByLessonIDParams struct {
|
|
||||||
UnitModuleLessonID int64 `json:"unit_module_lesson_id"`
|
|
||||||
Limit int32 `json:"limit"`
|
|
||||||
Offset int32 `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExamPrepListLessonPracticesByLessonIDRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UnitModuleLessonID int64 `json:"unit_module_lesson_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
StoryDescription pgtype.Text `json:"story_description"`
|
|
||||||
StoryImage pgtype.Text `json:"story_image"`
|
|
||||||
PersonaID pgtype.Int8 `json:"persona_id"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
QuickTips pgtype.Text `json:"quick_tips"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepListLessonPracticesByLessonID(ctx context.Context, arg ExamPrepListLessonPracticesByLessonIDParams) ([]ExamPrepListLessonPracticesByLessonIDRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, ExamPrepListLessonPracticesByLessonID, arg.UnitModuleLessonID, arg.Limit, arg.Offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []ExamPrepListLessonPracticesByLessonIDRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i ExamPrepListLessonPracticesByLessonIDRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitModuleLessonID,
|
|
||||||
&i.Title,
|
|
||||||
&i.StoryDescription,
|
|
||||||
&i.StoryImage,
|
|
||||||
&i.PersonaID,
|
|
||||||
&i.QuestionSetID,
|
|
||||||
&i.QuickTips,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepUpdateLessonPractice = `-- name: ExamPrepUpdateLessonPractice :one
|
|
||||||
UPDATE exam_prep.lesson_practices
|
|
||||||
SET
|
|
||||||
title = coalesce($1::varchar, title),
|
|
||||||
story_description = coalesce($2::text, story_description),
|
|
||||||
story_image = coalesce($3::text, story_image),
|
|
||||||
persona_id = coalesce($4::bigint, persona_id),
|
|
||||||
question_set_id = coalesce($5::bigint, question_set_id),
|
|
||||||
quick_tips = coalesce($6::text, quick_tips),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $7
|
|
||||||
RETURNING id, unit_module_lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepUpdateLessonPracticeParams struct {
|
|
||||||
Title pgtype.Text `json:"title"`
|
|
||||||
StoryDescription pgtype.Text `json:"story_description"`
|
|
||||||
StoryImage pgtype.Text `json:"story_image"`
|
|
||||||
PersonaID pgtype.Int8 `json:"persona_id"`
|
|
||||||
QuestionSetID pgtype.Int8 `json:"question_set_id"`
|
|
||||||
QuickTips pgtype.Text `json:"quick_tips"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepUpdateLessonPractice(ctx context.Context, arg ExamPrepUpdateLessonPracticeParams) (ExamPrepLessonPractice, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepUpdateLessonPractice,
|
|
||||||
arg.Title,
|
|
||||||
arg.StoryDescription,
|
|
||||||
arg.StoryImage,
|
|
||||||
arg.PersonaID,
|
|
||||||
arg.QuestionSetID,
|
|
||||||
arg.QuickTips,
|
|
||||||
arg.ID,
|
|
||||||
)
|
|
||||||
var i ExamPrepLessonPractice
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitModuleLessonID,
|
|
||||||
&i.Title,
|
|
||||||
&i.StoryDescription,
|
|
||||||
&i.StoryImage,
|
|
||||||
&i.PersonaID,
|
|
||||||
&i.QuestionSetID,
|
|
||||||
&i.QuickTips,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.30.0
|
|
||||||
// source: exam_prep_unit_module_lessons.sql
|
|
||||||
|
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ExamPrepCreateUnitModuleLesson = `-- name: ExamPrepCreateUnitModuleLesson :one
|
|
||||||
INSERT INTO exam_prep.unit_module_lessons (unit_module_id, title, video_url, thumbnail, description, sort_order)
|
|
||||||
SELECT
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
coalesce((
|
|
||||||
SELECT
|
|
||||||
max(l.sort_order)
|
|
||||||
FROM exam_prep.unit_module_lessons l
|
|
||||||
WHERE
|
|
||||||
l.unit_module_id = $1), 0) + 1
|
|
||||||
RETURNING
|
|
||||||
id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepCreateUnitModuleLessonParams struct {
|
|
||||||
UnitModuleID int64 `json:"unit_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
VideoUrl pgtype.Text `json:"video_url"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepCreateUnitModuleLesson(ctx context.Context, arg ExamPrepCreateUnitModuleLessonParams) (ExamPrepUnitModuleLesson, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepCreateUnitModuleLesson,
|
|
||||||
arg.UnitModuleID,
|
|
||||||
arg.Title,
|
|
||||||
arg.VideoUrl,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.Description,
|
|
||||||
)
|
|
||||||
var i ExamPrepUnitModuleLesson
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.VideoUrl,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.Description,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepDeleteUnitModuleLesson = `-- name: ExamPrepDeleteUnitModuleLesson :exec
|
|
||||||
DELETE FROM exam_prep.unit_module_lessons
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepDeleteUnitModuleLesson(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, ExamPrepDeleteUnitModuleLesson, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepGetUnitModuleLessonByID = `-- name: ExamPrepGetUnitModuleLessonByID :one
|
|
||||||
SELECT id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at
|
|
||||||
FROM exam_prep.unit_module_lessons
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepGetUnitModuleLessonByID(ctx context.Context, id int64) (ExamPrepUnitModuleLesson, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepGetUnitModuleLessonByID, id)
|
|
||||||
var i ExamPrepUnitModuleLesson
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.VideoUrl,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.Description,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepListUnitModuleLessonIDsByUnitModule = `-- name: ExamPrepListUnitModuleLessonIDsByUnitModule :many
|
|
||||||
SELECT
|
|
||||||
l.id
|
|
||||||
FROM exam_prep.unit_module_lessons l
|
|
||||||
WHERE
|
|
||||||
l.unit_module_id = $1
|
|
||||||
ORDER BY
|
|
||||||
l.id
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepListUnitModuleLessonIDsByUnitModule(ctx context.Context, unitModuleID int64) ([]int64, error) {
|
|
||||||
rows, err := q.db.Query(ctx, ExamPrepListUnitModuleLessonIDsByUnitModule, unitModuleID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []int64
|
|
||||||
for rows.Next() {
|
|
||||||
var id int64
|
|
||||||
if err := rows.Scan(&id); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, id)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepListUnitModuleLessonsByUnitModuleID = `-- name: ExamPrepListUnitModuleLessonsByUnitModuleID :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
l.id,
|
|
||||||
l.unit_module_id,
|
|
||||||
l.title,
|
|
||||||
l.video_url,
|
|
||||||
l.thumbnail,
|
|
||||||
l.description,
|
|
||||||
l.sort_order,
|
|
||||||
l.created_at,
|
|
||||||
l.updated_at
|
|
||||||
FROM exam_prep.unit_module_lessons l
|
|
||||||
WHERE
|
|
||||||
l.unit_module_id = $1
|
|
||||||
ORDER BY
|
|
||||||
l.sort_order ASC,
|
|
||||||
l.id ASC
|
|
||||||
LIMIT $2
|
|
||||||
OFFSET $3
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepListUnitModuleLessonsByUnitModuleIDParams struct {
|
|
||||||
UnitModuleID int64 `json:"unit_module_id"`
|
|
||||||
Limit int32 `json:"limit"`
|
|
||||||
Offset int32 `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExamPrepListUnitModuleLessonsByUnitModuleIDRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UnitModuleID int64 `json:"unit_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
VideoUrl pgtype.Text `json:"video_url"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepListUnitModuleLessonsByUnitModuleID(ctx context.Context, arg ExamPrepListUnitModuleLessonsByUnitModuleIDParams) ([]ExamPrepListUnitModuleLessonsByUnitModuleIDRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, ExamPrepListUnitModuleLessonsByUnitModuleID, arg.UnitModuleID, arg.Limit, arg.Offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []ExamPrepListUnitModuleLessonsByUnitModuleIDRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i ExamPrepListUnitModuleLessonsByUnitModuleIDRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.VideoUrl,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.Description,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepUpdateUnitModuleLesson = `-- name: ExamPrepUpdateUnitModuleLesson :one
|
|
||||||
UPDATE exam_prep.unit_module_lessons
|
|
||||||
SET
|
|
||||||
title = coalesce($1::varchar, title),
|
|
||||||
video_url = coalesce($2::text, video_url),
|
|
||||||
thumbnail = coalesce($3::text, thumbnail),
|
|
||||||
description = coalesce($4::text, description),
|
|
||||||
sort_order = coalesce($5::int, sort_order),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $6
|
|
||||||
RETURNING
|
|
||||||
id, unit_module_id, title, video_url, thumbnail, description, sort_order, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepUpdateUnitModuleLessonParams struct {
|
|
||||||
Title pgtype.Text `json:"title"`
|
|
||||||
VideoUrl pgtype.Text `json:"video_url"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
SortOrder pgtype.Int4 `json:"sort_order"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepUpdateUnitModuleLesson(ctx context.Context, arg ExamPrepUpdateUnitModuleLessonParams) (ExamPrepUnitModuleLesson, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepUpdateUnitModuleLesson,
|
|
||||||
arg.Title,
|
|
||||||
arg.VideoUrl,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.Description,
|
|
||||||
arg.SortOrder,
|
|
||||||
arg.ID,
|
|
||||||
)
|
|
||||||
var i ExamPrepUnitModuleLesson
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.VideoUrl,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.Description,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.30.0
|
|
||||||
// source: exam_prep_unit_modules.sql
|
|
||||||
|
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ExamPrepCreateUnitModule = `-- name: ExamPrepCreateUnitModule :one
|
|
||||||
INSERT INTO exam_prep.unit_modules (unit_id, name, description, thumbnail, icon, sort_order)
|
|
||||||
SELECT
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
coalesce((
|
|
||||||
SELECT
|
|
||||||
max(m.sort_order)
|
|
||||||
FROM exam_prep.unit_modules m
|
|
||||||
WHERE
|
|
||||||
m.unit_id = $1), 0) + 1
|
|
||||||
RETURNING
|
|
||||||
id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepCreateUnitModuleParams struct {
|
|
||||||
UnitID int64 `json:"unit_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Icon pgtype.Text `json:"icon"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepCreateUnitModule(ctx context.Context, arg ExamPrepCreateUnitModuleParams) (ExamPrepUnitModule, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepCreateUnitModule,
|
|
||||||
arg.UnitID,
|
|
||||||
arg.Name,
|
|
||||||
arg.Description,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.Icon,
|
|
||||||
)
|
|
||||||
var i ExamPrepUnitModule
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.Icon,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepDeleteUnitModule = `-- name: ExamPrepDeleteUnitModule :exec
|
|
||||||
DELETE FROM exam_prep.unit_modules
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepDeleteUnitModule(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, ExamPrepDeleteUnitModule, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepGetUnitModuleByID = `-- name: ExamPrepGetUnitModuleByID :one
|
|
||||||
SELECT id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at
|
|
||||||
FROM exam_prep.unit_modules
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepGetUnitModuleByID(ctx context.Context, id int64) (ExamPrepUnitModule, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepGetUnitModuleByID, id)
|
|
||||||
var i ExamPrepUnitModule
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.Icon,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepListUnitModuleIDsByUnit = `-- name: ExamPrepListUnitModuleIDsByUnit :many
|
|
||||||
SELECT
|
|
||||||
m.id
|
|
||||||
FROM exam_prep.unit_modules m
|
|
||||||
WHERE
|
|
||||||
m.unit_id = $1
|
|
||||||
ORDER BY
|
|
||||||
m.id
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepListUnitModuleIDsByUnit(ctx context.Context, unitID int64) ([]int64, error) {
|
|
||||||
rows, err := q.db.Query(ctx, ExamPrepListUnitModuleIDsByUnit, unitID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []int64
|
|
||||||
for rows.Next() {
|
|
||||||
var id int64
|
|
||||||
if err := rows.Scan(&id); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, id)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepListUnitModulesByUnit = `-- name: ExamPrepListUnitModulesByUnit :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
m.id,
|
|
||||||
m.unit_id,
|
|
||||||
m.name,
|
|
||||||
m.description,
|
|
||||||
m.thumbnail,
|
|
||||||
m.icon,
|
|
||||||
m.sort_order,
|
|
||||||
m.created_at,
|
|
||||||
m.updated_at
|
|
||||||
FROM exam_prep.unit_modules m
|
|
||||||
WHERE
|
|
||||||
m.unit_id = $1
|
|
||||||
ORDER BY
|
|
||||||
m.sort_order ASC,
|
|
||||||
m.id ASC
|
|
||||||
LIMIT $2
|
|
||||||
OFFSET $3
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepListUnitModulesByUnitParams struct {
|
|
||||||
UnitID int64 `json:"unit_id"`
|
|
||||||
Limit int32 `json:"limit"`
|
|
||||||
Offset int32 `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExamPrepListUnitModulesByUnitRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UnitID int64 `json:"unit_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Icon pgtype.Text `json:"icon"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepListUnitModulesByUnit(ctx context.Context, arg ExamPrepListUnitModulesByUnitParams) ([]ExamPrepListUnitModulesByUnitRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, ExamPrepListUnitModulesByUnit, arg.UnitID, arg.Limit, arg.Offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []ExamPrepListUnitModulesByUnitRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i ExamPrepListUnitModulesByUnitRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.Icon,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepUpdateUnitModule = `-- name: ExamPrepUpdateUnitModule :one
|
|
||||||
UPDATE exam_prep.unit_modules
|
|
||||||
SET
|
|
||||||
name = coalesce($1::varchar, name),
|
|
||||||
description = coalesce($2::text, description),
|
|
||||||
thumbnail = coalesce($3::text, thumbnail),
|
|
||||||
icon = coalesce($4::text, icon),
|
|
||||||
sort_order = coalesce($5::int, sort_order),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $6
|
|
||||||
RETURNING
|
|
||||||
id, unit_id, name, description, thumbnail, icon, sort_order, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepUpdateUnitModuleParams struct {
|
|
||||||
Name pgtype.Text `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Icon pgtype.Text `json:"icon"`
|
|
||||||
SortOrder pgtype.Int4 `json:"sort_order"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepUpdateUnitModule(ctx context.Context, arg ExamPrepUpdateUnitModuleParams) (ExamPrepUnitModule, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepUpdateUnitModule,
|
|
||||||
arg.Name,
|
|
||||||
arg.Description,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.Icon,
|
|
||||||
arg.SortOrder,
|
|
||||||
arg.ID,
|
|
||||||
)
|
|
||||||
var i ExamPrepUnitModule
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UnitID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.Icon,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.30.0
|
|
||||||
// source: exam_prep_units.sql
|
|
||||||
|
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ExamPrepCreateUnit = `-- name: ExamPrepCreateUnit :one
|
|
||||||
INSERT INTO exam_prep.units (catalog_course_id, name, description, thumbnail, sort_order)
|
|
||||||
SELECT
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
coalesce((
|
|
||||||
SELECT
|
|
||||||
max(u.sort_order)
|
|
||||||
FROM exam_prep.units u
|
|
||||||
WHERE
|
|
||||||
u.catalog_course_id = $1), 0) + 1
|
|
||||||
RETURNING
|
|
||||||
id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepCreateUnitParams struct {
|
|
||||||
CatalogCourseID int64 `json:"catalog_course_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepCreateUnit(ctx context.Context, arg ExamPrepCreateUnitParams) (ExamPrepUnit, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepCreateUnit,
|
|
||||||
arg.CatalogCourseID,
|
|
||||||
arg.Name,
|
|
||||||
arg.Description,
|
|
||||||
arg.Thumbnail,
|
|
||||||
)
|
|
||||||
var i ExamPrepUnit
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CatalogCourseID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepDeleteUnit = `-- name: ExamPrepDeleteUnit :exec
|
|
||||||
DELETE FROM exam_prep.units
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepDeleteUnit(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, ExamPrepDeleteUnit, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepGetUnitByID = `-- name: ExamPrepGetUnitByID :one
|
|
||||||
SELECT id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at
|
|
||||||
FROM exam_prep.units
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepGetUnitByID(ctx context.Context, id int64) (ExamPrepUnit, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepGetUnitByID, id)
|
|
||||||
var i ExamPrepUnit
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CatalogCourseID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepListUnitIDsByCatalogCourse = `-- name: ExamPrepListUnitIDsByCatalogCourse :many
|
|
||||||
SELECT
|
|
||||||
u.id
|
|
||||||
FROM exam_prep.units u
|
|
||||||
WHERE
|
|
||||||
u.catalog_course_id = $1
|
|
||||||
ORDER BY
|
|
||||||
u.id
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepListUnitIDsByCatalogCourse(ctx context.Context, catalogCourseID int64) ([]int64, error) {
|
|
||||||
rows, err := q.db.Query(ctx, ExamPrepListUnitIDsByCatalogCourse, catalogCourseID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []int64
|
|
||||||
for rows.Next() {
|
|
||||||
var id int64
|
|
||||||
if err := rows.Scan(&id); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, id)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepListUnitsByCatalogCourse = `-- name: ExamPrepListUnitsByCatalogCourse :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
u.id,
|
|
||||||
u.catalog_course_id,
|
|
||||||
u.name,
|
|
||||||
u.description,
|
|
||||||
u.thumbnail,
|
|
||||||
u.sort_order,
|
|
||||||
u.created_at,
|
|
||||||
u.updated_at
|
|
||||||
FROM exam_prep.units u
|
|
||||||
WHERE
|
|
||||||
u.catalog_course_id = $1
|
|
||||||
ORDER BY
|
|
||||||
u.sort_order ASC,
|
|
||||||
u.id ASC
|
|
||||||
LIMIT $2
|
|
||||||
OFFSET $3
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepListUnitsByCatalogCourseParams struct {
|
|
||||||
CatalogCourseID int64 `json:"catalog_course_id"`
|
|
||||||
Limit int32 `json:"limit"`
|
|
||||||
Offset int32 `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExamPrepListUnitsByCatalogCourseRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CatalogCourseID int64 `json:"catalog_course_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepListUnitsByCatalogCourse(ctx context.Context, arg ExamPrepListUnitsByCatalogCourseParams) ([]ExamPrepListUnitsByCatalogCourseRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, ExamPrepListUnitsByCatalogCourse, arg.CatalogCourseID, arg.Limit, arg.Offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []ExamPrepListUnitsByCatalogCourseRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i ExamPrepListUnitsByCatalogCourseRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.CatalogCourseID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ExamPrepUpdateUnit = `-- name: ExamPrepUpdateUnit :one
|
|
||||||
UPDATE exam_prep.units
|
|
||||||
SET
|
|
||||||
name = coalesce($1::varchar, name),
|
|
||||||
description = coalesce($2::text, description),
|
|
||||||
thumbnail = coalesce($3::text, thumbnail),
|
|
||||||
sort_order = coalesce($4::int, sort_order),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $5
|
|
||||||
RETURNING
|
|
||||||
id, catalog_course_id, name, description, thumbnail, sort_order, created_at, updated_at
|
|
||||||
`
|
|
||||||
|
|
||||||
type ExamPrepUpdateUnitParams struct {
|
|
||||||
Name pgtype.Text `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
SortOrder pgtype.Int4 `json:"sort_order"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ExamPrepUpdateUnit(ctx context.Context, arg ExamPrepUpdateUnitParams) (ExamPrepUnit, error) {
|
|
||||||
row := q.db.QueryRow(ctx, ExamPrepUpdateUnit,
|
|
||||||
arg.Name,
|
|
||||||
arg.Description,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.SortOrder,
|
|
||||||
arg.ID,
|
|
||||||
)
|
|
||||||
var i ExamPrepUnit
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CatalogCourseID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.SortOrder,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
@ -129,33 +129,7 @@ SELECT
|
||||||
c.thumbnail,
|
c.thumbnail,
|
||||||
c.sort_order,
|
c.sort_order,
|
||||||
c.created_at,
|
c.created_at,
|
||||||
c.updated_at,
|
c.updated_at
|
||||||
(
|
|
||||||
SELECT
|
|
||||||
COUNT(*)::bigint
|
|
||||||
FROM
|
|
||||||
modules m
|
|
||||||
WHERE
|
|
||||||
m.course_id = c.id) AS module_count,
|
|
||||||
(
|
|
||||||
SELECT
|
|
||||||
COUNT(*)::bigint
|
|
||||||
FROM
|
|
||||||
lessons l
|
|
||||||
INNER JOIN modules m ON l.module_id = m.id
|
|
||||||
WHERE
|
|
||||||
m.course_id = c.id) AS lesson_count,
|
|
||||||
-- Practices whose parent is the course only (lms_practices.course_id). Excludes
|
|
||||||
-- practices linked via module_id or lesson_id, even for modules/lessons in this course.
|
|
||||||
(
|
|
||||||
SELECT
|
|
||||||
COUNT(*)::bigint
|
|
||||||
FROM
|
|
||||||
lms_practices p
|
|
||||||
WHERE
|
|
||||||
p.course_id = c.id
|
|
||||||
AND p.module_id IS NULL
|
|
||||||
AND p.lesson_id IS NULL) AS practice_count
|
|
||||||
FROM
|
FROM
|
||||||
courses c
|
courses c
|
||||||
WHERE
|
WHERE
|
||||||
|
|
@ -173,18 +147,15 @@ type ListCoursesByProgramIDParams struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListCoursesByProgramIDRow struct {
|
type ListCoursesByProgramIDRow struct {
|
||||||
TotalCount int64 `json:"total_count"`
|
TotalCount int64 `json:"total_count"`
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
ProgramID int64 `json:"program_id"`
|
ProgramID int64 `json:"program_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
SortOrder int32 `json:"sort_order"`
|
SortOrder int32 `json:"sort_order"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
ModuleCount int64 `json:"module_count"`
|
|
||||||
LessonCount int64 `json:"lesson_count"`
|
|
||||||
PracticeCount int64 `json:"practice_count"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) {
|
func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) {
|
||||||
|
|
@ -206,9 +177,6 @@ func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByP
|
||||||
&i.SortOrder,
|
&i.SortOrder,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.ModuleCount,
|
|
||||||
&i.LessonCount,
|
|
||||||
&i.PracticeCount,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ package dbgen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const CountCoursesInProgram = `-- name: CountCoursesInProgram :one
|
const CountCoursesInProgram = `-- name: CountCoursesInProgram :one
|
||||||
|
|
@ -96,65 +94,6 @@ func (q *Queries) CountModulesInCourse(ctx context.Context, courseID int64) (int
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const CountPublishedPracticesInCourse = `-- name: CountPublishedPracticesInCourse :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
lp.course_id = $1
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) CountPublishedPracticesInCourse(ctx context.Context, courseID pgtype.Int8) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CountPublishedPracticesInCourse, courseID)
|
|
||||||
var n int32
|
|
||||||
err := row.Scan(&n)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CountPublishedPracticesInModule = `-- name: CountPublishedPracticesInModule :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
lp.module_id = $1
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
`
|
|
||||||
|
|
||||||
// Published practices in a module (module-level and lesson-level practices should carry module_id).
|
|
||||||
func (q *Queries) CountPublishedPracticesInModule(ctx context.Context, moduleID pgtype.Int8) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CountPublishedPracticesInModule, moduleID)
|
|
||||||
var n int32
|
|
||||||
err := row.Scan(&n)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CountPublishedPracticesInProgram = `-- name: CountPublishedPracticesInProgram :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN courses c ON c.id = lp.course_id
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
c.program_id = $1
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) CountPublishedPracticesInProgram(ctx context.Context, programID int64) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CountPublishedPracticesInProgram, programID)
|
|
||||||
var n int32
|
|
||||||
err := row.Scan(&n)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CountUserCompletedCoursesInProgram = `-- name: CountUserCompletedCoursesInProgram :one
|
const CountUserCompletedCoursesInProgram = `-- name: CountUserCompletedCoursesInProgram :one
|
||||||
SELECT
|
SELECT
|
||||||
count(*)::int AS n
|
count(*)::int AS n
|
||||||
|
|
@ -273,122 +212,6 @@ func (q *Queries) CountUserCompletedModulesInCourse(ctx context.Context, arg Cou
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const CountUserCompletedPublishedPracticesInCourse = `-- name: CountUserCompletedPublishedPracticesInCourse :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
lp.course_id = $1
|
|
||||||
AND upp.user_id = $2
|
|
||||||
AND upp.completed_at IS NOT NULL
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
`
|
|
||||||
|
|
||||||
type CountUserCompletedPublishedPracticesInCourseParams struct {
|
|
||||||
CourseID pgtype.Int8 `json:"course_id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CountUserCompletedPublishedPracticesInCourse(ctx context.Context, arg CountUserCompletedPublishedPracticesInCourseParams) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CountUserCompletedPublishedPracticesInCourse, arg.CourseID, arg.UserID)
|
|
||||||
var n int32
|
|
||||||
err := row.Scan(&n)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CountUserCompletedPublishedPracticesInModule = `-- name: CountUserCompletedPublishedPracticesInModule :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
lp.module_id = $1
|
|
||||||
AND upp.user_id = $2
|
|
||||||
AND upp.completed_at IS NOT NULL
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
`
|
|
||||||
|
|
||||||
type CountUserCompletedPublishedPracticesInModuleParams struct {
|
|
||||||
ModuleID pgtype.Int8 `json:"module_id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CountUserCompletedPublishedPracticesInModule(ctx context.Context, arg CountUserCompletedPublishedPracticesInModuleParams) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CountUserCompletedPublishedPracticesInModule, arg.ModuleID, arg.UserID)
|
|
||||||
var n int32
|
|
||||||
err := row.Scan(&n)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const CountUserCompletedPublishedPracticesInProgram = `-- name: CountUserCompletedPublishedPracticesInProgram :one
|
|
||||||
SELECT
|
|
||||||
count(*)::int AS n
|
|
||||||
FROM
|
|
||||||
lms_practices lp
|
|
||||||
INNER JOIN courses c ON c.id = lp.course_id
|
|
||||||
INNER JOIN question_sets qs ON qs.id = lp.question_set_id
|
|
||||||
INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id
|
|
||||||
WHERE
|
|
||||||
c.program_id = $1
|
|
||||||
AND upp.user_id = $2
|
|
||||||
AND upp.completed_at IS NOT NULL
|
|
||||||
AND qs.set_type = 'PRACTICE'
|
|
||||||
AND qs.status = 'PUBLISHED'
|
|
||||||
`
|
|
||||||
|
|
||||||
type CountUserCompletedPublishedPracticesInProgramParams struct {
|
|
||||||
ProgramID int64 `json:"program_id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CountUserCompletedPublishedPracticesInProgram(ctx context.Context, arg CountUserCompletedPublishedPracticesInProgramParams) (int32, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CountUserCompletedPublishedPracticesInProgram, arg.ProgramID, arg.UserID)
|
|
||||||
var n int32
|
|
||||||
err := row.Scan(&n)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetPracticeScopeByQuestionSetID = `-- name: GetPracticeScopeByQuestionSetID :one
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
course_id,
|
|
||||||
module_id,
|
|
||||||
lesson_id
|
|
||||||
FROM
|
|
||||||
lms_practices
|
|
||||||
WHERE
|
|
||||||
question_set_id = $1
|
|
||||||
ORDER BY
|
|
||||||
id DESC
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetPracticeScopeByQuestionSetIDRow struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CourseID pgtype.Int8 `json:"course_id"`
|
|
||||||
ModuleID pgtype.Int8 `json:"module_id"`
|
|
||||||
LessonID pgtype.Int8 `json:"lesson_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetPracticeScopeByQuestionSetID(ctx context.Context, questionSetID int64) (GetPracticeScopeByQuestionSetIDRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetPracticeScopeByQuestionSetID, questionSetID)
|
|
||||||
var i GetPracticeScopeByQuestionSetIDRow
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CourseID,
|
|
||||||
&i.ModuleID,
|
|
||||||
&i.LessonID,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetPreviousCourseInProgram = `-- name: GetPreviousCourseInProgram :one
|
const GetPreviousCourseInProgram = `-- name: GetPreviousCourseInProgram :one
|
||||||
SELECT
|
SELECT
|
||||||
c2.id, c2.program_id, c2.name, c2.description, c2.thumbnail, c2.created_at, c2.updated_at, c2.sort_order
|
c2.id, c2.program_id, c2.name, c2.description, c2.thumbnail, c2.created_at, c2.updated_at, c2.sort_order
|
||||||
|
|
|
||||||
|
|
@ -43,64 +43,6 @@ type Device struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExamPrepCatalogCourse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExamPrepLessonPractice struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UnitModuleLessonID int64 `json:"unit_module_lesson_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
StoryDescription pgtype.Text `json:"story_description"`
|
|
||||||
StoryImage pgtype.Text `json:"story_image"`
|
|
||||||
PersonaID pgtype.Int8 `json:"persona_id"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
QuickTips pgtype.Text `json:"quick_tips"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExamPrepUnit struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CatalogCourseID int64 `json:"catalog_course_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExamPrepUnitModule struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UnitID int64 `json:"unit_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Icon pgtype.Text `json:"icon"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExamPrepUnitModuleLesson struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UnitModuleID int64 `json:"unit_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
VideoUrl pgtype.Text `json:"video_url"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
SortOrder int32 `json:"sort_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GlobalSetting struct {
|
type GlobalSetting struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,7 @@ const GetOtp = `-- name: GetOtp :one
|
||||||
SELECT id, user_id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
|
SELECT id, user_id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
|
||||||
FROM otps
|
FROM otps
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1
|
||||||
ORDER BY id DESC
|
ORDER BY created_at DESC LIMIT 1
|
||||||
LIMIT 1
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetOtpRow struct {
|
type GetOtpRow struct {
|
||||||
|
|
@ -83,51 +82,6 @@ func (q *Queries) GetOtp(ctx context.Context, userID int64) (GetOtpRow, error) {
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetOtpByCode = `-- name: GetOtpByCode :one
|
|
||||||
SELECT id, user_id, sent_to, medium, otp_for, otp, used, used_at, created_at, expires_at
|
|
||||||
FROM otps
|
|
||||||
WHERE user_id = $1
|
|
||||||
AND otp = $2
|
|
||||||
ORDER BY id DESC
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetOtpByCodeParams struct {
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
Otp string `json:"otp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetOtpByCodeRow struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
SentTo string `json:"sent_to"`
|
|
||||||
Medium string `json:"medium"`
|
|
||||||
OtpFor string `json:"otp_for"`
|
|
||||||
Otp string `json:"otp"`
|
|
||||||
Used bool `json:"used"`
|
|
||||||
UsedAt pgtype.Timestamptz `json:"used_at"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetOtpByCode(ctx context.Context, arg GetOtpByCodeParams) (GetOtpByCodeRow, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetOtpByCode, arg.UserID, arg.Otp)
|
|
||||||
var i GetOtpByCodeRow
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.UserID,
|
|
||||||
&i.SentTo,
|
|
||||||
&i.Medium,
|
|
||||||
&i.OtpFor,
|
|
||||||
&i.Otp,
|
|
||||||
&i.Used,
|
|
||||||
&i.UsedAt,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.ExpiresAt,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const MarkOtpAsUsed = `-- name: MarkOtpAsUsed :exec
|
const MarkOtpAsUsed = `-- name: MarkOtpAsUsed :exec
|
||||||
UPDATE otps
|
UPDATE otps
|
||||||
SET used = TRUE, used_at = $2
|
SET used = TRUE, used_at = $2
|
||||||
|
|
|
||||||
|
|
@ -69,32 +69,27 @@ SELECT
|
||||||
q.explanation,
|
q.explanation,
|
||||||
q.tips,
|
q.tips,
|
||||||
q.voice_prompt,
|
q.voice_prompt,
|
||||||
q.sample_answer_voice_prompt,
|
q.image_url
|
||||||
q.image_url,
|
|
||||||
qaa.correct_answer_text AS audio_correct_answer_text
|
|
||||||
FROM question_set_items qsi
|
FROM question_set_items qsi
|
||||||
JOIN questions q ON q.id = qsi.question_id
|
JOIN questions q ON q.id = qsi.question_id
|
||||||
LEFT JOIN question_audio_answers qaa ON qaa.question_id = q.id
|
|
||||||
WHERE qsi.set_id = $1
|
WHERE qsi.set_id = $1
|
||||||
AND q.status = 'PUBLISHED'
|
AND q.status = 'PUBLISHED'
|
||||||
ORDER BY qsi.display_order
|
ORDER BY qsi.display_order
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetPublishedQuestionsInSetRow struct {
|
type GetPublishedQuestionsInSetRow struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
SetID int64 `json:"set_id"`
|
SetID int64 `json:"set_id"`
|
||||||
QuestionID int64 `json:"question_id"`
|
QuestionID int64 `json:"question_id"`
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
QuestionText string `json:"question_text"`
|
QuestionText string `json:"question_text"`
|
||||||
QuestionType string `json:"question_type"`
|
QuestionType string `json:"question_type"`
|
||||||
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
||||||
Points int32 `json:"points"`
|
Points int32 `json:"points"`
|
||||||
Explanation pgtype.Text `json:"explanation"`
|
Explanation pgtype.Text `json:"explanation"`
|
||||||
Tips pgtype.Text `json:"tips"`
|
Tips pgtype.Text `json:"tips"`
|
||||||
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
||||||
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
ImageUrl pgtype.Text `json:"image_url"`
|
|
||||||
AudioCorrectAnswerText pgtype.Text `json:"audio_correct_answer_text"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]GetPublishedQuestionsInSetRow, error) {
|
func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]GetPublishedQuestionsInSetRow, error) {
|
||||||
|
|
@ -118,9 +113,7 @@ func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) (
|
||||||
&i.Explanation,
|
&i.Explanation,
|
||||||
&i.Tips,
|
&i.Tips,
|
||||||
&i.VoicePrompt,
|
&i.VoicePrompt,
|
||||||
&i.SampleAnswerVoicePrompt,
|
|
||||||
&i.ImageUrl,
|
&i.ImageUrl,
|
||||||
&i.AudioCorrectAnswerText,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -145,34 +138,29 @@ SELECT
|
||||||
q.explanation,
|
q.explanation,
|
||||||
q.tips,
|
q.tips,
|
||||||
q.voice_prompt,
|
q.voice_prompt,
|
||||||
q.sample_answer_voice_prompt,
|
|
||||||
q.image_url,
|
q.image_url,
|
||||||
q.status as question_status,
|
q.status as question_status
|
||||||
qaa.correct_answer_text AS audio_correct_answer_text
|
|
||||||
FROM question_set_items qsi
|
FROM question_set_items qsi
|
||||||
JOIN questions q ON q.id = qsi.question_id
|
JOIN questions q ON q.id = qsi.question_id
|
||||||
LEFT JOIN question_audio_answers qaa ON qaa.question_id = q.id
|
|
||||||
WHERE qsi.set_id = $1
|
WHERE qsi.set_id = $1
|
||||||
AND q.status != 'ARCHIVED'
|
AND q.status != 'ARCHIVED'
|
||||||
ORDER BY qsi.display_order
|
ORDER BY qsi.display_order
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetQuestionSetItemsRow struct {
|
type GetQuestionSetItemsRow struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
SetID int64 `json:"set_id"`
|
SetID int64 `json:"set_id"`
|
||||||
QuestionID int64 `json:"question_id"`
|
QuestionID int64 `json:"question_id"`
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
QuestionText string `json:"question_text"`
|
QuestionText string `json:"question_text"`
|
||||||
QuestionType string `json:"question_type"`
|
QuestionType string `json:"question_type"`
|
||||||
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
DifficultyLevel pgtype.Text `json:"difficulty_level"`
|
||||||
Points int32 `json:"points"`
|
Points int32 `json:"points"`
|
||||||
Explanation pgtype.Text `json:"explanation"`
|
Explanation pgtype.Text `json:"explanation"`
|
||||||
Tips pgtype.Text `json:"tips"`
|
Tips pgtype.Text `json:"tips"`
|
||||||
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
VoicePrompt pgtype.Text `json:"voice_prompt"`
|
||||||
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
ImageUrl pgtype.Text `json:"image_url"`
|
||||||
ImageUrl pgtype.Text `json:"image_url"`
|
QuestionStatus string `json:"question_status"`
|
||||||
QuestionStatus string `json:"question_status"`
|
|
||||||
AudioCorrectAnswerText pgtype.Text `json:"audio_correct_answer_text"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQuestionSetItemsRow, error) {
|
func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQuestionSetItemsRow, error) {
|
||||||
|
|
@ -196,10 +184,8 @@ func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQu
|
||||||
&i.Explanation,
|
&i.Explanation,
|
||||||
&i.Tips,
|
&i.Tips,
|
||||||
&i.VoicePrompt,
|
&i.VoicePrompt,
|
||||||
&i.SampleAnswerVoicePrompt,
|
|
||||||
&i.ImageUrl,
|
&i.ImageUrl,
|
||||||
&i.QuestionStatus,
|
&i.QuestionStatus,
|
||||||
&i.AudioCorrectAnswerText,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,8 @@ package domain
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// DefaultCEFRCoursesByProgramName maps seeded program names to default course names
|
// DefaultCEFRCourseNames are the standard course names seeded for each program (migration 000048).
|
||||||
// (migrations 000048 and 000050). The API may still create courses with any name.
|
// Creating a course via the API may use any of these or a custom name.
|
||||||
var DefaultCEFRCoursesByProgramName = map[string][]string{
|
|
||||||
"Beginner": {"A1", "A2"},
|
|
||||||
"Intermediate": {"B1", "B2"},
|
|
||||||
"Advanced": {"C1", "C2"},
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultCEFRCourseNames is every CEFR label used in DefaultCEFRCoursesByProgramName.
|
|
||||||
var DefaultCEFRCourseNames = []string{"A1", "A2", "B1", "B2", "C1", "C2"}
|
var DefaultCEFRCourseNames = []string{"A1", "A2", "B1", "B2", "C1", "C2"}
|
||||||
|
|
||||||
// Course belongs to a Program.
|
// Course belongs to a Program.
|
||||||
|
|
@ -23,12 +16,7 @@ type Course struct {
|
||||||
SortOrder int `json:"sort_order"`
|
SortOrder int `json:"sort_order"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
// Populated on list-by-program. Practice count: lms_practices rows with course_id = course only
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
// (not practices attached to a module or lesson under this course).
|
|
||||||
ModuleCount int `json:"module_count"`
|
|
||||||
LessonCount int `json:"lesson_count"`
|
|
||||||
PracticeCount int `json:"practice_count"`
|
|
||||||
Access *LMSEntityAccess `json:"access,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateCourseInput struct {
|
type CreateCourseInput struct {
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// ExamPrepCatalogCourse is a top-level exam-prep track (e.g. DET, IELTS) in schema exam_prep — separate from LMS Learn English courses.
|
|
||||||
type ExamPrepCatalogCourse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
SortOrder int `json:"sort_order"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateExamPrepCatalogCourseInput struct {
|
|
||||||
Name string `json:"name" validate:"required"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateExamPrepCatalogCourseInput struct {
|
|
||||||
Name *string `json:"name,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
SortOrder *int `json:"sort_order,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// ExamPrepLesson is a video lesson under an exam-prep unit module (exam_prep.unit_module_lessons).
|
|
||||||
type ExamPrepLesson struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UnitModuleID int64 `json:"unit_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
VideoURL *string `json:"video_url,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
SortOrder int `json:"sort_order"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateExamPrepLessonInput struct {
|
|
||||||
Title string `json:"title" validate:"required"`
|
|
||||||
VideoURL *string `json:"video_url,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateExamPrepLessonInput struct {
|
|
||||||
Title *string `json:"title,omitempty"`
|
|
||||||
VideoURL *string `json:"video_url,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
SortOrder *int `json:"sort_order,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// ExamPrepModule is a module under an exam-prep unit (stored in exam_prep.unit_modules).
|
|
||||||
type ExamPrepModule struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UnitID int64 `json:"unit_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
Icon *string `json:"icon,omitempty"`
|
|
||||||
SortOrder int `json:"sort_order"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateExamPrepModuleInput struct {
|
|
||||||
Name string `json:"name" validate:"required"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
Icon *string `json:"icon,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateExamPrepModuleInput struct {
|
|
||||||
Name *string `json:"name,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
Icon *string `json:"icon,omitempty"`
|
|
||||||
SortOrder *int `json:"sort_order,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// ExamPrepPractice is question-set content tied to an exam-prep lesson; uses shared question_sets / questions.
|
|
||||||
type ExamPrepPractice struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
LessonID int64 `json:"lesson_id"` // exam_prep.unit_module_lessons.id
|
|
||||||
Title string `json:"title"`
|
|
||||||
StoryDescription *string `json:"story_description,omitempty"`
|
|
||||||
StoryImage *string `json:"story_image,omitempty"`
|
|
||||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
QuickTips *string `json:"quick_tips,omitempty"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateExamPrepPracticeInput is the body for POST .../exam-prep/lessons/{lessonId}/practices (lesson from path).
|
|
||||||
type CreateExamPrepPracticeInput struct {
|
|
||||||
Title string `json:"title" validate:"required"`
|
|
||||||
StoryDescription *string `json:"story_description,omitempty"`
|
|
||||||
StoryImage *string `json:"story_image,omitempty"`
|
|
||||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id" validate:"required,gt=0"`
|
|
||||||
QuickTips *string `json:"quick_tips,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateExamPrepPracticeInput struct {
|
|
||||||
Title *string `json:"title,omitempty"`
|
|
||||||
StoryDescription *string `json:"story_description,omitempty"`
|
|
||||||
StoryImage *string `json:"story_image,omitempty"`
|
|
||||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
|
||||||
QuestionSetID *int64 `json:"question_set_id,omitempty"`
|
|
||||||
QuickTips *string `json:"quick_tips,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// ExamPrepUnit is a chapter-like grouping under an exam-prep catalog course (schema exam_prep.units).
|
|
||||||
type ExamPrepUnit struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CatalogCourseID int64 `json:"catalog_course_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
SortOrder int `json:"sort_order"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateExamPrepUnitInput struct {
|
|
||||||
Name string `json:"name" validate:"required"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateExamPrepUnitInput struct {
|
|
||||||
Name *string `json:"name,omitempty"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
SortOrder *int `json:"sort_order,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,223 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Stimulus-side components for the question-type builder (Section A — question input types).
|
|
||||||
type StimulusComponentKind string
|
|
||||||
|
|
||||||
const (
|
|
||||||
StimulusPrepTime StimulusComponentKind = "PREP_TIME"
|
|
||||||
StimulusInstruction StimulusComponentKind = "INSTRUCTION"
|
|
||||||
StimulusAudioClip StimulusComponentKind = "AUDIO_CLIP"
|
|
||||||
StimulusTextPassage StimulusComponentKind = "TEXT_PASSAGE"
|
|
||||||
StimulusImage StimulusComponentKind = "IMAGE"
|
|
||||||
StimulusMatchingInputs StimulusComponentKind = "MATCHING_INPUTS"
|
|
||||||
StimulusSelectMissingWords StimulusComponentKind = "SELECT_MISSING_WORDS"
|
|
||||||
StimulusTable StimulusComponentKind = "TABLE"
|
|
||||||
StimulusFlowChart StimulusComponentKind = "FLOW_CHART"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Response-side components for the question-type builder (Section B — answer types).
|
|
||||||
type ResponseComponentKind string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ResponseAudioResponse ResponseComponentKind = "AUDIO_RESPONSE"
|
|
||||||
ResponseTextInput ResponseComponentKind = "TEXT_INPUT"
|
|
||||||
ResponseShortAnswer ResponseComponentKind = "SHORT_ANSWER"
|
|
||||||
ResponseMultipleChoice ResponseComponentKind = "MULTIPLE_CHOICE"
|
|
||||||
ResponseAnswerTimer ResponseComponentKind = "ANSWER_TIMER"
|
|
||||||
ResponseSelectMissingWords ResponseComponentKind = "SELECT_MISSING_WORDS"
|
|
||||||
ResponsePDFUpload ResponseComponentKind = "PDF_UPLOAD"
|
|
||||||
ResponseMatchingAnswer ResponseComponentKind = "MATCHING_ANSWER"
|
|
||||||
ResponseLabelSelection ResponseComponentKind = "LABEL_SELECTION"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
stimulusCatalog = []StimulusComponentKind{
|
|
||||||
StimulusPrepTime,
|
|
||||||
StimulusInstruction,
|
|
||||||
StimulusAudioClip,
|
|
||||||
StimulusTextPassage,
|
|
||||||
StimulusImage,
|
|
||||||
StimulusMatchingInputs,
|
|
||||||
StimulusSelectMissingWords,
|
|
||||||
StimulusTable,
|
|
||||||
StimulusFlowChart,
|
|
||||||
}
|
|
||||||
stimulusSet map[string]struct{}
|
|
||||||
|
|
||||||
responseCatalog = []ResponseComponentKind{
|
|
||||||
ResponseAudioResponse,
|
|
||||||
ResponseTextInput,
|
|
||||||
ResponseShortAnswer,
|
|
||||||
ResponseMultipleChoice,
|
|
||||||
ResponseAnswerTimer,
|
|
||||||
ResponseSelectMissingWords,
|
|
||||||
ResponsePDFUpload,
|
|
||||||
ResponseMatchingAnswer,
|
|
||||||
ResponseLabelSelection,
|
|
||||||
}
|
|
||||||
responseSet map[string]struct{}
|
|
||||||
|
|
||||||
// responseKindsAuxiliary are allowed but cannot be the only selected answer kinds.
|
|
||||||
responseKindsAuxiliary = map[string]struct{}{
|
|
||||||
string(ResponseAnswerTimer): {},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
stimulusSet = make(map[string]struct{}, len(stimulusCatalog))
|
|
||||||
for _, k := range stimulusCatalog {
|
|
||||||
stimulusSet[string(k)] = struct{}{}
|
|
||||||
}
|
|
||||||
responseSet = make(map[string]struct{}, len(responseCatalog))
|
|
||||||
for _, k := range responseCatalog {
|
|
||||||
responseSet[string(k)] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StimulusComponentCatalog returns all valid stimulus component kind strings (stable order).
|
|
||||||
func StimulusComponentCatalog() []string {
|
|
||||||
out := make([]string, len(stimulusCatalog))
|
|
||||||
for i, k := range stimulusCatalog {
|
|
||||||
out[i] = string(k)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResponseComponentCatalog returns all valid response component kind strings (stable order).
|
|
||||||
func ResponseComponentCatalog() []string {
|
|
||||||
out := make([]string, len(responseCatalog))
|
|
||||||
for i, k := range responseCatalog {
|
|
||||||
out[i] = string(k)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValidStimulusComponentKind reports whether s is a known stimulus component kind.
|
|
||||||
func IsValidStimulusComponentKind(s string) bool {
|
|
||||||
_, ok := stimulusSet[s]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValidResponseComponentKind reports whether s is a known response component kind.
|
|
||||||
func IsValidResponseComponentKind(s string) bool {
|
|
||||||
_, ok := responseSet[s]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateDynamicQuestionTypeDefinition checks stimulus and response component selections for a
|
|
||||||
// temporary dynamic question-type definition. Empty or duplicate entries, unknown kinds, and
|
|
||||||
// disallowed combinations return a non-nil error.
|
|
||||||
func ValidateDynamicQuestionTypeDefinition(stimulusKinds, responseKinds []string) error {
|
|
||||||
var errs []string
|
|
||||||
|
|
||||||
stimulus := normalizeKindList(stimulusKinds)
|
|
||||||
response := normalizeKindList(responseKinds)
|
|
||||||
|
|
||||||
if len(stimulus) == 0 {
|
|
||||||
errs = append(errs, "at least one stimulus (question input) component is required")
|
|
||||||
}
|
|
||||||
if len(response) == 0 {
|
|
||||||
errs = append(errs, "at least one response component is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, k := range stimulus {
|
|
||||||
if k == "" {
|
|
||||||
errs = append(errs, fmt.Sprintf("stimulus [%d]: empty component kind", i))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !IsValidStimulusComponentKind(k) {
|
|
||||||
errs = append(errs, fmt.Sprintf("stimulus: unknown component kind %q", k))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, k := range response {
|
|
||||||
if k == "" {
|
|
||||||
errs = append(errs, fmt.Sprintf("response [%d]: empty component kind", i))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !IsValidResponseComponentKind(k) {
|
|
||||||
errs = append(errs, fmt.Sprintf("response: unknown component kind %q", k))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dup := findDuplicates(stimulus); len(dup) > 0 {
|
|
||||||
errs = append(errs, fmt.Sprintf("stimulus: duplicate component kinds: %s", strings.Join(dup, ", ")))
|
|
||||||
}
|
|
||||||
if dup := findDuplicates(response); len(dup) > 0 {
|
|
||||||
errs = append(errs, fmt.Sprintf("response: duplicate component kinds: %s", strings.Join(dup, ", ")))
|
|
||||||
}
|
|
||||||
|
|
||||||
countPrep := 0
|
|
||||||
for _, k := range stimulus {
|
|
||||||
if k == string(StimulusPrepTime) {
|
|
||||||
countPrep++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if countPrep > 1 {
|
|
||||||
errs = append(errs, "stimulus: at most one PREP_TIME is allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
countAnsTimer := 0
|
|
||||||
for _, k := range response {
|
|
||||||
if k == string(ResponseAnswerTimer) {
|
|
||||||
countAnsTimer++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if countAnsTimer > 1 {
|
|
||||||
errs = append(errs, "response: at most one ANSWER_TIMER is allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(response) > 0 && onlyAuxiliaryResponseKinds(response) {
|
|
||||||
errs = append(errs, "response: at least one non-timer answer component is required (ANSWER_TIMER alone is not sufficient)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%s", strings.Join(errs, "; "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeKindList(in []string) []string {
|
|
||||||
var out []string
|
|
||||||
for _, s := range in {
|
|
||||||
t := strings.TrimSpace(s)
|
|
||||||
if t == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, t)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func findDuplicates(kinds []string) []string {
|
|
||||||
seen := make(map[string]int)
|
|
||||||
for _, k := range kinds {
|
|
||||||
seen[k]++
|
|
||||||
}
|
|
||||||
var dups []string
|
|
||||||
for k, n := range seen {
|
|
||||||
if n > 1 {
|
|
||||||
dups = append(dups, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(dups)
|
|
||||||
return dups
|
|
||||||
}
|
|
||||||
|
|
||||||
func onlyAuxiliaryResponseKinds(response []string) bool {
|
|
||||||
if len(response) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, k := range response {
|
|
||||||
if _, aux := responseKindsAuxiliary[k]; !aux {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateDynamicQuestionTypeDefinition_valid(t *testing.T) {
|
|
||||||
err := ValidateDynamicQuestionTypeDefinition(
|
|
||||||
[]string{"INSTRUCTION", "IMAGE"},
|
|
||||||
[]string{"AUDIO_RESPONSE"},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected nil, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateDynamicQuestionTypeDefinition_unknownStimulus(t *testing.T) {
|
|
||||||
err := ValidateDynamicQuestionTypeDefinition(
|
|
||||||
[]string{"NOT_A_KIND"},
|
|
||||||
[]string{"SHORT_ANSWER"},
|
|
||||||
)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "unknown") {
|
|
||||||
t.Fatalf("expected unknown stimulus error, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateDynamicQuestionTypeDefinition_emptyResponse(t *testing.T) {
|
|
||||||
err := ValidateDynamicQuestionTypeDefinition(
|
|
||||||
[]string{"INSTRUCTION"},
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "at least one response") {
|
|
||||||
t.Fatalf("expected empty response error, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateDynamicQuestionTypeDefinition_duplicateStimulus(t *testing.T) {
|
|
||||||
err := ValidateDynamicQuestionTypeDefinition(
|
|
||||||
[]string{"IMAGE", "IMAGE"},
|
|
||||||
[]string{"MULTIPLE_CHOICE"},
|
|
||||||
)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "duplicate") {
|
|
||||||
t.Fatalf("expected duplicate error, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateDynamicQuestionTypeDefinition_timerOnlyResponse(t *testing.T) {
|
|
||||||
err := ValidateDynamicQuestionTypeDefinition(
|
|
||||||
[]string{"TEXT_PASSAGE"},
|
|
||||||
[]string{"ANSWER_TIMER"},
|
|
||||||
)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "ANSWER_TIMER alone") {
|
|
||||||
t.Fatalf("expected auxiliary-only error, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateDynamicQuestionTypeDefinition_twoPrepTimes(t *testing.T) {
|
|
||||||
err := ValidateDynamicQuestionTypeDefinition(
|
|
||||||
[]string{"PREP_TIME", "PREP_TIME", "INSTRUCTION"},
|
|
||||||
[]string{"TEXT_INPUT"},
|
|
||||||
)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), "PREP_TIME") {
|
|
||||||
t.Fatalf("expected at most one PREP_TIME, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -14,10 +14,6 @@ type Client struct {
|
||||||
bucketName string
|
bucketName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) BucketName() string {
|
|
||||||
return c.bucketName
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(endpoint, accessKey, secretKey string, useSSL bool, bucketName string) (*Client, error) {
|
func NewClient(endpoint, accessKey, secretKey string, useSSL bool, bucketName string) (*Client, error) {
|
||||||
mc, err := minio.New(endpoint, &minio.Options{
|
mc, err := minio.New(endpoint, &minio.Options{
|
||||||
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
|
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package ports
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExamPrepCatalogCourseStore persists exam_prep.catalog_courses (DET / IELTS / … tracks).
|
|
||||||
type ExamPrepCatalogCourseStore interface {
|
|
||||||
CreateExamPrepCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error)
|
|
||||||
GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (domain.ExamPrepCatalogCourse, error)
|
|
||||||
ListExamPrepCatalogCourses(ctx context.Context, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error)
|
|
||||||
ListAllExamPrepCatalogCourseIDs(ctx context.Context) ([]int64, error)
|
|
||||||
UpdateExamPrepCatalogCourse(ctx context.Context, id int64, input domain.UpdateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error)
|
|
||||||
DeleteExamPrepCatalogCourse(ctx context.Context, id int64) error
|
|
||||||
ReorderExamPrepCatalogCourses(ctx context.Context, orderedIDs []int64) error
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package ports
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExamPrepLessonStore persists exam_prep.unit_module_lessons.
|
|
||||||
type ExamPrepLessonStore interface {
|
|
||||||
CreateExamPrepUnitModuleLesson(ctx context.Context, unitModuleID int64, input domain.CreateExamPrepLessonInput) (domain.ExamPrepLesson, error)
|
|
||||||
GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) (domain.ExamPrepLesson, error)
|
|
||||||
ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, unitModuleID int64, limit, offset int32) ([]domain.ExamPrepLesson, int64, error)
|
|
||||||
ListExamPrepUnitModuleLessonIDsByUnitModule(ctx context.Context, unitModuleID int64) ([]int64, error)
|
|
||||||
UpdateExamPrepUnitModuleLesson(ctx context.Context, id int64, input domain.UpdateExamPrepLessonInput) (domain.ExamPrepLesson, error)
|
|
||||||
DeleteExamPrepUnitModuleLesson(ctx context.Context, id int64) error
|
|
||||||
ReorderExamPrepUnitModuleLessonsInUnitModule(ctx context.Context, unitModuleID int64, orderedIDs []int64) error
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package ports
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExamPrepModuleStore persists exam_prep.unit_modules.
|
|
||||||
type ExamPrepModuleStore interface {
|
|
||||||
CreateExamPrepUnitModule(ctx context.Context, unitID int64, input domain.CreateExamPrepModuleInput) (domain.ExamPrepModule, error)
|
|
||||||
GetExamPrepUnitModuleByID(ctx context.Context, id int64) (domain.ExamPrepModule, error)
|
|
||||||
ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64, limit, offset int32) ([]domain.ExamPrepModule, int64, error)
|
|
||||||
ListExamPrepUnitModuleIDsByUnit(ctx context.Context, unitID int64) ([]int64, error)
|
|
||||||
UpdateExamPrepUnitModule(ctx context.Context, id int64, input domain.UpdateExamPrepModuleInput) (domain.ExamPrepModule, error)
|
|
||||||
DeleteExamPrepUnitModule(ctx context.Context, id int64) error
|
|
||||||
ReorderExamPrepUnitModulesInUnit(ctx context.Context, unitID int64, orderedIDs []int64) error
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package ports
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExamPrepPracticeStore persists exam_prep.lesson_practices.
|
|
||||||
type ExamPrepPracticeStore interface {
|
|
||||||
CreateExamPrepLessonPractice(ctx context.Context, lessonID int64, in domain.CreateExamPrepPracticeInput) (domain.ExamPrepPractice, error)
|
|
||||||
GetExamPrepLessonPracticeByID(ctx context.Context, id int64) (domain.ExamPrepPractice, error)
|
|
||||||
ListExamPrepLessonPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.ExamPrepPractice, int64, error)
|
|
||||||
UpdateExamPrepLessonPractice(ctx context.Context, id int64, input domain.UpdateExamPrepPracticeInput) (domain.ExamPrepPractice, error)
|
|
||||||
DeleteExamPrepLessonPractice(ctx context.Context, id int64) error
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package ports
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExamPrepUnitStore persists exam_prep.units.
|
|
||||||
type ExamPrepUnitStore interface {
|
|
||||||
CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, input domain.CreateExamPrepUnitInput) (domain.ExamPrepUnit, error)
|
|
||||||
GetExamPrepUnitByID(ctx context.Context, id int64) (domain.ExamPrepUnit, error)
|
|
||||||
ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, limit, offset int32) ([]domain.ExamPrepUnit, int64, error)
|
|
||||||
ListExamPrepUnitIDsByCatalogCourse(ctx context.Context, catalogCourseID int64) ([]int64, error)
|
|
||||||
UpdateExamPrepUnit(ctx context.Context, id int64, input domain.UpdateExamPrepUnitInput) (domain.ExamPrepUnit, error)
|
|
||||||
DeleteExamPrepUnit(ctx context.Context, id int64) error
|
|
||||||
ReorderExamPrepUnitsInCatalogCourse(ctx context.Context, catalogCourseID int64, orderedIDs []int64) error
|
|
||||||
}
|
|
||||||
|
|
@ -94,5 +94,4 @@ type OtpStore interface {
|
||||||
MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error
|
MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error
|
||||||
CreateOtp(ctx context.Context, otp domain.Otp) error
|
CreateOtp(ctx context.Context, otp domain.Otp) error
|
||||||
GetOtp(ctx context.Context, userID int64) (domain.Otp, error)
|
GetOtp(ctx context.Context, userID int64) (domain.Otp, error)
|
||||||
GetOtpByCode(ctx context.Context, userID int64, otpCode string) (domain.Otp, error)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func examPrepCatalogCourseToDomain(c dbgen.ExamPrepCatalogCourse) domain.ExamPrepCatalogCourse {
|
|
||||||
out := domain.ExamPrepCatalogCourse{
|
|
||||||
ID: c.ID,
|
|
||||||
Name: c.Name,
|
|
||||||
SortOrder: int(c.SortOrder),
|
|
||||||
}
|
|
||||||
out.Description = fromPgText(c.Description)
|
|
||||||
out.Thumbnail = fromPgText(c.Thumbnail)
|
|
||||||
out.CreatedAt = c.CreatedAt.Time
|
|
||||||
if c.UpdatedAt.Valid {
|
|
||||||
t := c.UpdatedAt.Time
|
|
||||||
out.UpdatedAt = &t
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateExamPrepCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
|
|
||||||
c, err := s.queries.ExamPrepCreateCatalogCourse(ctx, dbgen.ExamPrepCreateCatalogCourseParams{
|
|
||||||
Name: input.Name,
|
|
||||||
Description: toPgText(input.Description),
|
|
||||||
Thumbnail: toPgText(input.Thumbnail),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return domain.ExamPrepCatalogCourse{}, err
|
|
||||||
}
|
|
||||||
return examPrepCatalogCourseToDomain(c), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetExamPrepCatalogCourseByID(ctx context.Context, id int64) (domain.ExamPrepCatalogCourse, error) {
|
|
||||||
c, err := s.queries.ExamPrepGetCatalogCourseByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepCatalogCourse{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.ExamPrepCatalogCourse{}, err
|
|
||||||
}
|
|
||||||
return examPrepCatalogCourseToDomain(c), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListExamPrepCatalogCourses(ctx context.Context, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error) {
|
|
||||||
rows, err := s.queries.ExamPrepListCatalogCourses(ctx, dbgen.ExamPrepListCatalogCoursesParams{
|
|
||||||
Limit: limit,
|
|
||||||
Offset: offset,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return []domain.ExamPrepCatalogCourse{}, 0, nil
|
|
||||||
}
|
|
||||||
var total int64
|
|
||||||
out := make([]domain.ExamPrepCatalogCourse, 0, len(rows))
|
|
||||||
for i, r := range rows {
|
|
||||||
if i == 0 {
|
|
||||||
total = r.TotalCount
|
|
||||||
}
|
|
||||||
out = append(out, examPrepCatalogCourseToDomain(dbgen.ExamPrepCatalogCourse{
|
|
||||||
ID: r.ID,
|
|
||||||
Name: r.Name,
|
|
||||||
Description: r.Description,
|
|
||||||
Thumbnail: r.Thumbnail,
|
|
||||||
SortOrder: r.SortOrder,
|
|
||||||
CreatedAt: r.CreatedAt,
|
|
||||||
UpdatedAt: r.UpdatedAt,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return out, total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListAllExamPrepCatalogCourseIDs(ctx context.Context) ([]int64, error) {
|
|
||||||
return s.queries.ExamPrepListAllCatalogCourseIDs(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateExamPrepCatalogCourse(ctx context.Context, id int64, input domain.UpdateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
|
|
||||||
var nameText pgtype.Text
|
|
||||||
if input.Name != nil {
|
|
||||||
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
|
||||||
} else {
|
|
||||||
nameText = pgtype.Text{Valid: false}
|
|
||||||
}
|
|
||||||
c, err := s.queries.ExamPrepUpdateCatalogCourse(ctx, dbgen.ExamPrepUpdateCatalogCourseParams{
|
|
||||||
ID: id,
|
|
||||||
Name: nameText,
|
|
||||||
Description: optionalTextUpdate(input.Description),
|
|
||||||
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
|
||||||
SortOrder: optionalInt4Update(input.SortOrder),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepCatalogCourse{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.ExamPrepCatalogCourse{}, err
|
|
||||||
}
|
|
||||||
return examPrepCatalogCourseToDomain(c), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExamPrepCatalogCourse(ctx context.Context, id int64) error {
|
|
||||||
return s.queries.ExamPrepDeleteCatalogCourse(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func examPrepPracticeFromListRow(r dbgen.ExamPrepListLessonPracticesByLessonIDRow) domain.ExamPrepPractice {
|
|
||||||
return examPrepPracticeToDomain(dbgen.ExamPrepLessonPractice{
|
|
||||||
ID: r.ID,
|
|
||||||
UnitModuleLessonID: r.UnitModuleLessonID,
|
|
||||||
Title: r.Title,
|
|
||||||
StoryDescription: r.StoryDescription,
|
|
||||||
StoryImage: r.StoryImage,
|
|
||||||
PersonaID: r.PersonaID,
|
|
||||||
QuestionSetID: r.QuestionSetID,
|
|
||||||
QuickTips: r.QuickTips,
|
|
||||||
CreatedAt: r.CreatedAt,
|
|
||||||
UpdatedAt: r.UpdatedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func examPrepPracticeToDomain(p dbgen.ExamPrepLessonPractice) domain.ExamPrepPractice {
|
|
||||||
out := domain.ExamPrepPractice{
|
|
||||||
ID: p.ID,
|
|
||||||
LessonID: p.UnitModuleLessonID,
|
|
||||||
Title: p.Title,
|
|
||||||
QuestionSetID: p.QuestionSetID,
|
|
||||||
}
|
|
||||||
out.StoryDescription = fromPgText(p.StoryDescription)
|
|
||||||
out.StoryImage = fromPgText(p.StoryImage)
|
|
||||||
out.QuickTips = fromPgText(p.QuickTips)
|
|
||||||
out.PersonaID = fromPgInt8ID(p.PersonaID)
|
|
||||||
out.CreatedAt = p.CreatedAt.Time
|
|
||||||
if p.UpdatedAt.Valid {
|
|
||||||
t := p.UpdatedAt.Time
|
|
||||||
out.UpdatedAt = &t
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateExamPrepLessonPractice(ctx context.Context, lessonID int64, in domain.CreateExamPrepPracticeInput) (domain.ExamPrepPractice, error) {
|
|
||||||
p, err := s.queries.ExamPrepCreateLessonPractice(ctx, dbgen.ExamPrepCreateLessonPracticeParams{
|
|
||||||
UnitModuleLessonID: lessonID,
|
|
||||||
Title: in.Title,
|
|
||||||
StoryDescription: toPgText(in.StoryDescription),
|
|
||||||
StoryImage: toPgText(in.StoryImage),
|
|
||||||
PersonaID: int64PtrToPg8(in.PersonaID),
|
|
||||||
QuestionSetID: in.QuestionSetID,
|
|
||||||
QuickTips: toPgText(in.QuickTips),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return domain.ExamPrepPractice{}, err
|
|
||||||
}
|
|
||||||
return examPrepPracticeToDomain(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetExamPrepLessonPracticeByID(ctx context.Context, id int64) (domain.ExamPrepPractice, error) {
|
|
||||||
p, err := s.queries.ExamPrepGetLessonPracticeByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepPractice{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.ExamPrepPractice{}, err
|
|
||||||
}
|
|
||||||
return examPrepPracticeToDomain(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListExamPrepLessonPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.ExamPrepPractice, int64, error) {
|
|
||||||
rows, err := s.queries.ExamPrepListLessonPracticesByLessonID(ctx, dbgen.ExamPrepListLessonPracticesByLessonIDParams{
|
|
||||||
UnitModuleLessonID: lessonID,
|
|
||||||
Limit: limit,
|
|
||||||
Offset: offset,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return []domain.ExamPrepPractice{}, 0, nil
|
|
||||||
}
|
|
||||||
var total int64
|
|
||||||
out := make([]domain.ExamPrepPractice, 0, len(rows))
|
|
||||||
for i, r := range rows {
|
|
||||||
if i == 0 {
|
|
||||||
total = r.TotalCount
|
|
||||||
}
|
|
||||||
out = append(out, examPrepPracticeFromListRow(r))
|
|
||||||
}
|
|
||||||
return out, total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateExamPrepLessonPractice(ctx context.Context, id int64, input domain.UpdateExamPrepPracticeInput) (domain.ExamPrepPractice, error) {
|
|
||||||
var titleText pgtype.Text
|
|
||||||
if input.Title != nil {
|
|
||||||
titleText = pgtype.Text{String: *input.Title, Valid: true}
|
|
||||||
} else {
|
|
||||||
titleText = pgtype.Text{Valid: false}
|
|
||||||
}
|
|
||||||
qs := optionalInt8UpdateID(input.QuestionSetID)
|
|
||||||
p, err := s.queries.ExamPrepUpdateLessonPractice(ctx, dbgen.ExamPrepUpdateLessonPracticeParams{
|
|
||||||
ID: id,
|
|
||||||
Title: titleText,
|
|
||||||
StoryDescription: optionalTextUpdate(input.StoryDescription),
|
|
||||||
StoryImage: optionalTextUpdate(input.StoryImage),
|
|
||||||
PersonaID: optionalInt8UpdateID(input.PersonaID),
|
|
||||||
QuestionSetID: qs,
|
|
||||||
QuickTips: optionalTextUpdate(input.QuickTips),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepPractice{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.ExamPrepPractice{}, err
|
|
||||||
}
|
|
||||||
return examPrepPracticeToDomain(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExamPrepLessonPractice(ctx context.Context, id int64) error {
|
|
||||||
return s.queries.ExamPrepDeleteLessonPractice(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const lessonReorderSortBump int32 = 1_000_000
|
|
||||||
|
|
||||||
// ReorderExamPrepCatalogCourses sets sort_order to 1..n for all catalog course rows (transactional).
|
|
||||||
func (s *Store) ReorderExamPrepCatalogCourses(ctx context.Context, orderedIDs []int64) error {
|
|
||||||
tx, err := s.conn.Begin(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() { _ = tx.Rollback(ctx) }()
|
|
||||||
for i, id := range orderedIDs {
|
|
||||||
tag, err := tx.Exec(ctx, `
|
|
||||||
UPDATE exam_prep.catalog_courses
|
|
||||||
SET sort_order = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2`, int32(i+1), id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if tag.RowsAffected() == 0 {
|
|
||||||
return fmt.Errorf("exam prep catalog course id %d not found", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tx.Commit(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReorderExamPrepUnitsInCatalogCourse sets sort_order to 1..n for units under catalogCourseID.
|
|
||||||
func (s *Store) ReorderExamPrepUnitsInCatalogCourse(ctx context.Context, catalogCourseID int64, orderedIDs []int64) error {
|
|
||||||
tx, err := s.conn.Begin(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() { _ = tx.Rollback(ctx) }()
|
|
||||||
for i, id := range orderedIDs {
|
|
||||||
tag, err := tx.Exec(ctx, `
|
|
||||||
UPDATE exam_prep.units
|
|
||||||
SET sort_order = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2
|
|
||||||
AND catalog_course_id = $3`, int32(i+1), id, catalogCourseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if tag.RowsAffected() == 0 {
|
|
||||||
return fmt.Errorf("unit id %d not in catalog course %d", id, catalogCourseID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tx.Commit(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReorderExamPrepUnitModulesInUnit sets sort_order to 1..n for modules under unitID.
|
|
||||||
func (s *Store) ReorderExamPrepUnitModulesInUnit(ctx context.Context, unitID int64, orderedIDs []int64) error {
|
|
||||||
tx, err := s.conn.Begin(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() { _ = tx.Rollback(ctx) }()
|
|
||||||
for i, id := range orderedIDs {
|
|
||||||
tag, err := tx.Exec(ctx, `
|
|
||||||
UPDATE exam_prep.unit_modules
|
|
||||||
SET sort_order = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2
|
|
||||||
AND unit_id = $3`, int32(i+1), id, unitID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if tag.RowsAffected() == 0 {
|
|
||||||
return fmt.Errorf("module id %d not in unit %d", id, unitID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tx.Commit(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReorderExamPrepUnitModuleLessonsInUnitModule sets sort_order to 1..n under unitModuleID.
|
|
||||||
// Uses an intermediate bump so UNIQUE (unit_module_id, sort_order) is never violated mid-reorder.
|
|
||||||
func (s *Store) ReorderExamPrepUnitModuleLessonsInUnitModule(ctx context.Context, unitModuleID int64, orderedIDs []int64) error {
|
|
||||||
tx, err := s.conn.Begin(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() { _ = tx.Rollback(ctx) }()
|
|
||||||
|
|
||||||
if _, err := tx.Exec(ctx, `
|
|
||||||
UPDATE exam_prep.unit_module_lessons
|
|
||||||
SET sort_order = sort_order + $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE unit_module_id = $2`, lessonReorderSortBump, unitModuleID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, id := range orderedIDs {
|
|
||||||
tag, err := tx.Exec(ctx, `
|
|
||||||
UPDATE exam_prep.unit_module_lessons
|
|
||||||
SET sort_order = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2
|
|
||||||
AND unit_module_id = $3`, int32(i+1), id, unitModuleID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if tag.RowsAffected() == 0 {
|
|
||||||
return fmt.Errorf("lesson id %d not in exam prep module %d", id, unitModuleID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tx.Commit(ctx)
|
|
||||||
}
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func examPrepLessonToDomain(l dbgen.ExamPrepUnitModuleLesson) domain.ExamPrepLesson {
|
|
||||||
out := domain.ExamPrepLesson{
|
|
||||||
ID: l.ID,
|
|
||||||
UnitModuleID: l.UnitModuleID,
|
|
||||||
Title: l.Title,
|
|
||||||
SortOrder: int(l.SortOrder),
|
|
||||||
}
|
|
||||||
out.VideoURL = fromPgText(l.VideoUrl)
|
|
||||||
out.Thumbnail = fromPgText(l.Thumbnail)
|
|
||||||
out.Description = fromPgText(l.Description)
|
|
||||||
out.CreatedAt = l.CreatedAt.Time
|
|
||||||
if l.UpdatedAt.Valid {
|
|
||||||
t := l.UpdatedAt.Time
|
|
||||||
out.UpdatedAt = &t
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateExamPrepUnitModuleLesson(ctx context.Context, unitModuleID int64, input domain.CreateExamPrepLessonInput) (domain.ExamPrepLesson, error) {
|
|
||||||
l, err := s.queries.ExamPrepCreateUnitModuleLesson(ctx, dbgen.ExamPrepCreateUnitModuleLessonParams{
|
|
||||||
UnitModuleID: unitModuleID,
|
|
||||||
Title: input.Title,
|
|
||||||
VideoUrl: toPgText(input.VideoURL),
|
|
||||||
Thumbnail: toPgText(input.Thumbnail),
|
|
||||||
Description: toPgText(input.Description),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return domain.ExamPrepLesson{}, err
|
|
||||||
}
|
|
||||||
return examPrepLessonToDomain(l), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetExamPrepUnitModuleLessonByID(ctx context.Context, id int64) (domain.ExamPrepLesson, error) {
|
|
||||||
l, err := s.queries.ExamPrepGetUnitModuleLessonByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepLesson{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.ExamPrepLesson{}, err
|
|
||||||
}
|
|
||||||
return examPrepLessonToDomain(l), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListExamPrepUnitModuleLessonsByUnitModuleID(ctx context.Context, unitModuleID int64, limit, offset int32) ([]domain.ExamPrepLesson, int64, error) {
|
|
||||||
rows, err := s.queries.ExamPrepListUnitModuleLessonsByUnitModuleID(ctx, dbgen.ExamPrepListUnitModuleLessonsByUnitModuleIDParams{
|
|
||||||
UnitModuleID: unitModuleID,
|
|
||||||
Limit: limit,
|
|
||||||
Offset: offset,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return []domain.ExamPrepLesson{}, 0, nil
|
|
||||||
}
|
|
||||||
var total int64
|
|
||||||
out := make([]domain.ExamPrepLesson, 0, len(rows))
|
|
||||||
for i, r := range rows {
|
|
||||||
if i == 0 {
|
|
||||||
total = r.TotalCount
|
|
||||||
}
|
|
||||||
out = append(out, examPrepLessonToDomain(dbgen.ExamPrepUnitModuleLesson{
|
|
||||||
ID: r.ID,
|
|
||||||
UnitModuleID: r.UnitModuleID,
|
|
||||||
Title: r.Title,
|
|
||||||
VideoUrl: r.VideoUrl,
|
|
||||||
Thumbnail: r.Thumbnail,
|
|
||||||
Description: r.Description,
|
|
||||||
SortOrder: r.SortOrder,
|
|
||||||
CreatedAt: r.CreatedAt,
|
|
||||||
UpdatedAt: r.UpdatedAt,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return out, total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListExamPrepUnitModuleLessonIDsByUnitModule(ctx context.Context, unitModuleID int64) ([]int64, error) {
|
|
||||||
return s.queries.ExamPrepListUnitModuleLessonIDsByUnitModule(ctx, unitModuleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateExamPrepUnitModuleLesson(ctx context.Context, id int64, input domain.UpdateExamPrepLessonInput) (domain.ExamPrepLesson, error) {
|
|
||||||
var titleText pgtype.Text
|
|
||||||
if input.Title != nil {
|
|
||||||
titleText = pgtype.Text{String: *input.Title, Valid: true}
|
|
||||||
} else {
|
|
||||||
titleText = pgtype.Text{Valid: false}
|
|
||||||
}
|
|
||||||
l, err := s.queries.ExamPrepUpdateUnitModuleLesson(ctx, dbgen.ExamPrepUpdateUnitModuleLessonParams{
|
|
||||||
ID: id,
|
|
||||||
Title: titleText,
|
|
||||||
VideoUrl: optionalTextUpdate(input.VideoURL),
|
|
||||||
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
|
||||||
Description: optionalTextUpdate(input.Description),
|
|
||||||
SortOrder: optionalInt4Update(input.SortOrder),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepLesson{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.ExamPrepLesson{}, err
|
|
||||||
}
|
|
||||||
return examPrepLessonToDomain(l), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExamPrepUnitModuleLesson(ctx context.Context, id int64) error {
|
|
||||||
return s.queries.ExamPrepDeleteUnitModuleLesson(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func examPrepModuleToDomain(m dbgen.ExamPrepUnitModule) domain.ExamPrepModule {
|
|
||||||
out := domain.ExamPrepModule{
|
|
||||||
ID: m.ID,
|
|
||||||
UnitID: m.UnitID,
|
|
||||||
Name: m.Name,
|
|
||||||
SortOrder: int(m.SortOrder),
|
|
||||||
}
|
|
||||||
out.Description = fromPgText(m.Description)
|
|
||||||
out.Thumbnail = fromPgText(m.Thumbnail)
|
|
||||||
out.Icon = fromPgText(m.Icon)
|
|
||||||
out.CreatedAt = m.CreatedAt.Time
|
|
||||||
if m.UpdatedAt.Valid {
|
|
||||||
t := m.UpdatedAt.Time
|
|
||||||
out.UpdatedAt = &t
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateExamPrepUnitModule(ctx context.Context, unitID int64, input domain.CreateExamPrepModuleInput) (domain.ExamPrepModule, error) {
|
|
||||||
m, err := s.queries.ExamPrepCreateUnitModule(ctx, dbgen.ExamPrepCreateUnitModuleParams{
|
|
||||||
UnitID: unitID,
|
|
||||||
Name: input.Name,
|
|
||||||
Description: toPgText(input.Description),
|
|
||||||
Thumbnail: toPgText(input.Thumbnail),
|
|
||||||
Icon: toPgText(input.Icon),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return domain.ExamPrepModule{}, err
|
|
||||||
}
|
|
||||||
return examPrepModuleToDomain(m), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetExamPrepUnitModuleByID(ctx context.Context, id int64) (domain.ExamPrepModule, error) {
|
|
||||||
m, err := s.queries.ExamPrepGetUnitModuleByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepModule{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.ExamPrepModule{}, err
|
|
||||||
}
|
|
||||||
return examPrepModuleToDomain(m), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListExamPrepUnitModulesByUnit(ctx context.Context, unitID int64, limit, offset int32) ([]domain.ExamPrepModule, int64, error) {
|
|
||||||
rows, err := s.queries.ExamPrepListUnitModulesByUnit(ctx, dbgen.ExamPrepListUnitModulesByUnitParams{
|
|
||||||
UnitID: unitID,
|
|
||||||
Limit: limit,
|
|
||||||
Offset: offset,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return []domain.ExamPrepModule{}, 0, nil
|
|
||||||
}
|
|
||||||
var total int64
|
|
||||||
out := make([]domain.ExamPrepModule, 0, len(rows))
|
|
||||||
for i, r := range rows {
|
|
||||||
if i == 0 {
|
|
||||||
total = r.TotalCount
|
|
||||||
}
|
|
||||||
out = append(out, examPrepModuleToDomain(dbgen.ExamPrepUnitModule{
|
|
||||||
ID: r.ID,
|
|
||||||
UnitID: r.UnitID,
|
|
||||||
Name: r.Name,
|
|
||||||
Description: r.Description,
|
|
||||||
Thumbnail: r.Thumbnail,
|
|
||||||
Icon: r.Icon,
|
|
||||||
SortOrder: r.SortOrder,
|
|
||||||
CreatedAt: r.CreatedAt,
|
|
||||||
UpdatedAt: r.UpdatedAt,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return out, total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListExamPrepUnitModuleIDsByUnit(ctx context.Context, unitID int64) ([]int64, error) {
|
|
||||||
return s.queries.ExamPrepListUnitModuleIDsByUnit(ctx, unitID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateExamPrepUnitModule(ctx context.Context, id int64, input domain.UpdateExamPrepModuleInput) (domain.ExamPrepModule, error) {
|
|
||||||
var nameText pgtype.Text
|
|
||||||
if input.Name != nil {
|
|
||||||
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
|
||||||
} else {
|
|
||||||
nameText = pgtype.Text{Valid: false}
|
|
||||||
}
|
|
||||||
m, err := s.queries.ExamPrepUpdateUnitModule(ctx, dbgen.ExamPrepUpdateUnitModuleParams{
|
|
||||||
ID: id,
|
|
||||||
Name: nameText,
|
|
||||||
Description: optionalTextUpdate(input.Description),
|
|
||||||
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
|
||||||
Icon: optionalTextUpdate(input.Icon),
|
|
||||||
SortOrder: optionalInt4Update(input.SortOrder),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepModule{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.ExamPrepModule{}, err
|
|
||||||
}
|
|
||||||
return examPrepModuleToDomain(m), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExamPrepUnitModule(ctx context.Context, id int64) error {
|
|
||||||
return s.queries.ExamPrepDeleteUnitModule(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func examPrepUnitToDomain(u dbgen.ExamPrepUnit) domain.ExamPrepUnit {
|
|
||||||
out := domain.ExamPrepUnit{
|
|
||||||
ID: u.ID,
|
|
||||||
CatalogCourseID: u.CatalogCourseID,
|
|
||||||
Name: u.Name,
|
|
||||||
SortOrder: int(u.SortOrder),
|
|
||||||
}
|
|
||||||
out.Description = fromPgText(u.Description)
|
|
||||||
out.Thumbnail = fromPgText(u.Thumbnail)
|
|
||||||
out.CreatedAt = u.CreatedAt.Time
|
|
||||||
if u.UpdatedAt.Valid {
|
|
||||||
t := u.UpdatedAt.Time
|
|
||||||
out.UpdatedAt = &t
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CreateExamPrepUnit(ctx context.Context, catalogCourseID int64, input domain.CreateExamPrepUnitInput) (domain.ExamPrepUnit, error) {
|
|
||||||
u, err := s.queries.ExamPrepCreateUnit(ctx, dbgen.ExamPrepCreateUnitParams{
|
|
||||||
CatalogCourseID: catalogCourseID,
|
|
||||||
Name: input.Name,
|
|
||||||
Description: toPgText(input.Description),
|
|
||||||
Thumbnail: toPgText(input.Thumbnail),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return domain.ExamPrepUnit{}, err
|
|
||||||
}
|
|
||||||
return examPrepUnitToDomain(u), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetExamPrepUnitByID(ctx context.Context, id int64) (domain.ExamPrepUnit, error) {
|
|
||||||
u, err := s.queries.ExamPrepGetUnitByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepUnit{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.ExamPrepUnit{}, err
|
|
||||||
}
|
|
||||||
return examPrepUnitToDomain(u), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListExamPrepUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, limit, offset int32) ([]domain.ExamPrepUnit, int64, error) {
|
|
||||||
rows, err := s.queries.ExamPrepListUnitsByCatalogCourse(ctx, dbgen.ExamPrepListUnitsByCatalogCourseParams{
|
|
||||||
CatalogCourseID: catalogCourseID,
|
|
||||||
Limit: limit,
|
|
||||||
Offset: offset,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if len(rows) == 0 {
|
|
||||||
return []domain.ExamPrepUnit{}, 0, nil
|
|
||||||
}
|
|
||||||
var total int64
|
|
||||||
out := make([]domain.ExamPrepUnit, 0, len(rows))
|
|
||||||
for i, r := range rows {
|
|
||||||
if i == 0 {
|
|
||||||
total = r.TotalCount
|
|
||||||
}
|
|
||||||
out = append(out, examPrepUnitToDomain(dbgen.ExamPrepUnit{
|
|
||||||
ID: r.ID,
|
|
||||||
CatalogCourseID: r.CatalogCourseID,
|
|
||||||
Name: r.Name,
|
|
||||||
Description: r.Description,
|
|
||||||
Thumbnail: r.Thumbnail,
|
|
||||||
SortOrder: r.SortOrder,
|
|
||||||
CreatedAt: r.CreatedAt,
|
|
||||||
UpdatedAt: r.UpdatedAt,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return out, total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ListExamPrepUnitIDsByCatalogCourse(ctx context.Context, catalogCourseID int64) ([]int64, error) {
|
|
||||||
return s.queries.ExamPrepListUnitIDsByCatalogCourse(ctx, catalogCourseID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateExamPrepUnit(ctx context.Context, id int64, input domain.UpdateExamPrepUnitInput) (domain.ExamPrepUnit, error) {
|
|
||||||
var nameText pgtype.Text
|
|
||||||
if input.Name != nil {
|
|
||||||
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
|
||||||
} else {
|
|
||||||
nameText = pgtype.Text{Valid: false}
|
|
||||||
}
|
|
||||||
u, err := s.queries.ExamPrepUpdateUnit(ctx, dbgen.ExamPrepUpdateUnitParams{
|
|
||||||
ID: id,
|
|
||||||
Name: nameText,
|
|
||||||
Description: optionalTextUpdate(input.Description),
|
|
||||||
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
|
||||||
SortOrder: optionalInt4Update(input.SortOrder),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepUnit{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.ExamPrepUnit{}, err
|
|
||||||
}
|
|
||||||
return examPrepUnitToDomain(u), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteExamPrepUnit(ctx context.Context, id int64) error {
|
|
||||||
return s.queries.ExamPrepDeleteUnit(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
@ -38,89 +38,50 @@ func (s *Store) LmsUserHasLessonProgress(ctx context.Context, userID, lessonID i
|
||||||
return s.queries.UserHasLessonProgress(ctx, dbgen.UserHasLessonProgressParams{UserID: userID, LessonID: lessonID})
|
return s.queries.UserHasLessonProgress(ctx, dbgen.UserHasLessonProgressParams{UserID: userID, LessonID: lessonID})
|
||||||
}
|
}
|
||||||
|
|
||||||
// LmsUserLessonProgressInModule returns combined completed/total counts for lessons + published practices in a module.
|
// LmsUserLessonProgressInModule returns completed and total lesson counts in a module (for progress UI).
|
||||||
func (s *Store) LmsUserLessonProgressInModule(ctx context.Context, userID, moduleID int64) (completed, total int32, err error) {
|
func (s *Store) LmsUserLessonProgressInModule(ctx context.Context, userID, moduleID int64) (completed, total int32, err error) {
|
||||||
lessonTotal, err := s.queries.CountLessonsInModule(ctx, moduleID)
|
total, err = s.queries.CountLessonsInModule(ctx, moduleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
lessonCompleted, err := s.queries.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{
|
completed, err = s.queries.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{
|
||||||
ModuleID: moduleID,
|
ModuleID: moduleID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
practiceTotal, err := s.queries.CountPublishedPracticesInModule(ctx, toPgInt8(&moduleID))
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
practiceCompleted, err := s.queries.CountUserCompletedPublishedPracticesInModule(ctx, dbgen.CountUserCompletedPublishedPracticesInModuleParams{
|
|
||||||
ModuleID: toPgInt8(&moduleID),
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
total = lessonTotal + practiceTotal
|
|
||||||
completed = lessonCompleted + practiceCompleted
|
|
||||||
return completed, total, nil
|
return completed, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LmsUserLessonProgressInCourse returns combined completed/total counts for lessons + published practices in a course.
|
// LmsUserLessonProgressInCourse returns completed and total lesson counts in a course (all modules).
|
||||||
func (s *Store) LmsUserLessonProgressInCourse(ctx context.Context, userID, courseID int64) (completed, total int32, err error) {
|
func (s *Store) LmsUserLessonProgressInCourse(ctx context.Context, userID, courseID int64) (completed, total int32, err error) {
|
||||||
lessonTotal, err := s.queries.CountLessonsInCourse(ctx, courseID)
|
total, err = s.queries.CountLessonsInCourse(ctx, courseID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
lessonCompleted, err := s.queries.CountUserCompletedLessonsInCourse(ctx, dbgen.CountUserCompletedLessonsInCourseParams{
|
completed, err = s.queries.CountUserCompletedLessonsInCourse(ctx, dbgen.CountUserCompletedLessonsInCourseParams{
|
||||||
CourseID: courseID,
|
CourseID: courseID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
practiceTotal, err := s.queries.CountPublishedPracticesInCourse(ctx, toPgInt8(&courseID))
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
practiceCompleted, err := s.queries.CountUserCompletedPublishedPracticesInCourse(ctx, dbgen.CountUserCompletedPublishedPracticesInCourseParams{
|
|
||||||
CourseID: toPgInt8(&courseID),
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
total = lessonTotal + practiceTotal
|
|
||||||
completed = lessonCompleted + practiceCompleted
|
|
||||||
return completed, total, nil
|
return completed, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LmsUserLessonProgressInProgram returns combined completed/total counts for lessons + published practices in a program.
|
// LmsUserLessonProgressInProgram returns completed and total lesson counts in a program (all courses).
|
||||||
func (s *Store) LmsUserLessonProgressInProgram(ctx context.Context, userID, programID int64) (completed, total int32, err error) {
|
func (s *Store) LmsUserLessonProgressInProgram(ctx context.Context, userID, programID int64) (completed, total int32, err error) {
|
||||||
lessonTotal, err := s.queries.CountLessonsInProgram(ctx, programID)
|
total, err = s.queries.CountLessonsInProgram(ctx, programID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
lessonCompleted, err := s.queries.CountUserCompletedLessonsInProgram(ctx, dbgen.CountUserCompletedLessonsInProgramParams{
|
completed, err = s.queries.CountUserCompletedLessonsInProgram(ctx, dbgen.CountUserCompletedLessonsInProgramParams{
|
||||||
ProgramID: programID,
|
ProgramID: programID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
practiceTotal, err := s.queries.CountPublishedPracticesInProgram(ctx, programID)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
practiceCompleted, err := s.queries.CountUserCompletedPublishedPracticesInProgram(ctx, dbgen.CountUserCompletedPublishedPracticesInProgramParams{
|
|
||||||
ProgramID: programID,
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
total = lessonTotal + practiceTotal
|
|
||||||
completed = lessonCompleted + practiceCompleted
|
|
||||||
return completed, total, nil
|
return completed, total, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, lim
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
total = r.TotalCount
|
total = r.TotalCount
|
||||||
}
|
}
|
||||||
co := courseToDomain(dbgen.Course{
|
out = append(out, courseToDomain(dbgen.Course{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
ProgramID: r.ProgramID,
|
ProgramID: r.ProgramID,
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
|
|
@ -83,11 +83,7 @@ func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, lim
|
||||||
CreatedAt: r.CreatedAt,
|
CreatedAt: r.CreatedAt,
|
||||||
UpdatedAt: r.UpdatedAt,
|
UpdatedAt: r.UpdatedAt,
|
||||||
SortOrder: r.SortOrder,
|
SortOrder: r.SortOrder,
|
||||||
})
|
}))
|
||||||
co.ModuleCount = int(r.ModuleCount)
|
|
||||||
co.LessonCount = int(r.LessonCount)
|
|
||||||
co.PracticeCount = int(r.PracticeCount)
|
|
||||||
out = append(out, co)
|
|
||||||
}
|
}
|
||||||
return out, total, nil
|
return out, total, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,78 +93,13 @@ func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, co
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) {
|
func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) {
|
||||||
q, tx, err := s.BeginTx(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return domain.Module{}, err
|
|
||||||
}
|
|
||||||
defer tx.Rollback(ctx)
|
|
||||||
|
|
||||||
var current dbgen.Module
|
|
||||||
err = tx.QueryRow(ctx, `
|
|
||||||
SELECT id, program_id, course_id, name, description, icon, sort_order, created_at, updated_at
|
|
||||||
FROM modules
|
|
||||||
WHERE id = $1
|
|
||||||
FOR UPDATE
|
|
||||||
`, id).Scan(
|
|
||||||
¤t.ID,
|
|
||||||
¤t.ProgramID,
|
|
||||||
¤t.CourseID,
|
|
||||||
¤t.Name,
|
|
||||||
¤t.Description,
|
|
||||||
¤t.Icon,
|
|
||||||
¤t.SortOrder,
|
|
||||||
¤t.CreatedAt,
|
|
||||||
¤t.UpdatedAt,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.Module{}, pgx.ErrNoRows
|
|
||||||
}
|
|
||||||
return domain.Module{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.SortOrder != nil {
|
|
||||||
targetSort := int32(*input.SortOrder)
|
|
||||||
if targetSort != current.SortOrder {
|
|
||||||
var conflictID int64
|
|
||||||
err = tx.QueryRow(ctx, `
|
|
||||||
SELECT id
|
|
||||||
FROM modules
|
|
||||||
WHERE course_id = $1
|
|
||||||
AND sort_order = $2
|
|
||||||
AND id <> $3
|
|
||||||
FOR UPDATE
|
|
||||||
`, current.CourseID, targetSort, id).Scan(&conflictID)
|
|
||||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.Module{}, err
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
var tempSort int32
|
|
||||||
if err := tx.QueryRow(ctx, `
|
|
||||||
SELECT COALESCE(MIN(sort_order), 0) - 1
|
|
||||||
FROM modules
|
|
||||||
WHERE course_id = $1
|
|
||||||
`, current.CourseID).Scan(&tempSort); err != nil {
|
|
||||||
return domain.Module{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := tx.Exec(ctx, `UPDATE modules SET sort_order = $1 WHERE id = $2`, tempSort, id); err != nil {
|
|
||||||
return domain.Module{}, err
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec(ctx, `UPDATE modules SET sort_order = $1 WHERE id = $2`, current.SortOrder, conflictID); err != nil {
|
|
||||||
return domain.Module{}, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var nameText pgtype.Text
|
var nameText pgtype.Text
|
||||||
if input.Name != nil {
|
if input.Name != nil {
|
||||||
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
||||||
} else {
|
} else {
|
||||||
nameText = pgtype.Text{Valid: false}
|
nameText = pgtype.Text{Valid: false}
|
||||||
}
|
}
|
||||||
m, err := q.UpdateModule(ctx, dbgen.UpdateModuleParams{
|
m, err := s.queries.UpdateModule(ctx, dbgen.UpdateModuleParams{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: nameText,
|
Name: nameText,
|
||||||
Description: optionalTextUpdate(input.Description),
|
Description: optionalTextUpdate(input.Description),
|
||||||
|
|
@ -177,10 +112,6 @@ WHERE course_id = $1
|
||||||
}
|
}
|
||||||
return domain.Module{}, err
|
return domain.Module{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
|
||||||
return domain.Module{}, err
|
|
||||||
}
|
|
||||||
return moduleToDomain(m), nil
|
return moduleToDomain(m), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import (
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CompleteLessonForUser records lesson completion and cascades completion upward when
|
// CompleteLessonForUser records lesson completion and cascades to module, course, and program when the user
|
||||||
// both lesson and related practice requirements are satisfied.
|
// has fully completed the preceding scope. Runs in a single transaction.
|
||||||
func (s *Store) CompleteLessonForUser(ctx context.Context, userID, lessonID int64) error {
|
func (s *Store) CompleteLessonForUser(ctx context.Context, userID, lessonID int64) error {
|
||||||
q, tx, err := s.BeginTx(ctx)
|
q, tx, err := s.BeginTx(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -31,162 +31,56 @@ func (s *Store) CompleteLessonForUser(ctx context.Context, userID, lessonID int6
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
nLess, err := q.CountLessonsInModule(ctx, lesson.ModuleID)
|
||||||
if err := s.cascadeLMSCompletion(ctx, q, userID, mod.ID, crs.ID, crs.ProgramID); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
nDoneLess, err := q.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{
|
||||||
|
ModuleID: lesson.ModuleID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if nLess > 0 && nDoneLess >= nLess {
|
||||||
|
if err := q.InsertUserModuleProgress(ctx, dbgen.InsertUserModuleProgressParams{UserID: userID, ModuleID: mod.ID}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nMods, err := q.CountModulesInCourse(ctx, mod.CourseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nDoneMods, err := q.CountUserCompletedModulesInCourse(ctx, dbgen.CountUserCompletedModulesInCourseParams{
|
||||||
|
CourseID: mod.CourseID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if nMods > 0 && nDoneMods >= nMods {
|
||||||
|
if err := q.InsertUserCourseProgress(ctx, dbgen.InsertUserCourseProgressParams{UserID: userID, CourseID: crs.ID}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nCr, err := q.CountCoursesInProgram(ctx, crs.ProgramID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nCrDone, err := q.CountUserCompletedCoursesInProgram(ctx, dbgen.CountUserCompletedCoursesInProgramParams{
|
||||||
|
ProgramID: crs.ProgramID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if nCr > 0 && nCrDone >= nCr {
|
||||||
|
if err := q.InsertUserProgramProgress(ctx, dbgen.InsertUserProgramProgressParams{UserID: userID, ProgramID: crs.ProgramID}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := tx.Commit(ctx); err != nil {
|
if err := tx.Commit(ctx); err != nil {
|
||||||
return fmt.Errorf("commit: %w", err)
|
return fmt.Errorf("commit: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompletePracticeForUser records practice completion and cascades completion upward when
|
|
||||||
// both lesson and related practice requirements are satisfied.
|
|
||||||
func (s *Store) CompletePracticeForUser(ctx context.Context, userID, questionSetID int64) error {
|
|
||||||
q, tx, err := s.BeginTx(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("begin tx: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = tx.Rollback(ctx) }()
|
|
||||||
|
|
||||||
if _, err := q.MarkPracticeCompleted(ctx, dbgen.MarkPracticeCompletedParams{
|
|
||||||
UserID: userID,
|
|
||||||
QuestionSetID: questionSetID,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
scope, err := q.GetPracticeScopeByQuestionSetID(ctx, questionSetID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !scope.ModuleID.Valid {
|
|
||||||
return fmt.Errorf("practice %d is not linked to a module", questionSetID)
|
|
||||||
}
|
|
||||||
|
|
||||||
mod, err := q.GetModuleByID(ctx, scope.ModuleID.Int64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
crs, err := q.GetCourseByID(ctx, mod.CourseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.cascadeLMSCompletion(ctx, q, userID, mod.ID, crs.ID, crs.ProgramID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(ctx); err != nil {
|
|
||||||
return fmt.Errorf("commit: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) cascadeLMSCompletion(ctx context.Context, q *dbgen.Queries, userID, moduleID, courseID, programID int64) error {
|
|
||||||
moduleLessonsTotal, err := q.CountLessonsInModule(ctx, moduleID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
moduleLessonsDone, err := q.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{
|
|
||||||
ModuleID: moduleID,
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
modulePracticesTotal, err := q.CountPublishedPracticesInModule(ctx, toPgInt8(&moduleID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
modulePracticesDone, err := q.CountUserCompletedPublishedPracticesInModule(ctx, dbgen.CountUserCompletedPublishedPracticesInModuleParams{
|
|
||||||
ModuleID: toPgInt8(&moduleID),
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
moduleLessonsComplete := moduleLessonsTotal > 0 && moduleLessonsDone >= moduleLessonsTotal
|
|
||||||
modulePracticesComplete := modulePracticesDone >= modulePracticesTotal
|
|
||||||
if !moduleLessonsComplete || !modulePracticesComplete {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := q.InsertUserModuleProgress(ctx, dbgen.InsertUserModuleProgressParams{UserID: userID, ModuleID: moduleID}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
nMods, err := q.CountModulesInCourse(ctx, courseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
nDoneMods, err := q.CountUserCompletedModulesInCourse(ctx, dbgen.CountUserCompletedModulesInCourseParams{
|
|
||||||
CourseID: courseID,
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
coursePracticesTotal, err := q.CountPublishedPracticesInCourse(ctx, toPgInt8(&courseID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
coursePracticesDone, err := q.CountUserCompletedPublishedPracticesInCourse(ctx, dbgen.CountUserCompletedPublishedPracticesInCourseParams{
|
|
||||||
CourseID: toPgInt8(&courseID),
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
courseModulesComplete := nMods > 0 && nDoneMods >= nMods
|
|
||||||
coursePracticesComplete := coursePracticesDone >= coursePracticesTotal
|
|
||||||
if !courseModulesComplete || !coursePracticesComplete {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := q.InsertUserCourseProgress(ctx, dbgen.InsertUserCourseProgressParams{UserID: userID, CourseID: courseID}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
nCr, err := q.CountCoursesInProgram(ctx, programID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
nCrDone, err := q.CountUserCompletedCoursesInProgram(ctx, dbgen.CountUserCompletedCoursesInProgramParams{
|
|
||||||
ProgramID: programID,
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
programPracticesTotal, err := q.CountPublishedPracticesInProgram(ctx, programID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
programPracticesDone, err := q.CountUserCompletedPublishedPracticesInProgram(ctx, dbgen.CountUserCompletedPublishedPracticesInProgramParams{
|
|
||||||
ProgramID: programID,
|
|
||||||
UserID: userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
programCoursesComplete := nCr > 0 && nCrDone >= nCr
|
|
||||||
programPracticesComplete := programPracticesDone >= programPracticesTotal
|
|
||||||
if !programCoursesComplete || !programPracticesComplete {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := q.InsertUserProgramProgress(ctx, dbgen.InsertUserProgramProgressParams{UserID: userID, ProgramID: programID}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,6 @@ func (s *Store) GetOtp(ctx context.Context, userID int64) (domain.Otp, error) {
|
||||||
}
|
}
|
||||||
return domain.Otp{}, err
|
return domain.Otp{}, err
|
||||||
}
|
}
|
||||||
if !row.ExpiresAt.Valid {
|
|
||||||
return domain.Otp{}, domain.ErrOtpNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain.Otp{
|
return domain.Otp{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
UserID: row.UserID,
|
UserID: row.UserID,
|
||||||
|
|
@ -67,36 +63,6 @@ func (s *Store) GetOtp(ctx context.Context, userID int64) (domain.Otp, error) {
|
||||||
ExpiresAt: row.ExpiresAt.Time,
|
ExpiresAt: row.ExpiresAt.Time,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetOtpByCode(ctx context.Context, userID int64, otpCode string) (domain.Otp, error) {
|
|
||||||
row, err := s.queries.GetOtpByCode(ctx, dbgen.GetOtpByCodeParams{
|
|
||||||
UserID: userID,
|
|
||||||
Otp: otpCode,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
return domain.Otp{}, domain.ErrOtpNotFound
|
|
||||||
}
|
|
||||||
return domain.Otp{}, err
|
|
||||||
}
|
|
||||||
if !row.ExpiresAt.Valid {
|
|
||||||
return domain.Otp{}, domain.ErrOtpNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain.Otp{
|
|
||||||
ID: row.ID,
|
|
||||||
UserID: row.UserID,
|
|
||||||
SentTo: row.SentTo,
|
|
||||||
Medium: domain.OtpMedium(row.Medium),
|
|
||||||
For: domain.OtpFor(row.OtpFor),
|
|
||||||
Otp: row.Otp,
|
|
||||||
Used: row.Used,
|
|
||||||
UsedAt: row.UsedAt.Time,
|
|
||||||
CreatedAt: row.CreatedAt.Time,
|
|
||||||
ExpiresAt: row.ExpiresAt.Time,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error {
|
func (s *Store) MarkOtpAsUsed(ctx context.Context, otp domain.Otp) error {
|
||||||
return s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{
|
return s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{
|
||||||
ID: otp.ID,
|
ID: otp.ID,
|
||||||
|
|
|
||||||
|
|
@ -607,19 +607,6 @@ func (s *Store) GetQuestionSetsByType(ctx context.Context, setType string, limit
|
||||||
UpdatedAt: timePtr(r.UpdatedAt),
|
UpdatedAt: timePtr(r.UpdatedAt),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// COUNT(*) OVER() only appears when at least one row is returned.
|
|
||||||
// For out-of-range offsets, fetch total count explicitly so pagination metadata stays correct.
|
|
||||||
if len(rows) == 0 {
|
|
||||||
err = s.conn.QueryRow(
|
|
||||||
ctx,
|
|
||||||
`SELECT COUNT(*) FROM question_sets WHERE set_type = $1 AND status != 'ARCHIVED'`,
|
|
||||||
setType,
|
|
||||||
).Scan(&totalCount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, totalCount, nil
|
return result, totalCount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -737,17 +724,17 @@ func (s *Store) GetQuestionSetItems(ctx context.Context, setID int64) ([]domain.
|
||||||
QuestionID: r.QuestionID,
|
QuestionID: r.QuestionID,
|
||||||
DisplayOrder: r.DisplayOrder,
|
DisplayOrder: r.DisplayOrder,
|
||||||
},
|
},
|
||||||
QuestionText: r.QuestionText,
|
QuestionText: r.QuestionText,
|
||||||
QuestionType: r.QuestionType,
|
QuestionType: r.QuestionType,
|
||||||
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
||||||
Points: r.Points,
|
Points: r.Points,
|
||||||
Explanation: fromPgText(r.Explanation),
|
Explanation: fromPgText(r.Explanation),
|
||||||
Tips: fromPgText(r.Tips),
|
Tips: fromPgText(r.Tips),
|
||||||
VoicePrompt: fromPgText(r.VoicePrompt),
|
VoicePrompt: fromPgText(r.VoicePrompt),
|
||||||
SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt),
|
SampleAnswerVoicePrompt: nil,
|
||||||
ImageURL: fromPgText(r.ImageUrl),
|
ImageURL: fromPgText(r.ImageUrl),
|
||||||
AudioCorrectAnswerText: fromPgText(r.AudioCorrectAnswerText),
|
AudioCorrectAnswerText: nil,
|
||||||
QuestionStatus: r.QuestionStatus,
|
QuestionStatus: r.QuestionStatus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|
@ -812,17 +799,15 @@ func (s *Store) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]
|
||||||
QuestionID: r.QuestionID,
|
QuestionID: r.QuestionID,
|
||||||
DisplayOrder: r.DisplayOrder,
|
DisplayOrder: r.DisplayOrder,
|
||||||
},
|
},
|
||||||
QuestionText: r.QuestionText,
|
QuestionText: r.QuestionText,
|
||||||
QuestionType: r.QuestionType,
|
QuestionType: r.QuestionType,
|
||||||
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
DifficultyLevel: fromPgText(r.DifficultyLevel),
|
||||||
Points: r.Points,
|
Points: r.Points,
|
||||||
Explanation: fromPgText(r.Explanation),
|
Explanation: fromPgText(r.Explanation),
|
||||||
Tips: fromPgText(r.Tips),
|
Tips: fromPgText(r.Tips),
|
||||||
VoicePrompt: fromPgText(r.VoicePrompt),
|
VoicePrompt: fromPgText(r.VoicePrompt),
|
||||||
SampleAnswerVoicePrompt: fromPgText(r.SampleAnswerVoicePrompt),
|
ImageURL: fromPgText(r.ImageUrl),
|
||||||
ImageURL: fromPgText(r.ImageUrl),
|
QuestionStatus: "PUBLISHED",
|
||||||
AudioCorrectAnswerText: fromPgText(r.AudioCorrectAnswerText),
|
|
||||||
QuestionStatus: "PUBLISHED",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|
|
||||||
|
|
@ -95,13 +95,9 @@ func (s *Service) VerifyOtp(
|
||||||
return domain.LoginSuccess{}, err
|
return domain.LoginSuccess{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Retrieve OTP row matching submitted code.
|
// 1. Retrieve OTP
|
||||||
// This avoids false positives when another OTP row exists for the same user.
|
storedOtp, err := s.otpStore.GetOtp(ctx, user.ID)
|
||||||
storedOtp, err := s.otpStore.GetOtpByCode(ctx, user.ID, otpCode)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, domain.ErrOtpNotFound) {
|
|
||||||
return domain.LoginSuccess{}, domain.ErrInvalidOtp
|
|
||||||
}
|
|
||||||
return domain.LoginSuccess{}, err
|
return domain.LoginSuccess{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +111,12 @@ func (s *Service) VerifyOtp(
|
||||||
return domain.LoginSuccess{}, domain.ErrOtpExpired
|
return domain.LoginSuccess{}, domain.ErrOtpExpired
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Mark OTP as used
|
// 4. Invalid
|
||||||
|
if storedOtp.Otp != otpCode {
|
||||||
|
return domain.LoginSuccess{}, domain.ErrInvalidOtp
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Mark OTP as used
|
||||||
storedOtp.Used = true
|
storedOtp.Used = true
|
||||||
storedOtp.UsedAt = timePtr(time.Now())
|
storedOtp.UsedAt = timePtr(time.Now())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,407 +0,0 @@
|
||||||
package examprep
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"Yimaru-Backend/internal/ports"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrCatalogCourseNotFound = errors.New("exam prep catalog course not found")
|
|
||||||
var ErrUnitNotFound = errors.New("exam prep unit not found")
|
|
||||||
var ErrModuleNotFound = errors.New("exam prep module not found")
|
|
||||||
var ErrLessonNotFound = errors.New("exam prep lesson not found")
|
|
||||||
var ErrPracticeNotFound = errors.New("exam prep practice not found")
|
|
||||||
|
|
||||||
// examPrepStore is implemented by *repository.Store (catalog courses, units, modules, lessons, practices).
|
|
||||||
type examPrepStore interface {
|
|
||||||
ports.ExamPrepCatalogCourseStore
|
|
||||||
ports.ExamPrepUnitStore
|
|
||||||
ports.ExamPrepModuleStore
|
|
||||||
ports.ExamPrepLessonStore
|
|
||||||
ports.ExamPrepPracticeStore
|
|
||||||
}
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
store examPrepStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService(store examPrepStore) *Service {
|
|
||||||
return &Service{store: store}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateCatalogCourse(ctx context.Context, input domain.CreateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
|
|
||||||
return s.store.CreateExamPrepCatalogCourse(ctx, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetCatalogCourseByID(ctx context.Context, id int64) (domain.ExamPrepCatalogCourse, error) {
|
|
||||||
c, err := s.store.GetExamPrepCatalogCourseByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepCatalogCourse{}, ErrCatalogCourseNotFound
|
|
||||||
}
|
|
||||||
return domain.ExamPrepCatalogCourse{}, err
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListCatalogCourses(ctx context.Context, limit, offset int32) ([]domain.ExamPrepCatalogCourse, int64, error) {
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = 20
|
|
||||||
}
|
|
||||||
if limit > 200 {
|
|
||||||
limit = 200
|
|
||||||
}
|
|
||||||
if offset < 0 {
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
return s.store.ListExamPrepCatalogCourses(ctx, limit, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) UpdateCatalogCourse(ctx context.Context, id int64, input domain.UpdateExamPrepCatalogCourseInput) (domain.ExamPrepCatalogCourse, error) {
|
|
||||||
c, err := s.store.UpdateExamPrepCatalogCourse(ctx, id, input)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepCatalogCourse{}, ErrCatalogCourseNotFound
|
|
||||||
}
|
|
||||||
return domain.ExamPrepCatalogCourse{}, err
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) DeleteCatalogCourse(ctx context.Context, id int64) error {
|
|
||||||
if _, err := s.store.GetExamPrepCatalogCourseByID(ctx, id); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrCatalogCourseNotFound
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.store.DeleteExamPrepCatalogCourse(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ReorderCatalogCourses(ctx context.Context, ordered []int64) error {
|
|
||||||
expected, err := s.store.ListAllExamPrepCatalogCourseIDs(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := domain.ValidateReorderPermutation(ordered, expected); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(ordered) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.store.ReorderExamPrepCatalogCourses(ctx, ordered)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ensureCatalogCourse(ctx context.Context, catalogCourseID int64) error {
|
|
||||||
if _, err := s.store.GetExamPrepCatalogCourseByID(ctx, catalogCourseID); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrCatalogCourseNotFound
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateUnit(ctx context.Context, catalogCourseID int64, input domain.CreateExamPrepUnitInput) (domain.ExamPrepUnit, error) {
|
|
||||||
if err := s.ensureCatalogCourse(ctx, catalogCourseID); err != nil {
|
|
||||||
return domain.ExamPrepUnit{}, err
|
|
||||||
}
|
|
||||||
return s.store.CreateExamPrepUnit(ctx, catalogCourseID, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListUnitsByCatalogCourse(ctx context.Context, catalogCourseID int64, limit, offset int32) ([]domain.ExamPrepUnit, int64, error) {
|
|
||||||
if err := s.ensureCatalogCourse(ctx, catalogCourseID); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = 20
|
|
||||||
}
|
|
||||||
if limit > 200 {
|
|
||||||
limit = 200
|
|
||||||
}
|
|
||||||
if offset < 0 {
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
return s.store.ListExamPrepUnitsByCatalogCourse(ctx, catalogCourseID, limit, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetUnitByID(ctx context.Context, id int64) (domain.ExamPrepUnit, error) {
|
|
||||||
u, err := s.store.GetExamPrepUnitByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepUnit{}, ErrUnitNotFound
|
|
||||||
}
|
|
||||||
return domain.ExamPrepUnit{}, err
|
|
||||||
}
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) UpdateUnit(ctx context.Context, id int64, input domain.UpdateExamPrepUnitInput) (domain.ExamPrepUnit, error) {
|
|
||||||
u, err := s.store.UpdateExamPrepUnit(ctx, id, input)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepUnit{}, ErrUnitNotFound
|
|
||||||
}
|
|
||||||
return domain.ExamPrepUnit{}, err
|
|
||||||
}
|
|
||||||
return u, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) DeleteUnit(ctx context.Context, id int64) error {
|
|
||||||
if _, err := s.store.GetExamPrepUnitByID(ctx, id); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrUnitNotFound
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.store.DeleteExamPrepUnit(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ReorderUnitsInCatalogCourse(ctx context.Context, catalogCourseID int64, ordered []int64) error {
|
|
||||||
if err := s.ensureCatalogCourse(ctx, catalogCourseID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
expected, err := s.store.ListExamPrepUnitIDsByCatalogCourse(ctx, catalogCourseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := domain.ValidateReorderPermutation(ordered, expected); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(ordered) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.store.ReorderExamPrepUnitsInCatalogCourse(ctx, catalogCourseID, ordered)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ensureUnit(ctx context.Context, unitID int64) error {
|
|
||||||
if _, err := s.store.GetExamPrepUnitByID(ctx, unitID); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrUnitNotFound
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateModule(ctx context.Context, unitID int64, input domain.CreateExamPrepModuleInput) (domain.ExamPrepModule, error) {
|
|
||||||
if err := s.ensureUnit(ctx, unitID); err != nil {
|
|
||||||
return domain.ExamPrepModule{}, err
|
|
||||||
}
|
|
||||||
return s.store.CreateExamPrepUnitModule(ctx, unitID, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListModulesByUnit(ctx context.Context, unitID int64, limit, offset int32) ([]domain.ExamPrepModule, int64, error) {
|
|
||||||
if err := s.ensureUnit(ctx, unitID); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = 20
|
|
||||||
}
|
|
||||||
if limit > 200 {
|
|
||||||
limit = 200
|
|
||||||
}
|
|
||||||
if offset < 0 {
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
return s.store.ListExamPrepUnitModulesByUnit(ctx, unitID, limit, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetModuleByID(ctx context.Context, id int64) (domain.ExamPrepModule, error) {
|
|
||||||
m, err := s.store.GetExamPrepUnitModuleByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepModule{}, ErrModuleNotFound
|
|
||||||
}
|
|
||||||
return domain.ExamPrepModule{}, err
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) UpdateModule(ctx context.Context, id int64, input domain.UpdateExamPrepModuleInput) (domain.ExamPrepModule, error) {
|
|
||||||
m, err := s.store.UpdateExamPrepUnitModule(ctx, id, input)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepModule{}, ErrModuleNotFound
|
|
||||||
}
|
|
||||||
return domain.ExamPrepModule{}, err
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) DeleteModule(ctx context.Context, id int64) error {
|
|
||||||
if _, err := s.store.GetExamPrepUnitModuleByID(ctx, id); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrModuleNotFound
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.store.DeleteExamPrepUnitModule(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ReorderModulesInUnit(ctx context.Context, unitID int64, ordered []int64) error {
|
|
||||||
if err := s.ensureUnit(ctx, unitID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
expected, err := s.store.ListExamPrepUnitModuleIDsByUnit(ctx, unitID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := domain.ValidateReorderPermutation(ordered, expected); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(ordered) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.store.ReorderExamPrepUnitModulesInUnit(ctx, unitID, ordered)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ensureModule(ctx context.Context, unitModuleID int64) error {
|
|
||||||
if _, err := s.store.GetExamPrepUnitModuleByID(ctx, unitModuleID); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrModuleNotFound
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateLesson(ctx context.Context, unitModuleID int64, input domain.CreateExamPrepLessonInput) (domain.ExamPrepLesson, error) {
|
|
||||||
if err := s.ensureModule(ctx, unitModuleID); err != nil {
|
|
||||||
return domain.ExamPrepLesson{}, err
|
|
||||||
}
|
|
||||||
return s.store.CreateExamPrepUnitModuleLesson(ctx, unitModuleID, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListLessonsByUnitModule(ctx context.Context, unitModuleID int64, limit, offset int32) ([]domain.ExamPrepLesson, int64, error) {
|
|
||||||
if err := s.ensureModule(ctx, unitModuleID); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = 20
|
|
||||||
}
|
|
||||||
if limit > 200 {
|
|
||||||
limit = 200
|
|
||||||
}
|
|
||||||
if offset < 0 {
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
return s.store.ListExamPrepUnitModuleLessonsByUnitModuleID(ctx, unitModuleID, limit, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetLessonByID(ctx context.Context, id int64) (domain.ExamPrepLesson, error) {
|
|
||||||
l, err := s.store.GetExamPrepUnitModuleLessonByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepLesson{}, ErrLessonNotFound
|
|
||||||
}
|
|
||||||
return domain.ExamPrepLesson{}, err
|
|
||||||
}
|
|
||||||
return l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) UpdateLesson(ctx context.Context, id int64, input domain.UpdateExamPrepLessonInput) (domain.ExamPrepLesson, error) {
|
|
||||||
l, err := s.store.UpdateExamPrepUnitModuleLesson(ctx, id, input)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepLesson{}, ErrLessonNotFound
|
|
||||||
}
|
|
||||||
return domain.ExamPrepLesson{}, err
|
|
||||||
}
|
|
||||||
return l, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) DeleteLesson(ctx context.Context, id int64) error {
|
|
||||||
if _, err := s.store.GetExamPrepUnitModuleLessonByID(ctx, id); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrLessonNotFound
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.store.DeleteExamPrepUnitModuleLesson(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ReorderLessonsInUnitModule(ctx context.Context, unitModuleID int64, ordered []int64) error {
|
|
||||||
if err := s.ensureModule(ctx, unitModuleID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
expected, err := s.store.ListExamPrepUnitModuleLessonIDsByUnitModule(ctx, unitModuleID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := domain.ValidateReorderPermutation(ordered, expected); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(ordered) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.store.ReorderExamPrepUnitModuleLessonsInUnitModule(ctx, unitModuleID, ordered)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ensureLesson(ctx context.Context, lessonID int64) error {
|
|
||||||
if _, err := s.store.GetExamPrepUnitModuleLessonByID(ctx, lessonID); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrLessonNotFound
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) CreateExamPrepPractice(ctx context.Context, lessonID int64, input domain.CreateExamPrepPracticeInput) (domain.ExamPrepPractice, error) {
|
|
||||||
if err := s.ensureLesson(ctx, lessonID); err != nil {
|
|
||||||
return domain.ExamPrepPractice{}, err
|
|
||||||
}
|
|
||||||
return s.store.CreateExamPrepLessonPractice(ctx, lessonID, input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) ListExamPrepPracticesByLesson(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.ExamPrepPractice, int64, error) {
|
|
||||||
if err := s.ensureLesson(ctx, lessonID); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = 20
|
|
||||||
}
|
|
||||||
if limit > 200 {
|
|
||||||
limit = 200
|
|
||||||
}
|
|
||||||
if offset < 0 {
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
return s.store.ListExamPrepLessonPracticesByLessonID(ctx, lessonID, limit, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetExamPrepPracticeByID(ctx context.Context, id int64) (domain.ExamPrepPractice, error) {
|
|
||||||
p, err := s.store.GetExamPrepLessonPracticeByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepPractice{}, ErrPracticeNotFound
|
|
||||||
}
|
|
||||||
return domain.ExamPrepPractice{}, err
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) UpdateExamPrepPractice(ctx context.Context, id int64, input domain.UpdateExamPrepPracticeInput) (domain.ExamPrepPractice, error) {
|
|
||||||
p, err := s.store.UpdateExamPrepLessonPractice(ctx, id, input)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return domain.ExamPrepPractice{}, ErrPracticeNotFound
|
|
||||||
}
|
|
||||||
return domain.ExamPrepPractice{}, err
|
|
||||||
}
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) DeleteExamPrepPractice(ctx context.Context, id int64) error {
|
|
||||||
if _, err := s.store.GetExamPrepLessonPracticeByID(ctx, id); err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrPracticeNotFound
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.store.DeleteExamPrepLessonPractice(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
@ -31,11 +31,6 @@ func (s *Service) CompleteLessonForUser(ctx context.Context, userID, lessonID in
|
||||||
return s.store.CompleteLessonForUser(ctx, userID, lessonID)
|
return s.store.CompleteLessonForUser(ctx, userID, lessonID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompletePracticeForUser records practice completion and rolls up to module, course, and program when applicable.
|
|
||||||
func (s *Service) CompletePracticeForUser(ctx context.Context, userID, questionSetID int64) error {
|
|
||||||
return s.store.CompletePracticeForUser(ctx, userID, questionSetID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMyProgress returns completed lesson, module, course, and program IDs for the user.
|
// GetMyProgress returns completed lesson, module, course, and program IDs for the user.
|
||||||
func (s *Service) GetMyProgress(ctx context.Context, userID int64) (domain.LMSUserProgress, error) {
|
func (s *Service) GetMyProgress(ctx context.Context, userID int64) (domain.LMSUserProgress, error) {
|
||||||
return s.store.GetLMSUserProgressSnapshot(ctx, userID)
|
return s.store.GetLMSUserProgressSnapshot(ctx, userID)
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,6 @@ func (s *Service) GetURL(ctx context.Context, objectKey string, expiry time.Dura
|
||||||
return s.client.GetFileURL(ctx, objectKey, expiry)
|
return s.client.GetFileURL(ctx, objectKey, expiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) BucketName() string {
|
|
||||||
return s.client.BucketName()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes a file from MinIO.
|
// Delete removes a file from MinIO.
|
||||||
func (s *Service) Delete(ctx context.Context, objectKey string) error {
|
func (s *Service) Delete(ctx context.Context, objectKey string) error {
|
||||||
s.logger.Info("Deleting file from MinIO", zap.String("object_key", objectKey))
|
s.logger.Info("Deleting file from MinIO", zap.String("object_key", objectKey))
|
||||||
|
|
|
||||||
|
|
@ -29,37 +29,6 @@ var AllPermissions = []domain.PermissionSeed{
|
||||||
{Key: "programs.delete", Name: "Delete Program", Description: "Delete a program", GroupName: "Programs"},
|
{Key: "programs.delete", Name: "Delete Program", Description: "Delete a program", GroupName: "Programs"},
|
||||||
{Key: "programs.reorder", Name: "Reorder Programs", Description: "Set program order for the learning path (batch)", GroupName: "Programs"},
|
{Key: "programs.reorder", Name: "Reorder Programs", Description: "Set program order for the learning path (batch)", GroupName: "Programs"},
|
||||||
|
|
||||||
// Exam prep (schema exam_prep — DET / IELTS / TOEFL tracks; separate from LMS Learn English)
|
|
||||||
{Key: "exam_prep.catalog_courses.create", Name: "Create Exam Prep Catalog Course", Description: "Create a top-level exam prep catalog entry", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.catalog_courses.list", Name: "List Exam Prep Catalog Courses", Description: "List exam prep catalog courses", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.catalog_courses.get", Name: "Get Exam Prep Catalog Course", Description: "Get an exam prep catalog course by ID", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.catalog_courses.update", Name: "Update Exam Prep Catalog Course", Description: "Update an exam prep catalog course", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.catalog_courses.delete", Name: "Delete Exam Prep Catalog Course", Description: "Delete an exam prep catalog course", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.catalog_courses.reorder", Name: "Reorder Exam Prep Catalog Courses", Description: "Set global order of exam prep catalog courses", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.units.create", Name: "Create Exam Prep Unit", Description: "Create a unit under a catalog course", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.units.list", Name: "List Exam Prep Units", Description: "List units under a catalog course", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.units.get", Name: "Get Exam Prep Unit", Description: "Get an exam prep unit by ID", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.units.update", Name: "Update Exam Prep Unit", Description: "Update an exam prep unit", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.units.delete", Name: "Delete Exam Prep Unit", Description: "Delete an exam prep unit", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.units.reorder", Name: "Reorder Exam Prep Units", Description: "Reorder units within a catalog course", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.modules.create", Name: "Create Exam Prep Module", Description: "Create a module under an exam prep unit", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.modules.list", Name: "List Exam Prep Modules", Description: "List modules under a unit", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.modules.get", Name: "Get Exam Prep Module", Description: "Get an exam prep module by ID", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.modules.update", Name: "Update Exam Prep Module", Description: "Update an exam prep module", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.modules.delete", Name: "Delete Exam Prep Module", Description: "Delete an exam prep module", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.modules.reorder", Name: "Reorder Exam Prep Modules", Description: "Reorder modules within a unit", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.lessons.create", Name: "Create Exam Prep Lesson", Description: "Create a lesson under an exam prep unit module", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.lessons.list_by_module", Name: "List Exam Prep Lessons by Module", Description: "List lessons under an exam prep unit module", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.lessons.get", Name: "Get Exam Prep Lesson", Description: "Get an exam prep lesson by ID", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.lessons.update", Name: "Update Exam Prep Lesson", Description: "Update an exam prep lesson", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.lessons.delete", Name: "Delete Exam Prep Lesson", Description: "Delete an exam prep lesson", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.lessons.reorder", Name: "Reorder Exam Prep Lessons", Description: "Reorder lessons within an exam prep unit module", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.practices.create", Name: "Create Exam Prep Practice", Description: "Create a practice under an exam prep lesson (references question_sets)", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.practices.list_by_lesson", Name: "List Exam Prep Practices by Lesson", Description: "List practices for an exam prep lesson", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.practices.get", Name: "Get Exam Prep Practice", Description: "Get an exam prep practice by ID", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.practices.update", Name: "Update Exam Prep Practice", Description: "Update an exam prep practice", GroupName: "Exam Prep"},
|
|
||||||
{Key: "exam_prep.practices.delete", Name: "Delete Exam Prep Practice", Description: "Delete an exam prep practice", GroupName: "Exam Prep"},
|
|
||||||
|
|
||||||
// Modules (LMS, under a course)
|
// Modules (LMS, under a course)
|
||||||
{Key: "modules.create", Name: "Create Module", Description: "Create a module in a course", GroupName: "Modules"},
|
{Key: "modules.create", Name: "Create Module", Description: "Create a module in a course", GroupName: "Modules"},
|
||||||
{Key: "modules.get", Name: "Get Module", Description: "Get a module by ID", GroupName: "Modules"},
|
{Key: "modules.get", Name: "Get Module", Description: "Get a module by ID", GroupName: "Modules"},
|
||||||
|
|
@ -311,11 +280,6 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
|
|
||||||
// Programs
|
// Programs
|
||||||
"programs.create", "programs.list", "programs.get", "programs.update", "programs.delete", "programs.reorder",
|
"programs.create", "programs.list", "programs.get", "programs.update", "programs.delete", "programs.reorder",
|
||||||
"exam_prep.catalog_courses.create", "exam_prep.catalog_courses.list", "exam_prep.catalog_courses.get", "exam_prep.catalog_courses.update", "exam_prep.catalog_courses.delete", "exam_prep.catalog_courses.reorder",
|
|
||||||
"exam_prep.units.create", "exam_prep.units.list", "exam_prep.units.get", "exam_prep.units.update", "exam_prep.units.delete", "exam_prep.units.reorder",
|
|
||||||
"exam_prep.modules.create", "exam_prep.modules.list", "exam_prep.modules.get", "exam_prep.modules.update", "exam_prep.modules.delete", "exam_prep.modules.reorder",
|
|
||||||
"exam_prep.lessons.create", "exam_prep.lessons.list_by_module", "exam_prep.lessons.get", "exam_prep.lessons.update", "exam_prep.lessons.delete", "exam_prep.lessons.reorder",
|
|
||||||
"exam_prep.practices.create", "exam_prep.practices.list_by_lesson", "exam_prep.practices.get", "exam_prep.practices.update", "exam_prep.practices.delete",
|
|
||||||
"lms.get_my_progress",
|
"lms.get_my_progress",
|
||||||
|
|
||||||
// Modules
|
// Modules
|
||||||
|
|
@ -410,11 +374,6 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"learning_tree.get",
|
"learning_tree.get",
|
||||||
|
|
||||||
"programs.list", "programs.get",
|
"programs.list", "programs.get",
|
||||||
"exam_prep.catalog_courses.list", "exam_prep.catalog_courses.get",
|
|
||||||
"exam_prep.units.list", "exam_prep.units.get",
|
|
||||||
"exam_prep.modules.list", "exam_prep.modules.get",
|
|
||||||
"exam_prep.lessons.list_by_module", "exam_prep.lessons.get",
|
|
||||||
"exam_prep.practices.list_by_lesson", "exam_prep.practices.get",
|
|
||||||
"lms.get_my_progress",
|
"lms.get_my_progress",
|
||||||
|
|
||||||
// Questions (read + attempt)
|
// Questions (read + attempt)
|
||||||
|
|
@ -469,11 +428,6 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"learning_tree.get",
|
"learning_tree.get",
|
||||||
|
|
||||||
"programs.list", "programs.get",
|
"programs.list", "programs.get",
|
||||||
"exam_prep.catalog_courses.list", "exam_prep.catalog_courses.get",
|
|
||||||
"exam_prep.units.list", "exam_prep.units.get",
|
|
||||||
"exam_prep.modules.list", "exam_prep.modules.get",
|
|
||||||
"exam_prep.lessons.list_by_module", "exam_prep.lessons.get",
|
|
||||||
"exam_prep.practices.list_by_lesson", "exam_prep.practices.get",
|
|
||||||
"lms.get_my_progress",
|
"lms.get_my_progress",
|
||||||
|
|
||||||
// Questions (full — instructors create content)
|
// Questions (full — instructors create content)
|
||||||
|
|
@ -528,11 +482,6 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"learning_tree.get",
|
"learning_tree.get",
|
||||||
|
|
||||||
"programs.list", "programs.get",
|
"programs.list", "programs.get",
|
||||||
"exam_prep.catalog_courses.list", "exam_prep.catalog_courses.get",
|
|
||||||
"exam_prep.units.list", "exam_prep.units.get",
|
|
||||||
"exam_prep.modules.list", "exam_prep.modules.get",
|
|
||||||
"exam_prep.lessons.list_by_module", "exam_prep.lessons.get",
|
|
||||||
"exam_prep.practices.list_by_lesson", "exam_prep.practices.get",
|
|
||||||
|
|
||||||
// Questions (read)
|
// Questions (read)
|
||||||
"questions.list", "questions.search", "questions.get",
|
"questions.list", "questions.search", "questions.get",
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrPlanNotFound = errors.New("subscription plan not found")
|
ErrPlanNotFound = errors.New("subscription plan not found")
|
||||||
ErrSubscriptionNotFound = errors.New("subscription not found")
|
ErrSubscriptionNotFound = errors.New("subscription not found")
|
||||||
ErrSubscriptionNotOwned = errors.New("subscription does not belong to this user")
|
ErrAlreadySubscribed = errors.New("user already has an active subscription")
|
||||||
ErrAlreadySubscribed = errors.New("user already has an active subscription")
|
ErrInvalidPlan = errors.New("invalid subscription plan")
|
||||||
ErrInvalidPlan = errors.New("invalid subscription plan")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
|
@ -93,14 +90,7 @@ func (s *Service) Subscribe(ctx context.Context, userID, planID int64, paymentRe
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetSubscriptionByID(ctx context.Context, id int64) (*domain.UserSubscription, error) {
|
func (s *Service) GetSubscriptionByID(ctx context.Context, id int64) (*domain.UserSubscription, error) {
|
||||||
sub, err := s.store.GetUserSubscriptionByID(ctx, id)
|
return s.store.GetUserSubscriptionByID(ctx, id)
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return nil, ErrSubscriptionNotFound
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return sub, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetActiveSubscription(ctx context.Context, userID int64) (*domain.UserSubscription, error) {
|
func (s *Service) GetActiveSubscription(ctx context.Context, userID int64) (*domain.UserSubscription, error) {
|
||||||
|
|
@ -115,41 +105,19 @@ func (s *Service) HasActiveSubscription(ctx context.Context, userID int64) (bool
|
||||||
return s.store.HasActiveSubscription(ctx, userID)
|
return s.store.HasActiveSubscription(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) subscriptionOwnedBy(ctx context.Context, subscriptionID, userID int64) error {
|
func (s *Service) CancelSubscription(ctx context.Context, subscriptionID int64) error {
|
||||||
sub, err := s.store.GetUserSubscriptionByID(ctx, subscriptionID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
return ErrSubscriptionNotFound
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if sub.UserID != userID {
|
|
||||||
return ErrSubscriptionNotOwned
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelSubscriptionForUser cancels only if the subscription row belongs to userID.
|
|
||||||
func (s *Service) CancelSubscriptionForUser(ctx context.Context, subscriptionID, userID int64) error {
|
|
||||||
if err := s.subscriptionOwnedBy(ctx, subscriptionID, userID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.store.CancelUserSubscription(ctx, subscriptionID)
|
return s.store.CancelUserSubscription(ctx, subscriptionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAutoRenewForUser updates auto-renew only if the subscription belongs to userID.
|
func (s *Service) SetAutoRenew(ctx context.Context, subscriptionID int64, autoRenew bool) error {
|
||||||
func (s *Service) SetAutoRenewForUser(ctx context.Context, subscriptionID, userID int64, autoRenew bool) error {
|
|
||||||
if err := s.subscriptionOwnedBy(ctx, subscriptionID, userID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.store.UpdateAutoRenew(ctx, subscriptionID, autoRenew)
|
return s.store.UpdateAutoRenew(ctx, subscriptionID, autoRenew)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenewSubscription extends an existing subscription
|
// RenewSubscription extends an existing subscription
|
||||||
func (s *Service) RenewSubscription(ctx context.Context, subscriptionID int64) (*domain.UserSubscription, error) {
|
func (s *Service) RenewSubscription(ctx context.Context, subscriptionID int64) (*domain.UserSubscription, error) {
|
||||||
sub, err := s.GetSubscriptionByID(ctx, subscriptionID)
|
sub, err := s.store.GetUserSubscriptionByID(ctx, subscriptionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, ErrSubscriptionNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
plan, err := s.store.GetSubscriptionPlanByID(ctx, sub.PlanID)
|
plan, err := s.store.GetSubscriptionPlanByID(ctx, sub.PlanID)
|
||||||
|
|
@ -177,7 +145,7 @@ func (s *Service) RenewSubscription(ctx context.Context, subscriptionID int64) (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.GetSubscriptionByID(ctx, subscriptionID)
|
return s.store.GetUserSubscriptionByID(ctx, subscriptionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import (
|
||||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
"Yimaru-Backend/internal/services/courses"
|
"Yimaru-Backend/internal/services/courses"
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
|
||||||
"Yimaru-Backend/internal/services/lessons"
|
"Yimaru-Backend/internal/services/lessons"
|
||||||
"Yimaru-Backend/internal/services/lmsprogress"
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
"Yimaru-Backend/internal/services/modules"
|
"Yimaru-Backend/internal/services/modules"
|
||||||
|
|
@ -46,7 +45,6 @@ import (
|
||||||
type App struct {
|
type App struct {
|
||||||
assessmentSvc *assessment.Service
|
assessmentSvc *assessment.Service
|
||||||
questionsSvc *questions.Service
|
questionsSvc *questions.Service
|
||||||
examPrepSvc *examprep.Service
|
|
||||||
programSvc *programs.Service
|
programSvc *programs.Service
|
||||||
courseSvc *courses.Service
|
courseSvc *courses.Service
|
||||||
moduleSvc *modules.Service
|
moduleSvc *modules.Service
|
||||||
|
|
@ -84,7 +82,6 @@ type App struct {
|
||||||
func NewApp(
|
func NewApp(
|
||||||
assessmentSvc *assessment.Service,
|
assessmentSvc *assessment.Service,
|
||||||
questionsSvc *questions.Service,
|
questionsSvc *questions.Service,
|
||||||
examPrepSvc *examprep.Service,
|
|
||||||
programSvc *programs.Service,
|
programSvc *programs.Service,
|
||||||
courseSvc *courses.Service,
|
courseSvc *courses.Service,
|
||||||
moduleSvc *modules.Service,
|
moduleSvc *modules.Service,
|
||||||
|
|
@ -134,7 +131,6 @@ func NewApp(
|
||||||
s := &App{
|
s := &App{
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
examPrepSvc: examPrepSvc,
|
|
||||||
programSvc: programSvc,
|
programSvc: programSvc,
|
||||||
courseSvc: courseSvc,
|
courseSvc: courseSvc,
|
||||||
moduleSvc: moduleSvc,
|
moduleSvc: moduleSvc,
|
||||||
|
|
|
||||||
|
|
@ -1,235 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CreateExamPrepCatalogCourse godoc
|
|
||||||
// @Summary Create exam-prep catalog course
|
|
||||||
// @Description Top-level exam track (DET, IELTS, …) in schema exam_prep — separate from LMS programs/courses
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param body body domain.CreateExamPrepCatalogCourseInput true "Catalog course"
|
|
||||||
// @Success 201 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/exam-prep/catalog-courses [post]
|
|
||||||
func (h *Handler) CreateExamPrepCatalogCourse(c *fiber.Ctx) error {
|
|
||||||
var req domain.CreateExamPrepCatalogCourseInput
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Validation failed",
|
|
||||||
Error: firstValidationError(valErrs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
out, err := h.examPrepSvc.CreateCatalogCourse(c.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to create catalog course",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
||||||
Message: "Catalog course created successfully",
|
|
||||||
Data: out,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusCreated,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExamPrepCatalogCourses godoc
|
|
||||||
// @Summary List exam-prep catalog courses
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Produce json
|
|
||||||
// @Param limit query int false "Page size" default(20)
|
|
||||||
// @Param offset query int false "Offset" default(0)
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Router /api/v1/exam-prep/catalog-courses [get]
|
|
||||||
func (h *Handler) ListExamPrepCatalogCourses(c *fiber.Ctx) error {
|
|
||||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
|
||||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
|
||||||
items, total, err := h.examPrepSvc.ListCatalogCourses(c.Context(), int32(limit), int32(offset))
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to list catalog courses",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Catalog courses retrieved successfully",
|
|
||||||
Data: fiber.Map{
|
|
||||||
"catalog_courses": items,
|
|
||||||
"total_count": total,
|
|
||||||
"limit": limit,
|
|
||||||
"offset": offset,
|
|
||||||
},
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReorderExamPrepCatalogCourses godoc
|
|
||||||
// @Summary Reorder all exam-prep catalog courses
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param body body domain.ReorderIDsRequest true "ordered_ids: every catalog course id exactly once"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Router /api/v1/exam-prep/catalog-courses/reorder [put]
|
|
||||||
func (h *Handler) ReorderExamPrepCatalogCourses(c *fiber.Ctx) error {
|
|
||||||
var req domain.ReorderIDsRequest
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if req.OrderedIDs == nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "ordered_ids is required (use an empty array if there are no catalog courses)",
|
|
||||||
Error: "missing ordered_ids",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := h.examPrepSvc.ReorderCatalogCourses(c.Context(), req.OrderedIDs); err != nil {
|
|
||||||
if errors.Is(err, domain.ErrReorderInvalidIDSet) {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: err.Error(),
|
|
||||||
Error: "INVALID_REORDER",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to reorder catalog courses",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Catalog courses reordered successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExamPrepCatalogCourseByID godoc
|
|
||||||
// @Summary Get exam-prep catalog course by ID
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path int true "Catalog course ID"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Router /api/v1/exam-prep/catalog-courses/{id} [get]
|
|
||||||
func (h *Handler) GetExamPrepCatalogCourseByID(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid catalog course id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
out, err := h.examPrepSvc.GetCatalogCourseByID(c.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrCatalogCourseNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Catalog course not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to get catalog course",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Catalog course retrieved successfully",
|
|
||||||
Data: out,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExamPrepCatalogCourse godoc
|
|
||||||
// @Summary Update exam-prep catalog course
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param id path int true "Catalog course ID"
|
|
||||||
// @Param body body domain.UpdateExamPrepCatalogCourseInput true "Fields to update"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Router /api/v1/exam-prep/catalog-courses/{id} [put]
|
|
||||||
func (h *Handler) UpdateExamPrepCatalogCourse(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid catalog course id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.UpdateExamPrepCatalogCourseInput
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
out, err := h.examPrepSvc.UpdateCatalogCourse(c.Context(), id, req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrCatalogCourseNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Catalog course not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to update catalog course",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Catalog course updated successfully",
|
|
||||||
Data: out,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExamPrepCatalogCourse godoc
|
|
||||||
// @Summary Delete exam-prep catalog course
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param id path int true "Catalog course ID"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Router /api/v1/exam-prep/catalog-courses/{id} [delete]
|
|
||||||
func (h *Handler) DeleteExamPrepCatalogCourse(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid catalog course id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := h.examPrepSvc.DeleteCatalogCourse(c.Context(), id); err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrCatalogCourseNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Catalog course not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to delete catalog course",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Catalog course deleted successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CreateExamPrepLesson godoc
|
|
||||||
// @Summary Create exam-prep lesson (under a unit module)
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param moduleId path int true "Exam prep unit module ID"
|
|
||||||
// @Param body body domain.CreateExamPrepLessonInput true "Lesson"
|
|
||||||
// @Router /api/v1/exam-prep/modules/{moduleId}/lessons [post]
|
|
||||||
func (h *Handler) CreateExamPrepLesson(c *fiber.Ctx) error {
|
|
||||||
moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid module id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.CreateExamPrepLessonInput
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Validation failed",
|
|
||||||
Error: firstValidationError(valErrs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
les, err := h.examPrepSvc.CreateLesson(c.Context(), moduleID, req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrModuleNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Module not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to create lesson",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
||||||
Message: "Lesson created successfully",
|
|
||||||
Data: les,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusCreated,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExamPrepLessonsByUnitModule godoc
|
|
||||||
// @Summary List exam-prep lessons for a unit module
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param moduleId path int true "Exam prep unit module ID"
|
|
||||||
// @Param limit query int false "Page size" default(20)
|
|
||||||
// @Param offset query int false "Offset" default(0)
|
|
||||||
// @Router /api/v1/exam-prep/modules/{moduleId}/lessons [get]
|
|
||||||
func (h *Handler) ListExamPrepLessonsByUnitModule(c *fiber.Ctx) error {
|
|
||||||
moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid module id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
|
||||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
|
||||||
items, total, err := h.examPrepSvc.ListLessonsByUnitModule(c.Context(), moduleID, int32(limit), int32(offset))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrModuleNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Module not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to list lessons",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Lessons retrieved successfully",
|
|
||||||
Data: fiber.Map{
|
|
||||||
"lessons": items,
|
|
||||||
"total_count": total,
|
|
||||||
"limit": limit,
|
|
||||||
"offset": offset,
|
|
||||||
},
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReorderExamPrepLessonsInUnitModule godoc
|
|
||||||
// @Summary Reorder lessons within an exam-prep unit module
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Router /api/v1/exam-prep/modules/{moduleId}/lessons/reorder [put]
|
|
||||||
func (h *Handler) ReorderExamPrepLessonsInUnitModule(c *fiber.Ctx) error {
|
|
||||||
moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid module id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.ReorderIDsRequest
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if req.OrderedIDs == nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "ordered_ids is required (use an empty array if there are no lessons)",
|
|
||||||
Error: "missing ordered_ids",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := h.examPrepSvc.ReorderLessonsInUnitModule(c.Context(), moduleID, req.OrderedIDs); err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrModuleNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Module not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if errors.Is(err, domain.ErrReorderInvalidIDSet) {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: err.Error(),
|
|
||||||
Error: "INVALID_REORDER",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to reorder lessons",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Lessons reordered successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExamPrepLessonByID godoc
|
|
||||||
// @Summary Get exam-prep lesson by ID
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Router /api/v1/exam-prep/lessons/{id} [get]
|
|
||||||
func (h *Handler) GetExamPrepLessonByID(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid lesson id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
les, err := h.examPrepSvc.GetLessonByID(c.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrLessonNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Lesson not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to get lesson",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Lesson retrieved successfully",
|
|
||||||
Data: les,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExamPrepLesson godoc
|
|
||||||
// @Summary Update exam-prep lesson
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Router /api/v1/exam-prep/lessons/{id} [put]
|
|
||||||
func (h *Handler) UpdateExamPrepLesson(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid lesson id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.UpdateExamPrepLessonInput
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
les, err := h.examPrepSvc.UpdateLesson(c.Context(), id, req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrLessonNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Lesson not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to update lesson",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Lesson updated successfully",
|
|
||||||
Data: les,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExamPrepLesson godoc
|
|
||||||
// @Summary Delete exam-prep lesson
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Router /api/v1/exam-prep/lessons/{id} [delete]
|
|
||||||
func (h *Handler) DeleteExamPrepLesson(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid lesson id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := h.examPrepSvc.DeleteLesson(c.Context(), id); err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrLessonNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Lesson not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to delete lesson",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Lesson deleted successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CreateExamPrepModule godoc
|
|
||||||
// @Summary Create exam-prep module
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param unitId path int true "Unit ID"
|
|
||||||
// @Param body body domain.CreateExamPrepModuleInput true "Module"
|
|
||||||
// @Router /api/v1/exam-prep/units/{unitId}/modules [post]
|
|
||||||
func (h *Handler) CreateExamPrepModule(c *fiber.Ctx) error {
|
|
||||||
unitID, err := strconv.ParseInt(c.Params("unitId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid unit id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.CreateExamPrepModuleInput
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Validation failed",
|
|
||||||
Error: firstValidationError(valErrs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
out, err := h.examPrepSvc.CreateModule(c.Context(), unitID, req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrUnitNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Unit not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to create module",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
||||||
Message: "Module created successfully",
|
|
||||||
Data: out,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusCreated,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExamPrepModulesByUnit godoc
|
|
||||||
// @Summary List exam-prep modules for a unit
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param unitId path int true "Unit ID"
|
|
||||||
// @Router /api/v1/exam-prep/units/{unitId}/modules [get]
|
|
||||||
func (h *Handler) ListExamPrepModulesByUnit(c *fiber.Ctx) error {
|
|
||||||
unitID, err := strconv.ParseInt(c.Params("unitId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid unit id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
|
||||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
|
||||||
items, total, err := h.examPrepSvc.ListModulesByUnit(c.Context(), unitID, int32(limit), int32(offset))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrUnitNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Unit not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to list modules",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Modules retrieved successfully",
|
|
||||||
Data: fiber.Map{
|
|
||||||
"modules": items,
|
|
||||||
"total_count": total,
|
|
||||||
"limit": limit,
|
|
||||||
"offset": offset,
|
|
||||||
},
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReorderExamPrepModulesInUnit godoc
|
|
||||||
// @Summary Reorder modules within a unit
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param unitId path int true "Unit ID"
|
|
||||||
// @Param body body domain.ReorderIDsRequest true "ordered_ids"
|
|
||||||
// @Router /api/v1/exam-prep/units/{unitId}/modules/reorder [put]
|
|
||||||
func (h *Handler) ReorderExamPrepModulesInUnit(c *fiber.Ctx) error {
|
|
||||||
unitID, err := strconv.ParseInt(c.Params("unitId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid unit id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.ReorderIDsRequest
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if req.OrderedIDs == nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "ordered_ids is required (use an empty array if there are no modules)",
|
|
||||||
Error: "missing ordered_ids",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := h.examPrepSvc.ReorderModulesInUnit(c.Context(), unitID, req.OrderedIDs); err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrUnitNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Unit not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if errors.Is(err, domain.ErrReorderInvalidIDSet) {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: err.Error(),
|
|
||||||
Error: "INVALID_REORDER",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to reorder modules",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Modules reordered successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExamPrepModuleByID godoc
|
|
||||||
// @Summary Get exam-prep module by ID
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Router /api/v1/exam-prep/modules/{id} [get]
|
|
||||||
func (h *Handler) GetExamPrepModuleByID(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid module id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
out, err := h.examPrepSvc.GetModuleByID(c.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrModuleNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Module not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to get module",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Module retrieved successfully",
|
|
||||||
Data: out,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExamPrepModule godoc
|
|
||||||
// @Summary Update exam-prep module
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Router /api/v1/exam-prep/modules/{id} [put]
|
|
||||||
func (h *Handler) UpdateExamPrepModule(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid module id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.UpdateExamPrepModuleInput
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
out, err := h.examPrepSvc.UpdateModule(c.Context(), id, req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrModuleNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Module not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to update module",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Module updated successfully",
|
|
||||||
Data: out,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExamPrepModule godoc
|
|
||||||
// @Summary Delete exam-prep module
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Router /api/v1/exam-prep/modules/{id} [delete]
|
|
||||||
func (h *Handler) DeleteExamPrepModule(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid module id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := h.examPrepSvc.DeleteModule(c.Context(), id); err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrModuleNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Module not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to delete module",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Module deleted successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CreateExamPrepPractice godoc
|
|
||||||
// @Summary Create exam-prep practice (under a lesson; uses shared question_sets)
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param lessonId path int true "Exam prep lesson ID (unit_module_lessons.id)"
|
|
||||||
// @Param body body domain.CreateExamPrepPracticeInput true "Practice"
|
|
||||||
// @Router /api/v1/exam-prep/lessons/{lessonId}/practices [post]
|
|
||||||
func (h *Handler) CreateExamPrepPractice(c *fiber.Ctx) error {
|
|
||||||
lessonID, err := strconv.ParseInt(c.Params("lessonId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid lesson id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.CreateExamPrepPracticeInput
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Validation failed",
|
|
||||||
Error: firstValidationError(valErrs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p, err := h.examPrepSvc.CreateExamPrepPractice(c.Context(), lessonID, req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrLessonNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Lesson not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to create practice",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
||||||
Message: "Practice created successfully",
|
|
||||||
Data: p,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusCreated,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExamPrepPracticesByLesson godoc
|
|
||||||
// @Summary List exam-prep practices for a lesson
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param lessonId path int true "Exam prep lesson ID"
|
|
||||||
// @Param limit query int false "Page size" default(20)
|
|
||||||
// @Param offset query int false "Offset" default(0)
|
|
||||||
// @Router /api/v1/exam-prep/lessons/{lessonId}/practices [get]
|
|
||||||
func (h *Handler) ListExamPrepPracticesByLesson(c *fiber.Ctx) error {
|
|
||||||
lessonID, err := strconv.ParseInt(c.Params("lessonId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid lesson id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
|
||||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
|
||||||
items, total, err := h.examPrepSvc.ListExamPrepPracticesByLesson(c.Context(), lessonID, int32(limit), int32(offset))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrLessonNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Lesson not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to list practices",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Practices retrieved successfully",
|
|
||||||
Data: fiber.Map{
|
|
||||||
"practices": items,
|
|
||||||
"total_count": total,
|
|
||||||
"limit": limit,
|
|
||||||
"offset": offset,
|
|
||||||
},
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExamPrepPracticeByID godoc
|
|
||||||
// @Summary Get exam-prep practice by ID
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param id path int true "Exam prep practice ID"
|
|
||||||
// @Router /api/v1/exam-prep/practices/{id} [get]
|
|
||||||
func (h *Handler) GetExamPrepPracticeByID(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid practice id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p, err := h.examPrepSvc.GetExamPrepPracticeByID(c.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrPracticeNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Practice not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to load practice",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Practice retrieved successfully",
|
|
||||||
Data: p,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExamPrepPractice godoc
|
|
||||||
// @Summary Update exam-prep practice
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param id path int true "Exam prep practice ID"
|
|
||||||
// @Param body body domain.UpdateExamPrepPracticeInput true "Fields to update"
|
|
||||||
// @Router /api/v1/exam-prep/practices/{id} [put]
|
|
||||||
func (h *Handler) UpdateExamPrepPractice(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid practice id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.UpdateExamPrepPracticeInput
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
p, err := h.examPrepSvc.UpdateExamPrepPractice(c.Context(), id, req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrPracticeNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Practice not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to update practice",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Practice updated successfully",
|
|
||||||
Data: p,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExamPrepPractice godoc
|
|
||||||
// @Summary Delete exam-prep practice
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param id path int true "Exam prep practice ID"
|
|
||||||
// @Router /api/v1/exam-prep/practices/{id} [delete]
|
|
||||||
func (h *Handler) DeleteExamPrepPractice(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid practice id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := h.examPrepSvc.DeleteExamPrepPractice(c.Context(), id); err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrPracticeNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Practice not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to delete practice",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Practice deleted successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,266 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
|
||||||
"errors"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CreateExamPrepUnit godoc
|
|
||||||
// @Summary Create exam-prep unit
|
|
||||||
// @Description Unit under a catalog course (e.g. chapter title)
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param catalogCourseId path int true "Catalog course ID"
|
|
||||||
// @Param body body domain.CreateExamPrepUnitInput true "Unit"
|
|
||||||
// @Success 201 {object} domain.Response
|
|
||||||
// @Router /api/v1/exam-prep/catalog-courses/{catalogCourseId}/units [post]
|
|
||||||
func (h *Handler) CreateExamPrepUnit(c *fiber.Ctx) error {
|
|
||||||
catalogCourseID, err := strconv.ParseInt(c.Params("catalogCourseId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid catalog course id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.CreateExamPrepUnitInput
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Validation failed",
|
|
||||||
Error: firstValidationError(valErrs),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
out, err := h.examPrepSvc.CreateUnit(c.Context(), catalogCourseID, req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrCatalogCourseNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Catalog course not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to create unit",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
||||||
Message: "Unit created successfully",
|
|
||||||
Data: out,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusCreated,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExamPrepUnitsByCatalogCourse godoc
|
|
||||||
// @Summary List exam-prep units for a catalog course
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param catalogCourseId path int true "Catalog course ID"
|
|
||||||
// @Param limit query int false "Page size" default(20)
|
|
||||||
// @Param offset query int false "Offset" default(0)
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Router /api/v1/exam-prep/catalog-courses/{catalogCourseId}/units [get]
|
|
||||||
func (h *Handler) ListExamPrepUnitsByCatalogCourse(c *fiber.Ctx) error {
|
|
||||||
catalogCourseID, err := strconv.ParseInt(c.Params("catalogCourseId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid catalog course id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
|
||||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
|
||||||
items, total, err := h.examPrepSvc.ListUnitsByCatalogCourse(c.Context(), catalogCourseID, int32(limit), int32(offset))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrCatalogCourseNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Catalog course not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to list units",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Units retrieved successfully",
|
|
||||||
Data: fiber.Map{
|
|
||||||
"units": items,
|
|
||||||
"total_count": total,
|
|
||||||
"limit": limit,
|
|
||||||
"offset": offset,
|
|
||||||
},
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReorderExamPrepUnitsInCatalogCourse godoc
|
|
||||||
// @Summary Reorder units within a catalog course
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param catalogCourseId path int true "Catalog course ID"
|
|
||||||
// @Param body body domain.ReorderIDsRequest true "ordered_ids: every unit id in this catalog course, new order"
|
|
||||||
// @Router /api/v1/exam-prep/catalog-courses/{catalogCourseId}/units/reorder [put]
|
|
||||||
func (h *Handler) ReorderExamPrepUnitsInCatalogCourse(c *fiber.Ctx) error {
|
|
||||||
catalogCourseID, err := strconv.ParseInt(c.Params("catalogCourseId"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid catalog course id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.ReorderIDsRequest
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if req.OrderedIDs == nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "ordered_ids is required (use an empty array if there are no units)",
|
|
||||||
Error: "missing ordered_ids",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := h.examPrepSvc.ReorderUnitsInCatalogCourse(c.Context(), catalogCourseID, req.OrderedIDs); err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrCatalogCourseNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Catalog course not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if errors.Is(err, domain.ErrReorderInvalidIDSet) {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: err.Error(),
|
|
||||||
Error: "INVALID_REORDER",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to reorder units",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Units reordered successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExamPrepUnitByID godoc
|
|
||||||
// @Summary Get exam-prep unit by ID
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param id path int true "Unit ID"
|
|
||||||
// @Router /api/v1/exam-prep/units/{id} [get]
|
|
||||||
func (h *Handler) GetExamPrepUnitByID(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid unit id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
out, err := h.examPrepSvc.GetUnitByID(c.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrUnitNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Unit not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to get unit",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Unit retrieved successfully",
|
|
||||||
Data: out,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExamPrepUnit godoc
|
|
||||||
// @Summary Update exam-prep unit
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param id path int true "Unit ID"
|
|
||||||
// @Param body body domain.UpdateExamPrepUnitInput true "Fields to update"
|
|
||||||
// @Router /api/v1/exam-prep/units/{id} [put]
|
|
||||||
func (h *Handler) UpdateExamPrepUnit(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid unit id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
var req domain.UpdateExamPrepUnitInput
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
out, err := h.examPrepSvc.UpdateUnit(c.Context(), id, req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrUnitNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Unit not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to update unit",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Unit updated successfully",
|
|
||||||
Data: out,
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExamPrepUnit godoc
|
|
||||||
// @Summary Delete exam-prep unit
|
|
||||||
// @Tags exam-prep
|
|
||||||
// @Param id path int true "Unit ID"
|
|
||||||
// @Router /api/v1/exam-prep/units/{id} [delete]
|
|
||||||
func (h *Handler) DeleteExamPrepUnit(c *fiber.Ctx) error {
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid unit id",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := h.examPrepSvc.DeleteUnit(c.Context(), id); err != nil {
|
|
||||||
if errors.Is(err, examprep.ErrUnitNotFound) {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Unit not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to delete unit",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Unit deleted successfully",
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -32,10 +31,6 @@ type uploadMediaByURLReq struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type refreshFileURLReq struct {
|
|
||||||
Reference string `json:"reference"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveFileURL converts a stored file path to a usable URL.
|
// resolveFileURL converts a stored file path to a usable URL.
|
||||||
// If the path starts with "minio://", it generates a presigned URL.
|
// If the path starts with "minio://", it generates a presigned URL.
|
||||||
// Otherwise it returns the path as-is (e.g. "/static/...").
|
// Otherwise it returns the path as-is (e.g. "/static/...").
|
||||||
|
|
@ -87,93 +82,6 @@ func (h *Handler) GetFileURL(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) extractObjectKeyFromReference(reference string) (string, error) {
|
|
||||||
ref := strings.TrimSpace(reference)
|
|
||||||
if ref == "" {
|
|
||||||
return "", fmt.Errorf("reference is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(ref, "minio://") {
|
|
||||||
key := strings.TrimPrefix(ref, "minio://")
|
|
||||||
if key == "" {
|
|
||||||
return "", fmt.Errorf("invalid minio reference")
|
|
||||||
}
|
|
||||||
return key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := url.Parse(ref)
|
|
||||||
if err == nil && u.Scheme != "" && u.Host != "" {
|
|
||||||
path := strings.TrimPrefix(u.Path, "/")
|
|
||||||
if path == "" {
|
|
||||||
return "", fmt.Errorf("invalid file URL")
|
|
||||||
}
|
|
||||||
bucket := strings.TrimSpace(h.minioSvc.BucketName())
|
|
||||||
if bucket != "" {
|
|
||||||
prefix := bucket + "/"
|
|
||||||
if strings.HasPrefix(path, prefix) {
|
|
||||||
path = strings.TrimPrefix(path, prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if path == "" {
|
|
||||||
return "", fmt.Errorf("invalid file URL")
|
|
||||||
}
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return ref, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshFileURL generates a new presigned URL from an object key, minio:// URI, or stale presigned URL.
|
|
||||||
// @Summary Refresh presigned URL for a file
|
|
||||||
// @Tags files
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param body body refreshFileURLReq true "reference (object key, minio://..., or existing presigned URL)"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Router /api/v1/files/refresh-url [post]
|
|
||||||
func (h *Handler) RefreshFileURL(c *fiber.Ctx) error {
|
|
||||||
if h.minioSvc == nil {
|
|
||||||
return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{
|
|
||||||
Message: "File storage service is not available",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var req refreshFileURLReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
objectKey, err := h.extractObjectKeyFromReference(req.Reference)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid file reference",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
freshURL, err := h.minioSvc.GetURL(c.Context(), objectKey, 1*time.Hour)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to refresh file URL",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
||||||
Message: "File URL refreshed",
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"object_key": objectKey,
|
|
||||||
"url": freshURL,
|
|
||||||
"expires_in": int((1 * time.Hour).Seconds()),
|
|
||||||
},
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadMedia uploads an image/audio/video file and returns its URL and key.
|
// UploadMedia uploads an image/audio/video file and returns its URL and key.
|
||||||
// @Summary Upload media file
|
// @Summary Upload media file
|
||||||
// @Tags files
|
// @Tags files
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import (
|
||||||
rbacservice "Yimaru-Backend/internal/services/rbac"
|
rbacservice "Yimaru-Backend/internal/services/rbac"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
"Yimaru-Backend/internal/services/courses"
|
"Yimaru-Backend/internal/services/courses"
|
||||||
"Yimaru-Backend/internal/services/examprep"
|
|
||||||
"Yimaru-Backend/internal/services/lessons"
|
"Yimaru-Backend/internal/services/lessons"
|
||||||
"Yimaru-Backend/internal/services/lmsprogress"
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
"Yimaru-Backend/internal/services/modules"
|
"Yimaru-Backend/internal/services/modules"
|
||||||
|
|
@ -45,7 +44,6 @@ import (
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
assessmentSvc *assessment.Service
|
assessmentSvc *assessment.Service
|
||||||
questionsSvc *questions.Service
|
questionsSvc *questions.Service
|
||||||
examPrepSvc *examprep.Service
|
|
||||||
programSvc *programs.Service
|
programSvc *programs.Service
|
||||||
courseSvc *courses.Service
|
courseSvc *courses.Service
|
||||||
moduleSvc *modules.Service
|
moduleSvc *modules.Service
|
||||||
|
|
@ -79,7 +77,6 @@ type Handler struct {
|
||||||
func New(
|
func New(
|
||||||
assessmentSvc *assessment.Service,
|
assessmentSvc *assessment.Service,
|
||||||
questionsSvc *questions.Service,
|
questionsSvc *questions.Service,
|
||||||
examPrepSvc *examprep.Service,
|
|
||||||
programSvc *programs.Service,
|
programSvc *programs.Service,
|
||||||
courseSvc *courses.Service,
|
courseSvc *courses.Service,
|
||||||
moduleSvc *modules.Service,
|
moduleSvc *modules.Service,
|
||||||
|
|
@ -112,7 +109,6 @@ func New(
|
||||||
return &Handler{
|
return &Handler{
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
examPrepSvc: examPrepSvc,
|
|
||||||
programSvc: programSvc,
|
programSvc: programSvc,
|
||||||
courseSvc: courseSvc,
|
courseSvc: courseSvc,
|
||||||
moduleSvc: moduleSvc,
|
moduleSvc: moduleSvc,
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type componentCatalogRes struct {
|
|
||||||
StimulusKinds []string `json:"stimulus_component_kinds"`
|
|
||||||
ResponseKinds []string `json:"response_component_kinds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type validateQuestionTypeDefinitionReq struct {
|
|
||||||
StimulusComponentKinds []string `json:"stimulus_component_kinds"`
|
|
||||||
ResponseComponentKinds []string `json:"response_component_kinds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetQuestionTypeComponentCatalog godoc
|
|
||||||
// @Summary Question-type builder component catalog
|
|
||||||
// @Description Valid stimulus and response component kind codes for dynamic question-type definitions
|
|
||||||
// @Tags questions
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Router /api/v1/questions/component-catalog [get]
|
|
||||||
func (h *Handler) GetQuestionTypeComponentCatalog(c *fiber.Ctx) error {
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Component catalog",
|
|
||||||
Data: componentCatalogRes{
|
|
||||||
StimulusKinds: domain.StimulusComponentCatalog(),
|
|
||||||
ResponseKinds: domain.ResponseComponentCatalog(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateQuestionTypeDefinition godoc
|
|
||||||
// @Summary Validate dynamic question-type definition
|
|
||||||
// @Description Validates selected stimulus and response component kinds for temporary question-type definitions
|
|
||||||
// @Tags questions
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param body body validateQuestionTypeDefinitionReq true "Stimulus and response component kinds"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/questions/validate-question-type-definition [post]
|
|
||||||
func (h *Handler) ValidateQuestionTypeDefinition(c *fiber.Ctx) error {
|
|
||||||
var req validateQuestionTypeDefinitionReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := domain.ValidateDynamicQuestionTypeDefinition(req.StimulusComponentKinds, req.ResponseComponentKinds); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid question type definition",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Question type definition is valid",
|
|
||||||
Data: fiber.Map{"valid": true},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -550,27 +550,6 @@ type listQuestionSetsRes struct {
|
||||||
TotalCount int64 `json:"total_count"`
|
TotalCount int64 `json:"total_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var validInitialAssessmentLevels = map[string]struct{}{
|
|
||||||
"A1": {},
|
|
||||||
"A2": {},
|
|
||||||
"B1": {},
|
|
||||||
"B2": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeInitialAssessmentLevel(description *string) (string, error) {
|
|
||||||
if description == nil {
|
|
||||||
return "", fmt.Errorf("description is required and must be one of: A1, A2, B1, B2")
|
|
||||||
}
|
|
||||||
level := strings.ToUpper(strings.TrimSpace(*description))
|
|
||||||
if level == "" {
|
|
||||||
return "", fmt.Errorf("description is required and must be one of: A1, A2, B1, B2")
|
|
||||||
}
|
|
||||||
if _, ok := validInitialAssessmentLevels[level]; !ok {
|
|
||||||
return "", fmt.Errorf("description must be one of: A1, A2, B1, B2")
|
|
||||||
}
|
|
||||||
return level, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSequenceGatedPractice(set domain.QuestionSet) bool {
|
func isSequenceGatedPractice(set domain.QuestionSet) bool {
|
||||||
if !strings.EqualFold(set.SetType, string(domain.QuestionSetTypePractice)) || set.OwnerType == nil {
|
if !strings.EqualFold(set.SetType, string(domain.QuestionSetTypePractice)) || set.OwnerType == nil {
|
||||||
return false
|
return false
|
||||||
|
|
@ -626,19 +605,12 @@ func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.EqualFold(req.SetType, string(domain.QuestionSetTypeInitialAssessment)) {
|
if strings.EqualFold(req.SetType, string(domain.QuestionSetTypeInitialAssessment)) {
|
||||||
normalizedLevel, err := normalizeInitialAssessmentLevel(req.Description)
|
if req.OwnerType == nil || req.OwnerID == nil {
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid initial assessment level",
|
Message: "Invalid initial assessment ownership",
|
||||||
Error: err.Error(),
|
Error: "INITIAL_ASSESSMENT question sets must include owner_type and owner_id",
|
||||||
})
|
|
||||||
}
|
|
||||||
req.Description = &normalizedLevel
|
|
||||||
if req.PassingScore == nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid initial assessment set",
|
|
||||||
Error: "passing_score is required for INITIAL_ASSESSMENT question sets",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -790,7 +762,7 @@ func (h *Handler) GetQuestionSetsByType(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setResponses := make([]questionSetRes, 0, len(sets))
|
var setResponses []questionSetRes
|
||||||
for _, s := range sets {
|
for _, s := range sets {
|
||||||
setResponses = append(setResponses, questionSetRes{
|
setResponses = append(setResponses, questionSetRes{
|
||||||
ID: s.ID,
|
ID: s.ID,
|
||||||
|
|
@ -812,8 +784,6 @@ func (h *Handler) GetQuestionSetsByType(c *fiber.Ctx) error {
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Question sets retrieved successfully",
|
Message: "Question sets retrieved successfully",
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
Data: listQuestionSetsRes{
|
Data: listQuestionSetsRes{
|
||||||
QuestionSets: setResponses,
|
QuestionSets: setResponses,
|
||||||
TotalCount: totalCount,
|
TotalCount: totalCount,
|
||||||
|
|
@ -920,14 +890,6 @@ func (h *Handler) UpdateQuestionSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
existingSet, err := h.questionsSvc.GetQuestionSetByID(c.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Question set not found",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var req updateQuestionSetReq
|
var req updateQuestionSetReq
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
|
@ -936,46 +898,11 @@ func (h *Handler) UpdateQuestionSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
title := existingSet.Title
|
title := ""
|
||||||
if req.Title != nil {
|
if req.Title != nil {
|
||||||
title = *req.Title
|
title = *req.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.EqualFold(existingSet.SetType, string(domain.QuestionSetTypeInitialAssessment)) {
|
|
||||||
effectiveDescription := existingSet.Description
|
|
||||||
if req.Description != nil {
|
|
||||||
effectiveDescription = req.Description
|
|
||||||
}
|
|
||||||
normalizedLevel, err := normalizeInitialAssessmentLevel(effectiveDescription)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid initial assessment level",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
req.Description = &normalizedLevel
|
|
||||||
|
|
||||||
effectivePassingScore := existingSet.PassingScore
|
|
||||||
if req.PassingScore != nil {
|
|
||||||
effectivePassingScore = req.PassingScore
|
|
||||||
}
|
|
||||||
if effectivePassingScore == nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid initial assessment set",
|
|
||||||
Error: "passing_score is required for INITIAL_ASSESSMENT question sets",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
status := req.Status
|
|
||||||
if status == nil {
|
|
||||||
status = &existingSet.Status
|
|
||||||
}
|
|
||||||
shuffleQuestions := req.ShuffleQuestions
|
|
||||||
if shuffleQuestions == nil {
|
|
||||||
shuffleQuestions = &existingSet.ShuffleQuestions
|
|
||||||
}
|
|
||||||
|
|
||||||
input := domain.CreateQuestionSetInput{
|
input := domain.CreateQuestionSetInput{
|
||||||
Title: title,
|
Title: title,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
|
|
@ -983,8 +910,8 @@ func (h *Handler) UpdateQuestionSet(c *fiber.Ctx) error {
|
||||||
Persona: req.Persona,
|
Persona: req.Persona,
|
||||||
TimeLimitMinutes: req.TimeLimitMinutes,
|
TimeLimitMinutes: req.TimeLimitMinutes,
|
||||||
PassingScore: req.PassingScore,
|
PassingScore: req.PassingScore,
|
||||||
ShuffleQuestions: shuffleQuestions,
|
ShuffleQuestions: req.ShuffleQuestions,
|
||||||
Status: status,
|
Status: req.Status,
|
||||||
IntroVideoURL: req.IntroVideoURL,
|
IntroVideoURL: req.IntroVideoURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1200,64 +1127,11 @@ func (h *Handler) GetQuestionsInSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
questionResponses := make([]questionRes, 0, len(items))
|
itemResponses := questionSetItemsToRes(items)
|
||||||
for _, item := range items {
|
|
||||||
question, err := h.questionsSvc.GetQuestionWithDetails(c.Context(), item.QuestionID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to get question details",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
options := make([]optionRes, 0, len(question.Options))
|
|
||||||
for _, opt := range question.Options {
|
|
||||||
options = append(options, optionRes{
|
|
||||||
ID: opt.ID,
|
|
||||||
OptionText: opt.OptionText,
|
|
||||||
OptionOrder: opt.OptionOrder,
|
|
||||||
IsCorrect: opt.IsCorrect,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
shortAnswers := make([]shortAnswerRes, 0, len(question.ShortAnswers))
|
|
||||||
for _, sa := range question.ShortAnswers {
|
|
||||||
shortAnswers = append(shortAnswers, shortAnswerRes{
|
|
||||||
ID: sa.ID,
|
|
||||||
AcceptableAnswer: sa.AcceptableAnswer,
|
|
||||||
MatchType: sa.MatchType,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioCorrectAnswerText *string
|
|
||||||
if question.AudioAnswer != nil {
|
|
||||||
audioCorrectAnswerText = &question.AudioAnswer.CorrectAnswerText
|
|
||||||
}
|
|
||||||
|
|
||||||
questionResponses = append(questionResponses, questionRes{
|
|
||||||
ID: question.ID,
|
|
||||||
QuestionText: question.QuestionText,
|
|
||||||
QuestionType: question.QuestionType,
|
|
||||||
DifficultyLevel: question.DifficultyLevel,
|
|
||||||
Points: question.Points,
|
|
||||||
Explanation: question.Explanation,
|
|
||||||
Tips: question.Tips,
|
|
||||||
VoicePrompt: question.VoicePrompt,
|
|
||||||
SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt,
|
|
||||||
ImageURL: question.ImageURL,
|
|
||||||
Status: question.Status,
|
|
||||||
CreatedAt: question.CreatedAt.String(),
|
|
||||||
Options: options,
|
|
||||||
ShortAnswers: shortAnswers,
|
|
||||||
AudioCorrectAnswerText: audioCorrectAnswerText,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Questions retrieved successfully",
|
Message: "Questions retrieved successfully",
|
||||||
Success: true,
|
Data: itemResponses,
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
Data: questionResponses,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1397,7 +1271,7 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := h.lmsProgressSvc.CompletePracticeForUser(c.Context(), userID, set.ID); err != nil {
|
if err := h.questionsSvc.MarkPracticeCompleted(c.Context(), userID, set.ID); err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
Message: "Failed to complete practice",
|
Message: "Failed to complete practice",
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,8 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
subscriptionsvc "Yimaru-Backend/internal/services/subscriptions"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
|
@ -514,12 +512,6 @@ func (h *Handler) CheckSubscriptionStatus(c *fiber.Ctx) error {
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
// @Router /api/v1/subscriptions/{id}/cancel [post]
|
// @Router /api/v1/subscriptions/{id}/cancel [post]
|
||||||
func (h *Handler) CancelSubscription(c *fiber.Ctx) error {
|
func (h *Handler) CancelSubscription(c *fiber.Ctx) error {
|
||||||
userID, ok := c.Locals("user_id").(int64)
|
|
||||||
if !ok {
|
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Unauthorized",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
|
@ -527,25 +519,12 @@ func (h *Handler) CancelSubscription(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.subscriptionsSvc.CancelSubscriptionForUser(c.Context(), id, userID)
|
err = h.subscriptionsSvc.CancelSubscription(c.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
case errors.Is(err, subscriptionsvc.ErrSubscriptionNotFound):
|
Message: "Failed to cancel subscription",
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
Error: err.Error(),
|
||||||
Message: "Subscription not found",
|
})
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
case errors.Is(err, subscriptionsvc.ErrSubscriptionNotOwned):
|
|
||||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
|
||||||
Message: "You do not have access to this subscription",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to cancel subscription",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
|
|
@ -565,12 +544,6 @@ func (h *Handler) CancelSubscription(c *fiber.Ctx) error {
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
// @Router /api/v1/subscriptions/{id}/auto-renew [put]
|
// @Router /api/v1/subscriptions/{id}/auto-renew [put]
|
||||||
func (h *Handler) SetAutoRenew(c *fiber.Ctx) error {
|
func (h *Handler) SetAutoRenew(c *fiber.Ctx) error {
|
||||||
userID, ok := c.Locals("user_id").(int64)
|
|
||||||
if !ok {
|
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Unauthorized",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
|
@ -586,25 +559,12 @@ func (h *Handler) SetAutoRenew(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.subscriptionsSvc.SetAutoRenewForUser(c.Context(), id, userID, req.AutoRenew)
|
err = h.subscriptionsSvc.SetAutoRenew(c.Context(), id, req.AutoRenew)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
case errors.Is(err, subscriptionsvc.ErrSubscriptionNotFound):
|
Message: "Failed to update auto-renew setting",
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
Error: err.Error(),
|
||||||
Message: "Subscription not found",
|
})
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
case errors.Is(err, subscriptionsvc.ErrSubscriptionNotOwned):
|
|
||||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
|
||||||
Message: "You do not have access to this subscription",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to update auto-renew setting",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
|
|
|
||||||
|
|
@ -171,46 +171,6 @@ func (a *App) OnlyAdminAndAbove(c *fiber.Ctx) error {
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequireActiveSubscription enforces an active subscription for learner accounts.
|
|
||||||
// Staff roles (SUPER_ADMIN, ADMIN, INSTRUCTOR, SUPPORT) bypass this check.
|
|
||||||
// Use after authMiddleware on routes that deliver paid learning content.
|
|
||||||
func (a *App) RequireActiveSubscription() fiber.Handler {
|
|
||||||
return func(c *fiber.Ctx) error {
|
|
||||||
role, ok := c.Locals("role").(domain.Role)
|
|
||||||
if !ok {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Role not found in context")
|
|
||||||
}
|
|
||||||
switch role {
|
|
||||||
case domain.RoleSuperAdmin, domain.RoleAdmin, domain.RoleInstructor, domain.RoleSupport:
|
|
||||||
return c.Next()
|
|
||||||
case domain.RoleStudent:
|
|
||||||
userID, ok := c.Locals("user_id").(int64)
|
|
||||||
if !ok || userID == 0 {
|
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Unauthorized")
|
|
||||||
}
|
|
||||||
active, err := a.subscriptionsSvc.HasActiveSubscription(c.Context(), userID)
|
|
||||||
if err != nil {
|
|
||||||
a.mongoLoggerSvc.Error("subscription check failed",
|
|
||||||
zap.Int64("userID", userID),
|
|
||||||
zap.String("path", c.Path()),
|
|
||||||
zap.Error(err),
|
|
||||||
zap.Time("timestamp", time.Now()),
|
|
||||||
)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to verify subscription")
|
|
||||||
}
|
|
||||||
if !active {
|
|
||||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Active subscription required to access this content",
|
|
||||||
Error: "subscription_required",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return c.Next()
|
|
||||||
default:
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) RequirePermission(permKey string) fiber.Handler {
|
func (a *App) RequirePermission(permKey string) fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
userRole, ok := c.Locals("role").(domain.Role)
|
userRole, ok := c.Locals("role").(domain.Role)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ func (a *App) initAppRoutes() {
|
||||||
h := handlers.New(
|
h := handlers.New(
|
||||||
a.assessmentSvc,
|
a.assessmentSvc,
|
||||||
a.questionsSvc,
|
a.questionsSvc,
|
||||||
a.examPrepSvc,
|
|
||||||
a.programSvc,
|
a.programSvc,
|
||||||
a.courseSvc,
|
a.courseSvc,
|
||||||
a.moduleSvc,
|
a.moduleSvc,
|
||||||
|
|
@ -77,81 +76,43 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Post("/programs", a.authMiddleware, a.RequirePermission("programs.create"), h.CreateProgram)
|
groupV1.Post("/programs", a.authMiddleware, a.RequirePermission("programs.create"), h.CreateProgram)
|
||||||
groupV1.Get("/programs", a.authMiddleware, a.RequirePermission("programs.list"), h.ListPrograms)
|
groupV1.Get("/programs", a.authMiddleware, a.RequirePermission("programs.list"), h.ListPrograms)
|
||||||
groupV1.Put("/programs/reorder", a.authMiddleware, a.RequirePermission("programs.reorder"), h.ReorderPrograms)
|
groupV1.Put("/programs/reorder", a.authMiddleware, a.RequirePermission("programs.reorder"), h.ReorderPrograms)
|
||||||
groupV1.Get("/lms/progress", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lms.get_my_progress"), h.GetMyLMSProgress)
|
groupV1.Get("/lms/progress", a.authMiddleware, a.RequirePermission("lms.get_my_progress"), h.GetMyLMSProgress)
|
||||||
groupV1.Get("/programs/:id", a.authMiddleware, a.RequirePermission("programs.get"), h.GetProgram)
|
groupV1.Get("/programs/:id", a.authMiddleware, a.RequirePermission("programs.get"), h.GetProgram)
|
||||||
groupV1.Put("/programs/:id", a.authMiddleware, a.RequirePermission("programs.update"), h.UpdateProgram)
|
groupV1.Put("/programs/:id", a.authMiddleware, a.RequirePermission("programs.update"), h.UpdateProgram)
|
||||||
groupV1.Delete("/programs/:id", a.authMiddleware, a.RequirePermission("programs.delete"), h.DeleteProgram)
|
groupV1.Delete("/programs/:id", a.authMiddleware, a.RequirePermission("programs.delete"), h.DeleteProgram)
|
||||||
|
|
||||||
// Exam prep (schema exam_prep — separate from LMS Learn English). Students need an active subscription.
|
|
||||||
examPrep := groupV1.Group("/exam-prep", a.authMiddleware, a.RequireActiveSubscription())
|
|
||||||
examPrep.Post("/catalog-courses", a.RequirePermission("exam_prep.catalog_courses.create"), h.CreateExamPrepCatalogCourse)
|
|
||||||
examPrep.Get("/catalog-courses", a.RequirePermission("exam_prep.catalog_courses.list"), h.ListExamPrepCatalogCourses)
|
|
||||||
examPrep.Put("/catalog-courses/reorder", a.RequirePermission("exam_prep.catalog_courses.reorder"), h.ReorderExamPrepCatalogCourses)
|
|
||||||
examPrep.Get("/catalog-courses/:id", a.RequirePermission("exam_prep.catalog_courses.get"), h.GetExamPrepCatalogCourseByID)
|
|
||||||
examPrep.Put("/catalog-courses/:id", a.RequirePermission("exam_prep.catalog_courses.update"), h.UpdateExamPrepCatalogCourse)
|
|
||||||
examPrep.Delete("/catalog-courses/:id", a.RequirePermission("exam_prep.catalog_courses.delete"), h.DeleteExamPrepCatalogCourse)
|
|
||||||
|
|
||||||
examPrep.Post("/catalog-courses/:catalogCourseId/units", a.RequirePermission("exam_prep.units.create"), h.CreateExamPrepUnit)
|
|
||||||
examPrep.Get("/catalog-courses/:catalogCourseId/units", a.RequirePermission("exam_prep.units.list"), h.ListExamPrepUnitsByCatalogCourse)
|
|
||||||
examPrep.Put("/catalog-courses/:catalogCourseId/units/reorder", a.RequirePermission("exam_prep.units.reorder"), h.ReorderExamPrepUnitsInCatalogCourse)
|
|
||||||
|
|
||||||
examPrep.Post("/units/:unitId/modules", a.RequirePermission("exam_prep.modules.create"), h.CreateExamPrepModule)
|
|
||||||
examPrep.Get("/units/:unitId/modules", a.RequirePermission("exam_prep.modules.list"), h.ListExamPrepModulesByUnit)
|
|
||||||
examPrep.Put("/units/:unitId/modules/reorder", a.RequirePermission("exam_prep.modules.reorder"), h.ReorderExamPrepModulesInUnit)
|
|
||||||
|
|
||||||
examPrep.Post("/modules/:moduleId/lessons", a.RequirePermission("exam_prep.lessons.create"), h.CreateExamPrepLesson)
|
|
||||||
examPrep.Get("/modules/:moduleId/lessons", a.RequirePermission("exam_prep.lessons.list_by_module"), h.ListExamPrepLessonsByUnitModule)
|
|
||||||
examPrep.Put("/modules/:moduleId/lessons/reorder", a.RequirePermission("exam_prep.lessons.reorder"), h.ReorderExamPrepLessonsInUnitModule)
|
|
||||||
examPrep.Post("/lessons/:lessonId/practices", a.RequirePermission("exam_prep.practices.create"), h.CreateExamPrepPractice)
|
|
||||||
examPrep.Get("/lessons/:lessonId/practices", a.RequirePermission("exam_prep.practices.list_by_lesson"), h.ListExamPrepPracticesByLesson)
|
|
||||||
examPrep.Get("/practices/:id", a.RequirePermission("exam_prep.practices.get"), h.GetExamPrepPracticeByID)
|
|
||||||
examPrep.Put("/practices/:id", a.RequirePermission("exam_prep.practices.update"), h.UpdateExamPrepPractice)
|
|
||||||
examPrep.Delete("/practices/:id", a.RequirePermission("exam_prep.practices.delete"), h.DeleteExamPrepPractice)
|
|
||||||
examPrep.Get("/lessons/:id", a.RequirePermission("exam_prep.lessons.get"), h.GetExamPrepLessonByID)
|
|
||||||
examPrep.Put("/lessons/:id", a.RequirePermission("exam_prep.lessons.update"), h.UpdateExamPrepLesson)
|
|
||||||
examPrep.Delete("/lessons/:id", a.RequirePermission("exam_prep.lessons.delete"), h.DeleteExamPrepLesson)
|
|
||||||
|
|
||||||
examPrep.Get("/modules/:id", a.RequirePermission("exam_prep.modules.get"), h.GetExamPrepModuleByID)
|
|
||||||
examPrep.Put("/modules/:id", a.RequirePermission("exam_prep.modules.update"), h.UpdateExamPrepModule)
|
|
||||||
examPrep.Delete("/modules/:id", a.RequirePermission("exam_prep.modules.delete"), h.DeleteExamPrepModule)
|
|
||||||
|
|
||||||
examPrep.Get("/units/:id", a.RequirePermission("exam_prep.units.get"), h.GetExamPrepUnitByID)
|
|
||||||
examPrep.Put("/units/:id", a.RequirePermission("exam_prep.units.update"), h.UpdateExamPrepUnit)
|
|
||||||
examPrep.Delete("/units/:id", a.RequirePermission("exam_prep.units.delete"), h.DeleteExamPrepUnit)
|
|
||||||
|
|
||||||
// Courses
|
// Courses
|
||||||
groupV1.Post("/programs/:id/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
|
groupV1.Post("/programs/:id/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
|
||||||
groupV1.Put("/programs/:id/courses/reorder", a.authMiddleware, a.RequirePermission("courses.reorder"), h.ReorderCoursesInProgram)
|
groupV1.Put("/programs/:id/courses/reorder", a.authMiddleware, a.RequirePermission("courses.reorder"), h.ReorderCoursesInProgram)
|
||||||
groupV1.Get("/programs/:id/courses", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("courses.list_by_program"), h.ListCoursesByProgram)
|
groupV1.Get("/programs/:id/courses", a.authMiddleware, a.RequirePermission("courses.list_by_program"), h.ListCoursesByProgram)
|
||||||
groupV1.Get("/courses/:id/practices", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("practices.list"), h.ListPracticesByCourse)
|
groupV1.Get("/courses/:id/practices", a.authMiddleware, a.RequirePermission("practices.list"), h.ListPracticesByCourse)
|
||||||
groupV1.Get("/courses/:id", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("courses.get"), h.GetCourse)
|
groupV1.Get("/courses/:id", a.authMiddleware, a.RequirePermission("courses.get"), h.GetCourse)
|
||||||
groupV1.Put("/courses/:id", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse)
|
groupV1.Put("/courses/:id", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse)
|
||||||
groupV1.Delete("/courses/:id", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse)
|
groupV1.Delete("/courses/:id", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse)
|
||||||
groupV1.Post("/courses/:courseId/modules", a.authMiddleware, a.RequirePermission("modules.create"), h.CreateModule)
|
groupV1.Post("/courses/:courseId/modules", a.authMiddleware, a.RequirePermission("modules.create"), h.CreateModule)
|
||||||
groupV1.Put("/courses/:courseId/modules/reorder", a.authMiddleware, a.RequirePermission("modules.reorder"), h.ReorderModulesInCourse)
|
groupV1.Put("/courses/:courseId/modules/reorder", a.authMiddleware, a.RequirePermission("modules.reorder"), h.ReorderModulesInCourse)
|
||||||
groupV1.Get("/courses/:courseId/modules", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("modules.list_by_course"), h.ListModulesByCourse)
|
groupV1.Get("/courses/:courseId/modules", a.authMiddleware, a.RequirePermission("modules.list_by_course"), h.ListModulesByCourse)
|
||||||
|
|
||||||
// /modules/:moduleId/lessons before /modules/:id; /modules/:id/practices before /modules/:id
|
// /modules/:moduleId/lessons before /modules/:id; /modules/:id/practices before /modules/:id
|
||||||
groupV1.Post("/modules/:moduleId/lessons", a.authMiddleware, a.RequirePermission("lessons.create"), h.CreateLesson)
|
groupV1.Post("/modules/:moduleId/lessons", a.authMiddleware, a.RequirePermission("lessons.create"), h.CreateLesson)
|
||||||
groupV1.Get("/modules/:moduleId/lessons", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lessons.list_by_module"), h.ListLessonsByModule)
|
groupV1.Get("/modules/:moduleId/lessons", a.authMiddleware, a.RequirePermission("lessons.list_by_module"), h.ListLessonsByModule)
|
||||||
groupV1.Get("/modules/:id/practices", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("practices.list"), h.ListPracticesByModule)
|
groupV1.Get("/modules/:id/practices", a.authMiddleware, a.RequirePermission("practices.list"), h.ListPracticesByModule)
|
||||||
groupV1.Get("/modules/:id", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("modules.get"), h.GetModule)
|
groupV1.Get("/modules/:id", a.authMiddleware, a.RequirePermission("modules.get"), h.GetModule)
|
||||||
groupV1.Put("/modules/:id", a.authMiddleware, a.RequirePermission("modules.update"), h.UpdateModule)
|
groupV1.Put("/modules/:id", a.authMiddleware, a.RequirePermission("modules.update"), h.UpdateModule)
|
||||||
groupV1.Delete("/modules/:id", a.authMiddleware, a.RequirePermission("modules.delete"), h.DeleteModule)
|
groupV1.Delete("/modules/:id", a.authMiddleware, a.RequirePermission("modules.delete"), h.DeleteModule)
|
||||||
groupV1.Get("/lessons/:id/practices", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("practices.list"), h.ListPracticesByLesson)
|
groupV1.Get("/lessons/:id/practices", a.authMiddleware, a.RequirePermission("practices.list"), h.ListPracticesByLesson)
|
||||||
groupV1.Post("/lessons/:id/complete", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lessons.complete"), h.CompleteLesson)
|
groupV1.Post("/lessons/:id/complete", a.authMiddleware, a.RequirePermission("lessons.complete"), h.CompleteLesson)
|
||||||
groupV1.Get("/lessons/:id", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("lessons.get"), h.GetLesson)
|
groupV1.Get("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.get"), h.GetLesson)
|
||||||
groupV1.Put("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.update"), h.UpdateLesson)
|
groupV1.Put("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.update"), h.UpdateLesson)
|
||||||
groupV1.Delete("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.delete"), h.DeleteLesson)
|
groupV1.Delete("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.delete"), h.DeleteLesson)
|
||||||
|
|
||||||
groupV1.Post("/practices", a.authMiddleware, a.RequirePermission("practices.create"), h.CreatePractice)
|
groupV1.Post("/practices", a.authMiddleware, a.RequirePermission("practices.create"), h.CreatePractice)
|
||||||
groupV1.Get("/practices/:id", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("practices.get"), h.GetPractice)
|
groupV1.Get("/practices/:id", a.authMiddleware, a.RequirePermission("practices.get"), h.GetPractice)
|
||||||
groupV1.Put("/practices/:id", a.authMiddleware, a.RequirePermission("practices.update"), h.UpdatePractice)
|
groupV1.Put("/practices/:id", a.authMiddleware, a.RequirePermission("practices.update"), h.UpdatePractice)
|
||||||
groupV1.Delete("/practices/:id", a.authMiddleware, a.RequirePermission("practices.delete"), h.DeletePractice)
|
groupV1.Delete("/practices/:id", a.authMiddleware, a.RequirePermission("practices.delete"), h.DeletePractice)
|
||||||
|
|
||||||
// File storage (MinIO)
|
// File storage (MinIO)
|
||||||
groupV1.Get("/files/url", a.authMiddleware, h.GetFileURL)
|
groupV1.Get("/files/url", a.authMiddleware, h.GetFileURL)
|
||||||
groupV1.Post("/files/refresh-url", a.authMiddleware, h.RefreshFileURL)
|
|
||||||
groupV1.Post("/files/upload", a.authMiddleware, h.UploadMedia)
|
groupV1.Post("/files/upload", a.authMiddleware, h.UploadMedia)
|
||||||
groupV1.Post("/files/audio", a.authMiddleware, h.UploadAudio)
|
groupV1.Post("/files/audio", a.authMiddleware, h.UploadAudio)
|
||||||
groupV1.Post("/questions/audio-answer", a.authMiddleware, h.SubmitAudioAnswer)
|
groupV1.Post("/questions/audio-answer", a.authMiddleware, h.SubmitAudioAnswer)
|
||||||
|
|
@ -165,8 +126,6 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
||||||
groupV1.Get("/questions", a.authMiddleware, a.RequirePermission("questions.list"), h.ListQuestions)
|
groupV1.Get("/questions", a.authMiddleware, a.RequirePermission("questions.list"), h.ListQuestions)
|
||||||
groupV1.Get("/questions/search", a.authMiddleware, a.RequirePermission("questions.search"), h.SearchQuestions)
|
groupV1.Get("/questions/search", a.authMiddleware, a.RequirePermission("questions.search"), h.SearchQuestions)
|
||||||
groupV1.Get("/questions/component-catalog", a.authMiddleware, a.RequirePermission("questions.list"), h.GetQuestionTypeComponentCatalog)
|
|
||||||
groupV1.Post("/questions/validate-question-type-definition", a.authMiddleware, a.RequirePermission("questions.create"), h.ValidateQuestionTypeDefinition)
|
|
||||||
groupV1.Get("/questions/:id", a.authMiddleware, a.RequirePermission("questions.get"), h.GetQuestionByID)
|
groupV1.Get("/questions/:id", a.authMiddleware, a.RequirePermission("questions.get"), h.GetQuestionByID)
|
||||||
groupV1.Put("/questions/:id", a.authMiddleware, a.RequirePermission("questions.update"), h.UpdateQuestion)
|
groupV1.Put("/questions/:id", a.authMiddleware, a.RequirePermission("questions.update"), h.UpdateQuestion)
|
||||||
groupV1.Delete("/questions/:id", a.authMiddleware, a.RequirePermission("questions.delete"), h.DeleteQuestion)
|
groupV1.Delete("/questions/:id", a.authMiddleware, a.RequirePermission("questions.delete"), h.DeleteQuestion)
|
||||||
|
|
@ -182,7 +141,7 @@ func (a *App) initAppRoutes() {
|
||||||
// Question Set Items
|
// Question Set Items
|
||||||
groupV1.Post("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.add"), h.AddQuestionToSet)
|
groupV1.Post("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.add"), h.AddQuestionToSet)
|
||||||
groupV1.Get("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.list"), h.GetQuestionsInSet)
|
groupV1.Get("/question-sets/:setId/questions", a.authMiddleware, a.RequirePermission("question_set_items.list"), h.GetQuestionsInSet)
|
||||||
groupV1.Get("/practices/:practiceId/questions", a.authMiddleware, a.RequireActiveSubscription(), a.RequirePermission("question_set_items.list"), h.GetQuestionsByPractice)
|
groupV1.Get("/practices/:practiceId/questions", a.authMiddleware, a.RequirePermission("question_set_items.list"), h.GetQuestionsByPractice)
|
||||||
groupV1.Delete("/question-sets/:setId/questions/:questionId", a.authMiddleware, a.RequirePermission("question_set_items.remove"), h.RemoveQuestionFromSet)
|
groupV1.Delete("/question-sets/:setId/questions/:questionId", a.authMiddleware, a.RequirePermission("question_set_items.remove"), h.RemoveQuestionFromSet)
|
||||||
groupV1.Put("/question-sets/:setId/questions/:questionId/order", a.authMiddleware, a.RequirePermission("question_set_items.update_order"), h.UpdateQuestionOrderInSet)
|
groupV1.Put("/question-sets/:setId/questions/:questionId/order", a.authMiddleware, a.RequirePermission("question_set_items.update_order"), h.UpdateQuestionOrderInSet)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,457 +0,0 @@
|
||||||
{
|
|
||||||
"info": {
|
|
||||||
"_postman_id": "f7c2e4a1-8b3d-4e9f-a2c6-11dd99ee5501",
|
|
||||||
"name": "Yimaru Exam Prep (Duolingo)",
|
|
||||||
"description": "Exam-prep tree API (`/api/v1/exam-prep/...`): catalog courses → units → modules → lessons → practices. Requires Bearer token.\n\n**Courses** = `catalog-courses` in the backend. Set collection variables before chaining requests.",
|
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"type": "bearer",
|
|
||||||
"bearer": [
|
|
||||||
{
|
|
||||||
"key": "token",
|
|
||||||
"value": "{{accessToken}}",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "baseUrl",
|
|
||||||
"value": "http://localhost:8080"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "accessToken",
|
|
||||||
"value": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "catalogCourseId",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "unitId",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "moduleId",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "lessonId",
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "practiceId",
|
|
||||||
"value": "1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Duolingo",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Courses",
|
|
||||||
"description": "Backend route group: **`catalog-courses`** (`exam_prep.catalog_courses.*`)",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create catalog course",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"IELTS Prep\",\n \"description\": \"Optional description\",\n \"thumbnail\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.create`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List catalog courses",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses?limit=20&offset=0",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.list`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Reorder catalog courses",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"ordered_ids\": [1, 2, 3]\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/reorder",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.reorder`. Must include every id in scope exactly once."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get catalog course by ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.get`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update catalog course",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Updated name\",\n \"description\": null,\n \"thumbnail\": null,\n \"sort_order\": 1\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.update`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete catalog course",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}",
|
|
||||||
"description": "Permission: `exam_prep.catalog_courses.delete`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Units",
|
|
||||||
"description": "Nested under catalog course (`exam_prep.units.*`)",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create unit",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Grammar foundations\",\n \"description\": null,\n \"thumbnail\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}/units",
|
|
||||||
"description": "Permission: `exam_prep.units.create`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List units by catalog course",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}/units?limit=20&offset=0",
|
|
||||||
"description": "Permission: `exam_prep.units.list`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Reorder units in catalog course",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"ordered_ids\": [1, 2, 3]\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/catalog-courses/{{catalogCourseId}}/units/reorder",
|
|
||||||
"description": "Permission: `exam_prep.units.reorder`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get unit by ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}",
|
|
||||||
"description": "Permission: `exam_prep.units.get`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update unit",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Updated unit\",\n \"description\": null,\n \"thumbnail\": null,\n \"sort_order\": 1\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}",
|
|
||||||
"description": "Permission: `exam_prep.units.update`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete unit",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}",
|
|
||||||
"description": "Permission: `exam_prep.units.delete`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Modules",
|
|
||||||
"description": "Exam-prep **`unit_modules`** (`exam_prep.modules.*`)",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create module",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Present tense\",\n \"description\": null,\n \"thumbnail\": null,\n \"icon\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}/modules",
|
|
||||||
"description": "Permission: `exam_prep.modules.create`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List modules by unit",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}/modules?limit=20&offset=0",
|
|
||||||
"description": "Permission: `exam_prep.modules.list`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Reorder modules in unit",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"ordered_ids\": [1, 2, 3]\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/units/{{unitId}}/modules/reorder",
|
|
||||||
"description": "Permission: `exam_prep.modules.reorder`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get module by ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}",
|
|
||||||
"description": "Permission: `exam_prep.modules.get`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update module",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Updated module\",\n \"description\": null,\n \"thumbnail\": null,\n \"icon\": null,\n \"sort_order\": 1\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}",
|
|
||||||
"description": "Permission: `exam_prep.modules.update`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete module",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}",
|
|
||||||
"description": "Permission: `exam_prep.modules.delete`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Lessons",
|
|
||||||
"description": "`exam_prep.lessons.*`",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create lesson",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Intro video\",\n \"video_url\": \"https://example.com/video\",\n \"thumbnail\": null,\n \"description\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}/lessons",
|
|
||||||
"description": "Permission: `exam_prep.lessons.create`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List lessons by module",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}/lessons?limit=20&offset=0",
|
|
||||||
"description": "Permission: `exam_prep.lessons.list_by_module`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Reorder lessons in module",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"ordered_ids\": [1, 2, 3]\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/modules/{{moduleId}}/lessons/reorder",
|
|
||||||
"description": "Permission: `exam_prep.lessons.reorder`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get lesson by ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/lessons/{{lessonId}}",
|
|
||||||
"description": "Permission: `exam_prep.lessons.get`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update lesson",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Updated lesson\",\n \"video_url\": null,\n \"thumbnail\": null,\n \"description\": null,\n \"sort_order\": 1\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/lessons/{{lessonId}}",
|
|
||||||
"description": "Permission: `exam_prep.lessons.update`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete lesson",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/lessons/{{lessonId}}",
|
|
||||||
"description": "Permission: `exam_prep.lessons.delete`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Practices",
|
|
||||||
"description": "Tied to lesson; **`question_set_id`** references shared `question_sets`. `exam_prep.practices.*`",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create practice",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Drill: articles\",\n \"story_description\": null,\n \"story_image\": null,\n \"persona_id\": null,\n \"question_set_id\": 1,\n \"quick_tips\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/lessons/{{lessonId}}/practices",
|
|
||||||
"description": "Permission: `exam_prep.practices.create`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List practices by lesson",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/lessons/{{lessonId}}/practices?limit=20&offset=0",
|
|
||||||
"description": "Permission: `exam_prep.practices.list_by_lesson`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get practice by ID",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/practices/{{practiceId}}",
|
|
||||||
"description": "Permission: `exam_prep.practices.get`"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update practice",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Updated practice\",\n \"story_description\": null,\n \"story_image\": null,\n \"persona_id\": null,\n \"question_set_id\": 1,\n \"quick_tips\": null\n}"
|
|
||||||
},
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/practices/{{practiceId}}",
|
|
||||||
"description": "Permission: `exam_prep.practices.update`. Omit fields you do not change."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete practice",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"url": "{{baseUrl}}/api/v1/exam-prep/practices/{{practiceId}}",
|
|
||||||
"description": "Permission: `exam_prep.practices.delete`"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user