Files
spsays-bff/internal/handler/quiz.go
T
2026-10-16 15:23:06 +05:30

260 lines
8.7 KiB
Go

package handler
import (
"encoding/json"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/prabhupadasays/spsays-bff/internal/middleware"
"github.com/prabhupadasays/spsays-bff/internal/repository"
"github.com/prabhupadasays/spsays-bff/internal/service"
"github.com/prabhupadasays/spsays-bff/pkg/response"
)
// QuizHandler serves quiz authoring (admin) and quiz taking (users).
type QuizHandler struct {
svc *service.QuizService
}
func NewQuizHandler(svc *service.QuizService) *QuizHandler { return &QuizHandler{svc: svc} }
func writeQuizError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, repository.ErrQuizNotFound), errors.Is(err, service.ErrQuizUnpublished):
response.Error(w, http.StatusNotFound, "quiz not found")
case errors.Is(err, repository.ErrQuizNotStarted), errors.Is(err, repository.ErrQuizNotFinished):
response.Error(w, http.StatusConflict, err.Error())
case errors.Is(err, repository.ErrQuizNotCurrent), errors.Is(err, repository.ErrQuizAlreadyAnswer),
errors.Is(err, service.ErrQuizLocked), errors.Is(err, repository.ErrQuizTargetTaken):
response.Error(w, http.StatusConflict, err.Error())
case errors.Is(err, repository.ErrQuizBadOption), errors.Is(err, repository.ErrQuizChoiceCount),
errors.Is(err, service.ErrQuizNoCorrect),
errors.Is(err, service.ErrQuizTitle), errors.Is(err, service.ErrQuizNoQuestions),
errors.Is(err, service.ErrQuizTooMany), errors.Is(err, service.ErrQuizPrompt),
errors.Is(err, service.ErrQuizOptionCount), errors.Is(err, service.ErrQuizOptionBody),
errors.Is(err, service.ErrQuizOneCorrect), errors.Is(err, service.ErrQuizTarget):
response.Error(w, http.StatusBadRequest, err.Error())
default:
response.Error(w, http.StatusInternalServerError, "quiz request failed")
}
}
// ── Admin ─────────────────────────────────────────────────────────────────
type quizOptionReq struct {
Body string `json:"body"`
IsCorrect bool `json:"is_correct"`
}
type quizQuestionReq struct {
Prompt string `json:"prompt"`
Explanation string `json:"explanation"`
Multi bool `json:"multi"`
Options []quizOptionReq `json:"options"`
}
type quizReq struct {
Title string `json:"title"`
Description string `json:"description"`
TargetURL string `json:"target_url"`
TargetTitle string `json:"target_title"`
Questions []quizQuestionReq `json:"questions"`
}
func (q quizReq) draft() service.QuizDraft {
d := service.QuizDraft{
Title: q.Title, Description: q.Description,
TargetURL: q.TargetURL, TargetTitle: q.TargetTitle,
}
for _, qq := range q.Questions {
in := repository.QuestionInput{Prompt: qq.Prompt, Explanation: qq.Explanation, Multi: qq.Multi}
for _, o := range qq.Options {
in.Options = append(in.Options, repository.OptionInput{Body: o.Body, IsCorrect: o.IsCorrect})
}
d.Questions = append(d.Questions, in)
}
return d
}
func (h *QuizHandler) AdminList(w http.ResponseWriter, r *http.Request) {
list, err := h.svc.ListAdmin(r.Context())
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, list)
}
func (h *QuizHandler) AdminGet(w http.ResponseWriter, r *http.Request) {
q, err := h.svc.GetAdmin(r.Context(), chi.URLParam(r, "id"))
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, q)
}
func (h *QuizHandler) Create(w http.ResponseWriter, r *http.Request) {
var req quizReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid JSON body")
return
}
q, err := h.svc.Create(r.Context(), middleware.UserID(r.Context()), req.draft())
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusCreated, q)
}
func (h *QuizHandler) Update(w http.ResponseWriter, r *http.Request) {
var req quizReq
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
response.Error(w, http.StatusBadRequest, "invalid JSON body")
return
}
q, err := h.svc.Update(r.Context(), chi.URLParam(r, "id"), req.draft())
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, q)
}
func (h *QuizHandler) setPublished(w http.ResponseWriter, r *http.Request, on bool) {
q, err := h.svc.SetPublished(r.Context(), chi.URLParam(r, "id"), on)
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, q)
}
func (h *QuizHandler) Publish(w http.ResponseWriter, r *http.Request) { h.setPublished(w, r, true) }
func (h *QuizHandler) Unpublish(w http.ResponseWriter, r *http.Request) { h.setPublished(w, r, false) }
func (h *QuizHandler) Delete(w http.ResponseWriter, r *http.Request) {
if err := h.svc.Delete(r.Context(), chi.URLParam(r, "id")); err != nil {
writeQuizError(w, err)
return
}
response.NoContent(w)
}
func (h *QuizHandler) Participants(w http.ResponseWriter, r *http.Request) {
list, err := h.svc.Participants(r.Context(), chi.URLParam(r, "id"))
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, list)
}
func (h *QuizHandler) Responses(w http.ResponseWriter, r *http.Request) {
stats, err := h.svc.Responses(r.Context(), chi.URLParam(r, "id"))
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, stats)
}
func (h *QuizHandler) ParticipantResult(w http.ResponseWriter, r *http.Request) {
res, err := h.svc.ParticipantResult(r.Context(), chi.URLParam(r, "id"), chi.URLParam(r, "userID"))
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, res)
}
// ResetParticipant — DELETE /admin/quizzes/{id}/participants/{userID}. Deletes
// that person's attempt so the quiz is pending for them again.
func (h *QuizHandler) ResetParticipant(w http.ResponseWriter, r *http.Request) {
if err := h.svc.ResetParticipant(r.Context(), chi.URLParam(r, "id"), chi.URLParam(r, "userID")); err != nil {
writeQuizError(w, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
// ── Users ─────────────────────────────────────────────────────────────────
// ForPath — GET /quizzes/for?path=… Signed in only; returns the quiz on that
// page with the caller's progress, or null when the page has none.
func (h *QuizHandler) ForPath(w http.ResponseWriter, r *http.Request) {
card, err := h.svc.ForPath(r.Context(), middleware.UserID(r.Context()), r.URL.Query().Get("path"))
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, card)
}
func (h *QuizHandler) List(w http.ResponseWriter, r *http.Request) {
list, err := h.svc.ListPublished(r.Context(), middleware.UserID(r.Context()))
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, list)
}
func (h *QuizHandler) Get(w http.ResponseWriter, r *http.Request) {
card, err := h.svc.Card(r.Context(), middleware.UserID(r.Context()), chi.URLParam(r, "id"))
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, card)
}
func (h *QuizHandler) Start(w http.ResponseWriter, r *http.Request) {
play, err := h.svc.Start(r.Context(), middleware.UserID(r.Context()), chi.URLParam(r, "id"))
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, play)
}
func (h *QuizHandler) Answer(w http.ResponseWriter, r *http.Request) {
var req struct {
QuestionID string `json:"question_id"`
OptionIDs []string `json:"option_ids"`
// Single-option form, still accepted from older clients.
OptionID string `json:"option_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.QuestionID == "" {
response.Error(w, http.StatusBadRequest, "question_id and option_ids are required")
return
}
if len(req.OptionIDs) == 0 && req.OptionID != "" {
req.OptionIDs = []string{req.OptionID}
}
if len(req.OptionIDs) == 0 {
response.Error(w, http.StatusBadRequest, "question_id and option_ids are required")
return
}
play, err := h.svc.Answer(r.Context(), middleware.UserID(r.Context()),
chi.URLParam(r, "id"), req.QuestionID, req.OptionIDs)
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, play)
}
// Result — GET /quizzes/{id}/result. The caller's own; the user id is taken
// from the session, so there is no way to ask for anyone else's.
func (h *QuizHandler) Result(w http.ResponseWriter, r *http.Request) {
res, err := h.svc.Result(r.Context(), middleware.UserID(r.Context()), chi.URLParam(r, "id"))
if err != nil {
writeQuizError(w, err)
return
}
response.JSON(w, http.StatusOK, res)
}