From 49b40c0cc56272d9f7fa6a82597ca43a7c07603d Mon Sep 17 00:00:00 2001 From: Adriano Caloiaro Date: Wed, 22 Jan 2025 11:20:05 -0700 Subject: [PATCH] feat: add subject_id --- ...20250110190628_add_form_submissions.up.sql | 2 +- db/queries.sql | 14 +- handlers/collector.go | 31 +- handlers/handlers.go | 17 +- internal/default.go | 7 +- internal/queries.sql.go | 45 ++- routers/frmchi/router.go | 6 +- ui/common.templ | 16 +- ui/common_templ.go | 328 ++++++++++-------- ui/common_templ.txt | 4 +- 10 files changed, 282 insertions(+), 188 deletions(-) diff --git a/db/migrations/20250110190628_add_form_submissions.up.sql b/db/migrations/20250110190628_add_form_submissions.up.sql index 3d2da91..b982481 100644 --- a/db/migrations/20250110190628_add_form_submissions.up.sql +++ b/db/migrations/20250110190628_add_form_submissions.up.sql @@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS form_submissions ( id BIGINT PRIMARY KEY DEFAULT nextval('submission_ids'), form_id BIGINT REFERENCES forms(id) ON DELETE CASCADE, workspace_id TEXT NOT NULL, - subject_id TEXT, + subject_id TEXT DEFAULT NULL, fields jsonb default '{}' NOT NULL, status submission_status default 'partial' NOT NULL, created_at timestamptz not null default timezone('utc', now()), diff --git a/db/queries.sql b/db/queries.sql index 3ebadef..6c1530c 100644 --- a/db/queries.sql +++ b/db/queries.sql @@ -78,15 +78,21 @@ SET updated_at = timezone('utc', now()), -- name: SaveSubmission :one -INSERT INTO form_submissions (id, form_id, workspace_id, fields, status) -VALUES (coalesce(nullif(@id, 0), nextval('submission_ids'))::bigint, @form_id, @workspace_id, @fields, @status) ON conflict(id) DO +INSERT INTO form_submissions (id, form_id, workspace_id, subject_id, fields, status) +VALUES (coalesce(nullif(@id, 0), nextval('submission_ids'))::bigint, @form_id, @workspace_id, @subject_id, @fields, @status) ON conflict(id) DO UPDATE SET updated_at = timezone('utc', now()), fields = @fields, status = @status RETURNING *; -- name: GetShortCode :one -SELECT * FROM short_codes WHERE short_code = @short_code; + +SELECT * +FROM short_codes +WHERE workspace_id = @workspace_id + AND short_code = @short_code ; -- name: SaveShortCode :one -INSERT INTO short_codes (workspace_id, form_id, subject_id, short_code) VALUES (@workspace_id, @form_id, @subject_id, @short_code) RETURNING *; + +INSERT INTO short_codes (workspace_id, form_id, subject_id, short_code) +VALUES (@workspace_id, @form_id, @subject_id, @short_code) RETURNING *; diff --git a/handlers/collector.go b/handlers/collector.go index 1e5f281..f00f3f7 100644 --- a/handlers/collector.go +++ b/handlers/collector.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "log/slog" "maps" "net/http" @@ -12,6 +13,7 @@ import ( "github.com/acaloiaro/frm/types" "github.com/acaloiaro/frm/ui" "github.com/google/uuid" + "github.com/jackc/pgx/v5" ) // View renders the form viewer for the collector @@ -116,6 +118,23 @@ func Collect(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } + 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") // Validation renders whether there are errors or not errors, so that non-erroneous fields can be cleared of error messages // as the user corrects validation errors @@ -123,10 +142,13 @@ func Collect(w http.ResponseWriter, r *http.Request) { err = ui.Validation(allFields, errs).Render(ctx, w) if err != nil { slog.Error("[collector] error while reporting validation error", "error", err) - w.WriteHeader(http.StatusInternalServerError) return } - + // 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") + } formFieldValues := types.FormFieldValues{} for fieldID, fieldValue := range submission { formFieldValues[fieldID] = types.FormFieldSubmission{ @@ -141,16 +163,19 @@ func Collect(w http.ResponseWriter, r *http.Request) { } } _, err = internal.Q(ctx, i.DBArgs).SaveSubmission(ctx, internal.SaveSubmissionParams{ + // ID: submissionID, TODO save submission id FormID: formID, WorkspaceID: i.WorkspaceID, + SubjectID: &shortCode.SubjectID, Status: internal.SubmissionStatusPartial, Fields: formFieldValues, }) if err != nil { - slog.Error("[collector] unable to save submission") + slog.Error("[collector] unable to save submission", "error", err) w.WriteHeader(http.StatusInternalServerError) return } + } // validate validates forms diff --git a/handlers/handlers.go b/handlers/handlers.go index f6a11ce..037afe3 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -18,12 +18,6 @@ import ( "github.com/jackc/pgx/v5" ) -type contextKey string - -var FormIDContextKey contextKey = "frm_form_id" -var FieldIDContextKey contextKey = "frm_field_id" -var ShortCodeContextKey contextKey = "frm_short_code" - var ErrFormIDNotFound = errors.New("a form ID was not found in the request context") var ErrFieldIDNotFound = errors.New("a field ID was not found in the request context") @@ -685,10 +679,13 @@ func DeleteForm(w http.ResponseWriter, r *http.Request) { // formID gets the form ID from the request context func formID(ctx context.Context, f *frm.Frm) (formID *int64, err error) { var ok bool - formID, ok = ctx.Value(FormIDContextKey).(*int64) + formID, ok = ctx.Value(internal.FormIDContextKey).(*int64) if !ok { - if shortCode, ok := ctx.Value(ShortCodeContextKey).(*string); ok { - s, err := internal.Q(ctx, f.DBArgs).GetShortCode(ctx, *shortCode) + if shortCode, ok := ctx.Value(internal.ShortCodeContextKey).(*string); ok { + s, err := internal.Q(ctx, f.DBArgs).GetShortCode(ctx, internal.GetShortCodeParams{ + WorkspaceID: f.WorkspaceID, + ShortCode: *shortCode, + }) if err != nil { return nil, err } @@ -702,7 +699,7 @@ func formID(ctx context.Context, f *frm.Frm) (formID *int64, err error) { // fieldID gets the field id from the request context func fieldID(ctx context.Context) (fieldID *uuid.UUID, err error) { var ok bool - fieldID, ok = ctx.Value(FieldIDContextKey).(*uuid.UUID) + fieldID, ok = ctx.Value(internal.FieldIDContextKey).(*uuid.UUID) if !ok { return nil, ErrFieldIDNotFound } diff --git a/internal/default.go b/internal/default.go index 6d06caf..63a922d 100644 --- a/internal/default.go +++ b/internal/default.go @@ -31,8 +31,11 @@ const ( ) var ( - pool *pgxpool.Pool - shortcodeCharset = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + FormIDContextKey contextKey = "frm_form_id" + FieldIDContextKey contextKey = "frm_field_id" + ShortCodeContextKey contextKey = "frm_short_code" + pool *pgxpool.Pool + shortcodeCharset = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") ) type Forms []Form diff --git a/internal/queries.sql.go b/internal/queries.sql.go index a2153c1..f691e15 100644 --- a/internal/queries.sql.go +++ b/internal/queries.sql.go @@ -108,14 +108,26 @@ func (q *Queries) GetForm(ctx context.Context, arg GetFormParams) (Form, error) } const getShortCode = `-- name: GetShortCode :one -SELECT id, workspace_id, form_id, short_code, subject_id, created_at, updated_at FROM short_codes WHERE short_code = $1 + +SELECT id, workspace_id, form_id, short_code, subject_id, created_at, updated_at +FROM short_codes +WHERE workspace_id = $1 + AND short_code = $2 ` +type GetShortCodeParams struct { + WorkspaceID string `json:"workspace_id"` + ShortCode string `json:"short_code"` +} + // GetShortCode // -// SELECT id, workspace_id, form_id, short_code, subject_id, created_at, updated_at FROM short_codes WHERE short_code = $1 -func (q *Queries) GetShortCode(ctx context.Context, shortCode string) (ShortCode, error) { - row := q.db.QueryRow(ctx, getShortCode, shortCode) +// SELECT id, workspace_id, form_id, short_code, subject_id, created_at, updated_at +// FROM short_codes +// WHERE workspace_id = $1 +// AND short_code = $2 +func (q *Queries) GetShortCode(ctx context.Context, arg GetShortCodeParams) (ShortCode, error) { + row := q.db.QueryRow(ctx, getShortCode, arg.WorkspaceID, arg.ShortCode) var i ShortCode err := row.Scan( &i.ID, @@ -357,7 +369,9 @@ func (q *Queries) SaveDraft(ctx context.Context, arg SaveDraftParams) (Form, err } const saveShortCode = `-- name: SaveShortCode :one -INSERT INTO short_codes (workspace_id, form_id, subject_id, short_code) VALUES ($1, $2, $3, $4) RETURNING id, workspace_id, form_id, short_code, subject_id, created_at, updated_at + +INSERT INTO short_codes (workspace_id, form_id, subject_id, short_code) +VALUES ($1, $2, $3, $4) RETURNING id, workspace_id, form_id, short_code, subject_id, created_at, updated_at ` type SaveShortCodeParams struct { @@ -369,7 +383,8 @@ type SaveShortCodeParams struct { // SaveShortCode // -// INSERT INTO short_codes (workspace_id, form_id, subject_id, short_code) VALUES ($1, $2, $3, $4) RETURNING id, workspace_id, form_id, short_code, subject_id, created_at, updated_at +// INSERT INTO short_codes (workspace_id, form_id, subject_id, short_code) +// VALUES ($1, $2, $3, $4) RETURNING id, workspace_id, form_id, short_code, subject_id, created_at, updated_at func (q *Queries) SaveShortCode(ctx context.Context, arg SaveShortCodeParams) (ShortCode, error) { row := q.db.QueryRow(ctx, saveShortCode, arg.WorkspaceID, @@ -392,35 +407,37 @@ func (q *Queries) SaveShortCode(ctx context.Context, arg SaveShortCodeParams) (S const saveSubmission = `-- name: SaveSubmission :one -INSERT INTO form_submissions (id, form_id, workspace_id, fields, status) -VALUES (coalesce(nullif($1, 0), nextval('submission_ids'))::bigint, $2, $3, $4, $5) ON conflict(id) DO +INSERT INTO form_submissions (id, form_id, workspace_id, subject_id, fields, status) +VALUES (coalesce(nullif($1, 0), nextval('submission_ids'))::bigint, $2, $3, $4, $5, $6) ON conflict(id) DO UPDATE SET updated_at = timezone('utc', now()), - fields = $4, - status = $5 RETURNING id, form_id, workspace_id, subject_id, fields, status, created_at, updated_at + fields = $5, + status = $6 RETURNING id, form_id, workspace_id, subject_id, fields, status, created_at, updated_at ` type SaveSubmissionParams struct { ID interface{} `json:"id"` FormID *int64 `json:"form_id"` WorkspaceID string `json:"workspace_id"` + SubjectID *string `json:"subject_id"` Fields types.FormFieldValues `json:"fields"` Status SubmissionStatus `json:"status"` } // SaveSubmission // -// INSERT INTO form_submissions (id, form_id, workspace_id, fields, status) -// VALUES (coalesce(nullif($1, 0), nextval('submission_ids'))::bigint, $2, $3, $4, $5) ON conflict(id) DO +// INSERT INTO form_submissions (id, form_id, workspace_id, subject_id, fields, status) +// VALUES (coalesce(nullif($1, 0), nextval('submission_ids'))::bigint, $2, $3, $4, $5, $6) ON conflict(id) DO // UPDATE // SET updated_at = timezone('utc', now()), -// fields = $4, -// status = $5 RETURNING id, form_id, workspace_id, subject_id, fields, status, created_at, updated_at +// fields = $5, +// status = $6 RETURNING id, form_id, workspace_id, subject_id, fields, status, created_at, updated_at func (q *Queries) SaveSubmission(ctx context.Context, arg SaveSubmissionParams) (FormSubmission, error) { row := q.db.QueryRow(ctx, saveSubmission, arg.ID, arg.FormID, arg.WorkspaceID, + arg.SubjectID, arg.Fields, arg.Status, ) diff --git a/routers/frmchi/router.go b/routers/frmchi/router.go index 9e48c78..1c4817d 100644 --- a/routers/frmchi/router.go +++ b/routers/frmchi/router.go @@ -128,21 +128,21 @@ func addRequestContext(h http.Handler) http.Handler { w.WriteHeader(http.StatusNotFound) return } - ctx = context.WithValue(ctx, handlers.FormIDContextKey, &formID) + ctx = context.WithValue(ctx, internal.FormIDContextKey, &formID) case string(UrlParamFieldID): fieldID, err := uuid.Parse(chi.URLParam(r, string(UrlParamFieldID))) if err != nil { w.WriteHeader(http.StatusNotFound) return } - ctx = context.WithValue(ctx, handlers.FieldIDContextKey, &fieldID) + ctx = context.WithValue(ctx, internal.FieldIDContextKey, &fieldID) case string(UrlParamShortCode): shortCode := chi.URLParam(r, string(UrlParamShortCode)) if shortCode == "" { w.WriteHeader(http.StatusNotFound) return } - ctx = context.WithValue(ctx, handlers.ShortCodeContextKey, &shortCode) + ctx = context.WithValue(ctx, internal.ShortCodeContextKey, &shortCode) } } } diff --git a/ui/common.templ b/ui/common.templ index 07bed1d..417232a 100644 --- a/ui/common.templ +++ b/ui/common.templ @@ -5,6 +5,7 @@ import ( "strings" "context" "github.com/acaloiaro/frm" + "github.com/acaloiaro/frm/internal" "github.com/acaloiaro/frm/ui/selector" "sort" "github.com/acaloiaro/frm/types" @@ -825,6 +826,16 @@ func (v ViewerMetadata) JSON() string { return string(b) } +// shortCode gets the short code from the request context, if present +func shortCode(ctx context.Context) (shortCode *string) { + var ok bool + shortCode, ok = ctx.Value(internal.ShortCodeContextKey).(*string) + if !ok { + return + } + return +} + // FormView is a view of Forms templ FormView(form frm.Form, isPreview bool) {
@@ -838,7 +849,10 @@ templ FormView(form frm.Form, isPreview bool) {
- + + if sc := shortCode(ctx); sc != nil { + + } for _, field := range sortFields(form.Fields) {

-

+ +