diff --git a/db/migrations/000029_question_sets_intro_video_url.down.sql b/db/migrations/000029_question_sets_intro_video_url.down.sql new file mode 100644 index 0000000..3f75040 --- /dev/null +++ b/db/migrations/000029_question_sets_intro_video_url.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE question_sets + DROP COLUMN IF EXISTS intro_video_url; diff --git a/db/migrations/000029_question_sets_intro_video_url.up.sql b/db/migrations/000029_question_sets_intro_video_url.up.sql new file mode 100644 index 0000000..6456c94 --- /dev/null +++ b/db/migrations/000029_question_sets_intro_video_url.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE question_sets + ADD COLUMN intro_video_url TEXT; diff --git a/db/query/learning_tree.sql b/db/query/learning_tree.sql index 075a1b3..eddcc5f 100644 --- a/db/query/learning_tree.sql +++ b/db/query/learning_tree.sql @@ -44,7 +44,7 @@ WHERE sub_course_id = $1 AND status = 'PUBLISHED' ORDER BY display_order, id; -- name: GetSubCoursePracticesForLearningPath :many -SELECT id, title, description, persona, status, +SELECT id, title, description, persona, status, intro_video_url, (SELECT COUNT(*) FROM question_set_items WHERE set_id = qs.id) AS question_count FROM question_sets qs WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1 diff --git a/db/query/question_sets.sql b/db/query/question_sets.sql index bca258a..5b9a704 100644 --- a/db/query/question_sets.sql +++ b/db/query/question_sets.sql @@ -11,9 +11,10 @@ INSERT INTO question_sets ( passing_score, shuffle_questions, status, - sub_course_video_id + sub_course_video_id, + intro_video_url ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12, $13) RETURNING *; -- name: GetQuestionSetByID :one @@ -59,9 +60,10 @@ SET passing_score = COALESCE($6, passing_score), shuffle_questions = COALESCE($7, shuffle_questions), status = COALESCE($8, status), - sub_course_video_id = COALESCE($9, sub_course_video_id), + intro_video_url = COALESCE($9, intro_video_url), + sub_course_video_id = COALESCE($10, sub_course_video_id), updated_at = CURRENT_TIMESTAMP -WHERE id = $10; +WHERE id = $11; -- name: ArchiveQuestionSet :exec UPDATE question_sets diff --git a/gen/db/learning_tree.sql.go b/gen/db/learning_tree.sql.go index 0a19816..96caf99 100644 --- a/gen/db/learning_tree.sql.go +++ b/gen/db/learning_tree.sql.go @@ -146,7 +146,7 @@ func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTre } const GetSubCoursePracticesForLearningPath = `-- name: GetSubCoursePracticesForLearningPath :many -SELECT id, title, description, persona, status, +SELECT id, title, description, persona, status, intro_video_url, (SELECT COUNT(*) FROM question_set_items WHERE set_id = qs.id) AS question_count FROM question_sets qs WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $1 @@ -160,6 +160,7 @@ type GetSubCoursePracticesForLearningPathRow struct { Description pgtype.Text `json:"description"` Persona pgtype.Text `json:"persona"` Status string `json:"status"` + IntroVideoUrl pgtype.Text `json:"intro_video_url"` QuestionCount int64 `json:"question_count"` } @@ -178,6 +179,7 @@ func (q *Queries) GetSubCoursePracticesForLearningPath(ctx context.Context, owne &i.Description, &i.Persona, &i.Status, + &i.IntroVideoUrl, &i.QuestionCount, ); err != nil { return nil, err diff --git a/gen/db/models.go b/gen/db/models.go index a86ef36..15d19be 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -173,6 +173,7 @@ type QuestionSet struct { UpdatedAt pgtype.Timestamptz `json:"updated_at"` SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"` DisplayOrder int32 `json:"display_order"` + IntroVideoUrl pgtype.Text `json:"intro_video_url"` } type QuestionSetItem struct { diff --git a/gen/db/question_set_items.sql.go b/gen/db/question_set_items.sql.go index 62cfb28..a78a515 100644 --- a/gen/db/question_set_items.sql.go +++ b/gen/db/question_set_items.sql.go @@ -198,7 +198,7 @@ func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQu } const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many -SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order +SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order, qs.intro_video_url FROM question_sets qs JOIN question_set_items qsi ON qsi.set_id = qs.id WHERE qsi.question_id = $1 @@ -231,6 +231,7 @@ func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questio &i.UpdatedAt, &i.SubCourseVideoID, &i.DisplayOrder, + &i.IntroVideoUrl, ); err != nil { return nil, err } diff --git a/gen/db/question_sets.sql.go b/gen/db/question_sets.sql.go index dac6cbd..70167c9 100644 --- a/gen/db/question_sets.sql.go +++ b/gen/db/question_sets.sql.go @@ -64,10 +64,11 @@ INSERT INTO question_sets ( passing_score, shuffle_questions, status, - sub_course_video_id + sub_course_video_id, + intro_video_url ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12) -RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12, $13) +RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url ` type CreateQuestionSetParams struct { @@ -83,6 +84,7 @@ type CreateQuestionSetParams struct { Column10 interface{} `json:"column_10"` Column11 interface{} `json:"column_11"` SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"` + IntroVideoUrl pgtype.Text `json:"intro_video_url"` } func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetParams) (QuestionSet, error) { @@ -99,6 +101,7 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa arg.Column10, arg.Column11, arg.SubCourseVideoID, + arg.IntroVideoUrl, ) var i QuestionSet err := row.Scan( @@ -118,6 +121,7 @@ func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetPa &i.UpdatedAt, &i.SubCourseVideoID, &i.DisplayOrder, + &i.IntroVideoUrl, ) return i, err } @@ -133,7 +137,7 @@ func (q *Queries) DeleteQuestionSet(ctx context.Context, id int64) error { } const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one -SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order +SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url FROM question_sets WHERE set_type = 'INITIAL_ASSESSMENT' AND status = 'PUBLISHED' @@ -161,12 +165,13 @@ func (q *Queries) GetInitialAssessmentSet(ctx context.Context) (QuestionSet, err &i.UpdatedAt, &i.SubCourseVideoID, &i.DisplayOrder, + &i.IntroVideoUrl, ) return i, err } const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many -SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order +SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url FROM question_sets WHERE owner_type = $1 AND owner_id = $2 @@ -205,6 +210,7 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu &i.UpdatedAt, &i.SubCourseVideoID, &i.DisplayOrder, + &i.IntroVideoUrl, ); err != nil { return nil, err } @@ -217,7 +223,7 @@ func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPu } const GetQuestionSetByID = `-- name: GetQuestionSetByID :one -SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order +SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url FROM question_sets WHERE id = $1 ` @@ -242,12 +248,13 @@ func (q *Queries) GetQuestionSetByID(ctx context.Context, id int64) (QuestionSet &i.UpdatedAt, &i.SubCourseVideoID, &i.DisplayOrder, + &i.IntroVideoUrl, ) return i, err } const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many -SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order +SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url FROM question_sets WHERE owner_type = $1 AND owner_id = $2 @@ -286,6 +293,7 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet &i.UpdatedAt, &i.SubCourseVideoID, &i.DisplayOrder, + &i.IntroVideoUrl, ); err != nil { return nil, err } @@ -300,7 +308,7 @@ func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSet const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many SELECT COUNT(*) OVER () AS total_count, - qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order + qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id, qs.display_order, qs.intro_video_url FROM question_sets qs WHERE set_type = $1 AND status != 'ARCHIVED' @@ -333,6 +341,7 @@ type GetQuestionSetsByTypeRow struct { UpdatedAt pgtype.Timestamptz `json:"updated_at"` SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"` DisplayOrder int32 `json:"display_order"` + IntroVideoUrl pgtype.Text `json:"intro_video_url"` } func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSetsByTypeParams) ([]GetQuestionSetsByTypeRow, error) { @@ -362,6 +371,7 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets &i.UpdatedAt, &i.SubCourseVideoID, &i.DisplayOrder, + &i.IntroVideoUrl, ); err != nil { return nil, err } @@ -374,7 +384,7 @@ func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSets } const GetSubCourseInitialAssessmentSet = `-- name: GetSubCourseInitialAssessmentSet :one -SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order +SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id, display_order, intro_video_url FROM question_sets WHERE set_type = 'INITIAL_ASSESSMENT' AND owner_type = 'SUB_COURSE' @@ -404,6 +414,7 @@ func (q *Queries) GetSubCourseInitialAssessmentSet(ctx context.Context, ownerID &i.UpdatedAt, &i.SubCourseVideoID, &i.DisplayOrder, + &i.IntroVideoUrl, ) return i, err } @@ -507,9 +518,10 @@ SET passing_score = COALESCE($6, passing_score), shuffle_questions = COALESCE($7, shuffle_questions), status = COALESCE($8, status), - sub_course_video_id = COALESCE($9, sub_course_video_id), + intro_video_url = COALESCE($9, intro_video_url), + sub_course_video_id = COALESCE($10, sub_course_video_id), updated_at = CURRENT_TIMESTAMP -WHERE id = $10 +WHERE id = $11 ` type UpdateQuestionSetParams struct { @@ -521,6 +533,7 @@ type UpdateQuestionSetParams struct { PassingScore pgtype.Int4 `json:"passing_score"` ShuffleQuestions bool `json:"shuffle_questions"` Status string `json:"status"` + IntroVideoUrl pgtype.Text `json:"intro_video_url"` SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"` ID int64 `json:"id"` } @@ -535,6 +548,7 @@ func (q *Queries) UpdateQuestionSet(ctx context.Context, arg UpdateQuestionSetPa arg.PassingScore, arg.ShuffleQuestions, arg.Status, + arg.IntroVideoUrl, arg.SubCourseVideoID, arg.ID, ) diff --git a/internal/domain/course_management.go b/internal/domain/course_management.go index bec9ce1..91bad83 100644 --- a/internal/domain/course_management.go +++ b/internal/domain/course_management.go @@ -130,6 +130,7 @@ type LearningPathPractice struct { Description *string `json:"description,omitempty"` Persona *string `json:"persona,omitempty"` Status string `json:"status"` + IntroVideoURL *string `json:"intro_video_url,omitempty"` QuestionCount int64 `json:"question_count"` } diff --git a/internal/domain/questions.go b/internal/domain/questions.go index 5cb4dce..d5c9a20 100644 --- a/internal/domain/questions.go +++ b/internal/domain/questions.go @@ -104,6 +104,7 @@ type QuestionSet struct { ShuffleQuestions bool Status string SubCourseVideoID *int64 + IntroVideoURL *string UserPersonas []UserPersona CreatedAt time.Time UpdatedAt *time.Time @@ -170,6 +171,7 @@ type CreateQuestionSetInput struct { ShuffleQuestions *bool Status *string SubCourseVideoID *int64 + IntroVideoURL *string } // UserPersona represents a user acting as a persona in a practice session diff --git a/internal/repository/learning_tree.go b/internal/repository/learning_tree.go index 8b62cac..74e095c 100644 --- a/internal/repository/learning_tree.go +++ b/internal/repository/learning_tree.go @@ -154,6 +154,7 @@ func (s *Store) getSubCoursePracticesForPath(ctx context.Context, subCourseID in Description: ptrString(row.Description), Persona: ptrString(row.Persona), Status: row.Status, + IntroVideoURL: ptrString(row.IntroVideoUrl), QuestionCount: row.QuestionCount, } } diff --git a/internal/repository/questions.go b/internal/repository/questions.go index 82f49e2..92e2fc9 100644 --- a/internal/repository/questions.go +++ b/internal/repository/questions.go @@ -124,6 +124,7 @@ func questionSetToDomain(qs dbgen.QuestionSet) domain.QuestionSet { ShuffleQuestions: qs.ShuffleQuestions, Status: qs.Status, SubCourseVideoID: fromPgInt8(qs.SubCourseVideoID), + IntroVideoURL: fromPgText(qs.IntroVideoUrl), CreatedAt: qs.CreatedAt.Time, UpdatedAt: timePtr(qs.UpdatedAt), } @@ -542,6 +543,7 @@ func (s *Store) CreateQuestionSet(ctx context.Context, input domain.CreateQuesti Column10: shuffleQuestions, Column11: status, SubCourseVideoID: toPgInt8(input.SubCourseVideoID), + IntroVideoUrl: toPgText(input.IntroVideoURL), }) if err != nil { return domain.QuestionSet{}, err @@ -603,6 +605,7 @@ func (s *Store) GetQuestionSetsByType(ctx context.Context, setType string, limit ShuffleQuestions: r.ShuffleQuestions, Status: r.Status, SubCourseVideoID: fromPgInt8(r.SubCourseVideoID), + IntroVideoURL: fromPgText(r.IntroVideoUrl), CreatedAt: r.CreatedAt.Time, UpdatedAt: timePtr(r.UpdatedAt), } @@ -688,6 +691,7 @@ func (s *Store) UpdateQuestionSet(ctx context.Context, id int64, input domain.Cr PassingScore: toPgInt4(input.PassingScore), ShuffleQuestions: shuffleQuestions, Status: status, + IntroVideoUrl: toPgText(input.IntroVideoURL), SubCourseVideoID: toPgInt8(input.SubCourseVideoID), }) } diff --git a/internal/web_server/handlers/maintenance_handler.go b/internal/web_server/handlers/maintenance_handler.go index 6660781..d997360 100644 --- a/internal/web_server/handlers/maintenance_handler.go +++ b/internal/web_server/handlers/maintenance_handler.go @@ -16,6 +16,10 @@ type resetAndReseedReq struct { Confirm string `json:"confirm"` } +type clearCourseManagementReq struct { + Confirm string `json:"confirm"` +} + func extractInsertStatement(sqlContent string, tableName string) (string, bool) { pattern := fmt.Sprintf(`(?is)INSERT\s+INTO\s+%s\b.*?;`, regexp.QuoteMeta(tableName)) re := regexp.MustCompile(pattern) @@ -191,3 +195,71 @@ func (h *Handler) ResetAndReseedDatabase(c *fiber.Ctx) error { StatusCode: fiber.StatusOK, }) } + +// ClearCourseManagementData godoc +// @Summary Clear course management hierarchy data only +// @Description Truncates course_categories, courses, and sub_courses (same scope as reset-reseed) without re-inserting seed SQL. +// @Tags internal +// @Accept json +// @Produce json +// @Param X-Seed-Reset-Token header string false "Optional token when DB_RESET_RESEED_TOKEN is set" +// @Param body body clearCourseManagementReq true "Confirmation payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 403 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/internal/db/clear-course-management [post] +func (h *Handler) ClearCourseManagementData(c *fiber.Ctx) error { + if h.Cfg == nil || !h.Cfg.DBResetReseedEnabled { + return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ + Message: "Operation is disabled", + Error: "internal course management maintenance is disabled", + }) + } + + var req clearCourseManagementReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + if strings.TrimSpace(req.Confirm) != "CLEAR_COURSE_MANAGEMENT" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Confirmation required", + Error: `set confirm to "CLEAR_COURSE_MANAGEMENT"`, + }) + } + + expectedToken := strings.TrimSpace(h.Cfg.DBResetReseedToken) + if expectedToken != "" { + providedToken := strings.TrimSpace(c.Get("X-Seed-Reset-Token")) + if subtle.ConstantTimeCompare([]byte(providedToken), []byte(expectedToken)) != 1 { + return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ + Message: "Invalid reset token", + Error: "missing or invalid X-Seed-Reset-Token", + }) + } + } + + tableNames := []string{"course_categories", "courses", "sub_courses"} + sql := `BEGIN; +TRUNCATE TABLE sub_courses, courses, course_categories RESTART IDENTITY CASCADE; +COMMIT;` + + if _, err := h.analyticsDB.ExecRaw(c.Context(), sql); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to clear course management data", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Course management hierarchy cleared successfully (no re-seed)", + Data: map[string]interface{}{ + "tables": tableNames, + }, + Success: true, + StatusCode: fiber.StatusOK, + }) +} diff --git a/internal/web_server/handlers/questions.go b/internal/web_server/handlers/questions.go index 18f1606..1b5b694 100644 --- a/internal/web_server/handlers/questions.go +++ b/internal/web_server/handlers/questions.go @@ -525,6 +525,7 @@ type createQuestionSetReq struct { ShuffleQuestions *bool `json:"shuffle_questions"` Status *string `json:"status"` SubCourseVideoID *int64 `json:"sub_course_video_id"` + IntroVideoURL *string `json:"intro_video_url"` } type questionSetRes struct { @@ -541,6 +542,7 @@ type questionSetRes struct { ShuffleQuestions bool `json:"shuffle_questions"` Status string `json:"status"` SubCourseVideoID *int64 `json:"sub_course_video_id,omitempty"` + IntroVideoURL *string `json:"intro_video_url,omitempty"` CreatedAt string `json:"created_at"` QuestionCount *int64 `json:"question_count,omitempty"` } @@ -626,6 +628,7 @@ func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error { ShuffleQuestions: req.ShuffleQuestions, Status: req.Status, SubCourseVideoID: req.SubCourseVideoID, + IntroVideoURL: req.IntroVideoURL, } set, err := h.questionsSvc.CreateQuestionSet(c.Context(), input) @@ -659,6 +662,7 @@ func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error { ShuffleQuestions: set.ShuffleQuestions, Status: set.Status, SubCourseVideoID: set.SubCourseVideoID, + IntroVideoURL: set.IntroVideoURL, CreatedAt: set.CreatedAt.String(), }, }) @@ -711,6 +715,7 @@ func (h *Handler) GetSubCourseEntryAssessmentSet(c *fiber.Ctx) error { ShuffleQuestions: set.ShuffleQuestions, Status: set.Status, SubCourseVideoID: set.SubCourseVideoID, + IntroVideoURL: set.IntroVideoURL, CreatedAt: set.CreatedAt.String(), QuestionCount: &count, }, @@ -774,6 +779,7 @@ func (h *Handler) GetQuestionSetByID(c *fiber.Ctx) error { ShuffleQuestions: set.ShuffleQuestions, Status: set.Status, SubCourseVideoID: set.SubCourseVideoID, + IntroVideoURL: set.IntroVideoURL, CreatedAt: set.CreatedAt.String(), QuestionCount: &count, }, @@ -829,6 +835,7 @@ func (h *Handler) GetQuestionSetsByType(c *fiber.Ctx) error { ShuffleQuestions: s.ShuffleQuestions, Status: s.Status, SubCourseVideoID: s.SubCourseVideoID, + IntroVideoURL: s.IntroVideoURL, CreatedAt: s.CreatedAt.String(), }) } @@ -895,6 +902,7 @@ func (h *Handler) GetQuestionSetsByOwner(c *fiber.Ctx) error { ShuffleQuestions: s.ShuffleQuestions, Status: s.Status, SubCourseVideoID: s.SubCourseVideoID, + IntroVideoURL: s.IntroVideoURL, CreatedAt: s.CreatedAt.String(), }) } @@ -915,6 +923,7 @@ type updateQuestionSetReq struct { ShuffleQuestions *bool `json:"shuffle_questions"` Status *string `json:"status"` SubCourseVideoID *int64 `json:"sub_course_video_id"` + IntroVideoURL *string `json:"intro_video_url"` } // UpdateQuestionSet godoc @@ -962,6 +971,7 @@ func (h *Handler) UpdateQuestionSet(c *fiber.Ctx) error { ShuffleQuestions: req.ShuffleQuestions, Status: req.Status, SubCourseVideoID: req.SubCourseVideoID, + IntroVideoURL: req.IntroVideoURL, } err = h.questionsSvc.UpdateQuestionSet(c.Context(), id, input) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 5fc84f4..76491c8 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -245,6 +245,7 @@ func (a *App) initAppRoutes() { groupV1.Post("/user/me/deletion/cancel", a.authMiddleware, a.RequirePermission("users.cancel_delete_self"), h.CancelMyUserAccountDeletion) groupV1.Post("/internal/users/purge-due-deletions", a.authMiddleware, a.RequirePermission("users.purge_due_deletions"), h.PurgeDueDeletedUsers) groupV1.Post("/internal/db/reset-reseed", a.authMiddleware, a.RequirePermission("internal.db.reset_reseed"), h.ResetAndReseedDatabase) + groupV1.Post("/internal/db/clear-course-management", a.authMiddleware, a.RequirePermission("internal.db.reset_reseed"), h.ClearCourseManagementData) groupV1.Get("/user/single/:id", a.authMiddleware, a.RequirePermission("users.get"), h.GetUserByID) groupV1.Delete("/user/delete/:id", a.authMiddleware, a.RequirePermission("users.delete"), h.DeleteUser) groupV1.Post("/user/search", a.authMiddleware, a.RequirePermission("users.search"), h.SearchUserByNameOrPhone)