From 2ff1e89263d7e2c4f6760e128d01f21fcdc62509 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 14 Apr 2026 05:04:38 -0700 Subject: [PATCH] more course CRUD fix --- gen/db/compat_course_management.go | 113 +++++++++++ .../web_server/handlers/hierarchy_handler.go | 183 ++++++++++++++++++ internal/web_server/routes.go | 7 + 3 files changed, 303 insertions(+) create mode 100644 gen/db/compat_course_management.go diff --git a/gen/db/compat_course_management.go b/gen/db/compat_course_management.go new file mode 100644 index 0000000..d639ad6 --- /dev/null +++ b/gen/db/compat_course_management.go @@ -0,0 +1,113 @@ +package dbgen + +import ( + "context" +) + +func (q *Queries) GetSubModuleByIDCompat(ctx context.Context, id int64) (SubModule, error) { + row := q.db.QueryRow(ctx, ` +SELECT id, module_id, title, description, display_order, is_active, created_at, legacy_sub_course_id +FROM sub_modules +WHERE id = $1 +`, id) + var i SubModule + err := row.Scan( + &i.ID, + &i.ModuleID, + &i.Title, + &i.Description, + &i.DisplayOrder, + &i.IsActive, + &i.CreatedAt, + &i.LegacySubCourseID, + ) + return i, err +} + +func (q *Queries) UpdateSubModuleCompat(ctx context.Context, id int64, title string, description string, isActive bool) error { + _, err := q.db.Exec(ctx, ` +UPDATE sub_modules +SET + title = $1, + description = NULLIF($2, ''), + is_active = $3 +WHERE id = $4 +`, title, description, isActive, id) + return err +} + +func (q *Queries) DeleteSubModuleCompat(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, `DELETE FROM sub_modules WHERE id = $1`, id) + return err +} + +func (q *Queries) UpdateSubModuleVideoCompat(ctx context.Context, id int64, title string, description string, videoURL string) error { + _, err := q.db.Exec(ctx, ` +UPDATE sub_module_videos +SET + title = $1, + description = NULLIF($2, ''), + video_url = $3 +WHERE id = $4 +`, title, description, videoURL, id) + return err +} + +func (q *Queries) DeleteSubModuleVideoCompat(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, `DELETE FROM sub_module_videos WHERE id = $1`, id) + return err +} + +func (q *Queries) UpdatePracticeCompat(ctx context.Context, id int64, title string, description string, persona string) error { + _, err := q.db.Exec(ctx, ` +UPDATE question_sets +SET + title = $1, + description = NULLIF($2, ''), + persona = NULLIF($3, ''), + updated_at = CURRENT_TIMESTAMP +WHERE id = $4 +`, title, description, persona, id) + if err != nil { + return err + } + + _, err = q.db.Exec(ctx, ` +UPDATE sub_module_practices +SET + title = $1, + description = NULLIF($2, '') +WHERE question_set_id = $3 +`, title, description, id) + return err +} + +func (q *Queries) UpdatePracticeStatusCompat(ctx context.Context, id int64, isActive bool) error { + status := "ARCHIVED" + if isActive { + status = "PUBLISHED" + } + + _, err := q.db.Exec(ctx, ` +UPDATE question_sets +SET + status = $1, + updated_at = CURRENT_TIMESTAMP +WHERE id = $2 +`, status, id) + if err != nil { + return err + } + + _, err = q.db.Exec(ctx, ` +UPDATE sub_module_practices +SET is_active = $1 +WHERE question_set_id = $2 +`, isActive, id) + return err +} + +func (q *Queries) DeletePracticeCompat(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, `DELETE FROM question_sets WHERE id = $1`, id) + return err +} diff --git a/internal/web_server/handlers/hierarchy_handler.go b/internal/web_server/handlers/hierarchy_handler.go index 4c493d6..81c8314 100644 --- a/internal/web_server/handlers/hierarchy_handler.go +++ b/internal/web_server/handlers/hierarchy_handler.go @@ -81,6 +81,25 @@ type createSubModuleVideoReq struct { Status *string `json:"status"` } +type updateSubModuleReq struct { + Title *string `json:"title"` + Description *string `json:"description"` + IsActive *bool `json:"is_active"` +} + +type updateSubModuleVideoReq struct { + Title *string `json:"title"` + Description *string `json:"description"` + VideoURL *string `json:"video_url"` +} + +type updatePracticeReq struct { + Title *string `json:"title"` + Description *string `json:"description"` + Persona *string `json:"persona"` + IsActive *bool `json:"is_active"` +} + type attachSubModuleLessonReq struct { SubModuleID int64 `json:"sub_module_id"` QuestionSetID int64 `json:"question_set_id"` @@ -738,3 +757,167 @@ func (h *Handler) CreateSubModulePractice(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: created}) } +func (h *Handler) GetSubModuleVideos(c *fiber.Ctx) error { + subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64) + if err != nil || subModuleID <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"}) + } + + videos, err := h.analyticsDB.GetSubModuleVideos(c.Context(), subModuleID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module videos", Error: err.Error()}) + } + + return c.JSON(domain.Response{ + Message: "Sub-module videos retrieved successfully", + Data: map[string]interface{}{ + "videos": videos, + "total_count": len(videos), + }, + }) +} + +func (h *Handler) UpdateSubModule(c *fiber.Ctx) error { + subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64) + if err != nil || subModuleID <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"}) + } + + existing, err := h.analyticsDB.GetSubModuleByIDCompat(c.Context(), subModuleID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to load sub-module", Error: err.Error()}) + } + + var req updateSubModuleReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) + } + + title := existing.Title + if req.Title != nil { + title = *req.Title + } + description := "" + if existing.Description.Valid { + description = existing.Description.String + } + if req.Description != nil { + description = *req.Description + } + isActive := existing.IsActive + if req.IsActive != nil { + isActive = *req.IsActive + } + + if err := h.analyticsDB.UpdateSubModuleCompat(c.Context(), subModuleID, title, description, isActive); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update sub-module", Error: err.Error()}) + } + + updated, err := h.analyticsDB.GetSubModuleByIDCompat(c.Context(), subModuleID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Sub-module updated but failed to fetch latest record", Error: err.Error()}) + } + return c.JSON(domain.Response{Message: "Sub-module updated", Data: updated}) +} + +func (h *Handler) DeleteSubModule(c *fiber.Ctx) error { + subModuleID, err := strconv.ParseInt(c.Params("subModuleId"), 10, 64) + if err != nil || subModuleID <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid sub-module ID", Error: "subModuleId must be a positive integer"}) + } + + if err := h.analyticsDB.DeleteSubModuleCompat(c.Context(), subModuleID); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete sub-module", Error: err.Error()}) + } + return c.JSON(domain.Response{Message: "Sub-module deleted"}) +} + +func (h *Handler) UpdateSubModuleVideo(c *fiber.Ctx) error { + videoID, err := strconv.ParseInt(c.Params("videoId"), 10, 64) + if err != nil || videoID <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid video ID", Error: "videoId must be a positive integer"}) + } + + var req updateSubModuleVideoReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) + } + if req.Title == nil || strings.TrimSpace(*req.Title) == "" || req.VideoURL == nil || strings.TrimSpace(*req.VideoURL) == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title and video_url are required"}) + } + + description := "" + if req.Description != nil { + description = *req.Description + } + + if err := h.analyticsDB.UpdateSubModuleVideoCompat(c.Context(), videoID, *req.Title, description, *req.VideoURL); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update sub-module video", Error: err.Error()}) + } + return c.JSON(domain.Response{Message: "Sub-module video updated"}) +} + +func (h *Handler) DeleteSubModuleVideo(c *fiber.Ctx) error { + videoID, err := strconv.ParseInt(c.Params("videoId"), 10, 64) + if err != nil || videoID <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid video ID", Error: "videoId must be a positive integer"}) + } + + if err := h.analyticsDB.DeleteSubModuleVideoCompat(c.Context(), videoID); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete sub-module video", Error: err.Error()}) + } + return c.JSON(domain.Response{Message: "Sub-module video deleted"}) +} + +func (h *Handler) UpdatePractice(c *fiber.Ctx) error { + practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64) + if err != nil || practiceID <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice ID", Error: "practiceId must be a positive integer"}) + } + + var req updatePracticeReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) + } + + if req.IsActive != nil { + if err := h.analyticsDB.UpdatePracticeStatusCompat(c.Context(), practiceID, *req.IsActive); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice status", Error: err.Error()}) + } + return c.JSON(domain.Response{Message: "Practice status updated"}) + } + + title := "" + if req.Title != nil { + title = *req.Title + } + description := "" + if req.Description != nil { + description = *req.Description + } + persona := "" + if req.Persona != nil { + persona = *req.Persona + } + if strings.TrimSpace(title) == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "title is required"}) + } + + if err := h.analyticsDB.UpdatePracticeCompat(c.Context(), practiceID, title, description, persona); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to update practice", Error: err.Error()}) + } + return c.JSON(domain.Response{Message: "Practice updated"}) +} + +func (h *Handler) DeletePractice(c *fiber.Ctx) error { + practiceID, err := strconv.ParseInt(c.Params("practiceId"), 10, 64) + if err != nil || practiceID <= 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid practice ID", Error: "practiceId must be a positive integer"}) + } + + if err := h.analyticsDB.DeletePracticeCompat(c.Context(), practiceID); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to delete practice", Error: err.Error()}) + } + return c.JSON(domain.Response{Message: "Practice deleted"}) +} + diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 63c360c..6a86b56 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -91,9 +91,16 @@ func (a *App) initAppRoutes() { groupV1.Post("/course-management/levels", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateLevel) groupV1.Post("/course-management/modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateModule) groupV1.Post("/course-management/sub-modules", a.authMiddleware, a.RequirePermission("subcourses.create"), h.CreateSubModule) + groupV1.Put("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.update"), h.UpdateSubModule) + groupV1.Delete("/course-management/sub-modules/:subModuleId", a.authMiddleware, a.RequirePermission("subcourses.delete"), h.DeleteSubModule) + groupV1.Get("/course-management/sub-modules/:subModuleId/videos", a.authMiddleware, a.RequirePermission("videos.list"), h.GetSubModuleVideos) groupV1.Post("/course-management/sub-module-videos", a.authMiddleware, a.RequirePermission("videos.create"), h.CreateSubModuleVideo) + groupV1.Put("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.update"), h.UpdateSubModuleVideo) + groupV1.Delete("/course-management/sub-module-videos/:videoId", a.authMiddleware, a.RequirePermission("videos.delete"), h.DeleteSubModuleVideo) groupV1.Post("/course-management/sub-module-lessons", a.authMiddleware, a.RequirePermission("question_sets.update"), h.AttachSubModuleLesson) groupV1.Post("/course-management/sub-module-practices", a.authMiddleware, a.RequirePermission("question_sets.update"), h.CreateSubModulePractice) + groupV1.Put("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.update"), h.UpdatePractice) + groupV1.Delete("/course-management/practices/:practiceId", a.authMiddleware, a.RequirePermission("question_sets.delete"), h.DeletePractice) // Questions groupV1.Post("/questions", a.authMiddleware, a.RequirePermission("questions.create"), h.CreateQuestion)