260 lines
8.7 KiB
Go
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)
|
|
}
|