core/service/quiz_svc.go

523 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"github.com/gofiber/fiber/v2"
"penahub.gitlab.yandexcloud.net/backend/penahub_common/log_mw"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/middleware"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/model"
"penahub.gitlab.yandexcloud.net/backend/quiz/common.git/repository/quiz"
"penahub.gitlab.yandexcloud.net/backend/quiz/core/models"
"time"
"unicode/utf8"
"fmt"
)
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 (s *Service) 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.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 := s.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 (s *Service) 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.StatusTimeout &&
req.Status != model.StatusOffLimit {
return ctx.Status(fiber.StatusNotAcceptable).SendString("inappropriate status, allowed only '', " +
"'stop','start','draft', 'template','timeout','offlimit'")
}
res, cnt, err := s.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"`
}
func (s *Service) 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.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 := s.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 := s.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 (s *Service) 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 := s.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 (s *Service) 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 := s.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 (s *Service) 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 := s.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 (s *Service) 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 := s.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 (s *Service) 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 := s.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 (s *Service) 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 := s.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})
}