feat: add form response submition endpoint and forms filtering
Docker Build and Push / Build and Push Docker Image (push) Successful in 13m18s
Docker Build and Push / Build and Push Docker Image (push) Successful in 13m18s
This commit is contained in:
@@ -9,6 +9,8 @@ import (
|
||||
"net/http"
|
||||
"ristek-task-be/internal/db/sqlc/repository"
|
||||
"ristek-task-be/internal/middleware"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -142,6 +144,22 @@ func (h *Handler) FormsGet(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
q := r.URL.Query()
|
||||
search := strings.TrimSpace(q.Get("search"))
|
||||
status := strings.ToLower(strings.TrimSpace(q.Get("status")))
|
||||
sortBy := strings.ToLower(strings.TrimSpace(q.Get("sort_by")))
|
||||
sortDir := strings.ToLower(strings.TrimSpace(q.Get("sort_dir")))
|
||||
|
||||
if status != "has_responses" && status != "no_responses" {
|
||||
status = ""
|
||||
}
|
||||
if sortBy != "updated_at" {
|
||||
sortBy = "created_at"
|
||||
}
|
||||
if sortDir != "oldest" {
|
||||
sortDir = "newest"
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -163,6 +181,16 @@ func (h *Handler) FormsGet(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
items := make([]listItem, 0, len(forms))
|
||||
for _, f := range forms {
|
||||
if search != "" && !strings.Contains(strings.ToLower(f.Title), strings.ToLower(search)) {
|
||||
continue
|
||||
}
|
||||
if status == "has_responses" && f.ResponseCount == 0 {
|
||||
continue
|
||||
}
|
||||
if status == "no_responses" && f.ResponseCount > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
desc := ""
|
||||
if f.Description.Valid {
|
||||
desc = f.Description.String
|
||||
@@ -177,6 +205,19 @@ func (h *Handler) FormsGet(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
var ti, tj time.Time
|
||||
if sortBy == "updated_at" {
|
||||
ti, tj = items[i].UpdatedAt, items[j].UpdatedAt
|
||||
} else {
|
||||
ti, tj = items[i].CreatedAt, items[j].CreatedAt
|
||||
}
|
||||
if sortDir == "oldest" {
|
||||
return ti.Before(tj)
|
||||
}
|
||||
return ti.After(tj)
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(items)
|
||||
@@ -379,6 +420,18 @@ func (h *Handler) FormDelete(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
hasResponses, err := h.repository.FormHasResponses(ctx, formID)
|
||||
if err != nil {
|
||||
internalServerError(w, err)
|
||||
log.Printf("failed to check responses: %s", err)
|
||||
return
|
||||
}
|
||||
if hasResponses {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
_, _ = w.Write([]byte("form already has responses and cannot be deleted"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repository.DeleteForm(ctx, formID); err != nil {
|
||||
internalServerError(w, err)
|
||||
log.Printf("failed to delete form: %s", err)
|
||||
@@ -433,3 +486,213 @@ func (h *Handler) saveQuestions(ctx context.Context, formID uuid.UUID, inputs []
|
||||
|
||||
return questions, options, nil
|
||||
}
|
||||
|
||||
type AnswerInput struct {
|
||||
QuestionID string `json:"question_id"`
|
||||
Answer string `json:"answer"`
|
||||
}
|
||||
|
||||
type SubmitResponseRequest struct {
|
||||
Answers []AnswerInput `json:"answers"`
|
||||
}
|
||||
|
||||
type AnswerResponse struct {
|
||||
QuestionID string `json:"question_id"`
|
||||
Answer string `json:"answer"`
|
||||
}
|
||||
|
||||
type SubmitResponseResponse struct {
|
||||
ID string `json:"id"`
|
||||
FormID string `json:"form_id"`
|
||||
SubmittedAt string `json:"submitted_at"`
|
||||
Answers []AnswerResponse `json:"answers"`
|
||||
}
|
||||
|
||||
func (h *Handler) FormResponsesPost(w http.ResponseWriter, r *http.Request) {
|
||||
formID, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
badRequest(w, errors.New("invalid form id"))
|
||||
return
|
||||
}
|
||||
|
||||
var req SubmitResponseRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
badRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Answers) == 0 {
|
||||
badRequest(w, errors.New("answers are required"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
form, err := h.repository.GetFormByID(ctx, formID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte("form not found"))
|
||||
return
|
||||
}
|
||||
internalServerError(w, err)
|
||||
log.Printf("failed to get form: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
questions, err := h.repository.GetQuestionsByFormID(ctx, formID)
|
||||
if err != nil {
|
||||
internalServerError(w, err)
|
||||
log.Printf("failed to get questions: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
questionMap := make(map[uuid.UUID]repository.Question, len(questions))
|
||||
for _, q := range questions {
|
||||
questionMap[q.ID] = q
|
||||
}
|
||||
|
||||
answerMap := make(map[uuid.UUID]string, len(req.Answers))
|
||||
for _, a := range req.Answers {
|
||||
qid, err := uuid.Parse(a.QuestionID)
|
||||
if err != nil {
|
||||
badRequest(w, errors.New("invalid question_id: "+a.QuestionID))
|
||||
return
|
||||
}
|
||||
if _, exists := questionMap[qid]; !exists {
|
||||
badRequest(w, errors.New("question not found in form: "+a.QuestionID))
|
||||
return
|
||||
}
|
||||
answerMap[qid] = a.Answer
|
||||
}
|
||||
|
||||
for _, q := range questions {
|
||||
if q.Required {
|
||||
if ans, ok := answerMap[q.ID]; !ok || ans == "" {
|
||||
badRequest(w, errors.New("required question not answered: "+q.Title))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var userID *uuid.UUID
|
||||
if uid, ok := h.currentUserID(r); ok {
|
||||
userID = &uid
|
||||
}
|
||||
|
||||
formResp, err := h.repository.CreateFormResponse(ctx, repository.CreateFormResponseParams{
|
||||
FormID: form.ID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
internalServerError(w, err)
|
||||
log.Printf("failed to create form response: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.repository.IncrementResponseCount(ctx, formID); err != nil {
|
||||
log.Printf("failed to increment response count: %s", err)
|
||||
}
|
||||
|
||||
var answerResponses []AnswerResponse
|
||||
for qid, answerText := range answerMap {
|
||||
_, err := h.repository.CreateResponseAnswer(ctx, repository.CreateResponseAnswerParams{
|
||||
ResponseID: formResp.ID,
|
||||
QuestionID: qid,
|
||||
FormID: formID,
|
||||
AnswerText: pgtype.Text{String: answerText, Valid: answerText != ""},
|
||||
})
|
||||
if err != nil {
|
||||
internalServerError(w, err)
|
||||
log.Printf("failed to create response answer: %s", err)
|
||||
return
|
||||
}
|
||||
answerResponses = append(answerResponses, AnswerResponse{
|
||||
QuestionID: qid.String(),
|
||||
Answer: answerText,
|
||||
})
|
||||
}
|
||||
if answerResponses == nil {
|
||||
answerResponses = []AnswerResponse{}
|
||||
}
|
||||
|
||||
submittedAt := ""
|
||||
if formResp.SubmittedAt.Valid {
|
||||
submittedAt = formResp.SubmittedAt.Time.String()
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(SubmitResponseResponse{
|
||||
ID: formResp.ID.String(),
|
||||
FormID: formResp.FormID.String(),
|
||||
SubmittedAt: submittedAt,
|
||||
Answers: answerResponses,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) FormResponsesGet(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := h.currentUserID(r)
|
||||
if !ok {
|
||||
unauthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
formID, err := uuid.Parse(r.PathValue("id"))
|
||||
if err != nil {
|
||||
badRequest(w, errors.New("invalid form id"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
form, err := h.repository.GetFormByID(ctx, formID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if form.UserID != userID {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
responses, err := h.repository.GetFormResponsesByFormID(ctx, formID)
|
||||
if err != nil {
|
||||
internalServerError(w, err)
|
||||
log.Printf("failed to get responses: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
type responseItem struct {
|
||||
ID string `json:"id"`
|
||||
SubmittedAt string `json:"submitted_at"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
}
|
||||
items := make([]responseItem, 0, len(responses))
|
||||
for _, resp := range responses {
|
||||
uid := ""
|
||||
if resp.UserID != nil {
|
||||
uid = resp.UserID.String()
|
||||
}
|
||||
sat := ""
|
||||
if resp.SubmittedAt.Valid {
|
||||
sat = resp.SubmittedAt.Time.String()
|
||||
}
|
||||
items = append(items, responseItem{
|
||||
ID: resp.ID.String(),
|
||||
SubmittedAt: sat,
|
||||
UserID: uid,
|
||||
})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_ = json.NewEncoder(w).Encode(items)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user