Get initial nuisance and water resources working

This is a straight port of the form-encoded POST submission logic.

It is missing a bunch of data.
This commit is contained in:
Eli Ribble 2026-04-03 22:04:22 +00:00
parent 597aedc2af
commit 10e368c403
No known key found for this signature in database
12 changed files with 507 additions and 461 deletions

View file

@ -8,7 +8,6 @@ import (
"net/http"
"github.com/Gleipnir-Technology/nidus-sync/auth"
"github.com/Gleipnir-Technology/nidus-sync/html"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
@ -18,23 +17,28 @@ import (
"github.com/rs/zerolog/log"
)
type ErrorAPI struct {
Message string `json:"message"`
}
var decoder = schema.NewDecoder()
type handlerFunctionDelete func(context.Context, *http.Request, platform.User) *nhttp.ErrorWithStatus
type handlerFunctionGet[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) (*T, *nhttp.ErrorWithStatus)
type handlerFunctionGetImage func(context.Context, *http.Request, platform.User) (file.Collection, uuid.UUID, *nhttp.ErrorWithStatus)
type handlerFunctionGetSlice[T any] func(context.Context, *http.Request, resource.QueryParams) ([]*T, *nhttp.ErrorWithStatus)
type handlerFunctionGetSliceAuthenticated[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) ([]*T, *nhttp.ErrorWithStatus)
type handlerFunctionPost[RequestType any] func(context.Context, *http.Request, RequestType) (string, *nhttp.ErrorWithStatus)
type handlerFunctionPostAuthenticated[RequestType any, ResponseType any] func(context.Context, *http.Request, platform.User, RequestType) (ResponseType, *nhttp.ErrorWithStatus)
type handlerFunctionPostFormMultipart[RequestType any, ResponseType any] func(context.Context, *http.Request, RequestType) (*ResponseType, *nhttp.ErrorWithStatus)
type handlerFunctionPutAuthenticated[RequestType any] func(context.Context, *http.Request, platform.User, RequestType) (string, *nhttp.ErrorWithStatus)
func authenticatedHandlerDelete(f handlerFunctionDelete) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
ctx := r.Context()
e := f(ctx, r, u)
if e != nil {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err := json.Marshal(ErrorAPI{Message: e.Error()})
if err != nil {
log.Error().Err(err).Msg("failed to marshal error")
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), e.Status)
respondErrorStatus(w, e)
return
}
http.Error(w, "", http.StatusNoContent)
@ -42,39 +46,18 @@ func authenticatedHandlerDelete(f handlerFunctionDelete) http.Handler {
})
}
type handlerFunctionGetImage func(context.Context, *http.Request, platform.User) (file.Collection, uuid.UUID, *nhttp.ErrorWithStatus)
func authenticatedHandlerGetImage(f handlerFunctionGetImage) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
ctx := r.Context()
collection, uid, e := f(ctx, r, u)
if e != nil {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err := json.Marshal(ErrorAPI{Message: e.Error()})
if err != nil {
log.Error().Err(err).Msg("failed to marshal error")
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), e.Status)
respondErrorStatus(w, e)
return
}
file.ImageFileToWriter(collection, uid, w)
})
}
type handlerFunctionGet[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) (*T, *nhttp.ErrorWithStatus)
type wrappedHandler func(http.ResponseWriter, *http.Request)
type contentAuthenticated[T any] struct {
C T
Config html.ContentConfig
User platform.User
}
type ErrorAPI struct {
Message string `json:"message"`
}
func authenticatedHandlerJSON[T any](f handlerFunctionGet[T]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
ctx := r.Context()
@ -82,37 +65,25 @@ func authenticatedHandlerJSON[T any](f handlerFunctionGet[T]) http.Handler {
var params resource.QueryParams
err := decoder.Decode(&params, r.URL.Query())
if err != nil {
log.Error().Err(err).Msg("decode query failure")
http.Error(w, "failed to decode query", http.StatusInternalServerError)
respondErrorStatus(w, nhttp.NewBadRequest("failed to decode query: %w", err))
return
}
resp, e := f(ctx, r, u, params)
w.Header().Set("Content-Type", "application/json")
//log.Info().Str("template", template).Err(e).Msg("handler done")
if e != nil {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err = json.Marshal(ErrorAPI{Message: e.Error()})
if err != nil {
log.Error().Err(err).Msg("failed to marshal error")
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), e.Status)
respondErrorStatus(w, e)
return
}
body, err = json.Marshal(resp)
if err != nil {
log.Error().Err(err).Msg("failed to marshal json")
http.Error(w, "{\"message\": \"failed to marshal json\"}", http.StatusInternalServerError)
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
w.Write(body)
})
}
type handlerFunctionGetSlice[T any] func(context.Context, *http.Request, resource.QueryParams) ([]*T, *nhttp.ErrorWithStatus)
type handlerFunctionGetSliceAuthenticated[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) ([]*T, *nhttp.ErrorWithStatus)
func authenticatedHandlerJSONSlice[T any](f handlerFunctionGetSliceAuthenticated[T]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
ctx := r.Context()
@ -120,71 +91,24 @@ func authenticatedHandlerJSONSlice[T any](f handlerFunctionGetSliceAuthenticated
var params resource.QueryParams
err := decoder.Decode(&params, r.URL.Query())
if err != nil {
log.Error().Err(err).Msg("decode query failure")
http.Error(w, "failed to decode query", http.StatusInternalServerError)
respondErrorStatus(w, nhttp.NewBadRequest("failed to decode query: %w", err))
return
}
resp, e := f(ctx, r, u, params)
w.Header().Set("Content-Type", "application/json")
//log.Info().Str("template", template).Err(e).Msg("handler done")
if e != nil {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err = json.Marshal(ErrorAPI{Message: e.Error()})
if err != nil {
log.Error().Err(err).Msg("failed to marshal error")
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), e.Status)
respondErrorStatus(w, e)
return
}
body, err = json.Marshal(resp)
if err != nil {
log.Error().Err(err).Msg("failed to marshal json")
http.Error(w, "{\"message\": \"failed to marshal json\"}", http.StatusInternalServerError)
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
w.Write(body)
})
}
func handlerJSONSlice[T any](f handlerFunctionGetSlice[T]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var body []byte
var params resource.QueryParams
err := decoder.Decode(&params, r.URL.Query())
if err != nil {
log.Error().Err(err).Msg("decode query failure")
http.Error(w, "failed to decode query", http.StatusInternalServerError)
return
}
resp, e := f(ctx, r, params)
w.Header().Set("Content-Type", "application/json")
//log.Info().Str("template", template).Err(e).Msg("handler done")
if e != nil {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err = json.Marshal(ErrorAPI{Message: e.Error()})
if err != nil {
log.Error().Err(err).Msg("failed to marshal error")
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), e.Status)
return
}
body, err = json.Marshal(resp)
if err != nil {
log.Error().Err(err).Msg("failed to marshal json")
http.Error(w, "{\"message\": \"failed to marshal json\"}", http.StatusInternalServerError)
return
}
w.Write(body)
}
}
type handlerFunctionPost[ReqType any] func(context.Context, *http.Request, ReqType) (string, *nhttp.ErrorWithStatus)
type handlerFunctionPostAuthenticated[RequestType any, ResponseType any] func(context.Context, *http.Request, platform.User, RequestType) (ResponseType, *nhttp.ErrorWithStatus)
func authenticatedHandlerJSONPost[RequestType any, ResponseType any](f handlerFunctionPostAuthenticated[RequestType, ResponseType]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
w.Header().Set("Content-Type", "application/json")
@ -201,20 +125,17 @@ func authenticatedHandlerJSONPost[RequestType any, ResponseType any](f handlerFu
}
body, err := json.Marshal(resp)
if err != nil {
log.Error().Err(err).Msg("failed to marshal json")
http.Error(w, "{\"message\": \"failed to marshal json\"}", http.StatusInternalServerError)
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
w.Write(body)
})
}
type handlerFunctionPutAuthenticated[ReqType any] func(context.Context, *http.Request, platform.User, ReqType) (string, *nhttp.ErrorWithStatus)
func authenticatedHandlerJSONPut[ReqType any](f handlerFunctionPutAuthenticated[ReqType]) http.Handler {
func authenticatedHandlerJSONPut[RequestType any](f handlerFunctionPutAuthenticated[RequestType]) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
w.Header().Set("Content-Type", "application/json")
req, e := parseRequest[ReqType](r)
req, e := parseRequest[RequestType](r)
if e != nil {
serializeError(w, e)
return
@ -233,50 +154,6 @@ func authenticatedHandlerJSONPut[ReqType any](f handlerFunctionPutAuthenticated[
http.Redirect(w, r, path, http.StatusCreated)
})
}
func parseRequest[ReqType any](r *http.Request) (*ReqType, *nhttp.ErrorWithStatus) {
var req ReqType
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, nhttp.NewError("Failed to read body: %w", err)
}
err = json.Unmarshal(body, &req)
if err != nil {
return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "Failed to decode request: %w", err)
}
return &req, nil
}
func serializeError(w http.ResponseWriter, e *nhttp.ErrorWithStatus) {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err := json.Marshal(ErrorAPI{Message: e.Error()})
if err != nil {
log.Error().Err(err).Msg("failed to marshal error")
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), e.Status)
return
}
func handlerJSONPost[ReqType any](f handlerFunctionPost[ReqType]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
req, e := parseRequest[ReqType](r)
if e != nil {
serializeError(w, e)
return
}
ctx := r.Context()
path, e := f(ctx, r, *req)
if e != nil {
return
}
http.Redirect(w, r, path, http.StatusFound)
}
}
type postMultipartResponse struct {
URI string `json:"uri"`
}
func authenticatedHandlerPostMultipart[ResponseType any](f handlerFunctionPostAuthenticated[[]file.Upload, ResponseType], collection file.Collection) http.Handler {
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
@ -312,6 +189,99 @@ func authenticatedHandlerPostMultipart[ResponseType any](f handlerFunctionPostAu
w.Write(body)
})
}
func handlerJSONSlice[T any](f handlerFunctionGetSlice[T]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var body []byte
var params resource.QueryParams
err := decoder.Decode(&params, r.URL.Query())
if err != nil {
respondErrorStatus(w, nhttp.NewBadRequest("failed to decode query: %w", err))
return
}
resp, e := f(ctx, r, params)
w.Header().Set("Content-Type", "application/json")
//log.Info().Str("template", template).Err(e).Msg("handler done")
if e != nil {
respondErrorStatus(w, e)
return
}
body, err = json.Marshal(resp)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
w.Write(body)
}
}
func handlerJSONPost[RequestType any](f handlerFunctionPost[RequestType]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
req, e := parseRequest[RequestType](r)
if e != nil {
serializeError(w, e)
return
}
ctx := r.Context()
path, e := f(ctx, r, *req)
if e != nil {
return
}
http.Redirect(w, r, path, http.StatusFound)
}
}
func handlerFormPost[RequestType any, ResponseType any](f handlerFunctionPostFormMultipart[RequestType, ResponseType]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
err := r.ParseMultipartForm(32 << 12) // 128 MB buffer
if err != nil {
respondErrorStatus(w, nhttp.NewBadRequest("bad form: %w", err))
return
}
var req RequestType
err = decoder.Decode(&req, r.PostForm)
if err != nil {
respondErrorStatus(w, nhttp.NewBadRequest("decode form: %w", err))
return
}
ctx := r.Context()
resp, e := f(ctx, r, req)
if e != nil {
serializeError(w, e)
return
}
body, err := json.Marshal(resp)
if err != nil {
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
return
}
w.Write(body)
}
}
func parseRequest[RequestType any](r *http.Request) (*RequestType, *nhttp.ErrorWithStatus) {
var req RequestType
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, nhttp.NewError("Failed to read body: %w", err)
}
err = json.Unmarshal(body, &req)
if err != nil {
return nil, nhttp.NewErrorStatus(http.StatusBadRequest, "Failed to decode request: %w", err)
}
return &req, nil
}
func serializeError(w http.ResponseWriter, e *nhttp.ErrorWithStatus) {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err := json.Marshal(ErrorAPI{Message: e.Error()})
if err != nil {
log.Error().Err(err).Msg("failed to marshal error")
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), e.Status)
return
}
func respondError(w http.ResponseWriter, status int, format string, args ...any) {
outer_err := fmt.Errorf(format, args...)
body, err := json.Marshal(ErrorAPI{
@ -323,3 +293,13 @@ func respondError(w http.ResponseWriter, status int, format string, args ...any)
}
http.Error(w, string(body), status)
}
func respondErrorStatus(w http.ResponseWriter, e *nhttp.ErrorWithStatus) {
log.Warn().Int("status", e.Status).Err(e).Str("user message", e.Message).Msg("Responding with an error from api")
body, err := json.Marshal(ErrorAPI{Message: e.Error()})
if err != nil {
log.Error().Err(err).Msg("failed to marshal error")
http.Error(w, "{\"message\": \"boom. I can't even tell you what went wrong\"}", http.StatusInternalServerError)
return
}
http.Error(w, string(body), e.Status)
}

View file

@ -34,12 +34,17 @@ func AddRoutes(r *mux.Router) {
r.Handle("/leads", authenticatedHandlerJSON(lead.List)).Methods("GET")
r.Handle("/leads", authenticatedHandlerJSONPost(lead.Create)).Methods("POST")
r.Handle("/mosquito-source", auth.NewEnsureAuth(apiMosquitoSource)).Methods("GET")
r.Handle("/publicreport/invalid", authenticatedHandlerJSONPost(postPublicreportInvalid)).Methods("POST")
r.Handle("/publicreport/signal", authenticatedHandlerJSONPost(postPublicreportSignal)).Methods("POST")
r.Handle("/publicreport/message", authenticatedHandlerJSONPost(postPublicreportMessage)).Methods("POST")
r.Handle("/review/pool", authenticatedHandlerJSONPost(postReviewPool)).Methods("POST")
review_task := resource.ReviewTask(r)
r.Handle("/review-task", authenticatedHandlerJSON(review_task.List)).Methods("GET")
nuisance := resource.Nuisance(router)
r.HandleFunc("/rmo/nuisance", handlerFormPost(nuisance.Create)).Methods("POST")
water := resource.Water(router)
r.HandleFunc("/rmo/water", handlerFormPost(water.Create)).Methods("POST")
r.Handle("/service-request", auth.NewEnsureAuth(apiServiceRequest)).Methods("GET")
session := resource.Session(router)
r.Handle("/session", authenticatedHandlerJSON(session.Get)).Methods("GET").Name("session.get")

View file

@ -1,9 +1,16 @@
package rmo
package html
import (
"net/http"
)
func BoolFromForm(r *http.Request, k string) bool {
s := r.PostFormValue(k)
if s == "on" {
return true
}
return false
}
func postFormBool(r *http.Request, k string) *bool {
v := r.PostFormValue(k)
if v == "" {
@ -17,7 +24,7 @@ func postFormBool(r *http.Request, k string) *bool {
return &result
}
func postFormValueOrNone(r *http.Request, k string) string {
func PostFormValueOrNone(r *http.Request, k string) string {
v := r.PostFormValue(k)
if v == "" {
return "none"

View file

@ -1,4 +1,4 @@
package rmo
package html
import (
"bytes"
@ -16,7 +16,7 @@ import (
"net/http"
)
func extractImageUpload(headers *multipart.FileHeader) (upload platform.ImageUpload, err error) {
func ExtractImageUpload(headers *multipart.FileHeader) (upload platform.ImageUpload, err error) {
f, err := headers.Open()
if err != nil {
return upload, fmt.Errorf("Failed to open header: %w", err)
@ -55,11 +55,11 @@ func extractImageUpload(headers *multipart.FileHeader) (upload platform.ImageUpl
}, nil
}
func extractImageUploads(r *http.Request) (uploads []platform.ImageUpload, err error) {
func ExtractImageUploads(r *http.Request) (uploads []platform.ImageUpload, err error) {
uploads = make([]platform.ImageUpload, 0)
for _, fheaders := range r.MultipartForm.File {
for _, headers := range fheaders {
upload, err := extractImageUpload(headers)
upload, err := ExtractImageUpload(headers)
if err != nil {
return make([]platform.ImageUpload, 0), fmt.Errorf("Failed to extract photo upload: %w", err)
}

194
resource/nuisance.go Normal file
View file

@ -0,0 +1,194 @@
package resource
import (
"context"
"fmt"
"net/http"
"slices"
"strconv"
"time"
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/rs/zerolog/log"
)
func Nuisance(r *router) *nuisanceR {
return &nuisanceR{
router: r,
}
}
type nuisanceR struct {
router *router
}
type nuisance struct {
ID string `json:"id"`
}
type nuisanceForm struct {
AdditionalInfo string `schema:"additional-info"`
AddressRaw string `schema:"address"`
AddressCountry string `schema:"address-country"`
AddressLocality string `schema:"address-locality"`
AddressNumber string `schema:"address-number"`
AddressPostalCode string `schema:"address-postalcode"`
AddressRegion string `schema:"address-region"`
AddressStreet string `schema:"address-street"`
Duration string `schema:"duration"`
Latitude string `schema:"latitude"`
Longitude string `schema:"longitude"`
LatlngAccuracyType string `schema:"latlng-accuracy-type"`
LatlngAccuracyValue string `schema:"latlng-accuracy-value"`
MapZoom string `schema:"map-zoom"`
SourceStagnant bool `schema:"source-stagnant"`
SourceContainer bool `schema:"source-container"`
SourceDescription string `schema:"source-description"`
SourceGutters bool `schema:"source-gutters"`
SourceLocations []string `schema:"source-location"`
TODEarly bool `schema:"tod-early"`
TODDay bool `schema:"tod-day"`
TODEvening bool `schema:"tod-evening"`
TODNight bool `schema:"tod-night"`
}
func parseLatLng(r *http.Request) (platform.LatLng, error) {
result := platform.LatLng{
AccuracyType: enums.PublicreportAccuracytypeNone,
AccuracyValue: 0.0,
Latitude: nil,
Longitude: nil,
MapZoom: 0.0,
}
latitude_str := r.FormValue("latitude")
longitude_str := r.FormValue("longitude")
latlng_accuracy_type_str := r.PostFormValue("latlng-accuracy-type")
latlng_accuracy_value_str := r.PostFormValue("latlng-accuracy-value")
map_zoom_str := r.PostFormValue("map-zoom")
var err error
if latlng_accuracy_type_str != "" {
err := result.AccuracyType.Scan(latlng_accuracy_type_str)
if err != nil {
return result, fmt.Errorf("Failed to parse accuracy type '%s': %w", latlng_accuracy_type_str, err)
}
}
if latlng_accuracy_value_str != "" {
var t float64
t, err = strconv.ParseFloat(latlng_accuracy_value_str, 32)
if err != nil {
return result, fmt.Errorf("Failed to parse latlng_accuracy_value '%s': %w", latlng_accuracy_value_str, err)
}
result.AccuracyValue = float64(t)
}
if latitude_str != "" {
var t float64
t, err = strconv.ParseFloat(latitude_str, 64)
if err != nil {
return result, fmt.Errorf("Failed to parse latitude '%s': %w", latitude_str, err)
}
result.Latitude = &t
}
if longitude_str != "" {
var t float64
t, err := strconv.ParseFloat(longitude_str, 64)
if err != nil {
return result, fmt.Errorf("Failed to parse longitude '%s': %w", longitude_str, err)
}
result.Longitude = &t
}
if map_zoom_str != "" {
var t float64
t, err = strconv.ParseFloat(map_zoom_str, 32)
if err != nil {
return result, fmt.Errorf("Failed to parse map_zoom_str '%s': %w", map_zoom_str, err)
} else {
result.MapZoom = float32(t)
}
}
return result, nil
}
func (res *nuisanceR) Create(ctx context.Context, r *http.Request, n nuisanceForm) (*nuisance, *nhttp.ErrorWithStatus) {
duration := enums.PublicreportNuisancedurationtypeNone
is_location_frontyard := slices.Contains(n.SourceLocations, "frontyard")
is_location_backyard := slices.Contains(n.SourceLocations, "backyard")
is_location_garden := slices.Contains(n.SourceLocations, "garden")
is_location_pool := slices.Contains(n.SourceLocations, "pool-area")
is_location_other := slices.Contains(n.SourceLocations, "other")
latlng, err := parseLatLng(r)
err = duration.Scan(n.Duration)
if err != nil {
log.Warn().Err(err).Str("duration_str", n.Duration).Msg("Failed to interpret 'duration'")
}
uploads, err := html.ExtractImageUploads(r)
log.Info().Int("len", len(uploads)).Msg("extracted uploads")
if err != nil {
return nil, nhttp.NewError("Failed to extract image uploads: %w", err)
}
address := platform.Address{
Country: n.AddressCountry,
Locality: n.AddressLocality,
Number: n.AddressNumber,
PostalCode: n.AddressPostalCode,
Raw: n.AddressRaw,
Region: n.AddressRegion,
Street: n.AddressStreet,
Unit: "",
}
setter_report := models.PublicreportReportSetter{
//AddressID: omitnull.From(latlng.Cell.String()),
AddressRaw: omit.From(address.Raw),
AddressCountry: omit.From(address.Country),
AddressNumber: omit.From(address.Number),
AddressLocality: omit.From(address.Locality),
AddressPostalCode: omit.From(address.PostalCode),
AddressRegion: omit.From(address.Region),
AddressStreet: omit.From(address.Street),
Created: omit.From(time.Now()),
//H3cell: omitnull.From(latlng.Cell.String()),
LatlngAccuracyType: omit.From(latlng.AccuracyType),
LatlngAccuracyValue: omit.From(float32(latlng.AccuracyValue)),
//Location: omitnull.From(fmt.Sprintf("ST_GeometryFromText(Point(%s %s))", longitude, latitude)),
Location: omitnull.FromPtr[string](nil),
MapZoom: omit.From(latlng.MapZoom),
//OrganizationID: omitnull.FromPtr(organization_id),
//PublicID: omit.From(public_id),
ReporterEmail: omit.From(""),
ReporterName: omit.From(""),
ReporterPhone: omit.From(""),
ReportType: omit.From(enums.PublicreportReporttypeNuisance),
Status: omit.From(enums.PublicreportReportstatustypeReported),
}
setter_nuisance := models.PublicreportNuisanceSetter{
AdditionalInfo: omit.From(n.AdditionalInfo),
Duration: omit.From(duration),
IsLocationBackyard: omit.From(is_location_backyard),
IsLocationFrontyard: omit.From(is_location_frontyard),
IsLocationGarden: omit.From(is_location_garden),
IsLocationOther: omit.From(is_location_other),
IsLocationPool: omit.From(is_location_pool),
//ReportID omit.Val[int32]
SourceContainer: omit.From(n.SourceContainer),
SourceDescription: omit.From(n.SourceDescription),
SourceGutter: omit.From(n.SourceGutters),
SourceStagnant: omit.From(n.SourceStagnant),
TodDay: omit.From(n.TODDay),
TodEarly: omit.From(n.TODEarly),
TodEvening: omit.From(n.TODEvening),
TodNight: omit.From(n.TODNight),
}
report, err := platform.ReportNuisanceCreate(ctx, setter_report, setter_nuisance, latlng, address, uploads)
return &nuisance{
ID: report.PublicID,
}, nil
}

125
resource/water.go Normal file
View file

@ -0,0 +1,125 @@
package resource
import (
"context"
"net/http"
"time"
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html"
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/aarondl/opt/omit"
//"github.com/aarondl/opt/omitnull"
//"github.com/rs/zerolog/log"
)
func Water(r *router) *waterR {
return &waterR{
router: r,
}
}
type waterR struct {
router *router
}
type water struct {
ID string `json:"id"`
}
type waterForm struct {
AccessComments string `schema:"access-comments"`
AccessDog bool `schema:"access-dog"`
AccessFence bool `schema:"access-fence"`
AccessGate bool `schema:"access-gate"`
AccessLocked bool `schema:"access-locked"`
AccessOther bool `schema:"access-other"`
AddressRaw string `schema:"address"`
AddressCountry string `schema:"address-country"`
AddressLocality string `schema:"address-locality"`
AddressNumber string `schema:"address-number"`
AddressPostalCode string `schema:"address-postalcode"`
AddressRegion string `schema:"address-region"`
AddressStreet string `schema:"address-street"`
Comments string `schema:"comments"`
HasAdult bool `schema:"has-adult"`
HasBackyardPermission bool `schema:"backyard-permission"`
HasLarvae bool `schema:"has-larvae"`
HasPupae bool `schema:"has-pupae"`
IsReporterConfidential bool `schema:"reporter-confidential"`
IsReporter_owner bool `schema:"property-ownership"`
OwnerEmail string `schema:"owner-email"`
OwnerName string `schema:"owner-name"`
OwnerPhone string `schema:"owner-phone"`
}
func (res *waterR) Create(ctx context.Context, r *http.Request, w waterForm) (*water, *nhttp.ErrorWithStatus) {
latlng, err := parseLatLng(r)
if err != nil {
return nil, nhttp.NewError("Failed to parse lat lng for water report: %w", err)
}
uploads, err := html.ExtractImageUploads(r)
if err != nil {
return nil, nhttp.NewError("Failed to extract image uploads: %w", err)
}
address := platform.Address{
Country: w.AddressCountry,
Locality: w.AddressLocality,
Number: w.AddressNumber,
PostalCode: w.AddressPostalCode,
Raw: w.AddressRaw,
Region: w.AddressRegion,
Street: w.AddressStreet,
Unit: "",
}
setter_report := models.PublicreportReportSetter{
AddressRaw: omit.From(address.Raw),
AddressCountry: omit.From(address.Country),
AddressNumber: omit.From(address.Number),
AddressLocality: omit.From(address.Locality),
AddressPostalCode: omit.From(address.PostalCode),
AddressRegion: omit.From(address.Region),
AddressStreet: omit.From(address.Street),
Created: omit.From(time.Now()),
//H3cell: omitnull.From(geospatial.Cell.String()),
LatlngAccuracyType: omit.From(latlng.AccuracyType),
LatlngAccuracyValue: omit.From(float32(latlng.AccuracyValue)),
//Location: add later
MapZoom: omit.From(latlng.MapZoom),
//OrganizationID: omitnull.FromPtr(organization_id),
//PublicID: omit.From(public_id),
ReporterEmail: omit.From(""),
ReporterName: omit.From(""),
ReporterPhone: omit.From(""),
ReportType: omit.From(enums.PublicreportReporttypeWater),
Status: omit.From(enums.PublicreportReportstatustypeReported),
}
setter_water := models.PublicreportWaterSetter{
AccessComments: omit.From(w.AccessComments),
AccessDog: omit.From(w.AccessDog),
AccessFence: omit.From(w.AccessFence),
AccessGate: omit.From(w.AccessGate),
AccessLocked: omit.From(w.AccessLocked),
AccessOther: omit.From(w.AccessOther),
Comments: omit.From(w.Comments),
HasAdult: omit.From(w.HasAdult),
HasBackyardPermission: omit.From(w.HasBackyardPermission),
HasLarvae: omit.From(w.HasLarvae),
HasPupae: omit.From(w.HasPupae),
IsReporterConfidential: omit.From(w.IsReporterConfidential),
IsReporterOwner: omit.From(w.IsReporter_owner),
OwnerEmail: omit.From(w.OwnerEmail),
OwnerName: omit.From(w.OwnerName),
OwnerPhone: omit.From(w.OwnerPhone),
//ReportID omit.Val[int32]
}
report, err := platform.ReportWaterCreate(ctx, setter_report, setter_water, latlng, address, uploads)
if err != nil {
return nil, nhttp.NewError("Failed to save new report: %w", err)
}
return &water{
ID: report.PublicID,
}, nil
}

View file

@ -3,17 +3,10 @@ package rmo
import (
"fmt"
"net/http"
"slices"
"time"
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/report"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/rs/zerolog/log"
//"github.com/rs/zerolog/log"
)
type ContentNuisance struct {
@ -69,121 +62,3 @@ func getSubmitComplete(w http.ResponseWriter, r *http.Request) {
},
)
}
func postNuisance(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
if err != nil {
respondError(w, "Failed to parse form", err, http.StatusBadRequest)
return
}
additional_info := r.PostFormValue("additional-info")
address_raw := r.PostFormValue("address")
address_country := r.PostFormValue("address-country")
address_locality := r.PostFormValue("address-locality")
address_number := r.PostFormValue("address-number")
address_postal_code := r.PostFormValue("address-postalcode")
address_region := r.PostFormValue("address-region")
address_street := r.PostFormValue("address-street")
duration_str := postFormValueOrNone(r, "duration")
source_stagnant := boolFromForm(r, "source-stagnant")
source_container := boolFromForm(r, "source-container")
source_description := r.PostFormValue("source-description")
source_gutters := boolFromForm(r, "source-gutters")
source_locations := r.Form["source-location"]
tod_early := boolFromForm(r, "tod-early")
tod_day := boolFromForm(r, "tod-day")
tod_evening := boolFromForm(r, "tod-evening")
tod_night := boolFromForm(r, "tod-night")
duration := enums.PublicreportNuisancedurationtypeNone
is_location_frontyard := false
is_location_backyard := false
is_location_garden := false
is_location_pool := false
is_location_other := false
latlng, err := parseLatLng(r)
err = duration.Scan(duration_str)
if err != nil {
log.Warn().Err(err).Str("duration_str", duration_str).Msg("Failed to interpret 'duration'")
}
//log.Debug().Strs("source_locations", source_locations).Msg("parsing")
if slices.Contains(source_locations, "backyard") {
is_location_backyard = true
}
if slices.Contains(source_locations, "frontyard") {
is_location_frontyard = true
}
if slices.Contains(source_locations, "garden") {
is_location_garden = true
}
if slices.Contains(source_locations, "other") {
is_location_other = true
}
if slices.Contains(source_locations, "pool-area") {
is_location_pool = true
}
uploads, err := extractImageUploads(r)
log.Info().Int("len", len(uploads)).Msg("extracted uploads")
if err != nil {
respondError(w, "Failed to extract image uploads", err, http.StatusInternalServerError)
return
}
address := platform.Address{
Country: address_country,
Locality: address_locality,
Number: address_number,
PostalCode: address_postal_code,
Raw: address_raw,
Region: address_region,
Street: address_street,
Unit: "",
}
setter_report := models.PublicreportReportSetter{
//AddressID: omitnull.From(latlng.Cell.String()),
AddressRaw: omit.From(address.Raw),
AddressCountry: omit.From(address.Country),
AddressNumber: omit.From(address.Number),
AddressLocality: omit.From(address.Locality),
AddressPostalCode: omit.From(address.PostalCode),
AddressRegion: omit.From(address.Region),
AddressStreet: omit.From(address.Street),
Created: omit.From(time.Now()),
//H3cell: omitnull.From(latlng.Cell.String()),
LatlngAccuracyType: omit.From(latlng.AccuracyType),
LatlngAccuracyValue: omit.From(float32(latlng.AccuracyValue)),
//Location: omitnull.From(fmt.Sprintf("ST_GeometryFromText(Point(%s %s))", longitude, latitude)),
Location: omitnull.FromPtr[string](nil),
MapZoom: omit.From(latlng.MapZoom),
//OrganizationID: omitnull.FromPtr(organization_id),
//PublicID: omit.From(public_id),
ReporterEmail: omit.From(""),
ReporterName: omit.From(""),
ReporterPhone: omit.From(""),
ReportType: omit.From(enums.PublicreportReporttypeNuisance),
Status: omit.From(enums.PublicreportReportstatustypeReported),
}
setter_nuisance := models.PublicreportNuisanceSetter{
AdditionalInfo: omit.From(additional_info),
Duration: omit.From(duration),
IsLocationBackyard: omit.From(is_location_backyard),
IsLocationFrontyard: omit.From(is_location_frontyard),
IsLocationGarden: omit.From(is_location_garden),
IsLocationOther: omit.From(is_location_other),
IsLocationPool: omit.From(is_location_pool),
//ReportID omit.Val[int32]
SourceContainer: omit.From(source_container),
SourceDescription: omit.From(source_description),
SourceGutter: omit.From(source_gutters),
SourceStagnant: omit.From(source_stagnant),
TodDay: omit.From(tod_day),
TodEarly: omit.From(tod_early),
TodEvening: omit.From(tod_evening),
TodNight: omit.From(tod_night),
}
report, err := platform.ReportNuisanceCreate(ctx, setter_report, setter_nuisance, latlng, address, uploads)
http.Redirect(w, r, fmt.Sprintf("/submit-complete?report=%s", report.PublicID), http.StatusFound)
}

View file

@ -2,9 +2,7 @@ package rmo
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
//"github.com/Gleipnir-Technology/nidus-sync/config"
@ -12,8 +10,6 @@ import (
"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/platform"
//"github.com/gorilla/mux"
"github.com/stephenafamo/scan"
//"github.com/rs/zerolog/log"
@ -85,65 +81,6 @@ func getReportSuggestion(w http.ResponseWriter, r *http.Request) {
w.Write(jsonBody)
}
func parseLatLng(r *http.Request) (platform.LatLng, error) {
result := platform.LatLng{
AccuracyType: enums.PublicreportAccuracytypeNone,
AccuracyValue: 0.0,
Latitude: nil,
Longitude: nil,
MapZoom: 0.0,
}
latitude_str := r.FormValue("latitude")
longitude_str := r.FormValue("longitude")
latlng_accuracy_type_str := r.PostFormValue("latlng-accuracy-type")
latlng_accuracy_value_str := r.PostFormValue("latlng-accuracy-value")
map_zoom_str := r.PostFormValue("map-zoom")
var err error
if latlng_accuracy_type_str != "" {
err := result.AccuracyType.Scan(latlng_accuracy_type_str)
if err != nil {
return result, fmt.Errorf("Failed to parse accuracy type '%s': %w", latlng_accuracy_type_str, err)
}
}
if latlng_accuracy_value_str != "" {
var t float64
t, err = strconv.ParseFloat(latlng_accuracy_value_str, 32)
if err != nil {
return result, fmt.Errorf("Failed to parse latlng_accuracy_value '%s': %w", latlng_accuracy_value_str, err)
}
result.AccuracyValue = float64(t)
}
if latitude_str != "" {
var t float64
t, err = strconv.ParseFloat(latitude_str, 64)
if err != nil {
return result, fmt.Errorf("Failed to parse latitude '%s': %w", latitude_str, err)
}
result.Latitude = &t
}
if longitude_str != "" {
var t float64
t, err := strconv.ParseFloat(longitude_str, 64)
if err != nil {
return result, fmt.Errorf("Failed to parse longitude '%s': %w", longitude_str, err)
}
result.Longitude = &t
}
if map_zoom_str != "" {
var t float64
t, err = strconv.ParseFloat(map_zoom_str, 32)
if err != nil {
return result, fmt.Errorf("Failed to parse map_zoom_str '%s': %w", map_zoom_str, err)
} else {
result.MapZoom = float32(t)
}
}
return result, nil
}
func partialSearchParam(p string) string {
result := strings.ReplaceAll(p, "-", "")
result = strings.ToUpper(result)

View file

@ -8,11 +8,7 @@ import (
func Router(r *mux.Router) {
r.HandleFunc("/", getRoot).Methods("GET")
r.HandleFunc("/nuisance", getNuisance).Methods("GET")
r.HandleFunc("/nuisance", postNuisance).Methods("POST")
r.HandleFunc("/submit-complete", getSubmitComplete).Methods("GET")
r.HandleFunc("/water", getWater).Methods("GET")
r.HandleFunc("/water", postWater).Methods("POST")
r.HandleFunc("/district", getDistrictList).Methods("GET")
r.HandleFunc("/district/{slug}", getRootDistrict).Methods("GET")

View file

@ -1,15 +1,9 @@
package rmo
import (
"fmt"
"net/http"
"time"
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/aarondl/opt/omit"
)
type ContentWater struct {
@ -42,108 +36,3 @@ func getWaterDistrict(w http.ResponseWriter, r *http.Request) {
},
)
}
func postWater(w http.ResponseWriter, r *http.Request) {
err := r.ParseMultipartForm(32 << 10) // 32 MB buffer
if err != nil {
respondError(w, "Failed to parse form", err, http.StatusBadRequest)
return
}
access_comments := r.FormValue("access-comments")
access_dog := boolFromForm(r, "access-dog")
access_fence := boolFromForm(r, "access-fence")
access_gate := boolFromForm(r, "access-gate")
access_locked := boolFromForm(r, "access-locked")
access_other := boolFromForm(r, "access-other")
address_raw := r.FormValue("address")
address_country := r.FormValue("address-country")
address_locality := r.FormValue("address-locality")
address_number := r.FormValue("address-number")
address_postal_code := r.FormValue("address-postalcode")
address_region := r.FormValue("address-region")
address_street := r.FormValue("address-street")
comments := r.FormValue("comments")
has_adult := boolFromForm(r, "has-adult")
has_backyard_permission := boolFromForm(r, "backyard-permission")
has_larvae := boolFromForm(r, "has-larvae")
has_pupae := boolFromForm(r, "has-pupae")
is_reporter_confidential := boolFromForm(r, "reporter-confidential")
is_reporter_owner := boolFromForm(r, "property-ownership")
owner_email := r.FormValue("owner-email")
owner_name := r.FormValue("owner-name")
owner_phone := r.FormValue("owner-phone")
latlng, err := parseLatLng(r)
if err != nil {
respondError(w, "Failed to parse lat lng for water report", err, http.StatusInternalServerError)
return
}
ctx := r.Context()
uploads, err := extractImageUploads(r)
if err != nil {
respondError(w, "Failed to extract image uploads", err, http.StatusInternalServerError)
return
}
address := platform.Address{
Country: address_country,
Locality: address_locality,
Number: address_number,
PostalCode: address_postal_code,
Raw: address_raw,
Region: address_region,
Street: address_street,
Unit: "",
}
setter_report := models.PublicreportReportSetter{
AddressRaw: omit.From(address_raw),
AddressCountry: omit.From(address_country),
AddressLocality: omit.From(address_locality),
AddressNumber: omit.From(address_number),
AddressPostalCode: omit.From(address_postal_code),
AddressStreet: omit.From(address_street),
AddressRegion: omit.From(address_region),
Created: omit.From(time.Now()),
//H3cell: omitnull.From(geospatial.Cell.String()),
LatlngAccuracyType: omit.From(latlng.AccuracyType),
LatlngAccuracyValue: omit.From(float32(latlng.AccuracyValue)),
//Location: add later
MapZoom: omit.From(latlng.MapZoom),
//OrganizationID: omitnull.FromPtr(organization_id),
//PublicID: omit.From(public_id),
ReporterEmail: omit.From(""),
ReporterName: omit.From(""),
ReporterPhone: omit.From(""),
ReportType: omit.From(enums.PublicreportReporttypeWater),
Status: omit.From(enums.PublicreportReportstatustypeReported),
}
setter_water := models.PublicreportWaterSetter{
AccessComments: omit.From(access_comments),
AccessDog: omit.From(access_dog),
AccessFence: omit.From(access_fence),
AccessGate: omit.From(access_gate),
AccessLocked: omit.From(access_locked),
AccessOther: omit.From(access_other),
Comments: omit.From(comments),
HasAdult: omit.From(has_adult),
HasBackyardPermission: omit.From(has_backyard_permission),
HasLarvae: omit.From(has_larvae),
HasPupae: omit.From(has_pupae),
IsReporterConfidential: omit.From(is_reporter_confidential),
IsReporterOwner: omit.From(is_reporter_owner),
OwnerEmail: omit.From(owner_email),
OwnerName: omit.From(owner_name),
OwnerPhone: omit.From(owner_phone),
//ReportID omit.Val[int32]
}
report, err := platform.ReportWaterCreate(ctx, setter_report, setter_water, latlng, address, uploads)
if err != nil {
respondError(w, "Failed to save new report", err, http.StatusInternalServerError)
return
}
http.Redirect(w, r, fmt.Sprintf("/submit-complete?report=%s", report.PublicID), http.StatusFound)
}
func postWaterDistrict(w http.ResponseWriter, r *http.Request) {
}

View file

@ -128,7 +128,7 @@ const frameMarkers = () => {
if (props.markers.length === 1) {
// Single marker: pan to it
map.value.panTo(props.markers[0].location, { duration: 1000, zoom: 15 });
map.value.panTo(props.markers[0].location, { duration: 1000 });
} else {
// Multiple markers: fit bounds
const bounds = new maplibregl.LngLatBounds();

View file

@ -120,10 +120,9 @@ select.tall {
<!-- Report Form -->
<form
id="mosquitoNuisanceForm"
action="/api/rmo/nuisance"
method="POST"
@submit.prevent="doSubmit"
enctype="multipart/form-data"
ref="formElement"
>
<!-- Location Section -->
<div class="form-section">
@ -456,6 +455,13 @@ select.tall {
rows="2"
placeholder="Describe any other potential breeding sites you've noticed..."
></textarea>
</div>
</div>
<div class="row">
<div class="col-md-12">
<label for="image" class="form-label"
>Please provide a photo or two of the breeding source</label
>
<ImageUpload v-model="images" />
</div>
</div>
@ -497,7 +503,14 @@ select.tall {
</p>
</div>
<div class="col-md-4 text-md-end mt-3 mt-md-0">
<button type="submit" class="btn btn-primary btn-lg">
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<button
type="submit"
class="btn btn-primary btn-lg"
:disabled="isSubmitting"
>
Submit Report
</button>
</div>
@ -517,7 +530,10 @@ import type { Location, Marker } from "@/types";
import type { Address } from "@/type/stadia";
const currentLocation = ref<Location | null>(null);
const errorMessage = ref("");
const formElement = ref<HTMLFormElement | null>(null);
const images = ref<Image[]>([]);
const isSubmitting = ref(false);
const marker = ref<Marker | null>(null);
const showMore = ref<boolean>(false);
@ -562,4 +578,26 @@ function doMapMarkerDragEnd(location: Location) {
location: location,
};
}
async function doSubmit() {
if (!formElement.value) return;
isSubmitting.value = true;
errorMessage.value = "";
try {
const formData = new FormData(formElement.value);
images.value.forEach((image, index) => {
formData.append(`image[${index}]`, image.file, image.name);
});
await fetch("/api/rmo/nuisance", {
method: "POST",
body: formData,
// Don't set Content-Type, the borwser should do it
});
} catch (error) {
errorMessage.value =
error instanceof Error ? error.message : "Upload failed";
} finally {
isSubmitting.value = false;
}
}
</script>