From b35c9496b6fbbea8a50774fdf4255754076d81de Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 8 Jan 2026 15:34:48 +0000 Subject: [PATCH] Add the ability to register for updates on quick reports At this point it also appears that I'm correctly capturing the GPS location as both PostGIS data and as an H3 cell. --- db/dberrors/publicreport.quick.bob.go | 9 ++ db/dberrors/publicreport.quick.bob_test.go | 108 +++++++++++++ db/dbinfo/publicreport.quick.bob.go | 78 +++++++-- db/factory/bobfactory_main.bob.go | 4 +- db/factory/publicreport.quick.bob.go | 137 +++++++++++++--- db/migrations/00024_public_report.sql | 4 +- db/models/publicreport.quick.bob.go | 153 ++++++++++++------ htmlpage/public-reports/page.go | 16 +- .../template/quick-submit-complete.html | 2 +- htmlpage/public-reports/template/quick.html | 5 - .../register-notifications-complete.html | 115 +++++++++++++ public-report/endpoint.go | 70 ++++++-- public-report/report.go | 33 ++++ 13 files changed, 617 insertions(+), 117 deletions(-) create mode 100644 db/dberrors/publicreport.quick.bob_test.go create mode 100644 htmlpage/public-reports/template/register-notifications-complete.html create mode 100644 public-report/report.go diff --git a/db/dberrors/publicreport.quick.bob.go b/db/dberrors/publicreport.quick.bob.go index cbd533e1..f2d7db21 100644 --- a/db/dberrors/publicreport.quick.bob.go +++ b/db/dberrors/publicreport.quick.bob.go @@ -10,8 +10,17 @@ var PublicreportQuickErrors = &publicreportQuickErrors{ columns: []string{"id"}, s: "quick_pkey", }, + + ErrUniqueQuickPublicIdKey: &UniqueConstraintError{ + schema: "publicreport", + table: "quick", + columns: []string{"public_id"}, + s: "quick_public_id_key", + }, } type publicreportQuickErrors struct { ErrUniqueQuickPkey *UniqueConstraintError + + ErrUniqueQuickPublicIdKey *UniqueConstraintError } diff --git a/db/dberrors/publicreport.quick.bob_test.go b/db/dberrors/publicreport.quick.bob_test.go new file mode 100644 index 00000000..f9b93a7a --- /dev/null +++ b/db/dberrors/publicreport.quick.bob_test.go @@ -0,0 +1,108 @@ +// Code generated by BobGen psql v0.0.4-0.20260105020634-53e08d840e47+dirty. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package dberrors + +import ( + "context" + "errors" + "testing" + + factory "github.com/Gleipnir-Technology/nidus-sync/db/factory" + models "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/stephenafamo/bob" +) + +func TestPublicreportQuickUniqueConstraintErrors(t *testing.T) { + if testDB == nil { + t.Skip("No database connection provided") + } + + f := factory.New() + tests := []struct { + name string + expectedErr *UniqueConstraintError + conflictMods func(context.Context, *testing.T, bob.Executor, *models.PublicreportQuick) factory.PublicreportQuickModSlice + }{ + { + name: "ErrUniqueQuickPkey", + expectedErr: PublicreportQuickErrors.ErrUniqueQuickPkey, + conflictMods: func(ctx context.Context, t *testing.T, exec bob.Executor, obj *models.PublicreportQuick) factory.PublicreportQuickModSlice { + shouldUpdate := false + updateMods := make(factory.PublicreportQuickModSlice, 0, 1) + + if shouldUpdate { + if err := obj.Update(ctx, exec, f.NewPublicreportQuickWithContext(ctx, updateMods...).BuildSetter()); err != nil { + t.Fatalf("Error updating object: %v", err) + } + } + + return factory.PublicreportQuickModSlice{ + factory.PublicreportQuickMods.ID(obj.ID), + } + }, + }, + { + name: "ErrUniqueQuickPublicIdKey", + expectedErr: PublicreportQuickErrors.ErrUniqueQuickPublicIdKey, + conflictMods: func(ctx context.Context, t *testing.T, exec bob.Executor, obj *models.PublicreportQuick) factory.PublicreportQuickModSlice { + shouldUpdate := false + updateMods := make(factory.PublicreportQuickModSlice, 0, 1) + + if shouldUpdate { + if err := obj.Update(ctx, exec, f.NewPublicreportQuickWithContext(ctx, updateMods...).BuildSetter()); err != nil { + t.Fatalf("Error updating object: %v", err) + } + } + + return factory.PublicreportQuickModSlice{ + factory.PublicreportQuickMods.PublicID(obj.PublicID), + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + t.Cleanup(cancel) + + tx, err := testDB.Begin(ctx) + if err != nil { + t.Fatalf("Couldn't start database transaction: %v", err) + } + + defer func() { + if err := tx.Rollback(ctx); err != nil { + t.Fatalf("Error rolling back transaction: %v", err) + } + }() + + var exec bob.Executor = tx + + obj, err := f.NewPublicreportQuickWithContext(ctx, factory.PublicreportQuickMods.WithParentsCascading()).Create(ctx, exec) + if err != nil { + t.Fatal(err) + } + + obj2, err := f.NewPublicreportQuickWithContext(ctx).Create(ctx, exec) + if err != nil { + t.Fatal(err) + } + + err = obj2.Update(ctx, exec, f.NewPublicreportQuickWithContext(ctx, tt.conflictMods(ctx, t, exec, obj)...).BuildSetter()) + if !errors.Is(ErrUniqueConstraint, err) { + t.Fatalf("Expected: %s, Got: %v", tt.name, err) + } + if !errors.Is(tt.expectedErr, err) { + t.Fatalf("Expected: %s, Got: %v", tt.expectedErr.Error(), err) + } + if !ErrUniqueConstraint.Is(err) { + t.Fatalf("Expected: %s, Got: %v", tt.name, err) + } + if !tt.expectedErr.Is(err) { + t.Fatalf("Expected: %s, Got: %v", tt.expectedErr.Error(), err) + } + }) + } +} diff --git a/db/dbinfo/publicreport.quick.bob.go b/db/dbinfo/publicreport.quick.bob.go index 20de6e04..e9f862e1 100644 --- a/db/dbinfo/publicreport.quick.bob.go +++ b/db/dbinfo/publicreport.quick.bob.go @@ -60,9 +60,27 @@ var PublicreportQuicks = Table[ Generated: false, AutoIncr: false, }, - UUID: column{ - Name: "uuid", - DBType: "uuid", + PublicID: column{ + Name: "public_id", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + ReporterEmail: column{ + Name: "reporter_email", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + ReporterPhone: column{ + Name: "reporter_phone", + DBType: "text", Default: "", Comment: "", Nullable: false, @@ -88,6 +106,23 @@ var PublicreportQuicks = Table[ Where: "", Include: []string{}, }, + QuickPublicIDKey: index{ + Type: "btree", + Name: "quick_public_id_key", + Columns: []indexColumn{ + { + Name: "public_id", + Desc: null.FromCond(false, true), + IsExpression: false, + }, + }, + Unique: true, + Comment: "", + NullsFirst: []bool{false}, + NullsDistinct: false, + Where: "", + Include: []string{}, + }, }, PrimaryKey: &constraint{ Name: "quick_pkey", @@ -95,31 +130,42 @@ var PublicreportQuicks = Table[ Comment: "", }, + Uniques: publicreportQuickUniques{ + QuickPublicIDKey: constraint{ + Name: "quick_public_id_key", + Columns: []string{"public_id"}, + Comment: "", + }, + }, + Comment: "", } type publicreportQuickColumns struct { - ID column - Created column - Comments column - Location column - H3cell column - UUID column + ID column + Created column + Comments column + Location column + H3cell column + PublicID column + ReporterEmail column + ReporterPhone column } func (c publicreportQuickColumns) AsSlice() []column { return []column{ - c.ID, c.Created, c.Comments, c.Location, c.H3cell, c.UUID, + c.ID, c.Created, c.Comments, c.Location, c.H3cell, c.PublicID, c.ReporterEmail, c.ReporterPhone, } } type publicreportQuickIndexes struct { - QuickPkey index + QuickPkey index + QuickPublicIDKey index } func (i publicreportQuickIndexes) AsSlice() []index { return []index{ - i.QuickPkey, + i.QuickPkey, i.QuickPublicIDKey, } } @@ -129,10 +175,14 @@ func (f publicreportQuickForeignKeys) AsSlice() []foreignKey { return []foreignKey{} } -type publicreportQuickUniques struct{} +type publicreportQuickUniques struct { + QuickPublicIDKey constraint +} func (u publicreportQuickUniques) AsSlice() []constraint { - return []constraint{} + return []constraint{ + u.QuickPublicIDKey, + } } type publicreportQuickChecks struct{} diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index 0321918b..018e83b1 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -2396,7 +2396,9 @@ func (f *Factory) FromExistingPublicreportQuick(m *models.PublicreportQuick) *Pu o.Comments = func() string { return m.Comments } o.Location = func() null.Val[string] { return m.Location } o.H3cell = func() null.Val[string] { return m.H3cell } - o.UUID = func() uuid.UUID { return m.UUID } + o.PublicID = func() string { return m.PublicID } + o.ReporterEmail = func() string { return m.ReporterEmail } + o.ReporterPhone = func() string { return m.ReporterPhone } ctx := context.Background() if len(m.R.QuickPhotos) > 0 { diff --git a/db/factory/publicreport.quick.bob.go b/db/factory/publicreport.quick.bob.go index e69c2bc6..7ea2462b 100644 --- a/db/factory/publicreport.quick.bob.go +++ b/db/factory/publicreport.quick.bob.go @@ -12,7 +12,6 @@ import ( "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/google/uuid" "github.com/jaswdr/faker/v2" "github.com/stephenafamo/bob" ) @@ -38,12 +37,14 @@ func (mods PublicreportQuickModSlice) Apply(ctx context.Context, n *Publicreport // PublicreportQuickTemplate is an object representing the database table. // all columns are optional and should be set by mods type PublicreportQuickTemplate struct { - ID func() int32 - Created func() time.Time - Comments func() string - Location func() null.Val[string] - H3cell func() null.Val[string] - UUID func() uuid.UUID + ID func() int32 + Created func() time.Time + Comments func() string + Location func() null.Val[string] + H3cell func() null.Val[string] + PublicID func() string + ReporterEmail func() string + ReporterPhone func() string r publicreportQuickR f *Factory @@ -109,9 +110,17 @@ func (o PublicreportQuickTemplate) BuildSetter() *models.PublicreportQuickSetter val := o.H3cell() m.H3cell = omitnull.FromNull(val) } - if o.UUID != nil { - val := o.UUID() - m.UUID = omit.From(val) + if o.PublicID != nil { + val := o.PublicID() + m.PublicID = omit.From(val) + } + if o.ReporterEmail != nil { + val := o.ReporterEmail() + m.ReporterEmail = omit.From(val) + } + if o.ReporterPhone != nil { + val := o.ReporterPhone() + m.ReporterPhone = omit.From(val) } return m @@ -150,8 +159,14 @@ func (o PublicreportQuickTemplate) Build() *models.PublicreportQuick { if o.H3cell != nil { m.H3cell = o.H3cell() } - if o.UUID != nil { - m.UUID = o.UUID() + if o.PublicID != nil { + m.PublicID = o.PublicID() + } + if o.ReporterEmail != nil { + m.ReporterEmail = o.ReporterEmail() + } + if o.ReporterPhone != nil { + m.ReporterPhone = o.ReporterPhone() } o.setModelRels(m) @@ -181,9 +196,17 @@ func ensureCreatablePublicreportQuick(m *models.PublicreportQuickSetter) { val := random_string(nil) m.Comments = omit.From(val) } - if !(m.UUID.IsValue()) { - val := random_uuid_UUID(nil) - m.UUID = omit.From(val) + if !(m.PublicID.IsValue()) { + val := random_string(nil) + m.PublicID = omit.From(val) + } + if !(m.ReporterEmail.IsValue()) { + val := random_string(nil) + m.ReporterEmail = omit.From(val) + } + if !(m.ReporterPhone.IsValue()) { + val := random_string(nil) + m.ReporterPhone = omit.From(val) } } @@ -310,7 +333,9 @@ func (m publicreportQuickMods) RandomizeAllColumns(f *faker.Faker) PublicreportQ PublicreportQuickMods.RandomComments(f), PublicreportQuickMods.RandomLocation(f), PublicreportQuickMods.RandomH3cell(f), - PublicreportQuickMods.RandomUUID(f), + PublicreportQuickMods.RandomPublicID(f), + PublicreportQuickMods.RandomReporterEmail(f), + PublicreportQuickMods.RandomReporterPhone(f), } } @@ -514,32 +539,94 @@ func (m publicreportQuickMods) RandomH3cellNotNull(f *faker.Faker) PublicreportQ } // Set the model columns to this value -func (m publicreportQuickMods) UUID(val uuid.UUID) PublicreportQuickMod { +func (m publicreportQuickMods) PublicID(val string) PublicreportQuickMod { return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { - o.UUID = func() uuid.UUID { return val } + o.PublicID = func() string { return val } }) } // Set the Column from the function -func (m publicreportQuickMods) UUIDFunc(f func() uuid.UUID) PublicreportQuickMod { +func (m publicreportQuickMods) PublicIDFunc(f func() string) PublicreportQuickMod { return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { - o.UUID = f + o.PublicID = f }) } // Clear any values for the column -func (m publicreportQuickMods) UnsetUUID() PublicreportQuickMod { +func (m publicreportQuickMods) UnsetPublicID() PublicreportQuickMod { return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { - o.UUID = nil + o.PublicID = nil }) } // Generates a random value for the column using the given faker // if faker is nil, a default faker is used -func (m publicreportQuickMods) RandomUUID(f *faker.Faker) PublicreportQuickMod { +func (m publicreportQuickMods) RandomPublicID(f *faker.Faker) PublicreportQuickMod { return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { - o.UUID = func() uuid.UUID { - return random_uuid_UUID(f) + o.PublicID = func() string { + return random_string(f) + } + }) +} + +// Set the model columns to this value +func (m publicreportQuickMods) ReporterEmail(val string) PublicreportQuickMod { + return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { + o.ReporterEmail = func() string { return val } + }) +} + +// Set the Column from the function +func (m publicreportQuickMods) ReporterEmailFunc(f func() string) PublicreportQuickMod { + return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { + o.ReporterEmail = f + }) +} + +// Clear any values for the column +func (m publicreportQuickMods) UnsetReporterEmail() PublicreportQuickMod { + return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { + o.ReporterEmail = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m publicreportQuickMods) RandomReporterEmail(f *faker.Faker) PublicreportQuickMod { + return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { + o.ReporterEmail = func() string { + return random_string(f) + } + }) +} + +// Set the model columns to this value +func (m publicreportQuickMods) ReporterPhone(val string) PublicreportQuickMod { + return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { + o.ReporterPhone = func() string { return val } + }) +} + +// Set the Column from the function +func (m publicreportQuickMods) ReporterPhoneFunc(f func() string) PublicreportQuickMod { + return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { + o.ReporterPhone = f + }) +} + +// Clear any values for the column +func (m publicreportQuickMods) UnsetReporterPhone() PublicreportQuickMod { + return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { + o.ReporterPhone = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m publicreportQuickMods) RandomReporterPhone(f *faker.Faker) PublicreportQuickMod { + return PublicreportQuickModFunc(func(_ context.Context, o *PublicreportQuickTemplate) { + o.ReporterPhone = func() string { + return random_string(f) } }) } diff --git a/db/migrations/00024_public_report.sql b/db/migrations/00024_public_report.sql index d4429820..ee8d51c9 100644 --- a/db/migrations/00024_public_report.sql +++ b/db/migrations/00024_public_report.sql @@ -6,7 +6,9 @@ CREATE TABLE publicreport.quick ( comments TEXT NOT NULL, location GEOGRAPHY, h3cell h3index, - uuid UUID NOT NULL + public_id TEXT NOT NULL UNIQUE, + reporter_email TEXT NOT NULL, + reporter_phone TEXT NOT NULL ); CREATE TABLE publicreport.quick_photo ( diff --git a/db/models/publicreport.quick.bob.go b/db/models/publicreport.quick.bob.go index 612883e2..e67c9c3e 100644 --- a/db/models/publicreport.quick.bob.go +++ b/db/models/publicreport.quick.bob.go @@ -12,7 +12,6 @@ import ( "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/google/uuid" "github.com/stephenafamo/bob" "github.com/stephenafamo/bob/dialect/psql" "github.com/stephenafamo/bob/dialect/psql/dialect" @@ -27,12 +26,14 @@ import ( // PublicreportQuick is an object representing the database table. type PublicreportQuick struct { - ID int32 `db:"id,pk" ` - Created time.Time `db:"created" ` - Comments string `db:"comments" ` - Location null.Val[string] `db:"location" ` - H3cell null.Val[string] `db:"h3cell" ` - UUID uuid.UUID `db:"uuid" ` + ID int32 `db:"id,pk" ` + Created time.Time `db:"created" ` + Comments string `db:"comments" ` + Location null.Val[string] `db:"location" ` + H3cell null.Val[string] `db:"h3cell" ` + PublicID string `db:"public_id" ` + ReporterEmail string `db:"reporter_email" ` + ReporterPhone string `db:"reporter_phone" ` R publicreportQuickR `db:"-" ` } @@ -55,27 +56,31 @@ type publicreportQuickR struct { func buildPublicreportQuickColumns(alias string) publicreportQuickColumns { return publicreportQuickColumns{ ColumnsExpr: expr.NewColumnsExpr( - "id", "created", "comments", "location", "h3cell", "uuid", + "id", "created", "comments", "location", "h3cell", "public_id", "reporter_email", "reporter_phone", ).WithParent("publicreport.quick"), - tableAlias: alias, - ID: psql.Quote(alias, "id"), - Created: psql.Quote(alias, "created"), - Comments: psql.Quote(alias, "comments"), - Location: psql.Quote(alias, "location"), - H3cell: psql.Quote(alias, "h3cell"), - UUID: psql.Quote(alias, "uuid"), + tableAlias: alias, + ID: psql.Quote(alias, "id"), + Created: psql.Quote(alias, "created"), + Comments: psql.Quote(alias, "comments"), + Location: psql.Quote(alias, "location"), + H3cell: psql.Quote(alias, "h3cell"), + PublicID: psql.Quote(alias, "public_id"), + ReporterEmail: psql.Quote(alias, "reporter_email"), + ReporterPhone: psql.Quote(alias, "reporter_phone"), } } type publicreportQuickColumns struct { expr.ColumnsExpr - tableAlias string - ID psql.Expression - Created psql.Expression - Comments psql.Expression - Location psql.Expression - H3cell psql.Expression - UUID psql.Expression + tableAlias string + ID psql.Expression + Created psql.Expression + Comments psql.Expression + Location psql.Expression + H3cell psql.Expression + PublicID psql.Expression + ReporterEmail psql.Expression + ReporterPhone psql.Expression } func (c publicreportQuickColumns) Alias() string { @@ -90,16 +95,18 @@ func (publicreportQuickColumns) AliasedAs(alias string) publicreportQuickColumns // All values are optional, and do not have to be set // Generated columns are not included type PublicreportQuickSetter struct { - ID omit.Val[int32] `db:"id,pk" ` - Created omit.Val[time.Time] `db:"created" ` - Comments omit.Val[string] `db:"comments" ` - Location omitnull.Val[string] `db:"location" ` - H3cell omitnull.Val[string] `db:"h3cell" ` - UUID omit.Val[uuid.UUID] `db:"uuid" ` + ID omit.Val[int32] `db:"id,pk" ` + Created omit.Val[time.Time] `db:"created" ` + Comments omit.Val[string] `db:"comments" ` + Location omitnull.Val[string] `db:"location" ` + H3cell omitnull.Val[string] `db:"h3cell" ` + PublicID omit.Val[string] `db:"public_id" ` + ReporterEmail omit.Val[string] `db:"reporter_email" ` + ReporterPhone omit.Val[string] `db:"reporter_phone" ` } func (s PublicreportQuickSetter) SetColumns() []string { - vals := make([]string, 0, 6) + vals := make([]string, 0, 8) if s.ID.IsValue() { vals = append(vals, "id") } @@ -115,8 +122,14 @@ func (s PublicreportQuickSetter) SetColumns() []string { if !s.H3cell.IsUnset() { vals = append(vals, "h3cell") } - if s.UUID.IsValue() { - vals = append(vals, "uuid") + if s.PublicID.IsValue() { + vals = append(vals, "public_id") + } + if s.ReporterEmail.IsValue() { + vals = append(vals, "reporter_email") + } + if s.ReporterPhone.IsValue() { + vals = append(vals, "reporter_phone") } return vals } @@ -137,8 +150,14 @@ func (s PublicreportQuickSetter) Overwrite(t *PublicreportQuick) { if !s.H3cell.IsUnset() { t.H3cell = s.H3cell.MustGetNull() } - if s.UUID.IsValue() { - t.UUID = s.UUID.MustGet() + if s.PublicID.IsValue() { + t.PublicID = s.PublicID.MustGet() + } + if s.ReporterEmail.IsValue() { + t.ReporterEmail = s.ReporterEmail.MustGet() + } + if s.ReporterPhone.IsValue() { + t.ReporterPhone = s.ReporterPhone.MustGet() } } @@ -148,7 +167,7 @@ func (s *PublicreportQuickSetter) Apply(q *dialect.InsertQuery) { }) q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { - vals := make([]bob.Expression, 6) + vals := make([]bob.Expression, 8) if s.ID.IsValue() { vals[0] = psql.Arg(s.ID.MustGet()) } else { @@ -179,12 +198,24 @@ func (s *PublicreportQuickSetter) Apply(q *dialect.InsertQuery) { vals[4] = psql.Raw("DEFAULT") } - if s.UUID.IsValue() { - vals[5] = psql.Arg(s.UUID.MustGet()) + if s.PublicID.IsValue() { + vals[5] = psql.Arg(s.PublicID.MustGet()) } else { vals[5] = psql.Raw("DEFAULT") } + if s.ReporterEmail.IsValue() { + vals[6] = psql.Arg(s.ReporterEmail.MustGet()) + } else { + vals[6] = psql.Raw("DEFAULT") + } + + if s.ReporterPhone.IsValue() { + vals[7] = psql.Arg(s.ReporterPhone.MustGet()) + } else { + vals[7] = psql.Raw("DEFAULT") + } + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) } @@ -194,7 +225,7 @@ func (s PublicreportQuickSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { } func (s PublicreportQuickSetter) Expressions(prefix ...string) []bob.Expression { - exprs := make([]bob.Expression, 0, 6) + exprs := make([]bob.Expression, 0, 8) if s.ID.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ @@ -231,10 +262,24 @@ func (s PublicreportQuickSetter) Expressions(prefix ...string) []bob.Expression }}) } - if s.UUID.IsValue() { + if s.PublicID.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ - psql.Quote(append(prefix, "uuid")...), - psql.Arg(s.UUID), + psql.Quote(append(prefix, "public_id")...), + psql.Arg(s.PublicID), + }}) + } + + if s.ReporterEmail.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "reporter_email")...), + psql.Arg(s.ReporterEmail), + }}) + } + + if s.ReporterPhone.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "reporter_phone")...), + psql.Arg(s.ReporterPhone), }}) } @@ -557,12 +602,14 @@ func (publicreportQuick0 *PublicreportQuick) AttachQuickPhotos(ctx context.Conte } type publicreportQuickWhere[Q psql.Filterable] struct { - ID psql.WhereMod[Q, int32] - Created psql.WhereMod[Q, time.Time] - Comments psql.WhereMod[Q, string] - Location psql.WhereNullMod[Q, string] - H3cell psql.WhereNullMod[Q, string] - UUID psql.WhereMod[Q, uuid.UUID] + ID psql.WhereMod[Q, int32] + Created psql.WhereMod[Q, time.Time] + Comments psql.WhereMod[Q, string] + Location psql.WhereNullMod[Q, string] + H3cell psql.WhereNullMod[Q, string] + PublicID psql.WhereMod[Q, string] + ReporterEmail psql.WhereMod[Q, string] + ReporterPhone psql.WhereMod[Q, string] } func (publicreportQuickWhere[Q]) AliasedAs(alias string) publicreportQuickWhere[Q] { @@ -571,12 +618,14 @@ func (publicreportQuickWhere[Q]) AliasedAs(alias string) publicreportQuickWhere[ func buildPublicreportQuickWhere[Q psql.Filterable](cols publicreportQuickColumns) publicreportQuickWhere[Q] { return publicreportQuickWhere[Q]{ - ID: psql.Where[Q, int32](cols.ID), - Created: psql.Where[Q, time.Time](cols.Created), - Comments: psql.Where[Q, string](cols.Comments), - Location: psql.WhereNull[Q, string](cols.Location), - H3cell: psql.WhereNull[Q, string](cols.H3cell), - UUID: psql.Where[Q, uuid.UUID](cols.UUID), + ID: psql.Where[Q, int32](cols.ID), + Created: psql.Where[Q, time.Time](cols.Created), + Comments: psql.Where[Q, string](cols.Comments), + Location: psql.WhereNull[Q, string](cols.Location), + H3cell: psql.WhereNull[Q, string](cols.H3cell), + PublicID: psql.Where[Q, string](cols.PublicID), + ReporterEmail: psql.Where[Q, string](cols.ReporterEmail), + ReporterPhone: psql.Where[Q, string](cols.ReporterPhone), } } diff --git a/htmlpage/public-reports/page.go b/htmlpage/public-reports/page.go index 8a37af49..9f4d2188 100644 --- a/htmlpage/public-reports/page.go +++ b/htmlpage/public-reports/page.go @@ -19,16 +19,20 @@ type ContextQuick struct{} type ContextQuickSubmitComplete struct { ReportID string } +type ContextRegisterNotificationsComplete struct { + ReportID string +} type ContextRoot struct{} type ContextStatus struct{} var ( - Nuisance = buildTemplate("nuisance", "base") - Pool = buildTemplate("pool", "base") - Quick = buildTemplate("quick", "base") - QuickSubmitComplete = buildTemplate("quick-submit-complete", "base") - Root = buildTemplate("root", "base") - Status = buildTemplate("status", "base") + Nuisance = buildTemplate("nuisance", "base") + Pool = buildTemplate("pool", "base") + Quick = buildTemplate("quick", "base") + QuickSubmitComplete = buildTemplate("quick-submit-complete", "base") + RegisterNotificationsComplete = buildTemplate("register-notifications-complete", "base") + Root = buildTemplate("root", "base") + Status = buildTemplate("status", "base") ) var components = [...]string{"footer"} diff --git a/htmlpage/public-reports/template/quick-submit-complete.html b/htmlpage/public-reports/template/quick-submit-complete.html index be20cfb5..70b6e839 100644 --- a/htmlpage/public-reports/template/quick-submit-complete.html +++ b/htmlpage/public-reports/template/quick-submit-complete.html @@ -60,7 +60,7 @@

