Compare commits
5 Commits
9154dec067
...
5b53929d92
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b53929d92 | |||
| dc788c04cb | |||
| 6c672c4b20 | |||
| 9db9c9899a | |||
| 152478a96c |
47
cmd/main.go
47
cmd/main.go
|
|
@ -14,10 +14,15 @@ import (
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
"Yimaru-Backend/internal/services/course_management"
|
|
||||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||||
"Yimaru-Backend/internal/services/messenger"
|
"Yimaru-Backend/internal/services/messenger"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
|
coursesservice "Yimaru-Backend/internal/services/courses"
|
||||||
|
lessonsservice "Yimaru-Backend/internal/services/lessons"
|
||||||
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
|
moduleservice "Yimaru-Backend/internal/services/modules"
|
||||||
|
practicesservice "Yimaru-Backend/internal/services/practices"
|
||||||
|
programsservice "Yimaru-Backend/internal/services/programs"
|
||||||
"Yimaru-Backend/internal/services/questions"
|
"Yimaru-Backend/internal/services/questions"
|
||||||
"Yimaru-Backend/internal/services/recommendation"
|
"Yimaru-Backend/internal/services/recommendation"
|
||||||
"Yimaru-Backend/internal/services/settings"
|
"Yimaru-Backend/internal/services/settings"
|
||||||
|
|
@ -360,24 +365,10 @@ func main() {
|
||||||
logger.Info("Vimeo service disabled (VIMEO_ENABLED not set or missing access token)")
|
logger.Info("Vimeo service disabled (VIMEO_ENABLED not set or missing access token)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Course management service
|
// CloudConvert service for image/video optimization
|
||||||
courseSvc := course_management.NewService(
|
|
||||||
repository.NewUserStore(store),
|
|
||||||
repository.NewCourseStore(store),
|
|
||||||
repository.NewProgressionStore(store),
|
|
||||||
notificationSvc,
|
|
||||||
cfg,
|
|
||||||
)
|
|
||||||
// Wire up Vimeo service to course management
|
|
||||||
if vimeoSvc != nil {
|
|
||||||
courseSvc.SetVimeoService(vimeoSvc)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloudConvert service for video compression
|
|
||||||
var ccSvc *cloudconvertservice.Service
|
var ccSvc *cloudconvertservice.Service
|
||||||
if cfg.CloudConvert.Enabled && cfg.CloudConvert.APIKey != "" {
|
if cfg.CloudConvert.Enabled && cfg.CloudConvert.APIKey != "" {
|
||||||
ccSvc = cloudconvertservice.NewService(cfg.CloudConvert.APIKey, domain.MongoDBLogger)
|
ccSvc = cloudconvertservice.NewService(cfg.CloudConvert.APIKey, domain.MongoDBLogger)
|
||||||
courseSvc.SetCloudConvertService(ccSvc)
|
|
||||||
logger.Info("CloudConvert service initialized")
|
logger.Info("CloudConvert service initialized")
|
||||||
} else {
|
} else {
|
||||||
logger.Info("CloudConvert service disabled (CLOUDCONVERT_ENABLED not set or missing API key)")
|
logger.Info("CloudConvert service disabled (CLOUDCONVERT_ENABLED not set or missing API key)")
|
||||||
|
|
@ -402,6 +393,23 @@ func main() {
|
||||||
// Questions service (unified questions system)
|
// Questions service (unified questions system)
|
||||||
questionsSvc := questions.NewService(store)
|
questionsSvc := questions.NewService(store)
|
||||||
|
|
||||||
|
// LMS programs (top-level hierarchy)
|
||||||
|
programSvc := programsservice.NewService(store)
|
||||||
|
|
||||||
|
// LMS courses (under programs)
|
||||||
|
courseSvc := coursesservice.NewService(store, store)
|
||||||
|
|
||||||
|
// LMS modules (under courses)
|
||||||
|
moduleSvc := moduleservice.NewService(store, store)
|
||||||
|
|
||||||
|
// LMS lessons (under modules)
|
||||||
|
lessonSvc := lessonsservice.NewService(store, store)
|
||||||
|
|
||||||
|
lmsProgressSvc := lmsprogress.NewService(store)
|
||||||
|
|
||||||
|
// LMS practices (under course, module, or lesson)
|
||||||
|
practiceSvc := practicesservice.NewService(store, store, store, store, store, store)
|
||||||
|
|
||||||
// Subscriptions service
|
// Subscriptions service
|
||||||
subscriptionsSvc := subscriptions.NewService(store)
|
subscriptionsSvc := subscriptions.NewService(store)
|
||||||
|
|
||||||
|
|
@ -442,8 +450,13 @@ func main() {
|
||||||
// Initialize and start HTTP server
|
// Initialize and start HTTP server
|
||||||
app := httpserver.NewApp(
|
app := httpserver.NewApp(
|
||||||
assessmentSvc,
|
assessmentSvc,
|
||||||
courseSvc,
|
|
||||||
questionsSvc,
|
questionsSvc,
|
||||||
|
programSvc,
|
||||||
|
courseSvc,
|
||||||
|
moduleSvc,
|
||||||
|
lessonSvc,
|
||||||
|
lmsProgressSvc,
|
||||||
|
practiceSvc,
|
||||||
subscriptionsSvc,
|
subscriptionsSvc,
|
||||||
arifpaySvc,
|
arifpaySvc,
|
||||||
issueReportingSvc,
|
issueReportingSvc,
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
-- Intentionally empty: course hierarchy is not seeded from SQL.
|
|
||||||
-- Use admin/API or migrations to create content.
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
-- Restoring the removed course hierarchy is not supported; apply new migrations for the next model.
|
||||||
46
db/migrations/000041_remove_course_management_schema.up.sql
Normal file
46
db/migrations/000041_remove_course_management_schema.up.sql
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
-- Tear down the legacy course / learning-tree schema so a new hierarchy can be introduced.
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Entry-assessment automation on sub_courses (from 000024)
|
||||||
|
DROP TRIGGER IF EXISTS trg_sub_courses_create_entry_assessment ON sub_courses;
|
||||||
|
DROP FUNCTION IF EXISTS create_sub_course_entry_assessment();
|
||||||
|
DROP FUNCTION IF EXISTS clone_default_initial_assessment_items(BIGINT);
|
||||||
|
DROP INDEX IF EXISTS idx_question_sets_unique_subcourse_initial_assessment;
|
||||||
|
|
||||||
|
ALTER TABLE question_sets DROP COLUMN IF EXISTS sub_course_video_id;
|
||||||
|
|
||||||
|
-- Dependent objects first
|
||||||
|
DROP TABLE IF EXISTS user_sub_course_video_progress CASCADE;
|
||||||
|
DROP TABLE IF EXISTS user_practice_progress CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_course_prerequisites CASCADE;
|
||||||
|
DROP TABLE IF EXISTS user_sub_course_progress CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS sub_module_practices CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_module_capstones CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_module_lessons CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_module_videos CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_modules CASCADE;
|
||||||
|
DROP TABLE IF EXISTS module_capstones CASCADE;
|
||||||
|
DROP TABLE IF EXISTS modules CASCADE;
|
||||||
|
DROP TABLE IF EXISTS levels CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS sub_course_videos CASCADE;
|
||||||
|
DROP TABLE IF EXISTS sub_courses CASCADE;
|
||||||
|
DROP TABLE IF EXISTS course_sub_categories CASCADE;
|
||||||
|
DROP TABLE IF EXISTS courses CASCADE;
|
||||||
|
DROP TABLE IF EXISTS course_categories CASCADE;
|
||||||
|
|
||||||
|
-- Keep learner practice completion for the questions system (no sub_course column)
|
||||||
|
CREATE TABLE user_practice_progress (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE (user_id, question_set_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_user_practice_progress_user_id ON user_practice_progress(user_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
1
db/migrations/000042_programs.down.sql
Normal file
1
db/migrations/000042_programs.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS programs;
|
||||||
11
db/migrations/000042_programs.up.sql
Normal file
11
db/migrations/000042_programs.up.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
-- Top-level LMS program (e.g. Beginner / Intermediate / Advanced — labels come from admin config later).
|
||||||
|
CREATE TABLE programs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
thumbnail TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_programs_created_at ON programs (created_at DESC);
|
||||||
4
db/migrations/000043_seed_default_programs.down.sql
Normal file
4
db/migrations/000043_seed_default_programs.down.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
DELETE FROM programs
|
||||||
|
WHERE (name = 'Beginner' AND description = 'Default program for the beginner level.')
|
||||||
|
OR (name = 'Intermediate' AND description = 'Default program for the intermediate level.')
|
||||||
|
OR (name = 'Advanced' AND description = 'Default program for the advanced level.');
|
||||||
6
db/migrations/000043_seed_default_programs.up.sql
Normal file
6
db/migrations/000043_seed_default_programs.up.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Default top-level programs (hierarchy: Program → Course → …).
|
||||||
|
INSERT INTO programs (name, description, thumbnail)
|
||||||
|
VALUES
|
||||||
|
('Beginner', 'Default program for the beginner level.', NULL),
|
||||||
|
('Intermediate', 'Default program for the intermediate level.', NULL),
|
||||||
|
('Advanced', 'Default program for the advanced level.', NULL);
|
||||||
1
db/migrations/000044_lms_courses.down.sql
Normal file
1
db/migrations/000044_lms_courses.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS courses;
|
||||||
13
db/migrations/000044_lms_courses.up.sql
Normal file
13
db/migrations/000044_lms_courses.up.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
-- Courses belong to a Program (CEFR-style labels like A1..C2 will be configured separately).
|
||||||
|
CREATE TABLE courses (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
program_id BIGINT NOT NULL REFERENCES programs (id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
thumbnail TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_courses_program_id ON courses (program_id);
|
||||||
|
CREATE INDEX idx_courses_program_created ON courses (program_id, created_at DESC);
|
||||||
2
db/migrations/000045_lms_modules.down.sql
Normal file
2
db/migrations/000045_lms_modules.down.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
DROP TABLE IF EXISTS modules;
|
||||||
|
ALTER TABLE courses DROP CONSTRAINT IF EXISTS courses_program_id_id_key;
|
||||||
22
db/migrations/000045_lms_modules.up.sql
Normal file
22
db/migrations/000045_lms_modules.up.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
-- Modules belong to a Course; program_id is denormalized and enforced with the course by a composite FK.
|
||||||
|
ALTER TABLE courses
|
||||||
|
ADD CONSTRAINT courses_program_id_id_key UNIQUE (program_id, id);
|
||||||
|
|
||||||
|
CREATE TABLE modules (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
program_id BIGINT NOT NULL,
|
||||||
|
course_id BIGINT NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
icon TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
CONSTRAINT modules_course_scope_fkey
|
||||||
|
FOREIGN KEY (program_id, course_id)
|
||||||
|
REFERENCES courses (program_id, id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_modules_course_id ON modules (course_id);
|
||||||
|
CREATE INDEX idx_modules_program_id ON modules (program_id);
|
||||||
|
CREATE INDEX idx_modules_program_course_created ON modules (program_id, course_id, created_at DESC);
|
||||||
1
db/migrations/000046_lms_lessons.down.sql
Normal file
1
db/migrations/000046_lms_lessons.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS lessons;
|
||||||
14
db/migrations/000046_lms_lessons.up.sql
Normal file
14
db/migrations/000046_lms_lessons.up.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- Lessons belong to a Module.
|
||||||
|
CREATE TABLE lessons (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
module_id BIGINT NOT NULL REFERENCES modules (id) ON DELETE CASCADE,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
video_url TEXT,
|
||||||
|
thumbnail TEXT,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lessons_module_id ON lessons (module_id);
|
||||||
|
CREATE INDEX idx_lessons_module_created ON lessons (module_id, created_at DESC);
|
||||||
1
db/migrations/000047_lms_practices.down.sql
Normal file
1
db/migrations/000047_lms_practices.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS lms_practices;
|
||||||
29
db/migrations/000047_lms_practices.up.sql
Normal file
29
db/migrations/000047_lms_practices.up.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- Practices attach to exactly one of: course, module, or lesson.
|
||||||
|
CREATE TABLE lms_practices (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
course_id BIGINT REFERENCES courses (id) ON DELETE CASCADE,
|
||||||
|
module_id BIGINT REFERENCES modules (id) ON DELETE CASCADE,
|
||||||
|
lesson_id BIGINT REFERENCES 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,
|
||||||
|
CONSTRAINT lms_practices_one_parent CHECK (
|
||||||
|
(course_id IS NOT NULL)::int
|
||||||
|
+ (module_id IS NOT NULL)::int
|
||||||
|
+ (lesson_id IS NOT NULL)::int
|
||||||
|
= 1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_practices_course_id ON lms_practices (course_id);
|
||||||
|
CREATE INDEX idx_lms_practices_module_id ON lms_practices (module_id);
|
||||||
|
CREATE INDEX idx_lms_practices_lesson_id ON lms_practices (lesson_id);
|
||||||
|
CREATE INDEX idx_lms_practices_question_set_id ON lms_practices (question_set_id);
|
||||||
|
CREATE INDEX idx_lms_practices_course_created ON lms_practices (course_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_lms_practices_module_created ON lms_practices (module_id, created_at DESC);
|
||||||
|
CREATE INDEX idx_lms_practices_lesson_created ON lms_practices (lesson_id, created_at DESC);
|
||||||
3
db/migrations/000048_seed_default_courses.down.sql
Normal file
3
db/migrations/000048_seed_default_courses.down.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
DELETE FROM courses
|
||||||
|
WHERE description = 'Default CEFR level course (system seed).'
|
||||||
|
AND name IN ('A1', 'A2', 'B1', 'B2', 'C1', 'C2');
|
||||||
18
db/migrations/000048_seed_default_courses.up.sql
Normal file
18
db/migrations/000048_seed_default_courses.up.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- Default CEFR-style course names per program (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)
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
v.name,
|
||||||
|
'Default CEFR level course (system seed).',
|
||||||
|
NULL
|
||||||
|
FROM programs AS p
|
||||||
|
CROSS JOIN (
|
||||||
|
VALUES
|
||||||
|
('A1'),
|
||||||
|
('A2'),
|
||||||
|
('B1'),
|
||||||
|
('B2'),
|
||||||
|
('C1'),
|
||||||
|
('C2')
|
||||||
|
) AS v (name);
|
||||||
18
db/migrations/000049_lms_sequential_learning.down.sql
Normal file
18
db/migrations/000049_lms_sequential_learning.down.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
DROP TABLE IF EXISTS lms_user_program_progress;
|
||||||
|
DROP TABLE IF EXISTS lms_user_course_progress;
|
||||||
|
DROP TABLE IF EXISTS lms_user_module_progress;
|
||||||
|
DROP TABLE IF EXISTS lms_user_lesson_progress;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS uq_lessons_module_sort;
|
||||||
|
DROP INDEX IF EXISTS uq_modules_course_sort;
|
||||||
|
DROP INDEX IF EXISTS uq_courses_program_sort;
|
||||||
|
DROP INDEX IF EXISTS uq_programs_sort_order;
|
||||||
|
|
||||||
|
ALTER TABLE lessons
|
||||||
|
DROP COLUMN IF EXISTS sort_order;
|
||||||
|
ALTER TABLE modules
|
||||||
|
DROP COLUMN IF EXISTS sort_order;
|
||||||
|
ALTER TABLE courses
|
||||||
|
DROP COLUMN IF EXISTS sort_order;
|
||||||
|
ALTER TABLE programs
|
||||||
|
DROP COLUMN IF EXISTS sort_order;
|
||||||
150
db/migrations/000049_lms_sequential_learning.up.sql
Normal file
150
db/migrations/000049_lms_sequential_learning.up.sql
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
-- Sequential order for programs, courses, modules, and lessons (1 = first in each scope).
|
||||||
|
-- Progress tables mark completion; API enforces prerequisites for learners (STUDENT role).
|
||||||
|
|
||||||
|
ALTER TABLE programs
|
||||||
|
ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE courses
|
||||||
|
ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE modules
|
||||||
|
ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE lessons
|
||||||
|
ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Program order (one global sequence): Beginner -> Intermediate -> Advanced; others by id
|
||||||
|
UPDATE programs
|
||||||
|
SET sort_order = v.so
|
||||||
|
FROM (
|
||||||
|
VALUES
|
||||||
|
('Beginner', 1),
|
||||||
|
('Intermediate', 2),
|
||||||
|
('Advanced', 3)
|
||||||
|
) AS v (name, so)
|
||||||
|
WHERE programs.name = v.name;
|
||||||
|
|
||||||
|
UPDATE programs
|
||||||
|
SET sort_order = 1000 + r.rn
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
row_number() OVER (
|
||||||
|
ORDER BY id
|
||||||
|
) AS rn
|
||||||
|
FROM programs
|
||||||
|
WHERE
|
||||||
|
sort_order = 0
|
||||||
|
) AS r
|
||||||
|
WHERE
|
||||||
|
programs.id = r.id;
|
||||||
|
|
||||||
|
-- CEFR courses: A1..C2; remaining courses in each program: stable order
|
||||||
|
UPDATE courses
|
||||||
|
SET sort_order = CASE name
|
||||||
|
WHEN 'A1' THEN
|
||||||
|
1
|
||||||
|
WHEN 'A2' THEN
|
||||||
|
2
|
||||||
|
WHEN 'B1' THEN
|
||||||
|
3
|
||||||
|
WHEN 'B2' THEN
|
||||||
|
4
|
||||||
|
WHEN 'C1' THEN
|
||||||
|
5
|
||||||
|
WHEN 'C2' THEN
|
||||||
|
6
|
||||||
|
ELSE
|
||||||
|
0
|
||||||
|
END
|
||||||
|
WHERE
|
||||||
|
name IN ('A1', 'A2', 'B1', 'B2', 'C1', 'C2');
|
||||||
|
|
||||||
|
UPDATE courses c
|
||||||
|
SET sort_order = 2000 + s.rn
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
row_number() OVER (
|
||||||
|
PARTITION BY program_id
|
||||||
|
ORDER BY
|
||||||
|
id
|
||||||
|
) AS rn
|
||||||
|
FROM courses
|
||||||
|
WHERE
|
||||||
|
sort_order = 0
|
||||||
|
) AS s
|
||||||
|
WHERE
|
||||||
|
c.id = s.id;
|
||||||
|
|
||||||
|
UPDATE modules m
|
||||||
|
SET sort_order = r.rn
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
row_number() OVER (
|
||||||
|
PARTITION BY course_id
|
||||||
|
ORDER BY
|
||||||
|
id
|
||||||
|
) AS rn
|
||||||
|
FROM modules
|
||||||
|
) AS r
|
||||||
|
WHERE
|
||||||
|
m.id = r.id;
|
||||||
|
|
||||||
|
UPDATE lessons l
|
||||||
|
SET sort_order = r.rn
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
row_number() OVER (
|
||||||
|
PARTITION BY module_id
|
||||||
|
ORDER BY
|
||||||
|
id
|
||||||
|
) AS rn
|
||||||
|
FROM lessons
|
||||||
|
) AS r
|
||||||
|
WHERE
|
||||||
|
l.id = r.id;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX uq_programs_sort_order ON programs (sort_order);
|
||||||
|
CREATE UNIQUE INDEX uq_courses_program_sort ON courses (program_id, sort_order);
|
||||||
|
CREATE UNIQUE INDEX uq_modules_course_sort ON modules (course_id, sort_order);
|
||||||
|
CREATE UNIQUE INDEX uq_lessons_module_sort ON lessons (module_id, sort_order);
|
||||||
|
|
||||||
|
CREATE TABLE lms_user_lesson_progress (
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
lesson_id BIGINT NOT NULL REFERENCES lessons (id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, lesson_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_user_lesson_progress_user ON lms_user_lesson_progress (user_id);
|
||||||
|
CREATE INDEX idx_lms_user_lesson_progress_lesson ON lms_user_lesson_progress (lesson_id);
|
||||||
|
|
||||||
|
CREATE TABLE lms_user_module_progress (
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
module_id BIGINT NOT NULL REFERENCES modules (id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, module_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_user_module_progress_user ON lms_user_module_progress (user_id);
|
||||||
|
CREATE INDEX idx_lms_user_module_progress_module ON lms_user_module_progress (module_id);
|
||||||
|
|
||||||
|
CREATE TABLE lms_user_course_progress (
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
course_id BIGINT NOT NULL REFERENCES courses (id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, course_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_user_course_progress_user ON lms_user_course_progress (user_id);
|
||||||
|
CREATE INDEX idx_lms_user_course_progress_course ON lms_user_course_progress (course_id);
|
||||||
|
|
||||||
|
CREATE TABLE lms_user_program_progress (
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
program_id BIGINT NOT NULL REFERENCES programs (id) ON DELETE CASCADE,
|
||||||
|
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (user_id, program_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_lms_user_program_progress_user ON lms_user_program_progress (user_id);
|
||||||
|
CREATE INDEX idx_lms_user_program_progress_program ON lms_user_program_progress (program_id);
|
||||||
|
|
@ -163,10 +163,10 @@ ORDER BY d.date;
|
||||||
|
|
||||||
-- name: AnalyticsCourseCounts :one
|
-- name: AnalyticsCourseCounts :one
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT COUNT(*)::bigint FROM course_categories) AS total_categories,
|
0::bigint AS total_categories,
|
||||||
(SELECT COUNT(*)::bigint FROM courses) AS total_courses,
|
0::bigint AS total_courses,
|
||||||
(SELECT COUNT(*)::bigint FROM sub_courses) AS total_sub_courses,
|
0::bigint AS total_sub_courses,
|
||||||
(SELECT COUNT(*)::bigint FROM sub_course_videos) AS total_videos;
|
0::bigint AS total_videos;
|
||||||
|
|
||||||
-- =====================
|
-- =====================
|
||||||
-- Content Analytics
|
-- Content Analytics
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
-- name: CreateCourseCategory :one
|
|
||||||
INSERT INTO course_categories (
|
|
||||||
name,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, COALESCE($2, true))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: GetCourseCategoryByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM course_categories
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: GetAllCourseCategories :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
is_active,
|
|
||||||
created_at
|
|
||||||
FROM course_categories
|
|
||||||
ORDER BY display_order ASC, created_at DESC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: UpdateCourseCategory :exec
|
|
||||||
UPDATE course_categories
|
|
||||||
SET
|
|
||||||
name = COALESCE($1, name),
|
|
||||||
is_active = COALESCE($2, is_active)
|
|
||||||
WHERE id = $3;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: DeleteCourseCategory :exec
|
|
||||||
DELETE FROM course_categories
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ReorderCourseCategories :exec
|
|
||||||
UPDATE course_categories
|
|
||||||
SET display_order = bulk.position
|
|
||||||
FROM (
|
|
||||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
|
||||||
) AS bulk
|
|
||||||
WHERE course_categories.id = bulk.id;
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
-- name: CreateCourse :one
|
|
||||||
INSERT INTO courses (
|
|
||||||
category_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, COALESCE($6, true))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: GetCourseByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM courses
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: GetCoursesByCategory :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
id,
|
|
||||||
category_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
is_active
|
|
||||||
FROM courses
|
|
||||||
WHERE category_id = $1
|
|
||||||
ORDER BY display_order ASC, id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
-- name: GetAllCourses :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
c.id,
|
|
||||||
c.category_id,
|
|
||||||
c.sub_category_id,
|
|
||||||
c.title,
|
|
||||||
c.description,
|
|
||||||
c.thumbnail,
|
|
||||||
c.intro_video_url,
|
|
||||||
c.is_active
|
|
||||||
FROM courses c
|
|
||||||
ORDER BY c.display_order ASC, c.id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
-- name: GetHumanLanguageCourses :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
c.id,
|
|
||||||
c.category_id,
|
|
||||||
c.sub_category_id,
|
|
||||||
c.title,
|
|
||||||
c.description,
|
|
||||||
c.thumbnail,
|
|
||||||
c.intro_video_url,
|
|
||||||
c.is_active
|
|
||||||
FROM courses c
|
|
||||||
JOIN course_categories cc ON cc.id = c.category_id
|
|
||||||
WHERE lower(trim(cc.name)) = 'human language'
|
|
||||||
ORDER BY c.display_order ASC, c.id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
-- name: GetCoursesBySubCategory :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
c.id,
|
|
||||||
c.category_id,
|
|
||||||
c.sub_category_id,
|
|
||||||
c.title,
|
|
||||||
c.description,
|
|
||||||
c.thumbnail,
|
|
||||||
c.intro_video_url,
|
|
||||||
c.is_active
|
|
||||||
FROM courses c
|
|
||||||
WHERE c.sub_category_id = $1
|
|
||||||
ORDER BY c.display_order ASC, c.id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: UpdateCourse :exec
|
|
||||||
UPDATE courses
|
|
||||||
SET
|
|
||||||
title = COALESCE($1, title),
|
|
||||||
description = COALESCE($2, description),
|
|
||||||
thumbnail = COALESCE($3, thumbnail),
|
|
||||||
intro_video_url = COALESCE($4, intro_video_url),
|
|
||||||
is_active = COALESCE($5, is_active)
|
|
||||||
WHERE id = $6;
|
|
||||||
|
|
||||||
|
|
||||||
-- name: DeleteCourse :exec
|
|
||||||
DELETE FROM courses
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ReorderCourses :exec
|
|
||||||
UPDATE courses
|
|
||||||
SET display_order = bulk.position
|
|
||||||
FROM (
|
|
||||||
SELECT unnest(@ids::BIGINT[]) AS id, unnest(@positions::INT[]) AS position
|
|
||||||
) AS bulk
|
|
||||||
WHERE courses.id = bulk.id;
|
|
||||||
|
|
@ -1,586 +0,0 @@
|
||||||
-- name: GetCoursesWithHierarchy :many
|
|
||||||
SELECT
|
|
||||||
cc.id AS category_id,
|
|
||||||
cc.name AS category_name,
|
|
||||||
csc.id AS sub_category_id,
|
|
||||||
csc.name AS sub_category_name,
|
|
||||||
c.id AS course_id,
|
|
||||||
c.title AS course_title
|
|
||||||
FROM course_categories cc
|
|
||||||
LEFT JOIN course_sub_categories csc ON csc.category_id = cc.id AND csc.is_active = TRUE
|
|
||||||
LEFT JOIN courses c ON c.sub_category_id = csc.id AND c.is_active = TRUE
|
|
||||||
WHERE cc.is_active = TRUE
|
|
||||||
ORDER BY cc.id, csc.display_order, csc.id, c.id;
|
|
||||||
|
|
||||||
-- name: GetLevelsByCourseID :many
|
|
||||||
SELECT *
|
|
||||||
FROM levels
|
|
||||||
WHERE course_id = $1
|
|
||||||
AND is_active = TRUE
|
|
||||||
ORDER BY display_order ASC, id ASC;
|
|
||||||
|
|
||||||
-- name: GetAllLevels :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
l.*
|
|
||||||
FROM levels l
|
|
||||||
ORDER BY l.display_order ASC, l.id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
-- name: GetLevelByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM levels
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: GetModulesByLevelID :many
|
|
||||||
SELECT *
|
|
||||||
FROM modules
|
|
||||||
WHERE level_id = $1
|
|
||||||
AND is_active = TRUE
|
|
||||||
ORDER BY display_order ASC, id ASC;
|
|
||||||
|
|
||||||
-- name: GetAllModules :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
m.*
|
|
||||||
FROM modules m
|
|
||||||
ORDER BY m.display_order ASC, m.id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
-- name: GetModuleByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM modules
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: GetSubModulesByModuleID :many
|
|
||||||
SELECT *
|
|
||||||
FROM sub_modules
|
|
||||||
WHERE module_id = $1
|
|
||||||
AND is_active = TRUE
|
|
||||||
ORDER BY display_order ASC, id ASC;
|
|
||||||
|
|
||||||
-- name: GetAllSubModules :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
sm.*
|
|
||||||
FROM sub_modules sm
|
|
||||||
ORDER BY sm.display_order ASC, sm.id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
-- name: GetSubModuleByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM sub_modules
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: GetSubModuleVideos :many
|
|
||||||
SELECT *
|
|
||||||
FROM sub_module_videos
|
|
||||||
WHERE sub_module_id = $1
|
|
||||||
AND status != 'ARCHIVED'
|
|
||||||
ORDER BY display_order ASC, id ASC;
|
|
||||||
|
|
||||||
-- name: GetSubModuleLessons :many
|
|
||||||
SELECT *
|
|
||||||
FROM sub_module_lessons
|
|
||||||
WHERE sub_module_id = $1
|
|
||||||
AND is_active = TRUE
|
|
||||||
ORDER BY display_order ASC, id ASC;
|
|
||||||
|
|
||||||
-- name: GetSubModuleLessonsAll :many
|
|
||||||
SELECT *
|
|
||||||
FROM sub_module_lessons
|
|
||||||
WHERE sub_module_id = $1
|
|
||||||
ORDER BY display_order ASC, id ASC;
|
|
||||||
|
|
||||||
-- name: GetSubModuleLessonByID :one
|
|
||||||
SELECT *
|
|
||||||
FROM sub_module_lessons
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: GetSubModulePractices :many
|
|
||||||
SELECT
|
|
||||||
smp.id,
|
|
||||||
smp.sub_module_id,
|
|
||||||
smp.title,
|
|
||||||
smp.description,
|
|
||||||
smp.thumbnail,
|
|
||||||
smp.intro_video_url,
|
|
||||||
smp.question_set_id,
|
|
||||||
smp.display_order,
|
|
||||||
smp.is_active,
|
|
||||||
smp.inactive_since,
|
|
||||||
qs.status,
|
|
||||||
qs.set_type,
|
|
||||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
|
||||||
FROM sub_module_practices smp
|
|
||||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
|
||||||
WHERE smp.sub_module_id = $1
|
|
||||||
AND smp.is_active = TRUE
|
|
||||||
ORDER BY smp.display_order ASC, smp.id ASC;
|
|
||||||
|
|
||||||
-- name: GetSubModulePracticeByID :one
|
|
||||||
SELECT
|
|
||||||
smp.id,
|
|
||||||
smp.sub_module_id,
|
|
||||||
smp.title,
|
|
||||||
smp.description,
|
|
||||||
smp.thumbnail,
|
|
||||||
smp.intro_video_url,
|
|
||||||
smp.question_set_id,
|
|
||||||
smp.display_order,
|
|
||||||
smp.is_active,
|
|
||||||
smp.inactive_since,
|
|
||||||
qs.status,
|
|
||||||
qs.set_type,
|
|
||||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
|
||||||
FROM sub_module_practices smp
|
|
||||||
JOIN question_sets qs ON qs.id = smp.question_set_id
|
|
||||||
WHERE smp.is_active = TRUE
|
|
||||||
AND (smp.id = $1 OR smp.question_set_id = $1)
|
|
||||||
ORDER BY (smp.id = $1) DESC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- name: GetSubModuleCapstones :many
|
|
||||||
SELECT
|
|
||||||
smc.id,
|
|
||||||
smc.sub_module_id,
|
|
||||||
smc.title,
|
|
||||||
smc.description,
|
|
||||||
smc.tips,
|
|
||||||
smc.thumbnail,
|
|
||||||
smc.question_set_id,
|
|
||||||
smc.display_order,
|
|
||||||
smc.is_active,
|
|
||||||
smc.inactive_since,
|
|
||||||
qs.status,
|
|
||||||
qs.set_type,
|
|
||||||
qs.time_limit_minutes,
|
|
||||||
qs.passing_score,
|
|
||||||
qs.shuffle_questions,
|
|
||||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
|
||||||
FROM sub_module_capstones smc
|
|
||||||
JOIN question_sets qs ON qs.id = smc.question_set_id
|
|
||||||
WHERE smc.sub_module_id = $1
|
|
||||||
AND smc.is_active = TRUE
|
|
||||||
AND qs.set_type = 'CAPSTONE'
|
|
||||||
ORDER BY smc.display_order ASC, smc.id ASC;
|
|
||||||
|
|
||||||
-- name: GetSubModuleCapstoneByID :one
|
|
||||||
SELECT
|
|
||||||
smc.id,
|
|
||||||
smc.sub_module_id,
|
|
||||||
smc.title,
|
|
||||||
smc.description,
|
|
||||||
smc.tips,
|
|
||||||
smc.thumbnail,
|
|
||||||
smc.question_set_id,
|
|
||||||
smc.display_order,
|
|
||||||
smc.is_active,
|
|
||||||
smc.inactive_since,
|
|
||||||
qs.status,
|
|
||||||
qs.set_type,
|
|
||||||
qs.time_limit_minutes,
|
|
||||||
qs.passing_score,
|
|
||||||
qs.shuffle_questions,
|
|
||||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
|
||||||
FROM sub_module_capstones smc
|
|
||||||
JOIN question_sets qs ON qs.id = smc.question_set_id
|
|
||||||
WHERE smc.id = $1
|
|
||||||
AND smc.is_active = TRUE
|
|
||||||
AND qs.set_type = 'CAPSTONE';
|
|
||||||
|
|
||||||
-- name: GetFullHierarchyByCourseID :many
|
|
||||||
SELECT
|
|
||||||
c.id AS course_id,
|
|
||||||
c.title AS course_title,
|
|
||||||
l.id AS level_id,
|
|
||||||
l.cefr_level,
|
|
||||||
l.title AS level_title,
|
|
||||||
l.description AS level_description,
|
|
||||||
l.thumbnail AS level_thumbnail,
|
|
||||||
m.id AS module_id,
|
|
||||||
m.title AS module_title,
|
|
||||||
m.icon_url AS module_icon_url,
|
|
||||||
sm.id AS sub_module_id,
|
|
||||||
sm.title AS sub_module_title,
|
|
||||||
sm.description AS sub_module_description,
|
|
||||||
sm.thumbnail AS sub_module_thumbnail,
|
|
||||||
sm.tips AS sub_module_tips,
|
|
||||||
sm.display_order AS sub_module_display_order
|
|
||||||
FROM courses c
|
|
||||||
LEFT JOIN levels l ON l.course_id = c.id AND l.is_active = TRUE
|
|
||||||
LEFT JOIN modules m ON m.level_id = l.id AND m.is_active = TRUE
|
|
||||||
LEFT JOIN sub_modules sm ON sm.module_id = m.id AND sm.is_active = TRUE
|
|
||||||
WHERE c.id = $1
|
|
||||||
ORDER BY l.display_order, l.id, m.display_order, m.id, sm.display_order, sm.id;
|
|
||||||
|
|
||||||
-- name: CreateCourseSubCategory :one
|
|
||||||
INSERT INTO course_sub_categories (
|
|
||||||
category_id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: GetCourseSubCategories :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
csc.id,
|
|
||||||
csc.category_id,
|
|
||||||
cc.name AS category_name,
|
|
||||||
csc.name,
|
|
||||||
csc.description,
|
|
||||||
csc.display_order,
|
|
||||||
csc.is_active,
|
|
||||||
csc.created_at
|
|
||||||
FROM course_sub_categories csc
|
|
||||||
JOIN course_categories cc ON cc.id = csc.category_id
|
|
||||||
WHERE csc.is_active = TRUE
|
|
||||||
ORDER BY csc.display_order ASC, csc.id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
-- name: GetCourseSubCategoriesByCategoryID :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
csc.id,
|
|
||||||
csc.category_id,
|
|
||||||
cc.name AS category_name,
|
|
||||||
csc.name,
|
|
||||||
csc.description,
|
|
||||||
csc.display_order,
|
|
||||||
csc.is_active,
|
|
||||||
csc.created_at
|
|
||||||
FROM course_sub_categories csc
|
|
||||||
JOIN course_categories cc ON cc.id = csc.category_id
|
|
||||||
WHERE csc.category_id = $1
|
|
||||||
AND csc.is_active = TRUE
|
|
||||||
ORDER BY csc.display_order ASC, csc.id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
-- name: GetHumanLanguageCourseSubCategories :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
csc.id,
|
|
||||||
csc.category_id,
|
|
||||||
cc.name AS category_name,
|
|
||||||
csc.name,
|
|
||||||
csc.description,
|
|
||||||
csc.display_order,
|
|
||||||
csc.is_active,
|
|
||||||
csc.created_at
|
|
||||||
FROM course_sub_categories csc
|
|
||||||
JOIN course_categories cc ON cc.id = csc.category_id
|
|
||||||
WHERE csc.is_active = TRUE
|
|
||||||
AND cc.is_active = TRUE
|
|
||||||
AND lower(trim(cc.name)) = 'human language'
|
|
||||||
ORDER BY csc.display_order ASC, csc.id ASC
|
|
||||||
LIMIT sqlc.narg('limit')::INT
|
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
|
||||||
|
|
||||||
-- name: CreateLevel :one
|
|
||||||
INSERT INTO levels (
|
|
||||||
course_id,
|
|
||||||
cefr_level,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, COALESCE($6, 0), COALESCE($7, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: UpdateLevel :one
|
|
||||||
UPDATE levels
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = $2,
|
|
||||||
thumbnail = $3,
|
|
||||||
display_order = $4,
|
|
||||||
is_active = $5
|
|
||||||
WHERE id = $6
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateModule :one
|
|
||||||
INSERT INTO modules (
|
|
||||||
level_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
icon_url,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, COALESCE($5, 0), COALESCE($6, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: UpdateModule :one
|
|
||||||
UPDATE modules
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = $2,
|
|
||||||
icon_url = $3,
|
|
||||||
display_order = $4,
|
|
||||||
is_active = $5
|
|
||||||
WHERE id = $6
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateSubModule :one
|
|
||||||
INSERT INTO sub_modules (
|
|
||||||
module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
tips,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, COALESCE($6, 0), COALESCE($7, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: UpdateSubModule :one
|
|
||||||
UPDATE sub_modules
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = $2,
|
|
||||||
thumbnail = $3,
|
|
||||||
tips = $4,
|
|
||||||
display_order = $5,
|
|
||||||
is_active = $6
|
|
||||||
WHERE id = $7
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateSubModuleVideo :one
|
|
||||||
INSERT INTO sub_module_videos (
|
|
||||||
sub_module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
video_url,
|
|
||||||
duration,
|
|
||||||
resolution,
|
|
||||||
is_published,
|
|
||||||
publish_date,
|
|
||||||
visibility,
|
|
||||||
instructor_id,
|
|
||||||
thumbnail,
|
|
||||||
display_order,
|
|
||||||
status,
|
|
||||||
vimeo_id,
|
|
||||||
vimeo_embed_url,
|
|
||||||
vimeo_player_html,
|
|
||||||
vimeo_status,
|
|
||||||
video_host_provider
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
$1, $2, $3, $4, $5, $6,
|
|
||||||
COALESCE($7, FALSE), $8, $9, $10, $11,
|
|
||||||
COALESCE($12, 0), COALESCE($13, 'DRAFT'),
|
|
||||||
$14, $15, $16, $17, COALESCE($18, 'DIRECT')
|
|
||||||
)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateSubModuleLesson :one
|
|
||||||
INSERT INTO sub_module_lessons (
|
|
||||||
sub_module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
teaching_text,
|
|
||||||
teaching_image_url,
|
|
||||||
teaching_audio_url,
|
|
||||||
teaching_video_url,
|
|
||||||
display_order,
|
|
||||||
is_active,
|
|
||||||
inactive_since
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
$6,
|
|
||||||
$7,
|
|
||||||
$8,
|
|
||||||
COALESCE($9, 0),
|
|
||||||
COALESCE($10, TRUE),
|
|
||||||
CASE WHEN COALESCE($10, TRUE) THEN NULL ELSE NOW() END
|
|
||||||
)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: UpdateSubModuleLesson :one
|
|
||||||
UPDATE sub_module_lessons
|
|
||||||
SET
|
|
||||||
sub_module_id = $1,
|
|
||||||
title = $2,
|
|
||||||
description = $3,
|
|
||||||
thumbnail = $4,
|
|
||||||
teaching_text = $5,
|
|
||||||
teaching_image_url = $6,
|
|
||||||
teaching_audio_url = $7,
|
|
||||||
teaching_video_url = $8,
|
|
||||||
display_order = $9,
|
|
||||||
is_active = $10,
|
|
||||||
inactive_since = CASE
|
|
||||||
WHEN $10 THEN NULL
|
|
||||||
WHEN is_active THEN NOW()
|
|
||||||
ELSE inactive_since
|
|
||||||
END
|
|
||||||
WHERE id = $11
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateSubModulePractice :one
|
|
||||||
INSERT INTO sub_module_practices (
|
|
||||||
sub_module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
question_set_id,
|
|
||||||
display_order,
|
|
||||||
is_active,
|
|
||||||
inactive_since
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 0), COALESCE($8, TRUE), CASE WHEN COALESCE($8, TRUE) THEN NULL ELSE NOW() END)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: CreateSubModuleCapstone :one
|
|
||||||
INSERT INTO sub_module_capstones (
|
|
||||||
sub_module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
tips,
|
|
||||||
thumbnail,
|
|
||||||
question_set_id,
|
|
||||||
display_order,
|
|
||||||
is_active,
|
|
||||||
inactive_since
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 0), COALESCE($8, TRUE), CASE WHEN COALESCE($8, TRUE) THEN NULL ELSE NOW() END)
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: UpdateSubModuleCapstone :one
|
|
||||||
UPDATE sub_module_capstones
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = $2,
|
|
||||||
tips = $3,
|
|
||||||
thumbnail = $4,
|
|
||||||
display_order = $5,
|
|
||||||
is_active = $6,
|
|
||||||
inactive_since = CASE
|
|
||||||
WHEN $6 THEN NULL
|
|
||||||
WHEN is_active THEN NOW()
|
|
||||||
ELSE inactive_since
|
|
||||||
END
|
|
||||||
WHERE id = $7
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: PurgeInactiveSubModuleLessonsBefore :execrows
|
|
||||||
DELETE FROM sub_module_lessons
|
|
||||||
WHERE is_active = FALSE
|
|
||||||
AND inactive_since IS NOT NULL
|
|
||||||
AND inactive_since < $1;
|
|
||||||
|
|
||||||
-- name: PurgeInactiveSubModulePracticesBefore :execrows
|
|
||||||
DELETE FROM question_sets qs
|
|
||||||
USING (
|
|
||||||
SELECT question_set_id
|
|
||||||
FROM sub_module_practices
|
|
||||||
WHERE is_active = FALSE
|
|
||||||
AND inactive_since IS NOT NULL
|
|
||||||
AND inactive_since < $1
|
|
||||||
) doomed
|
|
||||||
WHERE qs.id = doomed.question_set_id;
|
|
||||||
|
|
||||||
-- name: PurgeInactiveSubModuleCapstonesBefore :execrows
|
|
||||||
DELETE FROM question_sets qs
|
|
||||||
USING (
|
|
||||||
SELECT question_set_id
|
|
||||||
FROM sub_module_capstones
|
|
||||||
WHERE is_active = FALSE
|
|
||||||
AND inactive_since IS NOT NULL
|
|
||||||
AND inactive_since < $1
|
|
||||||
) doomed
|
|
||||||
WHERE qs.id = doomed.question_set_id;
|
|
||||||
|
|
||||||
-- name: GetModuleCapstones :many
|
|
||||||
SELECT
|
|
||||||
mc.id,
|
|
||||||
mc.module_id,
|
|
||||||
mc.title,
|
|
||||||
mc.description,
|
|
||||||
mc.tips,
|
|
||||||
mc.thumbnail,
|
|
||||||
mc.question_set_id,
|
|
||||||
mc.display_order,
|
|
||||||
mc.is_active,
|
|
||||||
qs.status,
|
|
||||||
qs.set_type,
|
|
||||||
qs.time_limit_minutes,
|
|
||||||
qs.passing_score,
|
|
||||||
qs.shuffle_questions,
|
|
||||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
|
||||||
FROM module_capstones mc
|
|
||||||
JOIN question_sets qs ON qs.id = mc.question_set_id
|
|
||||||
WHERE mc.module_id = $1
|
|
||||||
AND mc.is_active = TRUE
|
|
||||||
AND qs.set_type = 'CAPSTONE'
|
|
||||||
ORDER BY mc.display_order ASC, mc.id ASC;
|
|
||||||
|
|
||||||
-- name: GetModuleCapstoneByID :one
|
|
||||||
SELECT
|
|
||||||
mc.id,
|
|
||||||
mc.module_id,
|
|
||||||
mc.title,
|
|
||||||
mc.description,
|
|
||||||
mc.tips,
|
|
||||||
mc.thumbnail,
|
|
||||||
mc.question_set_id,
|
|
||||||
mc.display_order,
|
|
||||||
mc.is_active,
|
|
||||||
qs.status,
|
|
||||||
qs.set_type,
|
|
||||||
qs.time_limit_minutes,
|
|
||||||
qs.passing_score,
|
|
||||||
qs.shuffle_questions,
|
|
||||||
(SELECT COUNT(*) FROM question_set_items qsi WHERE qsi.set_id = qs.id) AS question_count
|
|
||||||
FROM module_capstones mc
|
|
||||||
JOIN question_sets qs ON qs.id = mc.question_set_id
|
|
||||||
WHERE mc.id = $1
|
|
||||||
AND mc.is_active = TRUE
|
|
||||||
AND qs.set_type = 'CAPSTONE';
|
|
||||||
|
|
||||||
-- name: CreateModuleCapstone :one
|
|
||||||
INSERT INTO module_capstones (
|
|
||||||
module_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
tips,
|
|
||||||
thumbnail,
|
|
||||||
question_set_id,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, 0), COALESCE($8, TRUE))
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
-- name: UpdateModuleCapstone :one
|
|
||||||
UPDATE module_capstones
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = $2,
|
|
||||||
tips = $3,
|
|
||||||
thumbnail = $4,
|
|
||||||
display_order = $5,
|
|
||||||
is_active = $6
|
|
||||||
WHERE id = $7
|
|
||||||
RETURNING *;
|
|
||||||
|
|
||||||
67
db/query/lms_courses.sql
Normal file
67
db/query/lms_courses.sql
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
-- name: CreateCourse :one
|
||||||
|
INSERT INTO courses (program_id, name, description, thumbnail, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(c.sort_order)
|
||||||
|
FROM courses c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: GetCourseByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM courses
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListCourseIDsByProgram :many
|
||||||
|
SELECT
|
||||||
|
c.id
|
||||||
|
FROM
|
||||||
|
courses AS c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
ORDER BY
|
||||||
|
c.id;
|
||||||
|
|
||||||
|
-- name: ListCoursesByProgramID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
c.id,
|
||||||
|
c.program_id,
|
||||||
|
c.name,
|
||||||
|
c.description,
|
||||||
|
c.thumbnail,
|
||||||
|
c.sort_order,
|
||||||
|
c.created_at,
|
||||||
|
c.updated_at
|
||||||
|
FROM
|
||||||
|
courses c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
ORDER BY
|
||||||
|
c.sort_order ASC,
|
||||||
|
c.id ASC
|
||||||
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
|
-- name: UpdateCourse :one
|
||||||
|
UPDATE 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: DeleteCourse :exec
|
||||||
|
DELETE FROM courses
|
||||||
|
WHERE id = $1;
|
||||||
61
db/query/lms_lessons.sql
Normal file
61
db/query/lms_lessons.sql
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
-- name: CreateLesson :one
|
||||||
|
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(l.sort_order)
|
||||||
|
FROM lessons l
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: GetLessonByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM lessons
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListLessonsByModuleID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
l.id,
|
||||||
|
l.module_id,
|
||||||
|
l.title,
|
||||||
|
l.video_url,
|
||||||
|
l.thumbnail,
|
||||||
|
l.description,
|
||||||
|
l.sort_order,
|
||||||
|
l.created_at,
|
||||||
|
l.updated_at
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1
|
||||||
|
ORDER BY
|
||||||
|
l.sort_order ASC,
|
||||||
|
l.id ASC
|
||||||
|
LIMIT $2
|
||||||
|
OFFSET $3;
|
||||||
|
|
||||||
|
-- name: UpdateLesson :one
|
||||||
|
UPDATE 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: DeleteLesson :exec
|
||||||
|
DELETE FROM lessons
|
||||||
|
WHERE id = $1;
|
||||||
71
db/query/lms_modules.sql
Normal file
71
db/query/lms_modules.sql
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
-- name: CreateModule :one
|
||||||
|
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(m.sort_order)
|
||||||
|
FROM modules m
|
||||||
|
WHERE
|
||||||
|
m.course_id = $2), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: GetModuleByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM modules
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListModuleIDsByCourse :many
|
||||||
|
SELECT
|
||||||
|
m.id
|
||||||
|
FROM
|
||||||
|
modules AS m
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
ORDER BY
|
||||||
|
m.id;
|
||||||
|
|
||||||
|
-- name: ListModulesByProgramAndCourse :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
m.id,
|
||||||
|
m.program_id,
|
||||||
|
m.course_id,
|
||||||
|
m.name,
|
||||||
|
m.description,
|
||||||
|
m.icon,
|
||||||
|
m.sort_order,
|
||||||
|
m.created_at,
|
||||||
|
m.updated_at
|
||||||
|
FROM
|
||||||
|
modules m
|
||||||
|
WHERE
|
||||||
|
m.program_id = $1
|
||||||
|
AND m.course_id = $2
|
||||||
|
ORDER BY
|
||||||
|
m.sort_order ASC,
|
||||||
|
m.id ASC
|
||||||
|
LIMIT $3
|
||||||
|
OFFSET $4;
|
||||||
|
|
||||||
|
-- name: UpdateModule :one
|
||||||
|
UPDATE modules
|
||||||
|
SET
|
||||||
|
name = COALESCE(sqlc.narg('name')::varchar, name),
|
||||||
|
description = COALESCE(sqlc.narg('description')::text, description),
|
||||||
|
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: DeleteModule :exec
|
||||||
|
DELETE FROM modules
|
||||||
|
WHERE id = $1;
|
||||||
88
db/query/lms_practices.sql
Normal file
88
db/query/lms_practices.sql
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
-- name: CreateLmsPractice :one
|
||||||
|
INSERT INTO lms_practices (
|
||||||
|
course_id, module_id, lesson_id,
|
||||||
|
title, story_description, story_image, persona_id, question_set_id, quick_tips
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetLmsPracticeByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM lms_practices
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListLmsPracticesByCourseID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.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 lms_practices p
|
||||||
|
WHERE p.course_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
|
-- name: ListLmsPracticesByModuleID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.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 lms_practices p
|
||||||
|
WHERE p.module_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
|
-- name: ListLmsPracticesByLessonID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.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 lms_practices p
|
||||||
|
WHERE p.lesson_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3;
|
||||||
|
|
||||||
|
-- name: UpdateLmsPractice :one
|
||||||
|
UPDATE lms_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: DeleteLmsPractice :exec
|
||||||
|
DELETE FROM lms_practices
|
||||||
|
WHERE id = $1;
|
||||||
248
db/query/lms_progress.sql
Normal file
248
db/query/lms_progress.sql
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
-- name: GetPreviousProgram :one
|
||||||
|
SELECT
|
||||||
|
p2.*
|
||||||
|
FROM
|
||||||
|
programs AS p1
|
||||||
|
INNER JOIN programs AS p2 ON p2.sort_order = p1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
p1.id = $1;
|
||||||
|
|
||||||
|
-- name: GetPreviousCourseInProgram :one
|
||||||
|
SELECT
|
||||||
|
c2.*
|
||||||
|
FROM
|
||||||
|
courses AS c1
|
||||||
|
INNER JOIN courses AS c2 ON c2.program_id = c1.program_id
|
||||||
|
AND c2.sort_order = c1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
c1.id = $1;
|
||||||
|
|
||||||
|
-- name: GetPreviousModuleInCourse :one
|
||||||
|
SELECT
|
||||||
|
m2.*
|
||||||
|
FROM
|
||||||
|
modules AS m1
|
||||||
|
INNER JOIN modules AS m2 ON m2.course_id = m1.course_id
|
||||||
|
AND m2.sort_order = m1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
m1.id = $1;
|
||||||
|
|
||||||
|
-- name: GetPreviousLessonInModule :one
|
||||||
|
SELECT
|
||||||
|
l2.*
|
||||||
|
FROM
|
||||||
|
lessons AS l1
|
||||||
|
INNER JOIN lessons AS l2 ON l2.module_id = l1.module_id
|
||||||
|
AND l2.sort_order = l1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
l1.id = $1;
|
||||||
|
|
||||||
|
-- name: UserHasProgramProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_program_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND program_id = $2) AS v;
|
||||||
|
|
||||||
|
-- name: UserHasCourseProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND course_id = $2) AS v;
|
||||||
|
|
||||||
|
-- name: UserHasModuleProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND module_id = $2) AS v;
|
||||||
|
|
||||||
|
-- name: UserHasLessonProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND lesson_id = $2) AS v;
|
||||||
|
|
||||||
|
-- name: InsertUserLessonProgress :exec
|
||||||
|
INSERT INTO lms_user_lesson_progress (user_id, lesson_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, lesson_id)
|
||||||
|
DO NOTHING;
|
||||||
|
|
||||||
|
-- name: InsertUserModuleProgress :exec
|
||||||
|
INSERT INTO lms_user_module_progress (user_id, module_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, module_id)
|
||||||
|
DO NOTHING;
|
||||||
|
|
||||||
|
-- name: InsertUserCourseProgress :exec
|
||||||
|
INSERT INTO lms_user_course_progress (user_id, course_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, course_id)
|
||||||
|
DO NOTHING;
|
||||||
|
|
||||||
|
-- name: InsertUserProgramProgress :exec
|
||||||
|
INSERT INTO lms_user_program_progress (user_id, program_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, program_id)
|
||||||
|
DO NOTHING;
|
||||||
|
|
||||||
|
-- name: CountLessonsInModule :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons
|
||||||
|
WHERE
|
||||||
|
module_id = $1;
|
||||||
|
|
||||||
|
-- name: CountUserCompletedLessonsInModule :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1
|
||||||
|
AND ulp.user_id = $2;
|
||||||
|
|
||||||
|
-- name: CountModulesInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
modules
|
||||||
|
WHERE
|
||||||
|
course_id = $1;
|
||||||
|
|
||||||
|
-- name: CountUserCompletedModulesInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress ump
|
||||||
|
INNER JOIN modules m ON m.id = ump.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
AND ump.user_id = $2;
|
||||||
|
|
||||||
|
-- name: CountCoursesInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
courses
|
||||||
|
WHERE
|
||||||
|
program_id = $1;
|
||||||
|
|
||||||
|
-- name: CountUserCompletedCoursesInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress ucp
|
||||||
|
INNER JOIN courses c ON c.id = ucp.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
AND ucp.user_id = $2;
|
||||||
|
|
||||||
|
-- name: ListLMSCompletedLessonIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ulp.lesson_id
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress AS ulp
|
||||||
|
WHERE
|
||||||
|
ulp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ulp.completed_at ASC,
|
||||||
|
ulp.lesson_id ASC;
|
||||||
|
|
||||||
|
-- name: ListLMSCompletedModuleIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ump.module_id
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress AS ump
|
||||||
|
WHERE
|
||||||
|
ump.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ump.completed_at ASC,
|
||||||
|
ump.module_id ASC;
|
||||||
|
|
||||||
|
-- name: ListLMSCompletedCourseIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ucp.course_id
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress AS ucp
|
||||||
|
WHERE
|
||||||
|
ucp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ucp.completed_at ASC,
|
||||||
|
ucp.course_id ASC;
|
||||||
|
|
||||||
|
-- name: ListLMSCompletedProgramIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
upp.program_id
|
||||||
|
FROM
|
||||||
|
lms_user_program_progress AS upp
|
||||||
|
WHERE
|
||||||
|
upp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
upp.completed_at ASC,
|
||||||
|
upp.program_id ASC;
|
||||||
|
|
||||||
|
-- Lesson-based progress within a course (all modules).
|
||||||
|
-- name: CountLessonsInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1;
|
||||||
|
|
||||||
|
-- name: CountUserCompletedLessonsInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
AND ulp.user_id = $2;
|
||||||
|
|
||||||
|
-- Lesson-based progress within a program (all courses).
|
||||||
|
-- name: CountLessonsInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1;
|
||||||
|
|
||||||
|
-- name: CountUserCompletedLessonsInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
AND ulp.user_id = $2;
|
||||||
|
|
@ -29,27 +29,16 @@ LIMIT 1;
|
||||||
-- name: MarkPracticeCompleted :execrows
|
-- name: MarkPracticeCompleted :execrows
|
||||||
INSERT INTO user_practice_progress (
|
INSERT INTO user_practice_progress (
|
||||||
user_id,
|
user_id,
|
||||||
sub_course_id,
|
|
||||||
question_set_id,
|
question_set_id,
|
||||||
completed_at,
|
completed_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
SELECT
|
VALUES (
|
||||||
@user_id::BIGINT,
|
@user_id::BIGINT,
|
||||||
CASE
|
@question_set_id::BIGINT,
|
||||||
WHEN qs.owner_type = 'SUB_COURSE' THEN qs.owner_id
|
|
||||||
WHEN qs.owner_type = 'SUB_MODULE' THEN sm.legacy_sub_course_id
|
|
||||||
ELSE NULL
|
|
||||||
END,
|
|
||||||
qs.id,
|
|
||||||
CURRENT_TIMESTAMP,
|
CURRENT_TIMESTAMP,
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
FROM question_sets qs
|
)
|
||||||
LEFT JOIN sub_modules sm
|
|
||||||
ON qs.owner_type = 'SUB_MODULE'
|
|
||||||
AND qs.owner_id = sm.id
|
|
||||||
WHERE qs.id = @question_set_id::BIGINT
|
|
||||||
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
||||||
SET completed_at = EXCLUDED.completed_at,
|
SET completed_at = EXCLUDED.completed_at,
|
||||||
updated_at = EXCLUDED.updated_at;
|
updated_at = EXCLUDED.updated_at;
|
||||||
|
|
||||||
|
|
|
||||||
56
db/query/programs.sql
Normal file
56
db/query/programs.sql
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
-- name: CreateProgram :one
|
||||||
|
INSERT INTO programs (name, description, thumbnail, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(p.sort_order)
|
||||||
|
FROM programs AS p), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: GetProgramByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM programs
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListAllProgramIDs :many
|
||||||
|
SELECT
|
||||||
|
p.id
|
||||||
|
FROM
|
||||||
|
programs AS p
|
||||||
|
ORDER BY
|
||||||
|
p.id;
|
||||||
|
|
||||||
|
-- name: ListPrograms :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.thumbnail,
|
||||||
|
p.sort_order,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM programs p
|
||||||
|
ORDER BY p.sort_order ASC, p.id ASC
|
||||||
|
LIMIT $1 OFFSET $2;
|
||||||
|
|
||||||
|
-- name: UpdateProgram :one
|
||||||
|
UPDATE programs
|
||||||
|
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: DeleteProgram :exec
|
||||||
|
DELETE FROM programs
|
||||||
|
WHERE id = $1;
|
||||||
|
|
@ -11,10 +11,9 @@ INSERT INTO question_sets (
|
||||||
passing_score,
|
passing_score,
|
||||||
shuffle_questions,
|
shuffle_questions,
|
||||||
status,
|
status,
|
||||||
sub_course_video_id,
|
|
||||||
intro_video_url
|
intro_video_url
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12, $13)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetQuestionSetByID :one
|
-- name: GetQuestionSetByID :one
|
||||||
|
|
@ -61,9 +60,8 @@ SET
|
||||||
shuffle_questions = COALESCE($7, shuffle_questions),
|
shuffle_questions = COALESCE($7, shuffle_questions),
|
||||||
status = COALESCE($8, status),
|
status = COALESCE($8, status),
|
||||||
intro_video_url = COALESCE($9, intro_video_url),
|
intro_video_url = COALESCE($9, intro_video_url),
|
||||||
sub_course_video_id = COALESCE($10, sub_course_video_id),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $11;
|
WHERE id = $10;
|
||||||
|
|
||||||
-- name: ArchiveQuestionSet :exec
|
-- name: ArchiveQuestionSet :exec
|
||||||
UPDATE question_sets
|
UPDATE question_sets
|
||||||
|
|
@ -82,16 +80,6 @@ WHERE set_type = 'INITIAL_ASSESSMENT'
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
-- name: GetSubCourseInitialAssessmentSet :one
|
|
||||||
SELECT *
|
|
||||||
FROM question_sets
|
|
||||||
WHERE set_type = 'INITIAL_ASSESSMENT'
|
|
||||||
AND owner_type = 'SUB_COURSE'
|
|
||||||
AND owner_id = $1
|
|
||||||
AND status = 'PUBLISHED'
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 1;
|
|
||||||
|
|
||||||
-- name: AddUserPersonaToQuestionSet :one
|
-- name: AddUserPersonaToQuestionSet :one
|
||||||
INSERT INTO question_set_personas (
|
INSERT INTO question_set_personas (
|
||||||
question_set_id,
|
question_set_id,
|
||||||
|
|
@ -120,13 +108,6 @@ INNER JOIN question_set_personas qsp ON qsp.user_id = u.id
|
||||||
WHERE qsp.question_set_id = $1
|
WHERE qsp.question_set_id = $1
|
||||||
ORDER BY qsp.display_order ASC;
|
ORDER BY qsp.display_order ASC;
|
||||||
|
|
||||||
-- name: UpdateQuestionSetVideoLink :exec
|
|
||||||
UPDATE question_sets
|
|
||||||
SET
|
|
||||||
sub_course_video_id = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2;
|
|
||||||
|
|
||||||
-- name: ReorderQuestionSets :exec
|
-- name: ReorderQuestionSets :exec
|
||||||
UPDATE question_sets
|
UPDATE question_sets
|
||||||
SET display_order = bulk.position
|
SET display_order = bulk.position
|
||||||
|
|
|
||||||
|
|
@ -1,825 +0,0 @@
|
||||||
{
|
|
||||||
"info": {
|
|
||||||
"_postman_id": "d8d17a29-5f9c-4f06-95fd-12e9862f97f8",
|
|
||||||
"name": "Yimaru Backend - Course Management APIs",
|
|
||||||
"description": "Fully documented Postman collection for all course-management related endpoints in Yimaru Backend.\n\nAuthentication:\n- All endpoints require `Authorization: Bearer {{accessToken}}`.\n\nBase URL:\n- `{{baseUrl}}/api/{{apiVersion}}`\n\nNotes:\n- IDs in path params must be positive integers.\n- Some update endpoints support partial updates.\n- Endpoint-level permission requirements are documented in each request description.",
|
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
||||||
},
|
|
||||||
"variable": [
|
|
||||||
{
|
|
||||||
"key": "baseUrl",
|
|
||||||
"value": "https://api.yimaruacademy.com",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "apiVersion",
|
|
||||||
"value": "v1",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "accessToken",
|
|
||||||
"value": "",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "categoryId",
|
|
||||||
"value": "1",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "subCategoryId",
|
|
||||||
"value": "1",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "courseId",
|
|
||||||
"value": "1",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "levelId",
|
|
||||||
"value": "1",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "moduleId",
|
|
||||||
"value": "1",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "subModuleId",
|
|
||||||
"value": "1",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "videoId",
|
|
||||||
"value": "1",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "questionSetId",
|
|
||||||
"value": "1",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "practiceId",
|
|
||||||
"value": "1",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"auth": {
|
|
||||||
"type": "bearer",
|
|
||||||
"bearer": [
|
|
||||||
{
|
|
||||||
"key": "token",
|
|
||||||
"value": "{{accessToken}}",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Categories & Courses",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "List Course Categories",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/categories",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"categories"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Returns all course categories.\n\nPermission: `learning_tree.get`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create Course Category",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"name\": \"Grammar\",\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/categories",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"categories"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Creates a course category.\n\nRequired fields:\n- `name` (string)\n\nOptional fields:\n- `is_active` (boolean)\n\nPermission: `course_categories.create`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete Course Category",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/categories/{{categoryId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"categories",
|
|
||||||
"{{categoryId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Deletes a category by ID.\n\nPath params:\n- `categoryId` (int, required)\n\nPermission: `course_categories.delete`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List Courses By Category",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/categories/{{categoryId}}/courses?offset=0&limit=50",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"categories",
|
|
||||||
"{{categoryId}}",
|
|
||||||
"courses"
|
|
||||||
],
|
|
||||||
"query": [
|
|
||||||
{
|
|
||||||
"key": "offset",
|
|
||||||
"value": "0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "limit",
|
|
||||||
"value": "50"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Returns all courses under a category.\n\nPath params:\n- `categoryId` (int, required)\n\nQuery params:\n- `offset` (int, optional, default 0)\n- `limit` (int, optional, default 10000)\n\nPermission: `learning_tree.get`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create Course",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"category_id\": 1,\n \"sub_category_id\": 1,\n \"title\": \"English Basics\",\n \"description\": \"Beginner-level course\",\n \"thumbnail\": \"https://cdn.example.com/course-thumb.jpg\",\n \"intro_video_url\": \"https://cdn.example.com/intro.mp4\",\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"courses"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Creates a course.\n\nRequired fields:\n- `category_id` (int)\n- `title` (string)\n\nOptional fields:\n- `sub_category_id` (int)\n- `description` (string)\n- `thumbnail` (string URL)\n- `intro_video_url` (string URL)\n- `is_active` (boolean)\n\nPermission: `courses.create`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update Course",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"English Basics - Updated\",\n \"description\": \"Updated course description\",\n \"thumbnail\": \"https://cdn.example.com/new-thumb.jpg\",\n \"intro_video_url\": \"https://cdn.example.com/new-intro.mp4\",\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses/{{courseId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"courses",
|
|
||||||
"{{courseId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Updates an existing course (partial update accepted).\n\nPath params:\n- `courseId` (int, required)\n\nOptional fields:\n- `title`, `description`, `thumbnail`, `intro_video_url`, `is_active`\n\nPermission: `courses.update`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete Course",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses/{{courseId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"courses",
|
|
||||||
"{{courseId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Deletes a course.\n\nPath params:\n- `courseId` (int, required)\n\nPermission: `courses.delete`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update Course Thumbnail",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"thumbnail_url\": \"https://cdn.example.com/new-thumbnail.jpg\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses/{{courseId}}/thumbnail",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"courses",
|
|
||||||
"{{courseId}}",
|
|
||||||
"thumbnail"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Updates only the thumbnail of a course.\n\nPath params:\n- `courseId` (int, required)\n\nRequired fields:\n- `thumbnail_url` (string)\n\nPermission: `courses.upload_thumbnail`."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Hierarchy & Learning Path",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Get Unified Hierarchy",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/hierarchy",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"hierarchy"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Returns global hierarchy data.\n\nPermission: `learning_tree.get`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get Human Language Hierarchy",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/human-language/hierarchy",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"human-language",
|
|
||||||
"hierarchy"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Alias endpoint for unified hierarchy under human-language path.\n\nPermission: `learning_tree.get`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get Course Hierarchy",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses/{{courseId}}/hierarchy",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"courses",
|
|
||||||
"{{courseId}}",
|
|
||||||
"hierarchy"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Returns hierarchy nodes for one course.\n\nPath params:\n- `courseId` (int, required)\n\nPermission: `learning_tree.get`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Get Course Learning Path",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/courses/{{courseId}}/learning-path",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"courses",
|
|
||||||
"{{courseId}}",
|
|
||||||
"learning-path"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Returns learning-path projection for a course including sub-modules, videos, and practices.\n\nPath params:\n- `courseId` (int, required)\n\nPermission: `learning_tree.get`."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Sub-Categories, Levels, Modules, Sub-Modules",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Create Course Sub-Category",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"category_id\": 1,\n \"name\": \"Everyday Conversation\",\n \"description\": \"Spoken communication track\",\n \"display_order\": 1,\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-categories",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-categories"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Creates a sub-category under a course category.\n\nRequired fields:\n- `category_id` (int)\n- `name` (string)\n\nOptional fields:\n- `description` (string)\n- `display_order` (int)\n- `is_active` (boolean)\n\nPermission: `course_categories.create`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete Course Sub-Category",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-categories/{{subCategoryId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-categories",
|
|
||||||
"{{subCategoryId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Deletes a sub-category.\n\nPath params:\n- `subCategoryId` (int, required)\n\nPermission: `course_categories.delete`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create Level",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"course_id\": 1,\n \"cefr_level\": \"A1\",\n \"display_order\": 1,\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/levels",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"levels"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Creates a CEFR level under a course.\n\nRequired fields:\n- `course_id` (int)\n- `cefr_level` (one of: A1, A2, A3, B1, B2, B3, C1, C2, C3)\n\nOptional fields:\n- `display_order` (int)\n- `is_active` (boolean)\n\nPermission: `subcourses.create`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create Module",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"level_id\": 1,\n \"title\": \"Module 1: Introductions\",\n \"description\": \"Core introduction module\",\n \"display_order\": 1,\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/modules",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"modules"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Creates a module under a level.\n\nRequired fields:\n- `level_id` (int)\n- `title` (string)\n\nOptional fields:\n- `description`, `display_order`, `is_active`\n\nPermission: `subcourses.create`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete Module",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/modules/{{moduleId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"modules",
|
|
||||||
"{{moduleId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Deletes a module.\n\nPath params:\n- `moduleId` (int, required)\n\nPermission: `subcourses.delete`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create Sub-Module",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"module_id\": 1,\n \"title\": \"Sub-Module 1: Greetings\",\n \"description\": \"Greetings and polite expressions\",\n \"display_order\": 1,\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-modules",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-modules"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Creates a sub-module under a module.\n\nRequired fields:\n- `module_id` (int)\n- `title` (string)\n\nOptional fields:\n- `description`, `display_order`, `is_active`\n\nPermission: `subcourses.create`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update Sub-Module",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Sub-Module 1: Greetings (Updated)\",\n \"description\": \"Updated sub-module description\",\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-modules/{{subModuleId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-modules",
|
|
||||||
"{{subModuleId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Updates a sub-module.\n\nPath params:\n- `subModuleId` (int, required)\n\nOptional fields:\n- `title`, `description`, `is_active`\n\nPermission: `subcourses.update`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete Sub-Module",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-modules/{{subModuleId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-modules",
|
|
||||||
"{{subModuleId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Deletes a sub-module.\n\nPath params:\n- `subModuleId` (int, required)\n\nPermission: `subcourses.delete`."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Sub-Module Videos",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Get Sub-Module Videos",
|
|
||||||
"request": {
|
|
||||||
"method": "GET",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-modules/{{subModuleId}}/videos",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-modules",
|
|
||||||
"{{subModuleId}}",
|
|
||||||
"videos"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Lists videos for a given sub-module.\n\nPath params:\n- `subModuleId` (int, required)\n\nPermission: `videos.list`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create Sub-Module Video",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"sub_module_id\": 1,\n \"title\": \"Greeting Expressions\",\n \"description\": \"Main lesson video\",\n \"video_url\": \"https://cdn.example.com/videos/greetings.mp4\",\n \"duration\": 480,\n \"resolution\": \"1080p\",\n \"visibility\": \"public\",\n \"instructor_id\": \"instructor-123\",\n \"thumbnail\": \"https://cdn.example.com/thumbs/greetings.jpg\",\n \"display_order\": 1,\n \"status\": \"published\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-module-videos",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-module-videos"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Creates a video under a sub-module.\n\nRequired fields:\n- `sub_module_id` (int)\n- `title` (string)\n- `video_url` (string URL)\n\nOptional fields:\n- `description`, `duration`, `resolution`, `visibility`, `instructor_id`, `thumbnail`, `display_order`, `status`\n\nPermission: `videos.create`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update Sub-Module Video",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Greeting Expressions - Updated\",\n \"description\": \"Updated video description\",\n \"video_url\": \"https://cdn.example.com/videos/greetings-v2.mp4\"\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-module-videos/{{videoId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-module-videos",
|
|
||||||
"{{videoId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Updates a sub-module video.\n\nPath params:\n- `videoId` (int, required)\n\nRequired fields:\n- `title` (string)\n- `video_url` (string URL)\n\nOptional fields:\n- `description`\n\nPermission: `videos.update`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete Sub-Module Video",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-module-videos/{{videoId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-module-videos",
|
|
||||||
"{{videoId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Deletes a sub-module video.\n\nPath params:\n- `videoId` (int, required)\n\nPermission: `videos.delete`."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Lessons & Practices",
|
|
||||||
"item": [
|
|
||||||
{
|
|
||||||
"name": "Attach Sub-Module Lesson (Question Set)",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"sub_module_id\": 1,\n \"question_set_id\": 1,\n \"intro_video_url\": \"https://cdn.example.com/intro-lesson.mp4\",\n \"display_order\": 1,\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-module-lessons",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-module-lessons"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Links a question set lesson to a sub-module.\n\nRequired fields:\n- `sub_module_id` (int)\n- `question_set_id` (int)\n\nOptional fields:\n- `intro_video_url`, `display_order`, `is_active`\n\nPermission: `question_sets.update`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Create Sub-Module Practice",
|
|
||||||
"request": {
|
|
||||||
"method": "POST",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"sub_module_id\": 1,\n \"title\": \"Practice: Basic Greetings\",\n \"description\": \"Practice set for greetings\",\n \"thumbnail\": \"https://cdn.example.com/practice-thumb.jpg\",\n \"intro_video_url\": \"https://cdn.example.com/practice-intro.mp4\",\n \"question_set_id\": 1,\n \"display_order\": 1,\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/sub-module-practices",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"sub-module-practices"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Creates a practice under a sub-module.\n\nRequired fields:\n- `sub_module_id` (int)\n- `title` (string)\n- `question_set_id` (int)\n\nOptional fields:\n- `description`, `thumbnail`, `intro_video_url`, `display_order`, `is_active`\n\nPermission: `question_sets.update`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Update Practice",
|
|
||||||
"request": {
|
|
||||||
"method": "PUT",
|
|
||||||
"header": [
|
|
||||||
{
|
|
||||||
"key": "Content-Type",
|
|
||||||
"value": "application/json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"body": {
|
|
||||||
"mode": "raw",
|
|
||||||
"raw": "{\n \"title\": \"Practice: Basic Greetings - Updated\",\n \"description\": \"Updated practice details\",\n \"persona\": \"student\",\n \"is_active\": true\n}"
|
|
||||||
},
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/practices/{{practiceId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"practices",
|
|
||||||
"{{practiceId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Updates a practice.\n\nPath params:\n- `practiceId` (int, required)\n\nBehavior:\n- If `is_active` is provided, this endpoint updates practice status.\n- Otherwise, `title` is required and metadata update is applied.\n\nPermission: `question_sets.update`."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Delete Practice",
|
|
||||||
"request": {
|
|
||||||
"method": "DELETE",
|
|
||||||
"header": [],
|
|
||||||
"url": {
|
|
||||||
"raw": "{{baseUrl}}/api/{{apiVersion}}/course-management/practices/{{practiceId}}",
|
|
||||||
"host": [
|
|
||||||
"{{baseUrl}}"
|
|
||||||
],
|
|
||||||
"path": [
|
|
||||||
"api",
|
|
||||||
"{{apiVersion}}",
|
|
||||||
"course-management",
|
|
||||||
"practices",
|
|
||||||
"{{practiceId}}"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"description": "Deletes a practice.\n\nPath params:\n- `practiceId` (int, required)\n\nPermission: `question_sets.delete`."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
3972
docs/docs.go
3972
docs/docs.go
File diff suppressed because it is too large
Load Diff
3972
docs/swagger.json
3972
docs/swagger.json
File diff suppressed because it is too large
Load Diff
2634
docs/swagger.yaml
2634
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -12,10 +12,10 @@ import (
|
||||||
const AnalyticsCourseCounts = `-- name: AnalyticsCourseCounts :one
|
const AnalyticsCourseCounts = `-- name: AnalyticsCourseCounts :one
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
(SELECT COUNT(*)::bigint FROM course_categories) AS total_categories,
|
0::bigint AS total_categories,
|
||||||
(SELECT COUNT(*)::bigint FROM courses) AS total_courses,
|
0::bigint AS total_courses,
|
||||||
(SELECT COUNT(*)::bigint FROM sub_courses) AS total_sub_courses,
|
0::bigint AS total_sub_courses,
|
||||||
(SELECT COUNT(*)::bigint FROM sub_course_videos) AS total_videos
|
0::bigint AS total_videos
|
||||||
`
|
`
|
||||||
|
|
||||||
type AnalyticsCourseCountsRow struct {
|
type AnalyticsCourseCountsRow struct {
|
||||||
|
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (q *Queries) GetSubModuleByIDCompat(ctx context.Context, id int64) (SubModule, error) {
|
|
||||||
row := q.db.QueryRow(ctx, `
|
|
||||||
SELECT id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id, thumbnail, tips
|
|
||||||
FROM sub_modules
|
|
||||||
WHERE id = $1
|
|
||||||
`, id)
|
|
||||||
var i SubModule
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.ModuleID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.LegacySubCourseID,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.Tips,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateSubModuleCompat(ctx context.Context, id int64, title string, description string, thumbnail string, tips string, displayOrder int32, isActive bool) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_modules
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, ''),
|
|
||||||
thumbnail = NULLIF($3, ''),
|
|
||||||
tips = NULLIF($4, ''),
|
|
||||||
display_order = $5,
|
|
||||||
is_active = $6
|
|
||||||
WHERE id = $7
|
|
||||||
`, title, description, thumbnail, tips, displayOrder, isActive, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeleteSubModuleCompat(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM sub_modules WHERE id = $1`, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeleteModuleCompat(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM modules WHERE id = $1`, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateSubModuleVideoCompat(ctx context.Context, id int64, title string, description string, videoURL string) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_module_videos
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, ''),
|
|
||||||
video_url = $3
|
|
||||||
WHERE id = $4
|
|
||||||
`, title, description, videoURL, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeleteSubModuleVideoCompat(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM sub_module_videos WHERE id = $1`, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdatePracticeCompat(ctx context.Context, id int64, title string, description string, persona string) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE question_sets
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, ''),
|
|
||||||
persona = NULLIF($3, ''),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $4
|
|
||||||
`, title, description, persona, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_module_practices
|
|
||||||
SET
|
|
||||||
title = $1,
|
|
||||||
description = NULLIF($2, '')
|
|
||||||
WHERE question_set_id = $3
|
|
||||||
`, title, description, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdatePracticeStatusCompat(ctx context.Context, id int64, isActive bool) error {
|
|
||||||
status := "ARCHIVED"
|
|
||||||
if isActive {
|
|
||||||
status = "PUBLISHED"
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
UPDATE question_sets
|
|
||||||
SET
|
|
||||||
status = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2
|
|
||||||
`, status, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = q.db.Exec(ctx, `
|
|
||||||
UPDATE sub_module_practices
|
|
||||||
SET
|
|
||||||
is_active = $1,
|
|
||||||
inactive_since = CASE
|
|
||||||
WHEN $1 THEN NULL
|
|
||||||
WHEN is_active THEN NOW()
|
|
||||||
ELSE inactive_since
|
|
||||||
END
|
|
||||||
WHERE question_set_id = $2
|
|
||||||
`, isActive, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeletePracticeCompat(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM question_sets WHERE id = $1`, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteCapstoneCompat removes the backing question set (and cascades sub_module_capstones).
|
|
||||||
func (q *Queries) DeleteCapstoneCompat(ctx context.Context, capstoneID int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
DELETE FROM question_sets
|
|
||||||
WHERE id = (SELECT question_set_id FROM sub_module_capstones WHERE id = $1)
|
|
||||||
`, capstoneID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteModuleCapstoneCompat removes the backing question set (and cascades module_capstones).
|
|
||||||
func (q *Queries) DeleteModuleCapstoneCompat(ctx context.Context, capstoneID int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `
|
|
||||||
DELETE FROM question_sets
|
|
||||||
WHERE id = (SELECT question_set_id FROM module_capstones WHERE id = $1)
|
|
||||||
`, capstoneID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateCourseCompat(
|
|
||||||
ctx context.Context,
|
|
||||||
categoryID int64,
|
|
||||||
subCategoryID *int64,
|
|
||||||
title string,
|
|
||||||
description string,
|
|
||||||
thumbnail string,
|
|
||||||
introVideoURL string,
|
|
||||||
isActive bool,
|
|
||||||
) (Course, error) {
|
|
||||||
row := q.db.QueryRow(ctx, `
|
|
||||||
INSERT INTO courses (
|
|
||||||
category_id,
|
|
||||||
sub_category_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
NULLIF($4, ''),
|
|
||||||
NULLIF($5, ''),
|
|
||||||
NULLIF($6, ''),
|
|
||||||
$7
|
|
||||||
)
|
|
||||||
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
|
||||||
`, categoryID, subCategoryID, title, description, thumbnail, introVideoURL, isActive)
|
|
||||||
|
|
||||||
var i Course
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.SubCategoryID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return Course{}, err
|
|
||||||
}
|
|
||||||
if !i.SubCategoryID.Valid {
|
|
||||||
i.SubCategoryID = pgtype.Int8{Valid: false}
|
|
||||||
}
|
|
||||||
return i, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeleteCourseSubCategoryCompat(ctx context.Context, subCategoryID int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM courses WHERE sub_category_id = $1`, subCategoryID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = q.db.Exec(ctx, `DELETE FROM course_sub_categories WHERE id = $1`, subCategoryID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) DeleteCourseCategoryCompat(ctx context.Context, categoryID int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, `DELETE FROM courses WHERE category_id = $1`, categoryID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = q.db.Exec(ctx, `DELETE FROM course_sub_categories WHERE category_id = $1`, categoryID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = q.db.Exec(ctx, `DELETE FROM course_categories WHERE id = $1`, categoryID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.30.0
|
|
||||||
// source: course_catagories.sql
|
|
||||||
|
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
const CreateCourseCategory = `-- name: CreateCourseCategory :one
|
|
||||||
INSERT INTO course_categories (
|
|
||||||
name,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, COALESCE($2, true))
|
|
||||||
RETURNING id, name, is_active, created_at, display_order
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateCourseCategoryParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Column2 interface{} `json:"column_2"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateCourseCategory(ctx context.Context, arg CreateCourseCategoryParams) (CourseCategory, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CreateCourseCategory, arg.Name, arg.Column2)
|
|
||||||
var i CourseCategory
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeleteCourseCategory = `-- name: DeleteCourseCategory :exec
|
|
||||||
DELETE FROM course_categories
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteCourseCategory(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, DeleteCourseCategory, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetAllCourseCategories = `-- name: GetAllCourseCategories :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
is_active,
|
|
||||||
created_at
|
|
||||||
FROM course_categories
|
|
||||||
ORDER BY display_order ASC, created_at DESC
|
|
||||||
LIMIT $2::INT
|
|
||||||
OFFSET $1::INT
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetAllCourseCategoriesParams struct {
|
|
||||||
Offset pgtype.Int4 `json:"offset"`
|
|
||||||
Limit pgtype.Int4 `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetAllCourseCategoriesRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetAllCourseCategories(ctx context.Context, arg GetAllCourseCategoriesParams) ([]GetAllCourseCategoriesRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetAllCourseCategories, arg.Offset, arg.Limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetAllCourseCategoriesRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetAllCourseCategoriesRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetCourseCategoryByID = `-- name: GetCourseCategoryByID :one
|
|
||||||
SELECT id, name, is_active, created_at, display_order
|
|
||||||
FROM course_categories
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetCourseCategoryByID(ctx context.Context, id int64) (CourseCategory, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetCourseCategoryByID, id)
|
|
||||||
var i CourseCategory
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Name,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReorderCourseCategories = `-- name: ReorderCourseCategories :exec
|
|
||||||
UPDATE course_categories
|
|
||||||
SET display_order = bulk.position
|
|
||||||
FROM (
|
|
||||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
|
||||||
) AS bulk
|
|
||||||
WHERE course_categories.id = bulk.id
|
|
||||||
`
|
|
||||||
|
|
||||||
type ReorderCourseCategoriesParams struct {
|
|
||||||
Ids []int64 `json:"ids"`
|
|
||||||
Positions []int32 `json:"positions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ReorderCourseCategories(ctx context.Context, arg ReorderCourseCategoriesParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, ReorderCourseCategories, arg.Ids, arg.Positions)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const UpdateCourseCategory = `-- name: UpdateCourseCategory :exec
|
|
||||||
UPDATE course_categories
|
|
||||||
SET
|
|
||||||
name = COALESCE($1, name),
|
|
||||||
is_active = COALESCE($2, is_active)
|
|
||||||
WHERE id = $3
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateCourseCategoryParams struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateCourseCategory(ctx context.Context, arg UpdateCourseCategoryParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, UpdateCourseCategory, arg.Name, arg.IsActive, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
@ -1,401 +0,0 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// sqlc v1.30.0
|
|
||||||
// source: courses.sql
|
|
||||||
|
|
||||||
package dbgen
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
const CreateCourse = `-- name: CreateCourse :one
|
|
||||||
INSERT INTO courses (
|
|
||||||
category_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
is_active
|
|
||||||
)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, COALESCE($6, true))
|
|
||||||
RETURNING id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
|
||||||
`
|
|
||||||
|
|
||||||
type CreateCourseParams struct {
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
Column6 interface{} `json:"column_6"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) {
|
|
||||||
row := q.db.QueryRow(ctx, CreateCourse,
|
|
||||||
arg.CategoryID,
|
|
||||||
arg.Title,
|
|
||||||
arg.Description,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.IntroVideoUrl,
|
|
||||||
arg.Column6,
|
|
||||||
)
|
|
||||||
var i Course
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.SubCategoryID,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const DeleteCourse = `-- name: DeleteCourse :exec
|
|
||||||
DELETE FROM courses
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
|
||||||
_, err := q.db.Exec(ctx, DeleteCourse, id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetAllCourses = `-- name: GetAllCourses :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
c.id,
|
|
||||||
c.category_id,
|
|
||||||
c.sub_category_id,
|
|
||||||
c.title,
|
|
||||||
c.description,
|
|
||||||
c.thumbnail,
|
|
||||||
c.intro_video_url,
|
|
||||||
c.is_active
|
|
||||||
FROM courses c
|
|
||||||
ORDER BY c.display_order ASC, c.id ASC
|
|
||||||
LIMIT $2::INT
|
|
||||||
OFFSET $1::INT
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetAllCoursesParams struct {
|
|
||||||
Offset pgtype.Int4 `json:"offset"`
|
|
||||||
Limit pgtype.Int4 `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetAllCoursesRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetAllCourses(ctx context.Context, arg GetAllCoursesParams) ([]GetAllCoursesRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetAllCourses, arg.Offset, arg.Limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetAllCoursesRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetAllCoursesRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.SubCategoryID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.IsActive,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetCourseByID = `-- name: GetCourseByID :one
|
|
||||||
SELECT id, category_id, title, description, is_active, thumbnail, intro_video_url, display_order, sub_category_id
|
|
||||||
FROM courses
|
|
||||||
WHERE id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetCourseByID, id)
|
|
||||||
var i Course
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.IsActive,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.SubCategoryID,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetCoursesByCategory = `-- name: GetCoursesByCategory :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
id,
|
|
||||||
category_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
intro_video_url,
|
|
||||||
is_active
|
|
||||||
FROM courses
|
|
||||||
WHERE category_id = $1
|
|
||||||
ORDER BY display_order ASC, id ASC
|
|
||||||
LIMIT $3::INT
|
|
||||||
OFFSET $2::INT
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetCoursesByCategoryParams struct {
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Offset pgtype.Int4 `json:"offset"`
|
|
||||||
Limit pgtype.Int4 `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetCoursesByCategoryRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCategoryParams) ([]GetCoursesByCategoryRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetCoursesByCategory, arg.CategoryID, arg.Offset, arg.Limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetCoursesByCategoryRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetCoursesByCategoryRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.IsActive,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetCoursesBySubCategory = `-- name: GetCoursesBySubCategory :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
c.id,
|
|
||||||
c.category_id,
|
|
||||||
c.sub_category_id,
|
|
||||||
c.title,
|
|
||||||
c.description,
|
|
||||||
c.thumbnail,
|
|
||||||
c.intro_video_url,
|
|
||||||
c.is_active
|
|
||||||
FROM courses c
|
|
||||||
WHERE c.sub_category_id = $1
|
|
||||||
ORDER BY c.display_order ASC, c.id ASC
|
|
||||||
LIMIT $3::INT
|
|
||||||
OFFSET $2::INT
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetCoursesBySubCategoryParams struct {
|
|
||||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
|
||||||
Offset pgtype.Int4 `json:"offset"`
|
|
||||||
Limit pgtype.Int4 `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetCoursesBySubCategoryRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetCoursesBySubCategory(ctx context.Context, arg GetCoursesBySubCategoryParams) ([]GetCoursesBySubCategoryRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetCoursesBySubCategory, arg.SubCategoryID, arg.Offset, arg.Limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetCoursesBySubCategoryRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetCoursesBySubCategoryRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.SubCategoryID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.IsActive,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetHumanLanguageCourses = `-- name: GetHumanLanguageCourses :many
|
|
||||||
SELECT
|
|
||||||
COUNT(*) OVER () AS total_count,
|
|
||||||
c.id,
|
|
||||||
c.category_id,
|
|
||||||
c.sub_category_id,
|
|
||||||
c.title,
|
|
||||||
c.description,
|
|
||||||
c.thumbnail,
|
|
||||||
c.intro_video_url,
|
|
||||||
c.is_active
|
|
||||||
FROM courses c
|
|
||||||
JOIN course_categories cc ON cc.id = c.category_id
|
|
||||||
WHERE lower(trim(cc.name)) = 'human language'
|
|
||||||
ORDER BY c.display_order ASC, c.id ASC
|
|
||||||
LIMIT $2::INT
|
|
||||||
OFFSET $1::INT
|
|
||||||
`
|
|
||||||
|
|
||||||
type GetHumanLanguageCoursesParams struct {
|
|
||||||
Offset pgtype.Int4 `json:"offset"`
|
|
||||||
Limit pgtype.Int4 `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetHumanLanguageCoursesRow struct {
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) GetHumanLanguageCourses(ctx context.Context, arg GetHumanLanguageCoursesParams) ([]GetHumanLanguageCoursesRow, error) {
|
|
||||||
rows, err := q.db.Query(ctx, GetHumanLanguageCourses, arg.Offset, arg.Limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []GetHumanLanguageCoursesRow
|
|
||||||
for rows.Next() {
|
|
||||||
var i GetHumanLanguageCoursesRow
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.TotalCount,
|
|
||||||
&i.ID,
|
|
||||||
&i.CategoryID,
|
|
||||||
&i.SubCategoryID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.Thumbnail,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
&i.IsActive,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReorderCourses = `-- name: ReorderCourses :exec
|
|
||||||
UPDATE courses
|
|
||||||
SET display_order = bulk.position
|
|
||||||
FROM (
|
|
||||||
SELECT unnest($1::BIGINT[]) AS id, unnest($2::INT[]) AS position
|
|
||||||
) AS bulk
|
|
||||||
WHERE courses.id = bulk.id
|
|
||||||
`
|
|
||||||
|
|
||||||
type ReorderCoursesParams struct {
|
|
||||||
Ids []int64 `json:"ids"`
|
|
||||||
Positions []int32 `json:"positions"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) ReorderCourses(ctx context.Context, arg ReorderCoursesParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, ReorderCourses, arg.Ids, arg.Positions)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const UpdateCourse = `-- name: UpdateCourse :exec
|
|
||||||
UPDATE courses
|
|
||||||
SET
|
|
||||||
title = COALESCE($1, title),
|
|
||||||
description = COALESCE($2, description),
|
|
||||||
thumbnail = COALESCE($3, thumbnail),
|
|
||||||
intro_video_url = COALESCE($4, intro_video_url),
|
|
||||||
is_active = COALESCE($5, is_active)
|
|
||||||
WHERE id = $6
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateCourseParams struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, UpdateCourse,
|
|
||||||
arg.Title,
|
|
||||||
arg.Description,
|
|
||||||
arg.Thumbnail,
|
|
||||||
arg.IntroVideoUrl,
|
|
||||||
arg.IsActive,
|
|
||||||
arg.ID,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
233
gen/db/lms_courses.sql.go
Normal file
233
gen/db/lms_courses.sql.go
Normal file
|
|
@ -0,0 +1,233 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_courses.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateCourse = `-- name: CreateCourse :one
|
||||||
|
INSERT INTO courses (program_id, name, description, thumbnail, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(c.sort_order)
|
||||||
|
FROM courses c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
id, program_id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateCourseParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateCourse,
|
||||||
|
arg.ProgramID,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Thumbnail,
|
||||||
|
)
|
||||||
|
var i Course
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteCourse = `-- name: DeleteCourse :exec
|
||||||
|
DELETE FROM courses
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteCourse, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetCourseByID = `-- name: GetCourseByID :one
|
||||||
|
SELECT id, program_id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
FROM courses
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetCourseByID, id)
|
||||||
|
var i Course
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListCourseIDsByProgram = `-- name: ListCourseIDsByProgram :many
|
||||||
|
SELECT
|
||||||
|
c.id
|
||||||
|
FROM
|
||||||
|
courses AS c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
ORDER BY
|
||||||
|
c.id
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListCourseIDsByProgram(ctx context.Context, programID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListCourseIDsByProgram, programID)
|
||||||
|
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 ListCoursesByProgramID = `-- name: ListCoursesByProgramID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
c.id,
|
||||||
|
c.program_id,
|
||||||
|
c.name,
|
||||||
|
c.description,
|
||||||
|
c.thumbnail,
|
||||||
|
c.sort_order,
|
||||||
|
c.created_at,
|
||||||
|
c.updated_at
|
||||||
|
FROM
|
||||||
|
courses c
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
ORDER BY
|
||||||
|
c.sort_order ASC,
|
||||||
|
c.id ASC
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListCoursesByProgramIDParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListCoursesByProgramIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_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) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListCoursesByProgramID, arg.ProgramID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListCoursesByProgramIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListCoursesByProgramIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&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 UpdateCourse = `-- name: UpdateCourse :one
|
||||||
|
UPDATE 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, program_id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateCourseParams 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) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Course, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateCourse,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Thumbnail,
|
||||||
|
arg.SortOrder,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i Course
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
215
gen/db/lms_lessons.sql.go
Normal file
215
gen/db/lms_lessons.sql.go
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_lessons.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateLesson = `-- name: CreateLesson :one
|
||||||
|
INSERT INTO lessons (module_id, title, video_url, thumbnail, description, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(l.sort_order)
|
||||||
|
FROM lessons l
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateLessonParams struct {
|
||||||
|
ModuleID int64 `json:"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) CreateLesson(ctx context.Context, arg CreateLessonParams) (Lesson, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateLesson,
|
||||||
|
arg.ModuleID,
|
||||||
|
arg.Title,
|
||||||
|
arg.VideoUrl,
|
||||||
|
arg.Thumbnail,
|
||||||
|
arg.Description,
|
||||||
|
)
|
||||||
|
var i Lesson
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.VideoUrl,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteLesson = `-- name: DeleteLesson :exec
|
||||||
|
DELETE FROM lessons
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteLesson(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteLesson, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetLessonByID = `-- name: GetLessonByID :one
|
||||||
|
SELECT id, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order
|
||||||
|
FROM lessons
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetLessonByID(ctx context.Context, id int64) (Lesson, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetLessonByID, id)
|
||||||
|
var i Lesson
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.VideoUrl,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLessonsByModuleID = `-- name: ListLessonsByModuleID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
l.id,
|
||||||
|
l.module_id,
|
||||||
|
l.title,
|
||||||
|
l.video_url,
|
||||||
|
l.thumbnail,
|
||||||
|
l.description,
|
||||||
|
l.sort_order,
|
||||||
|
l.created_at,
|
||||||
|
l.updated_at
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1
|
||||||
|
ORDER BY
|
||||||
|
l.sort_order ASC,
|
||||||
|
l.id ASC
|
||||||
|
LIMIT $2
|
||||||
|
OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListLessonsByModuleIDParams struct {
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLessonsByModuleIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ModuleID int64 `json:"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) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByModuleIDParams) ([]ListLessonsByModuleIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLessonsByModuleID, arg.ModuleID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListLessonsByModuleIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListLessonsByModuleIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&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 UpdateLesson = `-- name: UpdateLesson :one
|
||||||
|
UPDATE 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, module_id, title, video_url, thumbnail, description, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateLessonParams 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) UpdateLesson(ctx context.Context, arg UpdateLessonParams) (Lesson, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateLesson,
|
||||||
|
arg.Title,
|
||||||
|
arg.VideoUrl,
|
||||||
|
arg.Thumbnail,
|
||||||
|
arg.Description,
|
||||||
|
arg.SortOrder,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i Lesson
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.VideoUrl,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
250
gen/db/lms_modules.sql.go
Normal file
250
gen/db/lms_modules.sql.go
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_modules.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateModule = `-- name: CreateModule :one
|
||||||
|
INSERT INTO modules (program_id, course_id, name, description, icon, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(m.sort_order)
|
||||||
|
FROM modules m
|
||||||
|
WHERE
|
||||||
|
m.course_id = $2), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateModuleParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Icon pgtype.Text `json:"icon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateModule,
|
||||||
|
arg.ProgramID,
|
||||||
|
arg.CourseID,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Icon,
|
||||||
|
)
|
||||||
|
var i Module
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Icon,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteModule = `-- name: DeleteModule :exec
|
||||||
|
DELETE FROM modules
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteModule(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteModule, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetModuleByID = `-- name: GetModuleByID :one
|
||||||
|
SELECT id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order
|
||||||
|
FROM modules
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetModuleByID, id)
|
||||||
|
var i Module
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Icon,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListModuleIDsByCourse = `-- name: ListModuleIDsByCourse :many
|
||||||
|
SELECT
|
||||||
|
m.id
|
||||||
|
FROM
|
||||||
|
modules AS m
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
ORDER BY
|
||||||
|
m.id
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListModuleIDsByCourse, courseID)
|
||||||
|
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 ListModulesByProgramAndCourse = `-- name: ListModulesByProgramAndCourse :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
m.id,
|
||||||
|
m.program_id,
|
||||||
|
m.course_id,
|
||||||
|
m.name,
|
||||||
|
m.description,
|
||||||
|
m.icon,
|
||||||
|
m.sort_order,
|
||||||
|
m.created_at,
|
||||||
|
m.updated_at
|
||||||
|
FROM
|
||||||
|
modules m
|
||||||
|
WHERE
|
||||||
|
m.program_id = $1
|
||||||
|
AND m.course_id = $2
|
||||||
|
ORDER BY
|
||||||
|
m.sort_order ASC,
|
||||||
|
m.id ASC
|
||||||
|
LIMIT $3
|
||||||
|
OFFSET $4
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListModulesByProgramAndCourseParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListModulesByProgramAndCourseRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
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) ListModulesByProgramAndCourse(ctx context.Context, arg ListModulesByProgramAndCourseParams) ([]ListModulesByProgramAndCourseRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListModulesByProgramAndCourse,
|
||||||
|
arg.ProgramID,
|
||||||
|
arg.CourseID,
|
||||||
|
arg.Limit,
|
||||||
|
arg.Offset,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListModulesByProgramAndCourseRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListModulesByProgramAndCourseRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&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 UpdateModule = `-- name: UpdateModule :one
|
||||||
|
UPDATE modules
|
||||||
|
SET
|
||||||
|
name = COALESCE($1::varchar, name),
|
||||||
|
description = COALESCE($2::text, description),
|
||||||
|
icon = COALESCE($3::text, icon),
|
||||||
|
sort_order = coalesce($4::int, sort_order),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE
|
||||||
|
id = $5
|
||||||
|
RETURNING
|
||||||
|
id, program_id, course_id, name, description, icon, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateModuleParams struct {
|
||||||
|
Name pgtype.Text `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Icon pgtype.Text `json:"icon"`
|
||||||
|
SortOrder pgtype.Int4 `json:"sort_order"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Module, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateModule,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Icon,
|
||||||
|
arg.SortOrder,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i Module
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Icon,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
381
gen/db/lms_practices.sql.go
Normal file
381
gen/db/lms_practices.sql.go
Normal file
|
|
@ -0,0 +1,381 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_practices.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateLmsPractice = `-- name: CreateLmsPractice :one
|
||||||
|
INSERT INTO lms_practices (
|
||||||
|
course_id, module_id, lesson_id,
|
||||||
|
title, story_description, story_image, persona_id, question_set_id, quick_tips
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateLmsPracticeParams struct {
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
LessonID pgtype.Int8 `json:"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) CreateLmsPractice(ctx context.Context, arg CreateLmsPracticeParams) (LmsPractice, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateLmsPractice,
|
||||||
|
arg.CourseID,
|
||||||
|
arg.ModuleID,
|
||||||
|
arg.LessonID,
|
||||||
|
arg.Title,
|
||||||
|
arg.StoryDescription,
|
||||||
|
arg.StoryImage,
|
||||||
|
arg.PersonaID,
|
||||||
|
arg.QuestionSetID,
|
||||||
|
arg.QuickTips,
|
||||||
|
)
|
||||||
|
var i LmsPractice
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&i.Title,
|
||||||
|
&i.StoryDescription,
|
||||||
|
&i.StoryImage,
|
||||||
|
&i.PersonaID,
|
||||||
|
&i.QuestionSetID,
|
||||||
|
&i.QuickTips,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteLmsPractice = `-- name: DeleteLmsPractice :exec
|
||||||
|
DELETE FROM lms_practices
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteLmsPractice(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteLmsPractice, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetLmsPracticeByID = `-- name: GetLmsPracticeByID :one
|
||||||
|
SELECT id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||||
|
FROM lms_practices
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetLmsPracticeByID(ctx context.Context, id int64) (LmsPractice, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetLmsPracticeByID, id)
|
||||||
|
var i LmsPractice
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&i.Title,
|
||||||
|
&i.StoryDescription,
|
||||||
|
&i.StoryImage,
|
||||||
|
&i.PersonaID,
|
||||||
|
&i.QuestionSetID,
|
||||||
|
&i.QuickTips,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLmsPracticesByCourseID = `-- name: ListLmsPracticesByCourseID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.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 lms_practices p
|
||||||
|
WHERE p.course_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListLmsPracticesByCourseIDParams struct {
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLmsPracticesByCourseIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
LessonID pgtype.Int8 `json:"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) ListLmsPracticesByCourseID(ctx context.Context, arg ListLmsPracticesByCourseIDParams) ([]ListLmsPracticesByCourseIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLmsPracticesByCourseID, arg.CourseID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListLmsPracticesByCourseIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListLmsPracticesByCourseIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&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 ListLmsPracticesByLessonID = `-- name: ListLmsPracticesByLessonID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.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 lms_practices p
|
||||||
|
WHERE p.lesson_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListLmsPracticesByLessonIDParams struct {
|
||||||
|
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLmsPracticesByLessonIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
LessonID pgtype.Int8 `json:"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) ListLmsPracticesByLessonID(ctx context.Context, arg ListLmsPracticesByLessonIDParams) ([]ListLmsPracticesByLessonIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLmsPracticesByLessonID, arg.LessonID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListLmsPracticesByLessonIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListLmsPracticesByLessonIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&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 ListLmsPracticesByModuleID = `-- name: ListLmsPracticesByModuleID :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.course_id,
|
||||||
|
p.module_id,
|
||||||
|
p.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 lms_practices p
|
||||||
|
WHERE p.module_id = $1
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListLmsPracticesByModuleIDParams struct {
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLmsPracticesByModuleIDRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
LessonID pgtype.Int8 `json:"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) ListLmsPracticesByModuleID(ctx context.Context, arg ListLmsPracticesByModuleIDParams) ([]ListLmsPracticesByModuleIDRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLmsPracticesByModuleID, arg.ModuleID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListLmsPracticesByModuleIDRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListLmsPracticesByModuleIDRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&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 UpdateLmsPractice = `-- name: UpdateLmsPractice :one
|
||||||
|
UPDATE lms_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, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateLmsPracticeParams 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) UpdateLmsPractice(ctx context.Context, arg UpdateLmsPracticeParams) (LmsPractice, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateLmsPractice,
|
||||||
|
arg.Title,
|
||||||
|
arg.StoryDescription,
|
||||||
|
arg.StoryImage,
|
||||||
|
arg.PersonaID,
|
||||||
|
arg.QuestionSetID,
|
||||||
|
arg.QuickTips,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i LmsPractice
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.LessonID,
|
||||||
|
&i.Title,
|
||||||
|
&i.StoryDescription,
|
||||||
|
&i.StoryImage,
|
||||||
|
&i.PersonaID,
|
||||||
|
&i.QuestionSetID,
|
||||||
|
&i.QuickTips,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
613
gen/db/lms_progress.sql.go
Normal file
613
gen/db/lms_progress.sql.go
Normal file
|
|
@ -0,0 +1,613 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: lms_progress.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CountCoursesInProgram = `-- name: CountCoursesInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
courses
|
||||||
|
WHERE
|
||||||
|
program_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountCoursesInProgram(ctx context.Context, programID int64) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountCoursesInProgram, programID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountLessonsInCourse = `-- name: CountLessonsInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
// Lesson-based progress within a course (all modules).
|
||||||
|
func (q *Queries) CountLessonsInCourse(ctx context.Context, courseID int64) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountLessonsInCourse, courseID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountLessonsInModule = `-- name: CountLessonsInModule :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons
|
||||||
|
WHERE
|
||||||
|
module_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountLessonsInModule(ctx context.Context, moduleID int64) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountLessonsInModule, moduleID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountLessonsInProgram = `-- name: CountLessonsInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lessons l
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
// Lesson-based progress within a program (all courses).
|
||||||
|
func (q *Queries) CountLessonsInProgram(ctx context.Context, programID int64) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountLessonsInProgram, programID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountModulesInCourse = `-- name: CountModulesInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
modules
|
||||||
|
WHERE
|
||||||
|
course_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountModulesInCourse(ctx context.Context, courseID int64) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountModulesInCourse, courseID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountUserCompletedCoursesInProgram = `-- name: CountUserCompletedCoursesInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress ucp
|
||||||
|
INNER JOIN courses c ON c.id = ucp.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
AND ucp.user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountUserCompletedCoursesInProgramParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountUserCompletedCoursesInProgram(ctx context.Context, arg CountUserCompletedCoursesInProgramParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountUserCompletedCoursesInProgram, arg.ProgramID, arg.UserID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountUserCompletedLessonsInCourse = `-- name: CountUserCompletedLessonsInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
AND ulp.user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountUserCompletedLessonsInCourseParams struct {
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountUserCompletedLessonsInCourse(ctx context.Context, arg CountUserCompletedLessonsInCourseParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountUserCompletedLessonsInCourse, arg.CourseID, arg.UserID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountUserCompletedLessonsInModule = `-- name: CountUserCompletedLessonsInModule :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
WHERE
|
||||||
|
l.module_id = $1
|
||||||
|
AND ulp.user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountUserCompletedLessonsInModuleParams struct {
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountUserCompletedLessonsInModule(ctx context.Context, arg CountUserCompletedLessonsInModuleParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountUserCompletedLessonsInModule, arg.ModuleID, arg.UserID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountUserCompletedLessonsInProgram = `-- name: CountUserCompletedLessonsInProgram :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress ulp
|
||||||
|
INNER JOIN lessons l ON l.id = ulp.lesson_id
|
||||||
|
INNER JOIN modules m ON m.id = l.module_id
|
||||||
|
INNER JOIN courses c ON c.id = m.course_id
|
||||||
|
WHERE
|
||||||
|
c.program_id = $1
|
||||||
|
AND ulp.user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountUserCompletedLessonsInProgramParams struct {
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountUserCompletedLessonsInProgram(ctx context.Context, arg CountUserCompletedLessonsInProgramParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountUserCompletedLessonsInProgram, arg.ProgramID, arg.UserID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const CountUserCompletedModulesInCourse = `-- name: CountUserCompletedModulesInCourse :one
|
||||||
|
SELECT
|
||||||
|
count(*)::int AS n
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress ump
|
||||||
|
INNER JOIN modules m ON m.id = ump.module_id
|
||||||
|
WHERE
|
||||||
|
m.course_id = $1
|
||||||
|
AND ump.user_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type CountUserCompletedModulesInCourseParams struct {
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CountUserCompletedModulesInCourse(ctx context.Context, arg CountUserCompletedModulesInCourseParams) (int32, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountUserCompletedModulesInCourse, arg.CourseID, arg.UserID)
|
||||||
|
var n int32
|
||||||
|
err := row.Scan(&n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetPreviousCourseInProgram = `-- name: GetPreviousCourseInProgram :one
|
||||||
|
SELECT
|
||||||
|
c2.id, c2.program_id, c2.name, c2.description, c2.thumbnail, c2.created_at, c2.updated_at, c2.sort_order
|
||||||
|
FROM
|
||||||
|
courses AS c1
|
||||||
|
INNER JOIN courses AS c2 ON c2.program_id = c1.program_id
|
||||||
|
AND c2.sort_order = c1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
c1.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPreviousCourseInProgram(ctx context.Context, id int64) (Course, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetPreviousCourseInProgram, id)
|
||||||
|
var i Course
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetPreviousLessonInModule = `-- name: GetPreviousLessonInModule :one
|
||||||
|
SELECT
|
||||||
|
l2.id, l2.module_id, l2.title, l2.video_url, l2.thumbnail, l2.description, l2.created_at, l2.updated_at, l2.sort_order
|
||||||
|
FROM
|
||||||
|
lessons AS l1
|
||||||
|
INNER JOIN lessons AS l2 ON l2.module_id = l1.module_id
|
||||||
|
AND l2.sort_order = l1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
l1.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPreviousLessonInModule(ctx context.Context, id int64) (Lesson, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetPreviousLessonInModule, id)
|
||||||
|
var i Lesson
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ModuleID,
|
||||||
|
&i.Title,
|
||||||
|
&i.VideoUrl,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.Description,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetPreviousModuleInCourse = `-- name: GetPreviousModuleInCourse :one
|
||||||
|
SELECT
|
||||||
|
m2.id, m2.program_id, m2.course_id, m2.name, m2.description, m2.icon, m2.created_at, m2.updated_at, m2.sort_order
|
||||||
|
FROM
|
||||||
|
modules AS m1
|
||||||
|
INNER JOIN modules AS m2 ON m2.course_id = m1.course_id
|
||||||
|
AND m2.sort_order = m1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
m1.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPreviousModuleInCourse(ctx context.Context, id int64) (Module, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetPreviousModuleInCourse, id)
|
||||||
|
var i Module
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProgramID,
|
||||||
|
&i.CourseID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Icon,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetPreviousProgram = `-- name: GetPreviousProgram :one
|
||||||
|
SELECT
|
||||||
|
p2.id, p2.name, p2.description, p2.thumbnail, p2.created_at, p2.updated_at, p2.sort_order
|
||||||
|
FROM
|
||||||
|
programs AS p1
|
||||||
|
INNER JOIN programs AS p2 ON p2.sort_order = p1.sort_order - 1
|
||||||
|
WHERE
|
||||||
|
p1.id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetPreviousProgram(ctx context.Context, id int64) (Program, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetPreviousProgram, id)
|
||||||
|
var i Program
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const InsertUserCourseProgress = `-- name: InsertUserCourseProgress :exec
|
||||||
|
INSERT INTO lms_user_course_progress (user_id, course_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, course_id)
|
||||||
|
DO NOTHING
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserCourseProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUserCourseProgress(ctx context.Context, arg InsertUserCourseProgressParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, InsertUserCourseProgress, arg.UserID, arg.CourseID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const InsertUserLessonProgress = `-- name: InsertUserLessonProgress :exec
|
||||||
|
INSERT INTO lms_user_lesson_progress (user_id, lesson_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, lesson_id)
|
||||||
|
DO NOTHING
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserLessonProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
LessonID int64 `json:"lesson_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUserLessonProgress(ctx context.Context, arg InsertUserLessonProgressParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, InsertUserLessonProgress, arg.UserID, arg.LessonID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const InsertUserModuleProgress = `-- name: InsertUserModuleProgress :exec
|
||||||
|
INSERT INTO lms_user_module_progress (user_id, module_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, module_id)
|
||||||
|
DO NOTHING
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserModuleProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUserModuleProgress(ctx context.Context, arg InsertUserModuleProgressParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, InsertUserModuleProgress, arg.UserID, arg.ModuleID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const InsertUserProgramProgress = `-- name: InsertUserProgramProgress :exec
|
||||||
|
INSERT INTO lms_user_program_progress (user_id, program_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id, program_id)
|
||||||
|
DO NOTHING
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserProgramProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUserProgramProgress(ctx context.Context, arg InsertUserProgramProgressParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, InsertUserProgramProgress, arg.UserID, arg.ProgramID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLMSCompletedCourseIDsByUser = `-- name: ListLMSCompletedCourseIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ucp.course_id
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress AS ucp
|
||||||
|
WHERE
|
||||||
|
ucp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ucp.completed_at ASC,
|
||||||
|
ucp.course_id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListLMSCompletedCourseIDsByUser(ctx context.Context, userID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLMSCompletedCourseIDsByUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var course_id int64
|
||||||
|
if err := rows.Scan(&course_id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, course_id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLMSCompletedLessonIDsByUser = `-- name: ListLMSCompletedLessonIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ulp.lesson_id
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress AS ulp
|
||||||
|
WHERE
|
||||||
|
ulp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ulp.completed_at ASC,
|
||||||
|
ulp.lesson_id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListLMSCompletedLessonIDsByUser(ctx context.Context, userID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLMSCompletedLessonIDsByUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var lesson_id int64
|
||||||
|
if err := rows.Scan(&lesson_id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, lesson_id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLMSCompletedModuleIDsByUser = `-- name: ListLMSCompletedModuleIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
ump.module_id
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress AS ump
|
||||||
|
WHERE
|
||||||
|
ump.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
ump.completed_at ASC,
|
||||||
|
ump.module_id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListLMSCompletedModuleIDsByUser(ctx context.Context, userID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLMSCompletedModuleIDsByUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var module_id int64
|
||||||
|
if err := rows.Scan(&module_id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, module_id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLMSCompletedProgramIDsByUser = `-- name: ListLMSCompletedProgramIDsByUser :many
|
||||||
|
SELECT
|
||||||
|
upp.program_id
|
||||||
|
FROM
|
||||||
|
lms_user_program_progress AS upp
|
||||||
|
WHERE
|
||||||
|
upp.user_id = $1
|
||||||
|
ORDER BY
|
||||||
|
upp.completed_at ASC,
|
||||||
|
upp.program_id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListLMSCompletedProgramIDsByUser(ctx context.Context, userID int64) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLMSCompletedProgramIDsByUser, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var program_id int64
|
||||||
|
if err := rows.Scan(&program_id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, program_id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserHasCourseProgress = `-- name: UserHasCourseProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_course_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND course_id = $2) AS v
|
||||||
|
`
|
||||||
|
|
||||||
|
type UserHasCourseProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UserHasCourseProgress(ctx context.Context, arg UserHasCourseProgressParams) (bool, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UserHasCourseProgress, arg.UserID, arg.CourseID)
|
||||||
|
var v bool
|
||||||
|
err := row.Scan(&v)
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserHasLessonProgress = `-- name: UserHasLessonProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_lesson_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND lesson_id = $2) AS v
|
||||||
|
`
|
||||||
|
|
||||||
|
type UserHasLessonProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
LessonID int64 `json:"lesson_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UserHasLessonProgress(ctx context.Context, arg UserHasLessonProgressParams) (bool, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UserHasLessonProgress, arg.UserID, arg.LessonID)
|
||||||
|
var v bool
|
||||||
|
err := row.Scan(&v)
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserHasModuleProgress = `-- name: UserHasModuleProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_module_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND module_id = $2) AS v
|
||||||
|
`
|
||||||
|
|
||||||
|
type UserHasModuleProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ModuleID int64 `json:"module_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UserHasModuleProgress(ctx context.Context, arg UserHasModuleProgressParams) (bool, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UserHasModuleProgress, arg.UserID, arg.ModuleID)
|
||||||
|
var v bool
|
||||||
|
err := row.Scan(&v)
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserHasProgramProgress = `-- name: UserHasProgramProgress :one
|
||||||
|
SELECT
|
||||||
|
EXISTS (
|
||||||
|
SELECT
|
||||||
|
1
|
||||||
|
FROM
|
||||||
|
lms_user_program_progress
|
||||||
|
WHERE
|
||||||
|
user_id = $1
|
||||||
|
AND program_id = $2) AS v
|
||||||
|
`
|
||||||
|
|
||||||
|
type UserHasProgramProgressParams struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UserHasProgramProgress(ctx context.Context, arg UserHasProgramProgressParams) (bool, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UserHasProgramProgress, arg.UserID, arg.ProgramID)
|
||||||
|
var v bool
|
||||||
|
err := row.Scan(&v)
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
258
gen/db/models.go
258
gen/db/models.go
|
|
@ -24,32 +24,13 @@ type ActivityLog struct {
|
||||||
|
|
||||||
type Course struct {
|
type Course struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
CategoryID int64 `json:"category_id"`
|
ProgramID int64 `json:"program_id"`
|
||||||
Title string `json:"title"`
|
Name string `json:"name"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
SubCategoryID pgtype.Int8 `json:"sub_category_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CourseCategory struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CourseSubCategory struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
|
|
@ -69,16 +50,16 @@ type GlobalSetting struct {
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Level struct {
|
type Lesson struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
CourseID int64 `json:"course_id"`
|
ModuleID int64 `json:"module_id"`
|
||||||
CefrLevel string `json:"cefr_level"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
VideoUrl pgtype.Text `json:"video_url"`
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LevelToSubCourse struct {
|
type LevelToSubCourse struct {
|
||||||
|
|
@ -86,28 +67,55 @@ type LevelToSubCourse struct {
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
SubCourseID int64 `json:"sub_course_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Module struct {
|
type LmsPractice struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
LevelID int64 `json:"level_id"`
|
CourseID pgtype.Int8 `json:"course_id"`
|
||||||
|
ModuleID pgtype.Int8 `json:"module_id"`
|
||||||
|
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
StoryDescription pgtype.Text `json:"story_description"`
|
||||||
DisplayOrder int32 `json:"display_order"`
|
StoryImage pgtype.Text `json:"story_image"`
|
||||||
IsActive bool `json:"is_active"`
|
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||||
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
|
QuickTips pgtype.Text `json:"quick_tips"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
IconUrl pgtype.Text `json:"icon_url"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModuleCapstone struct {
|
type LmsUserCourseProgress struct {
|
||||||
ID int64 `json:"id"`
|
UserID int64 `json:"user_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LmsUserLessonProgress struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
LessonID int64 `json:"lesson_id"`
|
||||||
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LmsUserModuleProgress struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
ModuleID int64 `json:"module_id"`
|
ModuleID int64 `json:"module_id"`
|
||||||
Title string `json:"title"`
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LmsUserProgramProgress struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Module struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
Tips pgtype.Text `json:"tips"`
|
Icon pgtype.Text `json:"icon"`
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModuleToSubCourse struct {
|
type ModuleToSubCourse struct {
|
||||||
|
|
@ -171,6 +179,16 @@ type Permission struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Program struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
SortOrder int32 `json:"sort_order"`
|
||||||
|
}
|
||||||
|
|
||||||
type Question struct {
|
type Question struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
QuestionText string `json:"question_text"`
|
QuestionText string `json:"question_text"`
|
||||||
|
|
@ -218,7 +236,6 @@ type QuestionSet struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
}
|
}
|
||||||
|
|
@ -315,132 +332,6 @@ type ScheduledNotification struct {
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubCourse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CourseID int64 `json:"course_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
Level string `json:"level"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
SubLevel string `json:"sub_level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCoursePrerequisite struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
|
||||||
PrerequisiteSubCourseID int64 `json:"prerequisite_sub_course_id"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCourseVideo struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
VideoUrl string `json:"video_url"`
|
|
||||||
Duration int32 `json:"duration"`
|
|
||||||
Resolution pgtype.Text `json:"resolution"`
|
|
||||||
IsPublished bool `json:"is_published"`
|
|
||||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
|
||||||
Visibility pgtype.Text `json:"visibility"`
|
|
||||||
InstructorID pgtype.Text `json:"instructor_id"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
// Vimeo video ID for videos hosted on Vimeo
|
|
||||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
|
||||||
// Vimeo player embed URL
|
|
||||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
|
||||||
// Vimeo iframe embed HTML code
|
|
||||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
|
||||||
// Vimeo video status: pending, uploading, transcoding, available, error
|
|
||||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
|
||||||
// Video hosting provider: DIRECT or VIMEO
|
|
||||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubModule struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
ModuleID int64 `json:"module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
LegacySubCourseID pgtype.Int8 `json:"legacy_sub_course_id"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
Tips pgtype.Text `json:"tips"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubModuleCapstone struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Tips pgtype.Text `json:"tips"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
InactiveSince pgtype.Timestamptz `json:"inactive_since"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubModuleLesson struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
TeachingText pgtype.Text `json:"teaching_text"`
|
|
||||||
TeachingImageUrl pgtype.Text `json:"teaching_image_url"`
|
|
||||||
TeachingAudioUrl pgtype.Text `json:"teaching_audio_url"`
|
|
||||||
TeachingVideoUrl pgtype.Text `json:"teaching_video_url"`
|
|
||||||
InactiveSince pgtype.Timestamptz `json:"inactive_since"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubModulePractice struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
InactiveSince pgtype.Timestamptz `json:"inactive_since"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubModuleVideo struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
SubModuleID int64 `json:"sub_module_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
VideoUrl string `json:"video_url"`
|
|
||||||
Duration pgtype.Int4 `json:"duration"`
|
|
||||||
Resolution pgtype.Text `json:"resolution"`
|
|
||||||
IsPublished bool `json:"is_published"`
|
|
||||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
|
||||||
Visibility pgtype.Text `json:"visibility"`
|
|
||||||
InstructorID pgtype.Text `json:"instructor_id"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
VimeoID pgtype.Text `json:"vimeo_id"`
|
|
||||||
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
|
|
||||||
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
|
|
||||||
VimeoStatus pgtype.Text `json:"vimeo_status"`
|
|
||||||
VideoHostProvider pgtype.Text `json:"video_host_provider"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubscriptionPlan struct {
|
type SubscriptionPlan struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
@ -540,33 +431,10 @@ type UserAudioResponse struct {
|
||||||
type UserPracticeProgress struct {
|
type UserPracticeProgress struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
SubCourseID pgtype.Int8 `json:"sub_course_id"`
|
|
||||||
QuestionSetID int64 `json:"question_set_id"`
|
QuestionSetID int64 `json:"question_set_id"`
|
||||||
CompletedAt pgtype.Timestamp `json:"completed_at"`
|
|
||||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserSubCourseProgress struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
ProgressPercentage int16 `json:"progress_percentage"`
|
|
||||||
StartedAt pgtype.Timestamptz `json:"started_at"`
|
|
||||||
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
CompletedAt pgtype.Timestamptz `json:"completed_at"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
|
||||||
type UserSubCourseVideoProgress struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UserID int64 `json:"user_id"`
|
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
|
||||||
VideoID int64 `json:"video_id"`
|
|
||||||
CompletedAt pgtype.Timestamp `json:"completed_at"`
|
|
||||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSubscription struct {
|
type UserSubscription struct {
|
||||||
|
|
|
||||||
|
|
@ -59,26 +59,16 @@ func (q *Queries) GetFirstIncompletePreviousPractice(ctx context.Context, arg Ge
|
||||||
const MarkPracticeCompleted = `-- name: MarkPracticeCompleted :execrows
|
const MarkPracticeCompleted = `-- name: MarkPracticeCompleted :execrows
|
||||||
INSERT INTO user_practice_progress (
|
INSERT INTO user_practice_progress (
|
||||||
user_id,
|
user_id,
|
||||||
sub_course_id,
|
|
||||||
question_set_id,
|
question_set_id,
|
||||||
completed_at,
|
completed_at,
|
||||||
updated_at
|
updated_at
|
||||||
)
|
)
|
||||||
SELECT
|
VALUES (
|
||||||
$1::BIGINT,
|
$1::BIGINT,
|
||||||
CASE
|
$2::BIGINT,
|
||||||
WHEN qs.owner_type = 'SUB_COURSE' THEN qs.owner_id
|
|
||||||
WHEN qs.owner_type = 'SUB_MODULE' THEN sm.legacy_sub_course_id
|
|
||||||
ELSE NULL
|
|
||||||
END,
|
|
||||||
qs.id,
|
|
||||||
CURRENT_TIMESTAMP,
|
CURRENT_TIMESTAMP,
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
FROM question_sets qs
|
)
|
||||||
LEFT JOIN sub_modules sm
|
|
||||||
ON qs.owner_type = 'SUB_MODULE'
|
|
||||||
AND qs.owner_id = sm.id
|
|
||||||
WHERE qs.id = $2::BIGINT
|
|
||||||
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
ON CONFLICT (user_id, question_set_id) DO UPDATE
|
||||||
SET completed_at = EXCLUDED.completed_at,
|
SET completed_at = EXCLUDED.completed_at,
|
||||||
updated_at = EXCLUDED.updated_at
|
updated_at = EXCLUDED.updated_at
|
||||||
|
|
|
||||||
210
gen/db/programs.sql.go
Normal file
210
gen/db/programs.sql.go
Normal file
|
|
@ -0,0 +1,210 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: programs.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateProgram = `-- name: CreateProgram :one
|
||||||
|
INSERT INTO programs (name, description, thumbnail, sort_order)
|
||||||
|
SELECT
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
coalesce((
|
||||||
|
SELECT
|
||||||
|
max(p.sort_order)
|
||||||
|
FROM programs AS p), 0) + 1
|
||||||
|
RETURNING
|
||||||
|
id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateProgramParams struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (Program, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateProgram, arg.Name, arg.Description, arg.Thumbnail)
|
||||||
|
var i Program
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteProgram = `-- name: DeleteProgram :exec
|
||||||
|
DELETE FROM programs
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteProgram(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, DeleteProgram, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetProgramByID = `-- name: GetProgramByID :one
|
||||||
|
SELECT id, name, description, thumbnail, created_at, updated_at, sort_order
|
||||||
|
FROM programs
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetProgramByID, id)
|
||||||
|
var i Program
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListAllProgramIDs = `-- name: ListAllProgramIDs :many
|
||||||
|
SELECT
|
||||||
|
p.id
|
||||||
|
FROM
|
||||||
|
programs AS p
|
||||||
|
ORDER BY
|
||||||
|
p.id
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListAllProgramIDs(ctx context.Context) ([]int64, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListAllProgramIDs)
|
||||||
|
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 ListPrograms = `-- name: ListPrograms :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.thumbnail,
|
||||||
|
p.sort_order,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM programs p
|
||||||
|
ORDER BY p.sort_order ASC, p.id ASC
|
||||||
|
LIMIT $1 OFFSET $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListProgramsParams struct {
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListProgramsRow 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) ListPrograms(ctx context.Context, arg ListProgramsParams) ([]ListProgramsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListPrograms, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListProgramsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListProgramsRow
|
||||||
|
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 UpdateProgram = `-- name: UpdateProgram :one
|
||||||
|
UPDATE programs
|
||||||
|
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, created_at, updated_at, sort_order
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateProgramParams 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) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (Program, error) {
|
||||||
|
row := q.db.QueryRow(ctx, UpdateProgram,
|
||||||
|
arg.Name,
|
||||||
|
arg.Description,
|
||||||
|
arg.Thumbnail,
|
||||||
|
arg.SortOrder,
|
||||||
|
arg.ID,
|
||||||
|
)
|
||||||
|
var i Program
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.SortOrder,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
@ -295,7 +295,7 @@ func (q *Queries) GetQuestionSetItemsPaginated(ctx context.Context, arg GetQuest
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many
|
const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many
|
||||||
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order, qs.intro_video_url
|
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.display_order, qs.intro_video_url
|
||||||
FROM question_sets qs
|
FROM question_sets qs
|
||||||
JOIN question_set_items qsi ON qsi.set_id = qs.id
|
JOIN question_set_items qsi ON qsi.set_id = qs.id
|
||||||
WHERE qsi.question_id = $1
|
WHERE qsi.question_id = $1
|
||||||
|
|
@ -326,7 +326,6 @@ func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questio
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -64,11 +64,10 @@ INSERT INTO question_sets (
|
||||||
passing_score,
|
passing_score,
|
||||||
shuffle_questions,
|
shuffle_questions,
|
||||||
status,
|
status,
|
||||||
sub_course_video_id,
|
|
||||||
intro_video_url
|
intro_video_url
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12, $13)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
|
||||||
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, display_order, intro_video_url
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateQuestionSetParams struct {
|
type CreateQuestionSetParams struct {
|
||||||
|
|
@ -83,7 +82,6 @@ type CreateQuestionSetParams struct {
|
||||||
PassingScore pgtype.Int4 `json:"passing_score"`
|
PassingScore pgtype.Int4 `json:"passing_score"`
|
||||||
Column10 interface{} `json:"column_10"`
|
Column10 interface{} `json:"column_10"`
|
||||||
Column11 interface{} `json:"column_11"`
|
Column11 interface{} `json:"column_11"`
|
||||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,7 +98,6 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa
|
||||||
arg.PassingScore,
|
arg.PassingScore,
|
||||||
arg.Column10,
|
arg.Column10,
|
||||||
arg.Column11,
|
arg.Column11,
|
||||||
arg.SubCourseVideoID,
|
|
||||||
arg.IntroVideoUrl,
|
arg.IntroVideoUrl,
|
||||||
)
|
)
|
||||||
var i QuestionSet
|
var i QuestionSet
|
||||||
|
|
@ -119,7 +116,6 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
)
|
)
|
||||||
|
|
@ -137,7 +133,7 @@ func (q *Queries) DeleteQuestionSet(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one
|
const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one
|
||||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, display_order, intro_video_url
|
||||||
FROM question_sets
|
FROM question_sets
|
||||||
WHERE set_type = 'INITIAL_ASSESSMENT'
|
WHERE set_type = 'INITIAL_ASSESSMENT'
|
||||||
AND status = 'PUBLISHED'
|
AND status = 'PUBLISHED'
|
||||||
|
|
@ -163,7 +159,6 @@ func (q *Queries) GetInitialAssessmentSet(ctx context.Context) (QuestionSet, err
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
)
|
)
|
||||||
|
|
@ -171,7 +166,7 @@ func (q *Queries) GetInitialAssessmentSet(ctx context.Context) (QuestionSet, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many
|
const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many
|
||||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, display_order, intro_video_url
|
||||||
FROM question_sets
|
FROM question_sets
|
||||||
WHERE owner_type = $1
|
WHERE owner_type = $1
|
||||||
AND owner_id = $2
|
AND owner_id = $2
|
||||||
|
|
@ -208,7 +203,6 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -223,7 +217,7 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetQuestionSetByID = `-- name: GetQuestionSetByID :one
|
const GetQuestionSetByID = `-- name: GetQuestionSetByID :one
|
||||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, display_order, intro_video_url
|
||||||
FROM question_sets
|
FROM question_sets
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -246,7 +240,6 @@ func (q *Queries) GetQuestionSetByID(ctx context.Context, id int64) (QuestionSet
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
)
|
)
|
||||||
|
|
@ -254,7 +247,7 @@ func (q *Queries) GetQuestionSetByID(ctx context.Context, id int64) (QuestionSet
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many
|
const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many
|
||||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, display_order, intro_video_url
|
||||||
FROM question_sets
|
FROM question_sets
|
||||||
WHERE owner_type = $1
|
WHERE owner_type = $1
|
||||||
AND owner_id = $2
|
AND owner_id = $2
|
||||||
|
|
@ -291,7 +284,6 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -308,7 +300,7 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet
|
||||||
const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many
|
const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) OVER () AS total_count,
|
COUNT(*) OVER () AS total_count,
|
||||||
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order, qs.intro_video_url
|
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.display_order, qs.intro_video_url
|
||||||
FROM question_sets qs
|
FROM question_sets qs
|
||||||
WHERE set_type = $1
|
WHERE set_type = $1
|
||||||
AND status != 'ARCHIVED'
|
AND status != 'ARCHIVED'
|
||||||
|
|
@ -339,7 +331,6 @@ type GetQuestionSetsByTypeRow struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +360,6 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets
|
||||||
&i.Status,
|
&i.Status,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
&i.IntroVideoUrl,
|
&i.IntroVideoUrl,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|
@ -383,42 +373,6 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetSubCourseInitialAssessmentSet = `-- name: GetSubCourseInitialAssessmentSet :one
|
|
||||||
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url
|
|
||||||
FROM question_sets
|
|
||||||
WHERE set_type = 'INITIAL_ASSESSMENT'
|
|
||||||
AND owner_type = 'SUB_COURSE'
|
|
||||||
AND owner_id = $1
|
|
||||||
AND status = 'PUBLISHED'
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT 1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) GetSubCourseInitialAssessmentSet(ctx context.Context, ownerID pgtype.Int8) (QuestionSet, error) {
|
|
||||||
row := q.db.QueryRow(ctx, GetSubCourseInitialAssessmentSet, ownerID)
|
|
||||||
var i QuestionSet
|
|
||||||
err := row.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.Title,
|
|
||||||
&i.Description,
|
|
||||||
&i.SetType,
|
|
||||||
&i.OwnerType,
|
|
||||||
&i.OwnerID,
|
|
||||||
&i.BannerImage,
|
|
||||||
&i.Persona,
|
|
||||||
&i.TimeLimitMinutes,
|
|
||||||
&i.PassingScore,
|
|
||||||
&i.ShuffleQuestions,
|
|
||||||
&i.Status,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.SubCourseVideoID,
|
|
||||||
&i.DisplayOrder,
|
|
||||||
&i.IntroVideoUrl,
|
|
||||||
)
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
|
|
||||||
const GetUserPersonasByQuestionSetID = `-- name: GetUserPersonasByQuestionSetID :many
|
const GetUserPersonasByQuestionSetID = `-- name: GetUserPersonasByQuestionSetID :many
|
||||||
SELECT
|
SELECT
|
||||||
u.id,
|
u.id,
|
||||||
|
|
@ -519,9 +473,8 @@ SET
|
||||||
shuffle_questions = COALESCE($7, shuffle_questions),
|
shuffle_questions = COALESCE($7, shuffle_questions),
|
||||||
status = COALESCE($8, status),
|
status = COALESCE($8, status),
|
||||||
intro_video_url = COALESCE($9, intro_video_url),
|
intro_video_url = COALESCE($9, intro_video_url),
|
||||||
sub_course_video_id = COALESCE($10, sub_course_video_id),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $11
|
WHERE id = $10
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateQuestionSetParams struct {
|
type UpdateQuestionSetParams struct {
|
||||||
|
|
@ -534,7 +487,6 @@ type UpdateQuestionSetParams struct {
|
||||||
ShuffleQuestions bool `json:"shuffle_questions"`
|
ShuffleQuestions bool `json:"shuffle_questions"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
IntroVideoUrl pgtype.Text `json:"intro_video_url"`
|
||||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -549,26 +501,7 @@ func (q *Queries) UpdateQuestionSet(ctx context.Context, arg UpdateQuestionSetPa
|
||||||
arg.ShuffleQuestions,
|
arg.ShuffleQuestions,
|
||||||
arg.Status,
|
arg.Status,
|
||||||
arg.IntroVideoUrl,
|
arg.IntroVideoUrl,
|
||||||
arg.SubCourseVideoID,
|
|
||||||
arg.ID,
|
arg.ID,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateQuestionSetVideoLink = `-- name: UpdateQuestionSetVideoLink :exec
|
|
||||||
UPDATE question_sets
|
|
||||||
SET
|
|
||||||
sub_course_video_id = $1,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2
|
|
||||||
`
|
|
||||||
|
|
||||||
type UpdateQuestionSetVideoLinkParams struct {
|
|
||||||
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (q *Queries) UpdateQuestionSetVideoLink(ctx context.Context, arg UpdateQuestionSetVideoLinkParams) error {
|
|
||||||
_, err := q.db.Exec(ctx, UpdateQuestionSetVideoLink, arg.SubCourseVideoID, arg.ID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,18 @@ const (
|
||||||
ActionIssueCreated ActivityAction = "ISSUE_CREATED"
|
ActionIssueCreated ActivityAction = "ISSUE_CREATED"
|
||||||
ActionIssueStatusUpdated ActivityAction = "ISSUE_STATUS_UPDATED"
|
ActionIssueStatusUpdated ActivityAction = "ISSUE_STATUS_UPDATED"
|
||||||
ActionIssueDeleted ActivityAction = "ISSUE_DELETED"
|
ActionIssueDeleted ActivityAction = "ISSUE_DELETED"
|
||||||
|
ActionProgramCreated ActivityAction = "PROGRAM_CREATED"
|
||||||
|
ActionProgramUpdated ActivityAction = "PROGRAM_UPDATED"
|
||||||
|
ActionProgramDeleted ActivityAction = "PROGRAM_DELETED"
|
||||||
|
ActionModuleCreated ActivityAction = "MODULE_CREATED"
|
||||||
|
ActionModuleUpdated ActivityAction = "MODULE_UPDATED"
|
||||||
|
ActionModuleDeleted ActivityAction = "MODULE_DELETED"
|
||||||
|
ActionLessonCreated ActivityAction = "LESSON_CREATED"
|
||||||
|
ActionLessonUpdated ActivityAction = "LESSON_UPDATED"
|
||||||
|
ActionLessonDeleted ActivityAction = "LESSON_DELETED"
|
||||||
|
ActionPracticeCreated ActivityAction = "PRACTICE_CREATED"
|
||||||
|
ActionPracticeUpdated ActivityAction = "PRACTICE_UPDATED"
|
||||||
|
ActionPracticeDeleted ActivityAction = "PRACTICE_DELETED"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceType string
|
type ResourceType string
|
||||||
|
|
@ -62,6 +74,10 @@ const (
|
||||||
ResourceQuestion ResourceType = "QUESTION"
|
ResourceQuestion ResourceType = "QUESTION"
|
||||||
ResourceQuestionSet ResourceType = "QUESTION_SET"
|
ResourceQuestionSet ResourceType = "QUESTION_SET"
|
||||||
ResourceIssue ResourceType = "ISSUE"
|
ResourceIssue ResourceType = "ISSUE"
|
||||||
|
ResourceProgram ResourceType = "PROGRAM"
|
||||||
|
ResourceModule ResourceType = "MODULE"
|
||||||
|
ResourceLesson ResourceType = "LESSON"
|
||||||
|
ResourcePractice ResourceType = "PRACTICE"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActivityLog struct {
|
type ActivityLog struct {
|
||||||
|
|
|
||||||
33
internal/domain/course.go
Normal file
33
internal/domain/course.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// DefaultCEFRCourseNames are the standard course names seeded for each program (migration 000048).
|
||||||
|
// Creating a course via the API may use any of these or a custom name.
|
||||||
|
var DefaultCEFRCourseNames = []string{"A1", "A2", "B1", "B2", "C1", "C2"}
|
||||||
|
|
||||||
|
// Course belongs to a Program.
|
||||||
|
type Course struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_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"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCourseInput struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateCourseInput struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
}
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type SubCourseLevel string
|
|
||||||
|
|
||||||
const (
|
|
||||||
SubCourseLevelBeginner SubCourseLevel = "BEGINNER"
|
|
||||||
SubCourseLevelIntermediate SubCourseLevel = "INTERMEDIATE"
|
|
||||||
SubCourseLevelAdvanced SubCourseLevel = "ADVANCED"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SubCourseSubLevel string
|
|
||||||
|
|
||||||
const (
|
|
||||||
SubCourseSubLevelA1 SubCourseSubLevel = "A1"
|
|
||||||
SubCourseSubLevelA2 SubCourseSubLevel = "A2"
|
|
||||||
SubCourseSubLevelA3 SubCourseSubLevel = "A3"
|
|
||||||
SubCourseSubLevelB1 SubCourseSubLevel = "B1"
|
|
||||||
SubCourseSubLevelB2 SubCourseSubLevel = "B2"
|
|
||||||
SubCourseSubLevelB3 SubCourseSubLevel = "B3"
|
|
||||||
SubCourseSubLevelC1 SubCourseSubLevel = "C1"
|
|
||||||
SubCourseSubLevelC2 SubCourseSubLevel = "C2"
|
|
||||||
SubCourseSubLevelC3 SubCourseSubLevel = "C3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ContentStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ContentStatusDraft ContentStatus = "DRAFT"
|
|
||||||
ContentStatusPublished ContentStatus = "PUBLISHED"
|
|
||||||
ContentStatusInactive ContentStatus = "INACTIVE"
|
|
||||||
ContentStatusArchived ContentStatus = "ARCHIVED"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TreeSubCourse struct {
|
|
||||||
ID int64
|
|
||||||
Title string
|
|
||||||
Level string
|
|
||||||
SubLevel string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TreeCourse struct {
|
|
||||||
ID int64
|
|
||||||
Title string
|
|
||||||
SubCourses []TreeSubCourse
|
|
||||||
}
|
|
||||||
|
|
||||||
type CourseCategory struct {
|
|
||||||
ID int64
|
|
||||||
Name string
|
|
||||||
IsActive bool
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type Course struct {
|
|
||||||
ID int64
|
|
||||||
CategoryID int64
|
|
||||||
Title string
|
|
||||||
Description *string
|
|
||||||
Thumbnail *string
|
|
||||||
IntroVideoURL *string
|
|
||||||
IsActive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCourse struct {
|
|
||||||
ID int64
|
|
||||||
CourseID int64
|
|
||||||
Title string
|
|
||||||
Description *string
|
|
||||||
Thumbnail *string
|
|
||||||
DisplayOrder int32
|
|
||||||
Level string
|
|
||||||
SubLevel string
|
|
||||||
IsActive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCourseVideo struct {
|
|
||||||
ID int64
|
|
||||||
SubCourseID int64
|
|
||||||
Title string
|
|
||||||
Description *string
|
|
||||||
VideoURL string
|
|
||||||
Duration int32
|
|
||||||
Resolution *string
|
|
||||||
InstructorID *string
|
|
||||||
Thumbnail *string
|
|
||||||
Visibility *string
|
|
||||||
DisplayOrder int32
|
|
||||||
IsPublished bool
|
|
||||||
PublishDate *time.Time
|
|
||||||
Status string
|
|
||||||
// Vimeo-specific fields
|
|
||||||
VimeoID *string
|
|
||||||
VimeoEmbedURL *string
|
|
||||||
VimeoPlayerHTML *string
|
|
||||||
VimeoStatus *string
|
|
||||||
}
|
|
||||||
|
|
||||||
type VideoHostProvider string
|
|
||||||
|
|
||||||
const (
|
|
||||||
VideoHostProviderDirect VideoHostProvider = "DIRECT"
|
|
||||||
VideoHostProviderVimeo VideoHostProvider = "VIMEO"
|
|
||||||
)
|
|
||||||
|
|
||||||
type VideoAccessBlock struct {
|
|
||||||
VideoID int64
|
|
||||||
Title string
|
|
||||||
DisplayOrder int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Learning Path types — full nested structure for a course
|
|
||||||
type LearningPathVideo struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
VideoURL string `json:"video_url"`
|
|
||||||
Duration int32 `json:"duration"`
|
|
||||||
Resolution *string `json:"resolution,omitempty"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
VimeoID *string `json:"vimeo_id,omitempty"`
|
|
||||||
VimeoEmbedURL *string `json:"vimeo_embed_url,omitempty"`
|
|
||||||
VideoHostProvider *string `json:"video_host_provider,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LearningPathPractice struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Persona *string `json:"persona,omitempty"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
IntroVideoURL *string `json:"intro_video_url,omitempty"`
|
|
||||||
QuestionCount int64 `json:"question_count"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LearningPathPrerequisite struct {
|
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Level string `json:"level"`
|
|
||||||
SubLevel string `json:"sub_level"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LearningPathSubCourse struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
Tips *string `json:"tips,omitempty"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
Level string `json:"level"`
|
|
||||||
SubLevel string `json:"sub_level"`
|
|
||||||
PrerequisiteCount int64 `json:"prerequisite_count"`
|
|
||||||
VideoCount int64 `json:"video_count"`
|
|
||||||
PracticeCount int64 `json:"practice_count"`
|
|
||||||
Prerequisites []LearningPathPrerequisite `json:"prerequisites"`
|
|
||||||
Videos []LearningPathVideo `json:"videos"`
|
|
||||||
Practices []LearningPathPractice `json:"practices"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LearningPath struct {
|
|
||||||
CourseID int64 `json:"course_id"`
|
|
||||||
CourseTitle string `json:"course_title"`
|
|
||||||
Description *string `json:"description,omitempty"`
|
|
||||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
|
||||||
IntroVideoURL *string `json:"intro_video_url,omitempty"`
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
CategoryName string `json:"category_name"`
|
|
||||||
SubCourses []LearningPathSubCourse `json:"sub_courses"`
|
|
||||||
}
|
|
||||||
32
internal/domain/lesson.go
Normal file
32
internal/domain/lesson.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Lesson belongs to a Module.
|
||||||
|
type Lesson struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ModuleID int64 `json:"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"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateLessonInput struct {
|
||||||
|
Title string `json:"title" validate:"required"`
|
||||||
|
VideoURL *string `json:"video_url,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateLessonInput 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"`
|
||||||
|
}
|
||||||
22
internal/domain/lms_access.go
Normal file
22
internal/domain/lms_access.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
// LMSEntityAccess describes learner gating for a program, course, module, or lesson.
|
||||||
|
// It is omitted (nil) for non-learner roles in API responses.
|
||||||
|
// Progress fields count completed lessons vs total lessons in that entity’s scope (lesson: 0 or 1 of 1).
|
||||||
|
type LMSEntityAccess struct {
|
||||||
|
IsAccessible bool `json:"is_accessible"`
|
||||||
|
IsCompleted bool `json:"is_completed"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
CompletedCount int `json:"completed_count"`
|
||||||
|
TotalCount int `json:"total_count"`
|
||||||
|
ProgressPercent int `json:"progress_percent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LMSUserProgress lists entity IDs the authenticated user has fully completed
|
||||||
|
// (lessons as marked complete; module/course/program when rollup conditions were met).
|
||||||
|
type LMSUserProgress struct {
|
||||||
|
LessonIDs []int64 `json:"lesson_ids"`
|
||||||
|
ModuleIDs []int64 `json:"module_ids"`
|
||||||
|
CourseIDs []int64 `json:"course_ids"`
|
||||||
|
ProgramIDs []int64 `json:"program_ids"`
|
||||||
|
}
|
||||||
30
internal/domain/module.go
Normal file
30
internal/domain/module.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Module belongs to a Course. program_id is the course’s program (stored for querying; not required from the client on create).
|
||||||
|
type Module struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ProgramID int64 `json:"program_id"`
|
||||||
|
CourseID int64 `json:"course_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Icon *string `json:"icon,omitempty"`
|
||||||
|
SortOrder int `json:"sort_order"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateModuleInput struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Icon *string `json:"icon,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateModuleInput struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Icon *string `json:"icon,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
}
|
||||||
47
internal/domain/practice.go
Normal file
47
internal/domain/practice.go
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ParentKind identifies which hierarchy entity owns a practice (exactly one).
|
||||||
|
type ParentKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ParentKindCourse ParentKind = "COURSE"
|
||||||
|
ParentKindModule ParentKind = "MODULE"
|
||||||
|
ParentKindLesson ParentKind = "LESSON"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Practice is question-set content (story, persona, tips) scoped to a course, module, or lesson.
|
||||||
|
type Practice struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ParentKind ParentKind `json:"parent_kind"`
|
||||||
|
ParentID int64 `json:"parent_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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePracticeInput struct {
|
||||||
|
ParentKind ParentKind `json:"parent_kind" validate:"required,oneof=COURSE MODULE LESSON"`
|
||||||
|
ParentID int64 `json:"parent_id" validate:"required,gt=0"`
|
||||||
|
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 UpdatePracticeInput 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"`
|
||||||
|
}
|
||||||
28
internal/domain/program.go
Normal file
28
internal/domain/program.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Program is the top-level container in the LMS hierarchy (e.g. tracks like Beginner / Intermediate / Advanced).
|
||||||
|
type Program 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"`
|
||||||
|
Access *LMSEntityAccess `json:"access,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateProgramInput struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateProgramInput struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
}
|
||||||
|
|
@ -1,84 +0,0 @@
|
||||||
package domain
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrPrerequisiteNotMet = errors.New("prerequisites not completed")
|
|
||||||
ErrProgressNotFound = errors.New("progress record not found")
|
|
||||||
ErrPrerequisiteExists = errors.New("prerequisite already exists")
|
|
||||||
ErrSelfPrerequisite = errors.New("sub-course cannot be its own prerequisite")
|
|
||||||
ErrSubCourseAlreadyStarted = errors.New("sub-course already started")
|
|
||||||
)
|
|
||||||
|
|
||||||
type SubCoursePrerequisite struct {
|
|
||||||
ID int64
|
|
||||||
SubCourseID int64
|
|
||||||
PrerequisiteSubCourseID int64
|
|
||||||
PrerequisiteTitle string
|
|
||||||
PrerequisiteLevel string
|
|
||||||
PrerequisiteDisplayOrder int32
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCourseDependent struct {
|
|
||||||
ID int64
|
|
||||||
SubCourseID int64
|
|
||||||
PrerequisiteSubCourseID int64
|
|
||||||
DependentTitle string
|
|
||||||
DependentLevel string
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProgressStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ProgressStatusNotStarted ProgressStatus = "NOT_STARTED"
|
|
||||||
ProgressStatusInProgress ProgressStatus = "IN_PROGRESS"
|
|
||||||
ProgressStatusCompleted ProgressStatus = "COMPLETED"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserSubCourseProgress struct {
|
|
||||||
ID int64
|
|
||||||
UserID int64
|
|
||||||
SubCourseID int64
|
|
||||||
Status ProgressStatus
|
|
||||||
ProgressPercentage int16
|
|
||||||
StartedAt *time.Time
|
|
||||||
CompletedAt *time.Time
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt *time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type SubCourseWithProgress struct {
|
|
||||||
SubCourseID int64
|
|
||||||
Title string
|
|
||||||
Description *string
|
|
||||||
Thumbnail *string
|
|
||||||
DisplayOrder int32
|
|
||||||
Level string
|
|
||||||
IsActive bool
|
|
||||||
ProgressStatus ProgressStatus
|
|
||||||
ProgressPercentage int16
|
|
||||||
StartedAt *time.Time
|
|
||||||
CompletedAt *time.Time
|
|
||||||
UnmetPrerequisitesCount int64
|
|
||||||
IsLocked bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserCourseProgressItem struct {
|
|
||||||
ID int64
|
|
||||||
UserID int64
|
|
||||||
SubCourseID int64
|
|
||||||
Status ProgressStatus
|
|
||||||
ProgressPercentage int16
|
|
||||||
StartedAt *time.Time
|
|
||||||
CompletedAt *time.Time
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt *time.Time
|
|
||||||
SubCourseTitle string
|
|
||||||
SubCourseLevel string
|
|
||||||
SubCourseDisplayOrder int32
|
|
||||||
}
|
|
||||||
|
|
@ -104,7 +104,6 @@ type QuestionSet struct {
|
||||||
PassingScore *int32
|
PassingScore *int32
|
||||||
ShuffleQuestions bool
|
ShuffleQuestions bool
|
||||||
Status string
|
Status string
|
||||||
SubCourseVideoID *int64
|
|
||||||
IntroVideoURL *string
|
IntroVideoURL *string
|
||||||
UserPersonas []UserPersona
|
UserPersonas []UserPersona
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
|
@ -173,7 +172,6 @@ type CreateQuestionSetInput struct {
|
||||||
PassingScore *int32
|
PassingScore *int32
|
||||||
ShuffleQuestions *bool
|
ShuffleQuestions *bool
|
||||||
Status *string
|
Status *string
|
||||||
SubCourseVideoID *int64
|
|
||||||
IntroVideoURL *string
|
IntroVideoURL *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
46
internal/domain/reorder.go
Normal file
46
internal/domain/reorder.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrReorderInvalidIDSet means ordered_ids is not an exact permutation of the current entities in scope.
|
||||||
|
var ErrReorderInvalidIDSet = errors.New("ordered_ids must list every id in this scope exactly once, with no duplicates")
|
||||||
|
|
||||||
|
// ReorderIDsRequest is the body for batch reorder endpoints (drag-and-drop UI).
|
||||||
|
// Send "ordered_ids": [] in display order. Must include every id in that scope (use GET list) when there is at least one entity.
|
||||||
|
type ReorderIDsRequest struct {
|
||||||
|
OrderedIDs []int64 `json:"ordered_ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateReorderPermutation checks that ordered contains the same multiset of ids as expected (new order vs current scope).
|
||||||
|
func ValidateReorderPermutation(ordered, expected []int64) error {
|
||||||
|
if len(expected) == 0 {
|
||||||
|
if len(ordered) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%w: no entities exist in this scope", ErrReorderInvalidIDSet)
|
||||||
|
}
|
||||||
|
if len(ordered) != len(expected) {
|
||||||
|
return fmt.Errorf("%w: want %d ids, got %d", ErrReorderInvalidIDSet, len(expected), len(ordered))
|
||||||
|
}
|
||||||
|
seen := make(map[int64]struct{}, len(ordered))
|
||||||
|
for _, id := range ordered {
|
||||||
|
if _, dup := seen[id]; dup {
|
||||||
|
return fmt.Errorf("%w: duplicate id %d", ErrReorderInvalidIDSet, id)
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
}
|
||||||
|
a := append([]int64(nil), expected...)
|
||||||
|
b := append([]int64(nil), ordered...)
|
||||||
|
sort.Slice(a, func(i, j int) bool { return a[i] < a[j] })
|
||||||
|
sort.Slice(b, func(i, j int) bool { return b[i] < b[j] })
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return fmt.Errorf("%w: id set does not match current scope", ErrReorderInvalidIDSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -1,216 +0,0 @@
|
||||||
package ports
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CourseStore interface {
|
|
||||||
// Course Categories
|
|
||||||
CreateCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
name string,
|
|
||||||
) (domain.CourseCategory, error)
|
|
||||||
GetCourseCategoryByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.CourseCategory, error)
|
|
||||||
GetAllCourseCategories(
|
|
||||||
ctx context.Context,
|
|
||||||
limit int32,
|
|
||||||
offset int32,
|
|
||||||
) ([]domain.CourseCategory, int64, error)
|
|
||||||
UpdateCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
name *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error
|
|
||||||
DeleteCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
|
|
||||||
// Courses
|
|
||||||
CreateCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
categoryID int64,
|
|
||||||
title string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
introVideoURL *string,
|
|
||||||
) (domain.Course, error)
|
|
||||||
GetCourseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.Course, error)
|
|
||||||
GetCoursesByCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
categoryID int64,
|
|
||||||
limit int32,
|
|
||||||
offset int32,
|
|
||||||
) ([]domain.Course, int64, error)
|
|
||||||
UpdateCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
introVideoURL *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error
|
|
||||||
DeleteCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
|
|
||||||
// Sub-courses
|
|
||||||
CreateSubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
courseID int64,
|
|
||||||
title string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
displayOrder *int32,
|
|
||||||
level string,
|
|
||||||
subLevel string,
|
|
||||||
) (domain.SubCourse, error)
|
|
||||||
GetSubCourseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.SubCourse, error)
|
|
||||||
GetSubCoursesByCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
courseID int64,
|
|
||||||
) ([]domain.SubCourse, int64, error)
|
|
||||||
ListSubCoursesByCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
courseID int64,
|
|
||||||
) ([]domain.SubCourse, error)
|
|
||||||
ListActiveSubCourses(
|
|
||||||
ctx context.Context,
|
|
||||||
) ([]domain.SubCourse, error)
|
|
||||||
UpdateSubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
displayOrder *int32,
|
|
||||||
level *string,
|
|
||||||
subLevel *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error
|
|
||||||
DeactivateSubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
DeleteSubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.SubCourse, error)
|
|
||||||
|
|
||||||
// Sub-course Videos
|
|
||||||
CreateSubCourseVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
subCourseID int64,
|
|
||||||
title string,
|
|
||||||
description *string,
|
|
||||||
videoURL string,
|
|
||||||
duration int32,
|
|
||||||
resolution *string,
|
|
||||||
instructorID *string,
|
|
||||||
thumbnail *string,
|
|
||||||
visibility *string,
|
|
||||||
displayOrder *int32,
|
|
||||||
status *string,
|
|
||||||
vimeoID *string,
|
|
||||||
vimeoEmbedURL *string,
|
|
||||||
vimeoPlayerHTML *string,
|
|
||||||
vimeoStatus *string,
|
|
||||||
videoHostProvider *string,
|
|
||||||
) (domain.SubCourseVideo, error)
|
|
||||||
GetSubCourseVideoByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.SubCourseVideo, error)
|
|
||||||
GetVideosBySubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
subCourseID int64,
|
|
||||||
) ([]domain.SubCourseVideo, int64, error)
|
|
||||||
GetPublishedVideosBySubCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
subCourseID int64,
|
|
||||||
) ([]domain.SubCourseVideo, error)
|
|
||||||
GetFirstIncompletePreviousVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
userID int64,
|
|
||||||
videoID int64,
|
|
||||||
) (*domain.VideoAccessBlock, error)
|
|
||||||
MarkVideoCompleted(
|
|
||||||
ctx context.Context,
|
|
||||||
userID int64,
|
|
||||||
videoID int64,
|
|
||||||
) error
|
|
||||||
PublishSubCourseVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
videoID int64,
|
|
||||||
) error
|
|
||||||
UpdateSubCourseVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
videoURL *string,
|
|
||||||
duration *int32,
|
|
||||||
resolution *string,
|
|
||||||
visibility *string,
|
|
||||||
thumbnail *string,
|
|
||||||
displayOrder *int32,
|
|
||||||
status *string,
|
|
||||||
) error
|
|
||||||
ArchiveSubCourseVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
DeleteSubCourseVideo(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
|
|
||||||
// Vimeo integration
|
|
||||||
UpdateVimeoStatus(ctx context.Context, videoID int64, status string) error
|
|
||||||
GetVideoByVimeoID(ctx context.Context, vimeoID string) (domain.SubCourseVideo, error)
|
|
||||||
|
|
||||||
// Learning Tree
|
|
||||||
GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error)
|
|
||||||
|
|
||||||
// Learning Path (full nested structure for a course)
|
|
||||||
GetCourseLearningPath(ctx context.Context, courseID int64) (domain.LearningPath, error)
|
|
||||||
|
|
||||||
// Reorder (drag-and-drop support)
|
|
||||||
ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error
|
|
||||||
ReorderCourses(ctx context.Context, ids []int64, positions []int32) error
|
|
||||||
ReorderSubCourses(ctx context.Context, ids []int64, positions []int32) error
|
|
||||||
ReorderSubCourseVideos(ctx context.Context, ids []int64, positions []int32) error
|
|
||||||
ReorderQuestionSets(ctx context.Context, ids []int64, positions []int32) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProgressionStore interface {
|
|
||||||
// Prerequisites (admin)
|
|
||||||
AddSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error
|
|
||||||
RemoveSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error
|
|
||||||
GetSubCoursePrerequisites(ctx context.Context, subCourseID int64) ([]domain.SubCoursePrerequisite, error)
|
|
||||||
GetSubCourseDependents(ctx context.Context, prerequisiteSubCourseID int64) ([]domain.SubCourseDependent, error)
|
|
||||||
CountUnmetPrerequisites(ctx context.Context, subCourseID, userID int64) (int64, error)
|
|
||||||
DeleteAllPrerequisitesForSubCourse(ctx context.Context, subCourseID int64) error
|
|
||||||
|
|
||||||
// User progress
|
|
||||||
StartSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error)
|
|
||||||
UpdateSubCourseProgress(ctx context.Context, userID, subCourseID int64, percentage int16) error
|
|
||||||
CompleteSubCourse(ctx context.Context, userID, subCourseID int64) error
|
|
||||||
RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error
|
|
||||||
GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error)
|
|
||||||
GetUserCourseProgress(ctx context.Context, userID, courseID int64) ([]domain.UserCourseProgressItem, error)
|
|
||||||
GetSubCoursesWithProgressByCourse(ctx context.Context, userID, courseID int64) ([]domain.SubCourseWithProgress, error)
|
|
||||||
}
|
|
||||||
16
internal/ports/lms_course.go
Normal file
16
internal/ports/lms_course.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CourseStore interface {
|
||||||
|
CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error)
|
||||||
|
GetCourseByID(ctx context.Context, id int64) (domain.Course, error)
|
||||||
|
ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error)
|
||||||
|
ListCourseIDsByProgram(ctx context.Context, programID int64) ([]int64, error)
|
||||||
|
ReorderCoursesInProgram(ctx context.Context, programID int64, orderedIDs []int64) error
|
||||||
|
UpdateCourse(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error)
|
||||||
|
DeleteCourse(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
14
internal/ports/lms_lesson.go
Normal file
14
internal/ports/lms_lesson.go
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LessonStore interface {
|
||||||
|
CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error)
|
||||||
|
GetLessonByID(ctx context.Context, id int64) (domain.Lesson, error)
|
||||||
|
ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error)
|
||||||
|
UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error)
|
||||||
|
DeleteLesson(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
16
internal/ports/lms_module.go
Normal file
16
internal/ports/lms_module.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ModuleStore interface {
|
||||||
|
CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error)
|
||||||
|
GetModuleByID(ctx context.Context, id int64) (domain.Module, error)
|
||||||
|
ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error)
|
||||||
|
ListModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error)
|
||||||
|
ReorderModulesInCourse(ctx context.Context, courseID int64, orderedIDs []int64) error
|
||||||
|
UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error)
|
||||||
|
DeleteModule(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
31
internal/ports/lms_practice.go
Normal file
31
internal/ports/lms_practice.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// QuestionSetByID is implemented by the questions store.
|
||||||
|
type QuestionSetByID interface {
|
||||||
|
GetQuestionSetByID(ctx context.Context, id int64) (domain.QuestionSet, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserByID is implemented by the user store.
|
||||||
|
type UserByID interface {
|
||||||
|
GetUserByID(ctx context.Context, id int64) (domain.User, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LmsPracticeStore interface {
|
||||||
|
// courseID, moduleID, lessonID: exactly one non-nil, matching in.ParentKind / in.ParentID.
|
||||||
|
CreateLmsPractice(
|
||||||
|
ctx context.Context,
|
||||||
|
in domain.CreatePracticeInput,
|
||||||
|
courseID, moduleID, lessonID *int64,
|
||||||
|
) (domain.Practice, error)
|
||||||
|
GetLmsPracticeByID(ctx context.Context, id int64) (domain.Practice, error)
|
||||||
|
ListLmsPracticesByCourseID(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||||
|
ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||||
|
ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||||
|
UpdateLmsPractice(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error)
|
||||||
|
DeleteLmsPractice(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
16
internal/ports/program.go
Normal file
16
internal/ports/program.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProgramStore interface {
|
||||||
|
CreateProgram(ctx context.Context, input domain.CreateProgramInput) (domain.Program, error)
|
||||||
|
GetProgramByID(ctx context.Context, id int64) (domain.Program, error)
|
||||||
|
ListPrograms(ctx context.Context, limit, offset int32) ([]domain.Program, int64, error)
|
||||||
|
ListAllProgramIDs(ctx context.Context) ([]int64, error)
|
||||||
|
ReorderPrograms(ctx context.Context, orderedIDs []int64) error
|
||||||
|
UpdateProgram(ctx context.Context, id int64, input domain.UpdateProgramInput) (domain.Program, error)
|
||||||
|
DeleteProgram(ctx context.Context, id int64) error
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,6 @@ type QuestionStore interface {
|
||||||
GetQuestionSetsByType(ctx context.Context, setType string, limit, offset int32) ([]domain.QuestionSet, int64, error)
|
GetQuestionSetsByType(ctx context.Context, setType string, limit, offset int32) ([]domain.QuestionSet, int64, error)
|
||||||
GetPublishedQuestionSetsByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.QuestionSet, error)
|
GetPublishedQuestionSetsByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.QuestionSet, error)
|
||||||
GetInitialAssessmentSet(ctx context.Context) (domain.QuestionSet, error)
|
GetInitialAssessmentSet(ctx context.Context) (domain.QuestionSet, error)
|
||||||
GetSubCourseInitialAssessmentSet(ctx context.Context, subCourseID int64) (domain.QuestionSet, error)
|
|
||||||
GetFirstIncompletePreviousPractice(ctx context.Context, userID int64, questionSetID int64) (*domain.PracticeAccessBlock, error)
|
GetFirstIncompletePreviousPractice(ctx context.Context, userID int64, questionSetID int64) (*domain.PracticeAccessBlock, error)
|
||||||
MarkPracticeCompleted(ctx context.Context, userID int64, questionSetID int64) error
|
MarkPracticeCompleted(ctx context.Context, userID int64, questionSetID int64) error
|
||||||
UpdateQuestionSet(ctx context.Context, id int64, input domain.CreateQuestionSetInput) error
|
UpdateQuestionSet(ctx context.Context, id int64, input domain.CreateQuestionSetInput) error
|
||||||
|
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewCourseStore(s *Store) *Store { return s }
|
|
||||||
func NewProgressionStore(s *Store) *Store { return s }
|
|
||||||
|
|
||||||
func (s *Store) CreateCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
name string,
|
|
||||||
) (domain.CourseCategory, error) {
|
|
||||||
|
|
||||||
row, err := s.queries.CreateCourseCategory(ctx, dbgen.CreateCourseCategoryParams{
|
|
||||||
Name: name,
|
|
||||||
Column2: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return domain.CourseCategory{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain.CourseCategory{
|
|
||||||
ID: row.ID,
|
|
||||||
Name: row.Name,
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
CreatedAt: row.CreatedAt.Time,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetCourseCategoryByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.CourseCategory, error) {
|
|
||||||
|
|
||||||
row, err := s.queries.GetCourseCategoryByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return domain.CourseCategory{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return domain.CourseCategory{
|
|
||||||
ID: row.ID,
|
|
||||||
Name: row.Name,
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
CreatedAt: row.CreatedAt.Time,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetAllCourseCategories(
|
|
||||||
ctx context.Context,
|
|
||||||
limit int32,
|
|
||||||
offset int32,
|
|
||||||
) ([]domain.CourseCategory, int64, error) {
|
|
||||||
|
|
||||||
rows, err := s.queries.GetAllCourseCategories(ctx, dbgen.GetAllCourseCategoriesParams{
|
|
||||||
Limit: pgtype.Int4{Int32: limit},
|
|
||||||
Offset: pgtype.Int4{Int32: offset},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
categories []domain.CourseCategory
|
|
||||||
totalCount int64
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, row := range rows {
|
|
||||||
if i == 0 {
|
|
||||||
totalCount = row.TotalCount
|
|
||||||
}
|
|
||||||
|
|
||||||
categories = append(categories, domain.CourseCategory{
|
|
||||||
ID: row.ID,
|
|
||||||
Name: row.Name,
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
CreatedAt: row.CreatedAt.Time,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return categories, totalCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
name *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error {
|
|
||||||
|
|
||||||
var (
|
|
||||||
nameVal string
|
|
||||||
isActiveVal bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if name != nil {
|
|
||||||
nameVal = *name
|
|
||||||
}
|
|
||||||
|
|
||||||
if isActive != nil {
|
|
||||||
isActiveVal = *isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.queries.UpdateCourseCategory(ctx, dbgen.UpdateCourseCategoryParams{
|
|
||||||
Name: nameVal,
|
|
||||||
IsActive: isActiveVal,
|
|
||||||
ID: id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteCourseCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error {
|
|
||||||
|
|
||||||
return s.queries.DeleteCourseCategory(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ReorderCourseCategories(ctx context.Context, ids []int64, positions []int32) error {
|
|
||||||
return s.queries.ReorderCourseCategories(ctx, dbgen.ReorderCourseCategoriesParams{
|
|
||||||
Ids: ids,
|
|
||||||
Positions: positions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
package repository
|
|
||||||
|
|
||||||
import (
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Store) CreateCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
categoryID int64,
|
|
||||||
title string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
introVideoURL *string,
|
|
||||||
) (domain.Course, error) {
|
|
||||||
var descVal, thumbVal, introVideoVal string
|
|
||||||
if description != nil {
|
|
||||||
descVal = *description
|
|
||||||
}
|
|
||||||
if thumbnail != nil {
|
|
||||||
thumbVal = *thumbnail
|
|
||||||
}
|
|
||||||
if introVideoURL != nil {
|
|
||||||
introVideoVal = *introVideoURL
|
|
||||||
}
|
|
||||||
|
|
||||||
row, err := s.queries.CreateCourse(ctx, dbgen.CreateCourseParams{
|
|
||||||
CategoryID: categoryID,
|
|
||||||
Title: title,
|
|
||||||
Description: pgtype.Text{String: descVal, Valid: description != nil},
|
|
||||||
Thumbnail: pgtype.Text{String: thumbVal, Valid: thumbnail != nil},
|
|
||||||
IntroVideoUrl: pgtype.Text{String: introVideoVal, Valid: introVideoURL != nil},
|
|
||||||
Column6: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return domain.Course{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapCourse(row), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetCourseByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.Course, error) {
|
|
||||||
|
|
||||||
row, err := s.queries.GetCourseByID(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return domain.Course{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapCourse(row), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetCoursesByCategory(
|
|
||||||
ctx context.Context,
|
|
||||||
categoryID int64,
|
|
||||||
limit int32,
|
|
||||||
offset int32,
|
|
||||||
) ([]domain.Course, int64, error) {
|
|
||||||
|
|
||||||
rows, err := s.queries.GetCoursesByCategory(ctx, dbgen.GetCoursesByCategoryParams{
|
|
||||||
CategoryID: categoryID,
|
|
||||||
Limit: pgtype.Int4{Int32: limit},
|
|
||||||
Offset: pgtype.Int4{Int32: offset},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
courses []domain.Course
|
|
||||||
totalCount int64
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, row := range rows {
|
|
||||||
if i == 0 {
|
|
||||||
totalCount = row.TotalCount
|
|
||||||
}
|
|
||||||
|
|
||||||
courses = append(courses, domain.Course{
|
|
||||||
ID: row.ID,
|
|
||||||
CategoryID: row.CategoryID,
|
|
||||||
Title: row.Title,
|
|
||||||
Description: ptrText(row.Description),
|
|
||||||
Thumbnail: ptrText(row.Thumbnail),
|
|
||||||
IntroVideoURL: ptrText(row.IntroVideoUrl),
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return courses, totalCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) UpdateCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
introVideoURL *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error {
|
|
||||||
var (
|
|
||||||
titleVal string
|
|
||||||
descriptionVal string
|
|
||||||
thumbnailVal string
|
|
||||||
introVideoVal string
|
|
||||||
isActiveVal bool
|
|
||||||
)
|
|
||||||
|
|
||||||
if title != nil {
|
|
||||||
titleVal = *title
|
|
||||||
}
|
|
||||||
if description != nil {
|
|
||||||
descriptionVal = *description
|
|
||||||
}
|
|
||||||
if thumbnail != nil {
|
|
||||||
thumbnailVal = *thumbnail
|
|
||||||
}
|
|
||||||
if introVideoURL != nil {
|
|
||||||
introVideoVal = *introVideoURL
|
|
||||||
}
|
|
||||||
if isActive != nil {
|
|
||||||
isActiveVal = *isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.queries.UpdateCourse(ctx, dbgen.UpdateCourseParams{
|
|
||||||
Title: titleVal,
|
|
||||||
Description: pgtype.Text{String: descriptionVal, Valid: description != nil},
|
|
||||||
Thumbnail: pgtype.Text{String: thumbnailVal, Valid: thumbnail != nil},
|
|
||||||
IntroVideoUrl: pgtype.Text{String: introVideoVal, Valid: introVideoURL != nil},
|
|
||||||
IsActive: isActiveVal,
|
|
||||||
ID: id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) DeleteCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error {
|
|
||||||
|
|
||||||
return s.queries.DeleteCourse(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapCourse(row dbgen.Course) domain.Course {
|
|
||||||
return domain.Course{
|
|
||||||
ID: row.ID,
|
|
||||||
CategoryID: row.CategoryID,
|
|
||||||
Title: row.Title,
|
|
||||||
Description: ptrText(row.Description),
|
|
||||||
Thumbnail: ptrText(row.Thumbnail),
|
|
||||||
IntroVideoURL: ptrText(row.IntroVideoUrl),
|
|
||||||
IsActive: row.IsActive,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) ReorderCourses(ctx context.Context, ids []int64, positions []int32) error {
|
|
||||||
return s.queries.ReorderCourses(ctx, dbgen.ReorderCoursesParams{
|
|
||||||
Ids: ids,
|
|
||||||
Positions: positions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func ptrText(t pgtype.Text) *string {
|
|
||||||
if t.Valid {
|
|
||||||
return &t.String
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
87
internal/repository/lms_access.go
Normal file
87
internal/repository/lms_access.go
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Store) LmsGetPreviousProgram(ctx context.Context, programID int64) (dbgen.Program, error) {
|
||||||
|
return s.queries.GetPreviousProgram(ctx, programID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsGetPreviousCourseInProgram(ctx context.Context, courseID int64) (dbgen.Course, error) {
|
||||||
|
return s.queries.GetPreviousCourseInProgram(ctx, courseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsGetPreviousModuleInCourse(ctx context.Context, moduleID int64) (dbgen.Module, error) {
|
||||||
|
return s.queries.GetPreviousModuleInCourse(ctx, moduleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsGetPreviousLessonInModule(ctx context.Context, lessonID int64) (dbgen.Lesson, error) {
|
||||||
|
return s.queries.GetPreviousLessonInModule(ctx, lessonID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsUserHasProgramProgress(ctx context.Context, userID, programID int64) (bool, error) {
|
||||||
|
return s.queries.UserHasProgramProgress(ctx, dbgen.UserHasProgramProgressParams{UserID: userID, ProgramID: programID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsUserHasCourseProgress(ctx context.Context, userID, courseID int64) (bool, error) {
|
||||||
|
return s.queries.UserHasCourseProgress(ctx, dbgen.UserHasCourseProgressParams{UserID: userID, CourseID: courseID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsUserHasModuleProgress(ctx context.Context, userID, moduleID int64) (bool, error) {
|
||||||
|
return s.queries.UserHasModuleProgress(ctx, dbgen.UserHasModuleProgressParams{UserID: userID, ModuleID: moduleID})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) LmsUserHasLessonProgress(ctx context.Context, userID, lessonID int64) (bool, error) {
|
||||||
|
return s.queries.UserHasLessonProgress(ctx, dbgen.UserHasLessonProgressParams{UserID: userID, LessonID: lessonID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
total, err = s.queries.CountLessonsInModule(ctx, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
completed, err = s.queries.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{
|
||||||
|
ModuleID: moduleID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return completed, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
total, err = s.queries.CountLessonsInCourse(ctx, courseID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
completed, err = s.queries.CountUserCompletedLessonsInCourse(ctx, dbgen.CountUserCompletedLessonsInCourseParams{
|
||||||
|
CourseID: courseID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return completed, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
total, err = s.queries.CountLessonsInProgram(ctx, programID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
completed, err = s.queries.CountUserCompletedLessonsInProgram(ctx, dbgen.CountUserCompletedLessonsInProgramParams{
|
||||||
|
ProgramID: programID,
|
||||||
|
UserID: userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
return completed, total, nil
|
||||||
|
}
|
||||||
116
internal/repository/lms_courses.go
Normal file
116
internal/repository/lms_courses.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
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 courseToDomain(c dbgen.Course) domain.Course {
|
||||||
|
out := domain.Course{
|
||||||
|
ID: c.ID,
|
||||||
|
ProgramID: c.ProgramID,
|
||||||
|
Name: c.Name,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
out.SortOrder = int(c.SortOrder)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error) {
|
||||||
|
c, err := s.queries.CreateCourse(ctx, dbgen.CreateCourseParams{
|
||||||
|
ProgramID: programID,
|
||||||
|
Name: input.Name,
|
||||||
|
Description: toPgText(input.Description),
|
||||||
|
Thumbnail: toPgText(input.Thumbnail),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
return courseToDomain(c), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListCourseIDsByProgram(ctx context.Context, programID int64) ([]int64, error) {
|
||||||
|
return s.queries.ListCourseIDsByProgram(ctx, programID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, error) {
|
||||||
|
c, err := s.queries.GetCourseByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Course{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
return courseToDomain(c), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error) {
|
||||||
|
rows, err := s.queries.ListCoursesByProgramID(ctx, dbgen.ListCoursesByProgramIDParams{
|
||||||
|
ProgramID: programID,
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.Course{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.Course, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, courseToDomain(dbgen.Course{
|
||||||
|
ID: r.ID,
|
||||||
|
ProgramID: r.ProgramID,
|
||||||
|
Name: r.Name,
|
||||||
|
Description: r.Description,
|
||||||
|
Thumbnail: r.Thumbnail,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
SortOrder: r.SortOrder,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateCourse(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, 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.UpdateCourse(ctx, dbgen.UpdateCourseParams{
|
||||||
|
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.Course{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
return courseToDomain(c), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteCourse(ctx context.Context, id int64) error {
|
||||||
|
return s.queries.DeleteCourse(ctx, id)
|
||||||
|
}
|
||||||
116
internal/repository/lms_lessons.go
Normal file
116
internal/repository/lms_lessons.go
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
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 lessonToDomain(l dbgen.Lesson) domain.Lesson {
|
||||||
|
out := domain.Lesson{
|
||||||
|
ID: l.ID,
|
||||||
|
ModuleID: l.ModuleID,
|
||||||
|
Title: l.Title,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
out.SortOrder = int(l.SortOrder)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error) {
|
||||||
|
l, err := s.queries.CreateLesson(ctx, dbgen.CreateLessonParams{
|
||||||
|
ModuleID: moduleID,
|
||||||
|
Title: input.Title,
|
||||||
|
VideoUrl: toPgText(input.VideoURL),
|
||||||
|
Thumbnail: toPgText(input.Thumbnail),
|
||||||
|
Description: toPgText(input.Description),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.Lesson{}, err
|
||||||
|
}
|
||||||
|
return lessonToDomain(l), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetLessonByID(ctx context.Context, id int64) (domain.Lesson, error) {
|
||||||
|
l, err := s.queries.GetLessonByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Lesson{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Lesson{}, err
|
||||||
|
}
|
||||||
|
return lessonToDomain(l), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error) {
|
||||||
|
rows, err := s.queries.ListLessonsByModuleID(ctx, dbgen.ListLessonsByModuleIDParams{
|
||||||
|
ModuleID: moduleID,
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.Lesson{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.Lesson, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, lessonToDomain(dbgen.Lesson{
|
||||||
|
ID: r.ID,
|
||||||
|
ModuleID: r.ModuleID,
|
||||||
|
Title: r.Title,
|
||||||
|
VideoUrl: r.VideoUrl,
|
||||||
|
Thumbnail: r.Thumbnail,
|
||||||
|
Description: r.Description,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
SortOrder: r.SortOrder,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, 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.UpdateLesson(ctx, dbgen.UpdateLessonParams{
|
||||||
|
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.Lesson{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Lesson{}, err
|
||||||
|
}
|
||||||
|
return lessonToDomain(l), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteLesson(ctx context.Context, id int64) error {
|
||||||
|
return s.queries.DeleteLesson(ctx, id)
|
||||||
|
}
|
||||||
120
internal/repository/lms_modules.go
Normal file
120
internal/repository/lms_modules.go
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
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 moduleToDomain(m dbgen.Module) domain.Module {
|
||||||
|
out := domain.Module{
|
||||||
|
ID: m.ID,
|
||||||
|
ProgramID: m.ProgramID,
|
||||||
|
CourseID: m.CourseID,
|
||||||
|
Name: m.Name,
|
||||||
|
}
|
||||||
|
out.Description = fromPgText(m.Description)
|
||||||
|
out.Icon = fromPgText(m.Icon)
|
||||||
|
out.CreatedAt = m.CreatedAt.Time
|
||||||
|
if m.UpdatedAt.Valid {
|
||||||
|
t := m.UpdatedAt.Time
|
||||||
|
out.UpdatedAt = &t
|
||||||
|
}
|
||||||
|
out.SortOrder = int(m.SortOrder)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error) {
|
||||||
|
m, err := s.queries.CreateModule(ctx, dbgen.CreateModuleParams{
|
||||||
|
ProgramID: programID,
|
||||||
|
CourseID: courseID,
|
||||||
|
Name: input.Name,
|
||||||
|
Description: toPgText(input.Description),
|
||||||
|
Icon: toPgText(input.Icon),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
return moduleToDomain(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListModuleIDsByCourse(ctx context.Context, courseID int64) ([]int64, error) {
|
||||||
|
return s.queries.ListModuleIDsByCourse(ctx, courseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, error) {
|
||||||
|
m, err := s.queries.GetModuleByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Module{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
return moduleToDomain(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error) {
|
||||||
|
rows, err := s.queries.ListModulesByProgramAndCourse(ctx, dbgen.ListModulesByProgramAndCourseParams{
|
||||||
|
ProgramID: programID,
|
||||||
|
CourseID: courseID,
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.Module{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.Module, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, moduleToDomain(dbgen.Module{
|
||||||
|
ID: r.ID,
|
||||||
|
ProgramID: r.ProgramID,
|
||||||
|
CourseID: r.CourseID,
|
||||||
|
Name: r.Name,
|
||||||
|
Description: r.Description,
|
||||||
|
Icon: r.Icon,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
SortOrder: r.SortOrder,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, 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.UpdateModule(ctx, dbgen.UpdateModuleParams{
|
||||||
|
ID: id,
|
||||||
|
Name: nameText,
|
||||||
|
Description: optionalTextUpdate(input.Description),
|
||||||
|
Icon: optionalTextUpdate(input.Icon),
|
||||||
|
SortOrder: optionalInt4Update(input.SortOrder),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Module{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
return moduleToDomain(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteModule(ctx context.Context, id int64) error {
|
||||||
|
return s.queries.DeleteModule(ctx, id)
|
||||||
|
}
|
||||||
232
internal/repository/lms_practices.go
Normal file
232
internal/repository/lms_practices.go
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
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 int64PtrToPg8(p *int64) pgtype.Int8 {
|
||||||
|
if p == nil {
|
||||||
|
return pgtype.Int8{Valid: false}
|
||||||
|
}
|
||||||
|
return pgtype.Int8{Int64: *p, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromPgInt8ID(c pgtype.Int8) *int64 {
|
||||||
|
if !c.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := c.Int64
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func lmsPracticeToDomain(p dbgen.LmsPractice) domain.Practice {
|
||||||
|
out := domain.Practice{
|
||||||
|
ID: p.ID,
|
||||||
|
Title: p.Title,
|
||||||
|
QuestionSetID: p.QuestionSetID,
|
||||||
|
}
|
||||||
|
if p.CourseID.Valid {
|
||||||
|
out.ParentKind = domain.ParentKindCourse
|
||||||
|
out.ParentID = p.CourseID.Int64
|
||||||
|
} else if p.ModuleID.Valid {
|
||||||
|
out.ParentKind = domain.ParentKindModule
|
||||||
|
out.ParentID = p.ModuleID.Int64
|
||||||
|
} else if p.LessonID.Valid {
|
||||||
|
out.ParentKind = domain.ParentKindLesson
|
||||||
|
out.ParentID = p.LessonID.Int64
|
||||||
|
}
|
||||||
|
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 lmsFromListRow(
|
||||||
|
id, qid int64, title string,
|
||||||
|
cid, mid, lid pgtype.Int8,
|
||||||
|
sd, si, qt pgtype.Text, pid pgtype.Int8,
|
||||||
|
ca, ua pgtype.Timestamptz,
|
||||||
|
) domain.Practice {
|
||||||
|
return lmsPracticeToDomain(dbgen.LmsPractice{
|
||||||
|
ID: id,
|
||||||
|
CourseID: cid,
|
||||||
|
ModuleID: mid,
|
||||||
|
LessonID: lid,
|
||||||
|
Title: title,
|
||||||
|
StoryDescription: sd,
|
||||||
|
StoryImage: si,
|
||||||
|
PersonaID: pid,
|
||||||
|
QuestionSetID: qid,
|
||||||
|
QuickTips: qt,
|
||||||
|
CreatedAt: ca,
|
||||||
|
UpdatedAt: ua,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLmsPractice sets exactly one of courseID, moduleID, lessonID (non-nil).
|
||||||
|
func (s *Store) CreateLmsPractice(
|
||||||
|
ctx context.Context,
|
||||||
|
in domain.CreatePracticeInput,
|
||||||
|
courseID, moduleID, lessonID *int64,
|
||||||
|
) (domain.Practice, error) {
|
||||||
|
p, err := s.queries.CreateLmsPractice(ctx, dbgen.CreateLmsPracticeParams{
|
||||||
|
CourseID: int64PtrToPg8(courseID),
|
||||||
|
ModuleID: int64PtrToPg8(moduleID),
|
||||||
|
LessonID: int64PtrToPg8(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.Practice{}, err
|
||||||
|
}
|
||||||
|
return lmsPracticeToDomain(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetLmsPracticeByID(ctx context.Context, id int64) (domain.Practice, error) {
|
||||||
|
p, err := s.queries.GetLmsPracticeByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Practice{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Practice{}, err
|
||||||
|
}
|
||||||
|
return lmsPracticeToDomain(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListLmsPracticesByCourseID(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||||
|
rows, err := s.queries.ListLmsPracticesByCourseID(ctx, dbgen.ListLmsPracticesByCourseIDParams{
|
||||||
|
CourseID: pgtype.Int8{Int64: courseID, Valid: true},
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.Practice{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.Practice, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, lmsFromListRow(
|
||||||
|
r.ID, r.QuestionSetID, r.Title,
|
||||||
|
r.CourseID, r.ModuleID, r.LessonID,
|
||||||
|
r.StoryDescription, r.StoryImage, r.QuickTips,
|
||||||
|
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||||
|
rows, err := s.queries.ListLmsPracticesByModuleID(ctx, dbgen.ListLmsPracticesByModuleIDParams{
|
||||||
|
ModuleID: pgtype.Int8{Int64: moduleID, Valid: true},
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.Practice{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.Practice, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, lmsFromListRow(
|
||||||
|
r.ID, r.QuestionSetID, r.Title,
|
||||||
|
r.CourseID, r.ModuleID, r.LessonID,
|
||||||
|
r.StoryDescription, r.StoryImage, r.QuickTips,
|
||||||
|
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||||
|
rows, err := s.queries.ListLmsPracticesByLessonID(ctx, dbgen.ListLmsPracticesByLessonIDParams{
|
||||||
|
LessonID: pgtype.Int8{Int64: lessonID, Valid: true},
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.Practice{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.Practice, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, lmsFromListRow(
|
||||||
|
r.ID, r.QuestionSetID, r.Title,
|
||||||
|
r.CourseID, r.ModuleID, r.LessonID,
|
||||||
|
r.StoryDescription, r.StoryImage, r.QuickTips,
|
||||||
|
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionalInt8UpdateID(val *int64) pgtype.Int8 {
|
||||||
|
if val == nil {
|
||||||
|
return pgtype.Int8{Valid: false}
|
||||||
|
}
|
||||||
|
return pgtype.Int8{Int64: *val, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateLmsPractice(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, 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.UpdateLmsPractice(ctx, dbgen.UpdateLmsPracticeParams{
|
||||||
|
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.Practice{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Practice{}, err
|
||||||
|
}
|
||||||
|
return lmsPracticeToDomain(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteLmsPractice(ctx context.Context, id int64) error {
|
||||||
|
return s.queries.DeleteLmsPractice(ctx, id)
|
||||||
|
}
|
||||||
86
internal/repository/lms_progress_tx.go
Normal file
86
internal/repository/lms_progress_tx.go
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CompleteLessonForUser records lesson completion and cascades to module, course, and program when the user
|
||||||
|
// has fully completed the preceding scope. Runs in a single transaction.
|
||||||
|
func (s *Store) CompleteLessonForUser(ctx context.Context, userID, lessonID 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.InsertUserLessonProgress(ctx, dbgen.InsertUserLessonProgressParams{UserID: userID, LessonID: lessonID}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lesson, err := q.GetLessonByID(ctx, lessonID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mod, err := q.GetModuleByID(ctx, lesson.ModuleID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
crs, err := q.GetCourseByID(ctx, mod.CourseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nLess, err := q.CountLessonsInModule(ctx, lesson.ModuleID)
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
|
return fmt.Errorf("commit: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
73
internal/repository/lms_reorder.go
Normal file
73
internal/repository/lms_reorder.go
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReorderPrograms sets sort_order to 1..n in the given order (transactional).
|
||||||
|
func (s *Store) ReorderPrograms(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 programs 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("program id %d not found", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Commit(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderCoursesInProgram sets sort_order for courses under programID (transactional).
|
||||||
|
func (s *Store) ReorderCoursesInProgram(ctx context.Context, programID 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 courses
|
||||||
|
SET sort_order = $1,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $2
|
||||||
|
AND program_id = $3`, int32(i+1), id, programID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("course id %d not in program %d", id, programID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Commit(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderModulesInCourse sets sort_order for modules under courseID (transactional).
|
||||||
|
func (s *Store) ReorderModulesInCourse(ctx context.Context, courseID 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 modules
|
||||||
|
SET sort_order = $1,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $2
|
||||||
|
AND course_id = $3`, int32(i+1), id, courseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return fmt.Errorf("module id %d not in course %d", id, courseID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Commit(ctx)
|
||||||
|
}
|
||||||
33
internal/repository/lms_user_progress_snapshot.go
Normal file
33
internal/repository/lms_user_progress_snapshot.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetLMSUserProgressSnapshot returns all completed lesson, module, course, and program IDs for a user.
|
||||||
|
func (s *Store) GetLMSUserProgressSnapshot(ctx context.Context, userID int64) (domain.LMSUserProgress, error) {
|
||||||
|
lessons, err := s.queries.ListLMSCompletedLessonIDsByUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LMSUserProgress{}, err
|
||||||
|
}
|
||||||
|
mods, err := s.queries.ListLMSCompletedModuleIDsByUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LMSUserProgress{}, err
|
||||||
|
}
|
||||||
|
courses, err := s.queries.ListLMSCompletedCourseIDsByUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LMSUserProgress{}, err
|
||||||
|
}
|
||||||
|
programs, err := s.queries.ListLMSCompletedProgramIDsByUser(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.LMSUserProgress{}, err
|
||||||
|
}
|
||||||
|
return domain.LMSUserProgress{
|
||||||
|
LessonIDs: lessons,
|
||||||
|
ModuleIDs: mods,
|
||||||
|
CourseIDs: courses,
|
||||||
|
ProgramIDs: programs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
126
internal/repository/programs.go
Normal file
126
internal/repository/programs.go
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
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 programToDomain(p dbgen.Program) domain.Program {
|
||||||
|
out := domain.Program{
|
||||||
|
ID: p.ID,
|
||||||
|
Name: p.Name,
|
||||||
|
}
|
||||||
|
out.Description = fromPgText(p.Description)
|
||||||
|
out.Thumbnail = fromPgText(p.Thumbnail)
|
||||||
|
out.CreatedAt = p.CreatedAt.Time
|
||||||
|
if p.UpdatedAt.Valid {
|
||||||
|
t := p.UpdatedAt.Time
|
||||||
|
out.UpdatedAt = &t
|
||||||
|
}
|
||||||
|
out.SortOrder = int(p.SortOrder)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateProgram(ctx context.Context, input domain.CreateProgramInput) (domain.Program, error) {
|
||||||
|
p, err := s.queries.CreateProgram(ctx, dbgen.CreateProgramParams{
|
||||||
|
Name: input.Name,
|
||||||
|
Description: toPgText(input.Description),
|
||||||
|
Thumbnail: toPgText(input.Thumbnail),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.Program{}, err
|
||||||
|
}
|
||||||
|
return programToDomain(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListAllProgramIDs(ctx context.Context) ([]int64, error) {
|
||||||
|
return s.queries.ListAllProgramIDs(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetProgramByID(ctx context.Context, id int64) (domain.Program, error) {
|
||||||
|
p, err := s.queries.GetProgramByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Program{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Program{}, err
|
||||||
|
}
|
||||||
|
return programToDomain(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListPrograms(ctx context.Context, limit, offset int32) ([]domain.Program, int64, error) {
|
||||||
|
rows, err := s.queries.ListPrograms(ctx, dbgen.ListProgramsParams{
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return []domain.Program{}, 0, nil
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
out := make([]domain.Program, 0, len(rows))
|
||||||
|
for i, r := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
total = r.TotalCount
|
||||||
|
}
|
||||||
|
out = append(out, programToDomain(dbgen.Program{
|
||||||
|
ID: r.ID,
|
||||||
|
Name: r.Name,
|
||||||
|
Description: r.Description,
|
||||||
|
Thumbnail: r.Thumbnail,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
UpdatedAt: r.UpdatedAt,
|
||||||
|
SortOrder: r.SortOrder,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return out, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionalTextUpdate(val *string) pgtype.Text {
|
||||||
|
if val == nil {
|
||||||
|
return pgtype.Text{Valid: false}
|
||||||
|
}
|
||||||
|
return pgtype.Text{String: *val, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func optionalInt4Update(v *int) pgtype.Int4 {
|
||||||
|
if v == nil {
|
||||||
|
return pgtype.Int4{Valid: false}
|
||||||
|
}
|
||||||
|
return pgtype.Int4{Int32: int32(*v), Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) UpdateProgram(ctx context.Context, id int64, input domain.UpdateProgramInput) (domain.Program, error) {
|
||||||
|
var nameText pgtype.Text
|
||||||
|
if input.Name != nil {
|
||||||
|
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
||||||
|
} else {
|
||||||
|
nameText = pgtype.Text{Valid: false}
|
||||||
|
}
|
||||||
|
p, err := s.queries.UpdateProgram(ctx, dbgen.UpdateProgramParams{
|
||||||
|
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.Program{}, pgx.ErrNoRows
|
||||||
|
}
|
||||||
|
return domain.Program{}, err
|
||||||
|
}
|
||||||
|
return programToDomain(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteProgram(ctx context.Context, id int64) error {
|
||||||
|
return s.queries.DeleteProgram(ctx, id)
|
||||||
|
}
|
||||||
|
|
@ -123,7 +123,6 @@ func questionSetToDomain(qs dbgen.QuestionSet) domain.QuestionSet {
|
||||||
PassingScore: fromPgInt4(qs.PassingScore),
|
PassingScore: fromPgInt4(qs.PassingScore),
|
||||||
ShuffleQuestions: qs.ShuffleQuestions,
|
ShuffleQuestions: qs.ShuffleQuestions,
|
||||||
Status: qs.Status,
|
Status: qs.Status,
|
||||||
SubCourseVideoID: fromPgInt8(qs.SubCourseVideoID),
|
|
||||||
IntroVideoURL: fromPgText(qs.IntroVideoUrl),
|
IntroVideoURL: fromPgText(qs.IntroVideoUrl),
|
||||||
CreatedAt: qs.CreatedAt.Time,
|
CreatedAt: qs.CreatedAt.Time,
|
||||||
UpdatedAt: timePtr(qs.UpdatedAt),
|
UpdatedAt: timePtr(qs.UpdatedAt),
|
||||||
|
|
@ -542,7 +541,6 @@ func (s *Store) CreateQuestionSet(ctx context.Context, input domain.CreateQuesti
|
||||||
PassingScore: toPgInt4(input.PassingScore),
|
PassingScore: toPgInt4(input.PassingScore),
|
||||||
Column10: shuffleQuestions,
|
Column10: shuffleQuestions,
|
||||||
Column11: status,
|
Column11: status,
|
||||||
SubCourseVideoID: toPgInt8(input.SubCourseVideoID),
|
|
||||||
IntroVideoUrl: toPgText(input.IntroVideoURL),
|
IntroVideoUrl: toPgText(input.IntroVideoURL),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -604,7 +602,6 @@ func (s *Store) GetQuestionSetsByType(ctx context.Context, setType string, limit
|
||||||
PassingScore: fromPgInt4(r.PassingScore),
|
PassingScore: fromPgInt4(r.PassingScore),
|
||||||
ShuffleQuestions: r.ShuffleQuestions,
|
ShuffleQuestions: r.ShuffleQuestions,
|
||||||
Status: r.Status,
|
Status: r.Status,
|
||||||
SubCourseVideoID: fromPgInt8(r.SubCourseVideoID),
|
|
||||||
IntroVideoURL: fromPgText(r.IntroVideoUrl),
|
IntroVideoURL: fromPgText(r.IntroVideoUrl),
|
||||||
CreatedAt: r.CreatedAt.Time,
|
CreatedAt: r.CreatedAt.Time,
|
||||||
UpdatedAt: timePtr(r.UpdatedAt),
|
UpdatedAt: timePtr(r.UpdatedAt),
|
||||||
|
|
@ -637,14 +634,6 @@ func (s *Store) GetInitialAssessmentSet(ctx context.Context) (domain.QuestionSet
|
||||||
return questionSetToDomain(qs), nil
|
return questionSetToDomain(qs), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetSubCourseInitialAssessmentSet(ctx context.Context, subCourseID int64) (domain.QuestionSet, error) {
|
|
||||||
qs, err := s.queries.GetSubCourseInitialAssessmentSet(ctx, pgtype.Int8{Int64: subCourseID, Valid: true})
|
|
||||||
if err != nil {
|
|
||||||
return domain.QuestionSet{}, err
|
|
||||||
}
|
|
||||||
return questionSetToDomain(qs), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) GetFirstIncompletePreviousPractice(ctx context.Context, userID int64, questionSetID int64) (*domain.PracticeAccessBlock, error) {
|
func (s *Store) GetFirstIncompletePreviousPractice(ctx context.Context, userID int64, questionSetID int64) (*domain.PracticeAccessBlock, error) {
|
||||||
row, err := s.queries.GetFirstIncompletePreviousPractice(ctx, dbgen.GetFirstIncompletePreviousPracticeParams{
|
row, err := s.queries.GetFirstIncompletePreviousPractice(ctx, dbgen.GetFirstIncompletePreviousPracticeParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
|
|
@ -692,7 +681,6 @@ func (s *Store) UpdateQuestionSet(ctx context.Context, id int64, input domain.Cr
|
||||||
ShuffleQuestions: shuffleQuestions,
|
ShuffleQuestions: shuffleQuestions,
|
||||||
Status: status,
|
Status: status,
|
||||||
IntroVideoUrl: toPgText(input.IntroVideoURL),
|
IntroVideoUrl: toPgText(input.IntroVideoURL),
|
||||||
SubCourseVideoID: toPgInt8(input.SubCourseVideoID),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
package course_management
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/config"
|
|
||||||
"Yimaru-Backend/internal/ports"
|
|
||||||
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
|
||||||
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Service struct {
|
|
||||||
userStore ports.UserStore
|
|
||||||
courseStore interface{}
|
|
||||||
progressionStore interface{}
|
|
||||||
notificationSvc *notificationservice.Service
|
|
||||||
vimeoSvc *vimeoservice.Service
|
|
||||||
cloudConvertSvc *cloudconvertservice.Service
|
|
||||||
config *config.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewService(
|
|
||||||
userStore ports.UserStore,
|
|
||||||
courseStore interface{},
|
|
||||||
progressionStore interface{},
|
|
||||||
notificationSvc *notificationservice.Service,
|
|
||||||
cfg *config.Config,
|
|
||||||
) *Service {
|
|
||||||
return &Service{
|
|
||||||
userStore: userStore,
|
|
||||||
courseStore: courseStore,
|
|
||||||
progressionStore: progressionStore,
|
|
||||||
notificationSvc: notificationSvc,
|
|
||||||
config: cfg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SetVimeoService(vimeoSvc *vimeoservice.Service) {
|
|
||||||
s.vimeoSvc = vimeoSvc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) HasVimeoService() bool {
|
|
||||||
return s.vimeoSvc != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) SetCloudConvertService(ccSvc *cloudconvertservice.Service) {
|
|
||||||
s.cloudConvertSvc = ccSvc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) HasCloudConvertService() bool {
|
|
||||||
return s.cloudConvertSvc != nil
|
|
||||||
}
|
|
||||||
105
internal/services/courses/service.go
Normal file
105
internal/services/courses/service.go
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
package courses
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/ports"
|
||||||
|
"Yimaru-Backend/internal/services/programs"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrCourseNotFound = errors.New("course not found")
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
courses ports.CourseStore
|
||||||
|
programs ports.ProgramStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(courses ports.CourseStore, programs ports.ProgramStore) *Service {
|
||||||
|
return &Service{courses: courses, programs: programs}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error) {
|
||||||
|
if _, err := s.programs.GetProgramByID(ctx, programID); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Course{}, programs.ErrProgramNotFound
|
||||||
|
}
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
return s.courses.CreateCourse(ctx, programID, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(ctx context.Context, id int64) (domain.Course, error) {
|
||||||
|
c, err := s.courses.GetCourseByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Course{}, ErrCourseNotFound
|
||||||
|
}
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListByProgram(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error) {
|
||||||
|
if _, err := s.programs.GetProgramByID(ctx, programID); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, 0, programs.ErrProgramNotFound
|
||||||
|
}
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit > 200 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
return s.courses.ListCoursesByProgramID(ctx, programID, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error) {
|
||||||
|
c, err := s.courses.UpdateCourse(ctx, id, input)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Course{}, ErrCourseNotFound
|
||||||
|
}
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||||
|
if _, err := s.courses.GetCourseByID(ctx, id); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrCourseNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.courses.DeleteCourse(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderInProgram sets course sort_order under a program. ordered must list every course id in that program
|
||||||
|
// exactly once (e.g. from GET /programs/{id}/courses) in the desired order.
|
||||||
|
func (s *Service) ReorderInProgram(ctx context.Context, programID int64, ordered []int64) error {
|
||||||
|
if _, err := s.programs.GetProgramByID(ctx, programID); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return programs.ErrProgramNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
expected, err := s.courses.ListCourseIDsByProgram(ctx, programID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := domain.ValidateReorderPermutation(ordered, expected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(ordered) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.courses.ReorderCoursesInProgram(ctx, programID, ordered)
|
||||||
|
}
|
||||||
88
internal/services/lessons/service.go
Normal file
88
internal/services/lessons/service.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package lessons
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/ports"
|
||||||
|
"Yimaru-Backend/internal/services/modules"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrLessonNotFound = errors.New("lesson not found")
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
lessons ports.LessonStore
|
||||||
|
modules ports.ModuleStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(lessons ports.LessonStore, modules ports.ModuleStore) *Service {
|
||||||
|
return &Service{lessons: lessons, modules: modules}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getModuleOrErr(ctx context.Context, moduleID int64) error {
|
||||||
|
_, err := s.modules.GetModuleByID(ctx, moduleID)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return modules.ErrModuleNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error) {
|
||||||
|
if err := s.getModuleOrErr(ctx, moduleID); err != nil {
|
||||||
|
return domain.Lesson{}, err
|
||||||
|
}
|
||||||
|
return s.lessons.CreateLesson(ctx, moduleID, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(ctx context.Context, id int64) (domain.Lesson, error) {
|
||||||
|
l, err := s.lessons.GetLessonByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Lesson{}, ErrLessonNotFound
|
||||||
|
}
|
||||||
|
return domain.Lesson{}, err
|
||||||
|
}
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListByModule(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error) {
|
||||||
|
if err := s.getModuleOrErr(ctx, moduleID); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit > 200 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
return s.lessons.ListLessonsByModuleID(ctx, moduleID, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error) {
|
||||||
|
l, err := s.lessons.UpdateLesson(ctx, id, input)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Lesson{}, ErrLessonNotFound
|
||||||
|
}
|
||||||
|
return domain.Lesson{}, err
|
||||||
|
}
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||||
|
if _, err := s.lessons.GetLessonByID(ctx, id); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrLessonNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.lessons.DeleteLesson(ctx, id)
|
||||||
|
}
|
||||||
279
internal/services/lmsprogress/service.go
Normal file
279
internal/services/lmsprogress/service.go
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
package lmsprogress
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/repository"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errPrevProgram = "Complete the previous program before accessing this one."
|
||||||
|
errPrevCourse = "Complete the previous course in this program first."
|
||||||
|
errPrevModule = "Complete the previous module in this course first."
|
||||||
|
errPrevLesson = "Complete the previous lesson in this module first."
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service enforces sequential LMS access for learners and records lesson progress.
|
||||||
|
type Service struct {
|
||||||
|
store *repository.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(store *repository.Store) *Service {
|
||||||
|
return &Service{store: store}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteLessonForUser records lesson completion and rolls up to module, course, and program when applicable.
|
||||||
|
func (s *Service) CompleteLessonForUser(ctx context.Context, userID, lessonID int64) error {
|
||||||
|
return s.store.CompleteLessonForUser(ctx, userID, lessonID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMyProgress returns completed lesson, module, course, and program IDs for the user.
|
||||||
|
func (s *Service) GetMyProgress(ctx context.Context, userID int64) (domain.LMSUserProgress, error) {
|
||||||
|
return s.store.GetLMSUserProgressSnapshot(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanAccessProgram returns whether the user may use content under this program (previous program must be fully completed if any).
|
||||||
|
func (s *Service) CanAccessProgram(ctx context.Context, userID, programID int64) (ok bool, reason string, err error) {
|
||||||
|
if _, err := s.store.GetProgramByID(ctx, programID); err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
prev, err := s.store.LmsGetPreviousProgram(ctx, programID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
has, err := s.store.LmsUserHasProgramProgress(ctx, userID, prev.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return false, errPrevProgram, nil
|
||||||
|
}
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanAccessCourse requires the parent program to be accessible and the previous course in the program to be completed.
|
||||||
|
func (s *Service) CanAccessCourse(ctx context.Context, userID, courseID int64) (ok bool, reason string, err error) {
|
||||||
|
c, err := s.store.GetCourseByID(ctx, courseID)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
ok, reason, err = s.CanAccessProgram(ctx, userID, c.ProgramID)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return ok, reason, err
|
||||||
|
}
|
||||||
|
prev, err := s.store.LmsGetPreviousCourseInProgram(ctx, courseID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
has, err := s.store.LmsUserHasCourseProgress(ctx, userID, prev.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return false, errPrevCourse, nil
|
||||||
|
}
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanAccessModule requires the course (and its program chain) to be accessible and the previous module in the course to be completed.
|
||||||
|
func (s *Service) CanAccessModule(ctx context.Context, userID, moduleID int64) (ok bool, reason string, err error) {
|
||||||
|
m, err := s.store.GetModuleByID(ctx, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
ok, reason, err = s.CanAccessCourse(ctx, userID, m.CourseID)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return ok, reason, err
|
||||||
|
}
|
||||||
|
prev, err := s.store.LmsGetPreviousModuleInCourse(ctx, moduleID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
has, err := s.store.LmsUserHasModuleProgress(ctx, userID, prev.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return false, errPrevModule, nil
|
||||||
|
}
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanAccessLesson requires the module chain to be accessible and the previous lesson in the module to be completed.
|
||||||
|
func (s *Service) CanAccessLesson(ctx context.Context, userID, lessonID int64) (ok bool, reason string, err error) {
|
||||||
|
lesson, err := s.store.GetLessonByID(ctx, lessonID)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
ok, reason, err = s.CanAccessModule(ctx, userID, lesson.ModuleID)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return ok, reason, err
|
||||||
|
}
|
||||||
|
prev, err := s.store.LmsGetPreviousLessonInModule(ctx, lessonID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
has, err := s.store.LmsUserHasLessonProgress(ctx, userID, prev.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return false, errPrevLesson, nil
|
||||||
|
}
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyAccessProgram sets p.Access for a learner. Non-learners: clears Access to omit from JSON.
|
||||||
|
func (s *Service) ApplyAccessProgram(ctx context.Context, role domain.Role, userID int64, p *domain.Program) error {
|
||||||
|
if role != domain.RoleStudent {
|
||||||
|
p.Access = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ok, reason, err := s.CanAccessProgram(ctx, userID, p.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
done, err := s.store.LmsUserHasProgramProgress(ctx, userID, p.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
comp, tot, err := s.store.LmsUserLessonProgressInProgram(ctx, userID, p.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c, t, pct := lmsProgressCounts(comp, tot, done)
|
||||||
|
p.Access = &domain.LMSEntityAccess{
|
||||||
|
IsAccessible: ok, IsCompleted: done, Reason: reasonIf(ok, reason),
|
||||||
|
CompletedCount: c, TotalCount: t, ProgressPercent: pct,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyAccessCourse sets c.Access for a learner.
|
||||||
|
func (s *Service) ApplyAccessCourse(ctx context.Context, role domain.Role, userID int64, c *domain.Course) error {
|
||||||
|
if role != domain.RoleStudent {
|
||||||
|
c.Access = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ok, reason, err := s.CanAccessCourse(ctx, userID, c.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
done, err := s.store.LmsUserHasCourseProgress(ctx, userID, c.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
comp, tot, err := s.store.LmsUserLessonProgressInCourse(ctx, userID, c.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cc, tt, pct := lmsProgressCounts(comp, tot, done)
|
||||||
|
c.Access = &domain.LMSEntityAccess{
|
||||||
|
IsAccessible: ok, IsCompleted: done, Reason: reasonIf(ok, reason),
|
||||||
|
CompletedCount: cc, TotalCount: tt, ProgressPercent: pct,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyAccessModule sets m.Access for a learner.
|
||||||
|
func (s *Service) ApplyAccessModule(ctx context.Context, role domain.Role, userID int64, m *domain.Module) error {
|
||||||
|
if role != domain.RoleStudent {
|
||||||
|
m.Access = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ok, reason, err := s.CanAccessModule(ctx, userID, m.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
done, err := s.store.LmsUserHasModuleProgress(ctx, userID, m.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
comp, tot, err := s.store.LmsUserLessonProgressInModule(ctx, userID, m.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cc, tt, pct := lmsProgressCounts(comp, tot, done)
|
||||||
|
m.Access = &domain.LMSEntityAccess{
|
||||||
|
IsAccessible: ok, IsCompleted: done, Reason: reasonIf(ok, reason),
|
||||||
|
CompletedCount: cc, TotalCount: tt, ProgressPercent: pct,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyAccessLesson sets l.Access for a learner.
|
||||||
|
func (s *Service) ApplyAccessLesson(ctx context.Context, role domain.Role, userID int64, les *domain.Lesson) error {
|
||||||
|
if role != domain.RoleStudent {
|
||||||
|
les.Access = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ok, reason, err := s.CanAccessLesson(ctx, userID, les.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
done, err := s.store.LmsUserHasLessonProgress(ctx, userID, les.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var comp, tot int32
|
||||||
|
if done {
|
||||||
|
comp, tot = 1, 1
|
||||||
|
} else {
|
||||||
|
comp, tot = 0, 1
|
||||||
|
}
|
||||||
|
c, t, pct := lmsProgressCounts(comp, tot, done)
|
||||||
|
les.Access = &domain.LMSEntityAccess{
|
||||||
|
IsAccessible: ok, IsCompleted: done, Reason: reasonIf(ok, reason),
|
||||||
|
CompletedCount: c, TotalCount: t, ProgressPercent: pct,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// lmsProgressCounts maps DB lesson completion counts to UI fields. Percent is 0–100; completed
|
||||||
|
// and total are aligned with isCompleted when the entity is fully done.
|
||||||
|
func lmsProgressCounts(completed, total int32, isCompleted bool) (c, t, pct int) {
|
||||||
|
c, t = int(completed), int(total)
|
||||||
|
if t < 0 {
|
||||||
|
t = 0
|
||||||
|
}
|
||||||
|
if c < 0 {
|
||||||
|
c = 0
|
||||||
|
}
|
||||||
|
if isCompleted {
|
||||||
|
if t > 0 {
|
||||||
|
return t, t, 100
|
||||||
|
}
|
||||||
|
return c, t, 100
|
||||||
|
}
|
||||||
|
if t == 0 {
|
||||||
|
return 0, 0, 0
|
||||||
|
}
|
||||||
|
pct = (c * 100) / t
|
||||||
|
if pct > 100 {
|
||||||
|
pct = 100
|
||||||
|
}
|
||||||
|
return c, t, pct
|
||||||
|
}
|
||||||
|
|
||||||
|
func reasonIf(ok bool, r string) string {
|
||||||
|
if ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
111
internal/services/modules/service.go
Normal file
111
internal/services/modules/service.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
package modules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/ports"
|
||||||
|
"Yimaru-Backend/internal/services/courses"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrModuleNotFound = errors.New("module not found")
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
modules ports.ModuleStore
|
||||||
|
courses ports.CourseStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(modules ports.ModuleStore, courses ports.CourseStore) *Service {
|
||||||
|
return &Service{modules: modules, courses: courses}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) getCourseOrErr(ctx context.Context, courseID int64) (domain.Course, error) {
|
||||||
|
c, err := s.courses.GetCourseByID(ctx, courseID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Course{}, courses.ErrCourseNotFound
|
||||||
|
}
|
||||||
|
return domain.Course{}, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create loads the course and stores program_id from it (parent program is not taken from the URL).
|
||||||
|
func (s *Service) Create(ctx context.Context, courseID int64, input domain.CreateModuleInput) (domain.Module, error) {
|
||||||
|
c, err := s.getCourseOrErr(ctx, courseID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
return s.modules.CreateModule(ctx, c.ProgramID, courseID, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(ctx context.Context, id int64) (domain.Module, error) {
|
||||||
|
m, err := s.modules.GetModuleByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Module{}, ErrModuleNotFound
|
||||||
|
}
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListByCourse loads the course and lists modules for its program_id and course_id.
|
||||||
|
func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Module, int64, error) {
|
||||||
|
c, err := s.getCourseOrErr(ctx, courseID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit > 200 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
return s.modules.ListModulesByProgramAndCourse(ctx, c.ProgramID, courseID, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) {
|
||||||
|
m, err := s.modules.UpdateModule(ctx, id, input)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Module{}, ErrModuleNotFound
|
||||||
|
}
|
||||||
|
return domain.Module{}, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||||
|
if _, err := s.modules.GetModuleByID(ctx, id); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrModuleNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.modules.DeleteModule(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderInCourse sets module sort_order under a course. ordered must list every module id in that course
|
||||||
|
// exactly once (e.g. from GET /courses/{id}/modules) in the desired order.
|
||||||
|
func (s *Service) ReorderInCourse(ctx context.Context, courseID int64, ordered []int64) error {
|
||||||
|
if _, err := s.getCourseOrErr(ctx, courseID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
expected, err := s.modules.ListModuleIDsByCourse(ctx, courseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := domain.ValidateReorderPermutation(ordered, expected); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(ordered) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.modules.ReorderModulesInCourse(ctx, courseID, ordered)
|
||||||
|
}
|
||||||
204
internal/services/practices/service.go
Normal file
204
internal/services/practices/service.go
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
package practices
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/ports"
|
||||||
|
"Yimaru-Backend/internal/services/courses"
|
||||||
|
"Yimaru-Backend/internal/services/lessons"
|
||||||
|
"Yimaru-Backend/internal/services/modules"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrPracticeNotFound = errors.New("practice not found")
|
||||||
|
ErrQuestionSetNotFound = errors.New("question set not found")
|
||||||
|
ErrInvalidPracticeParent = errors.New("parent_kind and parent_id do not match an allowed parent")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
practices ports.LmsPracticeStore
|
||||||
|
courses ports.CourseStore
|
||||||
|
modules ports.ModuleStore
|
||||||
|
lessons ports.LessonStore
|
||||||
|
qs ports.QuestionSetByID
|
||||||
|
users ports.UserByID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(
|
||||||
|
practices ports.LmsPracticeStore,
|
||||||
|
courses ports.CourseStore,
|
||||||
|
modules ports.ModuleStore,
|
||||||
|
lessons ports.LessonStore,
|
||||||
|
qs ports.QuestionSetByID,
|
||||||
|
users ports.UserByID,
|
||||||
|
) *Service {
|
||||||
|
return &Service{
|
||||||
|
practices: practices,
|
||||||
|
courses: courses,
|
||||||
|
modules: modules,
|
||||||
|
lessons: lessons,
|
||||||
|
qs: qs,
|
||||||
|
users: users,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) validateQuestionSet(ctx context.Context, id int64) error {
|
||||||
|
_, err := s.qs.GetQuestionSetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrQuestionSetNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) validatePersonaUser(ctx context.Context, id int64) error {
|
||||||
|
_, err := s.users.GetUserByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, domain.ErrUserNotFound) {
|
||||||
|
return domain.ErrUserNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) resolveParent(ctx context.Context, in domain.CreatePracticeInput) (courseID, moduleID, lessonID *int64, err error) {
|
||||||
|
pid := in.ParentID
|
||||||
|
switch in.ParentKind {
|
||||||
|
case domain.ParentKindCourse:
|
||||||
|
if _, e := s.courses.GetCourseByID(ctx, pid); e != nil {
|
||||||
|
if errors.Is(e, pgx.ErrNoRows) {
|
||||||
|
return nil, nil, nil, courses.ErrCourseNotFound
|
||||||
|
}
|
||||||
|
return nil, nil, nil, e
|
||||||
|
}
|
||||||
|
return &pid, nil, nil, nil
|
||||||
|
case domain.ParentKindModule:
|
||||||
|
if _, e := s.modules.GetModuleByID(ctx, pid); e != nil {
|
||||||
|
if errors.Is(e, pgx.ErrNoRows) {
|
||||||
|
return nil, nil, nil, modules.ErrModuleNotFound
|
||||||
|
}
|
||||||
|
return nil, nil, nil, e
|
||||||
|
}
|
||||||
|
return nil, &pid, nil, nil
|
||||||
|
case domain.ParentKindLesson:
|
||||||
|
if _, e := s.lessons.GetLessonByID(ctx, pid); e != nil {
|
||||||
|
if errors.Is(e, pgx.ErrNoRows) {
|
||||||
|
return nil, nil, nil, lessons.ErrLessonNotFound
|
||||||
|
}
|
||||||
|
return nil, nil, nil, e
|
||||||
|
}
|
||||||
|
return nil, nil, &pid, nil
|
||||||
|
default:
|
||||||
|
return nil, nil, nil, ErrInvalidPracticeParent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(ctx context.Context, in domain.CreatePracticeInput) (domain.Practice, error) {
|
||||||
|
if err := s.validateQuestionSet(ctx, in.QuestionSetID); err != nil {
|
||||||
|
return domain.Practice{}, err
|
||||||
|
}
|
||||||
|
if in.PersonaID != nil {
|
||||||
|
if err := s.validatePersonaUser(ctx, *in.PersonaID); err != nil {
|
||||||
|
return domain.Practice{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
courseID, moduleID, lessonID, err := s.resolveParent(ctx, in)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Practice{}, err
|
||||||
|
}
|
||||||
|
return s.practices.CreateLmsPractice(ctx, in, courseID, moduleID, lessonID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(ctx context.Context, id int64) (domain.Practice, error) {
|
||||||
|
p, err := s.practices.GetLmsPracticeByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Practice{}, ErrPracticeNotFound
|
||||||
|
}
|
||||||
|
return domain.Practice{}, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampPracticePage(limit, offset int32) (int32, int32) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit > 200 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
return limit, offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||||
|
if _, err := s.courses.GetCourseByID(ctx, courseID); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, 0, courses.ErrCourseNotFound
|
||||||
|
}
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
limit, offset = clampPracticePage(limit, offset)
|
||||||
|
return s.practices.ListLmsPracticesByCourseID(ctx, courseID, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListByModule(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||||
|
if _, err := s.modules.GetModuleByID(ctx, moduleID); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, 0, modules.ErrModuleNotFound
|
||||||
|
}
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
limit, offset = clampPracticePage(limit, offset)
|
||||||
|
return s.practices.ListLmsPracticesByModuleID(ctx, moduleID, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListByLesson(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||||
|
if _, err := s.lessons.GetLessonByID(ctx, lessonID); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return nil, 0, lessons.ErrLessonNotFound
|
||||||
|
}
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
limit, offset = clampPracticePage(limit, offset)
|
||||||
|
return s.practices.ListLmsPracticesByLessonID(ctx, lessonID, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error) {
|
||||||
|
if input.QuestionSetID != nil {
|
||||||
|
if err := s.validateQuestionSet(ctx, *input.QuestionSetID); err != nil {
|
||||||
|
return domain.Practice{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if input.PersonaID != nil {
|
||||||
|
if err := s.validatePersonaUser(ctx, *input.PersonaID); err != nil {
|
||||||
|
return domain.Practice{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p, err := s.practices.UpdateLmsPractice(ctx, id, input)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Practice{}, ErrPracticeNotFound
|
||||||
|
}
|
||||||
|
return domain.Practice{}, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||||
|
if _, err := s.practices.GetLmsPracticeByID(ctx, id); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrPracticeNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.practices.DeleteLmsPractice(ctx, id)
|
||||||
|
}
|
||||||
85
internal/services/programs/service.go
Normal file
85
internal/services/programs/service.go
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package programs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/ports"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrProgramNotFound = errors.New("program not found")
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
store ports.ProgramStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(store ports.ProgramStore) *Service {
|
||||||
|
return &Service{store: store}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(ctx context.Context, input domain.CreateProgramInput) (domain.Program, error) {
|
||||||
|
return s.store.CreateProgram(ctx, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(ctx context.Context, id int64) (domain.Program, error) {
|
||||||
|
p, err := s.store.GetProgramByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Program{}, ErrProgramNotFound
|
||||||
|
}
|
||||||
|
return domain.Program{}, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) List(ctx context.Context, limit, offset int32) ([]domain.Program, int64, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
if limit > 200 {
|
||||||
|
limit = 200
|
||||||
|
}
|
||||||
|
if offset < 0 {
|
||||||
|
offset = 0
|
||||||
|
}
|
||||||
|
return s.store.ListPrograms(ctx, limit, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateProgramInput) (domain.Program, error) {
|
||||||
|
p, err := s.store.UpdateProgram(ctx, id, input)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return domain.Program{}, ErrProgramNotFound
|
||||||
|
}
|
||||||
|
return domain.Program{}, err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||||
|
if _, err := s.store.GetProgramByID(ctx, id); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return ErrProgramNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.store.DeleteProgram(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder sets program sort_order from ordered ids (1..n). ordered must list every program id exactly once
|
||||||
|
// (e.g. from GET /programs) in the desired display / learning order.
|
||||||
|
func (s *Service) Reorder(ctx context.Context, ordered []int64) error {
|
||||||
|
expected, err := s.store.ListAllProgramIDs(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.ReorderPrograms(ctx, ordered)
|
||||||
|
}
|
||||||
|
|
@ -120,10 +120,6 @@ func (s *Service) GetInitialAssessmentSet(ctx context.Context) (domain.QuestionS
|
||||||
return s.questionStore.GetInitialAssessmentSet(ctx)
|
return s.questionStore.GetInitialAssessmentSet(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetSubCourseInitialAssessmentSet(ctx context.Context, subCourseID int64) (domain.QuestionSet, error) {
|
|
||||||
return s.questionStore.GetSubCourseInitialAssessmentSet(ctx, subCourseID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetFirstIncompletePreviousPractice(ctx context.Context, userID int64, questionSetID int64) (*domain.PracticeAccessBlock, error) {
|
func (s *Service) GetFirstIncompletePreviousPractice(ctx context.Context, userID int64, questionSetID int64) (*domain.PracticeAccessBlock, error) {
|
||||||
return s.questionStore.GetFirstIncompletePreviousPractice(ctx, userID, questionSetID)
|
return s.questionStore.GetFirstIncompletePreviousPractice(ctx, userID, questionSetID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,46 @@ var AllPermissions = []domain.PermissionSeed{
|
||||||
{Key: "courses.create", Name: "Create Course", Description: "Create a new course", GroupName: "Courses"},
|
{Key: "courses.create", Name: "Create Course", Description: "Create a new course", GroupName: "Courses"},
|
||||||
{Key: "courses.get", Name: "Get Course", Description: "Get a course by ID", GroupName: "Courses"},
|
{Key: "courses.get", Name: "Get Course", Description: "Get a course by ID", GroupName: "Courses"},
|
||||||
{Key: "courses.list_by_category", Name: "List Courses by Category", Description: "List courses by category", GroupName: "Courses"},
|
{Key: "courses.list_by_category", Name: "List Courses by Category", Description: "List courses by category", GroupName: "Courses"},
|
||||||
|
{Key: "courses.list_by_program", Name: "List Courses by Program", Description: "List courses under a program", GroupName: "Courses"},
|
||||||
{Key: "courses.update", Name: "Update Course", Description: "Update a course", GroupName: "Courses"},
|
{Key: "courses.update", Name: "Update Course", Description: "Update a course", GroupName: "Courses"},
|
||||||
{Key: "courses.upload_thumbnail", Name: "Upload Course Thumbnail", Description: "Upload course thumbnail image", GroupName: "Courses"},
|
{Key: "courses.upload_thumbnail", Name: "Upload Course Thumbnail", Description: "Upload course thumbnail image", GroupName: "Courses"},
|
||||||
{Key: "courses.delete", Name: "Delete Course", Description: "Delete a course", GroupName: "Courses"},
|
{Key: "courses.delete", Name: "Delete Course", Description: "Delete a course", GroupName: "Courses"},
|
||||||
{Key: "courses.reorder", Name: "Reorder Courses", Description: "Reorder courses", GroupName: "Courses"},
|
{Key: "courses.reorder", Name: "Reorder Courses", Description: "Reorder courses", GroupName: "Courses"},
|
||||||
|
|
||||||
|
// Programs (LMS top-level)
|
||||||
|
{Key: "programs.create", Name: "Create Program", Description: "Create a program", GroupName: "Programs"},
|
||||||
|
{Key: "programs.list", Name: "List Programs", Description: "List programs", GroupName: "Programs"},
|
||||||
|
{Key: "programs.get", Name: "Get Program", Description: "Get a program by ID", GroupName: "Programs"},
|
||||||
|
{Key: "programs.update", Name: "Update Program", Description: "Update 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"},
|
||||||
|
|
||||||
|
// Modules (LMS, under a course)
|
||||||
|
{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.list_by_course", Name: "List Modules by Course", Description: "List modules under a program and course", GroupName: "Modules"},
|
||||||
|
{Key: "modules.update", Name: "Update Module", Description: "Update a module", GroupName: "Modules"},
|
||||||
|
{Key: "modules.delete", Name: "Delete Module", Description: "Delete a module", GroupName: "Modules"},
|
||||||
|
{Key: "modules.reorder", Name: "Reorder Modules", Description: "Set module order within a course (batch)", GroupName: "Modules"},
|
||||||
|
|
||||||
|
// Lessons (LMS, under a module)
|
||||||
|
{Key: "lessons.create", Name: "Create Lesson", Description: "Create a lesson in a module", GroupName: "Lessons"},
|
||||||
|
{Key: "lessons.get", Name: "Get Lesson", Description: "Get a lesson by ID", GroupName: "Lessons"},
|
||||||
|
{Key: "lessons.complete", Name: "Complete Lesson", Description: "Mark a lesson as complete (sequential learning progress)", GroupName: "Lessons"},
|
||||||
|
{Key: "lessons.list_by_module", Name: "List Lessons by Module", Description: "List lessons under a module", GroupName: "Lessons"},
|
||||||
|
{Key: "lessons.update", Name: "Update Lesson", Description: "Update a lesson", GroupName: "Lessons"},
|
||||||
|
{Key: "lessons.delete", Name: "Delete Lesson", Description: "Delete a lesson", GroupName: "Lessons"},
|
||||||
|
|
||||||
|
// LMS progress (current user)
|
||||||
|
{Key: "lms.get_my_progress", Name: "Get My LMS Progress", Description: "List completed lesson, module, course, and program IDs for the authenticated user", GroupName: "LMS"},
|
||||||
|
|
||||||
|
// Practices (LMS, scoped to course, module, or lesson)
|
||||||
|
{Key: "practices.create", Name: "Create Practice", Description: "Create a practice", GroupName: "Practices"},
|
||||||
|
{Key: "practices.get", Name: "Get Practice", Description: "Get a practice by ID", GroupName: "Practices"},
|
||||||
|
{Key: "practices.list", Name: "List Practices", Description: "List practices by course, module, or lesson", GroupName: "Practices"},
|
||||||
|
{Key: "practices.update", Name: "Update Practice", Description: "Update a practice", GroupName: "Practices"},
|
||||||
|
{Key: "practices.delete", Name: "Delete Practice", Description: "Delete a practice", GroupName: "Practices"},
|
||||||
|
|
||||||
// Course Management - Sub-courses
|
// Course Management - Sub-courses
|
||||||
{Key: "subcourses.create", Name: "Create Sub-course", Description: "Create a new sub-course", GroupName: "Sub-courses"},
|
{Key: "subcourses.create", Name: "Create Sub-course", Description: "Create a new sub-course", GroupName: "Sub-courses"},
|
||||||
{Key: "subcourses.get", Name: "Get Sub-course", Description: "Get a sub-course by ID", GroupName: "Sub-courses"},
|
{Key: "subcourses.get", Name: "Get Sub-course", Description: "Get a sub-course by ID", GroupName: "Sub-courses"},
|
||||||
|
|
@ -236,13 +271,26 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"ADMIN": {
|
"ADMIN": {
|
||||||
// Course Management (full access)
|
// Course Management (full access)
|
||||||
"course_categories.create", "course_categories.list", "course_categories.get", "course_categories.update", "course_categories.delete", "course_categories.reorder",
|
"course_categories.create", "course_categories.list", "course_categories.get", "course_categories.update", "course_categories.delete", "course_categories.reorder",
|
||||||
"courses.create", "courses.get", "courses.list_by_category", "courses.update", "courses.upload_thumbnail", "courses.delete", "courses.reorder",
|
"courses.create", "courses.get", "courses.list_by_category", "courses.list_by_program", "courses.update", "courses.upload_thumbnail", "courses.delete", "courses.reorder",
|
||||||
"subcourses.create", "subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
"subcourses.create", "subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
||||||
"subcourses.update", "subcourses.upload_thumbnail", "subcourses.deactivate", "subcourses.delete", "subcourses.reorder",
|
"subcourses.update", "subcourses.upload_thumbnail", "subcourses.deactivate", "subcourses.delete", "subcourses.reorder",
|
||||||
"videos.create", "videos.create_vimeo", "videos.upload", "videos.import_vimeo", "videos.get",
|
"videos.create", "videos.create_vimeo", "videos.upload", "videos.import_vimeo", "videos.get",
|
||||||
"videos.list_by_subcourse", "videos.list_published", "videos.publish", "videos.update", "videos.delete", "videos.reorder",
|
"videos.list_by_subcourse", "videos.list_published", "videos.publish", "videos.update", "videos.delete", "videos.reorder",
|
||||||
"learning_tree.get", "practices.reorder",
|
"learning_tree.get", "practices.reorder",
|
||||||
|
|
||||||
|
// Programs
|
||||||
|
"programs.create", "programs.list", "programs.get", "programs.update", "programs.delete", "programs.reorder",
|
||||||
|
"lms.get_my_progress",
|
||||||
|
|
||||||
|
// Modules
|
||||||
|
"modules.create", "modules.get", "modules.list_by_course", "modules.update", "modules.delete", "modules.reorder",
|
||||||
|
|
||||||
|
// Lessons
|
||||||
|
"lessons.create", "lessons.get", "lessons.list_by_module", "lessons.complete", "lessons.update", "lessons.delete",
|
||||||
|
|
||||||
|
// Practices
|
||||||
|
"practices.create", "practices.get", "practices.list", "practices.update", "practices.delete",
|
||||||
|
|
||||||
// Questions (full access)
|
// Questions (full access)
|
||||||
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",
|
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",
|
||||||
"question_sets.create", "question_sets.list", "question_sets.list_by_owner", "question_sets.get", "question_sets.update", "question_sets.delete",
|
"question_sets.create", "question_sets.list", "question_sets.list_by_owner", "question_sets.get", "question_sets.update", "question_sets.delete",
|
||||||
|
|
@ -317,11 +365,17 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"STUDENT": {
|
"STUDENT": {
|
||||||
// Course browsing
|
// Course browsing
|
||||||
"course_categories.list", "course_categories.get",
|
"course_categories.list", "course_categories.get",
|
||||||
"courses.get", "courses.list_by_category",
|
"courses.get", "courses.list_by_program",
|
||||||
|
"modules.get", "modules.list_by_course",
|
||||||
|
"lessons.get", "lessons.list_by_module", "lessons.complete",
|
||||||
|
"practices.get", "practices.list",
|
||||||
"subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
"subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
||||||
"videos.get", "videos.list_by_subcourse", "videos.list_published",
|
"videos.get", "videos.list_by_subcourse", "videos.list_published",
|
||||||
"learning_tree.get",
|
"learning_tree.get",
|
||||||
|
|
||||||
|
"programs.list", "programs.get",
|
||||||
|
"lms.get_my_progress",
|
||||||
|
|
||||||
// Questions (read + attempt)
|
// Questions (read + attempt)
|
||||||
"questions.list", "questions.search", "questions.get",
|
"questions.list", "questions.search", "questions.get",
|
||||||
"question_sets.list", "question_sets.list_by_owner", "question_sets.get",
|
"question_sets.list", "question_sets.list_by_owner", "question_sets.get",
|
||||||
|
|
@ -365,11 +419,17 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"INSTRUCTOR": {
|
"INSTRUCTOR": {
|
||||||
// Course browsing + management
|
// Course browsing + management
|
||||||
"course_categories.list", "course_categories.get",
|
"course_categories.list", "course_categories.get",
|
||||||
"courses.get", "courses.list_by_category",
|
"courses.get", "courses.list_by_program",
|
||||||
|
"modules.get", "modules.list_by_course",
|
||||||
|
"lessons.get", "lessons.list_by_module", "lessons.complete",
|
||||||
|
"practices.get", "practices.list",
|
||||||
"subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
"subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
||||||
"videos.get", "videos.list_by_subcourse", "videos.list_published",
|
"videos.get", "videos.list_by_subcourse", "videos.list_published",
|
||||||
"learning_tree.get",
|
"learning_tree.get",
|
||||||
|
|
||||||
|
"programs.list", "programs.get",
|
||||||
|
"lms.get_my_progress",
|
||||||
|
|
||||||
// Questions (full — instructors create content)
|
// Questions (full — instructors create content)
|
||||||
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",
|
"questions.create", "questions.list", "questions.search", "questions.get", "questions.update", "questions.delete",
|
||||||
"question_sets.create", "question_sets.list", "question_sets.list_by_owner", "question_sets.get", "question_sets.update", "question_sets.delete",
|
"question_sets.create", "question_sets.list", "question_sets.list_by_owner", "question_sets.get", "question_sets.update", "question_sets.delete",
|
||||||
|
|
@ -413,11 +473,16 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"SUPPORT": {
|
"SUPPORT": {
|
||||||
// Course browsing (read-only)
|
// Course browsing (read-only)
|
||||||
"course_categories.list", "course_categories.get",
|
"course_categories.list", "course_categories.get",
|
||||||
"courses.get", "courses.list_by_category",
|
"courses.get", "courses.list_by_program",
|
||||||
|
"modules.get", "modules.list_by_course",
|
||||||
|
"lessons.get", "lessons.list_by_module",
|
||||||
|
"practices.get", "practices.list",
|
||||||
"subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
"subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
||||||
"videos.get", "videos.list_by_subcourse", "videos.list_published",
|
"videos.get", "videos.list_by_subcourse", "videos.list_published",
|
||||||
"learning_tree.get",
|
"learning_tree.get",
|
||||||
|
|
||||||
|
"programs.list", "programs.get",
|
||||||
|
|
||||||
// Questions (read)
|
// Questions (read)
|
||||||
"questions.list", "questions.search", "questions.get",
|
"questions.list", "questions.search", "questions.get",
|
||||||
"question_sets.list", "question_sets.list_by_owner", "question_sets.get",
|
"question_sets.list", "question_sets.list_by_owner", "question_sets.get",
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,15 @@ import (
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
||||||
"Yimaru-Backend/internal/services/course_management"
|
|
||||||
minioservice "Yimaru-Backend/internal/services/minio"
|
minioservice "Yimaru-Backend/internal/services/minio"
|
||||||
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/lessons"
|
||||||
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
|
"Yimaru-Backend/internal/services/modules"
|
||||||
|
"Yimaru-Backend/internal/services/practices"
|
||||||
|
"Yimaru-Backend/internal/services/programs"
|
||||||
"Yimaru-Backend/internal/services/questions"
|
"Yimaru-Backend/internal/services/questions"
|
||||||
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
||||||
rbacservice "Yimaru-Backend/internal/services/rbac"
|
rbacservice "Yimaru-Backend/internal/services/rbac"
|
||||||
|
|
@ -35,13 +40,17 @@ import (
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
assessmentSvc *assessment.Service
|
assessmentSvc *assessment.Service
|
||||||
courseSvc *course_management.Service
|
|
||||||
questionsSvc *questions.Service
|
questionsSvc *questions.Service
|
||||||
|
programSvc *programs.Service
|
||||||
|
courseSvc *courses.Service
|
||||||
|
moduleSvc *modules.Service
|
||||||
|
lessonSvc *lessons.Service
|
||||||
|
lmsProgressSvc *lmsprogress.Service
|
||||||
|
practiceSvc *practices.Service
|
||||||
subscriptionsSvc *subscriptions.Service
|
subscriptionsSvc *subscriptions.Service
|
||||||
arifpaySvc *arifpay.ArifpayService
|
arifpaySvc *arifpay.ArifpayService
|
||||||
issueReportingSvc *issuereporting.Service
|
issueReportingSvc *issuereporting.Service
|
||||||
|
|
@ -68,13 +77,17 @@ type App struct {
|
||||||
analyticsDB *dbgen.Queries
|
analyticsDB *dbgen.Queries
|
||||||
rbacSvc *rbacservice.Service
|
rbacSvc *rbacservice.Service
|
||||||
stopPurgeWorker context.CancelFunc
|
stopPurgeWorker context.CancelFunc
|
||||||
stopInactiveSubModuleContentPurge context.CancelFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(
|
func NewApp(
|
||||||
assessmentSvc *assessment.Service,
|
assessmentSvc *assessment.Service,
|
||||||
courseSvc *course_management.Service,
|
|
||||||
questionsSvc *questions.Service,
|
questionsSvc *questions.Service,
|
||||||
|
programSvc *programs.Service,
|
||||||
|
courseSvc *courses.Service,
|
||||||
|
moduleSvc *modules.Service,
|
||||||
|
lessonSvc *lessons.Service,
|
||||||
|
lmsProgressSvc *lmsprogress.Service,
|
||||||
|
practiceSvc *practices.Service,
|
||||||
subscriptionsSvc *subscriptions.Service,
|
subscriptionsSvc *subscriptions.Service,
|
||||||
arifpaySvc *arifpay.ArifpayService,
|
arifpaySvc *arifpay.ArifpayService,
|
||||||
issueReportingSvc *issuereporting.Service,
|
issueReportingSvc *issuereporting.Service,
|
||||||
|
|
@ -117,8 +130,13 @@ func NewApp(
|
||||||
|
|
||||||
s := &App{
|
s := &App{
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
courseSvc: courseSvc,
|
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
|
programSvc: programSvc,
|
||||||
|
courseSvc: courseSvc,
|
||||||
|
moduleSvc: moduleSvc,
|
||||||
|
lessonSvc: lessonSvc,
|
||||||
|
lmsProgressSvc: lmsProgressSvc,
|
||||||
|
practiceSvc: practiceSvc,
|
||||||
subscriptionsSvc: subscriptionsSvc,
|
subscriptionsSvc: subscriptionsSvc,
|
||||||
arifpaySvc: arifpaySvc,
|
arifpaySvc: arifpaySvc,
|
||||||
vimeoSvc: vimeoSvc,
|
vimeoSvc: vimeoSvc,
|
||||||
|
|
@ -154,8 +172,6 @@ func NewApp(
|
||||||
func (a *App) Run() error {
|
func (a *App) Run() error {
|
||||||
a.startAccountDeletionPurgeWorker()
|
a.startAccountDeletionPurgeWorker()
|
||||||
defer a.stopAccountDeletionPurgeWorker()
|
defer a.stopAccountDeletionPurgeWorker()
|
||||||
a.startInactiveSubModuleContentPurgeWorker()
|
|
||||||
defer a.stopInactiveSubModuleContentPurgeWorker()
|
|
||||||
return a.fiber.Listen(fmt.Sprintf(":%d", a.port))
|
return a.fiber.Listen(fmt.Sprintf(":%d", a.port))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,86 +236,3 @@ func (a *App) runAccountDeletionPurgeOnce(ctx context.Context, batchSize int32)
|
||||||
a.logger.Info("account deletion purge run completed", "deleted_count", deletedCount, "batch_size", batchSize)
|
a.logger.Info("account deletion purge run completed", "deleted_count", deletedCount, "batch_size", batchSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) startInactiveSubModuleContentPurgeWorker() {
|
|
||||||
if a.cfg == nil || !a.cfg.InactiveSubModuleContentPurgeEnabled {
|
|
||||||
a.logger.Info("inactive submodule content purge worker disabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
interval := a.cfg.InactiveSubModuleContentPurgeInterval
|
|
||||||
if interval <= 0 {
|
|
||||||
interval = 24 * time.Hour
|
|
||||||
}
|
|
||||||
|
|
||||||
retentionDays := a.cfg.InactiveSubModuleContentRetentionDays
|
|
||||||
if retentionDays < 1 {
|
|
||||||
retentionDays = 30
|
|
||||||
}
|
|
||||||
retention := time.Duration(retentionDays) * 24 * time.Hour
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
a.stopInactiveSubModuleContentPurge = cancel
|
|
||||||
|
|
||||||
a.logger.Info(
|
|
||||||
"starting inactive submodule content purge worker",
|
|
||||||
"interval", interval.String(),
|
|
||||||
"retention_days", retentionDays,
|
|
||||||
)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
a.runInactiveSubModuleContentPurgeOnce(ctx, retention)
|
|
||||||
|
|
||||||
ticker := time.NewTicker(interval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
a.logger.Info("inactive submodule content purge worker stopped")
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
a.runInactiveSubModuleContentPurgeOnce(ctx, retention)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) stopInactiveSubModuleContentPurgeWorker() {
|
|
||||||
if a.stopInactiveSubModuleContentPurge != nil {
|
|
||||||
a.stopInactiveSubModuleContentPurge()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) runInactiveSubModuleContentPurgeOnce(ctx context.Context, retention time.Duration) {
|
|
||||||
cutoff := time.Now().Add(-retention)
|
|
||||||
cutoffParam := pgtype.Timestamptz{Time: cutoff, Valid: true}
|
|
||||||
|
|
||||||
nLessons, err := a.analyticsDB.PurgeInactiveSubModuleLessonsBefore(ctx, cutoffParam)
|
|
||||||
if err != nil {
|
|
||||||
a.logger.Error("purge inactive submodule lessons failed", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nPractices, err := a.analyticsDB.PurgeInactiveSubModulePracticesBefore(ctx, cutoffParam)
|
|
||||||
if err != nil {
|
|
||||||
a.logger.Error("purge inactive submodule practices failed", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nCapstones, err := a.analyticsDB.PurgeInactiveSubModuleCapstonesBefore(ctx, cutoffParam)
|
|
||||||
if err != nil {
|
|
||||||
a.logger.Error("purge inactive submodule capstones failed", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if nLessons > 0 || nPractices > 0 || nCapstones > 0 {
|
|
||||||
a.logger.Info(
|
|
||||||
"inactive submodule content purge run completed",
|
|
||||||
"lessons_deleted", nLessons,
|
|
||||||
"practice_question_sets_deleted", nPractices,
|
|
||||||
"capstone_question_sets_deleted", nCapstones,
|
|
||||||
"cutoff", cutoff.UTC().Format(time.RFC3339),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
270
internal/web_server/handlers/course_handler.go
Normal file
270
internal/web_server/handlers/course_handler.go
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/services/courses"
|
||||||
|
"Yimaru-Backend/internal/services/programs"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateCourse godoc
|
||||||
|
// @Summary Create course
|
||||||
|
// @Description Create a course under a program
|
||||||
|
// @Tags courses
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Program ID"
|
||||||
|
// @Param body body domain.CreateCourseInput true "Course"
|
||||||
|
// @Success 201 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/programs/{id}/courses [post]
|
||||||
|
func (h *Handler) CreateCourse(c *fiber.Ctx) error {
|
||||||
|
programID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid program id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var req domain.CreateCourseInput
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
course, err := h.courseSvc.Create(c.Context(), programID, req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, programs.ErrProgramNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Program not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to create course",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
rid := course.ID
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseCreated, domain.ResourceCourse, &rid, "Created course: "+course.Name, nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
|
Message: "Course created successfully",
|
||||||
|
Data: course,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusCreated,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCoursesByProgram godoc
|
||||||
|
// @Summary List courses by program
|
||||||
|
// @Tags courses
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Program ID"
|
||||||
|
// @Param limit query int false "Page size" default(20)
|
||||||
|
// @Param offset query int false "Offset" default(0)
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/programs/{id}/courses [get]
|
||||||
|
func (h *Handler) ListCoursesByProgram(c *fiber.Ctx) error {
|
||||||
|
programID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid program id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||||
|
items, total, err := h.courseSvc.ListByProgram(c.Context(), programID, int32(limit), int32(offset))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, programs.ErrProgramNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Program not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to list courses",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
uid := c.Locals("user_id").(int64)
|
||||||
|
role := c.Locals("role").(domain.Role)
|
||||||
|
for i := range items {
|
||||||
|
if err := h.lmsProgressSvc.ApplyAccessCourse(c.Context(), role, uid, &items[i]); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to build course list",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Courses retrieved successfully",
|
||||||
|
Data: fiber.Map{
|
||||||
|
"courses": items,
|
||||||
|
"total_count": total,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
},
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCourse godoc
|
||||||
|
// @Summary Get course by ID
|
||||||
|
// @Tags courses
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Course ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/courses/{id} [get]
|
||||||
|
func (h *Handler) GetCourse(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 course id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
course, err := h.courseSvc.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Course not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to load course",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
uid := c.Locals("user_id").(int64)
|
||||||
|
role := c.Locals("role").(domain.Role)
|
||||||
|
if err := h.lmsProgressSvc.ApplyAccessCourse(c.Context(), role, uid, &course); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to evaluate course access",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := lmsBlockIfInaccessible(c, course.Access); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Course retrieved successfully",
|
||||||
|
Data: course,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCourse godoc
|
||||||
|
// @Summary Update course
|
||||||
|
// @Tags courses
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Course ID"
|
||||||
|
// @Param body body domain.UpdateCourseInput true "Fields to update"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/courses/{id} [put]
|
||||||
|
func (h *Handler) UpdateCourse(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 course id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var req domain.UpdateCourseInput
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
course, err := h.courseSvc.Update(c.Context(), id, req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Course not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to update course",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
rid := course.ID
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseUpdated, domain.ResourceCourse, &rid, "Updated course: "+course.Name, nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Course updated successfully",
|
||||||
|
Data: course,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCourse godoc
|
||||||
|
// @Summary Delete course
|
||||||
|
// @Tags courses
|
||||||
|
// @Param id path int true "Course ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/courses/{id} [delete]
|
||||||
|
func (h *Handler) DeleteCourse(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 course id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := h.courseSvc.Delete(c.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Course not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to delete course",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseDeleted, domain.ResourceCourse, &id, "Deleted course", nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Course deleted successfully",
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -10,13 +10,18 @@ import (
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
course_management "Yimaru-Backend/internal/services/course_management"
|
|
||||||
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
||||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||||
minioservice "Yimaru-Backend/internal/services/minio"
|
minioservice "Yimaru-Backend/internal/services/minio"
|
||||||
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
||||||
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/lessons"
|
||||||
|
"Yimaru-Backend/internal/services/lmsprogress"
|
||||||
|
"Yimaru-Backend/internal/services/modules"
|
||||||
|
"Yimaru-Backend/internal/services/practices"
|
||||||
|
"Yimaru-Backend/internal/services/programs"
|
||||||
"Yimaru-Backend/internal/services/questions"
|
"Yimaru-Backend/internal/services/questions"
|
||||||
"Yimaru-Backend/internal/services/recommendation"
|
"Yimaru-Backend/internal/services/recommendation"
|
||||||
"Yimaru-Backend/internal/services/subscriptions"
|
"Yimaru-Backend/internal/services/subscriptions"
|
||||||
|
|
@ -38,8 +43,13 @@ import (
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
assessmentSvc *assessment.Service
|
assessmentSvc *assessment.Service
|
||||||
courseMgmtSvc *course_management.Service
|
|
||||||
questionsSvc *questions.Service
|
questionsSvc *questions.Service
|
||||||
|
programSvc *programs.Service
|
||||||
|
courseSvc *courses.Service
|
||||||
|
moduleSvc *modules.Service
|
||||||
|
lessonSvc *lessons.Service
|
||||||
|
lmsProgressSvc *lmsprogress.Service
|
||||||
|
practiceSvc *practices.Service
|
||||||
subscriptionsSvc *subscriptions.Service
|
subscriptionsSvc *subscriptions.Service
|
||||||
arifpaySvc *arifpay.ArifpayService
|
arifpaySvc *arifpay.ArifpayService
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
|
@ -66,8 +76,13 @@ type Handler struct {
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
assessmentSvc *assessment.Service,
|
assessmentSvc *assessment.Service,
|
||||||
courseMgmtSvc *course_management.Service,
|
|
||||||
questionsSvc *questions.Service,
|
questionsSvc *questions.Service,
|
||||||
|
programSvc *programs.Service,
|
||||||
|
courseSvc *courses.Service,
|
||||||
|
moduleSvc *modules.Service,
|
||||||
|
lessonSvc *lessons.Service,
|
||||||
|
lmsProgressSvc *lmsprogress.Service,
|
||||||
|
practiceSvc *practices.Service,
|
||||||
subscriptionsSvc *subscriptions.Service,
|
subscriptionsSvc *subscriptions.Service,
|
||||||
arifpaySvc *arifpay.ArifpayService,
|
arifpaySvc *arifpay.ArifpayService,
|
||||||
logger *slog.Logger,
|
logger *slog.Logger,
|
||||||
|
|
@ -93,8 +108,13 @@ func New(
|
||||||
) *Handler {
|
) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
courseMgmtSvc: courseMgmtSvc,
|
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
|
programSvc: programSvc,
|
||||||
|
courseSvc: courseSvc,
|
||||||
|
moduleSvc: moduleSvc,
|
||||||
|
lessonSvc: lessonSvc,
|
||||||
|
lmsProgressSvc: lmsProgressSvc,
|
||||||
|
practiceSvc: practiceSvc,
|
||||||
subscriptionsSvc: subscriptionsSvc,
|
subscriptionsSvc: subscriptionsSvc,
|
||||||
arifpaySvc: arifpaySvc,
|
arifpaySvc: arifpaySvc,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
308
internal/web_server/handlers/lesson_handler.go
Normal file
308
internal/web_server/handlers/lesson_handler.go
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/services/lessons"
|
||||||
|
"Yimaru-Backend/internal/services/modules"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateLesson godoc
|
||||||
|
// @Summary Create lesson
|
||||||
|
// @Tags lessons
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param moduleId path int true "Module ID"
|
||||||
|
// @Param body body domain.CreateLessonInput true "Lesson"
|
||||||
|
// @Router /api/v1/modules/{moduleId}/lessons [post]
|
||||||
|
func (h *Handler) CreateLesson(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.CreateLessonInput
|
||||||
|
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.lessonSvc.Create(c.Context(), moduleID, req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, modules.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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
rid := les.ID
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionLessonCreated, domain.ResourceLesson, &rid, "Created lesson: "+les.Title, nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
|
Message: "Lesson created successfully",
|
||||||
|
Data: les,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusCreated,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListLessonsByModule godoc
|
||||||
|
// @Tags lessons
|
||||||
|
// @Param moduleId path int true "Module ID"
|
||||||
|
// @Param limit query int false "Page size" default(20)
|
||||||
|
// @Param offset query int false "Offset" default(0)
|
||||||
|
// @Router /api/v1/modules/{moduleId}/lessons [get]
|
||||||
|
func (h *Handler) ListLessonsByModule(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.lessonSvc.ListByModule(c.Context(), moduleID, int32(limit), int32(offset))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, modules.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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
uid := c.Locals("user_id").(int64)
|
||||||
|
role := c.Locals("role").(domain.Role)
|
||||||
|
for i := range items {
|
||||||
|
if err := h.lmsProgressSvc.ApplyAccessLesson(c.Context(), role, uid, &items[i]); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to build lesson list",
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLesson godoc
|
||||||
|
// @Tags lessons
|
||||||
|
// @Param id path int true "Lesson ID"
|
||||||
|
// @Router /api/v1/lessons/{id} [get]
|
||||||
|
func (h *Handler) GetLesson(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.lessonSvc.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, lessons.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 load lesson",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
uid := c.Locals("user_id").(int64)
|
||||||
|
role := c.Locals("role").(domain.Role)
|
||||||
|
if err := h.lmsProgressSvc.ApplyAccessLesson(c.Context(), role, uid, &les); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to evaluate lesson access",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := lmsBlockIfInaccessible(c, les.Access); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Lesson retrieved successfully",
|
||||||
|
Data: les,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLesson godoc
|
||||||
|
// @Tags lessons
|
||||||
|
// @Param id path int true "Lesson ID"
|
||||||
|
// @Param body body domain.UpdateLessonInput true "Fields to update"
|
||||||
|
// @Router /api/v1/lessons/{id} [put]
|
||||||
|
func (h *Handler) UpdateLesson(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.UpdateLessonInput
|
||||||
|
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.lessonSvc.Update(c.Context(), id, req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, lessons.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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
rid := les.ID
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionLessonUpdated, domain.ResourceLesson, &rid, "Updated lesson: "+les.Title, nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Lesson updated successfully",
|
||||||
|
Data: les,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteLesson godoc
|
||||||
|
// @Tags lessons
|
||||||
|
// @Param id path int true "Lesson ID"
|
||||||
|
// @Router /api/v1/lessons/{id} [delete]
|
||||||
|
func (h *Handler) DeleteLesson(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.lessonSvc.Delete(c.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, lessons.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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionLessonDeleted, domain.ResourceLesson, &id, "Deleted lesson", nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Lesson deleted successfully",
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteLesson godoc
|
||||||
|
// @Summary Mark a lesson as completed
|
||||||
|
// @Description Records lesson completion; may cascade to module, course, and program progress for the authenticated user. Learners must meet sequential prerequisites; staff bypass checks.
|
||||||
|
// @Tags lessons
|
||||||
|
// @Param id path int true "Lesson ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 403 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/lessons/{id}/complete [post]
|
||||||
|
func (h *Handler) CompleteLesson(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.lessonSvc.GetByID(c.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, lessons.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 load lesson",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
uid := c.Locals("user_id").(int64)
|
||||||
|
role := c.Locals("role").(domain.Role)
|
||||||
|
if role == domain.RoleStudent {
|
||||||
|
ok, reason, err := h.lmsProgressSvc.CanAccessLesson(c.Context(), uid, id)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to verify lesson access",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||||
|
Message: reason,
|
||||||
|
Error: "LMS_PREREQUISITE_NOT_MET",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := h.lmsProgressSvc.CompleteLessonForUser(c.Context(), uid, id); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to record lesson progress",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Lesson marked complete",
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
18
internal/web_server/handlers/lms_gating.go
Normal file
18
internal/web_server/handlers/lms_gating.go
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// lmsBlockIfInaccessible returns a 403 response when a learner is blocked; otherwise nil.
|
||||||
|
func lmsBlockIfInaccessible(c *fiber.Ctx, a *domain.LMSEntityAccess) error {
|
||||||
|
if a == nil || a.IsAccessible {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
||||||
|
Message: a.Reason,
|
||||||
|
Error: "LMS_PREREQUISITE_NOT_MET",
|
||||||
|
})
|
||||||
|
}
|
||||||
32
internal/web_server/handlers/lms_progress_handler.go
Normal file
32
internal/web_server/handlers/lms_progress_handler.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetMyLMSProgress godoc
|
||||||
|
// @Summary Get my LMS completion history
|
||||||
|
// @Description Returns completed lesson, module, course, and program IDs for the authenticated user (ordered by completion time, then id).
|
||||||
|
// @Tags lms
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/lms/progress [get]
|
||||||
|
func (h *Handler) GetMyLMSProgress(c *fiber.Ctx) error {
|
||||||
|
uid := c.Locals("user_id").(int64)
|
||||||
|
prog, err := h.lmsProgressSvc.GetMyProgress(c.Context(), uid)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to load learning progress",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "LMS progress retrieved successfully",
|
||||||
|
Data: prog,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
178
internal/web_server/handlers/lms_reorder_handler.go
Normal file
178
internal/web_server/handlers/lms_reorder_handler.go
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/services/courses"
|
||||||
|
"Yimaru-Backend/internal/services/programs"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReorderPrograms godoc
|
||||||
|
// @Summary Reorder all programs
|
||||||
|
// @Description Sets learning order of programs. Body must list every current program id exactly once, in the desired order (index 0 = first in path).
|
||||||
|
// @Tags programs
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body domain.ReorderIDsRequest true "New order: ordered_ids is the full set of program ids"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/programs/reorder [put]
|
||||||
|
func (h *Handler) ReorderPrograms(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 programs)",
|
||||||
|
Error: "missing ordered_ids",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := h.programSvc.Reorder(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 programs",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionProgramUpdated, domain.ResourceProgram, nil, "Reordered programs", nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Programs reordered successfully",
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderCoursesInProgram godoc
|
||||||
|
// @Summary Reorder courses within a program
|
||||||
|
// @Param id path int true "Program ID"
|
||||||
|
// @Param body body domain.ReorderIDsRequest true "ordered_ids: every course id in this program, in the new order"
|
||||||
|
// @Tags courses
|
||||||
|
// @Router /api/v1/programs/{id}/courses/reorder [put]
|
||||||
|
func (h *Handler) ReorderCoursesInProgram(c *fiber.Ctx) error {
|
||||||
|
programID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid program 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 the program has no courses)",
|
||||||
|
Error: "missing ordered_ids",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := h.courseSvc.ReorderInProgram(c.Context(), programID, req.OrderedIDs); err != nil {
|
||||||
|
if errors.Is(err, programs.ErrProgramNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Program 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 courses",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
msg := "Reordered courses in program"
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseUpdated, domain.ResourceProgram, &programID, msg, nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Courses reordered successfully",
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReorderModulesInCourse godoc
|
||||||
|
// @Summary Reorder modules within a course
|
||||||
|
// @Param courseId path int true "Course ID"
|
||||||
|
// @Param body body domain.ReorderIDsRequest true "ordered_ids: every module id in this course, in the new order"
|
||||||
|
// @Tags modules
|
||||||
|
// @Router /api/v1/courses/{courseId}/modules/reorder [put]
|
||||||
|
func (h *Handler) ReorderModulesInCourse(c *fiber.Ctx) error {
|
||||||
|
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid 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 the course has no modules)",
|
||||||
|
Error: "missing ordered_ids",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := h.moduleSvc.ReorderInCourse(c.Context(), courseID, req.OrderedIDs); err != nil {
|
||||||
|
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "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 modules",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionModuleUpdated, domain.ResourceCourse, &courseID, "Reordered modules in course", nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Modules reordered successfully",
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"Yimaru-Backend/internal/domain"
|
|
||||||
"crypto/subtle"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type resetAndReseedReq struct {
|
|
||||||
Confirm string `json:"confirm"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type clearCourseManagementReq struct {
|
|
||||||
Confirm string `json:"confirm"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractInsertStatement(sqlContent string, tableName string) (string, bool) {
|
|
||||||
pattern := fmt.Sprintf(`(?is)INSERT\s+INTO\s+%s\b.*?;`, regexp.QuoteMeta(tableName))
|
|
||||||
re := regexp.MustCompile(pattern)
|
|
||||||
statement := strings.TrimSpace(re.FindString(sqlContent))
|
|
||||||
if statement == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
return statement, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveSeedDir(seedDir string) (string, error) {
|
|
||||||
cleanSeedDir := strings.TrimSpace(seedDir)
|
|
||||||
if cleanSeedDir == "" {
|
|
||||||
cleanSeedDir = "db/data"
|
|
||||||
}
|
|
||||||
|
|
||||||
// If absolute, use directly.
|
|
||||||
if filepath.IsAbs(cleanSeedDir) {
|
|
||||||
info, err := os.Stat(cleanSeedDir)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
return "", fmt.Errorf("seed dir is not a directory: %s", cleanSeedDir)
|
|
||||||
}
|
|
||||||
return cleanSeedDir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates := make([]string, 0, 5)
|
|
||||||
|
|
||||||
// 1) Relative to current working directory.
|
|
||||||
candidates = append(candidates, filepath.Clean(cleanSeedDir))
|
|
||||||
|
|
||||||
// 2) Relative to executable directory (and parents).
|
|
||||||
if exePath, err := os.Executable(); err == nil {
|
|
||||||
exeDir := filepath.Dir(exePath)
|
|
||||||
candidates = append(candidates,
|
|
||||||
filepath.Join(exeDir, cleanSeedDir),
|
|
||||||
filepath.Join(exeDir, "..", cleanSeedDir),
|
|
||||||
filepath.Join(exeDir, "..", "..", cleanSeedDir),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, candidate := range candidates {
|
|
||||||
info, err := os.Stat(candidate)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
|
||||||
return candidate, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("seed directory not found (tried: %s)", strings.Join(candidates, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetAndReseedDatabase godoc
|
|
||||||
// @Summary Reset and reseed database
|
|
||||||
// @Description Truncates course_categories, courses, and sub_courses. If seed SQL contains INSERTs for those tables (e.g. 007_course_management_seed.sql), they are replayed; otherwise tables are left empty after truncate.
|
|
||||||
// @Tags internal
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param X-Seed-Reset-Token header string true "Reset token configured in DB_RESET_RESEED_TOKEN"
|
|
||||||
// @Param body body resetAndReseedReq true "Confirmation payload"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 403 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/internal/db/reset-reseed [post]
|
|
||||||
func (h *Handler) ResetAndReseedDatabase(c *fiber.Ctx) error {
|
|
||||||
if h.Cfg == nil || !h.Cfg.DBResetReseedEnabled {
|
|
||||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Operation is disabled",
|
|
||||||
Error: "DB_RESET_RESEED_ENABLED must be set to true",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var req resetAndReseedReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(req.Confirm) != "RESET_AND_RESEED" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Confirmation required",
|
|
||||||
Error: `set confirm to "RESET_AND_RESEED"`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedToken := strings.TrimSpace(h.Cfg.DBResetReseedToken)
|
|
||||||
if expectedToken != "" {
|
|
||||||
providedToken := strings.TrimSpace(c.Get("X-Seed-Reset-Token"))
|
|
||||||
if subtle.ConstantTimeCompare([]byte(providedToken), []byte(expectedToken)) != 1 {
|
|
||||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid reset token",
|
|
||||||
Error: "missing or invalid X-Seed-Reset-Token",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seedDir, err := resolveSeedDir(h.Cfg.DBSeedDir)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to resolve seed directory",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
seedCandidates := []string{
|
|
||||||
filepath.Join(seedDir, "007_course_management_seed.sql"),
|
|
||||||
filepath.Join(seedDir, "001_initial_seed_data.sql"),
|
|
||||||
}
|
|
||||||
tableNames := []string{"course_categories", "courses", "sub_courses"}
|
|
||||||
statements := map[string]string{}
|
|
||||||
statementSource := map[string]string{}
|
|
||||||
|
|
||||||
for _, file := range seedCandidates {
|
|
||||||
contentBytes, readErr := os.ReadFile(file)
|
|
||||||
if readErr != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
content := string(contentBytes)
|
|
||||||
for _, tableName := range tableNames {
|
|
||||||
if _, exists := statements[tableName]; exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if stmt, ok := extractInsertStatement(content, tableName); ok {
|
|
||||||
statements[tableName] = stmt
|
|
||||||
statementSource[tableName] = file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
missing := 0
|
|
||||||
for _, tableName := range tableNames {
|
|
||||||
if _, ok := statements[tableName]; !ok {
|
|
||||||
missing++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if missing != 0 && missing != len(tableNames) {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Incomplete course seed SQL",
|
|
||||||
Error: "seed files must define INSERT for all of course_categories, courses, and sub_courses, or none of them (truncate-only)",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var sqlBuilder strings.Builder
|
|
||||||
sqlBuilder.WriteString("BEGIN;\n")
|
|
||||||
sqlBuilder.WriteString("TRUNCATE TABLE sub_courses, courses, course_categories RESTART IDENTITY CASCADE;\n")
|
|
||||||
if missing == 0 {
|
|
||||||
for _, tableName := range tableNames {
|
|
||||||
sqlBuilder.WriteString("\n-- ")
|
|
||||||
sqlBuilder.WriteString(tableName)
|
|
||||||
sqlBuilder.WriteString(" from ")
|
|
||||||
sqlBuilder.WriteString(statementSource[tableName])
|
|
||||||
sqlBuilder.WriteString("\n")
|
|
||||||
sqlBuilder.WriteString(statements[tableName])
|
|
||||||
sqlBuilder.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sqlBuilder.WriteString("COMMIT;")
|
|
||||||
|
|
||||||
if _, err := h.analyticsDB.ExecRaw(c.Context(), sqlBuilder.String()); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to reset and reseed database",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := "Course management hierarchy reset and reseed completed successfully"
|
|
||||||
if missing == len(tableNames) {
|
|
||||||
msg = "Course management hierarchy truncated successfully (no INSERT seed configured; tables empty)"
|
|
||||||
}
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: msg,
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"seed_dir": seedDir,
|
|
||||||
"tables": tableNames,
|
|
||||||
"sources": statementSource,
|
|
||||||
"reseeded": missing == 0,
|
|
||||||
"truncate_only": missing == len(tableNames),
|
|
||||||
},
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearCourseManagementData godoc
|
|
||||||
// @Summary Clear course management hierarchy data only
|
|
||||||
// @Description Truncates course_categories, courses, and sub_courses (same scope as reset-reseed) without re-inserting seed SQL.
|
|
||||||
// @Tags internal
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param X-Seed-Reset-Token header string false "Optional token when DB_RESET_RESEED_TOKEN is set"
|
|
||||||
// @Param body body clearCourseManagementReq true "Confirmation payload"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 403 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/internal/db/clear-course-management [post]
|
|
||||||
func (h *Handler) ClearCourseManagementData(c *fiber.Ctx) error {
|
|
||||||
if h.Cfg == nil || !h.Cfg.DBResetReseedEnabled {
|
|
||||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Operation is disabled",
|
|
||||||
Error: "internal course management maintenance is disabled",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var req clearCourseManagementReq
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid request body",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(req.Confirm) != "CLEAR_COURSE_MANAGEMENT" {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Confirmation required",
|
|
||||||
Error: `set confirm to "CLEAR_COURSE_MANAGEMENT"`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedToken := strings.TrimSpace(h.Cfg.DBResetReseedToken)
|
|
||||||
if expectedToken != "" {
|
|
||||||
providedToken := strings.TrimSpace(c.Get("X-Seed-Reset-Token"))
|
|
||||||
if subtle.ConstantTimeCompare([]byte(providedToken), []byte(expectedToken)) != 1 {
|
|
||||||
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid reset token",
|
|
||||||
Error: "missing or invalid X-Seed-Reset-Token",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tableNames := []string{"course_categories", "courses", "sub_courses"}
|
|
||||||
sql := `BEGIN;
|
|
||||||
TRUNCATE TABLE sub_courses, courses, course_categories RESTART IDENTITY CASCADE;
|
|
||||||
COMMIT;`
|
|
||||||
|
|
||||||
if _, err := h.analyticsDB.ExecRaw(c.Context(), sql); err != nil {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Failed to clear course management data",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Course management hierarchy cleared successfully (no re-seed)",
|
|
||||||
Data: map[string]interface{}{
|
|
||||||
"tables": tableNames,
|
|
||||||
},
|
|
||||||
Success: true,
|
|
||||||
StatusCode: fiber.StatusOK,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
257
internal/web_server/handlers/module_handler.go
Normal file
257
internal/web_server/handlers/module_handler.go
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/services/courses"
|
||||||
|
"Yimaru-Backend/internal/services/modules"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateModule godoc
|
||||||
|
// @Summary Create module
|
||||||
|
// @Description Create a module under a course; parent program is taken from the course.
|
||||||
|
// @Tags modules
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param courseId path int true "Course ID"
|
||||||
|
// @Param body body domain.CreateModuleInput true "Module"
|
||||||
|
// @Success 201 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/courses/{courseId}/modules [post]
|
||||||
|
func (h *Handler) CreateModule(c *fiber.Ctx) error {
|
||||||
|
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid course id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var req domain.CreateModuleInput
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mod, err := h.moduleSvc.Create(c.Context(), courseID, req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Course not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to create module",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
rid := mod.ID
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionModuleCreated, domain.ResourceModule, &rid, "Created module: "+mod.Name, nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
|
Message: "Module created successfully",
|
||||||
|
Data: mod,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusCreated,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListModulesByCourse godoc
|
||||||
|
// @Summary List modules for a course
|
||||||
|
// @Tags modules
|
||||||
|
// @Produce json
|
||||||
|
// @Param courseId path int true "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/courses/{courseId}/modules [get]
|
||||||
|
func (h *Handler) ListModulesByCourse(c *fiber.Ctx) error {
|
||||||
|
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid course id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||||
|
items, total, err := h.moduleSvc.ListByCourse(c.Context(), courseID, int32(limit), int32(offset))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Course not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to list modules",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
uid := c.Locals("user_id").(int64)
|
||||||
|
role := c.Locals("role").(domain.Role)
|
||||||
|
for i := range items {
|
||||||
|
if err := h.lmsProgressSvc.ApplyAccessModule(c.Context(), role, uid, &items[i]); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to build module list",
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetModule godoc
|
||||||
|
// @Tags modules
|
||||||
|
// @Param id path int true "Module ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Router /api/v1/modules/{id} [get]
|
||||||
|
func (h *Handler) GetModule(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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mod, err := h.moduleSvc.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, modules.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 load module",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
uid := c.Locals("user_id").(int64)
|
||||||
|
role := c.Locals("role").(domain.Role)
|
||||||
|
if err := h.lmsProgressSvc.ApplyAccessModule(c.Context(), role, uid, &mod); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to evaluate module access",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := lmsBlockIfInaccessible(c, mod.Access); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Module retrieved successfully",
|
||||||
|
Data: mod,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateModule godoc
|
||||||
|
// @Tags modules
|
||||||
|
// @Param id path int true "Module ID"
|
||||||
|
// @Param body body domain.UpdateModuleInput true "Fields to update"
|
||||||
|
// @Router /api/v1/modules/{id} [put]
|
||||||
|
func (h *Handler) UpdateModule(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.UpdateModuleInput
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
mod, err := h.moduleSvc.Update(c.Context(), id, req)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, modules.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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
rid := mod.ID
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionModuleUpdated, domain.ResourceModule, &rid, "Updated module: "+mod.Name, nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Module updated successfully",
|
||||||
|
Data: mod,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteModule godoc
|
||||||
|
// @Tags modules
|
||||||
|
// @Param id path int true "Module ID"
|
||||||
|
// @Router /api/v1/modules/{id} [delete]
|
||||||
|
func (h *Handler) DeleteModule(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.moduleSvc.Delete(c.Context(), id); err != nil {
|
||||||
|
if errors.Is(err, modules.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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionModuleDeleted, domain.ResourceModule, &id, "Deleted module", nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Module deleted successfully",
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user