module+lesson+practice implementations
This commit is contained in:
parent
152478a96c
commit
9db9c9899a
20
cmd/main.go
20
cmd/main.go
|
|
@ -17,6 +17,10 @@ import (
|
|||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||
"Yimaru-Backend/internal/services/messenger"
|
||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||
coursesservice "Yimaru-Backend/internal/services/courses"
|
||||
lessonsservice "Yimaru-Backend/internal/services/lessons"
|
||||
moduleservice "Yimaru-Backend/internal/services/modules"
|
||||
practicesservice "Yimaru-Backend/internal/services/practices"
|
||||
programsservice "Yimaru-Backend/internal/services/programs"
|
||||
"Yimaru-Backend/internal/services/questions"
|
||||
"Yimaru-Backend/internal/services/recommendation"
|
||||
|
|
@ -391,6 +395,18 @@ func main() {
|
|||
// LMS programs (top-level hierarchy)
|
||||
programSvc := programsservice.NewService(store)
|
||||
|
||||
// LMS courses (under programs)
|
||||
courseSvc := coursesservice.NewService(store, store)
|
||||
|
||||
// LMS modules (under courses)
|
||||
moduleSvc := moduleservice.NewService(store, store)
|
||||
|
||||
// LMS lessons (under modules)
|
||||
lessonSvc := lessonsservice.NewService(store, store)
|
||||
|
||||
// LMS practices (under course, module, or lesson)
|
||||
practiceSvc := practicesservice.NewService(store, store, store, store, store, store)
|
||||
|
||||
// Subscriptions service
|
||||
subscriptionsSvc := subscriptions.NewService(store)
|
||||
|
||||
|
|
@ -433,6 +449,10 @@ func main() {
|
|||
assessmentSvc,
|
||||
questionsSvc,
|
||||
programSvc,
|
||||
courseSvc,
|
||||
moduleSvc,
|
||||
lessonSvc,
|
||||
practiceSvc,
|
||||
subscriptionsSvc,
|
||||
arifpaySvc,
|
||||
issueReportingSvc,
|
||||
|
|
|
|||
4
db/migrations/000043_seed_default_programs.down.sql
Normal file
4
db/migrations/000043_seed_default_programs.down.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
DELETE FROM programs
|
||||
WHERE (name = 'Beginner' AND description = 'Default program for the beginner level.')
|
||||
OR (name = 'Intermediate' AND description = 'Default program for the intermediate level.')
|
||||
OR (name = 'Advanced' AND description = 'Default program for the advanced level.');
|
||||
6
db/migrations/000043_seed_default_programs.up.sql
Normal file
6
db/migrations/000043_seed_default_programs.up.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- Default top-level programs (hierarchy: Program → Course → …).
|
||||
INSERT INTO programs (name, description, thumbnail)
|
||||
VALUES
|
||||
('Beginner', 'Default program for the beginner level.', NULL),
|
||||
('Intermediate', 'Default program for the intermediate level.', NULL),
|
||||
('Advanced', 'Default program for the advanced level.', NULL);
|
||||
1
db/migrations/000044_lms_courses.down.sql
Normal file
1
db/migrations/000044_lms_courses.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS courses;
|
||||
13
db/migrations/000044_lms_courses.up.sql
Normal file
13
db/migrations/000044_lms_courses.up.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-- Courses belong to a Program (CEFR-style labels like A1..C2 will be configured separately).
|
||||
CREATE TABLE courses (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
program_id BIGINT NOT NULL REFERENCES programs (id) ON DELETE CASCADE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
thumbnail TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX idx_courses_program_id ON courses (program_id);
|
||||
CREATE INDEX idx_courses_program_created ON courses (program_id, created_at DESC);
|
||||
2
db/migrations/000045_lms_modules.down.sql
Normal file
2
db/migrations/000045_lms_modules.down.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE IF EXISTS modules;
|
||||
ALTER TABLE courses DROP CONSTRAINT IF EXISTS courses_program_id_id_key;
|
||||
22
db/migrations/000045_lms_modules.up.sql
Normal file
22
db/migrations/000045_lms_modules.up.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-- Modules belong to a Course; program_id is denormalized and enforced with the course by a composite FK.
|
||||
ALTER TABLE courses
|
||||
ADD CONSTRAINT courses_program_id_id_key UNIQUE (program_id, id);
|
||||
|
||||
CREATE TABLE modules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
program_id BIGINT NOT NULL,
|
||||
course_id BIGINT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
icon TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ,
|
||||
CONSTRAINT modules_course_scope_fkey
|
||||
FOREIGN KEY (program_id, course_id)
|
||||
REFERENCES courses (program_id, id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_modules_course_id ON modules (course_id);
|
||||
CREATE INDEX idx_modules_program_id ON modules (program_id);
|
||||
CREATE INDEX idx_modules_program_course_created ON modules (program_id, course_id, created_at DESC);
|
||||
1
db/migrations/000046_lms_lessons.down.sql
Normal file
1
db/migrations/000046_lms_lessons.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS lessons;
|
||||
14
db/migrations/000046_lms_lessons.up.sql
Normal file
14
db/migrations/000046_lms_lessons.up.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
-- Lessons belong to a Module.
|
||||
CREATE TABLE lessons (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
module_id BIGINT NOT NULL REFERENCES modules (id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
video_url TEXT,
|
||||
thumbnail TEXT,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX idx_lessons_module_id ON lessons (module_id);
|
||||
CREATE INDEX idx_lessons_module_created ON lessons (module_id, created_at DESC);
|
||||
1
db/migrations/000047_lms_practices.down.sql
Normal file
1
db/migrations/000047_lms_practices.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS lms_practices;
|
||||
29
db/migrations/000047_lms_practices.up.sql
Normal file
29
db/migrations/000047_lms_practices.up.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- Practices attach to exactly one of: course, module, or lesson.
|
||||
CREATE TABLE lms_practices (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
course_id BIGINT REFERENCES courses (id) ON DELETE CASCADE,
|
||||
module_id BIGINT REFERENCES modules (id) ON DELETE CASCADE,
|
||||
lesson_id BIGINT REFERENCES lessons (id) ON DELETE CASCADE,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
story_description TEXT,
|
||||
story_image TEXT,
|
||||
persona_id BIGINT REFERENCES users (id) ON DELETE SET NULL,
|
||||
question_set_id BIGINT NOT NULL REFERENCES question_sets (id) ON DELETE RESTRICT,
|
||||
quick_tips TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ,
|
||||
CONSTRAINT lms_practices_one_parent CHECK (
|
||||
(course_id IS NOT NULL)::int
|
||||
+ (module_id IS NOT NULL)::int
|
||||
+ (lesson_id IS NOT NULL)::int
|
||||
= 1
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_lms_practices_course_id ON lms_practices (course_id);
|
||||
CREATE INDEX idx_lms_practices_module_id ON lms_practices (module_id);
|
||||
CREATE INDEX idx_lms_practices_lesson_id ON lms_practices (lesson_id);
|
||||
CREATE INDEX idx_lms_practices_question_set_id ON lms_practices (question_set_id);
|
||||
CREATE INDEX idx_lms_practices_course_created ON lms_practices (course_id, created_at DESC);
|
||||
CREATE INDEX idx_lms_practices_module_created ON lms_practices (module_id, created_at DESC);
|
||||
CREATE INDEX idx_lms_practices_lesson_created ON lms_practices (lesson_id, created_at DESC);
|
||||
38
db/query/lms_courses.sql
Normal file
38
db/query/lms_courses.sql
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
-- name: CreateCourse :one
|
||||
INSERT INTO courses (program_id, name, description, thumbnail)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetCourseByID :one
|
||||
SELECT *
|
||||
FROM courses
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListCoursesByProgramID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
c.id,
|
||||
c.program_id,
|
||||
c.name,
|
||||
c.description,
|
||||
c.thumbnail,
|
||||
c.created_at,
|
||||
c.updated_at
|
||||
FROM courses c
|
||||
WHERE c.program_id = $1
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
-- name: UpdateCourse :one
|
||||
UPDATE courses
|
||||
SET
|
||||
name = COALESCE(sqlc.narg('name')::varchar, name),
|
||||
description = COALESCE(sqlc.narg('description')::text, description),
|
||||
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = sqlc.arg('id')
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteCourse :exec
|
||||
DELETE FROM courses
|
||||
WHERE id = $1;
|
||||
40
db/query/lms_lessons.sql
Normal file
40
db/query/lms_lessons.sql
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
-- name: CreateLesson :one
|
||||
INSERT INTO lessons (module_id, title, video_url, thumbnail, description)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetLessonByID :one
|
||||
SELECT *
|
||||
FROM lessons
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListLessonsByModuleID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
l.id,
|
||||
l.module_id,
|
||||
l.title,
|
||||
l.video_url,
|
||||
l.thumbnail,
|
||||
l.description,
|
||||
l.created_at,
|
||||
l.updated_at
|
||||
FROM lessons l
|
||||
WHERE l.module_id = $1
|
||||
ORDER BY l.created_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
-- name: UpdateLesson :one
|
||||
UPDATE lessons
|
||||
SET
|
||||
title = COALESCE(sqlc.narg('title')::varchar, title),
|
||||
video_url = COALESCE(sqlc.narg('video_url')::text, video_url),
|
||||
thumbnail = COALESCE(sqlc.narg('thumbnail')::text, thumbnail),
|
||||
description = COALESCE(sqlc.narg('description')::text, description),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = sqlc.arg('id')
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteLesson :exec
|
||||
DELETE FROM lessons
|
||||
WHERE id = $1;
|
||||
40
db/query/lms_modules.sql
Normal file
40
db/query/lms_modules.sql
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
-- name: CreateModule :one
|
||||
INSERT INTO modules (program_id, course_id, name, description, icon)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetModuleByID :one
|
||||
SELECT *
|
||||
FROM modules
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListModulesByProgramAndCourse :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
m.id,
|
||||
m.program_id,
|
||||
m.course_id,
|
||||
m.name,
|
||||
m.description,
|
||||
m.icon,
|
||||
m.created_at,
|
||||
m.updated_at
|
||||
FROM modules m
|
||||
WHERE m.program_id = $1
|
||||
AND m.course_id = $2
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT $3 OFFSET $4;
|
||||
|
||||
-- name: UpdateModule :one
|
||||
UPDATE modules
|
||||
SET
|
||||
name = COALESCE(sqlc.narg('name')::varchar, name),
|
||||
description = COALESCE(sqlc.narg('description')::text, description),
|
||||
icon = COALESCE(sqlc.narg('icon')::text, icon),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = sqlc.arg('id')
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteModule :exec
|
||||
DELETE FROM modules
|
||||
WHERE id = $1;
|
||||
88
db/query/lms_practices.sql
Normal file
88
db/query/lms_practices.sql
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
-- name: CreateLmsPractice :one
|
||||
INSERT INTO lms_practices (
|
||||
course_id, module_id, lesson_id,
|
||||
title, story_description, story_image, persona_id, question_set_id, quick_tips
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetLmsPracticeByID :one
|
||||
SELECT *
|
||||
FROM lms_practices
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ListLmsPracticesByCourseID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
p.id,
|
||||
p.course_id,
|
||||
p.module_id,
|
||||
p.lesson_id,
|
||||
p.title,
|
||||
p.story_description,
|
||||
p.story_image,
|
||||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.course_id = $1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
-- name: ListLmsPracticesByModuleID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
p.id,
|
||||
p.course_id,
|
||||
p.module_id,
|
||||
p.lesson_id,
|
||||
p.title,
|
||||
p.story_description,
|
||||
p.story_image,
|
||||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.module_id = $1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
-- name: ListLmsPracticesByLessonID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
p.id,
|
||||
p.course_id,
|
||||
p.module_id,
|
||||
p.lesson_id,
|
||||
p.title,
|
||||
p.story_description,
|
||||
p.story_image,
|
||||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.lesson_id = $1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
-- name: UpdateLmsPractice :one
|
||||
UPDATE lms_practices
|
||||
SET
|
||||
title = COALESCE(sqlc.narg('title')::varchar, title),
|
||||
story_description = COALESCE(sqlc.narg('story_description')::text, story_description),
|
||||
story_image = COALESCE(sqlc.narg('story_image')::text, story_image),
|
||||
persona_id = COALESCE(sqlc.narg('persona_id')::bigint, persona_id),
|
||||
question_set_id = COALESCE(sqlc.narg('question_set_id')::bigint, question_set_id),
|
||||
quick_tips = COALESCE(sqlc.narg('quick_tips')::text, quick_tips),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = sqlc.arg('id')
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteLmsPractice :exec
|
||||
DELETE FROM lms_practices
|
||||
WHERE id = $1;
|
||||
176
gen/db/lms_courses.sql.go
Normal file
176
gen/db/lms_courses.sql.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: lms_courses.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CreateCourse = `-- name: CreateCourse :one
|
||||
INSERT INTO courses (program_id, name, description, thumbnail)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id, program_id, name, description, thumbnail, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateCourseParams struct {
|
||||
ProgramID int64 `json:"program_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) {
|
||||
row := q.db.QueryRow(ctx, CreateCourse,
|
||||
arg.ProgramID,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.Thumbnail,
|
||||
)
|
||||
var i Course
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProgramID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const DeleteCourse = `-- name: DeleteCourse :exec
|
||||
DELETE FROM courses
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, DeleteCourse, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetCourseByID = `-- name: GetCourseByID :one
|
||||
SELECT id, program_id, name, description, thumbnail, created_at, updated_at
|
||||
FROM courses
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
||||
row := q.db.QueryRow(ctx, GetCourseByID, id)
|
||||
var i Course
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProgramID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const ListCoursesByProgramID = `-- name: ListCoursesByProgramID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
c.id,
|
||||
c.program_id,
|
||||
c.name,
|
||||
c.description,
|
||||
c.thumbnail,
|
||||
c.created_at,
|
||||
c.updated_at
|
||||
FROM courses c
|
||||
WHERE c.program_id = $1
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type ListCoursesByProgramIDParams struct {
|
||||
ProgramID int64 `json:"program_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
type ListCoursesByProgramIDRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
ProgramID int64 `json:"program_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListCoursesByProgramID(ctx context.Context, arg ListCoursesByProgramIDParams) ([]ListCoursesByProgramIDRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListCoursesByProgramID, arg.ProgramID, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListCoursesByProgramIDRow
|
||||
for rows.Next() {
|
||||
var i ListCoursesByProgramIDRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.ProgramID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const UpdateCourse = `-- name: UpdateCourse :one
|
||||
UPDATE courses
|
||||
SET
|
||||
name = COALESCE($1::varchar, name),
|
||||
description = COALESCE($2::text, description),
|
||||
thumbnail = COALESCE($3::text, thumbnail),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $4
|
||||
RETURNING id, program_id, name, description, thumbnail, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateCourseParams struct {
|
||||
Name pgtype.Text `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Course, error) {
|
||||
row := q.db.QueryRow(ctx, UpdateCourse,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.Thumbnail,
|
||||
arg.ID,
|
||||
)
|
||||
var i Course
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProgramID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Thumbnail,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
187
gen/db/lms_lessons.sql.go
Normal file
187
gen/db/lms_lessons.sql.go
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: lms_lessons.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CreateLesson = `-- name: CreateLesson :one
|
||||
INSERT INTO lessons (module_id, title, video_url, thumbnail, description)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, module_id, title, video_url, thumbnail, description, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateLessonParams struct {
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Title string `json:"title"`
|
||||
VideoUrl pgtype.Text `json:"video_url"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateLesson(ctx context.Context, arg CreateLessonParams) (Lesson, error) {
|
||||
row := q.db.QueryRow(ctx, CreateLesson,
|
||||
arg.ModuleID,
|
||||
arg.Title,
|
||||
arg.VideoUrl,
|
||||
arg.Thumbnail,
|
||||
arg.Description,
|
||||
)
|
||||
var i Lesson
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ModuleID,
|
||||
&i.Title,
|
||||
&i.VideoUrl,
|
||||
&i.Thumbnail,
|
||||
&i.Description,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const DeleteLesson = `-- name: DeleteLesson :exec
|
||||
DELETE FROM lessons
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteLesson(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, DeleteLesson, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetLessonByID = `-- name: GetLessonByID :one
|
||||
SELECT id, module_id, title, video_url, thumbnail, description, created_at, updated_at
|
||||
FROM lessons
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLessonByID(ctx context.Context, id int64) (Lesson, error) {
|
||||
row := q.db.QueryRow(ctx, GetLessonByID, id)
|
||||
var i Lesson
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ModuleID,
|
||||
&i.Title,
|
||||
&i.VideoUrl,
|
||||
&i.Thumbnail,
|
||||
&i.Description,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const ListLessonsByModuleID = `-- name: ListLessonsByModuleID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
l.id,
|
||||
l.module_id,
|
||||
l.title,
|
||||
l.video_url,
|
||||
l.thumbnail,
|
||||
l.description,
|
||||
l.created_at,
|
||||
l.updated_at
|
||||
FROM lessons l
|
||||
WHERE l.module_id = $1
|
||||
ORDER BY l.created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type ListLessonsByModuleIDParams struct {
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
type ListLessonsByModuleIDRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Title string `json:"title"`
|
||||
VideoUrl pgtype.Text `json:"video_url"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListLessonsByModuleID(ctx context.Context, arg ListLessonsByModuleIDParams) ([]ListLessonsByModuleIDRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListLessonsByModuleID, arg.ModuleID, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListLessonsByModuleIDRow
|
||||
for rows.Next() {
|
||||
var i ListLessonsByModuleIDRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.ModuleID,
|
||||
&i.Title,
|
||||
&i.VideoUrl,
|
||||
&i.Thumbnail,
|
||||
&i.Description,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const UpdateLesson = `-- name: UpdateLesson :one
|
||||
UPDATE lessons
|
||||
SET
|
||||
title = COALESCE($1::varchar, title),
|
||||
video_url = COALESCE($2::text, video_url),
|
||||
thumbnail = COALESCE($3::text, thumbnail),
|
||||
description = COALESCE($4::text, description),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $5
|
||||
RETURNING id, module_id, title, video_url, thumbnail, description, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateLessonParams struct {
|
||||
Title pgtype.Text `json:"title"`
|
||||
VideoUrl pgtype.Text `json:"video_url"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateLesson(ctx context.Context, arg UpdateLessonParams) (Lesson, error) {
|
||||
row := q.db.QueryRow(ctx, UpdateLesson,
|
||||
arg.Title,
|
||||
arg.VideoUrl,
|
||||
arg.Thumbnail,
|
||||
arg.Description,
|
||||
arg.ID,
|
||||
)
|
||||
var i Lesson
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ModuleID,
|
||||
&i.Title,
|
||||
&i.VideoUrl,
|
||||
&i.Thumbnail,
|
||||
&i.Description,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
191
gen/db/lms_modules.sql.go
Normal file
191
gen/db/lms_modules.sql.go
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: lms_modules.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CreateModule = `-- name: CreateModule :one
|
||||
INSERT INTO modules (program_id, course_id, name, description, icon)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, program_id, course_id, name, description, icon, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateModuleParams struct {
|
||||
ProgramID int64 `json:"program_id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Icon pgtype.Text `json:"icon"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) {
|
||||
row := q.db.QueryRow(ctx, CreateModule,
|
||||
arg.ProgramID,
|
||||
arg.CourseID,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.Icon,
|
||||
)
|
||||
var i Module
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProgramID,
|
||||
&i.CourseID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Icon,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const DeleteModule = `-- name: DeleteModule :exec
|
||||
DELETE FROM modules
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteModule(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, DeleteModule, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetModuleByID = `-- name: GetModuleByID :one
|
||||
SELECT id, program_id, course_id, name, description, icon, created_at, updated_at
|
||||
FROM modules
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) {
|
||||
row := q.db.QueryRow(ctx, GetModuleByID, id)
|
||||
var i Module
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProgramID,
|
||||
&i.CourseID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Icon,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const ListModulesByProgramAndCourse = `-- name: ListModulesByProgramAndCourse :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
m.id,
|
||||
m.program_id,
|
||||
m.course_id,
|
||||
m.name,
|
||||
m.description,
|
||||
m.icon,
|
||||
m.created_at,
|
||||
m.updated_at
|
||||
FROM modules m
|
||||
WHERE m.program_id = $1
|
||||
AND m.course_id = $2
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
`
|
||||
|
||||
type ListModulesByProgramAndCourseParams struct {
|
||||
ProgramID int64 `json:"program_id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
type ListModulesByProgramAndCourseRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
ProgramID int64 `json:"program_id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Icon pgtype.Text `json:"icon"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListModulesByProgramAndCourse(ctx context.Context, arg ListModulesByProgramAndCourseParams) ([]ListModulesByProgramAndCourseRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListModulesByProgramAndCourse,
|
||||
arg.ProgramID,
|
||||
arg.CourseID,
|
||||
arg.Limit,
|
||||
arg.Offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListModulesByProgramAndCourseRow
|
||||
for rows.Next() {
|
||||
var i ListModulesByProgramAndCourseRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.ProgramID,
|
||||
&i.CourseID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Icon,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const UpdateModule = `-- name: UpdateModule :one
|
||||
UPDATE modules
|
||||
SET
|
||||
name = COALESCE($1::varchar, name),
|
||||
description = COALESCE($2::text, description),
|
||||
icon = COALESCE($3::text, icon),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $4
|
||||
RETURNING id, program_id, course_id, name, description, icon, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateModuleParams struct {
|
||||
Name pgtype.Text `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Icon pgtype.Text `json:"icon"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Module, error) {
|
||||
row := q.db.QueryRow(ctx, UpdateModule,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.Icon,
|
||||
arg.ID,
|
||||
)
|
||||
var i Module
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProgramID,
|
||||
&i.CourseID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Icon,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
381
gen/db/lms_practices.sql.go
Normal file
381
gen/db/lms_practices.sql.go
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: lms_practices.sql
|
||||
|
||||
package dbgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const CreateLmsPractice = `-- name: CreateLmsPractice :one
|
||||
INSERT INTO lms_practices (
|
||||
course_id, module_id, lesson_id,
|
||||
title, story_description, story_image, persona_id, question_set_id, quick_tips
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateLmsPracticeParams struct {
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||
Title string `json:"title"`
|
||||
StoryDescription pgtype.Text `json:"story_description"`
|
||||
StoryImage pgtype.Text `json:"story_image"`
|
||||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateLmsPractice(ctx context.Context, arg CreateLmsPracticeParams) (LmsPractice, error) {
|
||||
row := q.db.QueryRow(ctx, CreateLmsPractice,
|
||||
arg.CourseID,
|
||||
arg.ModuleID,
|
||||
arg.LessonID,
|
||||
arg.Title,
|
||||
arg.StoryDescription,
|
||||
arg.StoryImage,
|
||||
arg.PersonaID,
|
||||
arg.QuestionSetID,
|
||||
arg.QuickTips,
|
||||
)
|
||||
var i LmsPractice
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.ModuleID,
|
||||
&i.LessonID,
|
||||
&i.Title,
|
||||
&i.StoryDescription,
|
||||
&i.StoryImage,
|
||||
&i.PersonaID,
|
||||
&i.QuestionSetID,
|
||||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const DeleteLmsPractice = `-- name: DeleteLmsPractice :exec
|
||||
DELETE FROM lms_practices
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteLmsPractice(ctx context.Context, id int64) error {
|
||||
_, err := q.db.Exec(ctx, DeleteLmsPractice, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const GetLmsPracticeByID = `-- name: GetLmsPracticeByID :one
|
||||
SELECT id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||
FROM lms_practices
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLmsPracticeByID(ctx context.Context, id int64) (LmsPractice, error) {
|
||||
row := q.db.QueryRow(ctx, GetLmsPracticeByID, id)
|
||||
var i LmsPractice
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.ModuleID,
|
||||
&i.LessonID,
|
||||
&i.Title,
|
||||
&i.StoryDescription,
|
||||
&i.StoryImage,
|
||||
&i.PersonaID,
|
||||
&i.QuestionSetID,
|
||||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const ListLmsPracticesByCourseID = `-- name: ListLmsPracticesByCourseID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
p.id,
|
||||
p.course_id,
|
||||
p.module_id,
|
||||
p.lesson_id,
|
||||
p.title,
|
||||
p.story_description,
|
||||
p.story_image,
|
||||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.course_id = $1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type ListLmsPracticesByCourseIDParams struct {
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
type ListLmsPracticesByCourseIDRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||
Title string `json:"title"`
|
||||
StoryDescription pgtype.Text `json:"story_description"`
|
||||
StoryImage pgtype.Text `json:"story_image"`
|
||||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListLmsPracticesByCourseID(ctx context.Context, arg ListLmsPracticesByCourseIDParams) ([]ListLmsPracticesByCourseIDRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListLmsPracticesByCourseID, arg.CourseID, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListLmsPracticesByCourseIDRow
|
||||
for rows.Next() {
|
||||
var i ListLmsPracticesByCourseIDRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.ModuleID,
|
||||
&i.LessonID,
|
||||
&i.Title,
|
||||
&i.StoryDescription,
|
||||
&i.StoryImage,
|
||||
&i.PersonaID,
|
||||
&i.QuestionSetID,
|
||||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ListLmsPracticesByLessonID = `-- name: ListLmsPracticesByLessonID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
p.id,
|
||||
p.course_id,
|
||||
p.module_id,
|
||||
p.lesson_id,
|
||||
p.title,
|
||||
p.story_description,
|
||||
p.story_image,
|
||||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.lesson_id = $1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type ListLmsPracticesByLessonIDParams struct {
|
||||
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
type ListLmsPracticesByLessonIDRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||
Title string `json:"title"`
|
||||
StoryDescription pgtype.Text `json:"story_description"`
|
||||
StoryImage pgtype.Text `json:"story_image"`
|
||||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListLmsPracticesByLessonID(ctx context.Context, arg ListLmsPracticesByLessonIDParams) ([]ListLmsPracticesByLessonIDRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListLmsPracticesByLessonID, arg.LessonID, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListLmsPracticesByLessonIDRow
|
||||
for rows.Next() {
|
||||
var i ListLmsPracticesByLessonIDRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.ModuleID,
|
||||
&i.LessonID,
|
||||
&i.Title,
|
||||
&i.StoryDescription,
|
||||
&i.StoryImage,
|
||||
&i.PersonaID,
|
||||
&i.QuestionSetID,
|
||||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const ListLmsPracticesByModuleID = `-- name: ListLmsPracticesByModuleID :many
|
||||
SELECT
|
||||
COUNT(*) OVER () AS total_count,
|
||||
p.id,
|
||||
p.course_id,
|
||||
p.module_id,
|
||||
p.lesson_id,
|
||||
p.title,
|
||||
p.story_description,
|
||||
p.story_image,
|
||||
p.persona_id,
|
||||
p.question_set_id,
|
||||
p.quick_tips,
|
||||
p.created_at,
|
||||
p.updated_at
|
||||
FROM lms_practices p
|
||||
WHERE p.module_id = $1
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type ListLmsPracticesByModuleIDParams struct {
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
Offset int32 `json:"offset"`
|
||||
}
|
||||
|
||||
type ListLmsPracticesByModuleIDRow struct {
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ID int64 `json:"id"`
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||
Title string `json:"title"`
|
||||
StoryDescription pgtype.Text `json:"story_description"`
|
||||
StoryImage pgtype.Text `json:"story_image"`
|
||||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListLmsPracticesByModuleID(ctx context.Context, arg ListLmsPracticesByModuleIDParams) ([]ListLmsPracticesByModuleIDRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListLmsPracticesByModuleID, arg.ModuleID, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListLmsPracticesByModuleIDRow
|
||||
for rows.Next() {
|
||||
var i ListLmsPracticesByModuleIDRow
|
||||
if err := rows.Scan(
|
||||
&i.TotalCount,
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.ModuleID,
|
||||
&i.LessonID,
|
||||
&i.Title,
|
||||
&i.StoryDescription,
|
||||
&i.StoryImage,
|
||||
&i.PersonaID,
|
||||
&i.QuestionSetID,
|
||||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const UpdateLmsPractice = `-- name: UpdateLmsPractice :one
|
||||
UPDATE lms_practices
|
||||
SET
|
||||
title = COALESCE($1::varchar, title),
|
||||
story_description = COALESCE($2::text, story_description),
|
||||
story_image = COALESCE($3::text, story_image),
|
||||
persona_id = COALESCE($4::bigint, persona_id),
|
||||
question_set_id = COALESCE($5::bigint, question_set_id),
|
||||
quick_tips = COALESCE($6::text, quick_tips),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $7
|
||||
RETURNING id, course_id, module_id, lesson_id, title, story_description, story_image, persona_id, question_set_id, quick_tips, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateLmsPracticeParams struct {
|
||||
Title pgtype.Text `json:"title"`
|
||||
StoryDescription pgtype.Text `json:"story_description"`
|
||||
StoryImage pgtype.Text `json:"story_image"`
|
||||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID pgtype.Int8 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateLmsPractice(ctx context.Context, arg UpdateLmsPracticeParams) (LmsPractice, error) {
|
||||
row := q.db.QueryRow(ctx, UpdateLmsPractice,
|
||||
arg.Title,
|
||||
arg.StoryDescription,
|
||||
arg.StoryImage,
|
||||
arg.PersonaID,
|
||||
arg.QuestionSetID,
|
||||
arg.QuickTips,
|
||||
arg.ID,
|
||||
)
|
||||
var i LmsPractice
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.CourseID,
|
||||
&i.ModuleID,
|
||||
&i.LessonID,
|
||||
&i.Title,
|
||||
&i.StoryDescription,
|
||||
&i.StoryImage,
|
||||
&i.PersonaID,
|
||||
&i.QuestionSetID,
|
||||
&i.QuickTips,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -22,6 +22,16 @@ type ActivityLog struct {
|
|||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Course struct {
|
||||
ID int64 `json:"id"`
|
||||
ProgramID int64 `json:"program_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 Device struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
|
|
@ -39,11 +49,48 @@ type GlobalSetting struct {
|
|||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Lesson struct {
|
||||
ID int64 `json:"id"`
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Title string `json:"title"`
|
||||
VideoUrl pgtype.Text `json:"video_url"`
|
||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type LevelToSubCourse struct {
|
||||
LevelID int64 `json:"level_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
}
|
||||
|
||||
type LmsPractice struct {
|
||||
ID int64 `json:"id"`
|
||||
CourseID pgtype.Int8 `json:"course_id"`
|
||||
ModuleID pgtype.Int8 `json:"module_id"`
|
||||
LessonID pgtype.Int8 `json:"lesson_id"`
|
||||
Title string `json:"title"`
|
||||
StoryDescription pgtype.Text `json:"story_description"`
|
||||
StoryImage pgtype.Text `json:"story_image"`
|
||||
PersonaID pgtype.Int8 `json:"persona_id"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips pgtype.Text `json:"quick_tips"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Module struct {
|
||||
ID int64 `json:"id"`
|
||||
ProgramID int64 `json:"program_id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Name string `json:"name"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
Icon pgtype.Text `json:"icon"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ModuleToSubCourse struct {
|
||||
ModuleID int64 `json:"module_id"`
|
||||
SubCourseID int64 `json:"sub_course_id"`
|
||||
|
|
|
|||
|
|
@ -48,6 +48,15 @@ const (
|
|||
ActionProgramCreated ActivityAction = "PROGRAM_CREATED"
|
||||
ActionProgramUpdated ActivityAction = "PROGRAM_UPDATED"
|
||||
ActionProgramDeleted ActivityAction = "PROGRAM_DELETED"
|
||||
ActionModuleCreated ActivityAction = "MODULE_CREATED"
|
||||
ActionModuleUpdated ActivityAction = "MODULE_UPDATED"
|
||||
ActionModuleDeleted ActivityAction = "MODULE_DELETED"
|
||||
ActionLessonCreated ActivityAction = "LESSON_CREATED"
|
||||
ActionLessonUpdated ActivityAction = "LESSON_UPDATED"
|
||||
ActionLessonDeleted ActivityAction = "LESSON_DELETED"
|
||||
ActionPracticeCreated ActivityAction = "PRACTICE_CREATED"
|
||||
ActionPracticeUpdated ActivityAction = "PRACTICE_UPDATED"
|
||||
ActionPracticeDeleted ActivityAction = "PRACTICE_DELETED"
|
||||
)
|
||||
|
||||
type ResourceType string
|
||||
|
|
@ -66,6 +75,9 @@ const (
|
|||
ResourceQuestionSet ResourceType = "QUESTION_SET"
|
||||
ResourceIssue ResourceType = "ISSUE"
|
||||
ResourceProgram ResourceType = "PROGRAM"
|
||||
ResourceModule ResourceType = "MODULE"
|
||||
ResourceLesson ResourceType = "LESSON"
|
||||
ResourcePractice ResourceType = "PRACTICE"
|
||||
)
|
||||
|
||||
type ActivityLog struct {
|
||||
|
|
|
|||
26
internal/domain/course.go
Normal file
26
internal/domain/course.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// Course belongs to a Program (e.g. A1, A2, … labels are configured separately).
|
||||
type Course struct {
|
||||
ID int64 `json:"id"`
|
||||
ProgramID int64 `json:"program_id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type CreateCourseInput struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateCourseInput struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
}
|
||||
29
internal/domain/lesson.go
Normal file
29
internal/domain/lesson.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// Lesson belongs to a Module.
|
||||
type Lesson struct {
|
||||
ID int64 `json:"id"`
|
||||
ModuleID int64 `json:"module_id"`
|
||||
Title string `json:"title"`
|
||||
VideoURL *string `json:"video_url,omitempty"`
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type CreateLessonInput struct {
|
||||
Title string `json:"title" validate:"required"`
|
||||
VideoURL *string `json:"video_url,omitempty"`
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateLessonInput struct {
|
||||
Title *string `json:"title,omitempty"`
|
||||
VideoURL *string `json:"video_url,omitempty"`
|
||||
Thumbnail *string `json:"thumbnail,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
27
internal/domain/module.go
Normal file
27
internal/domain/module.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// Module belongs to a Course. program_id is the course’s program (stored for querying; not required from the client on create).
|
||||
type Module struct {
|
||||
ID int64 `json:"id"`
|
||||
ProgramID int64 `json:"program_id"`
|
||||
CourseID int64 `json:"course_id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type CreateModuleInput struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateModuleInput struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
}
|
||||
47
internal/domain/practice.go
Normal file
47
internal/domain/practice.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
// ParentKind identifies which hierarchy entity owns a practice (exactly one).
|
||||
type ParentKind string
|
||||
|
||||
const (
|
||||
ParentKindCourse ParentKind = "COURSE"
|
||||
ParentKindModule ParentKind = "MODULE"
|
||||
ParentKindLesson ParentKind = "LESSON"
|
||||
)
|
||||
|
||||
// Practice is question-set content (story, persona, tips) scoped to a course, module, or lesson.
|
||||
type Practice struct {
|
||||
ID int64 `json:"id"`
|
||||
ParentKind ParentKind `json:"parent_kind"`
|
||||
ParentID int64 `json:"parent_id"`
|
||||
Title string `json:"title"`
|
||||
StoryDescription *string `json:"story_description,omitempty"`
|
||||
StoryImage *string `json:"story_image,omitempty"`
|
||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID int64 `json:"question_set_id"`
|
||||
QuickTips *string `json:"quick_tips,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type CreatePracticeInput struct {
|
||||
ParentKind ParentKind `json:"parent_kind" validate:"required,oneof=COURSE MODULE LESSON"`
|
||||
ParentID int64 `json:"parent_id" validate:"required,gt=0"`
|
||||
Title string `json:"title" validate:"required"`
|
||||
StoryDescription *string `json:"story_description,omitempty"`
|
||||
StoryImage *string `json:"story_image,omitempty"`
|
||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID int64 `json:"question_set_id" validate:"required,gt=0"`
|
||||
QuickTips *string `json:"quick_tips,omitempty"`
|
||||
}
|
||||
|
||||
type UpdatePracticeInput struct {
|
||||
Title *string `json:"title,omitempty"`
|
||||
StoryDescription *string `json:"story_description,omitempty"`
|
||||
StoryImage *string `json:"story_image,omitempty"`
|
||||
PersonaID *int64 `json:"persona_id,omitempty"`
|
||||
QuestionSetID *int64 `json:"question_set_id,omitempty"`
|
||||
QuickTips *string `json:"quick_tips,omitempty"`
|
||||
}
|
||||
14
internal/ports/lms_course.go
Normal file
14
internal/ports/lms_course.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package ports
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
type CourseStore interface {
|
||||
CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error)
|
||||
GetCourseByID(ctx context.Context, id int64) (domain.Course, error)
|
||||
ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error)
|
||||
UpdateCourse(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error)
|
||||
DeleteCourse(ctx context.Context, id int64) error
|
||||
}
|
||||
14
internal/ports/lms_lesson.go
Normal file
14
internal/ports/lms_lesson.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package ports
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
type LessonStore interface {
|
||||
CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error)
|
||||
GetLessonByID(ctx context.Context, id int64) (domain.Lesson, error)
|
||||
ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error)
|
||||
UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error)
|
||||
DeleteLesson(ctx context.Context, id int64) error
|
||||
}
|
||||
14
internal/ports/lms_module.go
Normal file
14
internal/ports/lms_module.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package ports
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
type ModuleStore interface {
|
||||
CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error)
|
||||
GetModuleByID(ctx context.Context, id int64) (domain.Module, error)
|
||||
ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error)
|
||||
UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error)
|
||||
DeleteModule(ctx context.Context, id int64) error
|
||||
}
|
||||
31
internal/ports/lms_practice.go
Normal file
31
internal/ports/lms_practice.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package ports
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"context"
|
||||
)
|
||||
|
||||
// QuestionSetByID is implemented by the questions store.
|
||||
type QuestionSetByID interface {
|
||||
GetQuestionSetByID(ctx context.Context, id int64) (domain.QuestionSet, error)
|
||||
}
|
||||
|
||||
// UserByID is implemented by the user store.
|
||||
type UserByID interface {
|
||||
GetUserByID(ctx context.Context, id int64) (domain.User, error)
|
||||
}
|
||||
|
||||
type LmsPracticeStore interface {
|
||||
// courseID, moduleID, lessonID: exactly one non-nil, matching in.ParentKind / in.ParentID.
|
||||
CreateLmsPractice(
|
||||
ctx context.Context,
|
||||
in domain.CreatePracticeInput,
|
||||
courseID, moduleID, lessonID *int64,
|
||||
) (domain.Practice, error)
|
||||
GetLmsPracticeByID(ctx context.Context, id int64) (domain.Practice, error)
|
||||
ListLmsPracticesByCourseID(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||
ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||
ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error)
|
||||
UpdateLmsPractice(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error)
|
||||
DeleteLmsPractice(ctx context.Context, id int64) error
|
||||
}
|
||||
109
internal/repository/lms_courses.go
Normal file
109
internal/repository/lms_courses.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func courseToDomain(c dbgen.Course) domain.Course {
|
||||
out := domain.Course{
|
||||
ID: c.ID,
|
||||
ProgramID: c.ProgramID,
|
||||
Name: c.Name,
|
||||
}
|
||||
out.Description = fromPgText(c.Description)
|
||||
out.Thumbnail = fromPgText(c.Thumbnail)
|
||||
out.CreatedAt = c.CreatedAt.Time
|
||||
if c.UpdatedAt.Valid {
|
||||
t := c.UpdatedAt.Time
|
||||
out.UpdatedAt = &t
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Store) CreateCourse(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error) {
|
||||
c, err := s.queries.CreateCourse(ctx, dbgen.CreateCourseParams{
|
||||
ProgramID: programID,
|
||||
Name: input.Name,
|
||||
Description: toPgText(input.Description),
|
||||
Thumbnail: toPgText(input.Thumbnail),
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Course{}, err
|
||||
}
|
||||
return courseToDomain(c), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, error) {
|
||||
c, err := s.queries.GetCourseByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Course{}, pgx.ErrNoRows
|
||||
}
|
||||
return domain.Course{}, err
|
||||
}
|
||||
return courseToDomain(c), nil
|
||||
}
|
||||
|
||||
func (s *Store) ListCoursesByProgramID(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error) {
|
||||
rows, err := s.queries.ListCoursesByProgramID(ctx, dbgen.ListCoursesByProgramIDParams{
|
||||
ProgramID: programID,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return []domain.Course{}, 0, nil
|
||||
}
|
||||
var total int64
|
||||
out := make([]domain.Course, 0, len(rows))
|
||||
for i, r := range rows {
|
||||
if i == 0 {
|
||||
total = r.TotalCount
|
||||
}
|
||||
out = append(out, courseToDomain(dbgen.Course{
|
||||
ID: r.ID,
|
||||
ProgramID: r.ProgramID,
|
||||
Name: r.Name,
|
||||
Description: r.Description,
|
||||
Thumbnail: r.Thumbnail,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
}))
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateCourse(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error) {
|
||||
var nameText pgtype.Text
|
||||
if input.Name != nil {
|
||||
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
||||
} else {
|
||||
nameText = pgtype.Text{Valid: false}
|
||||
}
|
||||
c, err := s.queries.UpdateCourse(ctx, dbgen.UpdateCourseParams{
|
||||
ID: id,
|
||||
Name: nameText,
|
||||
Description: optionalTextUpdate(input.Description),
|
||||
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Course{}, pgx.ErrNoRows
|
||||
}
|
||||
return domain.Course{}, err
|
||||
}
|
||||
return courseToDomain(c), nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteCourse(ctx context.Context, id int64) error {
|
||||
return s.queries.DeleteCourse(ctx, id)
|
||||
}
|
||||
113
internal/repository/lms_lessons.go
Normal file
113
internal/repository/lms_lessons.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func lessonToDomain(l dbgen.Lesson) domain.Lesson {
|
||||
out := domain.Lesson{
|
||||
ID: l.ID,
|
||||
ModuleID: l.ModuleID,
|
||||
Title: l.Title,
|
||||
}
|
||||
out.VideoURL = fromPgText(l.VideoUrl)
|
||||
out.Thumbnail = fromPgText(l.Thumbnail)
|
||||
out.Description = fromPgText(l.Description)
|
||||
out.CreatedAt = l.CreatedAt.Time
|
||||
if l.UpdatedAt.Valid {
|
||||
t := l.UpdatedAt.Time
|
||||
out.UpdatedAt = &t
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Store) CreateLesson(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error) {
|
||||
l, err := s.queries.CreateLesson(ctx, dbgen.CreateLessonParams{
|
||||
ModuleID: moduleID,
|
||||
Title: input.Title,
|
||||
VideoUrl: toPgText(input.VideoURL),
|
||||
Thumbnail: toPgText(input.Thumbnail),
|
||||
Description: toPgText(input.Description),
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Lesson{}, err
|
||||
}
|
||||
return lessonToDomain(l), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetLessonByID(ctx context.Context, id int64) (domain.Lesson, error) {
|
||||
l, err := s.queries.GetLessonByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Lesson{}, pgx.ErrNoRows
|
||||
}
|
||||
return domain.Lesson{}, err
|
||||
}
|
||||
return lessonToDomain(l), nil
|
||||
}
|
||||
|
||||
func (s *Store) ListLessonsByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error) {
|
||||
rows, err := s.queries.ListLessonsByModuleID(ctx, dbgen.ListLessonsByModuleIDParams{
|
||||
ModuleID: moduleID,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return []domain.Lesson{}, 0, nil
|
||||
}
|
||||
var total int64
|
||||
out := make([]domain.Lesson, 0, len(rows))
|
||||
for i, r := range rows {
|
||||
if i == 0 {
|
||||
total = r.TotalCount
|
||||
}
|
||||
out = append(out, lessonToDomain(dbgen.Lesson{
|
||||
ID: r.ID,
|
||||
ModuleID: r.ModuleID,
|
||||
Title: r.Title,
|
||||
VideoUrl: r.VideoUrl,
|
||||
Thumbnail: r.Thumbnail,
|
||||
Description: r.Description,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
}))
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateLesson(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error) {
|
||||
var titleText pgtype.Text
|
||||
if input.Title != nil {
|
||||
titleText = pgtype.Text{String: *input.Title, Valid: true}
|
||||
} else {
|
||||
titleText = pgtype.Text{Valid: false}
|
||||
}
|
||||
l, err := s.queries.UpdateLesson(ctx, dbgen.UpdateLessonParams{
|
||||
ID: id,
|
||||
Title: titleText,
|
||||
VideoUrl: optionalTextUpdate(input.VideoURL),
|
||||
Thumbnail: optionalTextUpdate(input.Thumbnail),
|
||||
Description: optionalTextUpdate(input.Description),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Lesson{}, pgx.ErrNoRows
|
||||
}
|
||||
return domain.Lesson{}, err
|
||||
}
|
||||
return lessonToDomain(l), nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteLesson(ctx context.Context, id int64) error {
|
||||
return s.queries.DeleteLesson(ctx, id)
|
||||
}
|
||||
113
internal/repository/lms_modules.go
Normal file
113
internal/repository/lms_modules.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func moduleToDomain(m dbgen.Module) domain.Module {
|
||||
out := domain.Module{
|
||||
ID: m.ID,
|
||||
ProgramID: m.ProgramID,
|
||||
CourseID: m.CourseID,
|
||||
Name: m.Name,
|
||||
}
|
||||
out.Description = fromPgText(m.Description)
|
||||
out.Icon = fromPgText(m.Icon)
|
||||
out.CreatedAt = m.CreatedAt.Time
|
||||
if m.UpdatedAt.Valid {
|
||||
t := m.UpdatedAt.Time
|
||||
out.UpdatedAt = &t
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Store) CreateModule(ctx context.Context, programID, courseID int64, input domain.CreateModuleInput) (domain.Module, error) {
|
||||
m, err := s.queries.CreateModule(ctx, dbgen.CreateModuleParams{
|
||||
ProgramID: programID,
|
||||
CourseID: courseID,
|
||||
Name: input.Name,
|
||||
Description: toPgText(input.Description),
|
||||
Icon: toPgText(input.Icon),
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Module{}, err
|
||||
}
|
||||
return moduleToDomain(m), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, error) {
|
||||
m, err := s.queries.GetModuleByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Module{}, pgx.ErrNoRows
|
||||
}
|
||||
return domain.Module{}, err
|
||||
}
|
||||
return moduleToDomain(m), nil
|
||||
}
|
||||
|
||||
func (s *Store) ListModulesByProgramAndCourse(ctx context.Context, programID, courseID int64, limit, offset int32) ([]domain.Module, int64, error) {
|
||||
rows, err := s.queries.ListModulesByProgramAndCourse(ctx, dbgen.ListModulesByProgramAndCourseParams{
|
||||
ProgramID: programID,
|
||||
CourseID: courseID,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return []domain.Module{}, 0, nil
|
||||
}
|
||||
var total int64
|
||||
out := make([]domain.Module, 0, len(rows))
|
||||
for i, r := range rows {
|
||||
if i == 0 {
|
||||
total = r.TotalCount
|
||||
}
|
||||
out = append(out, moduleToDomain(dbgen.Module{
|
||||
ID: r.ID,
|
||||
ProgramID: r.ProgramID,
|
||||
CourseID: r.CourseID,
|
||||
Name: r.Name,
|
||||
Description: r.Description,
|
||||
Icon: r.Icon,
|
||||
CreatedAt: r.CreatedAt,
|
||||
UpdatedAt: r.UpdatedAt,
|
||||
}))
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateModule(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) {
|
||||
var nameText pgtype.Text
|
||||
if input.Name != nil {
|
||||
nameText = pgtype.Text{String: *input.Name, Valid: true}
|
||||
} else {
|
||||
nameText = pgtype.Text{Valid: false}
|
||||
}
|
||||
m, err := s.queries.UpdateModule(ctx, dbgen.UpdateModuleParams{
|
||||
ID: id,
|
||||
Name: nameText,
|
||||
Description: optionalTextUpdate(input.Description),
|
||||
Icon: optionalTextUpdate(input.Icon),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Module{}, pgx.ErrNoRows
|
||||
}
|
||||
return domain.Module{}, err
|
||||
}
|
||||
return moduleToDomain(m), nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteModule(ctx context.Context, id int64) error {
|
||||
return s.queries.DeleteModule(ctx, id)
|
||||
}
|
||||
232
internal/repository/lms_practices.go
Normal file
232
internal/repository/lms_practices.go
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
dbgen "Yimaru-Backend/gen/db"
|
||||
"Yimaru-Backend/internal/domain"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func int64PtrToPg8(p *int64) pgtype.Int8 {
|
||||
if p == nil {
|
||||
return pgtype.Int8{Valid: false}
|
||||
}
|
||||
return pgtype.Int8{Int64: *p, Valid: true}
|
||||
}
|
||||
|
||||
func fromPgInt8ID(c pgtype.Int8) *int64 {
|
||||
if !c.Valid {
|
||||
return nil
|
||||
}
|
||||
v := c.Int64
|
||||
return &v
|
||||
}
|
||||
|
||||
func lmsPracticeToDomain(p dbgen.LmsPractice) domain.Practice {
|
||||
out := domain.Practice{
|
||||
ID: p.ID,
|
||||
Title: p.Title,
|
||||
QuestionSetID: p.QuestionSetID,
|
||||
}
|
||||
if p.CourseID.Valid {
|
||||
out.ParentKind = domain.ParentKindCourse
|
||||
out.ParentID = p.CourseID.Int64
|
||||
} else if p.ModuleID.Valid {
|
||||
out.ParentKind = domain.ParentKindModule
|
||||
out.ParentID = p.ModuleID.Int64
|
||||
} else if p.LessonID.Valid {
|
||||
out.ParentKind = domain.ParentKindLesson
|
||||
out.ParentID = p.LessonID.Int64
|
||||
}
|
||||
out.StoryDescription = fromPgText(p.StoryDescription)
|
||||
out.StoryImage = fromPgText(p.StoryImage)
|
||||
out.QuickTips = fromPgText(p.QuickTips)
|
||||
out.PersonaID = fromPgInt8ID(p.PersonaID)
|
||||
out.CreatedAt = p.CreatedAt.Time
|
||||
if p.UpdatedAt.Valid {
|
||||
t := p.UpdatedAt.Time
|
||||
out.UpdatedAt = &t
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func lmsFromListRow(
|
||||
id, qid int64, title string,
|
||||
cid, mid, lid pgtype.Int8,
|
||||
sd, si, qt pgtype.Text, pid pgtype.Int8,
|
||||
ca, ua pgtype.Timestamptz,
|
||||
) domain.Practice {
|
||||
return lmsPracticeToDomain(dbgen.LmsPractice{
|
||||
ID: id,
|
||||
CourseID: cid,
|
||||
ModuleID: mid,
|
||||
LessonID: lid,
|
||||
Title: title,
|
||||
StoryDescription: sd,
|
||||
StoryImage: si,
|
||||
PersonaID: pid,
|
||||
QuestionSetID: qid,
|
||||
QuickTips: qt,
|
||||
CreatedAt: ca,
|
||||
UpdatedAt: ua,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateLmsPractice sets exactly one of courseID, moduleID, lessonID (non-nil).
|
||||
func (s *Store) CreateLmsPractice(
|
||||
ctx context.Context,
|
||||
in domain.CreatePracticeInput,
|
||||
courseID, moduleID, lessonID *int64,
|
||||
) (domain.Practice, error) {
|
||||
p, err := s.queries.CreateLmsPractice(ctx, dbgen.CreateLmsPracticeParams{
|
||||
CourseID: int64PtrToPg8(courseID),
|
||||
ModuleID: int64PtrToPg8(moduleID),
|
||||
LessonID: int64PtrToPg8(lessonID),
|
||||
Title: in.Title,
|
||||
StoryDescription: toPgText(in.StoryDescription),
|
||||
StoryImage: toPgText(in.StoryImage),
|
||||
PersonaID: int64PtrToPg8(in.PersonaID),
|
||||
QuestionSetID: in.QuestionSetID,
|
||||
QuickTips: toPgText(in.QuickTips),
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Practice{}, err
|
||||
}
|
||||
return lmsPracticeToDomain(p), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetLmsPracticeByID(ctx context.Context, id int64) (domain.Practice, error) {
|
||||
p, err := s.queries.GetLmsPracticeByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Practice{}, pgx.ErrNoRows
|
||||
}
|
||||
return domain.Practice{}, err
|
||||
}
|
||||
return lmsPracticeToDomain(p), nil
|
||||
}
|
||||
|
||||
func (s *Store) ListLmsPracticesByCourseID(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||
rows, err := s.queries.ListLmsPracticesByCourseID(ctx, dbgen.ListLmsPracticesByCourseIDParams{
|
||||
CourseID: pgtype.Int8{Int64: courseID, Valid: true},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return []domain.Practice{}, 0, nil
|
||||
}
|
||||
var total int64
|
||||
out := make([]domain.Practice, 0, len(rows))
|
||||
for i, r := range rows {
|
||||
if i == 0 {
|
||||
total = r.TotalCount
|
||||
}
|
||||
out = append(out, lmsFromListRow(
|
||||
r.ID, r.QuestionSetID, r.Title,
|
||||
r.CourseID, r.ModuleID, r.LessonID,
|
||||
r.StoryDescription, r.StoryImage, r.QuickTips,
|
||||
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
||||
))
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListLmsPracticesByModuleID(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||
rows, err := s.queries.ListLmsPracticesByModuleID(ctx, dbgen.ListLmsPracticesByModuleIDParams{
|
||||
ModuleID: pgtype.Int8{Int64: moduleID, Valid: true},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return []domain.Practice{}, 0, nil
|
||||
}
|
||||
var total int64
|
||||
out := make([]domain.Practice, 0, len(rows))
|
||||
for i, r := range rows {
|
||||
if i == 0 {
|
||||
total = r.TotalCount
|
||||
}
|
||||
out = append(out, lmsFromListRow(
|
||||
r.ID, r.QuestionSetID, r.Title,
|
||||
r.CourseID, r.ModuleID, r.LessonID,
|
||||
r.StoryDescription, r.StoryImage, r.QuickTips,
|
||||
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
||||
))
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListLmsPracticesByLessonID(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||
rows, err := s.queries.ListLmsPracticesByLessonID(ctx, dbgen.ListLmsPracticesByLessonIDParams{
|
||||
LessonID: pgtype.Int8{Int64: lessonID, Valid: true},
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return []domain.Practice{}, 0, nil
|
||||
}
|
||||
var total int64
|
||||
out := make([]domain.Practice, 0, len(rows))
|
||||
for i, r := range rows {
|
||||
if i == 0 {
|
||||
total = r.TotalCount
|
||||
}
|
||||
out = append(out, lmsFromListRow(
|
||||
r.ID, r.QuestionSetID, r.Title,
|
||||
r.CourseID, r.ModuleID, r.LessonID,
|
||||
r.StoryDescription, r.StoryImage, r.QuickTips,
|
||||
r.PersonaID, r.CreatedAt, r.UpdatedAt,
|
||||
))
|
||||
}
|
||||
return out, total, nil
|
||||
}
|
||||
|
||||
func optionalInt8UpdateID(val *int64) pgtype.Int8 {
|
||||
if val == nil {
|
||||
return pgtype.Int8{Valid: false}
|
||||
}
|
||||
return pgtype.Int8{Int64: *val, Valid: true}
|
||||
}
|
||||
|
||||
func (s *Store) UpdateLmsPractice(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error) {
|
||||
var titleText pgtype.Text
|
||||
if input.Title != nil {
|
||||
titleText = pgtype.Text{String: *input.Title, Valid: true}
|
||||
} else {
|
||||
titleText = pgtype.Text{Valid: false}
|
||||
}
|
||||
qs := optionalInt8UpdateID(input.QuestionSetID)
|
||||
p, err := s.queries.UpdateLmsPractice(ctx, dbgen.UpdateLmsPracticeParams{
|
||||
ID: id,
|
||||
Title: titleText,
|
||||
StoryDescription: optionalTextUpdate(input.StoryDescription),
|
||||
StoryImage: optionalTextUpdate(input.StoryImage),
|
||||
PersonaID: optionalInt8UpdateID(input.PersonaID),
|
||||
QuestionSetID: qs,
|
||||
QuickTips: optionalTextUpdate(input.QuickTips),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Practice{}, pgx.ErrNoRows
|
||||
}
|
||||
return domain.Practice{}, err
|
||||
}
|
||||
return lmsPracticeToDomain(p), nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteLmsPractice(ctx context.Context, id int64) error {
|
||||
return s.queries.DeleteLmsPractice(ctx, id)
|
||||
}
|
||||
83
internal/services/courses/service.go
Normal file
83
internal/services/courses/service.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package courses
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"Yimaru-Backend/internal/services/programs"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
var ErrCourseNotFound = errors.New("course not found")
|
||||
|
||||
type Service struct {
|
||||
courses ports.CourseStore
|
||||
programs ports.ProgramStore
|
||||
}
|
||||
|
||||
func NewService(courses ports.CourseStore, programs ports.ProgramStore) *Service {
|
||||
return &Service{courses: courses, programs: programs}
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, programID int64, input domain.CreateCourseInput) (domain.Course, error) {
|
||||
if _, err := s.programs.GetProgramByID(ctx, programID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Course{}, programs.ErrProgramNotFound
|
||||
}
|
||||
return domain.Course{}, err
|
||||
}
|
||||
return s.courses.CreateCourse(ctx, programID, input)
|
||||
}
|
||||
|
||||
func (s *Service) GetByID(ctx context.Context, id int64) (domain.Course, error) {
|
||||
c, err := s.courses.GetCourseByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Course{}, ErrCourseNotFound
|
||||
}
|
||||
return domain.Course{}, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListByProgram(ctx context.Context, programID int64, limit, offset int32) ([]domain.Course, int64, error) {
|
||||
if _, err := s.programs.GetProgramByID(ctx, programID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, 0, programs.ErrProgramNotFound
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
return s.courses.ListCoursesByProgramID(ctx, programID, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateCourseInput) (domain.Course, error) {
|
||||
c, err := s.courses.UpdateCourse(ctx, id, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Course{}, ErrCourseNotFound
|
||||
}
|
||||
return domain.Course{}, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||
if _, err := s.courses.GetCourseByID(ctx, id); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrCourseNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return s.courses.DeleteCourse(ctx, id)
|
||||
}
|
||||
88
internal/services/lessons/service.go
Normal file
88
internal/services/lessons/service.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package lessons
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"Yimaru-Backend/internal/services/modules"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
var ErrLessonNotFound = errors.New("lesson not found")
|
||||
|
||||
type Service struct {
|
||||
lessons ports.LessonStore
|
||||
modules ports.ModuleStore
|
||||
}
|
||||
|
||||
func NewService(lessons ports.LessonStore, modules ports.ModuleStore) *Service {
|
||||
return &Service{lessons: lessons, modules: modules}
|
||||
}
|
||||
|
||||
func (s *Service) getModuleOrErr(ctx context.Context, moduleID int64) error {
|
||||
_, err := s.modules.GetModuleByID(ctx, moduleID)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return modules.ErrModuleNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, moduleID int64, input domain.CreateLessonInput) (domain.Lesson, error) {
|
||||
if err := s.getModuleOrErr(ctx, moduleID); err != nil {
|
||||
return domain.Lesson{}, err
|
||||
}
|
||||
return s.lessons.CreateLesson(ctx, moduleID, input)
|
||||
}
|
||||
|
||||
func (s *Service) GetByID(ctx context.Context, id int64) (domain.Lesson, error) {
|
||||
l, err := s.lessons.GetLessonByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Lesson{}, ErrLessonNotFound
|
||||
}
|
||||
return domain.Lesson{}, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListByModule(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Lesson, int64, error) {
|
||||
if err := s.getModuleOrErr(ctx, moduleID); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
return s.lessons.ListLessonsByModuleID(ctx, moduleID, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateLessonInput) (domain.Lesson, error) {
|
||||
l, err := s.lessons.UpdateLesson(ctx, id, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Lesson{}, ErrLessonNotFound
|
||||
}
|
||||
return domain.Lesson{}, err
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||
if _, err := s.lessons.GetLessonByID(ctx, id); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrLessonNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return s.lessons.DeleteLesson(ctx, id)
|
||||
}
|
||||
92
internal/services/modules/service.go
Normal file
92
internal/services/modules/service.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package modules
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"Yimaru-Backend/internal/services/courses"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
var ErrModuleNotFound = errors.New("module not found")
|
||||
|
||||
type Service struct {
|
||||
modules ports.ModuleStore
|
||||
courses ports.CourseStore
|
||||
}
|
||||
|
||||
func NewService(modules ports.ModuleStore, courses ports.CourseStore) *Service {
|
||||
return &Service{modules: modules, courses: courses}
|
||||
}
|
||||
|
||||
func (s *Service) getCourseOrErr(ctx context.Context, courseID int64) (domain.Course, error) {
|
||||
c, err := s.courses.GetCourseByID(ctx, courseID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Course{}, courses.ErrCourseNotFound
|
||||
}
|
||||
return domain.Course{}, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Create loads the course and stores program_id from it (parent program is not taken from the URL).
|
||||
func (s *Service) Create(ctx context.Context, courseID int64, input domain.CreateModuleInput) (domain.Module, error) {
|
||||
c, err := s.getCourseOrErr(ctx, courseID)
|
||||
if err != nil {
|
||||
return domain.Module{}, err
|
||||
}
|
||||
return s.modules.CreateModule(ctx, c.ProgramID, courseID, input)
|
||||
}
|
||||
|
||||
func (s *Service) GetByID(ctx context.Context, id int64) (domain.Module, error) {
|
||||
m, err := s.modules.GetModuleByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Module{}, ErrModuleNotFound
|
||||
}
|
||||
return domain.Module{}, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ListByCourse loads the course and lists modules for its program_id and course_id.
|
||||
func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Module, int64, error) {
|
||||
c, err := s.getCourseOrErr(ctx, courseID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
return s.modules.ListModulesByProgramAndCourse(ctx, c.ProgramID, courseID, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdateModuleInput) (domain.Module, error) {
|
||||
m, err := s.modules.UpdateModule(ctx, id, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Module{}, ErrModuleNotFound
|
||||
}
|
||||
return domain.Module{}, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||
if _, err := s.modules.GetModuleByID(ctx, id); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrModuleNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return s.modules.DeleteModule(ctx, id)
|
||||
}
|
||||
204
internal/services/practices/service.go
Normal file
204
internal/services/practices/service.go
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
package practices
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/ports"
|
||||
"Yimaru-Backend/internal/services/courses"
|
||||
"Yimaru-Backend/internal/services/lessons"
|
||||
"Yimaru-Backend/internal/services/modules"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPracticeNotFound = errors.New("practice not found")
|
||||
ErrQuestionSetNotFound = errors.New("question set not found")
|
||||
ErrInvalidPracticeParent = errors.New("parent_kind and parent_id do not match an allowed parent")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
practices ports.LmsPracticeStore
|
||||
courses ports.CourseStore
|
||||
modules ports.ModuleStore
|
||||
lessons ports.LessonStore
|
||||
qs ports.QuestionSetByID
|
||||
users ports.UserByID
|
||||
}
|
||||
|
||||
func NewService(
|
||||
practices ports.LmsPracticeStore,
|
||||
courses ports.CourseStore,
|
||||
modules ports.ModuleStore,
|
||||
lessons ports.LessonStore,
|
||||
qs ports.QuestionSetByID,
|
||||
users ports.UserByID,
|
||||
) *Service {
|
||||
return &Service{
|
||||
practices: practices,
|
||||
courses: courses,
|
||||
modules: modules,
|
||||
lessons: lessons,
|
||||
qs: qs,
|
||||
users: users,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) validateQuestionSet(ctx context.Context, id int64) error {
|
||||
_, err := s.qs.GetQuestionSetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrQuestionSetNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) validatePersonaUser(ctx context.Context, id int64) error {
|
||||
_, err := s.users.GetUserByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrUserNotFound) {
|
||||
return domain.ErrUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) resolveParent(ctx context.Context, in domain.CreatePracticeInput) (courseID, moduleID, lessonID *int64, err error) {
|
||||
pid := in.ParentID
|
||||
switch in.ParentKind {
|
||||
case domain.ParentKindCourse:
|
||||
if _, e := s.courses.GetCourseByID(ctx, pid); e != nil {
|
||||
if errors.Is(e, pgx.ErrNoRows) {
|
||||
return nil, nil, nil, courses.ErrCourseNotFound
|
||||
}
|
||||
return nil, nil, nil, e
|
||||
}
|
||||
return &pid, nil, nil, nil
|
||||
case domain.ParentKindModule:
|
||||
if _, e := s.modules.GetModuleByID(ctx, pid); e != nil {
|
||||
if errors.Is(e, pgx.ErrNoRows) {
|
||||
return nil, nil, nil, modules.ErrModuleNotFound
|
||||
}
|
||||
return nil, nil, nil, e
|
||||
}
|
||||
return nil, &pid, nil, nil
|
||||
case domain.ParentKindLesson:
|
||||
if _, e := s.lessons.GetLessonByID(ctx, pid); e != nil {
|
||||
if errors.Is(e, pgx.ErrNoRows) {
|
||||
return nil, nil, nil, lessons.ErrLessonNotFound
|
||||
}
|
||||
return nil, nil, nil, e
|
||||
}
|
||||
return nil, nil, &pid, nil
|
||||
default:
|
||||
return nil, nil, nil, ErrInvalidPracticeParent
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, in domain.CreatePracticeInput) (domain.Practice, error) {
|
||||
if err := s.validateQuestionSet(ctx, in.QuestionSetID); err != nil {
|
||||
return domain.Practice{}, err
|
||||
}
|
||||
if in.PersonaID != nil {
|
||||
if err := s.validatePersonaUser(ctx, *in.PersonaID); err != nil {
|
||||
return domain.Practice{}, err
|
||||
}
|
||||
}
|
||||
courseID, moduleID, lessonID, err := s.resolveParent(ctx, in)
|
||||
if err != nil {
|
||||
return domain.Practice{}, err
|
||||
}
|
||||
return s.practices.CreateLmsPractice(ctx, in, courseID, moduleID, lessonID)
|
||||
}
|
||||
|
||||
func (s *Service) GetByID(ctx context.Context, id int64) (domain.Practice, error) {
|
||||
p, err := s.practices.GetLmsPracticeByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Practice{}, ErrPracticeNotFound
|
||||
}
|
||||
return domain.Practice{}, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func clampPracticePage(limit, offset int32) (int32, int32) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
return limit, offset
|
||||
}
|
||||
|
||||
func (s *Service) ListByCourse(ctx context.Context, courseID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||
if _, err := s.courses.GetCourseByID(ctx, courseID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, 0, courses.ErrCourseNotFound
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
limit, offset = clampPracticePage(limit, offset)
|
||||
return s.practices.ListLmsPracticesByCourseID(ctx, courseID, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) ListByModule(ctx context.Context, moduleID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||
if _, err := s.modules.GetModuleByID(ctx, moduleID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, 0, modules.ErrModuleNotFound
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
limit, offset = clampPracticePage(limit, offset)
|
||||
return s.practices.ListLmsPracticesByModuleID(ctx, moduleID, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) ListByLesson(ctx context.Context, lessonID int64, limit, offset int32) ([]domain.Practice, int64, error) {
|
||||
if _, err := s.lessons.GetLessonByID(ctx, lessonID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, 0, lessons.ErrLessonNotFound
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
limit, offset = clampPracticePage(limit, offset)
|
||||
return s.practices.ListLmsPracticesByLessonID(ctx, lessonID, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id int64, input domain.UpdatePracticeInput) (domain.Practice, error) {
|
||||
if input.QuestionSetID != nil {
|
||||
if err := s.validateQuestionSet(ctx, *input.QuestionSetID); err != nil {
|
||||
return domain.Practice{}, err
|
||||
}
|
||||
}
|
||||
if input.PersonaID != nil {
|
||||
if err := s.validatePersonaUser(ctx, *input.PersonaID); err != nil {
|
||||
return domain.Practice{}, err
|
||||
}
|
||||
}
|
||||
p, err := s.practices.UpdateLmsPractice(ctx, id, input)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.Practice{}, ErrPracticeNotFound
|
||||
}
|
||||
return domain.Practice{}, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||
if _, err := s.practices.GetLmsPracticeByID(ctx, id); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrPracticeNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
return s.practices.DeleteLmsPractice(ctx, id)
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "courses.create", Name: "Create Course", Description: "Create a new course", GroupName: "Courses"},
|
||||
{Key: "courses.get", Name: "Get Course", Description: "Get a course by ID", GroupName: "Courses"},
|
||||
{Key: "courses.list_by_category", Name: "List Courses by Category", Description: "List courses by category", GroupName: "Courses"},
|
||||
{Key: "courses.list_by_program", Name: "List Courses by Program", Description: "List courses under a program", GroupName: "Courses"},
|
||||
{Key: "courses.update", Name: "Update Course", Description: "Update a course", GroupName: "Courses"},
|
||||
{Key: "courses.upload_thumbnail", Name: "Upload Course Thumbnail", Description: "Upload course thumbnail image", GroupName: "Courses"},
|
||||
{Key: "courses.delete", Name: "Delete Course", Description: "Delete a course", GroupName: "Courses"},
|
||||
|
|
@ -27,6 +28,27 @@ var AllPermissions = []domain.PermissionSeed{
|
|||
{Key: "programs.update", Name: "Update Program", Description: "Update a program", GroupName: "Programs"},
|
||||
{Key: "programs.delete", Name: "Delete Program", Description: "Delete a program", GroupName: "Programs"},
|
||||
|
||||
// Modules (LMS, under a course)
|
||||
{Key: "modules.create", Name: "Create Module", Description: "Create a module in a course", GroupName: "Modules"},
|
||||
{Key: "modules.get", Name: "Get Module", Description: "Get a module by ID", GroupName: "Modules"},
|
||||
{Key: "modules.list_by_course", Name: "List Modules by Course", Description: "List modules under a program and course", GroupName: "Modules"},
|
||||
{Key: "modules.update", Name: "Update Module", Description: "Update a module", GroupName: "Modules"},
|
||||
{Key: "modules.delete", Name: "Delete Module", Description: "Delete a module", GroupName: "Modules"},
|
||||
|
||||
// Lessons (LMS, under a module)
|
||||
{Key: "lessons.create", Name: "Create Lesson", Description: "Create a lesson in a module", GroupName: "Lessons"},
|
||||
{Key: "lessons.get", Name: "Get Lesson", Description: "Get a lesson by ID", GroupName: "Lessons"},
|
||||
{Key: "lessons.list_by_module", Name: "List Lessons by Module", Description: "List lessons under a module", GroupName: "Lessons"},
|
||||
{Key: "lessons.update", Name: "Update Lesson", Description: "Update a lesson", GroupName: "Lessons"},
|
||||
{Key: "lessons.delete", Name: "Delete Lesson", Description: "Delete a lesson", GroupName: "Lessons"},
|
||||
|
||||
// Practices (LMS, scoped to course, module, or lesson)
|
||||
{Key: "practices.create", Name: "Create Practice", Description: "Create a practice", GroupName: "Practices"},
|
||||
{Key: "practices.get", Name: "Get Practice", Description: "Get a practice by ID", GroupName: "Practices"},
|
||||
{Key: "practices.list", Name: "List Practices", Description: "List practices by course, module, or lesson", GroupName: "Practices"},
|
||||
{Key: "practices.update", Name: "Update Practice", Description: "Update a practice", GroupName: "Practices"},
|
||||
{Key: "practices.delete", Name: "Delete Practice", Description: "Delete a practice", GroupName: "Practices"},
|
||||
|
||||
// Course Management - Sub-courses
|
||||
{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"},
|
||||
|
|
@ -243,7 +265,7 @@ var DefaultRolePermissions = map[string][]string{
|
|||
"ADMIN": {
|
||||
// Course Management (full access)
|
||||
"course_categories.create", "course_categories.list", "course_categories.get", "course_categories.update", "course_categories.delete", "course_categories.reorder",
|
||||
"courses.create", "courses.get", "courses.list_by_category", "courses.update", "courses.upload_thumbnail", "courses.delete", "courses.reorder",
|
||||
"courses.create", "courses.get", "courses.list_by_category", "courses.list_by_program", "courses.update", "courses.upload_thumbnail", "courses.delete", "courses.reorder",
|
||||
"subcourses.create", "subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
||||
"subcourses.update", "subcourses.upload_thumbnail", "subcourses.deactivate", "subcourses.delete", "subcourses.reorder",
|
||||
"videos.create", "videos.create_vimeo", "videos.upload", "videos.import_vimeo", "videos.get",
|
||||
|
|
@ -253,6 +275,15 @@ var DefaultRolePermissions = map[string][]string{
|
|||
// Programs
|
||||
"programs.create", "programs.list", "programs.get", "programs.update", "programs.delete",
|
||||
|
||||
// Modules
|
||||
"modules.create", "modules.get", "modules.list_by_course", "modules.update", "modules.delete",
|
||||
|
||||
// Lessons
|
||||
"lessons.create", "lessons.get", "lessons.list_by_module", "lessons.update", "lessons.delete",
|
||||
|
||||
// Practices
|
||||
"practices.create", "practices.get", "practices.list", "practices.update", "practices.delete",
|
||||
|
||||
// Questions (full access)
|
||||
"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",
|
||||
|
|
@ -327,7 +358,10 @@ var DefaultRolePermissions = map[string][]string{
|
|||
"STUDENT": {
|
||||
// Course browsing
|
||||
"course_categories.list", "course_categories.get",
|
||||
"courses.get", "courses.list_by_category",
|
||||
"courses.get", "courses.list_by_program",
|
||||
"modules.get", "modules.list_by_course",
|
||||
"lessons.get", "lessons.list_by_module",
|
||||
"practices.get", "practices.list",
|
||||
"subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
||||
"videos.get", "videos.list_by_subcourse", "videos.list_published",
|
||||
"learning_tree.get",
|
||||
|
|
@ -377,7 +411,10 @@ var DefaultRolePermissions = map[string][]string{
|
|||
"INSTRUCTOR": {
|
||||
// Course browsing + management
|
||||
"course_categories.list", "course_categories.get",
|
||||
"courses.get", "courses.list_by_category",
|
||||
"courses.get", "courses.list_by_program",
|
||||
"modules.get", "modules.list_by_course",
|
||||
"lessons.get", "lessons.list_by_module",
|
||||
"practices.get", "practices.list",
|
||||
"subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
||||
"videos.get", "videos.list_by_subcourse", "videos.list_published",
|
||||
"learning_tree.get",
|
||||
|
|
@ -425,7 +462,10 @@ var DefaultRolePermissions = map[string][]string{
|
|||
"SUPPORT": {
|
||||
// Course browsing (read-only)
|
||||
"course_categories.list", "course_categories.get",
|
||||
"courses.get", "courses.list_by_category",
|
||||
"courses.get", "courses.list_by_program",
|
||||
"modules.get", "modules.list_by_course",
|
||||
"lessons.get", "lessons.list_by_module",
|
||||
"practices.get", "practices.list",
|
||||
"subcourses.get", "subcourses.list_by_course", "subcourses.list_by_course_list", "subcourses.list_active",
|
||||
"videos.get", "videos.list_by_subcourse", "videos.list_published",
|
||||
"learning_tree.get",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import (
|
|||
minioservice "Yimaru-Backend/internal/services/minio"
|
||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||
"Yimaru-Backend/internal/services/courses"
|
||||
"Yimaru-Backend/internal/services/lessons"
|
||||
"Yimaru-Backend/internal/services/modules"
|
||||
"Yimaru-Backend/internal/services/practices"
|
||||
"Yimaru-Backend/internal/services/programs"
|
||||
"Yimaru-Backend/internal/services/questions"
|
||||
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
||||
|
|
@ -41,6 +45,10 @@ type App struct {
|
|||
assessmentSvc *assessment.Service
|
||||
questionsSvc *questions.Service
|
||||
programSvc *programs.Service
|
||||
courseSvc *courses.Service
|
||||
moduleSvc *modules.Service
|
||||
lessonSvc *lessons.Service
|
||||
practiceSvc *practices.Service
|
||||
subscriptionsSvc *subscriptions.Service
|
||||
arifpaySvc *arifpay.ArifpayService
|
||||
issueReportingSvc *issuereporting.Service
|
||||
|
|
@ -73,6 +81,10 @@ func NewApp(
|
|||
assessmentSvc *assessment.Service,
|
||||
questionsSvc *questions.Service,
|
||||
programSvc *programs.Service,
|
||||
courseSvc *courses.Service,
|
||||
moduleSvc *modules.Service,
|
||||
lessonSvc *lessons.Service,
|
||||
practiceSvc *practices.Service,
|
||||
subscriptionsSvc *subscriptions.Service,
|
||||
arifpaySvc *arifpay.ArifpayService,
|
||||
issueReportingSvc *issuereporting.Service,
|
||||
|
|
@ -117,6 +129,10 @@ func NewApp(
|
|||
assessmentSvc: assessmentSvc,
|
||||
questionsSvc: questionsSvc,
|
||||
programSvc: programSvc,
|
||||
courseSvc: courseSvc,
|
||||
moduleSvc: moduleSvc,
|
||||
lessonSvc: lessonSvc,
|
||||
practiceSvc: practiceSvc,
|
||||
subscriptionsSvc: subscriptionsSvc,
|
||||
arifpaySvc: arifpaySvc,
|
||||
vimeoSvc: vimeoSvc,
|
||||
|
|
|
|||
249
internal/web_server/handlers/course_handler.go
Normal file
249
internal/web_server/handlers/course_handler.go
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/services/courses"
|
||||
"Yimaru-Backend/internal/services/programs"
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// CreateCourse godoc
|
||||
// @Summary Create course
|
||||
// @Description Create a course under a program
|
||||
// @Tags courses
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Program ID"
|
||||
// @Param body body domain.CreateCourseInput true "Course"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Failure 404 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/programs/{id}/courses [post]
|
||||
func (h *Handler) CreateCourse(c *fiber.Ctx) error {
|
||||
programID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid program id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
var req domain.CreateCourseInput
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Validation failed",
|
||||
Error: firstValidationError(valErrs),
|
||||
})
|
||||
}
|
||||
course, err := h.courseSvc.Create(c.Context(), programID, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, programs.ErrProgramNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Program not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to create course",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
rid := course.ID
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseCreated, domain.ResourceCourse, &rid, "Created course: "+course.Name, nil, &ip, &ua)
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Course created successfully",
|
||||
Data: course,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusCreated,
|
||||
})
|
||||
}
|
||||
|
||||
// ListCoursesByProgram godoc
|
||||
// @Summary List courses by program
|
||||
// @Tags courses
|
||||
// @Produce json
|
||||
// @Param id path int true "Program ID"
|
||||
// @Param limit query int false "Page size" default(20)
|
||||
// @Param offset query int false "Offset" default(0)
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 404 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/programs/{id}/courses [get]
|
||||
func (h *Handler) ListCoursesByProgram(c *fiber.Ctx) error {
|
||||
programID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid program id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||
items, total, err := h.courseSvc.ListByProgram(c.Context(), programID, int32(limit), int32(offset))
|
||||
if err != nil {
|
||||
if errors.Is(err, programs.ErrProgramNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Program not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to list courses",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Courses retrieved successfully",
|
||||
Data: fiber.Map{
|
||||
"courses": items,
|
||||
"total_count": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// GetCourse godoc
|
||||
// @Summary Get course by ID
|
||||
// @Tags courses
|
||||
// @Produce json
|
||||
// @Param id path int true "Course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 404 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/courses/{id} [get]
|
||||
func (h *Handler) GetCourse(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
course, err := h.courseSvc.GetByID(c.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Course not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load course",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course retrieved successfully",
|
||||
Data: course,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCourse godoc
|
||||
// @Summary Update course
|
||||
// @Tags courses
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Course ID"
|
||||
// @Param body body domain.UpdateCourseInput true "Fields to update"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 404 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/courses/{id} [put]
|
||||
func (h *Handler) UpdateCourse(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
var req domain.UpdateCourseInput
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
course, err := h.courseSvc.Update(c.Context(), id, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Course not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update course",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
rid := course.ID
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseUpdated, domain.ResourceCourse, &rid, "Updated course: "+course.Name, nil, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course updated successfully",
|
||||
Data: course,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCourse godoc
|
||||
// @Summary Delete course
|
||||
// @Tags courses
|
||||
// @Param id path int true "Course ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Failure 404 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/courses/{id} [delete]
|
||||
func (h *Handler) DeleteCourse(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.courseSvc.Delete(c.Context(), id); err != nil {
|
||||
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Course not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to delete course",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseDeleted, domain.ResourceCourse, &id, "Deleted course", nil, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Course deleted successfully",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
|
@ -16,6 +16,10 @@ import (
|
|||
ratingsservice "Yimaru-Backend/internal/services/ratings"
|
||||
rbacservice "Yimaru-Backend/internal/services/rbac"
|
||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||
"Yimaru-Backend/internal/services/courses"
|
||||
"Yimaru-Backend/internal/services/lessons"
|
||||
"Yimaru-Backend/internal/services/modules"
|
||||
"Yimaru-Backend/internal/services/practices"
|
||||
"Yimaru-Backend/internal/services/programs"
|
||||
"Yimaru-Backend/internal/services/questions"
|
||||
"Yimaru-Backend/internal/services/recommendation"
|
||||
|
|
@ -40,6 +44,10 @@ type Handler struct {
|
|||
assessmentSvc *assessment.Service
|
||||
questionsSvc *questions.Service
|
||||
programSvc *programs.Service
|
||||
courseSvc *courses.Service
|
||||
moduleSvc *modules.Service
|
||||
lessonSvc *lessons.Service
|
||||
practiceSvc *practices.Service
|
||||
subscriptionsSvc *subscriptions.Service
|
||||
arifpaySvc *arifpay.ArifpayService
|
||||
logger *slog.Logger
|
||||
|
|
@ -68,6 +76,10 @@ func New(
|
|||
assessmentSvc *assessment.Service,
|
||||
questionsSvc *questions.Service,
|
||||
programSvc *programs.Service,
|
||||
courseSvc *courses.Service,
|
||||
moduleSvc *modules.Service,
|
||||
lessonSvc *lessons.Service,
|
||||
practiceSvc *practices.Service,
|
||||
subscriptionsSvc *subscriptions.Service,
|
||||
arifpaySvc *arifpay.ArifpayService,
|
||||
logger *slog.Logger,
|
||||
|
|
@ -95,6 +107,10 @@ func New(
|
|||
assessmentSvc: assessmentSvc,
|
||||
questionsSvc: questionsSvc,
|
||||
programSvc: programSvc,
|
||||
courseSvc: courseSvc,
|
||||
moduleSvc: moduleSvc,
|
||||
lessonSvc: lessonSvc,
|
||||
practiceSvc: practiceSvc,
|
||||
subscriptionsSvc: subscriptionsSvc,
|
||||
arifpaySvc: arifpaySvc,
|
||||
logger: logger,
|
||||
|
|
|
|||
229
internal/web_server/handlers/lesson_handler.go
Normal file
229
internal/web_server/handlers/lesson_handler.go
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/services/lessons"
|
||||
"Yimaru-Backend/internal/services/modules"
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// CreateLesson godoc
|
||||
// @Summary Create lesson
|
||||
// @Tags lessons
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param moduleId path int true "Module ID"
|
||||
// @Param body body domain.CreateLessonInput true "Lesson"
|
||||
// @Router /api/v1/modules/{moduleId}/lessons [post]
|
||||
func (h *Handler) CreateLesson(c *fiber.Ctx) error {
|
||||
moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid module id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
var req domain.CreateLessonInput
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Validation failed",
|
||||
Error: firstValidationError(valErrs),
|
||||
})
|
||||
}
|
||||
les, err := h.lessonSvc.Create(c.Context(), moduleID, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, modules.ErrModuleNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Module not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to create lesson",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
rid := les.ID
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionLessonCreated, domain.ResourceLesson, &rid, "Created lesson: "+les.Title, nil, &ip, &ua)
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Lesson created successfully",
|
||||
Data: les,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusCreated,
|
||||
})
|
||||
}
|
||||
|
||||
// ListLessonsByModule godoc
|
||||
// @Tags lessons
|
||||
// @Param moduleId path int true "Module ID"
|
||||
// @Param limit query int false "Page size" default(20)
|
||||
// @Param offset query int false "Offset" default(0)
|
||||
// @Router /api/v1/modules/{moduleId}/lessons [get]
|
||||
func (h *Handler) ListLessonsByModule(c *fiber.Ctx) error {
|
||||
moduleID, err := strconv.ParseInt(c.Params("moduleId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid module id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||
items, total, err := h.lessonSvc.ListByModule(c.Context(), moduleID, int32(limit), int32(offset))
|
||||
if err != nil {
|
||||
if errors.Is(err, modules.ErrModuleNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Module not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to list lessons",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Lessons retrieved successfully",
|
||||
Data: fiber.Map{
|
||||
"lessons": items,
|
||||
"total_count": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// GetLesson godoc
|
||||
// @Tags lessons
|
||||
// @Param id path int true "Lesson ID"
|
||||
// @Router /api/v1/lessons/{id} [get]
|
||||
func (h *Handler) GetLesson(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid lesson id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
les, err := h.lessonSvc.GetByID(c.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, lessons.ErrLessonNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Lesson not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load lesson",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Lesson retrieved successfully",
|
||||
Data: les,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateLesson godoc
|
||||
// @Tags lessons
|
||||
// @Param id path int true "Lesson ID"
|
||||
// @Param body body domain.UpdateLessonInput true "Fields to update"
|
||||
// @Router /api/v1/lessons/{id} [put]
|
||||
func (h *Handler) UpdateLesson(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid lesson id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
var req domain.UpdateLessonInput
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
les, err := h.lessonSvc.Update(c.Context(), id, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, lessons.ErrLessonNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Lesson not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update lesson",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
rid := les.ID
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionLessonUpdated, domain.ResourceLesson, &rid, "Updated lesson: "+les.Title, nil, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Lesson updated successfully",
|
||||
Data: les,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteLesson godoc
|
||||
// @Tags lessons
|
||||
// @Param id path int true "Lesson ID"
|
||||
// @Router /api/v1/lessons/{id} [delete]
|
||||
func (h *Handler) DeleteLesson(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid lesson id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.lessonSvc.Delete(c.Context(), id); err != nil {
|
||||
if errors.Is(err, lessons.ErrLessonNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Lesson not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to delete lesson",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionLessonDeleted, domain.ResourceLesson, &id, "Deleted lesson", nil, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Lesson deleted successfully",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
236
internal/web_server/handlers/module_handler.go
Normal file
236
internal/web_server/handlers/module_handler.go
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/services/courses"
|
||||
"Yimaru-Backend/internal/services/modules"
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// CreateModule godoc
|
||||
// @Summary Create module
|
||||
// @Description Create a module under a course; parent program is taken from the course.
|
||||
// @Tags modules
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param courseId path int true "Course ID"
|
||||
// @Param body body domain.CreateModuleInput true "Module"
|
||||
// @Success 201 {object} domain.Response
|
||||
// @Failure 400 {object} domain.ErrorResponse
|
||||
// @Router /api/v1/courses/{courseId}/modules [post]
|
||||
func (h *Handler) CreateModule(c *fiber.Ctx) error {
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
var req domain.CreateModuleInput
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Validation failed",
|
||||
Error: firstValidationError(valErrs),
|
||||
})
|
||||
}
|
||||
mod, err := h.moduleSvc.Create(c.Context(), courseID, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Course not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to create module",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
rid := mod.ID
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionModuleCreated, domain.ResourceModule, &rid, "Created module: "+mod.Name, nil, &ip, &ua)
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Module created successfully",
|
||||
Data: mod,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusCreated,
|
||||
})
|
||||
}
|
||||
|
||||
// ListModulesByCourse godoc
|
||||
// @Summary List modules for a course
|
||||
// @Tags modules
|
||||
// @Produce json
|
||||
// @Param courseId path int true "Course ID"
|
||||
// @Param limit query int false "Page size" default(20)
|
||||
// @Param offset query int false "Offset" default(0)
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/courses/{courseId}/modules [get]
|
||||
func (h *Handler) ListModulesByCourse(c *fiber.Ctx) error {
|
||||
courseID, err := strconv.ParseInt(c.Params("courseId"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid course id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||
items, total, err := h.moduleSvc.ListByCourse(c.Context(), courseID, int32(limit), int32(offset))
|
||||
if err != nil {
|
||||
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Course not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to list modules",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Modules retrieved successfully",
|
||||
Data: fiber.Map{
|
||||
"modules": items,
|
||||
"total_count": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// GetModule godoc
|
||||
// @Tags modules
|
||||
// @Param id path int true "Module ID"
|
||||
// @Success 200 {object} domain.Response
|
||||
// @Router /api/v1/modules/{id} [get]
|
||||
func (h *Handler) GetModule(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid module id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
mod, err := h.moduleSvc.GetByID(c.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, modules.ErrModuleNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Module not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to load module",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Module retrieved successfully",
|
||||
Data: mod,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateModule godoc
|
||||
// @Tags modules
|
||||
// @Param id path int true "Module ID"
|
||||
// @Param body body domain.UpdateModuleInput true "Fields to update"
|
||||
// @Router /api/v1/modules/{id} [put]
|
||||
func (h *Handler) UpdateModule(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid module id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
var req domain.UpdateModuleInput
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid request body",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
mod, err := h.moduleSvc.Update(c.Context(), id, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, modules.ErrModuleNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Module not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to update module",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
rid := mod.ID
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionModuleUpdated, domain.ResourceModule, &rid, "Updated module: "+mod.Name, nil, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Module updated successfully",
|
||||
Data: mod,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteModule godoc
|
||||
// @Tags modules
|
||||
// @Param id path int true "Module ID"
|
||||
// @Router /api/v1/modules/{id} [delete]
|
||||
func (h *Handler) DeleteModule(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid module id",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
if err := h.moduleSvc.Delete(c.Context(), id); err != nil {
|
||||
if errors.Is(err, modules.ErrModuleNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||
Message: "Module not found",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Failed to delete module",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionModuleDeleted, domain.ResourceModule, &id, "Deleted module", nil, &ip, &ua)
|
||||
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Module deleted successfully",
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
237
internal/web_server/handlers/practice_handler.go
Normal file
237
internal/web_server/handlers/practice_handler.go
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"Yimaru-Backend/internal/domain"
|
||||
"Yimaru-Backend/internal/services/courses"
|
||||
"Yimaru-Backend/internal/services/lessons"
|
||||
"Yimaru-Backend/internal/services/modules"
|
||||
"Yimaru-Backend/internal/services/practices"
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// CreatePractice godoc
|
||||
// @Tags practices
|
||||
// @Accept json
|
||||
// @Param body body domain.CreatePracticeInput true "Practice (parent_kind: COURSE | MODULE | LESSON)"
|
||||
// @Router /api/v1/practices [post]
|
||||
func (h *Handler) CreatePractice(c *fiber.Ctx) error {
|
||||
var req domain.CreatePracticeInput
|
||||
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.practiceSvc.Create(c.Context(), req)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, courses.ErrCourseNotFound):
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Course not found", Error: err.Error()})
|
||||
case errors.Is(err, modules.ErrModuleNotFound):
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Module not found", Error: err.Error()})
|
||||
case errors.Is(err, lessons.ErrLessonNotFound):
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Lesson not found", Error: err.Error()})
|
||||
case errors.Is(err, practices.ErrQuestionSetNotFound):
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Question set not found", Error: err.Error()})
|
||||
case errors.Is(err, domain.ErrUserNotFound):
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Persona user not found", Error: err.Error()})
|
||||
case errors.Is(err, practices.ErrInvalidPracticeParent):
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid parent", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create practice", Error: err.Error()})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
rid := p.ID
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionPracticeCreated, domain.ResourcePractice, &rid, "Created practice: "+p.Title, nil, &ip, &ua)
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||
Message: "Practice created successfully",
|
||||
Data: p,
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusCreated,
|
||||
})
|
||||
}
|
||||
|
||||
// ListPracticesByCourse godoc
|
||||
// @Tags practices
|
||||
// @Param id path int true "Course ID"
|
||||
// @Router /api/v1/courses/{id}/practices [get]
|
||||
func (h *Handler) ListPracticesByCourse(c *fiber.Ctx) error {
|
||||
courseID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course id", Error: err.Error()})
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||
items, total, err := h.practiceSvc.ListByCourse(c.Context(), courseID, int32(limit), int32(offset))
|
||||
if err != nil {
|
||||
if errors.Is(err, courses.ErrCourseNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Course not found", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list practices", Error: err.Error()})
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practices retrieved successfully",
|
||||
Data: fiber.Map{
|
||||
"practices": items,
|
||||
"total_count": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// ListPracticesByModule godoc
|
||||
// @Tags practices
|
||||
// @Param id path int true "Module ID"
|
||||
// @Router /api/v1/modules/{id}/practices [get]
|
||||
func (h *Handler) ListPracticesByModule(c *fiber.Ctx) error {
|
||||
moduleID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid module id", Error: err.Error()})
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||
items, total, err := h.practiceSvc.ListByModule(c.Context(), moduleID, int32(limit), int32(offset))
|
||||
if err != nil {
|
||||
if errors.Is(err, modules.ErrModuleNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Module not found", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list practices", Error: err.Error()})
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practices retrieved successfully",
|
||||
Data: fiber.Map{
|
||||
"practices": items,
|
||||
"total_count": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// ListPracticesByLesson godoc
|
||||
// @Tags practices
|
||||
// @Param id path int true "Lesson ID"
|
||||
// @Router /api/v1/lessons/{id}/practices [get]
|
||||
func (h *Handler) ListPracticesByLesson(c *fiber.Ctx) error {
|
||||
lessonID, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid lesson id", Error: err.Error()})
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||
items, total, err := h.practiceSvc.ListByLesson(c.Context(), lessonID, int32(limit), int32(offset))
|
||||
if err != nil {
|
||||
if errors.Is(err, lessons.ErrLessonNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Lesson not found", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to list practices", Error: err.Error()})
|
||||
}
|
||||
return c.JSON(domain.Response{
|
||||
Message: "Practices retrieved successfully",
|
||||
Data: fiber.Map{
|
||||
"practices": items,
|
||||
"total_count": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
},
|
||||
Success: true,
|
||||
StatusCode: fiber.StatusOK,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPractice godoc
|
||||
// @Tags practices
|
||||
// @Param id path int true "Practice ID"
|
||||
// @Router /api/v1/practices/{id} [get]
|
||||
func (h *Handler) GetPractice(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice id", Error: err.Error()})
|
||||
}
|
||||
p, err := h.practiceSvc.GetByID(c.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, practices.ErrPracticeNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load practice", Error: err.Error()})
|
||||
}
|
||||
return c.JSON(domain.Response{Message: "Practice retrieved successfully", Data: p, Success: true, StatusCode: fiber.StatusOK})
|
||||
}
|
||||
|
||||
// UpdatePractice godoc
|
||||
// @Tags practices
|
||||
// @Param id path int true "Practice ID"
|
||||
// @Param body body domain.UpdatePracticeInput true "Fields to update (parent is immutable)"
|
||||
// @Router /api/v1/practices/{id} [put]
|
||||
func (h *Handler) UpdatePractice(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice id", Error: err.Error()})
|
||||
}
|
||||
var req domain.UpdatePracticeInput
|
||||
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.practiceSvc.Update(c.Context(), id, req)
|
||||
if err != nil {
|
||||
if errors.Is(err, practices.ErrPracticeNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found", Error: err.Error()})
|
||||
}
|
||||
if errors.Is(err, practices.ErrQuestionSetNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Question set not found", Error: err.Error()})
|
||||
}
|
||||
if errors.Is(err, domain.ErrUserNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Persona user not found", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice", Error: err.Error()})
|
||||
}
|
||||
actorID := c.Locals("user_id").(int64)
|
||||
actorRole := string(c.Locals("role").(domain.Role))
|
||||
ip := c.IP()
|
||||
ua := c.Get("User-Agent")
|
||||
rid := p.ID
|
||||
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionPracticeUpdated, domain.ResourcePractice, &rid, "Updated practice: "+p.Title, nil, &ip, &ua)
|
||||
return c.JSON(domain.Response{Message: "Practice updated successfully", Data: p, Success: true, StatusCode: fiber.StatusOK})
|
||||
}
|
||||
|
||||
// DeletePractice godoc
|
||||
// @Tags practices
|
||||
// @Param id path int true "Practice ID"
|
||||
// @Router /api/v1/practices/{id} [delete]
|
||||
func (h *Handler) DeletePractice(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice id", Error: err.Error()})
|
||||
}
|
||||
if err := h.practiceSvc.Delete(c.Context(), id); err != nil {
|
||||
if errors.Is(err, practices.ErrPracticeNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{Message: "Practice not found", Error: err.Error()})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete practice", Error: err.Error()})
|
||||
}
|
||||
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.ActionPracticeDeleted, domain.ResourcePractice, &id, "Deleted practice", nil, &ip, &ua)
|
||||
return c.JSON(domain.Response{Message: "Practice deleted successfully", Success: true, StatusCode: fiber.StatusOK})
|
||||
}
|
||||
|
|
@ -16,6 +16,10 @@ func (a *App) initAppRoutes() {
|
|||
a.assessmentSvc,
|
||||
a.questionsSvc,
|
||||
a.programSvc,
|
||||
a.courseSvc,
|
||||
a.moduleSvc,
|
||||
a.lessonSvc,
|
||||
a.practiceSvc,
|
||||
a.subscriptionsSvc,
|
||||
a.arifpaySvc,
|
||||
a.logger,
|
||||
|
|
@ -74,6 +78,33 @@ func (a *App) initAppRoutes() {
|
|||
groupV1.Put("/programs/:id", a.authMiddleware, a.RequirePermission("programs.update"), h.UpdateProgram)
|
||||
groupV1.Delete("/programs/:id", a.authMiddleware, a.RequirePermission("programs.delete"), h.DeleteProgram)
|
||||
|
||||
// Courses
|
||||
groupV1.Post("/programs/:id/courses", a.authMiddleware, a.RequirePermission("courses.create"), h.CreateCourse)
|
||||
groupV1.Get("/programs/:id/courses", a.authMiddleware, a.RequirePermission("courses.list_by_program"), h.ListCoursesByProgram)
|
||||
groupV1.Get("/courses/:id/practices", a.authMiddleware, a.RequirePermission("practices.list"), h.ListPracticesByCourse)
|
||||
groupV1.Get("/courses/:id", a.authMiddleware, a.RequirePermission("courses.get"), h.GetCourse)
|
||||
groupV1.Put("/courses/:id", a.authMiddleware, a.RequirePermission("courses.update"), h.UpdateCourse)
|
||||
groupV1.Delete("/courses/:id", a.authMiddleware, a.RequirePermission("courses.delete"), h.DeleteCourse)
|
||||
groupV1.Post("/courses/:courseId/modules", a.authMiddleware, a.RequirePermission("modules.create"), h.CreateModule)
|
||||
groupV1.Get("/courses/:courseId/modules", a.authMiddleware, a.RequirePermission("modules.list_by_course"), h.ListModulesByCourse)
|
||||
|
||||
// /modules/:moduleId/lessons before /modules/:id; /modules/:id/practices before /modules/:id
|
||||
groupV1.Post("/modules/:moduleId/lessons", a.authMiddleware, a.RequirePermission("lessons.create"), h.CreateLesson)
|
||||
groupV1.Get("/modules/:moduleId/lessons", a.authMiddleware, a.RequirePermission("lessons.list_by_module"), h.ListLessonsByModule)
|
||||
groupV1.Get("/modules/:id/practices", a.authMiddleware, a.RequirePermission("practices.list"), h.ListPracticesByModule)
|
||||
groupV1.Get("/modules/:id", a.authMiddleware, a.RequirePermission("modules.get"), h.GetModule)
|
||||
groupV1.Put("/modules/:id", a.authMiddleware, a.RequirePermission("modules.update"), h.UpdateModule)
|
||||
groupV1.Delete("/modules/:id", a.authMiddleware, a.RequirePermission("modules.delete"), h.DeleteModule)
|
||||
groupV1.Get("/lessons/:id/practices", a.authMiddleware, a.RequirePermission("practices.list"), h.ListPracticesByLesson)
|
||||
groupV1.Get("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.get"), h.GetLesson)
|
||||
groupV1.Put("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.update"), h.UpdateLesson)
|
||||
groupV1.Delete("/lessons/:id", a.authMiddleware, a.RequirePermission("lessons.delete"), h.DeleteLesson)
|
||||
|
||||
groupV1.Post("/practices", a.authMiddleware, a.RequirePermission("practices.create"), h.CreatePractice)
|
||||
groupV1.Get("/practices/:id", a.authMiddleware, a.RequirePermission("practices.get"), h.GetPractice)
|
||||
groupV1.Put("/practices/:id", a.authMiddleware, a.RequirePermission("practices.update"), h.UpdatePractice)
|
||||
groupV1.Delete("/practices/:id", a.authMiddleware, a.RequirePermission("practices.delete"), h.DeletePractice)
|
||||
|
||||
// File storage (MinIO)
|
||||
groupV1.Get("/files/url", a.authMiddleware, h.GetFileURL)
|
||||
groupV1.Post("/files/upload", a.authMiddleware, h.UploadMedia)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user