Provide your contact information to receive updates about your report.

- +
diff --git a/htmlpage/public-reports/template/quick.html b/htmlpage/public-reports/template/quick.html index a6c910e1..85de263b 100644 --- a/htmlpage/public-reports/template/quick.html +++ b/htmlpage/public-reports/template/quick.html @@ -60,16 +60,12 @@ document.addEventListener('DOMContentLoaded', function() { const locationStatus = document.getElementById('locationStatus'); const latitudeInput = document.getElementById('latitude'); const longitudeInput = document.getElementById('longitude'); - const createdInput = document.getElementById('created'); const submitButton = document.getElementById('submitButton'); const loadingOverlay = document.getElementById('loadingOverlay'); // Get current location requestLocation(); - // Set current time - createdInput.value = new Date().toISOString(); - // Handle photo selection photoInput.addEventListener('change', handlePhotoSelection); @@ -251,7 +247,6 @@ document.addEventListener('DOMContentLoaded', function() { -
diff --git a/htmlpage/public-reports/template/register-notifications-complete.html b/htmlpage/public-reports/template/register-notifications-complete.html new file mode 100644 index 00000000..783c0824 --- /dev/null +++ b/htmlpage/public-reports/template/register-notifications-complete.html @@ -0,0 +1,115 @@ +{{template "base.html" .}} + +{{define "title"}}Dash{{end}} +{{define "extraheader"}} + + +{{end}} +{{define "content"}} +
+
+
+ +
+
+

+ + + + Notifications Registered +

+
+
+
+
+ + + +
+

Thank You!

+

Your contact information has been successfully registered for report updates.

+
+ Report ID: + {{.ReportID}} +
+
+ +
+ + +
+
+ + + + + What to Expect +
+
    +
  • + + + + + A confirmation message has been sent to your contact information. +
  • +
  • + + + + + + You will receive updates when: +
      +
    • Your report is assigned to a specialist
    • +
    • A site visit is scheduled
    • +
    • Treatment or remediation is completed
    • +
    • The case is resolved
    • +
    +
  • +
  • + + + + You can check your report status anytime using your Report ID. +
  • +
+
+ + + +
+
+ + +
+
+
+ + + + + Need Help? +
+

If you need to update your contact information or have questions about your report, please contact our Mosquito Control Unit at (123) 456-7890 or mosquito@example.gov and reference your Report ID.

+
+
+
+
+
+{{end}} diff --git a/public-report/endpoint.go b/public-report/endpoint.go index 6e7d0aa2..374cdd72 100644 --- a/public-report/endpoint.go +++ b/public-report/endpoint.go @@ -20,8 +20,6 @@ import ( "github.com/google/uuid" "github.com/rs/zerolog/log" "github.com/stephenafamo/bob/dialect/psql" - //"github.com/stephenafamo/bob/dialect/psql/dialect" - //"github.com/stephenafamo/bob/dialect/psql/im" "github.com/stephenafamo/bob/dialect/psql/um" ) @@ -33,6 +31,8 @@ func Router() chi.Router { r.Get("/quick", getQuick) r.Post("/quick-submit", postQuick) r.Get("/quick-submit-complete", getQuickSubmitComplete) + r.Post("/register-notifications", postRegisterNotifications) + r.Get("/register-notifications-complete", getRegisterNotificationsComplete) r.Get("/status", getStatus) localFS := http.Dir("./static") htmlpage.FileServer(r, "/static", localFS, publicreports.EmbeddedStaticFS, "static") @@ -78,6 +78,16 @@ func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) { }, ) } +func getRegisterNotificationsComplete(w http.ResponseWriter, r *http.Request) { + report := r.URL.Query().Get("report") + htmlpage.RenderOrError( + w, + publicreports.RegisterNotificationsComplete, + publicreports.ContextRegisterNotificationsComplete{ + ReportID: report, + }, + ) +} func getStatus(w http.ResponseWriter, r *http.Request) { htmlpage.RenderOrError( w, @@ -88,13 +98,11 @@ func getStatus(w http.ResponseWriter, r *http.Request) { func postQuick(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(32 << 10) // 32 MB buffer if err != nil { - log.Error().Err(err).Msg("Failed to parse form") respondError(w, "Failed to parse form", err, http.StatusBadRequest) return } lat := r.FormValue("latitude") lng := r.FormValue("longitude") - created := r.FormValue("created") comments := r.FormValue("comments") //photos := r.FormValue("photos") @@ -108,9 +116,9 @@ func postQuick(w http.ResponseWriter, r *http.Request) { respondError(w, "Failed to create parse longitude", err, http.StatusBadRequest) return } - u, err := uuid.NewUUID() + u, err := GenerateReportID() if err != nil { - respondError(w, "Failed to create quick report uuid", err, http.StatusInternalServerError) + respondError(w, "Failed to create quick report public ID", err, http.StatusInternalServerError) return } c, err := h3utils.GetCell(longitude, latitude, 15) @@ -118,8 +126,10 @@ func postQuick(w http.ResponseWriter, r *http.Request) { Created: omit.From(time.Now()), Comments: omit.From(comments), //Location: omitnull.From(fmt.Sprintf("ST_GeometryFromText(Point(%s %s))", longitude, latitude)), - H3cell: omitnull.From(c.String()), - UUID: omit.From(u), + H3cell: omitnull.From(c.String()), + PublicID: omit.From(u), + ReporterEmail: omit.From(""), + ReporterPhone: omit.From(""), } quick, err := models.PublicreportQuicks.Insert(&setter).One(r.Context(), db.PGInstance.BobDB) if err != nil { @@ -135,7 +145,7 @@ func postQuick(w http.ResponseWriter, r *http.Request) { respondError(w, "Failed to insert publicreport", err, http.StatusInternalServerError) return } - log.Info().Float64("latitude", latitude).Float64("longitude", longitude).Str("created", created).Msg("Got upload") + log.Info().Float64("latitude", latitude).Float64("longitude", longitude).Msg("Got upload") photoSetters := make([]*models.PublicreportQuickPhotoSetter, 0) for _, fheaders := range r.MultipartForm.File { for _, headers := range fheaders { @@ -179,12 +189,48 @@ func postQuick(w http.ResponseWriter, r *http.Request) { }) } } - /*err = quick.InsertQuickPhotos(r.Context(), db.PGInstance.BobDB, photoSetters...) + err = quick.InsertQuickPhotos(r.Context(), db.PGInstance.BobDB, photoSetters...) if err != nil { respondError(w, "Failed to create photo records", err, http.StatusInternalServerError) return - }*/ - http.Redirect(w, r, "/quick-submit-complete?report=123", http.StatusFound) + } + http.Redirect(w, r, fmt.Sprintf("/quick-submit-complete?report=%s", u), http.StatusFound) +} + +func postRegisterNotifications(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + respondError(w, "Failed to parse form", err, http.StatusBadRequest) + return + } + consent := r.PostFormValue("consent") + email := r.PostFormValue("email") + phone := r.PostFormValue("phone") + report_id := r.PostFormValue("report_id") + if consent != "on" { + respondError(w, "You must consent", nil, http.StatusBadRequest) + return + } + result, err := psql.Update( + um.Table("publicreport.quick"), + um.SetCol("reporter_email").ToArg(email), + um.SetCol("reporter_phone").ToArg(phone), + um.Where(psql.Quote("public_id").EQ(psql.Arg(report_id))), + ).Exec(r.Context(), db.PGInstance.BobDB) + if err != nil { + respondError(w, "Failed to update report", err, http.StatusInternalServerError) + return + } + rowcount, err := result.RowsAffected() + if err != nil { + respondError(w, "Failed to get rows affected", err, http.StatusInternalServerError) + return + } + if rowcount == 0 { + http.Redirect(w, r, fmt.Sprintf("/error?code=no-rows-affected&report=%s", report_id), http.StatusFound) + } else { + http.Redirect(w, r, fmt.Sprintf("/register-notifications-complete?report=%s", report_id), http.StatusFound) + } } // Respond with an error that is visible to the user diff --git a/public-report/report.go b/public-report/report.go new file mode 100644 index 00000000..d557e909 --- /dev/null +++ b/public-report/report.go @@ -0,0 +1,33 @@ +package publicreport + +import ( + "crypto/rand" + "fmt" + "math/big" + "strings" +) + +// GenerateReportID creates a 12-character random string using only unambiguous +// capital letters and numbers +func GenerateReportID() (string, error) { + // Define character set (no O, I, Z to avoid confusion) + const charset = "ABCDEFGHJKLMNPQRSTUVWXY0123456789" + const length = 12 + + var builder strings.Builder + builder.Grow(length) + + // Use crypto/rand for secure randomness + for i := 0; i < length; i++ { + // Generate a random index within our charset + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", fmt.Errorf("failed to generate random number: %w", err) + } + + // Add the randomly selected character to our ID + builder.WriteByte(charset[n.Int64()]) + } + + return builder.String(), nil +}