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
|
|
|
|
|
}
|