From 908ac4faeaf1c48d50d1d5a16d6bc9be83a18194 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 19 Mar 2026 17:41:38 +0000 Subject: [PATCH] Make signals, not leads, from public reports. --- api/publicreport.go | 19 +- api/routes.go | 2 +- db/dbinfo/signal.bob.go | 24 ++- .../00119_signal_from_publicreport.sql | 4 + db/models/signal.bob.go | 197 +++++++++++++++++- db/models/site.bob.go | 181 ++++++++++++++++ html/template/sync/communication-root.html | 19 +- platform/lead.go | 79 ------- platform/signal.go | 108 ++++++++++ 9 files changed, 531 insertions(+), 102 deletions(-) create mode 100644 db/migrations/00119_signal_from_publicreport.sql create mode 100644 platform/signal.go diff --git a/api/publicreport.go b/api/publicreport.go index 65f64483..fc69d381 100644 --- a/api/publicreport.go +++ b/api/publicreport.go @@ -10,17 +10,20 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/platform" ) -type formPublicreportLead struct { +type formPublicreportSignal struct { ReportID string `json:"reportID"` } +type createdSignal struct { + ID int32 `json:"id"` +} -func postPublicreportLead(ctx context.Context, r *http.Request, user platform.User, req formPublicreportLead) (*createdLead, *nhttp.ErrorWithStatus) { - lead_id, err := platform.LeadCreateFromPublicreport(ctx, user, req.ReportID) +func postPublicreportSignal(ctx context.Context, r *http.Request, user platform.User, req formPublicreportSignal) (*createdSignal, *nhttp.ErrorWithStatus) { + signal_id, err := platform.SignalCreateFromPublicreport(ctx, user, req.ReportID) if err != nil { - return nil, nhttp.NewError("create lead: %w", err) + return nil, nhttp.NewError("create signal: %w", err) } - return &createdLead{ - ID: *lead_id, + return &createdSignal{ + ID: *signal_id, }, nil } @@ -31,10 +34,10 @@ type createdReport struct { URI string `json:"uri"` } -func postPublicreportInvalid(ctx context.Context, r *http.Request, user platform.User, req formPublicreportLead) (*createdReport, *nhttp.ErrorWithStatus) { +func postPublicreportInvalid(ctx context.Context, r *http.Request, user platform.User, req formPublicreportSignal) (*createdReport, *nhttp.ErrorWithStatus) { err := platform.PublicreportInvalid(ctx, user, req.ReportID) if err != nil { - return nil, nhttp.NewError("create lead: %w", err) + return nil, nhttp.NewError("create signal: %w", err) } return &createdReport{ URI: config.MakeURLNidus("/publicreport/%s", req.ReportID), diff --git a/api/routes.go b/api/routes.go index a458b6b9..25c9b43f 100644 --- a/api/routes.go +++ b/api/routes.go @@ -22,7 +22,7 @@ func AddRoutes(r chi.Router) { r.Method("POST", "/leads", authenticatedHandlerJSONPost(postLeads)) r.Method("GET", "/mosquito-source", auth.NewEnsureAuth(apiMosquitoSource)) r.Method("POST", "/publicreport/invalid", authenticatedHandlerJSONPost(postPublicreportInvalid)) - r.Method("POST", "/publicreport/lead", authenticatedHandlerJSONPost(postPublicreportLead)) + r.Method("POST", "/publicreport/signal", authenticatedHandlerJSONPost(postPublicreportSignal)) r.Method("POST", "/publicreport/message", authenticatedHandlerJSONPost(postPublicreportMessage)) r.Method("POST", "/review/pool", authenticatedHandlerJSONPost(postReviewPool)) r.Method("GET", "/review-task/pool", authenticatedHandlerJSON(listReviewTaskPool)) diff --git a/db/dbinfo/signal.bob.go b/db/dbinfo/signal.bob.go index c4f3c930..bffd3f25 100644 --- a/db/dbinfo/signal.bob.go +++ b/db/dbinfo/signal.bob.go @@ -96,6 +96,15 @@ var Signals = Table[ Generated: false, AutoIncr: false, }, + SiteID: column{ + Name: "site_id", + DBType: "integer", + Default: "NULL", + Comment: "", + Nullable: true, + Generated: false, + AutoIncr: false, + }, }, Indexes: signalIndexes{ SignalPkey: index{ @@ -149,6 +158,15 @@ var Signals = Table[ ForeignTable: "organization", ForeignColumns: []string{"id"}, }, + SignalSignalSiteIDFkey: foreignKey{ + constraint: constraint{ + Name: "signal.signal_site_id_fkey", + Columns: []string{"site_id"}, + Comment: "", + }, + ForeignTable: "site", + ForeignColumns: []string{"id"}, + }, }, Comment: "", @@ -164,11 +182,12 @@ type signalColumns struct { Species column Title column Type column + SiteID column } func (c signalColumns) AsSlice() []column { return []column{ - c.Addressed, c.Addressor, c.Created, c.Creator, c.ID, c.OrganizationID, c.Species, c.Title, c.Type, + c.Addressed, c.Addressor, c.Created, c.Creator, c.ID, c.OrganizationID, c.Species, c.Title, c.Type, c.SiteID, } } @@ -186,11 +205,12 @@ type signalForeignKeys struct { SignalSignalAddressorFkey foreignKey SignalSignalCreatorFkey foreignKey SignalSignalOrganizationIDFkey foreignKey + SignalSignalSiteIDFkey foreignKey } func (f signalForeignKeys) AsSlice() []foreignKey { return []foreignKey{ - f.SignalSignalAddressorFkey, f.SignalSignalCreatorFkey, f.SignalSignalOrganizationIDFkey, + f.SignalSignalAddressorFkey, f.SignalSignalCreatorFkey, f.SignalSignalOrganizationIDFkey, f.SignalSignalSiteIDFkey, } } diff --git a/db/migrations/00119_signal_from_publicreport.sql b/db/migrations/00119_signal_from_publicreport.sql new file mode 100644 index 00000000..6cbe8521 --- /dev/null +++ b/db/migrations/00119_signal_from_publicreport.sql @@ -0,0 +1,4 @@ +-- +goose Up +ALTER TABLE signal ADD COLUMN site_id INTEGER REFERENCES site(id); +-- +goose Down +ALTER TABLE signal DROP COLUMN site_id; diff --git a/db/models/signal.bob.go b/db/models/signal.bob.go index fa3c2610..732ffa4a 100644 --- a/db/models/signal.bob.go +++ b/db/models/signal.bob.go @@ -35,6 +35,7 @@ type Signal struct { Species null.Val[enums.Mosquitospecies] `db:"species" ` Title string `db:"title" ` Type enums.Signaltype `db:"type_" ` + SiteID null.Val[int32] `db:"site_id" ` R signalR `db:"-" ` } @@ -54,12 +55,13 @@ type signalR struct { AddressorUser *User // signal.signal_addressor_fkey CreatorUser *User // signal.signal_creator_fkey Organization *Organization // signal.signal_organization_id_fkey + Site *Site // signal.signal_site_id_fkey } func buildSignalColumns(alias string) signalColumns { return signalColumns{ ColumnsExpr: expr.NewColumnsExpr( - "addressed", "addressor", "created", "creator", "id", "organization_id", "species", "title", "type_", + "addressed", "addressor", "created", "creator", "id", "organization_id", "species", "title", "type_", "site_id", ).WithParent("signal"), tableAlias: alias, Addressed: psql.Quote(alias, "addressed"), @@ -71,6 +73,7 @@ func buildSignalColumns(alias string) signalColumns { Species: psql.Quote(alias, "species"), Title: psql.Quote(alias, "title"), Type: psql.Quote(alias, "type_"), + SiteID: psql.Quote(alias, "site_id"), } } @@ -86,6 +89,7 @@ type signalColumns struct { Species psql.Expression Title psql.Expression Type psql.Expression + SiteID psql.Expression } func (c signalColumns) Alias() string { @@ -109,10 +113,11 @@ type SignalSetter struct { Species omitnull.Val[enums.Mosquitospecies] `db:"species" ` Title omit.Val[string] `db:"title" ` Type omit.Val[enums.Signaltype] `db:"type_" ` + SiteID omitnull.Val[int32] `db:"site_id" ` } func (s SignalSetter) SetColumns() []string { - vals := make([]string, 0, 9) + vals := make([]string, 0, 10) if !s.Addressed.IsUnset() { vals = append(vals, "addressed") } @@ -140,6 +145,9 @@ func (s SignalSetter) SetColumns() []string { if s.Type.IsValue() { vals = append(vals, "type_") } + if !s.SiteID.IsUnset() { + vals = append(vals, "site_id") + } return vals } @@ -171,6 +179,9 @@ func (s SignalSetter) Overwrite(t *Signal) { if s.Type.IsValue() { t.Type = s.Type.MustGet() } + if !s.SiteID.IsUnset() { + t.SiteID = s.SiteID.MustGetNull() + } } func (s *SignalSetter) Apply(q *dialect.InsertQuery) { @@ -179,7 +190,7 @@ func (s *SignalSetter) 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, 9) + vals := make([]bob.Expression, 10) if !s.Addressed.IsUnset() { vals[0] = psql.Arg(s.Addressed.MustGetNull()) } else { @@ -234,6 +245,12 @@ func (s *SignalSetter) Apply(q *dialect.InsertQuery) { vals[8] = psql.Raw("DEFAULT") } + if !s.SiteID.IsUnset() { + vals[9] = psql.Arg(s.SiteID.MustGetNull()) + } else { + vals[9] = psql.Raw("DEFAULT") + } + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) } @@ -243,7 +260,7 @@ func (s SignalSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { } func (s SignalSetter) Expressions(prefix ...string) []bob.Expression { - exprs := make([]bob.Expression, 0, 9) + exprs := make([]bob.Expression, 0, 10) if !s.Addressed.IsUnset() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ @@ -308,6 +325,13 @@ func (s SignalSetter) Expressions(prefix ...string) []bob.Expression { }}) } + if !s.SiteID.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "site_id")...), + psql.Arg(s.SiteID), + }}) + } + return exprs } @@ -606,6 +630,30 @@ func (os SignalSlice) Organization(mods ...bob.Mod[*dialect.SelectQuery]) Organi )...) } +// Site starts a query for related objects on site +func (o *Signal) Site(mods ...bob.Mod[*dialect.SelectQuery]) SitesQuery { + return Sites.Query(append(mods, + sm.Where(Sites.Columns.ID.EQ(psql.Arg(o.SiteID))), + )...) +} + +func (os SignalSlice) Site(mods ...bob.Mod[*dialect.SelectQuery]) SitesQuery { + pkSiteID := make(pgtypes.Array[null.Val[int32]], 0, len(os)) + for _, o := range os { + if o == nil { + continue + } + pkSiteID = append(pkSiteID, o.SiteID) + } + PKArgExpr := psql.Select(sm.Columns( + psql.F("unnest", psql.Cast(psql.Arg(pkSiteID), "integer[]")), + )) + + return Sites.Query(append(mods, + sm.Where(psql.Group(Sites.Columns.ID).OP("IN", PKArgExpr)), + )...) +} + func attachSignalAddressorUser0(ctx context.Context, exec bob.Executor, count int, signal0 *Signal, user1 *User) (*Signal, error) { setter := &SignalSetter{ Addressor: omitnull.From(user1.ID), @@ -750,6 +798,54 @@ func (signal0 *Signal) AttachOrganization(ctx context.Context, exec bob.Executor return nil } +func attachSignalSite0(ctx context.Context, exec bob.Executor, count int, signal0 *Signal, site1 *Site) (*Signal, error) { + setter := &SignalSetter{ + SiteID: omitnull.From(site1.ID), + } + + err := signal0.Update(ctx, exec, setter) + if err != nil { + return nil, fmt.Errorf("attachSignalSite0: %w", err) + } + + return signal0, nil +} + +func (signal0 *Signal) InsertSite(ctx context.Context, exec bob.Executor, related *SiteSetter) error { + var err error + + site1, err := Sites.Insert(related).One(ctx, exec) + if err != nil { + return fmt.Errorf("inserting related objects: %w", err) + } + + _, err = attachSignalSite0(ctx, exec, 1, signal0, site1) + if err != nil { + return err + } + + signal0.R.Site = site1 + + site1.R.Signals = append(site1.R.Signals, signal0) + + return nil +} + +func (signal0 *Signal) AttachSite(ctx context.Context, exec bob.Executor, site1 *Site) error { + var err error + + _, err = attachSignalSite0(ctx, exec, 1, signal0, site1) + if err != nil { + return err + } + + signal0.R.Site = site1 + + site1.R.Signals = append(site1.R.Signals, signal0) + + return nil +} + type signalWhere[Q psql.Filterable] struct { Addressed psql.WhereNullMod[Q, time.Time] Addressor psql.WhereNullMod[Q, int32] @@ -760,6 +856,7 @@ type signalWhere[Q psql.Filterable] struct { Species psql.WhereNullMod[Q, enums.Mosquitospecies] Title psql.WhereMod[Q, string] Type psql.WhereMod[Q, enums.Signaltype] + SiteID psql.WhereNullMod[Q, int32] } func (signalWhere[Q]) AliasedAs(alias string) signalWhere[Q] { @@ -777,6 +874,7 @@ func buildSignalWhere[Q psql.Filterable](cols signalColumns) signalWhere[Q] { Species: psql.WhereNull[Q, enums.Mosquitospecies](cols.Species), Title: psql.Where[Q, string](cols.Title), Type: psql.Where[Q, enums.Signaltype](cols.Type), + SiteID: psql.WhereNull[Q, int32](cols.SiteID), } } @@ -818,6 +916,18 @@ func (o *Signal) Preload(name string, retrieved any) error { o.R.Organization = rel + if rel != nil { + rel.R.Signals = SignalSlice{o} + } + return nil + case "Site": + rel, ok := retrieved.(*Site) + if !ok { + return fmt.Errorf("signal cannot load %T as %q", retrieved, name) + } + + o.R.Site = rel + if rel != nil { rel.R.Signals = SignalSlice{o} } @@ -831,6 +941,7 @@ type signalPreloader struct { AddressorUser func(...psql.PreloadOption) psql.Preloader CreatorUser func(...psql.PreloadOption) psql.Preloader Organization func(...psql.PreloadOption) psql.Preloader + Site func(...psql.PreloadOption) psql.Preloader } func buildSignalPreloader() signalPreloader { @@ -874,6 +985,19 @@ func buildSignalPreloader() signalPreloader { }, }, Organizations.Columns.Names(), opts...) }, + Site: func(opts ...psql.PreloadOption) psql.Preloader { + return psql.Preload[*Site, SiteSlice](psql.PreloadRel{ + Name: "Site", + Sides: []psql.PreloadSide{ + { + From: Signals, + To: Sites, + FromColumns: []string{"site_id"}, + ToColumns: []string{"id"}, + }, + }, + }, Sites.Columns.Names(), opts...) + }, } } @@ -881,6 +1005,7 @@ type signalThenLoader[Q orm.Loadable] struct { AddressorUser func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] CreatorUser func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] Organization func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] + Site func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] } func buildSignalThenLoader[Q orm.Loadable]() signalThenLoader[Q] { @@ -893,6 +1018,9 @@ func buildSignalThenLoader[Q orm.Loadable]() signalThenLoader[Q] { type OrganizationLoadInterface interface { LoadOrganization(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } + type SiteLoadInterface interface { + LoadSite(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } return signalThenLoader[Q]{ AddressorUser: thenLoadBuilder[Q]( @@ -913,6 +1041,12 @@ func buildSignalThenLoader[Q orm.Loadable]() signalThenLoader[Q] { return retrieved.LoadOrganization(ctx, exec, mods...) }, ), + Site: thenLoadBuilder[Q]( + "Site", + func(ctx context.Context, exec bob.Executor, retrieved SiteLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadSite(ctx, exec, mods...) + }, + ), } } @@ -1074,3 +1208,58 @@ func (os SignalSlice) LoadOrganization(ctx context.Context, exec bob.Executor, m return nil } + +// LoadSite loads the signal's Site into the .R struct +func (o *Signal) LoadSite(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + // Reset the relationship + o.R.Site = nil + + related, err := o.Site(mods...).One(ctx, exec) + if err != nil { + return err + } + + related.R.Signals = SignalSlice{o} + + o.R.Site = related + return nil +} + +// LoadSite loads the signal's Site into the .R struct +func (os SignalSlice) LoadSite(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + sites, err := os.Site(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, o := range os { + if o == nil { + continue + } + + for _, rel := range sites { + if !o.SiteID.IsValue() { + continue + } + + if !(o.SiteID.IsValue() && o.SiteID.MustGet() == rel.ID) { + continue + } + + rel.R.Signals = append(rel.R.Signals, o) + + o.R.Site = rel + break + } + } + + return nil +} diff --git a/db/models/site.bob.go b/db/models/site.bob.go index 66af17a0..d8c39986 100644 --- a/db/models/site.bob.go +++ b/db/models/site.bob.go @@ -57,6 +57,7 @@ type siteR struct { Features FeatureSlice // feature.feature_site_id_fkey Leads LeadSlice // lead.lead_site_id_fkey Residents ResidentSlice // resident.resident_site_id_fkey + Signals SignalSlice // signal.signal_site_id_fkey Address *Address // site.site_address_id_fkey CreatorUser *User // site.site_creator_id_fkey File *FileuploadFile // site.site_file_id_fkey @@ -701,6 +702,30 @@ func (os SiteSlice) Residents(mods ...bob.Mod[*dialect.SelectQuery]) ResidentsQu )...) } +// Signals starts a query for related objects on signal +func (o *Site) Signals(mods ...bob.Mod[*dialect.SelectQuery]) SignalsQuery { + return Signals.Query(append(mods, + sm.Where(Signals.Columns.SiteID.EQ(psql.Arg(o.ID))), + )...) +} + +func (os SiteSlice) Signals(mods ...bob.Mod[*dialect.SelectQuery]) SignalsQuery { + pkID := make(pgtypes.Array[int32], 0, len(os)) + for _, o := range os { + if o == nil { + continue + } + pkID = append(pkID, o.ID) + } + PKArgExpr := psql.Select(sm.Columns( + psql.F("unnest", psql.Cast(psql.Arg(pkID), "integer[]")), + )) + + return Signals.Query(append(mods, + sm.Where(psql.Group(Signals.Columns.SiteID).OP("IN", PKArgExpr)), + )...) +} + // Address starts a query for related objects on address func (o *Site) Address(mods ...bob.Mod[*dialect.SelectQuery]) AddressesQuery { return Addresses.Query(append(mods, @@ -1001,6 +1026,74 @@ func (site0 *Site) AttachResidents(ctx context.Context, exec bob.Executor, relat return nil } +func insertSiteSignals0(ctx context.Context, exec bob.Executor, signals1 []*SignalSetter, site0 *Site) (SignalSlice, error) { + for i := range signals1 { + signals1[i].SiteID = omitnull.From(site0.ID) + } + + ret, err := Signals.Insert(bob.ToMods(signals1...)).All(ctx, exec) + if err != nil { + return ret, fmt.Errorf("insertSiteSignals0: %w", err) + } + + return ret, nil +} + +func attachSiteSignals0(ctx context.Context, exec bob.Executor, count int, signals1 SignalSlice, site0 *Site) (SignalSlice, error) { + setter := &SignalSetter{ + SiteID: omitnull.From(site0.ID), + } + + err := signals1.UpdateAll(ctx, exec, *setter) + if err != nil { + return nil, fmt.Errorf("attachSiteSignals0: %w", err) + } + + return signals1, nil +} + +func (site0 *Site) InsertSignals(ctx context.Context, exec bob.Executor, related ...*SignalSetter) error { + if len(related) == 0 { + return nil + } + + var err error + + signals1, err := insertSiteSignals0(ctx, exec, related, site0) + if err != nil { + return err + } + + site0.R.Signals = append(site0.R.Signals, signals1...) + + for _, rel := range signals1 { + rel.R.Site = site0 + } + return nil +} + +func (site0 *Site) AttachSignals(ctx context.Context, exec bob.Executor, related ...*Signal) error { + if len(related) == 0 { + return nil + } + + var err error + signals1 := SignalSlice(related) + + _, err = attachSiteSignals0(ctx, exec, len(related), signals1, site0) + if err != nil { + return err + } + + site0.R.Signals = append(site0.R.Signals, signals1...) + + for _, rel := range related { + rel.R.Site = site0 + } + + return nil +} + func attachSiteAddress0(ctx context.Context, exec bob.Executor, count int, site0 *Site, address1 *Address) (*Site, error) { setter := &SiteSetter{ AddressID: omit.From(address1.ID), @@ -1273,6 +1366,20 @@ func (o *Site) Preload(name string, retrieved any) error { o.R.Residents = rels + for _, rel := range rels { + if rel != nil { + rel.R.Site = o + } + } + return nil + case "Signals": + rels, ok := retrieved.(SignalSlice) + if !ok { + return fmt.Errorf("site cannot load %T as %q", retrieved, name) + } + + o.R.Signals = rels + for _, rel := range rels { if rel != nil { rel.R.Site = o @@ -1400,6 +1507,7 @@ type siteThenLoader[Q orm.Loadable] struct { Features func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] Leads func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] Residents func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] + Signals func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] Address func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] CreatorUser func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] File func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] @@ -1416,6 +1524,9 @@ func buildSiteThenLoader[Q orm.Loadable]() siteThenLoader[Q] { type ResidentsLoadInterface interface { LoadResidents(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } + type SignalsLoadInterface interface { + LoadSignals(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } type AddressLoadInterface interface { LoadAddress(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } @@ -1448,6 +1559,12 @@ func buildSiteThenLoader[Q orm.Loadable]() siteThenLoader[Q] { return retrieved.LoadResidents(ctx, exec, mods...) }, ), + Signals: thenLoadBuilder[Q]( + "Signals", + func(ctx context.Context, exec bob.Executor, retrieved SignalsLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadSignals(ctx, exec, mods...) + }, + ), Address: thenLoadBuilder[Q]( "Address", func(ctx context.Context, exec bob.Executor, retrieved AddressLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { @@ -1661,6 +1778,70 @@ func (os SiteSlice) LoadResidents(ctx context.Context, exec bob.Executor, mods . return nil } +// LoadSignals loads the site's Signals into the .R struct +func (o *Site) LoadSignals(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + // Reset the relationship + o.R.Signals = nil + + related, err := o.Signals(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, rel := range related { + rel.R.Site = o + } + + o.R.Signals = related + return nil +} + +// LoadSignals loads the site's Signals into the .R struct +func (os SiteSlice) LoadSignals(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + signals, err := os.Signals(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, o := range os { + if o == nil { + continue + } + + o.R.Signals = nil + } + + for _, o := range os { + if o == nil { + continue + } + + for _, rel := range signals { + + if !rel.SiteID.IsValue() { + continue + } + if !(rel.SiteID.IsValue() && o.ID == rel.SiteID.MustGet()) { + continue + } + + rel.R.Site = o + + o.R.Signals = append(o.R.Signals, rel) + } + } + + return nil +} + // LoadAddress loads the site's Address into the .R struct func (o *Site) LoadAddress(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { if o == nil { diff --git a/html/template/sync/communication-root.html b/html/template/sync/communication-root.html index 2a9efc12..95bbf2bf 100644 --- a/html/template/sync/communication-root.html +++ b/html/template/sync/communication-root.html @@ -154,12 +154,12 @@ } }, - async createLead() { + async createSignal() { try { const payload = { reportID: this.selectedCommunication.id, }; - const response = await fetch(`api/publicreport/lead`, { + const response = await fetch(`api/publicreport/signal`, { method: "POST", headers: { "Content-Type": "application/json", @@ -167,7 +167,7 @@ body: JSON.stringify(payload), }); if (!response.ok) { - throw new Error("Failed to submit lead"); + throw new Error("Failed to submit signal"); } // Remove from list after creating lead this.removeCurrentFromList(); @@ -759,13 +759,16 @@
- +
- - Creates a new service lead from this report + This report is useful signal
@@ -775,7 +778,7 @@ Mark Invalid - Mark as spam, duplicate, or out of district + This report isn't useful
diff --git a/platform/lead.go b/platform/lead.go index 4fb6777d..3ec2c209 100644 --- a/platform/lead.go +++ b/platform/lead.go @@ -2,7 +2,6 @@ package platform import ( "context" - "errors" "fmt" "time" @@ -65,84 +64,6 @@ func LeadCreate(ctx context.Context, user User, signal_id int32, site_id int32, return &lead.ID, nil } -// Create a lead from the given signal and site -func LeadCreateFromPublicreport(ctx context.Context, user User, report_id string) (*int32, error) { - txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil) - defer txn.Rollback(ctx) - if err != nil { - return nil, fmt.Errorf("start transaction: %w", err) - } - - report, err := models.PublicreportReports.Query( - models.SelectWhere.PublicreportReports.PublicID.EQ(report_id), - models.SelectWhere.PublicreportReports.OrganizationID.EQ(user.Organization.ID()), - ).One(ctx, txn) - if err != nil { - return nil, fmt.Errorf("query report existence: %w", err) - } - - // At this point we have a report. We need to decide where to put it based on either the address or - // the location. - var site_id int32 - if report.AddressID.IsValue() { - site, err := siteFromAddress(ctx, txn, user, report.AddressID.MustGet()) - if err != nil { - return nil, fmt.Errorf("site from address: %w", err) - } - site_id = site.ID - } else if report.LocationLatitude.IsValue() && report.LocationLongitude.IsValue() { - site, err := siteFromLocation(ctx, txn, user, Location{ - Latitude: report.LocationLatitude.MustGet(), - Longitude: report.LocationLongitude.MustGet(), - }) - if err != nil { - return nil, fmt.Errorf("site from address: %w", err) - } - site_id = site.ID - - } else if report.AddressRaw != "" { - // At this point we don't have an address, and we don't have GPS - // We'll try geocoding and creating an address from that. - site, err := siteFromAddressRaw(ctx, txn, user, report.AddressRaw) - if err != nil { - return nil, fmt.Errorf("site from address: %w", err) - } - site_id = site.ID - } else { - // We have no structured address, no GPS, no unstructued address. - // There's really nothing we can make this lead from and have it be meaningful - return nil, errors.New("Refusing to create a lead with no location data.") - } - - lead_type := enums.LeadtypeUnknown - switch report.ReportType { - case enums.PublicreportReporttypeNuisance: - lead_type = enums.LeadtypePublicreportNuisance - case enums.PublicreportReporttypeWater: - lead_type = enums.LeadtypePublicreportWater - } - lead, err := models.Leads.Insert(&models.LeadSetter{ - Created: omit.From(time.Now()), - Creator: omit.From(int32(user.ID)), - // ID - OrganizationID: omit.From(int32(user.Organization.ID())), - SiteID: omitnull.From(site_id), - Type: omit.From(lead_type), - }).One(ctx, txn) - _, err = psql.Update( - um.Table(psql.Quote("publicreport", "report")), - um.SetCol("reviewed").ToArg(time.Now()), - um.SetCol("reviewer_id").ToArg(user.ID), - um.SetCol("status").ToArg(enums.PublicreportReportstatustypeReviewed), - um.Where(psql.Quote("public_id").EQ(psql.Arg(report_id))), - ).Exec(ctx, txn) - if err != nil { - return nil, fmt.Errorf("failed to update report %d: %w", report_id, err) - } - txn.Commit(ctx) - - return &lead.ID, nil -} func siteFromAddress(ctx context.Context, txn bob.Tx, user User, address_id int32) (*models.Site, error) { site, err := models.Sites.Query( models.SelectWhere.Sites.AddressID.EQ(address_id), diff --git a/platform/signal.go b/platform/signal.go new file mode 100644 index 00000000..1cc8e06d --- /dev/null +++ b/platform/signal.go @@ -0,0 +1,108 @@ +package platform + +import ( + "context" + "errors" + "fmt" + "time" + + //"github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/Gleipnir-Technology/nidus-sync/db/models" + //"github.com/Gleipnir-Technology/nidus-sync/platform/geocode" + //"github.com/Gleipnir-Technology/nidus-sync/platform/geom" + "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" + //"github.com/rs/zerolog/log" +) + +// Create a lead from the given signal and site +func SignalCreateFromPublicreport(ctx context.Context, user User, report_id string) (*int32, error) { + txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil) + defer txn.Rollback(ctx) + if err != nil { + return nil, fmt.Errorf("start transaction: %w", err) + } + + report, err := models.PublicreportReports.Query( + models.SelectWhere.PublicreportReports.PublicID.EQ(report_id), + models.SelectWhere.PublicreportReports.OrganizationID.EQ(user.Organization.ID()), + ).One(ctx, txn) + if err != nil { + return nil, fmt.Errorf("query report existence: %w", err) + } + + // At this point we have a report. We need to decide where to put it based on either the address or + // the location. + var site_id int32 + if report.AddressID.IsValue() { + site, err := siteFromAddress(ctx, txn, user, report.AddressID.MustGet()) + if err != nil { + return nil, fmt.Errorf("site from address: %w", err) + } + site_id = site.ID + } else if report.LocationLatitude.IsValue() && report.LocationLongitude.IsValue() { + site, err := siteFromLocation(ctx, txn, user, Location{ + Latitude: report.LocationLatitude.MustGet(), + Longitude: report.LocationLongitude.MustGet(), + }) + if err != nil { + return nil, fmt.Errorf("site from address: %w", err) + } + site_id = site.ID + + } else if report.AddressRaw != "" { + // At this point we don't have an address, and we don't have GPS + // We'll try geocoding and creating an address from that. + site, err := siteFromAddressRaw(ctx, txn, user, report.AddressRaw) + if err != nil { + return nil, fmt.Errorf("site from address: %w", err) + } + site_id = site.ID + } else { + // We have no structured address, no GPS, no unstructued address. + // There's really nothing we can make this lead from and have it be meaningful + return nil, errors.New("Refusing to create a lead with no location data.") + } + + var signal_type enums.Signaltype + switch report.ReportType { + case enums.PublicreportReporttypeNuisance: + signal_type = enums.SignaltypePublicreportNuisance + case enums.PublicreportReporttypeWater: + signal_type = enums.SignaltypePublicreportWater + default: + return nil, fmt.Errorf("Unrecognized report type %s", string(report.ReportType)) + } + signal, err := models.Signals.Insert(&models.SignalSetter{ + Addressed: omitnull.FromPtr[time.Time](nil), + Addressor: omitnull.FromPtr[int32](nil), + Created: omit.From(time.Now()), + Creator: omit.From(int32(user.ID)), + // ID + OrganizationID: omit.From(int32(user.Organization.ID())), + Species: omitnull.FromPtr[enums.Mosquitospecies](nil), + SiteID: omitnull.From(site_id), + Title: omit.From[string](""), + Type: omit.From[enums.Signaltype](signal_type), + }).One(ctx, txn) + if err != nil { + return nil, fmt.Errorf("create signal: %w", err) + } + _, err = psql.Update( + um.Table(psql.Quote("publicreport", "report")), + um.SetCol("reviewed").ToArg(time.Now()), + um.SetCol("reviewer_id").ToArg(user.ID), + um.SetCol("status").ToArg(enums.PublicreportReportstatustypeReviewed), + um.Where(psql.Quote("public_id").EQ(psql.Arg(report_id))), + ).Exec(ctx, txn) + if err != nil { + return nil, fmt.Errorf("failed to update report %d: %w", report_id, err) + } + txn.Commit(ctx) + + return &signal.ID, nil +}