package lessons import ( "sort" "strings" "github.com/gofiber/fiber/v2" "github.com/google/uuid" "gorm.io/gorm" "lms.de/backend/modules/database" "lms.de/backend/modules/structs" "lms.de/backend/modules/utils" ) func GetLessons(c *fiber.Ctx) error { // swagger:operation GET /v1/lessons lessons GetLessons // --- // summary: Get lessons. // consumes: // - application/json // produces: // - application/json // responses: // '200': // description: Lessons retrieved successfully. // schema: // type: array // items: // "$ref": "#/definitions/LessonResponse" // '500': // description: Failed to retrieve lessons. var lessons []structs.LessonResponse database.DB.Model(&structs.Lesson{}).Where("organization_id = ?", c.Locals("organizationId")).Find(&lessons) return c.JSON(lessons) } func CreateLesson(c *fiber.Ctx) error { // swagger:operation POST /v1/lessons lessons CreateLesson // --- // summary: Create lesson. // consumes: // - application/json // produces: // - application/json // responses: // '200': // description: Lesson created successfully. // schema: // "$ref": "#/definitions/CreateLessonResponse" // '500': // description: Failed to create lesson. lesson := structs.Lesson{ Id: uuid.New().String(), OrganizationId: c.Locals("organizationId").(string), State: structs.LessonStateDraft, Title: "Test", CreatorUserId: c.Locals("userId").(string), } database.DB.Create(&lesson) return c.JSON( structs.CreateLessonResponse{ Id: lesson.Id, }, ) } func GetLessonContents(c *fiber.Ctx) error { // swagger:operation GET /v1/lessons/{lessonId}/contents lessons GetLessonContents // --- // summary: Get lesson contents. // consumes: // - application/json // produces: // - application/json // parameters: // - name: lessonId // in: path // required: true // type: string // responses: // '200': // description: Lesson contents retrieved successfully. // schema: // type: array // items: // "$ref": "#/definitions/LessonContent" // '500': // description: Failed to retrieve lesson contents. var params structs.LessonIdParam if err := c.ParamsParser(¶ms); err != nil { return c.SendStatus(fiber.StatusBadRequest) } var lessonContents []structs.GetLessonContentsResponse database.DB.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId).Find(&lessonContents) // sort contents by position sort.SliceStable(lessonContents, func(i, j int) bool { return lessonContents[i].Position < lessonContents[j].Position }) return c.JSON(lessonContents) } func GetLessonSettings(c *fiber.Ctx) error { // swagger:operation GET /v1/lessons/{lessonId}/settings lessons GetLessonSettings // --- // summary: Get lesson settings. // consumes: // - application/json // produces: // - application/json // parameters: // - name: lessonId // in: path // required: true // type: string // responses: // '200': // description: Lesson settings retrieved successfully. // schema: // "$ref": "#/definitions/LessonSettings" // '500': // description: Failed to retrieve lesson settings. var params structs.LessonIdParam if err := c.ParamsParser(¶ms); err != nil { return c.SendStatus(fiber.StatusBadRequest) } var lessonSettings structs.LessonSettings database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).First(&lessonSettings) return c.JSON(lessonSettings) } func UpdateLessonPreviewTitle(c *fiber.Ctx) error { // swagger:operation PATCH /v1/lessons/{lessonId}/preview/title lessons UpdateLessonPreviewTitle // --- // summary: Update lesson preview title. // consumes: // - application/json // produces: // - application/json // parameters: // - name: lessonId // in: path // required: true // type: string // - name: body // in: body // schema: // "$ref": "#/definitions/UpdateLessonPreviewRequest" // responses: // '200': // description: Lesson preview updated successfully. // schema: // "$ref": "#/definitions/UpdateLessonPreviewResponse" // '500': // description: Failed to update lesson preview. var params structs.LessonIdParam if err := c.ParamsParser(¶ms); err != nil { return c.SendStatus(fiber.StatusBadRequest) } var body structs.UpdateLessonPreviewTitleRequest if err := c.BodyParser(&body); err != nil { return c.SendStatus(fiber.StatusBadRequest) } database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).Update("title", body.Title) return c.JSON( fiber.Map{ "message": "Lesson preview updated successfully", }, ) } func UpdateLessonPreviewThumbnail(c *fiber.Ctx) error { // swagger:operation POST /v1/lessons/{lessonId}/preview/thumbnail lessons UpdateLessonPreviewThumbnail // --- // summary: Update lesson preview thumbnail. // consumes: // - multipart/form-data // produces: // - application/json // parameters: // - name: lessonId // in: path // required: true // type: string // - name: body // in: body // schema: // "$ref": "#/definitions/UpdateLessonPreviewThumbnailRequest" // responses: // '200': // description: Lesson preview updated successfully. // '500': // description: Failed to update lesson preview. var params structs.LessonIdParam if err := c.ParamsParser(¶ms); err != nil { return c.SendStatus(fiber.StatusBadRequest) } fileHeader, err := c.FormFile("file") if err != nil { return c.SendStatus(fiber.StatusBadRequest) } if fileHeader.Size > utils.MaxImageSize { return c.SendStatus(fiber.StatusBadRequest) } if !utils.IsFileTypeAllowed(fileHeader.Header.Get("Content-Type"), utils.AcceptedImageFileTypes) { return c.SendStatus(fiber.StatusBadRequest) } // get current thumbnail lesson := structs.Lesson{} database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).First(&lesson) // delete current thumbnail if lesson.ThumbnailUrl != "" { utils.DeleteFile(lesson.ThumbnailUrl) } fileName := uuid.New().String() + "." + strings.Split(fileHeader.Header["Content-Type"][0], "/")[1] databasePath, publicPath := utils.GetFullImagePath(c.Locals("organizationId").(string), params.LessonId) utils.CreateFolderStructureIfNotExists(publicPath) database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).Update("thumbnail_url", databasePath+fileName) return c.SaveFile(fileHeader, publicPath+fileName) } func UpdateLessonState(c *fiber.Ctx) error { // swagger:operation PATCH /v1/lessons/{lessonId}/state lessons UpdateLessonState // --- // summary: Update lesson state. // consumes: // - application/json // produces: // - application/json // parameters: // - name: lessonId // in: path // required: true // type: string // - name: body // in: body // schema: // "$ref": "#/definitions/UpdateLessonStateRequest" // responses: // '200': // description: Lesson state updated successfully. // '500': // description: Failed to update lesson state. var params structs.LessonIdParam if err := c.ParamsParser(¶ms); err != nil { return c.SendStatus(fiber.StatusBadRequest) } var body structs.UpdateLessonStateRequest if err := c.BodyParser(&body); err != nil { return c.SendStatus(fiber.StatusBadRequest) } database.DB.Model(&structs.Lesson{}).Where("id = ?", params.LessonId).Update("state", body.State) return c.JSON( fiber.Map{ "message": "Lesson state updated successfully", }, ) } func AddLessonContent(c *fiber.Ctx) error { // swagger:operation POST /v1/lessons/{lessonId}/contents lessons AddLessonContent // --- // summary: Add lesson content. // consumes: // - application/json // produces: // - application/json // parameters: // - name: lessonId // in: path // required: true // type: string // - name: body // in: body // schema: // "$ref": "#/definitions/AddLessonContentRequest" // responses: // '200': // description: Lesson content added successfully. // schema: // "$ref": "#/definitions/AddLessonContentResponse" // '500': // description: Failed to add lesson content. var params structs.LessonIdParam if err := c.ParamsParser(¶ms); err != nil { return c.SendStatus(fiber.StatusBadRequest) } var body structs.AddLessonContentRequest if err := c.BodyParser(&body); err != nil { return c.SendStatus(fiber.StatusBadRequest) } // get last position var lastContent structs.LessonContent database.DB.Select("position").Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId).Order("position desc").First(&lastContent) // create new content content := structs.LessonContent{ Id: uuid.New().String(), LessonId: params.LessonId, Page: 1, Position: lastContent.Position + 1, Type: body.Type, Data: body.Data, } database.DB.Create(&content) return c.JSON( structs.AddLessonContentResponse{ Id: content.Id, }, ) } func UploadLessonContentFile(c *fiber.Ctx) error { // swagger:operation POST /v1/lessons/{lessonId}/contents/{contentId}/file/{type} lessons UploadLessonContentFile // --- // summary: Upload lesson content file. // consumes: // - multipart/form-data // produces: // - application/json // parameters: // - name: lessonId // in: path // required: true // type: string // - name: contentId // in: path // required: true // type: string // - name: file // in: formData // required: true // type: file // responses: // '200': // description: Lesson content file uploaded successfully. // '500': // description: Failed to upload lesson content file. var params structs.UploadLessonContentFileParam if err := c.ParamsParser(¶ms); err != nil { return c.SendStatus(fiber.StatusBadRequest) } fileHeader, err := c.FormFile("file") if err != nil { return c.SendStatus(fiber.StatusBadRequest) } if params.Type == "image" { if fileHeader.Size > utils.MaxImageSize { return c.SendStatus(fiber.StatusBadRequest) } if !utils.IsFileTypeAllowed(fileHeader.Header.Get("Content-Type"), utils.AcceptedImageFileTypes) { return c.SendStatus(fiber.StatusBadRequest) } } else if params.Type == "video" { if fileHeader.Size > utils.MaxVideoSize { return c.SendStatus(fiber.StatusBadRequest) } if !utils.IsFileTypeAllowed(fileHeader.Header.Get("Content-Type"), utils.AcceptedVideoFileTypes) { return c.SendStatus(fiber.StatusBadRequest) } } else { return c.SendStatus(fiber.StatusBadRequest) } // get current file content := structs.LessonContent{} database.DB.Select("type", "data").Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).First(&content) // delete current image if content.Type == utils.LessonContentTypeImage && content.Data != "" { utils.DeleteFile(content.Data) } fileName := uuid.New().String() + "." + strings.Split(fileHeader.Header["Content-Type"][0], "/")[1] databasePath, publicPath := utils.GetFullImagePath(c.Locals("organizationId").(string), params.LessonId) utils.CreateFolderStructureIfNotExists(publicPath) database.DB.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).Update("data", databasePath+fileName) if err := c.SaveFile(fileHeader, publicPath+fileName); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Failed to save file", }) } return c.JSON(fiber.Map{ "Data": databasePath + fileName, }) } func UpdateLessonContent(c *fiber.Ctx) error { // swagger:operation PATCH /v1/lessons/{lessonId}/contents/{contentId} lessons UpdateLessonContent // --- // summary: Update lesson content. // consumes: // - application/json // produces: // - application/json // parameters: // - name: lessonId // in: path // required: true // type: string // - name: contentId // in: path // required: true // type: string // - name: body // in: body // schema: // "$ref": "#/definitions/UpdateLessonContentRequest" // responses: // '200': // description: Lesson content updated successfully. // '500': // description: Failed to update lesson content. var params structs.LessonIdAndContentIdParam if err := c.ParamsParser(¶ms); err != nil { return c.SendStatus(fiber.StatusBadRequest) } var body structs.UpdateLessonContentRequest if err := c.BodyParser(&body); err != nil { return c.SendStatus(fiber.StatusBadRequest) } database.DB.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).Update("data", body.Data) return c.JSON( fiber.Map{ "message": "Lesson content updated successfully", }, ) } func UpdateLessonContentPosition(c *fiber.Ctx) error { // swagger:operation PATCH /v1/lessons/{lessonId}/contents/{contentId}/position lessons UpdateLessonContentPosition // --- // summary: Update lesson content position. // consumes: // - application/json // produces: // - application/json // parameters: // - name: lessonId // in: path // required: true // type: string // - name: contentId // in: path // required: true // type: string // - name: body // in: body // schema: // "$ref": "#/definitions/UpdateLessonContentPositionRequest" // responses: // '200': // description: Lesson content position updated successfully. // '500': // description: Failed to update lesson content position. var params structs.LessonIdAndContentIdParam if err := c.ParamsParser(¶ms); err != nil { return c.SendStatus(fiber.StatusBadRequest) } var body structs.UpdateLessonContentPositionRequest if err := c.BodyParser(&body); err != nil { return c.SendStatus(fiber.StatusBadRequest) } // update position newPosition := body.Position // Begin a transaction to ensure consistency tx := database.DB.Begin() if tx.Error != nil { return c.SendStatus(fiber.StatusInternalServerError) } // Fetch the current position of the content being moved var currentContent structs.LessonContent if err := tx.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).First(¤tContent).Error; err != nil { tx.Rollback() return c.SendStatus(fiber.StatusNotFound) } oldPosition := currentContent.Position if oldPosition == newPosition { // No need to update if the position hasn't changed return c.JSON(fiber.Map{ "message": "Lesson content position updated successfully", }) } if oldPosition < newPosition { // Items between oldPosition and newPosition need to be shifted down if err := tx.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId). Where("position > ? AND position <= ?", oldPosition, newPosition). Update("position", gorm.Expr("position - 1")).Error; err != nil { tx.Rollback() return c.SendStatus(fiber.StatusInternalServerError) } } else { // Items between newPosition and oldPosition need to be shifted up if err := tx.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId). Where("position >= ? AND position < ?", newPosition, oldPosition). Update("position", gorm.Expr("position + 1")).Error; err != nil { tx.Rollback() return c.SendStatus(fiber.StatusInternalServerError) } } // Update the position of the moved content if err := tx.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).Update("position", newPosition).Error; err != nil { tx.Rollback() return c.SendStatus(fiber.StatusInternalServerError) } // Commit the transaction if err := tx.Commit().Error; err != nil { return c.SendStatus(fiber.StatusInternalServerError) } return c.JSON( fiber.Map{ "message": "Lesson content position updated successfully", }, ) } func DeleteLessonContent(c *fiber.Ctx) error { // swagger:operation DELETE /v1/lessons/{lessonId}/contents/{contentId} lessons DeleteLessonContent // --- // summary: Delete lesson content. // consumes: // - application/json // produces: // - application/json // parameters: // - name: lessonId // in: path // required: true // type: string // - name: contentId // in: path // required: true // type: string // responses: // '200': // description: Lesson content deleted successfully. // '500': // description: Failed to delete lesson content. var params structs.LessonIdAndContentIdParam if err := c.ParamsParser(¶ms); err != nil { return c.SendStatus(fiber.StatusBadRequest) } // Begin a transaction to ensure consistency tx := database.DB.Begin() if tx.Error != nil { return c.SendStatus(fiber.StatusInternalServerError) } // Fetch the current position of the content being deleted var content structs.LessonContent if err := tx.Model(&structs.LessonContent{}).Where("id = ?", params.ContentId).First(&content).Error; err != nil { tx.Rollback() return c.SendStatus(fiber.StatusNotFound) } // Delete the content if err := tx.Delete(&content).Error; err != nil { tx.Rollback() return c.SendStatus(fiber.StatusInternalServerError) } // Shift down the positions of all contents with a higher position if err := tx.Model(&structs.LessonContent{}).Where("lesson_id = ?", params.LessonId). Where("position > ?", content.Position). Update("position", gorm.Expr("position - 1")).Error; err != nil { tx.Rollback() return c.SendStatus(fiber.StatusInternalServerError) } // Commit the transaction if err := tx.Commit().Error; err != nil { return c.SendStatus(fiber.StatusInternalServerError) } return c.JSON( fiber.Map{ "message": "Lesson content deleted successfully", }, ) }