package quiz import ( "fmt" "gitea.pena/PenaSide/common/log_mw" "gitea.pena/SQuiz/common/dal" "gitea.pena/SQuiz/common/middleware" "gitea.pena/SQuiz/common/model" "gitea.pena/SQuiz/common/repository/quiz" "gitea.pena/SQuiz/core/internal/brokers" "gitea.pena/SQuiz/core/internal/models" "github.com/gofiber/fiber/v2" "strconv" "time" "unicode/utf8" ) type Deps struct { DAL *dal.DAL ProducerGigaChat *brokers.Producer } type Quiz struct { dal *dal.DAL producerGigaChat *brokers.Producer } func NewQuizController(deps Deps) *Quiz { return &Quiz{ dal: deps.DAL, producerGigaChat: deps.ProducerGigaChat, } } type CreateQuizReq struct { Fingerprinting bool `json:"fingerprinting"` // true if you need to store device id Repeatable bool `json:"repeatable"` // make it true for allow more than one quiz checkouting NotePrevented bool `json:"note_prevented"` // note answers even if the quiz was aborted MailNotifications bool `json:"mail_notifications"` // set true if you want get an email with every quiz passing UniqueAnswers bool `json:"unique_answers"` // set true if we you mention only last quiz passing Name string `json:"name"` // max 700 chars Description string `json:"description"` Config string `json:"config"` // serialize json with config for page rules. fill it up only if implement one form scenario Status string `json:"status"` // status of quiz as enum. see Status const. fill it up only if implement one form scenario Limit uint64 `json:"limit"` // max count of quiz passing DueTo uint64 `json:"due_to"` // time when quiz is end QuestionCnt uint64 `json:"question_cnt"` // for creating at one request TimeOfPassing uint64 `json:"time_of_passing"` // amount of seconds for give all appropriate answers for quiz Pausable bool `json:"pausable"` // true allows to pause the quiz taking Super bool `json:"super"` // set true if you want to create group GroupId uint64 `json:"group_id"` // if you create quiz in group provide there the id of super quiz } // CreateQuiz handler for quiz creating request func (r *Quiz) CreateQuiz(ctx *fiber.Ctx) error { var req CreateQuizReq if err := ctx.BodyParser(&req); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data") } accountId, ok := middleware.GetAccountId(ctx) if !ok { return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required") } hlogger := log_mw.ExtractLogger(ctx) // check that we can store name if utf8.RuneCountInString(req.Name) > 700 { return ctx.Status(fiber.StatusUnprocessableEntity).SendString("name field should have less then 700 chars") } // status should be empty or equal one of status enum strings // I mean not draft, template, stop, start statuses if req.Status != "" && req.Status != model.StatusDraft && req.Status != model.StatusTemplate && req.Status != model.StatusAI && req.Status != model.StatusStop && req.Status != model.StatusStart { return ctx.Status(fiber.StatusNotAcceptable).SendString("status on creating must be only draft,template,stop,start") } // DueTo should be bigger then now if req.DueTo != 0 && req.DueTo <= uint64(time.Now().Unix()) { return ctx.Status(fiber.StatusNotAcceptable).SendString("due to time must be lesser then now") } // you can pause quiz only if it has deadline for passing if req.Pausable && req.TimeOfPassing == 0 { return ctx.Status(fiber.StatusConflict).SendString("you can pause quiz only if it has deadline for passing") } record := model.Quiz{ AccountId: accountId, Fingerprinting: req.Fingerprinting, Repeatable: req.Repeatable, NotePrevented: req.NotePrevented, MailNotifications: req.MailNotifications, UniqueAnswers: req.UniqueAnswers, Name: req.Name, Description: req.Description, Config: req.Config, Status: req.Status, Limit: req.Limit, DueTo: req.DueTo, TimeOfPassing: req.TimeOfPassing, Pausable: req.Pausable, QuestionsCount: req.QuestionCnt, ParentIds: []int32{}, Super: req.Super, GroupId: req.GroupId, } quizID, err := r.dal.QuizRepo.CreateQuiz(ctx.Context(), &record) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } hlogger.Emit(models.InfoQuizCreated{ CtxUserID: accountId, CtxIDInt: int64(quizID), }) return ctx.Status(fiber.StatusCreated).JSON(record) } // GetQuizListReq request struct for paginated quiz table type GetQuizListReq struct { Limit uint64 `json:"limit"` Page uint64 `json:"page"` From int64 `json:"from"` To int64 `json:"to"` Search string `json:"search"` Status string `json:"status"` Deleted bool `json:"deleted"` Archived bool `json:"archived"` Super bool `json:"super"` GroupId uint64 `json:"group_id"` } type GetQuizListResp struct { Count uint64 `json:"count"` Items []model.Quiz `json:"items"` } // GetQuizList handler for paginated list quiz func (r *Quiz) GetQuizList(ctx *fiber.Ctx) error { var req GetQuizListReq if err := ctx.BodyParser(&req); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data") } accountId, ok := middleware.GetAccountId(ctx) if !ok { return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required") } if req.Status != "" && req.Status != model.StatusStop && req.Status != model.StatusStart && req.Status != model.StatusDraft && req.Status != model.StatusTemplate && req.Status != model.StatusAI && req.Status != model.StatusTimeout && req.Status != model.StatusOffLimit { return ctx.Status(fiber.StatusNotAcceptable).SendString("inappropriate status, allowed only '', " + "'stop','start','draft', 'template','timeout','offlimit'") } res, cnt, err := r.dal.QuizRepo.GetQuizList(ctx.Context(), quiz.GetQuizListDeps{ Limit: req.Limit, Offset: req.Limit * req.Page, From: uint64(req.From), To: uint64(req.To), Group: req.GroupId, Deleted: req.Deleted, Archived: req.Archived, Super: req.Super, Search: req.Search, Status: req.Status, AccountId: accountId, }) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } return ctx.JSON(GetQuizListResp{ Items: res, Count: cnt, }) } type UpdateQuizReq struct { Id uint64 `json:"id"` Fingerprinting bool `json:"fp"` Repeatable bool `json:"rep"` NotePrevented bool `json:"note_prevented"` MailNotifications bool `json:"mailing"` UniqueAnswers bool `json:"uniq"` Name string `json:"name"` Description string `json:"desc"` Config string `json:"conf"` Status string `json:"status"` Limit uint64 `json:"limit"` DueTo uint64 `json:"due_to"` TimeOfPassing uint64 `json:"time_of_passing"` Pausable bool `json:"pausable"` QuestionCnt uint64 `json:"question_cnt"` // for creating at one request Super bool `json:"super"` GroupId uint64 `json:"group_id"` } type UpdateResp struct { Updated uint64 `json:"updated"` } func (r *Quiz) UpdateQuiz(ctx *fiber.Ctx) error { var req UpdateQuizReq if err := ctx.BodyParser(&req); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data") } accountId, ok := middleware.GetAccountId(ctx) if !ok { return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required") } hlogger := log_mw.ExtractLogger(ctx) if req.Id == 0 { return ctx.Status(fiber.StatusFailedDependency).SendString("need id of question for update") } if utf8.RuneCountInString(req.Name) > 700 { return ctx.Status(fiber.StatusUnprocessableEntity).SendString("name field should have less then 700 chars") } // status should be empty or equal one of status enum strings // I mean not draft, template, stop, start statuses if req.Status != "" && req.Status != model.StatusDraft && req.Status != model.StatusTemplate && req.Status != model.StatusAI && req.Status != model.StatusStop && req.Status != model.StatusStart { return ctx.Status(fiber.StatusNotAcceptable).SendString("status on creating must be only draft,template,stop,start") } // DueTo should be bigger then now if req.DueTo != 0 && req.DueTo <= uint64(time.Now().Unix()) { return ctx.Status(fiber.StatusNotAcceptable).SendString("due to time must be lesser then now") } // you can pause quiz only if it has deadline for passing if req.Pausable && req.TimeOfPassing == 0 { return ctx.Status(fiber.StatusConflict).SendString("you can pause quiz only if it has deadline for passing") } quiz, err := r.dal.QuizRepo.MoveToHistoryQuiz(ctx.Context(), req.Id, accountId) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } quiz.ParentIds = append(quiz.ParentIds, int32(quiz.Id)) quiz.Id = req.Id quiz.Version += 1 if req.Fingerprinting != quiz.Fingerprinting { quiz.Fingerprinting = req.Fingerprinting } if req.Repeatable != quiz.Repeatable { quiz.Repeatable = req.Repeatable } if req.MailNotifications != quiz.MailNotifications { quiz.MailNotifications = req.MailNotifications } if req.NotePrevented != quiz.NotePrevented { quiz.NotePrevented = req.NotePrevented } if req.UniqueAnswers != quiz.UniqueAnswers { quiz.UniqueAnswers = req.UniqueAnswers } if req.Pausable != quiz.Pausable { quiz.Pausable = req.Pausable } if req.Name != "" && req.Name != quiz.Name { quiz.Name = req.Name } if req.Description != "" && req.Description != quiz.Description { quiz.Description = req.Description } if req.Status != "" && req.Status != quiz.Status { quiz.Status = req.Status } if req.TimeOfPassing != quiz.TimeOfPassing { quiz.TimeOfPassing = req.TimeOfPassing } if req.DueTo != quiz.DueTo { quiz.DueTo = req.DueTo } if req.Limit != quiz.Limit { quiz.Limit = req.Limit } if req.Config != "" && req.Config != quiz.Config { quiz.Config = req.Config } if req.Super != quiz.Super { quiz.Super = req.Super } if req.GroupId != quiz.GroupId { quiz.GroupId = req.GroupId } quiz.QuestionsCount = req.QuestionCnt quiz.ParentIds = append(quiz.ParentIds, int32(quiz.Id)) if err := r.dal.QuizRepo.UpdateQuiz(ctx.Context(), accountId, quiz); err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } if req.Status == model.StatusStart { hlogger.Emit(models.InfoQuizPublish{ CtxUserID: accountId, CtxIDInt: int64(quiz.Id), }) } if req.Status == model.StatusStop { hlogger.Emit(models.InfoQuizStop{ CtxUserID: accountId, CtxIDInt: int64(quiz.Id), }) } return ctx.JSON(UpdateResp{ Updated: quiz.Id, }) } // CopyQuizReq request struct for copy quiz type CopyQuizReq struct { Id uint64 `json:"id"` } // CopyQuiz request handler for copy quiz func (r *Quiz) CopyQuiz(ctx *fiber.Ctx) error { var req CopyQuizReq if err := ctx.BodyParser(&req); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data") } accountId, ok := middleware.GetAccountId(ctx) if !ok { return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required") } if req.Id == 0 { return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided") } quiz, err := r.dal.QuizRepo.CopyQuiz(ctx.Context(), accountId, req.Id) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } return ctx.JSON(UpdateResp{ Updated: quiz.Id, }) } // GetQuizHistoryReq struct of get history request type GetQuizHistoryReq struct { Id uint64 `json:"id"` Limit uint64 `json:"l"` Page uint64 `json:"p"` } // GetQuizHistory handler for history of quiz func (r *Quiz) GetQuizHistory(ctx *fiber.Ctx) error { var req GetQuizHistoryReq if err := ctx.BodyParser(&req); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data") } accountId, ok := middleware.GetAccountId(ctx) if !ok { return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required") } if req.Id == 0 { return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided") } history, err := r.dal.QuizRepo.QuizHistory(ctx.Context(), quiz.QuizHistoryDeps{ Id: req.Id, Limit: req.Limit, Offset: req.Page * req.Limit, AccountId: accountId, }) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } return ctx.Status(fiber.StatusCreated).JSON(history) } // DeactivateReq request structure for archiving and deleting type DeactivateReq struct { Id uint64 `json:"id"` } type DeactivateResp struct { Deactivated uint64 `json:"deactivated"` } // DeleteQuiz handler for fake delete quiz func (r *Quiz) DeleteQuiz(ctx *fiber.Ctx) error { var req DeactivateReq if err := ctx.BodyParser(&req); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data") } accountId, ok := middleware.GetAccountId(ctx) if !ok { return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required") } hlogger := log_mw.ExtractLogger(ctx) if req.Id == 0 { return ctx.Status(fiber.StatusFailedDependency).SendString("id for deleting is required") } deleted, err := r.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } hlogger.Emit(models.InfoQuizDelete{ CtxUserID: accountId, CtxIDInt: int64(req.Id), }) return ctx.JSON(DeactivateResp{ Deactivated: deleted.Id, }) } // ArchiveQuiz handler for archiving quiz func (r *Quiz) ArchiveQuiz(ctx *fiber.Ctx) error { var req DeactivateReq if err := ctx.BodyParser(&req); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data") } accountId, ok := middleware.GetAccountId(ctx) if !ok { return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required") } if req.Id == 0 { return ctx.Status(fiber.StatusFailedDependency).SendString("id for archive quiz is required") } archived, err := r.dal.QuizRepo.DeleteQuiz(ctx.Context(), accountId, req.Id) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } return ctx.JSON(DeactivateResp{ Deactivated: archived.Id, }) } type QuizMoveReq struct { Qid, AccountID string } func (r *Quiz) QuizMove(ctx *fiber.Ctx) error { var req QuizMoveReq if err := ctx.BodyParser(&req); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data") } if req.Qid == "" || req.AccountID == "" { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request qid and accountID is required") } resp, err := r.dal.QuizRepo.QuizMove(ctx.Context(), req.Qid, req.AccountID) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } return ctx.Status(fiber.StatusOK).JSON(resp) } func (r *Quiz) TemplateCopy(ctx *fiber.Ctx) error { accountID, ok := middleware.GetAccountId(ctx) if !ok { return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required") } hlogger := log_mw.ExtractLogger(ctx) var req struct { Qid string `json:"qid"` } if err := ctx.BodyParser(&req); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data") } if req.Qid == "" { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request qid is required") } qizID, err := r.dal.QuizRepo.TemplateCopy(ctx.Context(), accountID, req.Qid) if err != nil { fmt.Println("TEMPLERR", err) return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } hlogger.Emit(models.InfoQuizTemplateCopy{ CtxUserID: accountID, // todo либо возвращать id копируемого квиза либо поле с qid // для него потому что id получаем уже в запросе sql //CtxID: req.Qid, CtxQuizID: qizID, }) return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"id": qizID}) } func (s *Quiz) CreateQuizAuditory(ctx *fiber.Ctx) error { var req struct { Sex bool `json:"sex"` // false - female, true - male Age string `json:"age"` } if err := ctx.BodyParser(&req); err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data") } quizIDStr := ctx.Params("quizID") quizID, err := strconv.ParseInt(quizIDStr, 10, 64) if err != nil { return ctx.Status(fiber.StatusBadRequest).SendString("invalid quiz ID") } if quizID == 0 || req.Age == "" { return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request missing required fields") } result, err := s.dal.QuizRepo.CreateQuizAudience(ctx.Context(), quiz.DepsCreateQuizAudience{ QuizID: quizID, Age: req.Age, Sex: req.Sex, }) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } err = s.producerGigaChat.ToGigaChatNotify(ctx.Context(), brokers.MessageGigaChat{ ID: result, QuizID: quizID, Age: req.Age, Sex: req.Sex, }) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } return ctx.Status(fiber.StatusOK).JSON(fiber.Map{"ID": result}) } func (s *Quiz) GetQuizAuditory(ctx *fiber.Ctx) error { quizIDStr := ctx.Params("quizID") quizID, err := strconv.ParseInt(quizIDStr, 10, 64) if err != nil || quizID == 0 { return ctx.Status(fiber.StatusBadRequest).SendString("invalid quiz ID") } accountID, ok := middleware.GetAccountId(ctx) if !ok { return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required") } isOwner, err := s.dal.QuizRepo.CheckQuizOwner(ctx.Context(), accountID, uint64(quizID)) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString("failed to check ownership: " + err.Error()) } if !isOwner { return ctx.Status(fiber.StatusForbidden).SendString("you are not the owner") } result, err := s.dal.QuizRepo.GetQuizAudience(ctx.Context(), quizID) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } return ctx.Status(fiber.StatusOK).JSON(result) } func (s *Quiz) DeleteQuizAuditory(ctx *fiber.Ctx) error { quizIDStr := ctx.Params("quizID") quizID, err := strconv.ParseInt(quizIDStr, 10, 64) if err != nil || quizID == 0 { return ctx.Status(fiber.StatusBadRequest).SendString("invalid quiz ID") } audienceIDStr := ctx.Params("auditoryID") audienceID, err := strconv.ParseInt(audienceIDStr, 10, 64) if err != nil || audienceID == 0 { return ctx.Status(fiber.StatusBadRequest).SendString("invalid audience ID") } accountID, ok := middleware.GetAccountId(ctx) if !ok { return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required") } isOwner, err := s.dal.QuizRepo.CheckIsOwnerAudience(ctx.Context(), quizID, audienceID, accountID) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString("failed to check ownership: " + err.Error()) } if !isOwner { return ctx.Status(fiber.StatusForbidden).SendString("you are not the owner of this quiz audience") } err = s.dal.QuizRepo.DeleteQuizAudience(ctx.Context(), quizID, audienceID) if err != nil { return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error()) } return ctx.SendStatus(fiber.StatusOK) }