From ce6c6c1cc1d97651db42779146a801be69a533e4 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 9 Mar 2026 22:59:21 +0000 Subject: [PATCH] Initial render of standing water reports from the public --- api/communication.go | 29 +++- html/template/rmo/water.html | 276 +++++++++++++++++---------------- platform/publicreport/image.go | 56 ++++++- platform/types/image.go | 7 +- rmo/status.go | 38 ++--- rmo/water.go | 16 +- 6 files changed, 246 insertions(+), 176 deletions(-) diff --git a/api/communication.go b/api/communication.go index fb8b7253..b92579bd 100644 --- a/api/communication.go +++ b/api/communication.go @@ -9,6 +9,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/models" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform/publicreport" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" "github.com/google/uuid" //"github.com/rs/zerolog/log" ) @@ -19,22 +20,26 @@ type reporter struct { Name string `json:"name"` } type communication struct { - Created time.Time `json:"created"` - ID string `json:"id"` - PublicReport publicreport.Nuisance `json:"public_report"` - Type string `json:"type"` + Created time.Time `json:"created"` + ID string `json:"id"` + PublicReport types.PublicReport `json:"public_report"` + Type string `json:"type"` } type contentListCommunication struct { Communications []communication `json:"communications"` } func listCommunication(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, query queryParams) (*contentListCommunication, *nhttp.ErrorWithStatus) { - reports, err := publicreport.NuisanceReportForOrganization(ctx, org.ID) + nreports, err := publicreport.NuisanceReportForOrganization(ctx, org.ID) if err != nil { - return nil, nhttp.NewError("query report: %w", err) + return nil, nhttp.NewError("nuisance report query: %w", err) } - comms := make([]communication, len(reports)) - for i, report := range reports { + wreports, err := publicreport.WaterReportForOrganization(ctx, org.ID) + if err != nil { + return nil, nhttp.NewError("water report query: %w", err) + } + comms := make([]communication, len(nreports)+len(wreports)) + for i, report := range nreports { comms[i] = communication{ Created: report.Created, ID: report.PublicID, @@ -42,6 +47,14 @@ func listCommunication(ctx context.Context, r *http.Request, org *models.Organiz Type: "nuisance", } } + for i, report := range wreports { + comms[i+len(nreports)] = communication{ + Created: report.Created, + ID: report.PublicID, + PublicReport: report, + Type: "water", + } + } return &contentListCommunication{ Communications: comms, }, nil diff --git a/html/template/rmo/water.html b/html/template/rmo/water.html index 1b02eafb..5ee65482 100644 --- a/html/template/rmo/water.html +++ b/html/template/rmo/water.html @@ -27,143 +27,152 @@ - + {{ end }} {{ define "content" }} {{ if (eq .District nil) }} @@ -283,7 +292,6 @@ document.addEventListener('DOMContentLoaded', function() {
diff --git a/platform/publicreport/image.go b/platform/publicreport/image.go index b9f10a29..5a4737cb 100644 --- a/platform/publicreport/image.go +++ b/platform/publicreport/image.go @@ -28,7 +28,7 @@ LEFT JOIN publicreport.image_exif e ON i.id = e.image_id WHERE i.id IN (1, 2, 3, 4) GROUP BY i.id; */ -// Get all the images that belong to the list of report IDs +// Get all the images that belong to the list of nuisance report IDs func loadImagesForReportNuisance(ctx context.Context, org_id int32, report_ids []int32) (results map[int32][]types.Image, err error) { rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( sm.Columns( @@ -39,7 +39,7 @@ func loadImagesForReportNuisance(ctx context.Context, org_id int32, report_ids [ "COALESCE(MAX(e.value) FILTER (WHERE e.name = 'Make'), '') AS exif_make", "COALESCE(MAX(e.value) FILTER (WHERE e.name = 'Model'), '') AS exif_model", "COALESCE(MAX(e.value) FILTER (WHERE e.name = 'DateTime'), '') AS exif_datetime", - "ni.nuisance_id AS nuisance_id", + "ni.nuisance_id AS report_id", ), sm.From("publicreport.image").As("i"), sm.LeftJoin("publicreport.image_exif").As("e").OnEQ( @@ -66,12 +66,60 @@ func loadImagesForReportNuisance(ctx context.Context, org_id int32, report_ids [ } results = make(map[int32][]types.Image, len(report_ids)) for _, row := range rows { - r, ok := results[row.NuisanceID] + r, ok := results[row.ReportID] if !ok { r = make([]types.Image, 0) } r = append(r, row) - results[row.NuisanceID] = r + results[row.ReportID] = r + } + return results, nil +} + +// Get all the images that belong to the list of water report IDs +func loadImagesForReportWater(ctx context.Context, org_id int32, report_ids []int32) (results map[int32][]types.Image, err error) { + rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( + sm.Columns( + "i.storage_uuid AS uuid", + "COALESCE(ST_X(i.location), 0) AS \"location.longitude\"", + "COALESCE(ST_Y(i.location), 0) AS \"location.latitude\"", + "ST_Distance(i.location::geography, w.location::geography) AS \"distance_from_reporter_meters\"", + "COALESCE(MAX(e.value) FILTER (WHERE e.name = 'Make'), '') AS exif_make", + "COALESCE(MAX(e.value) FILTER (WHERE e.name = 'Model'), '') AS exif_model", + "COALESCE(MAX(e.value) FILTER (WHERE e.name = 'DateTime'), '') AS exif_datetime", + "wi.water_id AS report_id", + ), + sm.From("publicreport.image").As("i"), + sm.LeftJoin("publicreport.image_exif").As("e").OnEQ( + psql.Quote("i", "id"), + psql.Quote("e", "image_id"), + ), + sm.InnerJoin("publicreport.water_image").As("wi").OnEQ( + psql.Quote("wi", "image_id"), + psql.Quote("i", "id"), + ), + sm.InnerJoin("publicreport.water").As("w").OnEQ( + psql.Quote("wi", "water_id"), + psql.Quote("w", "id"), + ), + sm.Where(psql.Quote("wi", "water_id").EQ(psql.Any(report_ids))), + sm.GroupBy( + //psql.Quote("i", "id"), + //psql.Quote("ni", "nuisance_id"), + psql.Raw("i.id, wi.water_id, w.location"), + ), + ), scan.StructMapper[types.Image]()) + if err != nil { + return nil, fmt.Errorf("get images: %w", err) + } + results = make(map[int32][]types.Image, len(report_ids)) + for _, row := range rows { + r, ok := results[row.ReportID] + if !ok { + r = make([]types.Image, 0) + } + r = append(r, row) + results[row.ReportID] = r } return results, nil } diff --git a/platform/types/image.go b/platform/types/image.go index 30987ce1..1009d4b0 100644 --- a/platform/types/image.go +++ b/platform/types/image.go @@ -7,6 +7,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/google/uuid" + //"github.com/rs/zerolog/log" ) type Exif struct { @@ -35,13 +36,13 @@ func (e Exif) MarshalJSON() ([]byte, error) { } type Image struct { - DistanceToReporterMeters float64 `db:"distance_from_reporter_meters"` + DistanceToReporterMeters *float64 `db:"distance_from_reporter_meters"` Exif Exif `db:"-" json:"exif"` ExifMake string `db:"exif_make" json:"-"` ExifModel string `db:"exif_model" json:"-"` ExifDateTime string `db:"exif_datetime" json:"-"` Location Location `db:"location"` - NuisanceID int32 `db:"nuisance_id"` + ReportID int32 `db:"report_id" json:"-"` URLContent string `db:"-" json:"url_content"` UUID uuid.UUID `db:"uuid"` } @@ -55,7 +56,7 @@ func (i *Image) MarshalJSON() ([]byte, error) { Model: i.ExifModel, } to_marshal["location"] = i.Location - to_marshal["nuisance_id"] = i.NuisanceID + //to_marshal["report_id"] = i.ReportID to_marshal["url_content"] = config.MakeURLNidus("/api/image/%s/content", i.UUID.String()) to_marshal["uuid"] = i.UUID diff --git a/rmo/status.go b/rmo/status.go index eb320da9..a45cb252 100644 --- a/rmo/status.go +++ b/rmo/status.go @@ -202,20 +202,20 @@ func contentFromNuisance(ctx context.Context, report_id string) (result ContentS return result, err } func contentFromWater(ctx context.Context, report_id string) (result ContentStatusByID, err error) { - pool, err := models.PublicreportWaters.Query( + water, err := models.PublicreportWaters.Query( models.SelectWhere.PublicreportWaters.PublicID.EQ(report_id), ).One(ctx, db.PGInstance.BobDB) if err != nil { - return result, fmt.Errorf("Failed to query pool %s: %w", report_id, err) + return result, fmt.Errorf("Failed to query water %s: %w", report_id, err) } - images, err := sql.PublicreportImageWithJSONByWaterID(pool.ID).All(ctx, db.PGInstance.BobDB) + images, err := sql.PublicreportImageWithJSONByWaterID(water.ID).All(ctx, db.PGInstance.BobDB) if err != nil { return result, fmt.Errorf("Failed to get images %s: %w", report_id, err) } - if !pool.OrganizationID.IsNull() { - org_id := pool.OrganizationID.MustGet() + if !water.OrganizationID.IsNull() { + org_id := water.OrganizationID.MustGet() org, err := models.FindOrganization(ctx, db.PGInstance.BobDB, org_id) if err != nil { return result, fmt.Errorf("Failed to get district %d information: %w", org_id, err) @@ -224,44 +224,44 @@ func contentFromWater(ctx context.Context, report_id string) (result ContentStat } result.Report.ID = report_id - result.Report.Address = pool.AddressRaw - result.Report.Created = pool.Created + result.Report.Address = water.AddressRaw + result.Report.Created = water.Created result.Report.ImageCount = len(images) - result.Report.Status = strings.Title(pool.Status.String()) + result.Report.Status = strings.Title(water.Status.String()) result.Report.Type = "Mosquito Nuisance" result.Report.Details = []DetailEntry{ DetailEntry{ Name: "Has a gate that affects access?", - Value: strconv.FormatBool(pool.AccessGate), + Value: strconv.FormatBool(water.AccessGate), }, DetailEntry{ Name: "Has dog that affects access?", - Value: strconv.FormatBool(pool.AccessDog), + Value: strconv.FormatBool(water.AccessDog), }, DetailEntry{ Name: "Has a fence that affects access?", - Value: strconv.FormatBool(pool.AccessFence), + Value: strconv.FormatBool(water.AccessFence), }, DetailEntry{ Name: "Has a locked entrace that affects access?", - Value: strconv.FormatBool(pool.AccessLocked), + Value: strconv.FormatBool(water.AccessLocked), }, DetailEntry{ Name: "Reporter observed larvae (wigglers)?", - Value: strconv.FormatBool(pool.HasLarvae), + Value: strconv.FormatBool(water.HasLarvae), }, DetailEntry{ Name: "Reporter observed pupae (tumblers)?", - Value: strconv.FormatBool(pool.HasPupae), + Value: strconv.FormatBool(water.HasPupae), }, DetailEntry{ Name: "Reporter observed adult mosquitoes?", - Value: strconv.FormatBool(pool.HasAdult), + Value: strconv.FormatBool(water.HasAdult), }, } result.Timeline = []TimelineEntry{ TimelineEntry{ - At: pool.Created, + At: water.Created, Detail: "Initial report was submitted", Title: "Created", }, @@ -273,11 +273,11 @@ func contentFromWater(ctx context.Context, report_id string) (result ContentStat sm.Columns( psql.F("ST_AsGeoJSON", "location"), ), - sm.From("publicreport.pool"), + sm.From("publicreport.water"), sm.Where(psql.Quote("public_id").EQ(psql.Arg(report_id))), ), scan.SingleColumnMapper[string]) if err != nil { - return result, fmt.Errorf("Failed to query pool %s: %w", report_id, err) + return result, fmt.Errorf("Failed to query water %s: %w", report_id, err) } result.Report.Location = location @@ -299,7 +299,7 @@ func getStatusByID(w http.ResponseWriter, r *http.Request) { switch location.TableName.MustGet() { case "nuisance": content, err = contentFromNuisance(ctx, report_id) - case "pool": + case "water": content, err = contentFromWater(ctx, report_id) } if err != nil { diff --git a/rmo/water.go b/rmo/water.go index 9e523718..fa9bbe9d 100644 --- a/rmo/water.go +++ b/rmo/water.go @@ -81,7 +81,7 @@ func postWater(w http.ResponseWriter, r *http.Request) { latlng, err := parseLatLng(r) if err != nil { - respondError(w, "Failed to parse lat lng for pool report", err, http.StatusInternalServerError) + respondError(w, "Failed to parse lat lng for water report", err, http.StatusInternalServerError) return } @@ -92,7 +92,7 @@ func postWater(w http.ResponseWriter, r *http.Request) { } public_id, err := report.GenerateReportID() if err != nil { - respondError(w, "Failed to create pool report public ID", err, http.StatusInternalServerError) + respondError(w, "Failed to create water report public ID", err, http.StatusInternalServerError) return } @@ -156,7 +156,7 @@ func postWater(w http.ResponseWriter, r *http.Request) { ReporterPhone: omit.From(""), Status: omit.From(enums.PublicreportReportstatustypeReported), } - pool, err := models.PublicreportWaters.Insert(&setter).One(ctx, tx) + water, err := models.PublicreportWaters.Insert(&setter).One(ctx, tx) if err != nil { respondError(w, "Failed to create database record", err, http.StatusInternalServerError) return @@ -164,22 +164,22 @@ func postWater(w http.ResponseWriter, r *http.Request) { if geospatial.Populated { _, err = psql.Update( - um.Table("publicreport.pool"), + um.Table("publicreport.water"), um.SetCol("h3cell").ToArg(geospatial.Cell), um.SetCol("location").To(geospatial.GeometryQuery), - um.Where(psql.Quote("id").EQ(psql.Arg(pool.ID))), + um.Where(psql.Quote("id").EQ(psql.Arg(water.ID))), ).Exec(ctx, tx) if err != nil { - respondError(w, "Failed to update publicreport.pool geospatial", err, http.StatusInternalServerError) + respondError(w, "Failed to update publicreport.water geospatial", err, http.StatusInternalServerError) return } } - log.Info().Int32("id", pool.ID).Str("public_id", pool.PublicID).Msg("Created pool report") + log.Info().Int32("id", water.ID).Str("public_id", water.PublicID).Msg("Created water report") setters := make([]*models.PublicreportWaterImageSetter, 0) for _, image := range images { setters = append(setters, &models.PublicreportWaterImageSetter{ ImageID: omit.From(int32(image.ID)), - WaterID: omit.From(int32(pool.ID)), + WaterID: omit.From(int32(water.ID)), }) } if len(setters) > 0 {