added program
This commit is contained in:
parent
9154dec067
commit
152478a96c
23
cmd/main.go
23
cmd/main.go
|
|
@ -14,10 +14,10 @@ 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"
|
||||||
|
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 +360,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 +388,9 @@ 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)
|
||||||
|
|
||||||
// Subscriptions service
|
// Subscriptions service
|
||||||
subscriptionsSvc := subscriptions.NewService(store)
|
subscriptionsSvc := subscriptions.NewService(store)
|
||||||
|
|
||||||
|
|
@ -442,8 +431,8 @@ func main() {
|
||||||
// Initialize and start HTTP server
|
// Initialize and start HTTP server
|
||||||
app := httpserver.NewApp(
|
app := httpserver.NewApp(
|
||||||
assessmentSvc,
|
assessmentSvc,
|
||||||
courseSvc,
|
|
||||||
questionsSvc,
|
questionsSvc,
|
||||||
|
programSvc,
|
||||||
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);
|
||||||
|
|
@ -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 *;
|
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
36
db/query/programs.sql
Normal file
36
db/query/programs.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
-- name: CreateProgram :one
|
||||||
|
INSERT INTO programs (name, description, thumbnail)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetProgramByID :one
|
||||||
|
SELECT *
|
||||||
|
FROM programs
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ListPrograms :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.thumbnail,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM programs p
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
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),
|
||||||
|
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`."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
237
gen/db/models.go
237
gen/db/models.go
|
|
@ -22,36 +22,6 @@ type ActivityLog struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Course struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CategoryID int64 `json:"category_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
|
|
@ -69,47 +39,11 @@ type GlobalSetting struct {
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Level struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CourseID int64 `json:"course_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"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LevelToSubCourse struct {
|
type LevelToSubCourse struct {
|
||||||
LevelID int64 `json:"level_id"`
|
LevelID int64 `json:"level_id"`
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
SubCourseID int64 `json:"sub_course_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Module struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
LevelID int64 `json:"level_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"`
|
|
||||||
IconUrl pgtype.Text `json:"icon_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModuleCapstone struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
ModuleID int64 `json:"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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModuleToSubCourse struct {
|
type ModuleToSubCourse struct {
|
||||||
ModuleID int64 `json:"module_id"`
|
ModuleID int64 `json:"module_id"`
|
||||||
SubCourseID int64 `json:"sub_course_id"`
|
SubCourseID int64 `json:"sub_course_id"`
|
||||||
|
|
@ -171,6 +105,15 @@ 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"`
|
||||||
|
}
|
||||||
|
|
||||||
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 +161,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 +257,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"`
|
||||||
|
|
@ -538,35 +354,12 @@ 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.Timestamptz `json:"completed_at"`
|
||||||
CompletedAt pgtype.Timestamp `json:"completed_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `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"`
|
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_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
|
||||||
|
|
|
||||||
162
gen/db/programs.sql.go
Normal file
162
gen/db/programs.sql.go
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
// 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)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id, name, description, thumbnail, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListPrograms = `-- name: ListPrograms :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER () AS total_count,
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.description,
|
||||||
|
p.thumbnail,
|
||||||
|
p.created_at,
|
||||||
|
p.updated_at
|
||||||
|
FROM programs p
|
||||||
|
ORDER BY p.created_at DESC
|
||||||
|
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"`
|
||||||
|
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.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),
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = $4
|
||||||
|
RETURNING id, name, description, thumbnail, created_at, updated_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateProgramParams struct {
|
||||||
|
Name pgtype.Text `json:"name"`
|
||||||
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
|
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.ID,
|
||||||
|
)
|
||||||
|
var i Program
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
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,9 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ResourceType string
|
type ResourceType string
|
||||||
|
|
@ -62,6 +65,7 @@ const (
|
||||||
ResourceQuestion ResourceType = "QUESTION"
|
ResourceQuestion ResourceType = "QUESTION"
|
||||||
ResourceQuestionSet ResourceType = "QUESTION_SET"
|
ResourceQuestionSet ResourceType = "QUESTION_SET"
|
||||||
ResourceIssue ResourceType = "ISSUE"
|
ResourceIssue ResourceType = "ISSUE"
|
||||||
|
ResourceProgram ResourceType = "PROGRAM"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ActivityLog struct {
|
type ActivityLog struct {
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
|
||||||
}
|
|
||||||
25
internal/domain/program.go
Normal file
25
internal/domain/program.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
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"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt *time.Time `json:"updated_at,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"`
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
14
internal/ports/program.go
Normal file
14
internal/ports/program.go
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
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)
|
||||||
|
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
|
|
||||||
}
|
|
||||||
112
internal/repository/programs.go
Normal file
112
internal/repository/programs.go
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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) 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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
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 (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),
|
||||||
|
})
|
||||||
|
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
|
|
||||||
}
|
|
||||||
69
internal/services/programs/service.go
Normal file
69
internal/services/programs/service.go
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,13 @@ var AllPermissions = []domain.PermissionSeed{
|
||||||
{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"},
|
||||||
|
|
||||||
// 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"},
|
||||||
|
|
@ -243,6 +250,9 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"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",
|
||||||
|
|
||||||
// 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",
|
||||||
|
|
@ -322,6 +332,8 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"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 + 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",
|
||||||
|
|
@ -418,6 +430,8 @@ var DefaultRolePermissions = map[string][]string{
|
||||||
"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,10 @@ 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/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,14 +35,13 @@ 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
|
||||||
subscriptionsSvc *subscriptions.Service
|
subscriptionsSvc *subscriptions.Service
|
||||||
arifpaySvc *arifpay.ArifpayService
|
arifpaySvc *arifpay.ArifpayService
|
||||||
issueReportingSvc *issuereporting.Service
|
issueReportingSvc *issuereporting.Service
|
||||||
vimeoSvc *vimeoservice.Service
|
vimeoSvc *vimeoservice.Service
|
||||||
|
|
@ -64,17 +63,16 @@ type App struct {
|
||||||
validator *customvalidator.CustomValidator
|
validator *customvalidator.CustomValidator
|
||||||
JwtConfig jwtutil.JwtConfig
|
JwtConfig jwtutil.JwtConfig
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
mongoLoggerSvc *zap.Logger
|
mongoLoggerSvc *zap.Logger
|
||||||
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,
|
||||||
subscriptionsSvc *subscriptions.Service,
|
subscriptionsSvc *subscriptions.Service,
|
||||||
arifpaySvc *arifpay.ArifpayService,
|
arifpaySvc *arifpay.ArifpayService,
|
||||||
issueReportingSvc *issuereporting.Service,
|
issueReportingSvc *issuereporting.Service,
|
||||||
|
|
@ -117,8 +115,8 @@ func NewApp(
|
||||||
|
|
||||||
s := &App{
|
s := &App{
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
courseSvc: courseSvc,
|
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
|
programSvc: programSvc,
|
||||||
subscriptionsSvc: subscriptionsSvc,
|
subscriptionsSvc: subscriptionsSvc,
|
||||||
arifpaySvc: arifpaySvc,
|
arifpaySvc: arifpaySvc,
|
||||||
vimeoSvc: vimeoSvc,
|
vimeoSvc: vimeoSvc,
|
||||||
|
|
@ -154,8 +152,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 +216,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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@ 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/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"
|
||||||
|
|
@ -37,10 +37,10 @@ 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
|
||||||
subscriptionsSvc *subscriptions.Service
|
subscriptionsSvc *subscriptions.Service
|
||||||
arifpaySvc *arifpay.ArifpayService
|
arifpaySvc *arifpay.ArifpayService
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
settingSvc *settings.Service
|
settingSvc *settings.Service
|
||||||
|
|
@ -66,8 +66,8 @@ 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,
|
||||||
subscriptionsSvc *subscriptions.Service,
|
subscriptionsSvc *subscriptions.Service,
|
||||||
arifpaySvc *arifpay.ArifpayService,
|
arifpaySvc *arifpay.ArifpayService,
|
||||||
logger *slog.Logger,
|
logger *slog.Logger,
|
||||||
|
|
@ -93,8 +93,8 @@ func New(
|
||||||
) *Handler {
|
) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
assessmentSvc: assessmentSvc,
|
assessmentSvc: assessmentSvc,
|
||||||
courseMgmtSvc: courseMgmtSvc,
|
|
||||||
questionsSvc: questionsSvc,
|
questionsSvc: questionsSvc,
|
||||||
|
programSvc: programSvc,
|
||||||
subscriptionsSvc: subscriptionsSvc,
|
subscriptionsSvc: subscriptionsSvc,
|
||||||
arifpaySvc: arifpaySvc,
|
arifpaySvc: arifpaySvc,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
227
internal/web_server/handlers/program_handler.go
Normal file
227
internal/web_server/handlers/program_handler.go
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/services/programs"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateProgram godoc
|
||||||
|
// @Summary Create program
|
||||||
|
// @Description Create a top-level LMS program
|
||||||
|
// @Tags programs
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body domain.CreateProgramInput true "Program"
|
||||||
|
// @Success 201 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/programs [post]
|
||||||
|
func (h *Handler) CreateProgram(c *fiber.Ctx) error {
|
||||||
|
var req domain.CreateProgramInput
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Validation failed",
|
||||||
|
Error: firstValidationError(valErrs),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
p, err := h.programSvc.Create(c.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to create program",
|
||||||
|
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.ActionProgramCreated, domain.ResourceProgram, &p.ID, "Created program: "+p.Name, nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
|
Message: "Program created successfully",
|
||||||
|
Data: p,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusCreated,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPrograms godoc
|
||||||
|
// @Summary List programs
|
||||||
|
// @Description Paginated list of programs
|
||||||
|
// @Tags programs
|
||||||
|
// @Produce json
|
||||||
|
// @Param limit query int false "Page size" default(20)
|
||||||
|
// @Param offset query int false "Offset" default(0)
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/programs [get]
|
||||||
|
func (h *Handler) ListPrograms(c *fiber.Ctx) error {
|
||||||
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||||
|
items, total, err := h.programSvc.List(c.Context(), int32(limit), int32(offset))
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to list programs",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Programs retrieved successfully",
|
||||||
|
Data: fiber.Map{
|
||||||
|
"programs": items,
|
||||||
|
"total_count": total,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
},
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProgram godoc
|
||||||
|
// @Summary Get program by ID
|
||||||
|
// @Tags programs
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Program ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/programs/{id} [get]
|
||||||
|
func (h *Handler) GetProgram(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 program id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
p, err := h.programSvc.GetByID(c.Context(), id)
|
||||||
|
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 load program",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Program retrieved successfully",
|
||||||
|
Data: p,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProgram godoc
|
||||||
|
// @Summary Update program
|
||||||
|
// @Tags programs
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Program ID"
|
||||||
|
// @Param body body domain.UpdateProgramInput true "Fields to update"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/programs/{id} [put]
|
||||||
|
func (h *Handler) UpdateProgram(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 program id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var req domain.UpdateProgramInput
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
p, err := h.programSvc.Update(c.Context(), id, 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 update program",
|
||||||
|
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, &p.ID, "Updated program: "+p.Name, nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Program updated successfully",
|
||||||
|
Data: p,
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProgram godoc
|
||||||
|
// @Summary Delete program
|
||||||
|
// @Tags programs
|
||||||
|
// @Param id path int true "Program ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/programs/{id} [delete]
|
||||||
|
func (h *Handler) DeleteProgram(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 program id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := h.programSvc.Delete(c.Context(), id); 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 delete program",
|
||||||
|
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.ActionProgramDeleted, domain.ResourceProgram, &id, "Deleted program", nil, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Program deleted successfully",
|
||||||
|
Success: true,
|
||||||
|
StatusCode: fiber.StatusOK,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstValidationError(errs map[string]string) string {
|
||||||
|
for _, v := range errs {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "validation error"
|
||||||
|
}
|
||||||
|
|
@ -524,7 +524,6 @@ type createQuestionSetReq struct {
|
||||||
PassingScore *int32 `json:"passing_score"`
|
PassingScore *int32 `json:"passing_score"`
|
||||||
ShuffleQuestions *bool `json:"shuffle_questions"`
|
ShuffleQuestions *bool `json:"shuffle_questions"`
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
SubCourseVideoID *int64 `json:"sub_course_video_id"`
|
|
||||||
IntroVideoURL *string `json:"intro_video_url"`
|
IntroVideoURL *string `json:"intro_video_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -540,9 +539,8 @@ type questionSetRes struct {
|
||||||
TimeLimitMinutes *int32 `json:"time_limit_minutes,omitempty"`
|
TimeLimitMinutes *int32 `json:"time_limit_minutes,omitempty"`
|
||||||
PassingScore *int32 `json:"passing_score,omitempty"`
|
PassingScore *int32 `json:"passing_score,omitempty"`
|
||||||
ShuffleQuestions bool `json:"shuffle_questions"`
|
ShuffleQuestions bool `json:"shuffle_questions"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
SubCourseVideoID *int64 `json:"sub_course_video_id,omitempty"`
|
IntroVideoURL *string `json:"intro_video_url,omitempty"`
|
||||||
IntroVideoURL *string `json:"intro_video_url,omitempty"`
|
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
QuestionCount *int64 `json:"question_count,omitempty"`
|
QuestionCount *int64 `json:"question_count,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
@ -552,18 +550,20 @@ type listQuestionSetsRes struct {
|
||||||
TotalCount int64 `json:"total_count"`
|
TotalCount int64 `json:"total_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func isSubCoursePractice(set domain.QuestionSet) bool {
|
func isSequenceGatedPractice(set domain.QuestionSet) bool {
|
||||||
return strings.EqualFold(set.SetType, string(domain.QuestionSetTypePractice)) &&
|
if !strings.EqualFold(set.SetType, string(domain.QuestionSetTypePractice)) || set.OwnerType == nil {
|
||||||
set.OwnerType != nil &&
|
return false
|
||||||
strings.EqualFold(*set.OwnerType, "SUB_COURSE")
|
}
|
||||||
|
ot := strings.ToUpper(strings.TrimSpace(*set.OwnerType))
|
||||||
|
return ot == "SUB_COURSE" || ot == "SUB_MODULE"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) enforcePracticeSequenceForStudent(c *fiber.Ctx, set domain.QuestionSet) error {
|
func (h *Handler) enforcePracticeSequenceForStudent(c *fiber.Ctx, set domain.QuestionSet) error {
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
if role != domain.RoleStudent || !isSubCoursePractice(set) {
|
if role != domain.RoleStudent || !isSequenceGatedPractice(set) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !strings.EqualFold(set.Status, string(domain.ContentStatusPublished)) {
|
if !strings.EqualFold(set.Status, "PUBLISHED") {
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Practice not found")
|
return fiber.NewError(fiber.StatusNotFound, "Practice not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -607,10 +607,10 @@ func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.EqualFold(req.SetType, string(domain.QuestionSetTypeInitialAssessment)) {
|
if strings.EqualFold(req.SetType, string(domain.QuestionSetTypeInitialAssessment)) {
|
||||||
if req.OwnerType == nil || !strings.EqualFold(*req.OwnerType, "SUB_COURSE") || req.OwnerID == nil {
|
if req.OwnerType == nil || req.OwnerID == nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
Message: "Invalid initial assessment ownership",
|
Message: "Invalid initial assessment ownership",
|
||||||
Error: "INITIAL_ASSESSMENT question sets must include owner_type=SUB_COURSE and owner_id",
|
Error: "INITIAL_ASSESSMENT question sets must include owner_type and owner_id",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -627,7 +627,6 @@ func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error {
|
||||||
PassingScore: req.PassingScore,
|
PassingScore: req.PassingScore,
|
||||||
ShuffleQuestions: req.ShuffleQuestions,
|
ShuffleQuestions: req.ShuffleQuestions,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
SubCourseVideoID: req.SubCourseVideoID,
|
|
||||||
IntroVideoURL: req.IntroVideoURL,
|
IntroVideoURL: req.IntroVideoURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -661,67 +660,12 @@ func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error {
|
||||||
PassingScore: set.PassingScore,
|
PassingScore: set.PassingScore,
|
||||||
ShuffleQuestions: set.ShuffleQuestions,
|
ShuffleQuestions: set.ShuffleQuestions,
|
||||||
Status: set.Status,
|
Status: set.Status,
|
||||||
SubCourseVideoID: set.SubCourseVideoID,
|
|
||||||
IntroVideoURL: set.IntroVideoURL,
|
IntroVideoURL: set.IntroVideoURL,
|
||||||
CreatedAt: set.CreatedAt.String(),
|
CreatedAt: set.CreatedAt.String(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSubCourseEntryAssessmentSet godoc
|
|
||||||
// @Summary Get entry assessment set for a sub-course
|
|
||||||
// @Description Returns the published INITIAL_ASSESSMENT question set for the given sub-course
|
|
||||||
// @Tags question-sets
|
|
||||||
// @Produce json
|
|
||||||
// @Param subCourseId path int true "Sub-course ID"
|
|
||||||
// @Success 200 {object} domain.Response
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Failure 404 {object} domain.ErrorResponse
|
|
||||||
// @Failure 500 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/question-sets/sub-courses/{subCourseId}/entry-assessment [get]
|
|
||||||
func (h *Handler) GetSubCourseEntryAssessmentSet(c *fiber.Ctx) error {
|
|
||||||
subCourseIDStr := c.Params("subCourseId")
|
|
||||||
subCourseID, err := strconv.ParseInt(subCourseIDStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Invalid sub-course ID",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
set, err := h.questionsSvc.GetSubCourseInitialAssessmentSet(c.Context(), subCourseID)
|
|
||||||
if err != nil {
|
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
||||||
Message: "Entry assessment set not found for sub-course",
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
count, _ := h.questionsSvc.CountQuestionsInSet(c.Context(), set.ID)
|
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
|
||||||
Message: "Entry assessment set retrieved successfully",
|
|
||||||
Data: questionSetRes{
|
|
||||||
ID: set.ID,
|
|
||||||
Title: set.Title,
|
|
||||||
Description: set.Description,
|
|
||||||
SetType: set.SetType,
|
|
||||||
OwnerType: set.OwnerType,
|
|
||||||
OwnerID: set.OwnerID,
|
|
||||||
BannerImage: set.BannerImage,
|
|
||||||
Persona: set.Persona,
|
|
||||||
TimeLimitMinutes: set.TimeLimitMinutes,
|
|
||||||
PassingScore: set.PassingScore,
|
|
||||||
ShuffleQuestions: set.ShuffleQuestions,
|
|
||||||
Status: set.Status,
|
|
||||||
SubCourseVideoID: set.SubCourseVideoID,
|
|
||||||
IntroVideoURL: set.IntroVideoURL,
|
|
||||||
CreatedAt: set.CreatedAt.String(),
|
|
||||||
QuestionCount: &count,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetQuestionSetByID godoc
|
// GetQuestionSetByID godoc
|
||||||
// @Summary Get question set by ID
|
// @Summary Get question set by ID
|
||||||
// @Description Returns a question set with question count
|
// @Description Returns a question set with question count
|
||||||
|
|
@ -778,7 +722,6 @@ func (h *Handler) GetQuestionSetByID(c *fiber.Ctx) error {
|
||||||
PassingScore: set.PassingScore,
|
PassingScore: set.PassingScore,
|
||||||
ShuffleQuestions: set.ShuffleQuestions,
|
ShuffleQuestions: set.ShuffleQuestions,
|
||||||
Status: set.Status,
|
Status: set.Status,
|
||||||
SubCourseVideoID: set.SubCourseVideoID,
|
|
||||||
IntroVideoURL: set.IntroVideoURL,
|
IntroVideoURL: set.IntroVideoURL,
|
||||||
CreatedAt: set.CreatedAt.String(),
|
CreatedAt: set.CreatedAt.String(),
|
||||||
QuestionCount: &count,
|
QuestionCount: &count,
|
||||||
|
|
@ -834,7 +777,6 @@ func (h *Handler) GetQuestionSetsByType(c *fiber.Ctx) error {
|
||||||
PassingScore: s.PassingScore,
|
PassingScore: s.PassingScore,
|
||||||
ShuffleQuestions: s.ShuffleQuestions,
|
ShuffleQuestions: s.ShuffleQuestions,
|
||||||
Status: s.Status,
|
Status: s.Status,
|
||||||
SubCourseVideoID: s.SubCourseVideoID,
|
|
||||||
IntroVideoURL: s.IntroVideoURL,
|
IntroVideoURL: s.IntroVideoURL,
|
||||||
CreatedAt: s.CreatedAt.String(),
|
CreatedAt: s.CreatedAt.String(),
|
||||||
})
|
})
|
||||||
|
|
@ -901,7 +843,6 @@ func (h *Handler) GetQuestionSetsByOwner(c *fiber.Ctx) error {
|
||||||
PassingScore: s.PassingScore,
|
PassingScore: s.PassingScore,
|
||||||
ShuffleQuestions: s.ShuffleQuestions,
|
ShuffleQuestions: s.ShuffleQuestions,
|
||||||
Status: s.Status,
|
Status: s.Status,
|
||||||
SubCourseVideoID: s.SubCourseVideoID,
|
|
||||||
IntroVideoURL: s.IntroVideoURL,
|
IntroVideoURL: s.IntroVideoURL,
|
||||||
CreatedAt: s.CreatedAt.String(),
|
CreatedAt: s.CreatedAt.String(),
|
||||||
})
|
})
|
||||||
|
|
@ -923,9 +864,8 @@ type updateQuestionSetReq struct {
|
||||||
TimeLimitMinutes *int32 `json:"time_limit_minutes"`
|
TimeLimitMinutes *int32 `json:"time_limit_minutes"`
|
||||||
PassingScore *int32 `json:"passing_score"`
|
PassingScore *int32 `json:"passing_score"`
|
||||||
ShuffleQuestions *bool `json:"shuffle_questions"`
|
ShuffleQuestions *bool `json:"shuffle_questions"`
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
SubCourseVideoID *int64 `json:"sub_course_video_id"`
|
IntroVideoURL *string `json:"intro_video_url"`
|
||||||
IntroVideoURL *string `json:"intro_video_url"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateQuestionSet godoc
|
// UpdateQuestionSet godoc
|
||||||
|
|
@ -972,7 +912,6 @@ func (h *Handler) UpdateQuestionSet(c *fiber.Ctx) error {
|
||||||
PassingScore: req.PassingScore,
|
PassingScore: req.PassingScore,
|
||||||
ShuffleQuestions: req.ShuffleQuestions,
|
ShuffleQuestions: req.ShuffleQuestions,
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
SubCourseVideoID: req.SubCourseVideoID,
|
|
||||||
IntroVideoURL: req.IntroVideoURL,
|
IntroVideoURL: req.IntroVideoURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1319,7 +1258,7 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error {
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if !isSubCoursePractice(set) || !strings.EqualFold(set.Status, string(domain.ContentStatusPublished)) {
|
if !isSequenceGatedPractice(set) || !strings.EqualFold(set.Status, "PUBLISHED") {
|
||||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
Message: "Practice not found",
|
Message: "Practice not found",
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ import (
|
||||||
func (a *App) initAppRoutes() {
|
func (a *App) initAppRoutes() {
|
||||||
h := handlers.New(
|
h := handlers.New(
|
||||||
a.assessmentSvc,
|
a.assessmentSvc,
|
||||||
a.courseSvc,
|
|
||||||
a.questionsSvc,
|
a.questionsSvc,
|
||||||
|
a.programSvc,
|
||||||
a.subscriptionsSvc,
|
a.subscriptionsSvc,
|
||||||
a.arifpaySvc,
|
a.arifpaySvc,
|
||||||
a.logger,
|
a.logger,
|
||||||
|
|
@ -67,6 +67,13 @@ func (a *App) initAppRoutes() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Programs (LMS top-level)
|
||||||
|
groupV1.Post("/programs", a.authMiddleware, a.RequirePermission("programs.create"), h.CreateProgram)
|
||||||
|
groupV1.Get("/programs", a.authMiddleware, a.RequirePermission("programs.list"), h.ListPrograms)
|
||||||
|
groupV1.Get("/programs/:id", a.authMiddleware, a.RequirePermission("programs.get"), h.GetProgram)
|
||||||
|
groupV1.Put("/programs/:id", a.authMiddleware, a.RequirePermission("programs.update"), h.UpdateProgram)
|
||||||
|
groupV1.Delete("/programs/:id", a.authMiddleware, a.RequirePermission("programs.delete"), h.DeleteProgram)
|
||||||
|
|
||||||
// File storage (MinIO)
|
// File storage (MinIO)
|
||||||
groupV1.Get("/files/url", a.authMiddleware, h.GetFileURL)
|
groupV1.Get("/files/url", a.authMiddleware, h.GetFileURL)
|
||||||
groupV1.Post("/files/upload", a.authMiddleware, h.UploadMedia)
|
groupV1.Post("/files/upload", a.authMiddleware, h.UploadMedia)
|
||||||
|
|
@ -78,68 +85,6 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Get("/assessment/questions", h.ListAssessmentQuestions)
|
groupV1.Get("/assessment/questions", h.ListAssessmentQuestions)
|
||||||
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
|
||||||
|
|
||||||
// Unified Course Management (single hierarchy model)
|
|
||||||
groupV1.Get("/course-management/categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseCategories)
|
|
||||||
groupV1.Get("/course-management/categories/:categoryId/sub-categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseSubCategoriesByCategory)
|
|
||||||
groupV1.Get("/course-management/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllCourses)
|
|
||||||
groupV1.Get("/course-management/human-language/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListHumanLanguageCourses)
|
|
||||||
groupV1.Get("/course-management/sub-categories/:subCategoryId/courses", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCoursesBySubCategory)
|
|
||||||
groupV1.Get("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetCourseByID)
|
|
||||||
groupV1.Get("/course-management/levels", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllLevels)
|
|
||||||
groupV1.Get("/course-management/courses/:courseId/levels", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListLevelsByCourse)
|
|
||||||
groupV1.Get("/course-management/levels/:levelId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetLevelByID)
|
|
||||||
groupV1.Get("/course-management/modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllModules)
|
|
||||||
groupV1.Get("/course-management/levels/:levelId/modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListModulesByLevel)
|
|
||||||
groupV1.Get("/course-management/modules/:moduleId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetModuleByID)
|
|
||||||
groupV1.Get("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListAllSubModules)
|
|
||||||
groupV1.Get("/course-management/modules/:moduleId/sub-modules", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListSubModulesByModule)
|
|
||||||
groupV1.Get("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleByID)
|
|
||||||
groupV1.Post("/course-management/categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseCategory)
|
|
||||||
groupV1.Delete("/course-management/categories/:categoryId", a.authMiddleware, a.RequirePermission("course_categories.delete"), h.DeleteCourseCategory)
|
|
||||||
groupV1.Post("/course-management/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
|
|
||||||
groupV1.Put("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse)
|
|
||||||
groupV1.Delete("/course-management/courses/:courseId", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse)
|
|
||||||
groupV1.Post("/course-management/courses/:courseId/thumbnail", a.authMiddleware, a.RequirePermission("courses.upload_thumbnail"), h.UpdateCourseThumbnail)
|
|
||||||
groupV1.Get("/course-management/courses/:courseId/learning-path", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.CourseLearningPath)
|
|
||||||
groupV1.Get("/course-management/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy)
|
|
||||||
groupV1.Get("/course-management/human-language/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchy)
|
|
||||||
groupV1.Get("/course-management/courses/:courseId/hierarchy", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.UnifiedHierarchyByCourse)
|
|
||||||
groupV1.Get("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListCourseSubCategories)
|
|
||||||
groupV1.Get("/course-management/human-language/sub-categories", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.ListHumanLanguageCourseSubCategories)
|
|
||||||
groupV1.Post("/course-management/sub-categories", a.authMiddleware, a.RequirePermission("course_categories.create"), h.CreateCourseSubCategory)
|
|
||||||
groupV1.Delete("/course-management/sub-categories/:subCategoryId", a.authMiddleware, a.RequirePermission("course_categories.delete"), h.DeleteCourseSubCategory)
|
|
||||||
groupV1.Post("/course-management/levels", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateLevel)
|
|
||||||
groupV1.Put("/course-management/levels/:levelId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateLevel)
|
|
||||||
groupV1.Post("/course-management/modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateModule)
|
|
||||||
groupV1.Put("/course-management/modules/:moduleId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateModule)
|
|
||||||
groupV1.Delete("/course-management/modules/:moduleId", a.authMiddleware, a.RequirePermission("subcourses.delete"), h.DeleteModule)
|
|
||||||
groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule)
|
|
||||||
groupV1.Put("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubModule)
|
|
||||||
groupV1.Delete("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.delete"), h.DeleteSubModule)
|
|
||||||
groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo)
|
|
||||||
groupV1.Put("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubModuleVideo)
|
|
||||||
groupV1.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo)
|
|
||||||
groupV1.Get("/course-management/sub-modules/:subModuleId/lessons", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleLessons)
|
|
||||||
groupV1.Get("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("learning_tree.get"), h.GetSubModuleLessonByID)
|
|
||||||
groupV1.Put("/course-management/sub-module-lessons/:lessonId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubModuleLesson)
|
|
||||||
groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModuleLesson)
|
|
||||||
groupV1.Get("/course-management/sub-modules/:subModuleId/practices", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetSubModulePractices)
|
|
||||||
groupV1.Get("/course-management/practices/:practiceId/detail", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModulePracticeDetail)
|
|
||||||
groupV1.Get("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModulePracticeByID)
|
|
||||||
groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice)
|
|
||||||
groupV1.Put("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdatePractice)
|
|
||||||
groupV1.Delete("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeletePractice)
|
|
||||||
groupV1.Get("/course-management/sub-modules/:subModuleId/capstones", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetSubModuleCapstones)
|
|
||||||
groupV1.Get("/course-management/capstones/:capstoneId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetSubModuleCapstoneByID)
|
|
||||||
groupV1.Post("/course-management/sub-module-capstones", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModuleCapstone)
|
|
||||||
groupV1.Put("/course-management/capstones/:capstoneId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdateSubModuleCapstone)
|
|
||||||
groupV1.Delete("/course-management/capstones/:capstoneId", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeleteCapstone)
|
|
||||||
groupV1.Get("/course-management/modules/:moduleId/capstones", a.authMiddleware, a.RequirePermission("question_sets.list"), h.GetModuleCapstones)
|
|
||||||
groupV1.Get("/course-management/module-capstones/:moduleCapstoneId", a.authMiddleware, a.RequirePermission("question_sets.get"), h.GetModuleCapstoneByID)
|
|
||||||
groupV1.Post("/course-management/module-capstones", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateModuleCapstone)
|
|
||||||
groupV1.Put("/course-management/module-capstones/:moduleCapstoneId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdateModuleCapstone)
|
|
||||||
groupV1.Delete("/course-management/module-capstones/:moduleCapstoneId", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeleteModuleCapstone)
|
|
||||||
|
|
||||||
// Questions
|
// Questions
|
||||||
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)
|
||||||
groupV1.Get("/questions", a.authMiddleware, a.RequirePermission("questions.list"), h.ListQuestions)
|
groupV1.Get("/questions", a.authMiddleware, a.RequirePermission("questions.list"), h.ListQuestions)
|
||||||
|
|
@ -255,8 +200,6 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Delete("/user/me", a.authMiddleware, a.RequirePermission("users.delete_self"), h.DeleteMyUserAccount)
|
groupV1.Delete("/user/me", a.authMiddleware, a.RequirePermission("users.delete_self"), h.DeleteMyUserAccount)
|
||||||
groupV1.Post("/user/me/deletion/cancel", a.authMiddleware, a.RequirePermission("users.cancel_delete_self"), h.CancelMyUserAccountDeletion)
|
groupV1.Post("/user/me/deletion/cancel", a.authMiddleware, a.RequirePermission("users.cancel_delete_self"), h.CancelMyUserAccountDeletion)
|
||||||
groupV1.Post("/internal/users/purge-due-deletions", a.authMiddleware, a.RequirePermission("users.purge_due_deletions"), h.PurgeDueDeletedUsers)
|
groupV1.Post("/internal/users/purge-due-deletions", a.authMiddleware, a.RequirePermission("users.purge_due_deletions"), h.PurgeDueDeletedUsers)
|
||||||
groupV1.Post("/internal/db/reset-reseed", a.authMiddleware, a.RequirePermission("internal.db.reset_reseed"), h.ResetAndReseedDatabase)
|
|
||||||
groupV1.Post("/internal/db/clear-course-management", a.authMiddleware, a.RequirePermission("internal.db.reset_reseed"), h.ClearCourseManagementData)
|
|
||||||
groupV1.Get("/user/single/:id", a.authMiddleware, a.RequirePermission("users.get"), h.GetUserByID)
|
groupV1.Get("/user/single/:id", a.authMiddleware, a.RequirePermission("users.get"), h.GetUserByID)
|
||||||
groupV1.Delete("/user/delete/:id", a.authMiddleware, a.RequirePermission("users.delete"), h.DeleteUser)
|
groupV1.Delete("/user/delete/:id", a.authMiddleware, a.RequirePermission("users.delete"), h.DeleteUser)
|
||||||
groupV1.Post("/user/search", a.authMiddleware, a.RequirePermission("users.search"), h.SearchUserByNameOrPhone)
|
groupV1.Post("/user/search", a.authMiddleware, a.RequirePermission("users.search"), h.SearchUserByNameOrPhone)
|
||||||
|
|
@ -343,8 +286,6 @@ func (a *App) initAppRoutes() {
|
||||||
teamGroup.Delete("/members/:id", a.authMiddleware, a.RequirePermission("team.members.delete"), h.DeleteTeamMember)
|
teamGroup.Delete("/members/:id", a.authMiddleware, a.RequirePermission("team.members.delete"), h.DeleteTeamMember)
|
||||||
teamGroup.Post("/members/:id/change-password", a.authMiddleware, a.RequirePermission("team.members.change_password"), h.ChangeTeamMemberPassword)
|
teamGroup.Post("/members/:id/change-password", a.authMiddleware, a.RequirePermission("team.members.change_password"), h.ChangeTeamMemberPassword)
|
||||||
|
|
||||||
// Legacy sub-course prerequisite/progression routes removed after hierarchy cutover.
|
|
||||||
|
|
||||||
// Ratings
|
// Ratings
|
||||||
groupV1.Post("/ratings", a.authMiddleware, a.RequirePermission("ratings.submit"), h.SubmitRating)
|
groupV1.Post("/ratings", a.authMiddleware, a.RequirePermission("ratings.submit"), h.SubmitRating)
|
||||||
groupV1.Get("/ratings", a.authMiddleware, a.RequirePermission("ratings.list_by_target"), h.GetRatingsByTarget)
|
groupV1.Get("/ratings", a.authMiddleware, a.RequirePermission("ratings.list_by_target"), h.GetRatingsByTarget)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user