frm/handlers/collector.go

185 lines
5.6 KiB
Go
Raw Normal View History

2025-01-10 18:52:12 +00:00
package handlers
import (
2025-01-22 18:20:05 +00:00
"errors"
2025-01-10 18:52:12 +00:00
"log/slog"
"maps"
"net/http"
"net/url"
"slices"
"github.com/acaloiaro/frm"
"github.com/acaloiaro/frm/internal"
"github.com/acaloiaro/frm/types"
"github.com/acaloiaro/frm/ui"
2025-01-28 15:43:28 +00:00
"github.com/acaloiaro/frm/ui/collector"
2025-01-11 00:11:33 +00:00
"github.com/google/uuid"
2025-01-22 18:20:05 +00:00
"github.com/jackc/pgx/v5"
2025-01-10 18:52:12 +00:00
)
2025-01-14 16:46:22 +00:00
// ShortCode handles requsts for form short codes and renders the corresponding form
//
// When Forms are submitted via short URL, submissions are attributed to the subject with which the short code was
// generated.
2025-01-18 16:09:16 +00:00
func ShortCode(w http.ResponseWriter, r *http.Request) {
2025-01-14 16:46:22 +00:00
ctx := r.Context()
i, err := frm.Instance(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
2025-01-18 16:09:16 +00:00
formID, err := formID(ctx, i)
2025-01-14 16:46:22 +00:00
if err != nil {
2025-01-18 16:09:16 +00:00
slog.Error("unable to view form", "error", err)
2025-01-14 16:46:22 +00:00
w.WriteHeader(http.StatusNotFound)
2025-01-10 18:52:12 +00:00
return
}
f, err := internal.Q(ctx, i.DBArgs).GetForm(ctx, internal.GetFormParams{
WorkspaceID: i.WorkspaceID,
ID: *formID,
})
if err != nil {
2025-01-18 16:09:16 +00:00
slog.Error("unable to view form", "error", err)
2025-01-10 18:52:12 +00:00
w.WriteHeader(http.StatusNotFound)
return
}
2025-02-13 22:15:36 +00:00
var ok bool
shortCode, ok := ctx.Value(internal.ShortCodeContextKey).(*string)
if !ok {
slog.Error("no short code provided")
w.WriteHeader(http.StatusNotFound)
return
}
2025-01-10 18:52:12 +00:00
// Render the form collector
2025-02-13 22:15:36 +00:00
err = collector.Viewer(collector.ViewerArgs{Form: (frm.Form)(f), ShortCode: *shortCode}).Render(ctx, w)
2025-01-10 18:52:12 +00:00
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
// Collect handles collector form submissions
func Collect(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
i, err := frm.Instance(ctx)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
2025-01-18 16:09:16 +00:00
formID, err := formID(ctx, i)
2025-01-23 14:31:12 +00:00
if err != nil || formID == nil {
2025-01-10 18:52:12 +00:00
w.WriteHeader(http.StatusNotFound)
return
}
f, err := internal.Q(ctx, i.DBArgs).GetForm(ctx, internal.GetFormParams{
WorkspaceID: i.WorkspaceID,
ID: *formID,
})
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
err = r.ParseForm()
if err != nil {
slog.Error("[collector] unable to parse form", "error", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
2025-01-11 00:11:33 +00:00
submission := r.Form
errs := validate(f, submission)
2025-01-10 18:52:12 +00:00
if errs.Any() {
2025-02-11 15:31:12 +00:00
slog.Debug("[collector] failed validation", "errors", errs)
2025-01-10 18:52:12 +00:00
w.WriteHeader(http.StatusBadRequest)
2025-01-30 19:04:48 +00:00
err = ui.ValidationErrors(errs).Render(r.Context(), w)
if err != nil {
slog.Error("[collector] unable to render validation error response", "errors", errs)
w.WriteHeader(http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusBadRequest)
}
return
2025-01-10 18:52:12 +00:00
}
2025-01-22 18:20:05 +00:00
sc := submission.Get("short_code")
submission.Del("short_code")
arg := internal.GetShortCodeParams{
WorkspaceID: i.WorkspaceID,
ShortCode: sc,
}
// Submissions without short codes are anonymous, and valid
shortCode, err := internal.Q(ctx, i.DBArgs).GetShortCode(ctx, arg)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
slog.Info("[collector] unable to find provided short code for workspace", "errors", err, "params", arg)
w.WriteHeader(http.StatusInternalServerError)
} else if errors.Is(err, pgx.ErrNoRows) {
slog.Info("[collector] short code not found", "params", arg)
}
// TODO: Keep track of the submission id
// submissionID := r.Form.Get("id")
2025-01-10 18:52:12 +00:00
2025-02-11 15:31:12 +00:00
// Validation renders whether there are errors or not, so that non-erroneous fields can be cleared of error messages
2025-01-11 00:11:33 +00:00
// as the user corrects validation errors
2025-01-10 18:52:12 +00:00
allFields := slices.Collect(maps.Keys(f.Fields))
err = ui.Validation(allFields, errs).Render(ctx, w)
if err != nil {
slog.Error("[collector] error while reporting validation error", "error", err)
return
}
2025-01-22 18:20:05 +00:00
// submitted forms only have a submission id when they've been previously submitted and the subject has re-submitted
if submission.Has("submission_id") {
// TODO do something with the submission id
submission.Del("submission_id")
}
2025-01-11 00:11:33 +00:00
formFieldValues := types.FormFieldValues{}
for fieldID, fieldValue := range submission {
formFieldValues[fieldID] = types.FormFieldSubmission{
ID: uuid.New(),
FormFieldID: uuid.MustParse(fieldID), // TODO do not use MustParse
Order: f.Fields[fieldID].Order,
Required: f.Fields[fieldID].Required,
Hidden: f.Fields[fieldID].Hidden,
Type: f.Fields[fieldID].Type,
DataType: f.Fields[fieldID].DataType,
Value: fieldValue,
}
}
2025-01-23 14:31:12 +00:00
var s internal.FormSubmission
s, err = internal.Q(ctx, i.DBArgs).SaveSubmission(ctx, internal.SaveSubmissionParams{
2025-01-22 18:20:05 +00:00
// ID: submissionID, TODO save submission id
2025-01-23 14:31:12 +00:00
FormID: *formID,
2025-01-11 00:11:33 +00:00
WorkspaceID: i.WorkspaceID,
2025-01-22 18:20:05 +00:00
SubjectID: &shortCode.SubjectID,
2025-01-11 00:11:33 +00:00
Status: internal.SubmissionStatusPartial,
Fields: formFieldValues,
})
if err != nil {
2025-01-22 18:20:05 +00:00
slog.Error("[collector] unable to save submission", "error", err)
2025-01-11 00:11:33 +00:00
w.WriteHeader(http.StatusInternalServerError)
return
}
2025-01-23 14:31:12 +00:00
if i.Receiver != nil {
err = i.Receiver(ctx, (frm.FormSubmission)(s))
if err != nil {
slog.Error("[collector] unable to execute submission receiver", "error", err)
}
}
2025-01-28 15:43:28 +00:00
err = collector.ThankYou().Render(ctx, w)
2025-01-22 19:38:53 +00:00
if err != nil {
slog.Error("[collector] unable to render thank you page", "error", err)
w.WriteHeader(http.StatusInternalServerError)
}
2025-01-10 18:52:12 +00:00
}
// validate validates forms
func validate(f internal.Form, submission url.Values) (errs types.ValidationErrors) {
errs = types.ValidationErrors{}
for submittedFieldID := range maps.Keys(submission) {
field := f.Fields[submittedFieldID]
formFieldValue := submission[submittedFieldID]
if err := field.Validate(formFieldValue); err != nil {
errs[submittedFieldID] = err
}
}
return errs
}