423 lines
13 KiB
Go
423 lines
13 KiB
Go
package question
|
||
|
||
import (
|
||
"gitea.pena/PenaSide/common/log_mw"
|
||
"gitea.pena/SQuiz/common/dal"
|
||
"gitea.pena/SQuiz/common/middleware"
|
||
"gitea.pena/SQuiz/common/model"
|
||
"gitea.pena/SQuiz/core/internal/models"
|
||
"github.com/gofiber/fiber/v2"
|
||
"github.com/lib/pq"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
type Deps struct {
|
||
DAL *dal.DAL
|
||
}
|
||
|
||
type Question struct {
|
||
dal *dal.DAL
|
||
}
|
||
|
||
func NewQuestionController(deps Deps) *Question {
|
||
return &Question{dal: deps.DAL}
|
||
}
|
||
|
||
// QuestionCreateReq request structure for creating Question
|
||
type QuestionCreateReq struct {
|
||
QuizId uint64 `json:"quiz_id"` // relation to quiz
|
||
|
||
Title string `json:"title"` // title of question
|
||
Description string `json:"description"` // additional content in question such as pics, html markup or plain text
|
||
Type string `json:"type"` // button/select/file/checkbox/text
|
||
Required bool `json:"required"` // set true if question must be answered for valid quiz passing
|
||
Page int `json:"page"` // set page of question
|
||
Content string `json:"content"` // json serialized config of question
|
||
}
|
||
|
||
// CreateQuestion service handler for creating question for quiz
|
||
// todo нужна проверка на то что квиз принадлежит пользователю, не помешает
|
||
func (r *Question) CreateQuestion(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 QuestionCreateReq
|
||
if err := ctx.BodyParser(&req); err != nil {
|
||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||
}
|
||
|
||
isOwner, err := r.dal.QuizRepo.CheckQuizOwner(ctx.Context(), accountID, req.QuizId)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
if !isOwner {
|
||
return ctx.Status(fiber.StatusUnauthorized).SendString("not the owner")
|
||
}
|
||
|
||
if utf8.RuneCountInString(req.Title) >= 512 {
|
||
return ctx.Status(fiber.StatusUnprocessableEntity).SendString("title field should have less then 512 chars")
|
||
}
|
||
|
||
if req.Type != model.TypeText &&
|
||
req.Type != model.TypeVariant &&
|
||
req.Type != model.TypeImages &&
|
||
req.Type != model.TypeSelect &&
|
||
req.Type != model.TypeVarImages &&
|
||
req.Type != model.TypeEmoji &&
|
||
req.Type != model.TypeDate &&
|
||
req.Type != model.TypeNumber &&
|
||
req.Type != model.TypePage &&
|
||
req.Type != model.TypeRating &&
|
||
req.Type != model.TypeResult &&
|
||
req.Type != model.TypeFile {
|
||
return ctx.Status(fiber.StatusNotAcceptable).SendString("type must be only test,button,file,checkbox,select, none")
|
||
}
|
||
|
||
result := model.Question{
|
||
QuizId: req.QuizId,
|
||
Title: req.Title,
|
||
Description: req.Description,
|
||
Type: req.Type,
|
||
Required: req.Required,
|
||
Deleted: false,
|
||
Page: req.Page,
|
||
Content: req.Content,
|
||
}
|
||
|
||
questionID, err := r.dal.QuestionRepo.CreateQuestion(ctx.Context(), &result)
|
||
if err != nil {
|
||
if e, ok := err.(*pq.Error); ok {
|
||
if e.Constraint == "quiz_relation" {
|
||
return ctx.Status(fiber.StatusFailedDependency).SendString(e.Error())
|
||
}
|
||
}
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
|
||
hlogger.Emit(models.InfoQuestionCreate{
|
||
CtxUserID: accountID,
|
||
CtxIDInt: int64(req.QuizId),
|
||
CtxQuestionID: int64(questionID),
|
||
})
|
||
|
||
return ctx.Status(fiber.StatusOK).JSON(result)
|
||
}
|
||
|
||
// GetQuestionListReq request structure for get question page
|
||
type GetQuestionListReq struct {
|
||
Limit uint64 `json:"limit"` // page size
|
||
Page uint64 `json:"page"` // page number
|
||
From int64 `json:"from"` // start of time period
|
||
To int64 `json:"to"` // end of time period
|
||
QuizId uint64 `json:"quiz_id"` // relation to quiz
|
||
|
||
Search string `json:"search"` // search string to search in files
|
||
Type string `json:"type"` // type of questions. check types in model
|
||
Deleted bool `json:"deleted"` // true to get only deleted questions
|
||
Required bool `json:"required"`
|
||
}
|
||
|
||
// GetQuestionListResp response to get page questions with count of all filtered items
|
||
type GetQuestionListResp struct {
|
||
Count uint64 `json:"count"`
|
||
Items []model.Question `json:"items"`
|
||
}
|
||
|
||
// GetQuestionList handler for paginated list question
|
||
// todo нужна проверка на то что квиз принадлежит пользователю, не помешает
|
||
func (r *Question) GetQuestionList(ctx *fiber.Ctx) error {
|
||
accountID, ok := middleware.GetAccountId(ctx)
|
||
if !ok {
|
||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||
}
|
||
|
||
var req GetQuestionListReq
|
||
if err := ctx.BodyParser(&req); err != nil {
|
||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||
}
|
||
|
||
if req.Type != "" &&
|
||
req.Type != model.TypeText &&
|
||
req.Type != model.TypeVariant &&
|
||
req.Type != model.TypeImages &&
|
||
req.Type != model.TypeSelect &&
|
||
req.Type != model.TypeVarImages &&
|
||
req.Type != model.TypeEmoji &&
|
||
req.Type != model.TypeDate &&
|
||
req.Type != model.TypeNumber &&
|
||
req.Type != model.TypePage &&
|
||
req.Type != model.TypeRating &&
|
||
req.Type != model.TypeResult &&
|
||
req.Type != model.TypeFile {
|
||
return ctx.Status(fiber.StatusNotAcceptable).SendString("inappropriate type, allowed only '', " +
|
||
"'test','none','file', 'button','select','checkbox'")
|
||
}
|
||
|
||
isOwner, err := r.dal.QuizRepo.CheckQuizOwner(ctx.Context(), accountID, req.QuizId)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
|
||
if !isOwner {
|
||
return ctx.Status(fiber.StatusUnauthorized).SendString("not the owner")
|
||
}
|
||
|
||
res, cnt, err := r.dal.QuestionRepo.GetQuestionList(ctx.Context(),
|
||
req.Limit,
|
||
req.Page*req.Limit,
|
||
uint64(req.From),
|
||
uint64(req.To),
|
||
req.QuizId,
|
||
req.Deleted,
|
||
req.Required,
|
||
req.Search,
|
||
req.Type,
|
||
)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
|
||
return ctx.JSON(GetQuestionListResp{
|
||
Items: res,
|
||
Count: cnt,
|
||
})
|
||
}
|
||
|
||
// UpdateQuestionReq struct for request to update question
|
||
type UpdateQuestionReq struct {
|
||
Id uint64 `json:"id"`
|
||
Title string `json:"title"`
|
||
Description string `json:"desc"`
|
||
Type string `json:"type"`
|
||
Required bool `json:"required"`
|
||
Content string `json:"content"`
|
||
Page int `json:"page"`
|
||
}
|
||
|
||
// UpdateResp id you change question that you need only new question id
|
||
type UpdateResp struct {
|
||
Updated uint64 `json:"updated"`
|
||
}
|
||
|
||
// UpdateQuestion handler for update question
|
||
// todo нужна проверка на то что квиз принадлежит пользователю, не помешает
|
||
func (r *Question) UpdateQuestion(ctx *fiber.Ctx) error {
|
||
accountID, ok := middleware.GetAccountId(ctx)
|
||
if !ok {
|
||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||
}
|
||
|
||
var req UpdateQuestionReq
|
||
if err := ctx.BodyParser(&req); err != nil {
|
||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||
}
|
||
|
||
if req.Id == 0 {
|
||
return ctx.Status(fiber.StatusFailedDependency).SendString("need id of question for update")
|
||
}
|
||
|
||
isOwner, err := r.dal.QuestionRepo.CheckQuestionOwner(ctx.Context(), accountID, req.Id)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
|
||
if !isOwner {
|
||
return ctx.Status(fiber.StatusUnauthorized).SendString("not the owner")
|
||
}
|
||
|
||
if utf8.RuneCountInString(req.Title) >= 512 {
|
||
return ctx.Status(fiber.StatusUnprocessableEntity).SendString("title field should have less then 512 chars")
|
||
}
|
||
|
||
if req.Type != model.TypeText &&
|
||
req.Type != model.TypeVariant &&
|
||
req.Type != model.TypeImages &&
|
||
req.Type != model.TypeSelect &&
|
||
req.Type != model.TypeVarImages &&
|
||
req.Type != model.TypeEmoji &&
|
||
req.Type != model.TypeDate &&
|
||
req.Type != model.TypeNumber &&
|
||
req.Type != model.TypePage &&
|
||
req.Type != model.TypeRating &&
|
||
req.Type != model.TypeFile &&
|
||
req.Type != model.TypeResult &&
|
||
req.Type != "" {
|
||
return ctx.Status(fiber.StatusNotAcceptable).SendString("type must be only test,button,file,checkbox,select, none or empty string")
|
||
}
|
||
|
||
question, err := r.dal.QuestionRepo.MoveToHistoryQuestion(ctx.Context(), req.Id)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
question.ParentIds = append(question.ParentIds, int32(question.Id))
|
||
question.Id = req.Id
|
||
question.Version += 1
|
||
|
||
if req.Title != "" {
|
||
question.Title = req.Title
|
||
}
|
||
|
||
if req.Description != "" {
|
||
question.Description = req.Description
|
||
}
|
||
|
||
if req.Page != 0 {
|
||
question.Page = req.Page
|
||
}
|
||
|
||
if req.Type != "" {
|
||
question.Type = req.Type
|
||
}
|
||
|
||
if req.Required != question.Required {
|
||
question.Required = req.Required
|
||
}
|
||
|
||
if req.Content != question.Content {
|
||
question.Content = req.Content
|
||
}
|
||
|
||
if err := r.dal.QuestionRepo.UpdateQuestion(ctx.Context(), question); err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
|
||
return ctx.JSON(UpdateResp{
|
||
Updated: question.Id,
|
||
})
|
||
}
|
||
|
||
// CopyQuestionReq request struct for copy or duplicate question
|
||
type CopyQuestionReq struct {
|
||
Id uint64 `json:"id"`
|
||
QuizId uint64 `json:"quiz_id"`
|
||
}
|
||
|
||
// CopyQuestion handler for copy question
|
||
// todo копирование может происходить с чужого опроса? если нет тоже проверку надо делать на принадлежность
|
||
func (r *Question) CopyQuestion(ctx *fiber.Ctx) error {
|
||
accountID, ok := middleware.GetAccountId(ctx)
|
||
if !ok {
|
||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||
}
|
||
|
||
var req CopyQuestionReq
|
||
if err := ctx.BodyParser(&req); err != nil {
|
||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||
}
|
||
|
||
if req.Id == 0 {
|
||
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
|
||
}
|
||
|
||
isOwner, err := r.dal.QuestionRepo.CheckQuestionOwner(ctx.Context(), accountID, req.Id)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
|
||
if !isOwner {
|
||
return ctx.Status(fiber.StatusUnauthorized).SendString("not the owner")
|
||
}
|
||
|
||
question, err := r.dal.QuestionRepo.CopyQuestion(ctx.Context(), req.Id, req.QuizId)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
|
||
return ctx.JSON(UpdateResp{
|
||
Updated: question.Id,
|
||
})
|
||
}
|
||
|
||
// GetQuestionHistoryReq struct of get history request
|
||
type GetQuestionHistoryReq struct {
|
||
Id uint64 `json:"id"`
|
||
Limit uint64 `json:"l"`
|
||
Page uint64 `json:"p"`
|
||
}
|
||
|
||
// GetQuestionHistory handler for history of quiz
|
||
// todo нужна проверка на то что квиз принадлежит пользователю, не помешает
|
||
func (r *Question) GetQuestionHistory(ctx *fiber.Ctx) error {
|
||
accountID, ok := middleware.GetAccountId(ctx)
|
||
if !ok {
|
||
return ctx.Status(fiber.StatusUnauthorized).SendString("account id is required")
|
||
}
|
||
|
||
var req GetQuestionHistoryReq
|
||
if err := ctx.BodyParser(&req); err != nil {
|
||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||
}
|
||
|
||
if req.Id == 0 {
|
||
return ctx.Status(fiber.StatusFailedDependency).SendString("no id provided")
|
||
}
|
||
|
||
isOwner, err := r.dal.QuestionRepo.CheckQuestionOwner(ctx.Context(), accountID, req.Id)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
|
||
if !isOwner {
|
||
return ctx.Status(fiber.StatusUnauthorized).SendString("not the owner")
|
||
}
|
||
|
||
history, err := r.dal.QuestionRepo.QuestionHistory(ctx.Context(), req.Id, req.Limit, req.Page*req.Limit)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
|
||
return ctx.Status(fiber.StatusOK).JSON(history)
|
||
}
|
||
|
||
type DeactivateResp struct {
|
||
Deactivated uint64 `json:"deactivated"`
|
||
}
|
||
|
||
// DeleteQuestion handler for fake delete question
|
||
// todo нужна проверка на то что квиз принадлежит пользователю, не помешает
|
||
func (r *Question) DeleteQuestion(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 {
|
||
Id uint64 `json:"id"`
|
||
}
|
||
|
||
if err := ctx.BodyParser(&req); err != nil {
|
||
return ctx.Status(fiber.StatusBadRequest).SendString("Invalid request data")
|
||
}
|
||
|
||
if req.Id == 0 {
|
||
return ctx.Status(fiber.StatusFailedDependency).SendString("id for deleting question is required")
|
||
}
|
||
|
||
isOwner, err := r.dal.QuestionRepo.CheckQuestionOwner(ctx.Context(), accountID, req.Id)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
if !isOwner {
|
||
return ctx.Status(fiber.StatusUnauthorized).SendString("not the owner")
|
||
}
|
||
|
||
deleted, err := r.dal.QuestionRepo.DeleteQuestion(ctx.Context(), req.Id)
|
||
if err != nil {
|
||
return ctx.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||
}
|
||
|
||
hlogger.Emit(models.InfoQuestionDelete{
|
||
CtxUserID: accountID,
|
||
CtxIDInt: int64(deleted.QuizId),
|
||
CtxQuestionID: int64(deleted.Id),
|
||
})
|
||
|
||
return ctx.JSON(DeactivateResp{
|
||
Deactivated: deleted.Id,
|
||
})
|
||
}
|