feat(tests): UpdateQuestion (full-replace) + ReorderQuestions
UpdateQuestion (PUT /tests/{id}/questions/{questionId}):
- транзакционно: UPDATE test_questions SET ... + DELETE test_answers +
INSERT новых ответов. question_id стабилен — test_attempt_answers
(FK CASCADE) остаются. Снимок ответа в payload — для будущего
показа истории, в MVP не реализовано;
- семантика full-replace: проще на клиенте (один POST вместо
per-answer патчей), атомарно на сервере.
ReorderQuestions (POST /tests/{id}/questions/reorder):
- батч-апдейт position через UNNEST(uuid[], int[]) — один запрос
вместо N UPDATE'ов; идемпотентно;
- /reorder регистрируется ДО /{questionId} чтобы chi не заматчил
его как questionId="reorder".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -227,6 +227,112 @@ func (h *TestHandler) CreateQuestion(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusCreated, q)
|
||||
}
|
||||
|
||||
// UpdateQuestion — PUT /tests/{id}/questions/{questionId}. Full-replace
|
||||
// семантика (см. repository.UpdateQuestion): метаданные обновляются,
|
||||
// ответы пересоздаются. question_id стабилен.
|
||||
func (h *TestHandler) UpdateQuestion(w http.ResponseWriter, r *http.Request) {
|
||||
uid, ok := userIDFromHeader(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
testID, err := parseUUID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid test id")
|
||||
return
|
||||
}
|
||||
qID, err := parseUUID(chi.URLParam(r, "questionId"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid question id")
|
||||
return
|
||||
}
|
||||
t, err := h.repo.Get(r.Context(), testID)
|
||||
if err != nil {
|
||||
writeRepoError(w, r, err, "get test")
|
||||
return
|
||||
}
|
||||
if t.OwnerUserID != uid {
|
||||
writeError(w, http.StatusForbidden, "only owner can edit questions")
|
||||
return
|
||||
}
|
||||
var req model.CreateQuestionRequest
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Text) == "" {
|
||||
writeError(w, http.StatusBadRequest, "text is required")
|
||||
return
|
||||
}
|
||||
if req.Kind != "single" && req.Kind != "multi" && req.Kind != "text" {
|
||||
writeError(w, http.StatusBadRequest, "kind must be single|multi|text")
|
||||
return
|
||||
}
|
||||
if req.Points <= 0 {
|
||||
req.Points = 1
|
||||
}
|
||||
q, err := h.repo.UpdateQuestion(r.Context(), qID, req)
|
||||
if err != nil {
|
||||
writeRepoError(w, r, err, "update question")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, q)
|
||||
}
|
||||
|
||||
// ReorderQuestions — POST /tests/{id}/questions/reorder. Body:
|
||||
// {"items": [{"id": "uuid", "position": 0}, ...]}
|
||||
func (h *TestHandler) ReorderQuestions(w http.ResponseWriter, r *http.Request) {
|
||||
uid, ok := userIDFromHeader(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
testID, err := parseUUID(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid test id")
|
||||
return
|
||||
}
|
||||
t, err := h.repo.Get(r.Context(), testID)
|
||||
if err != nil {
|
||||
writeRepoError(w, r, err, "get test")
|
||||
return
|
||||
}
|
||||
if t.OwnerUserID != uid {
|
||||
writeError(w, http.StatusForbidden, "only owner can reorder")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
Position int `json:"position"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := decodeJSON(r, &req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid body")
|
||||
return
|
||||
}
|
||||
items := make([]struct {
|
||||
ID uuid.UUID
|
||||
Position int
|
||||
}, 0, len(req.Items))
|
||||
for _, it := range req.Items {
|
||||
id, err := parseUUID(it.ID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid item id")
|
||||
return
|
||||
}
|
||||
items = append(items, struct {
|
||||
ID uuid.UUID
|
||||
Position int
|
||||
}{ID: id, Position: it.Position})
|
||||
}
|
||||
if err := h.repo.ReorderQuestions(r.Context(), testID, items); err != nil {
|
||||
writeRepoError(w, r, err, "reorder questions")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *TestHandler) DeleteQuestion(w http.ResponseWriter, r *http.Request) {
|
||||
uid, ok := userIDFromHeader(r)
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user