From 884634a2d785462e4cc04ff73f7562bbe7d5b522 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 9 Mar 2026 15:03:01 +0000 Subject: [PATCH] Split apart comms logic into platform-lever funcs --- api/communication.go | 188 ++--------------------------- api/review_task.go | 27 +++-- api/signal.go | 27 +++-- auth/auth.go | 2 +- platform/publicreport/image.go | 67 ++++++++++ platform/publicreport/nuisance.go | 103 ++++++++++++++++ {api => platform/types}/address.go | 2 +- platform/types/image.go | 14 +++ platform/types/location.go | 6 + 9 files changed, 232 insertions(+), 204 deletions(-) create mode 100644 platform/publicreport/image.go create mode 100644 platform/publicreport/nuisance.go rename {api => platform/types}/address.go (96%) create mode 100644 platform/types/image.go create mode 100644 platform/types/location.go diff --git a/api/communication.go b/api/communication.go index be74a6c6..fb8b7253 100644 --- a/api/communication.go +++ b/api/communication.go @@ -5,18 +5,12 @@ import ( "net/http" "time" - "github.com/Gleipnir-Technology/bob" - "github.com/Gleipnir-Technology/bob/dialect/psql" - "github.com/Gleipnir-Technology/bob/dialect/psql/sm" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/db" "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/google/uuid" - //"github.com/Gleipnir-Technology/nidus-sync/platform" - //"github.com/aarondl/opt/null" - "github.com/rs/zerolog/log" - "github.com/stephenafamo/scan" + //"github.com/rs/zerolog/log" ) type reporter struct { @@ -24,186 +18,28 @@ type reporter struct { HasPhone bool `json:"has_phone"` Name string `json:"name"` } -type publicReport struct { - AdditionalInfo string `json:"additional_info"` - Address Address `json:"address"` - AddressAsGiven string `json:"address_as_given"` - Duration string `json:"duration"` - Images []string `json:"images"` - IsLocationBackyard bool `json:"is_location_backyard"` - IsLocationFrontyard bool `json:"is_location_frontyard"` - IsLocationGarden bool `json:"is_location_garden"` - IsLocationOther bool `json:"is_location_other"` - IsLocationPool bool `json:"is_location_pool"` - Location Location `json:"location"` - Reporter reporter `json:"reporter"` - SourceContainer bool `json:"source_container"` - SourceDescription string `json:"source_description"` - SourceGutter bool `json:"source_gutter"` - SourceStagnant bool `json:"source_stagnant"` - TODDay bool `json:"time_of_day_day"` - TODEarly bool `json:"time_of_day_early"` - TODEvening bool `json:"time_of_day_evening"` - TODNight bool `json:"time_of_day_night"` -} type communication struct { - Created time.Time `json:"created"` - ID string `json:"id"` - PublicReport publicReport `json:"public_report"` - Type string `json:"type"` + Created time.Time `json:"created"` + ID string `json:"id"` + PublicReport publicreport.Nuisance `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) { - type _Report struct { - AdditionalInfo string `db:"additional_info"` - AddressCountry string `db:"address_country"` - AddressAsGiven string `db:"address"` - AddressNumber string `db:"address_number"` - AddressPlace string `db:"address_place"` - AddressPostcode string `db:"address_postcode"` - AddressRegion string `db:"address_region"` - AddressStreet string `db:"address_street"` - Created time.Time `db:"created"` - Duration string `db:"duration"` - IsLocationBackyard bool `db:"is_location_backyard"` - IsLocationFrontyard bool `db:"is_location_frontyard"` - IsLocationGarden bool `db:"is_location_garden"` - IsLocationOther bool `db:"is_location_other"` - IsLocationPool bool `db:"is_location_pool"` - Latitude float64 `db:"latitude"` - Longitude float64 `db:"longitude"` - PublicID string `db:"public_id"` - ReporterEmail *string `db:"reporter_email"` - ReporterName *string `db:"reporter_name"` - ReporterPhone *string `db:"reporter_phone"` - SourceContainer bool `db:"source_container"` - SourceDescription string `db:"source_description"` - SourceGutter bool `db:"source_gutter"` - SourceStagnant bool `db:"source_stagnant"` - TODDay bool `db:"tod_day"` - TODEarly bool `db:"tod_early"` - TODEvening bool `db:"tod_evening"` - TODNight bool `db:"tod_night"` - } - reports, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( - sm.Columns( - "additional_info", - "address", - "address_country", - "address_number", - "address_place", - "address_postcode", - "address_region", - "address_street", - "created", - "duration", - "is_location_backyard", - "is_location_frontyard", - "is_location_garden", - "is_location_other", - "is_location_pool", - "ST_Y(location::geometry::geometry(point, 4326)) AS latitude", - "ST_X(location::geometry::geometry(point, 4326)) AS longitude", - "public_id", - "reporter_email", - "reporter_phone", - "reporter_name", - "source_container", - "source_description", - "source_gutter", - "source_stagnant", - "tod_day", - "tod_early", - "tod_evening", - "tod_night", - ), - sm.From("publicreport.nuisance"), - sm.Where(psql.Quote("publicreport", "nuisance", "organization_id").EQ(psql.Arg(org.ID))), - ), scan.StructMapper[_Report]()) + reports, err := publicreport.NuisanceReportForOrganization(ctx, org.ID) if err != nil { - return nil, nhttp.NewError("get reports: %w", err) - } - type _Row struct { - PublicID string `db:"nuisance_public_id"` - StorageUUID uuid.UUID `db:"storage_uuid"` - } - rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( - sm.Columns( - "n.public_id AS nuisance_public_id", - "i.storage_uuid AS storage_uuid", - ), - sm.From("publicreport.nuisance").As("n"), - sm.InnerJoin("publicreport.nuisance_image").As("ni").OnEQ( - psql.Quote("n", "id"), - psql.Quote("ni", "nuisance_id"), - ), - sm.InnerJoin("publicreport.image").As("i").OnEQ( - psql.Quote("ni", "image_id"), - psql.Quote("i", "id"), - ), - sm.Where(psql.Quote("n", "organization_id").EQ(psql.Arg(org.ID))), - ), scan.StructMapper[_Row]()) - if err != nil { - return nil, nhttp.NewError("get images: %w") - } - id_to_images := make(map[string][]uuid.UUID, len(reports)) - for _, row := range rows { - r, ok := id_to_images[row.PublicID] - if !ok { - r = make([]uuid.UUID, 0) - } - r = append(r, row.StorageUUID) - id_to_images[row.PublicID] = r + return nil, nhttp.NewError("query report: %w", err) } comms := make([]communication, len(reports)) for i, report := range reports { - name := "" - if report.ReporterName != nil { - name = *report.ReporterName - } comms[i] = communication{ - Created: report.Created, - ID: report.PublicID, - PublicReport: publicReport{ - Address: Address{ - Country: report.AddressCountry, - Locality: report.AddressPlace, - Number: report.AddressNumber, - PostalCode: report.AddressPostcode, - Region: report.AddressRegion, - Street: report.AddressStreet, - }, - AddressAsGiven: report.AddressAsGiven, - AdditionalInfo: report.AdditionalInfo, - Duration: report.Duration, - Images: toImageURLs(id_to_images, report.PublicID), - IsLocationBackyard: report.IsLocationBackyard, - IsLocationFrontyard: report.IsLocationFrontyard, - IsLocationGarden: report.IsLocationGarden, - IsLocationOther: report.IsLocationOther, - IsLocationPool: report.IsLocationPool, - Location: Location{ - Latitude: report.Latitude, - Longitude: report.Longitude, - }, - Reporter: reporter{ - Name: name, - HasEmail: report.ReporterEmail != nil, - HasPhone: report.ReporterPhone != nil, - }, - SourceContainer: report.SourceContainer, - SourceDescription: report.SourceDescription, - SourceGutter: report.SourceGutter, - SourceStagnant: report.SourceStagnant, - TODDay: report.TODDay, - TODEarly: report.TODEarly, - TODEvening: report.TODEvening, - TODNight: report.TODNight, - }, - Type: "nuisance", + Created: report.Created, + ID: report.PublicID, + PublicReport: report, + Type: "nuisance", } } return &contentListCommunication{ diff --git a/api/review_task.go b/api/review_task.go index 170e2749..fb902f09 100644 --- a/api/review_task.go +++ b/api/review_task.go @@ -12,12 +12,13 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/models" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" //"github.com/aarondl/opt/null" "github.com/stephenafamo/scan" ) type reviewTaskPool struct { - Address Address `json:"address"` + Address types.Address `json:"address"` Condition string `json:"condition"` Created time.Time `json:"created"` Creator platform.User `json:"creator"` @@ -32,18 +33,18 @@ type contentListReviewTaskPool struct { func listReviewTaskPool(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, query queryParams) (*contentListReviewTaskPool, *nhttp.ErrorWithStatus) { type _Row struct { - Address Address `db:"address"` - Condition string `db:"condition"` - Created time.Time `db:"created"` - CreatorID int32 `db:"creator_id"` - ID int32 `db:"id"` - Latitude float64 `db:"latitude"` - Longitude float64 `db:"longitude"` - Reviewed *time.Time `db:"reviewed"` - ReviewerID *int32 `db:"reviewer_id"` - Species *string `db:"species"` - Title string `db:"title"` - Type string `db:"type"` + Address types.Address `db:"address"` + Condition string `db:"condition"` + Created time.Time `db:"created"` + CreatorID int32 `db:"creator_id"` + ID int32 `db:"id"` + Latitude float64 `db:"latitude"` + Longitude float64 `db:"longitude"` + Reviewed *time.Time `db:"reviewed"` + ReviewerID *int32 `db:"reviewer_id"` + Species *string `db:"species"` + Title string `db:"title"` + Type string `db:"type"` } limit := 20 if query.Limit != nil { diff --git a/api/signal.go b/api/signal.go index 343a81a0..7bed17e4 100644 --- a/api/signal.go +++ b/api/signal.go @@ -12,12 +12,13 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/models" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" //"github.com/aarondl/opt/null" "github.com/stephenafamo/scan" ) type signal struct { - Address Address `json:"address"` + Address types.Address `json:"address"` Addressed *time.Time `json:"addressed"` Addressor *platform.User `json:"addressor"` Created time.Time `json:"created"` @@ -34,18 +35,18 @@ type contentListSignal struct { func listSignal(ctx context.Context, r *http.Request, org *models.Organization, user *models.User, query queryParams) (*contentListSignal, *nhttp.ErrorWithStatus) { type _Row struct { - Address Address `db:"address"` - Addressed *time.Time `db:"addressed"` - Addressor *int32 `db:"addressor"` - Created time.Time `db:"created"` - Creator int32 `db:"creator_id"` - ID int32 `db:"id"` - Latitude float64 `db:"latitude"` - Longitude float64 `db:"longitude"` - Location Location `db:"location"` - Species *string `db:"species"` - Title string `db:"title"` - Type string `db:"type"` + Address types.Address `db:"address"` + Addressed *time.Time `db:"addressed"` + Addressor *int32 `db:"addressor"` + Created time.Time `db:"created"` + Creator int32 `db:"creator_id"` + ID int32 `db:"id"` + Latitude float64 `db:"latitude"` + Longitude float64 `db:"longitude"` + Location Location `db:"location"` + Species *string `db:"species"` + Title string `db:"title"` + Type string `db:"type"` } limit := 20 if query.Limit != nil { diff --git a/auth/auth.go b/auth/auth.go index 6e76a044..e0c97551 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -89,7 +89,7 @@ func (ea *EnsureAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { var msg []byte // Separate return codes for different authentication failures if _, ok := err.(*NoCredentialsError); ok { - fmt.Println("No credentials present and no session") + log.Info().Msg("No credentials present and no session") w.Header().Set("WWW-Authenticate-Error", "no-credentials") msg = []byte("Please provide credentials.\n") } else if _, ok := err.(*NoUserError); ok { diff --git a/platform/publicreport/image.go b/platform/publicreport/image.go new file mode 100644 index 00000000..1a43f5b3 --- /dev/null +++ b/platform/publicreport/image.go @@ -0,0 +1,67 @@ +package publicreport + +import ( + "context" + "fmt" + + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + //"github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/db" + //"github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" + //"github.com/google/uuid" + //"github.com/rs/zerolog/log" + "github.com/stephenafamo/scan" +) + +/* +SELECT + i.*, + MAX(e.value) FILTER (WHERE e.name = 'Make') as exif_make, + MAX(e.value) FILTER (WHERE e.name = 'Model') as exif_model, + MAX(e.value) FILTER (WHERE e.name = 'DateTime') as exif_datetime, + MAX(e.value) FILTER (WHERE e.name = 'GPSLatitude') as exif_gps_lat +FROM publicreport.image i +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 +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( + "i.storage_uuid AS uuid", + "ST_X(location) AS location.longitude", + "ST_Y(location) AS location.latitude", + "MAX(e.value) FILTER (WHERE e.name = 'Make') AS exif_make", + "MAX(e.value) FILTER (WHERE e.name = 'Model') AS exif_model", + "MAX(e.value) FILTER (WHERE e.name = 'DateTime') AS exif_datetime", + "ni.nuisance_id AS nuisance_id", + ), + sm.From("publicreport.image").As("i"), + sm.LeftJoin("publicreport.image_exif").As("e").OnEQ( + psql.Quote("r", "id"), + psql.Quote("e", "image_id"), + ), + sm.InnerJoin("publicreport.nuisance_image").As("ni").OnEQ( + psql.Quote("ni", "image_id"), + psql.Quote("i", "id"), + ), + sm.Where(psql.Quote("ni", "nuisance_id").In(psql.Arg(report_ids))), + ), 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.NuisanceID] + if !ok { + r = make([]types.Image, 0) + } + r = append(r, row) + results[row.NuisanceID] = r + } + return results, nil +} diff --git a/platform/publicreport/nuisance.go b/platform/publicreport/nuisance.go new file mode 100644 index 00000000..905670a0 --- /dev/null +++ b/platform/publicreport/nuisance.go @@ -0,0 +1,103 @@ +package publicreport + +import ( + "context" + "fmt" + "time" + + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + //"github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/platform/types" + //"github.com/Gleipnir-Technology/nidus-sync/db/models" + //"github.com/google/uuid" + //"github.com/rs/zerolog/log" + "github.com/stephenafamo/scan" +) + +type Nuisance struct { + AdditionalInfo string `db:"additional_info"` + Address types.Address `db:"address"` + AddressAsGiven string `db:"address_as_given"` + Created time.Time `db:"created"` + Duration string `db:"duration"` + ID int32 `db:"id"` + Images []types.Image + IsLocationBackyard bool `db:"is_location_backyard"` + IsLocationFrontyard bool `db:"is_location_frontyard"` + IsLocationGarden bool `db:"is_location_garden"` + IsLocationOther bool `db:"is_location_other"` + IsLocationPool bool `db:"is_location_pool"` + Location types.Location `db:"location"` + PublicID string `db:"public_id"` + Reporter Reporter `db:"reporter"` + SourceContainer bool `db:"source_container"` + SourceDescription string `db:"source_description"` + SourceGutter bool `db:"source_gutter"` + SourceStagnant bool `db:"source_stagnant"` + TODDay bool `db:"tod_day"` + TODEarly bool `db:"tod_early"` + TODEvening bool `db:"tod_evening"` + TODNight bool `db:"tod_night"` +} +type Reporter struct { + Email *string `db:"reporter_email"` + Name *string `db:"reporter_name"` + Phone *string `db:"reporter_phone"` +} + +func NuisanceReportForOrganization(ctx context.Context, org_id int32) ([]Nuisance, error) { + reports, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( + sm.Columns( + "additional_info", + "address AS address_as_given", + "address_country AS \"address.country\"", + "address_number AS \"address.number\"", + "address_place AS \"address.place\"", + "address_postcode AS \"address.postcode\"", + "address_region AS \"address.region\"", + "address_street AS \"address.street\"", + "created", + "duration", + "id", + "is_location_backyard", + "is_location_frontyard", + "is_location_garden", + "is_location_other", + "is_location_pool", + "ST_Y(location::geometry::geometry(point, 4326)) AS \"location.latitude\"", + "ST_X(location::geometry::geometry(point, 4326)) AS \"location.longitude\"", + "public_id", + "reporter_email AS \"reporter.email\"", + "reporter_name AS \"reporter.name\"", + "reporter_phone AS \"reporter.phone\"", + "source_container", + "source_description", + "source_gutter", + "source_stagnant", + "tod_day", + "tod_early", + "tod_evening", + "tod_night", + ), + sm.From("publicreport.nuisance"), + sm.Where(psql.Quote("publicreport", "nuisance", "organization_id").EQ(psql.Arg(org_id))), + ), scan.StructMapper[Nuisance]()) + if err != nil { + return nil, fmt.Errorf("get reports: %w", err) + } + report_ids := make([]int32, len(reports)) + for _, report := range reports { + report_ids = append(report_ids, report.ID) + } + images_by_id, err := loadImagesForReportNuisance(ctx, org_id, report_ids) + if err != nil { + return nil, fmt.Errorf("images for report: %w", err) + } + for _, report := range reports { + report.Images = images_by_id[report.ID] + } + return reports, nil +} diff --git a/api/address.go b/platform/types/address.go similarity index 96% rename from api/address.go rename to platform/types/address.go index 3cfa61bc..d2063c1d 100644 --- a/api/address.go +++ b/platform/types/address.go @@ -1,4 +1,4 @@ -package api +package types type Address struct { Country string `db:"country" json:"country"` diff --git a/platform/types/image.go b/platform/types/image.go new file mode 100644 index 00000000..5346a657 --- /dev/null +++ b/platform/types/image.go @@ -0,0 +1,14 @@ +package types + +import ( + "github.com/google/uuid" +) + +type Image struct { + ExifMake string `db:"exif_make"` + ExifModel string `db:"exif_model"` + ExifDateTime string `db:"exif_datetime"` + Location Location `db:"location"` + NuisanceID int32 `db:"nuisance_id"` + UUID uuid.UUID `db:"uuid"` +} diff --git a/platform/types/location.go b/platform/types/location.go new file mode 100644 index 00000000..1844741a --- /dev/null +++ b/platform/types/location.go @@ -0,0 +1,6 @@ +package types + +type Location struct { + Latitude float64 `db:"latitude"` + Longitude float64 `db:"longitude"` +}