diff --git a/api/handler.go b/api/handler.go index c5501f85..3d9d4e1c 100644 --- a/api/handler.go +++ b/api/handler.go @@ -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(¶ms, 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(¶ms, 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(¶ms, 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(¶ms, 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) +} diff --git a/api/routes.go b/api/routes.go index 4c3d3187..74263142 100644 --- a/api/routes.go +++ b/api/routes.go @@ -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") diff --git a/rmo/form.go b/html/form.go similarity index 59% rename from rmo/form.go rename to html/form.go index 66631f81..f5d1a19b 100644 --- a/rmo/form.go +++ b/html/form.go @@ -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" diff --git a/rmo/image-upload.go b/html/image-upload.go similarity index 91% rename from rmo/image-upload.go rename to html/image-upload.go index ef45e356..c145381f 100644 --- a/rmo/image-upload.go +++ b/html/image-upload.go @@ -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) } diff --git a/resource/nuisance.go b/resource/nuisance.go new file mode 100644 index 00000000..f46fe4a0 --- /dev/null +++ b/resource/nuisance.go @@ -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 +} diff --git a/resource/water.go b/resource/water.go new file mode 100644 index 00000000..948b25e7 --- /dev/null +++ b/resource/water.go @@ -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 +} diff --git a/rmo/nuisance.go b/rmo/nuisance.go index 4ae5e349..6673c291 100644 --- a/rmo/nuisance.go +++ b/rmo/nuisance.go @@ -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) -} diff --git a/rmo/report.go b/rmo/report.go index 207b994e..ca9ad8db 100644 --- a/rmo/report.go +++ b/rmo/report.go @@ -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) diff --git a/rmo/routes.go b/rmo/routes.go index 5ed7d44d..170a98b1 100644 --- a/rmo/routes.go +++ b/rmo/routes.go @@ -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") diff --git a/rmo/water.go b/rmo/water.go index bbed67cf..0a9bee57 100644 --- a/rmo/water.go +++ b/rmo/water.go @@ -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) { -} diff --git a/ts/components/MapLocator.vue b/ts/components/MapLocator.vue index 85dcfc7e..9f3fc239 100644 --- a/ts/components/MapLocator.vue +++ b/ts/components/MapLocator.vue @@ -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(); diff --git a/ts/rmo/content/Nuisance.vue b/ts/rmo/content/Nuisance.vue index fc069bcd..189bf734 100644 --- a/ts/rmo/content/Nuisance.vue +++ b/ts/rmo/content/Nuisance.vue @@ -120,10 +120,9 @@ select.tall {
@@ -456,6 +455,13 @@ select.tall { rows="2" placeholder="Describe any other potential breeding sites you've noticed..." > +
+ +
+
+
@@ -497,7 +503,14 @@ select.tall {

-
@@ -517,7 +530,10 @@ import type { Location, Marker } from "@/types"; import type { Address } from "@/type/stadia"; const currentLocation = ref(null); +const errorMessage = ref(""); +const formElement = ref(null); const images = ref([]); +const isSubmitting = ref(false); const marker = ref(null); const showMore = ref(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; + } +}