Wire up events for creating new public reports
This involved moving a lot of stuff to the platform layer since I don't want event interfaces leaking out. Also this includes a fix to the user authentication which I had previously broken by making a platform-layer user object independent of the database layer.
This commit is contained in:
parent
9a5cc4cf97
commit
e8d865d0ab
24 changed files with 915 additions and 541 deletions
136
api/event.go
136
api/event.go
|
|
@ -7,77 +7,45 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var connectionsSSE map[*ConnectionSSE]bool = make(map[*ConnectionSSE]bool, 0)
|
var connectionsSSE map[*ConnectionSSE]bool = make(map[*ConnectionSSE]bool, 0)
|
||||||
|
|
||||||
func streamEvents(w http.ResponseWriter, r *http.Request, u platform.User) {
|
|
||||||
// Set headers for SSE
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
|
||||||
w.Header().Set("Connection", "keep-alive")
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
|
|
||||||
connection := ConnectionSSE{
|
|
||||||
chanState: make(chan MessageSSE),
|
|
||||||
id: fmt.Sprintf("%d", time.Now().UnixNano()),
|
|
||||||
}
|
|
||||||
connectionsSSE[&connection] = true
|
|
||||||
// Send an initial connected event
|
|
||||||
fmt.Fprintf(w, "event: connected\ndata: {\"status\": \"connected\", \"time\": \"%s\"}\n\n", time.Now().Format(time.RFC3339))
|
|
||||||
w.(http.Flusher).Flush()
|
|
||||||
|
|
||||||
// Keep the connection open with a ticker sending periodic events
|
|
||||||
ticker := time.NewTicker(5 * time.Second)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
// Use a channel to detect when the client disconnects
|
|
||||||
done := r.Context().Done()
|
|
||||||
|
|
||||||
// Keep connection open until client disconnects
|
|
||||||
var err error
|
|
||||||
for {
|
|
||||||
err = nil
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
log.Info().Msg("Client closed connection")
|
|
||||||
return
|
|
||||||
case t := <-ticker.C:
|
|
||||||
// Send a heartbeat message
|
|
||||||
err = connection.SendHeartbeat(w, t)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("Failed to send state from webserver")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MessageHeartbeat struct {
|
|
||||||
Time time.Time `json:"time"`
|
|
||||||
}
|
|
||||||
type MessageSSE struct {
|
|
||||||
Content any `json:"content"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
type ConnectionSSE struct {
|
type ConnectionSSE struct {
|
||||||
chanState chan MessageSSE
|
chanEvent chan platform.Event
|
||||||
id string
|
id uuid.UUID
|
||||||
|
organizationID int32
|
||||||
|
userID int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConnectionSSE) SendMessage(w http.ResponseWriter, m MessageSSE) error {
|
func (c *ConnectionSSE) SendEvent(w http.ResponseWriter, m platform.Event) error {
|
||||||
return send(w, MessageSSE{
|
return send(w, m)
|
||||||
Type: "heartbeat",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
func (c *ConnectionSSE) SendHeartbeat(w http.ResponseWriter, t time.Time) error {
|
func (c *ConnectionSSE) SendHeartbeat(w http.ResponseWriter, t time.Time) error {
|
||||||
return send(w, MessageSSE{
|
return send(w, platform.Event{
|
||||||
Content: MessageHeartbeat{
|
Resource: "clock",
|
||||||
Time: t,
|
Time: t,
|
||||||
},
|
Type: platform.EventTypeHeartbeat,
|
||||||
Type: "heartbeat",
|
URI: "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
func SetEventChannel(chan_envelopes <-chan platform.Envelope) {
|
||||||
|
go func() {
|
||||||
|
for envelope := range chan_envelopes {
|
||||||
|
for conn, _ := range connectionsSSE {
|
||||||
|
if conn.organizationID == envelope.OrganizationID {
|
||||||
|
log.Debug().Int("type", int(envelope.Event.Type)).Int32("env-org", envelope.OrganizationID).Msg("pushed event to client")
|
||||||
|
conn.chanEvent <- envelope.Event
|
||||||
|
} else {
|
||||||
|
log.Debug().Int("type", int(envelope.Event.Type)).Int32("env-org", envelope.OrganizationID).Int32("conn-org", conn.organizationID).Msg("skipped event, bad org")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
func send[T any](w http.ResponseWriter, msg T) error {
|
func send[T any](w http.ResponseWriter, msg T) error {
|
||||||
jsonData, err := json.Marshal(msg)
|
jsonData, err := json.Marshal(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -92,3 +60,55 @@ func send[T any](w http.ResponseWriter, msg T) error {
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func streamEvents(w http.ResponseWriter, r *http.Request, u platform.User) {
|
||||||
|
// Set headers for SSE
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
w.Header().Set("Connection", "keep-alive")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
uid, err := uuid.NewUUID()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to create uuid")
|
||||||
|
}
|
||||||
|
connection := ConnectionSSE{
|
||||||
|
chanEvent: make(chan platform.Event),
|
||||||
|
id: uid,
|
||||||
|
organizationID: u.Organization.ID(),
|
||||||
|
userID: u.ID,
|
||||||
|
}
|
||||||
|
connectionsSSE[&connection] = true
|
||||||
|
log.Debug().Int32("org", u.Organization.ID()).Int("user", u.ID).Str("id", uid.String()).Msg("connected SSE client")
|
||||||
|
|
||||||
|
// Send an initial connected event
|
||||||
|
fmt.Fprintf(w, "event: connected\ndata: {\"status\": \"connected\", \"time\": \"%s\"}\n\n", time.Now().Format(time.RFC3339))
|
||||||
|
w.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
// Keep the connection open with a ticker sending periodic events
|
||||||
|
ticker := time.NewTicker(5 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
// Use a channel to detect when the client disconnects
|
||||||
|
done := r.Context().Done()
|
||||||
|
|
||||||
|
// Keep connection open until client disconnects
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
log.Debug().Int32("org", u.Organization.ID()).Int("user", u.ID).Str("id", uid.String()).Msg("Client closed connection")
|
||||||
|
delete(connectionsSSE, &connection)
|
||||||
|
return
|
||||||
|
case t := <-ticker.C:
|
||||||
|
// Send a heartbeat message
|
||||||
|
err = connection.SendHeartbeat(w, t)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to send heartbeat")
|
||||||
|
}
|
||||||
|
case e := <-connection.chanEvent:
|
||||||
|
err = connection.SendEvent(w, e)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to send heartbeat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ func AddUserSession(r *http.Request, user *platform.User) {
|
||||||
id := strconv.Itoa(int(user.ID))
|
id := strconv.Itoa(int(user.ID))
|
||||||
sessionManager.Put(r.Context(), "user_id", id)
|
sessionManager.Put(r.Context(), "user_id", id)
|
||||||
sessionManager.Put(r.Context(), "username", user.Username)
|
sessionManager.Put(r.Context(), "username", user.Username)
|
||||||
|
log.Debug().Str("id", id).Str("username", user.Username).Msg("added user session")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAuthenticatedUser(r *http.Request) (*platform.User, error) {
|
func GetAuthenticatedUser(r *http.Request) (*platform.User, error) {
|
||||||
|
|
@ -46,7 +47,7 @@ func GetAuthenticatedUser(r *http.Request) (*platform.User, error) {
|
||||||
}
|
}
|
||||||
username := sessionManager.GetString(ctx, "username")
|
username := sessionManager.GetString(ctx, "username")
|
||||||
if user_id > 0 && username != "" {
|
if user_id > 0 && username != "" {
|
||||||
return platform.UserByID(ctx, user_id)
|
return platform.UserByID(ctx, int32(user_id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If we can't get the user from the session try to get from auth headers
|
// If we can't get the user from the session try to get from auth headers
|
||||||
|
|
@ -114,7 +115,7 @@ func SigninUser(r *http.Request, username string, password string) (*platform.Us
|
||||||
func SignoutUser(r *http.Request, user platform.User) {
|
func SignoutUser(r *http.Request, user platform.User) {
|
||||||
sessionManager.Put(r.Context(), "user_id", "")
|
sessionManager.Put(r.Context(), "user_id", "")
|
||||||
sessionManager.Put(r.Context(), "username", "")
|
sessionManager.Put(r.Context(), "username", "")
|
||||||
log.Info().Str("username", user.Username).Int32("user_id", int32(user.ID)).Msg("Ended user session")
|
log.Info().Str("username", user.Username).Int("user_id", (user.ID)).Msg("Ended user session")
|
||||||
}
|
}
|
||||||
|
|
||||||
func SignupUser(ctx context.Context, username string, name string, password string) (*platform.User, error) {
|
func SignupUser(ctx context.Context, username string, name string, password string) (*platform.User, error) {
|
||||||
|
|
@ -148,6 +149,9 @@ func redact(s string) string {
|
||||||
|
|
||||||
func validatePassword(password, hash string) bool {
|
func validatePassword(password, hash string) bool {
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Str("password", password).Str("hash", hash).Msg("!validate password")
|
||||||
|
}
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
120
html/static/js/events.js
Normal file
120
html/static/js/events.js
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
// sse-manager.js - Include this in your common template
|
||||||
|
window.SSEManager = (function () {
|
||||||
|
let eventSource = null;
|
||||||
|
let subscribers = new Map();
|
||||||
|
let isConnected = false;
|
||||||
|
let connectionPromise = null;
|
||||||
|
|
||||||
|
function subscribe(eventType, handler) {
|
||||||
|
if (!subscribers.has(eventType)) {
|
||||||
|
subscribers.set(eventType, []);
|
||||||
|
}
|
||||||
|
subscribers.get(eventType).push(handler);
|
||||||
|
|
||||||
|
// If already connected, attach the listener immediately
|
||||||
|
if (isConnected && eventSource) {
|
||||||
|
eventSource.addEventListener(eventType, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsubscribe(eventType, handler) {
|
||||||
|
if (subscribers.has(eventType)) {
|
||||||
|
const handlers = subscribers.get(eventType);
|
||||||
|
const index = handlers.indexOf(handler);
|
||||||
|
if (index > -1) {
|
||||||
|
handlers.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.removeEventListener(eventType, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect(url) {
|
||||||
|
if (connectionPromise) {
|
||||||
|
return connectionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionPromise = new Promise((resolve, reject) => {
|
||||||
|
eventSource = new EventSource(url);
|
||||||
|
|
||||||
|
eventSource.onopen = function () {
|
||||||
|
isConnected = true;
|
||||||
|
|
||||||
|
// Attach all pre-registered handlers
|
||||||
|
subscribers.forEach((handlers, eventType) => {
|
||||||
|
handlers.forEach((handler) => {
|
||||||
|
eventSource.addEventListener("message", (message) => {
|
||||||
|
const data = JSON.parse(message.data);
|
||||||
|
handler(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("SSE connected");
|
||||||
|
resolve(eventSource);
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.onerror = function (err) {
|
||||||
|
console.error("SSE error:", err);
|
||||||
|
isConnected = false;
|
||||||
|
|
||||||
|
// Reconnect after delay
|
||||||
|
setTimeout(() => {
|
||||||
|
connectionPromise = null;
|
||||||
|
connect(url);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return connectionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
eventSource = null;
|
||||||
|
isConnected = false;
|
||||||
|
connectionPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ready(callback) {
|
||||||
|
if (connectionPromise) {
|
||||||
|
connectionPromise.then(callback);
|
||||||
|
} else {
|
||||||
|
// If connect hasn't been called yet, queue it
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
if (connectionPromise) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
connectionPromise.then(callback);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
subscribe,
|
||||||
|
unsubscribe,
|
||||||
|
ready,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Initialize SSE for navigation notifications
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
SSEManager.connect("/api/events");
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateNotificationBadge(data) {
|
||||||
|
const badge = document.querySelector(".notification-badge");
|
||||||
|
if (badge) {
|
||||||
|
badge.textContent = data.count;
|
||||||
|
badge.style.display = data.count > 0 ? "block" : "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,8 +10,12 @@
|
||||||
<link rel="stylesheet" href="/static/vendor/css/bootstrap-icons.min.css" />
|
<link rel="stylesheet" href="/static/vendor/css/bootstrap-icons.min.css" />
|
||||||
<!-- favicon -->
|
<!-- favicon -->
|
||||||
<link rel="icon" href="/static/favicon-sync.ico" type="image/x-icon" />
|
<link rel="icon" href="/static/favicon-sync.ico" type="image/x-icon" />
|
||||||
|
<script src="/static/js/events.js"></script>
|
||||||
{{ block "extraheader" . }}{{ end }}
|
{{ block "extraheader" . }}{{ end }}
|
||||||
<script>
|
<script>
|
||||||
|
SSEManager.subscribe("*", function (e) {
|
||||||
|
console.log("event", e);
|
||||||
|
});
|
||||||
function restoreLocalStorage() {
|
function restoreLocalStorage() {
|
||||||
const expanded = localStorage.getItem("sidebar.expanded");
|
const expanded = localStorage.getItem("sidebar.expanded");
|
||||||
if (expanded == "false") {
|
if (expanded == "false") {
|
||||||
|
|
@ -60,7 +64,6 @@
|
||||||
setTooltipsForSidebar();
|
setTooltipsForSidebar();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/js/events.js"></script>
|
|
||||||
{{ if not .Config.IsProductionEnvironment }}
|
{{ if not .Config.IsProductionEnvironment }}
|
||||||
<script src="/.flogo/injector.js"></script>
|
<script src="/.flogo/injector.js"></script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,50 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- SSE Testing -->
|
||||||
|
<div class="card mb-5">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<i class="bi bi-bell"></i> Server-sent event testing
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form action="/sudo/sse" method="POST">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="organizationID" class="form-label"
|
||||||
|
>Organization ID</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
id="organization-id"
|
||||||
|
name="organizationID"
|
||||||
|
placeholder="Organization ID"
|
||||||
|
type="text"
|
||||||
|
value="{{ .Organization.ID }}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="content" class="form-label">Content</label>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
id="content"
|
||||||
|
name="content"
|
||||||
|
placeholder="Message content"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-danger">
|
||||||
|
Send SSE<i class="bi bi-send"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-5" />
|
||||||
<!-- Push Notification Testing -->
|
<!-- Push Notification Testing -->
|
||||||
<div class="card mb-5">
|
<div class="card mb-5">
|
||||||
<div class="card-header bg-danger text-white">
|
<div class="card-header bg-danger text-white">
|
||||||
|
|
|
||||||
6
main.go
6
main.go
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/api"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/auth"
|
"github.com/Gleipnir-Technology/nidus-sync/auth"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||||
|
|
@ -162,6 +163,10 @@ func main() {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
chan_envelope := make(chan platform.Envelope, 10)
|
||||||
|
platform.SetEventChannel(chan_envelope)
|
||||||
|
api.SetEventChannel(chan_envelope)
|
||||||
|
|
||||||
// Wait for the interrupt signal to gracefully shut down
|
// Wait for the interrupt signal to gracefully shut down
|
||||||
signalCh := make(chan os.Signal, 1)
|
signalCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
@ -176,6 +181,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
close(chan_envelope)
|
||||||
platform.BackgroundWaitForExit()
|
platform.BackgroundWaitForExit()
|
||||||
|
|
||||||
log.Info().Msg("Shutdown complete")
|
log.Info().Msg("Shutdown complete")
|
||||||
|
|
|
||||||
7
platform/address.go
Normal file
7
platform/address.go
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Address = types.Address
|
||||||
|
|
@ -34,3 +34,38 @@ func DistrictForLocation(ctx context.Context, lng float64, lat float64) (*models
|
||||||
return nil, errors.New("too many organizations")
|
return nil, errors.New("too many organizations")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
func MatchDistrict(ctx context.Context, longitude, latitude *float64, images []ImageUpload) (*int32, error) {
|
||||||
|
var err error
|
||||||
|
var org *models.Organization
|
||||||
|
for _, image := range images {
|
||||||
|
if image.Exif == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if image.Exif.GPS == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
org, err = DistrictForLocation(ctx, image.Exif.GPS.Longitude, image.Exif.GPS.Latitude)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to get district for location")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if org != nil {
|
||||||
|
return &org.ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if longitude == nil || latitude == nil {
|
||||||
|
log.Debug().Msg("No location from images, no latlng for the report itself, cannot match")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
org, err = DistrictForLocation(ctx, *longitude, *latitude)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to get district for location")
|
||||||
|
return nil, fmt.Errorf("Failed to get district for location: %w", err)
|
||||||
|
}
|
||||||
|
if org == nil {
|
||||||
|
log.Debug().Err(err).Float64("lng", *longitude).Float64("lat", *latitude).Msg("No district match by report location")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
log.Debug().Err(err).Int32("org_id", org.ID).Float64("lng", *longitude).Float64("lat", *latitude).Msg("Found district match by report location")
|
||||||
|
return &org.ID, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
27
platform/event.go
Normal file
27
platform/event.go
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Envelope = event.Envelope
|
||||||
|
type Event = event.Event
|
||||||
|
|
||||||
|
const EventTypeHeartbeat = event.EventTypeHeartbeat
|
||||||
|
|
||||||
|
func SetEventChannel(chan_events chan<- Envelope) {
|
||||||
|
event.SetEventChannel(chan_events)
|
||||||
|
}
|
||||||
|
func SudoEvent(org_id int32, content string) {
|
||||||
|
go event.Send(event.Envelope{
|
||||||
|
Event: Event{
|
||||||
|
Resource: "sudo",
|
||||||
|
Time: time.Now(),
|
||||||
|
Type: event.EventTypeSudo,
|
||||||
|
URI: content,
|
||||||
|
},
|
||||||
|
OrganizationID: org_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
68
platform/event/event.go
Normal file
68
platform/event/event.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var chanEvents chan<- Envelope
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Type EventType `json:"type"`
|
||||||
|
URI string `json:"uri"`
|
||||||
|
}
|
||||||
|
type Envelope struct {
|
||||||
|
OrganizationID int32
|
||||||
|
Event Event
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetEventChannel(chan_events chan<- Envelope) {
|
||||||
|
chanEvents = chan_events
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventTypeCreated EventType = iota
|
||||||
|
EventTypeDeleted
|
||||||
|
EventTypeModified
|
||||||
|
EventTypeHeartbeat
|
||||||
|
EventTypeSudo
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResourceType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeRMONuisance = iota
|
||||||
|
TypeRMOWater
|
||||||
|
)
|
||||||
|
|
||||||
|
func Created(type_ ResourceType, organization_id int32, uri_id string) {
|
||||||
|
var resource string
|
||||||
|
var uri string
|
||||||
|
switch type_ {
|
||||||
|
case TypeRMONuisance:
|
||||||
|
resource = "rmo:nuisance"
|
||||||
|
uri = config.MakeURLReport("/report/%s", uri_id)
|
||||||
|
case TypeRMOWater:
|
||||||
|
resource = "rmo:water"
|
||||||
|
uri = config.MakeURLReport("/report/%s", uri_id)
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
go Send(Envelope{
|
||||||
|
Event: Event{
|
||||||
|
Resource: resource,
|
||||||
|
Time: time.Now(),
|
||||||
|
Type: EventTypeCreated,
|
||||||
|
URI: uri,
|
||||||
|
},
|
||||||
|
OrganizationID: organization_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func Send(env Envelope) {
|
||||||
|
chanEvents <- env
|
||||||
|
|
||||||
|
}
|
||||||
148
platform/image.go
Normal file
148
platform/image.go
Normal file
|
|
@ -0,0 +1,148 @@
|
||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
_ "image/gif" // register GIF format
|
||||||
|
_ "image/jpeg" // register JPEG format
|
||||||
|
_ "image/png" // register PNG format
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Gleipnir-Technology/bob"
|
||||||
|
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
||||||
|
"github.com/Gleipnir-Technology/bob/dialect/psql/um"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||||
|
"github.com/aarondl/opt/omit"
|
||||||
|
"github.com/aarondl/opt/omitnull"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/rwcarlsen/goexif/exif"
|
||||||
|
"github.com/rwcarlsen/goexif/tiff"
|
||||||
|
//exif "github.com/rwcarlsen/goexif/exif"
|
||||||
|
//"github.com/dsoprea/go-exif-extra/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GPS struct {
|
||||||
|
Latitude float64
|
||||||
|
Longitude float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExifCollection struct {
|
||||||
|
GPS *GPS
|
||||||
|
Tags map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageUpload struct {
|
||||||
|
Bounds image.Rectangle
|
||||||
|
ContentType string
|
||||||
|
Exif *ExifCollection
|
||||||
|
Format string
|
||||||
|
|
||||||
|
UploadFilesize int
|
||||||
|
UploadFilename string
|
||||||
|
UUID uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ExifCollection) Walk(name exif.FieldName, tag *tiff.Tag) error {
|
||||||
|
e.Tags[string(name)] = tag.String()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func ImageExtractExif(content_type string, file_bytes []byte) (result *ExifCollection, err error) {
|
||||||
|
/*
|
||||||
|
Using "github.com/evanoberholster/imagemeta"
|
||||||
|
meta, err := imagemeta.Decode(bytes.NewReader(file_bytes))
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("Failed to decode image meta: %w", err)
|
||||||
|
}
|
||||||
|
result.GPS = &GPS{
|
||||||
|
Latitude: meta.GPS.Latitude(),
|
||||||
|
Longitude: meta.GPS.Longitude(),
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
*/
|
||||||
|
|
||||||
|
e, err := exif.Decode(bytes.NewReader(file_bytes))
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "exif: failed to find exif intro marker" {
|
||||||
|
return nil, nil
|
||||||
|
} else if errors.Is(err, io.EOF) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Failed to decode image meta: %w", err)
|
||||||
|
}
|
||||||
|
lat, lng, _ := e.LatLong()
|
||||||
|
result = &ExifCollection{
|
||||||
|
GPS: &GPS{
|
||||||
|
Latitude: lat,
|
||||||
|
Longitude: lng,
|
||||||
|
},
|
||||||
|
Tags: make(map[string]string, 0),
|
||||||
|
}
|
||||||
|
err = e.Walk(result)
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveImageUploads(ctx context.Context, tx bob.Tx, uploads []ImageUpload) (models.PublicreportImageSlice, error) {
|
||||||
|
images := make(models.PublicreportImageSlice, 0)
|
||||||
|
for _, u := range uploads {
|
||||||
|
image, err := models.PublicreportImages.Insert(&models.PublicreportImageSetter{
|
||||||
|
ContentType: omit.From(u.ContentType),
|
||||||
|
|
||||||
|
Created: omit.From(time.Now()),
|
||||||
|
//Location: psql.Raw("NULL"),
|
||||||
|
Location: omitnull.FromPtr[string](nil),
|
||||||
|
ResolutionX: omit.From(int32(u.Bounds.Max.X)),
|
||||||
|
ResolutionY: omit.From(int32(u.Bounds.Max.Y)),
|
||||||
|
StorageUUID: omit.From(u.UUID),
|
||||||
|
StorageSize: omit.From(int64(u.UploadFilesize)),
|
||||||
|
UploadedFilename: omit.From(u.UploadFilename),
|
||||||
|
}).One(ctx, tx)
|
||||||
|
if err != nil {
|
||||||
|
return images, fmt.Errorf("Failed to create photo records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: figure out how to do this via the setter...?
|
||||||
|
if u.Exif != nil {
|
||||||
|
if u.Exif.GPS != nil {
|
||||||
|
_, err = psql.Update(
|
||||||
|
um.Table("publicreport.image"),
|
||||||
|
um.SetCol("location").To(fmt.Sprintf("ST_Point(%f, %f, 4326)", u.Exif.GPS.Longitude, u.Exif.GPS.Latitude)),
|
||||||
|
um.Where(psql.Quote("id").EQ(psql.Arg(image.ID))),
|
||||||
|
).Exec(ctx, tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
exif_setters := make([]*models.PublicreportImageExifSetter, 0)
|
||||||
|
for k, v := range u.Exif.Tags {
|
||||||
|
to_save := trimQuotes(v)
|
||||||
|
exif_setters = append(exif_setters, &models.PublicreportImageExifSetter{
|
||||||
|
ImageID: omit.From(image.ID),
|
||||||
|
Name: omit.From(k),
|
||||||
|
Value: omit.From(to_save),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(exif_setters) > 0 {
|
||||||
|
_, err = models.PublicreportImageExifs.Insert(bob.ToMods(exif_setters...)).Exec(ctx, tx)
|
||||||
|
if err != nil {
|
||||||
|
return images, fmt.Errorf("Failed to create photo exif records: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info().Int32("id", image.ID).Int("tags", len(u.Exif.Tags)).Msg("Saved an uploaded file to the database")
|
||||||
|
} else {
|
||||||
|
log.Info().Int32("id", image.ID).Int("tags", 0).Msg("Saved an uploaded file without EXIF data")
|
||||||
|
}
|
||||||
|
images = append(images, image)
|
||||||
|
}
|
||||||
|
return images, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a string like "\"foo\"" return "foo".
|
||||||
|
func trimQuotes(s string) string {
|
||||||
|
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
|
||||||
|
return s[1 : len(s)-1]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
57
platform/latlng.go
Normal file
57
platform/latlng.go
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/uber/h3-go/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LatLng struct {
|
||||||
|
Latitude *float64
|
||||||
|
Longitude *float64
|
||||||
|
MapZoom float32
|
||||||
|
AccuracyValue float64
|
||||||
|
AccuracyType enums.PublicreportAccuracytype
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LatLng) Resolution() uint {
|
||||||
|
switch l.AccuracyType {
|
||||||
|
// These accuracy_type strings come from the Mapbox Geocoding API definition and
|
||||||
|
// are far from scientific
|
||||||
|
case enums.PublicreportAccuracytypeRooftop:
|
||||||
|
return 14
|
||||||
|
case enums.PublicreportAccuracytypeParcel:
|
||||||
|
return 13
|
||||||
|
case enums.PublicreportAccuracytypePoint:
|
||||||
|
return 13
|
||||||
|
case enums.PublicreportAccuracytypeInterpolated:
|
||||||
|
return 12
|
||||||
|
case enums.PublicreportAccuracytypeApproximate:
|
||||||
|
return 11
|
||||||
|
case enums.PublicreportAccuracytypeIntersection:
|
||||||
|
return 10
|
||||||
|
// This is a special indicator that we got our location from the browser measurements
|
||||||
|
case enums.PublicreportAccuracytypeBrowser:
|
||||||
|
return uint(h3utils.MeterAccuracyToH3Resolution(l.AccuracyValue))
|
||||||
|
default:
|
||||||
|
log.Warn().Str("accuracy-type", string(l.AccuracyType)).Msg("unrecognized accuracy type, this indicates either a weird client or misbehaving web page. Defaulting to resolution 13")
|
||||||
|
return 13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (l LatLng) H3Cell() (*h3.Cell, error) {
|
||||||
|
if l.Longitude == nil || l.Latitude == nil {
|
||||||
|
return nil, errors.New("nil lat/lng")
|
||||||
|
}
|
||||||
|
result, err := h3utils.GetCell(*l.Longitude, *l.Latitude, int(l.Resolution()))
|
||||||
|
return &result, err
|
||||||
|
}
|
||||||
|
func (l LatLng) GeometryQuery() (string, error) {
|
||||||
|
if l.Longitude == nil || l.Latitude == nil {
|
||||||
|
return "", errors.New("nil lat/lng")
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("ST_Point(%f, %f, 4326)", *l.Longitude, *l.Latitude), nil
|
||||||
|
}
|
||||||
104
platform/nuisance.go
Normal file
104
platform/nuisance.go
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Gleipnir-Technology/bob"
|
||||||
|
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
||||||
|
"github.com/Gleipnir-Technology/bob/dialect/psql/um"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/event"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/geocode"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/report"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||||
|
"github.com/aarondl/opt/omit"
|
||||||
|
"github.com/aarondl/opt/omitnull"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NuisanceCreate(ctx context.Context, setter models.PublicreportNuisanceSetter, latlng LatLng, address Address, images []ImageUpload) (public_id string, err error) {
|
||||||
|
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create txn: %w", err)
|
||||||
|
}
|
||||||
|
defer txn.Rollback(ctx)
|
||||||
|
|
||||||
|
public_id, err = report.GenerateReportID()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("create public ID: %w", err)
|
||||||
|
}
|
||||||
|
setter.PublicID = omit.From(public_id)
|
||||||
|
|
||||||
|
// If we've got an locality value it was set by geocoding so we should save it
|
||||||
|
var a *models.Address
|
||||||
|
if address.Locality != "" && latlng.Latitude != nil && latlng.Longitude != nil {
|
||||||
|
a, err = geocode.EnsureAddress(ctx, txn, address, types.Location{
|
||||||
|
Latitude: *latlng.Latitude,
|
||||||
|
Longitude: *latlng.Longitude,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to ensure address: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saved_images, err := saveImageUploads(ctx, txn, images)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to save image uploads: %w", err)
|
||||||
|
}
|
||||||
|
var organization_id *int32
|
||||||
|
organization_id, err = MatchDistrict(ctx, latlng.Longitude, latlng.Latitude, images)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to match district")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a != nil {
|
||||||
|
setter.AddressID = omitnull.From(a.ID)
|
||||||
|
}
|
||||||
|
if organization_id != nil {
|
||||||
|
setter.OrganizationID = omitnull.FromPtr(organization_id)
|
||||||
|
}
|
||||||
|
nuisance, err := models.PublicreportNuisances.Insert(&setter).One(ctx, txn)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to create database record: %w", err)
|
||||||
|
}
|
||||||
|
if latlng.Latitude != nil && latlng.Longitude != nil {
|
||||||
|
h3cell, _ := latlng.H3Cell()
|
||||||
|
geom_query, _ := latlng.GeometryQuery()
|
||||||
|
_, err = psql.Update(
|
||||||
|
um.Table("publicreport.nuisance"),
|
||||||
|
um.SetCol("h3cell").ToArg(h3cell),
|
||||||
|
um.SetCol("location").To(geom_query),
|
||||||
|
um.Where(psql.Quote("id").EQ(psql.Arg(nuisance.ID))),
|
||||||
|
).Exec(ctx, txn)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to insert publicreport.nuisance geospatial", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info().Str("public_id", public_id).Int32("id", nuisance.ID).Msg("Created nuisance report")
|
||||||
|
if len(saved_images) > 0 {
|
||||||
|
setters := make([]*models.PublicreportNuisanceImageSetter, 0)
|
||||||
|
for _, image := range saved_images {
|
||||||
|
setters = append(setters, &models.PublicreportNuisanceImageSetter{
|
||||||
|
ImageID: omit.From(int32(image.ID)),
|
||||||
|
NuisanceID: omit.From(int32(nuisance.ID)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_, err = models.PublicreportNuisanceImages.Insert(bob.ToMods(setters...)).Exec(ctx, txn)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to save reference to images: %w", err)
|
||||||
|
}
|
||||||
|
log.Info().Int("len", len(images)).Msg("saved uploaded images")
|
||||||
|
}
|
||||||
|
txn.Commit(ctx)
|
||||||
|
|
||||||
|
if organization_id != nil {
|
||||||
|
event.Created(
|
||||||
|
event.TypeRMONuisance,
|
||||||
|
*organization_id,
|
||||||
|
nuisance.PublicID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nuisance.PublicID, nil
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ type Address struct {
|
||||||
Locality string `db:"locality" json:"locality"`
|
Locality string `db:"locality" json:"locality"`
|
||||||
Number string `db:"number" json:"number"`
|
Number string `db:"number" json:"number"`
|
||||||
PostalCode string `db:"postal_code" json:"postal_code"`
|
PostalCode string `db:"postal_code" json:"postal_code"`
|
||||||
|
Raw string `db:"-" json:"raw"`
|
||||||
Region string `db:"region" json:"region"`
|
Region string `db:"region" json:"region"`
|
||||||
Street string `db:"street" json:"street"`
|
Street string `db:"street" json:"street"`
|
||||||
Unit string `db:"unit" json:"unit"`
|
Unit string `db:"unit" json:"unit"`
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,22 @@ type User struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u User) HasRoot() bool {
|
func (u User) HasRoot() bool {
|
||||||
return u.model.Role != enums.UserroleRoot
|
return u.model.Role == enums.UserroleRoot
|
||||||
|
}
|
||||||
|
func newUser(org Organization, user *models.User) User {
|
||||||
|
return User{
|
||||||
|
DisplayName: user.DisplayName,
|
||||||
|
ID: int(user.ID),
|
||||||
|
Initials: extractInitials(user.DisplayName),
|
||||||
|
Notifications: []Notification{},
|
||||||
|
Organization: org,
|
||||||
|
PasswordHash: user.PasswordHash,
|
||||||
|
PasswordHashType: string(user.PasswordHashType),
|
||||||
|
Role: user.Role.String(),
|
||||||
|
Username: user.Username,
|
||||||
|
|
||||||
|
model: user,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateUser(ctx context.Context, username string, name string, password_hash string) (*User, error) {
|
func CreateUser(ctx context.Context, username string, name string, password_hash string) (*User, error) {
|
||||||
|
|
@ -60,19 +75,11 @@ func CreateUser(ctx context.Context, username string, name string, password_hash
|
||||||
return nil, fmt.Errorf("Failed to create user: %w", err)
|
return nil, fmt.Errorf("Failed to create user: %w", err)
|
||||||
}
|
}
|
||||||
log.Info().Int32("id", user.ID).Str("username", user.Username).Msg("Created user")
|
log.Info().Int32("id", user.ID).Str("username", user.Username).Msg("Created user")
|
||||||
return &User{
|
u := newUser(newOrganization(o), user)
|
||||||
DisplayName: user.DisplayName,
|
return &u, nil
|
||||||
Initials: extractInitials(user.DisplayName),
|
|
||||||
Notifications: []Notification{},
|
|
||||||
Organization: newOrganization(o),
|
|
||||||
Role: user.Role.String(),
|
|
||||||
Username: user.Username,
|
|
||||||
|
|
||||||
model: user,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
func UserByID(ctx context.Context, user_id int) (*User, error) {
|
func UserByID(ctx context.Context, user_id int32) (*User, error) {
|
||||||
return getUser(ctx, models.SelectWhere.Users.ID.EQ(int32(user_id)))
|
return getUser(ctx, models.SelectWhere.Users.ID.EQ(user_id))
|
||||||
}
|
}
|
||||||
func UserByUsername(ctx context.Context, username string) (*User, error) {
|
func UserByUsername(ctx context.Context, username string) (*User, error) {
|
||||||
return getUser(ctx, models.SelectWhere.Users.Username.EQ(username))
|
return getUser(ctx, models.SelectWhere.Users.Username.EQ(username))
|
||||||
|
|
@ -84,15 +91,8 @@ func UsersByOrg(ctx context.Context, org Organization) (map[int32]*User, error)
|
||||||
}
|
}
|
||||||
results := make(map[int32]*User, len(users))
|
results := make(map[int32]*User, len(users))
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
results[user.ID] = &User{
|
u := newUser(org, user)
|
||||||
DisplayName: user.DisplayName,
|
results[user.ID] = &u
|
||||||
Initials: "",
|
|
||||||
Notifications: []Notification{},
|
|
||||||
Organization: org,
|
|
||||||
Role: user.Role.String(),
|
|
||||||
Username: user.Username,
|
|
||||||
model: user,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +102,7 @@ func getUser(ctx context.Context, where mods.Where[*dialect.SelectQuery]) (*User
|
||||||
where,
|
where,
|
||||||
).One(ctx, db.PGInstance.BobDB)
|
).One(ctx, db.PGInstance.BobDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("getUser failed")
|
||||||
if err.Error() == "No such user" || err.Error() == "sql: no rows in result set" {
|
if err.Error() == "No such user" || err.Error() == "sql: no rows in result set" {
|
||||||
return nil, &NoUserError{}
|
return nil, &NoUserError{}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -112,14 +113,8 @@ func getUser(ctx context.Context, where mods.Where[*dialect.SelectQuery]) (*User
|
||||||
}
|
}
|
||||||
org := newOrganization(user.R.Organization)
|
org := newOrganization(user.R.Organization)
|
||||||
|
|
||||||
return &User{
|
u := newUser(org, user)
|
||||||
DisplayName: user.DisplayName,
|
return &u, nil
|
||||||
Initials: extractInitials(user.DisplayName),
|
|
||||||
Notifications: []Notification{},
|
|
||||||
Organization: org,
|
|
||||||
Role: user.Role.String(),
|
|
||||||
Username: user.Username,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
func extractInitials(name string) string {
|
func extractInitials(name string) string {
|
||||||
parts := strings.Fields(name)
|
parts := strings.Fields(name)
|
||||||
|
|
|
||||||
104
platform/water.go
Normal file
104
platform/water.go
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Gleipnir-Technology/bob"
|
||||||
|
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
||||||
|
"github.com/Gleipnir-Technology/bob/dialect/psql/um"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/event"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/geocode"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/report"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||||
|
"github.com/aarondl/opt/omit"
|
||||||
|
"github.com/aarondl/opt/omitnull"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WaterCreate(ctx context.Context, setter models.PublicreportWaterSetter, latlng LatLng, address Address, images []ImageUpload) (public_id string, err error) {
|
||||||
|
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to create transaction: %w", err)
|
||||||
|
}
|
||||||
|
defer txn.Rollback(ctx)
|
||||||
|
public_id, err = report.GenerateReportID()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to create water report public ID", err)
|
||||||
|
}
|
||||||
|
setter.PublicID = omit.From(public_id)
|
||||||
|
|
||||||
|
// If we've got an locality value it was set by geocoding so we should save it
|
||||||
|
var a *models.Address
|
||||||
|
if address.Locality != "" && latlng.Latitude != nil && latlng.Longitude != nil {
|
||||||
|
a, err = geocode.EnsureAddress(ctx, txn, address, types.Location{
|
||||||
|
Latitude: *latlng.Latitude,
|
||||||
|
Longitude: *latlng.Longitude,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to ensure address: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saved_images, err := saveImageUploads(ctx, txn, images)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to save image uploads", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization_id *int32
|
||||||
|
organization_id, err = MatchDistrict(ctx, latlng.Longitude, latlng.Latitude, images)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Msg("Failed to match district")
|
||||||
|
}
|
||||||
|
if a != nil {
|
||||||
|
setter.AddressID = omitnull.From(a.ID)
|
||||||
|
}
|
||||||
|
if organization_id != nil {
|
||||||
|
setter.OrganizationID = omitnull.FromPtr(organization_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
water, err := models.PublicreportWaters.Insert(&setter).One(ctx, txn)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to create database record", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if latlng.Latitude != nil && latlng.Longitude != nil {
|
||||||
|
h3cell, _ := latlng.H3Cell()
|
||||||
|
geom_query, _ := latlng.GeometryQuery()
|
||||||
|
_, err = psql.Update(
|
||||||
|
um.Table("publicreport.water"),
|
||||||
|
um.SetCol("h3cell").ToArg(h3cell),
|
||||||
|
um.SetCol("location").To(geom_query),
|
||||||
|
um.Where(psql.Quote("id").EQ(psql.Arg(water.ID))),
|
||||||
|
).Exec(ctx, txn)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to update publicreport.water geospatial", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Info().Int32("id", water.ID).Str("public_id", water.PublicID).Msg("Created water report")
|
||||||
|
setters := make([]*models.PublicreportWaterImageSetter, 0)
|
||||||
|
for _, image := range saved_images {
|
||||||
|
setters = append(setters, &models.PublicreportWaterImageSetter{
|
||||||
|
ImageID: omit.From(int32(image.ID)),
|
||||||
|
WaterID: omit.From(int32(water.ID)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(setters) > 0 {
|
||||||
|
_, err = models.PublicreportWaterImages.Insert(bob.ToMods(setters...)).Exec(ctx, txn)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Failed to save upload relationships", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
txn.Commit(ctx)
|
||||||
|
|
||||||
|
if organization_id != nil {
|
||||||
|
event.Created(
|
||||||
|
event.TypeRMOWater,
|
||||||
|
*organization_id,
|
||||||
|
water.PublicID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return water.PublicID, nil
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
package rmo
|
package rmo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
|
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
|
||||||
|
|
@ -10,9 +8,7 @@ import (
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/html"
|
"github.com/Gleipnir-Technology/nidus-sync/html"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContentDistrict struct {
|
type ContentDistrict struct {
|
||||||
|
|
@ -57,42 +53,6 @@ func getDistrictList(w http.ResponseWriter, r *http.Request) {
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
func matchDistrict(ctx context.Context, longitude, latitude *float64, images []ImageUpload) (*int32, error) {
|
|
||||||
var err error
|
|
||||||
var org *models.Organization
|
|
||||||
for _, image := range images {
|
|
||||||
if image.Exif == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if image.Exif.GPS == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
org, err = platform.DistrictForLocation(ctx, image.Exif.GPS.Longitude, image.Exif.GPS.Latitude)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("Failed to get district for location")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if org != nil {
|
|
||||||
return &org.ID, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if longitude == nil || latitude == nil {
|
|
||||||
log.Debug().Msg("No location from images, no latlng for the report itself, cannot match")
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
org, err = platform.DistrictForLocation(ctx, *longitude, *latitude)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("Failed to get district for location")
|
|
||||||
return nil, fmt.Errorf("Failed to get district for location: %w", err)
|
|
||||||
}
|
|
||||||
if org == nil {
|
|
||||||
log.Debug().Err(err).Float64("lng", *longitude).Float64("lat", *latitude).Msg("No district match by report location")
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
log.Debug().Err(err).Int32("org_id", org.ID).Float64("lng", *longitude).Float64("lat", *latitude).Msg("Found district match by report location")
|
|
||||||
return &org.ID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newContentDistrict(d *models.Organization) *ContentDistrict {
|
func newContentDistrict(d *models.Organization) *ContentDistrict {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
package rmo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/uber/h3-go/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GeospatialData struct {
|
|
||||||
Cell h3.Cell
|
|
||||||
GeometryQuery string
|
|
||||||
Populated bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func geospatialFromForm(r *http.Request) (GeospatialData, error) {
|
|
||||||
lat := r.FormValue("latitude")
|
|
||||||
lng := r.FormValue("longitude")
|
|
||||||
accuracy_type := r.FormValue("latlng-accuracy-type")
|
|
||||||
accuracy_value := r.FormValue("latlng-accuracy-value")
|
|
||||||
|
|
||||||
if lat == "" || lng == "" {
|
|
||||||
return GeospatialData{Populated: false}, nil
|
|
||||||
}
|
|
||||||
latitude, err := strconv.ParseFloat(lat, 64)
|
|
||||||
if err != nil {
|
|
||||||
return GeospatialData{Populated: false}, fmt.Errorf("Failed to create parse latitude: %v", err)
|
|
||||||
}
|
|
||||||
longitude, err := strconv.ParseFloat(lng, 64)
|
|
||||||
if err != nil {
|
|
||||||
return GeospatialData{Populated: false}, fmt.Errorf("Failed to create parse longitude: %v", err)
|
|
||||||
}
|
|
||||||
var resolution int
|
|
||||||
switch accuracy_type {
|
|
||||||
// These accuracy_type strings come from the Mapbox Geocoding API definition and
|
|
||||||
// are far from scientific
|
|
||||||
case "rooftop":
|
|
||||||
resolution = 14
|
|
||||||
case "parcel":
|
|
||||||
resolution = 13
|
|
||||||
case "point":
|
|
||||||
resolution = 13
|
|
||||||
case "interpolated":
|
|
||||||
resolution = 12
|
|
||||||
case "approximate":
|
|
||||||
resolution = 11
|
|
||||||
case "intersection":
|
|
||||||
resolution = 10
|
|
||||||
// This is a special indicator that we got our location from the browser measurements
|
|
||||||
case "meters":
|
|
||||||
case "browser":
|
|
||||||
accuracy_in_meters, err := strconv.ParseFloat(accuracy_value, 64)
|
|
||||||
if err != nil {
|
|
||||||
return GeospatialData{Populated: false}, fmt.Errorf("Failed to parse '%s' as an accuracy in meters: %v", accuracy_value, err)
|
|
||||||
}
|
|
||||||
resolution = h3utils.MeterAccuracyToH3Resolution(accuracy_in_meters)
|
|
||||||
default:
|
|
||||||
log.Warn().Str("accuracy-type", accuracy_type).Msg("unrecognized accuracy type, this indicates either a weird client or misbehaving web page. Defaulting to resolution 13")
|
|
||||||
resolution = 13
|
|
||||||
}
|
|
||||||
cell, err := h3utils.GetCell(longitude, latitude, resolution)
|
|
||||||
return GeospatialData{
|
|
||||||
Cell: cell,
|
|
||||||
GeometryQuery: fmt.Sprintf("ST_Point(%f, %f, 4326)", longitude, latitude),
|
|
||||||
Populated: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
@ -2,9 +2,11 @@ package rmo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
"image"
|
"image"
|
||||||
_ "image/gif" // register GIF format
|
_ "image/gif" // register GIF format
|
||||||
_ "image/jpeg" // register JPEG format
|
_ "image/jpeg" // register JPEG format
|
||||||
|
|
@ -12,84 +14,9 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Gleipnir-Technology/bob"
|
|
||||||
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
|
||||||
"github.com/Gleipnir-Technology/bob/dialect/psql/um"
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/platform/file"
|
|
||||||
"github.com/aarondl/opt/omit"
|
|
||||||
"github.com/aarondl/opt/omitnull"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/rwcarlsen/goexif/exif"
|
|
||||||
"github.com/rwcarlsen/goexif/tiff"
|
|
||||||
//exif "github.com/rwcarlsen/goexif/exif"
|
|
||||||
//"github.com/dsoprea/go-exif-extra/format"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GPS struct {
|
func extractImageUpload(headers *multipart.FileHeader) (upload platform.ImageUpload, err error) {
|
||||||
Latitude float64
|
|
||||||
Longitude float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExifCollection struct {
|
|
||||||
GPS *GPS
|
|
||||||
Tags map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageUpload struct {
|
|
||||||
Bounds image.Rectangle
|
|
||||||
ContentType string
|
|
||||||
Exif *ExifCollection
|
|
||||||
Format string
|
|
||||||
|
|
||||||
UploadFilesize int
|
|
||||||
UploadFilename string
|
|
||||||
UUID uuid.UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ExifCollection) Walk(name exif.FieldName, tag *tiff.Tag) error {
|
|
||||||
e.Tags[string(name)] = tag.String()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func extractExif(content_type string, file_bytes []byte) (result *ExifCollection, err error) {
|
|
||||||
/*
|
|
||||||
Using "github.com/evanoberholster/imagemeta"
|
|
||||||
meta, err := imagemeta.Decode(bytes.NewReader(file_bytes))
|
|
||||||
if err != nil {
|
|
||||||
return result, fmt.Errorf("Failed to decode image meta: %w", err)
|
|
||||||
}
|
|
||||||
result.GPS = &GPS{
|
|
||||||
Latitude: meta.GPS.Latitude(),
|
|
||||||
Longitude: meta.GPS.Longitude(),
|
|
||||||
}
|
|
||||||
return result, err
|
|
||||||
*/
|
|
||||||
|
|
||||||
e, err := exif.Decode(bytes.NewReader(file_bytes))
|
|
||||||
if err != nil {
|
|
||||||
if err.Error() == "exif: failed to find exif intro marker" {
|
|
||||||
return nil, nil
|
|
||||||
} else if errors.Is(err, io.EOF) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("Failed to decode image meta: %w", err)
|
|
||||||
}
|
|
||||||
lat, lng, _ := e.LatLong()
|
|
||||||
result = &ExifCollection{
|
|
||||||
GPS: &GPS{
|
|
||||||
Latitude: lat,
|
|
||||||
Longitude: lng,
|
|
||||||
},
|
|
||||||
Tags: make(map[string]string, 0),
|
|
||||||
}
|
|
||||||
err = e.Walk(result)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractImageUpload(headers *multipart.FileHeader) (upload ImageUpload, err error) {
|
|
||||||
f, err := headers.Open()
|
f, err := headers.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return upload, fmt.Errorf("Failed to open header: %w", err)
|
return upload, fmt.Errorf("Failed to open header: %w", err)
|
||||||
|
|
@ -99,7 +26,7 @@ func extractImageUpload(headers *multipart.FileHeader) (upload ImageUpload, err
|
||||||
file_bytes, err := io.ReadAll(f)
|
file_bytes, err := io.ReadAll(f)
|
||||||
content_type := http.DetectContentType(file_bytes)
|
content_type := http.DetectContentType(file_bytes)
|
||||||
|
|
||||||
exif, err := extractExif(content_type, file_bytes)
|
exif, err := platform.ImageExtractExif(content_type, file_bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return upload, fmt.Errorf("Failed to extract EXIF data: %w", err)
|
return upload, fmt.Errorf("Failed to extract EXIF data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +44,7 @@ func extractImageUpload(headers *multipart.FileHeader) (upload ImageUpload, err
|
||||||
return upload, fmt.Errorf("Failed to write image file to disk: %w", err)
|
return upload, fmt.Errorf("Failed to write image file to disk: %w", err)
|
||||||
}
|
}
|
||||||
log.Info().Int("size", len(file_bytes)).Str("uploaded_filename", headers.Filename).Str("content-type", content_type).Str("uuid", u.String()).Msg("Saved an uploaded file to disk")
|
log.Info().Int("size", len(file_bytes)).Str("uploaded_filename", headers.Filename).Str("content-type", content_type).Str("uuid", u.String()).Msg("Saved an uploaded file to disk")
|
||||||
return ImageUpload{
|
return platform.ImageUpload{
|
||||||
Bounds: i.Bounds(),
|
Bounds: i.Bounds(),
|
||||||
ContentType: content_type,
|
ContentType: content_type,
|
||||||
Exif: exif,
|
Exif: exif,
|
||||||
|
|
@ -128,77 +55,16 @@ func extractImageUpload(headers *multipart.FileHeader) (upload ImageUpload, err
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractImageUploads(r *http.Request) (uploads []ImageUpload, err error) {
|
func extractImageUploads(r *http.Request) (uploads []platform.ImageUpload, err error) {
|
||||||
uploads = make([]ImageUpload, 0)
|
uploads = make([]platform.ImageUpload, 0)
|
||||||
for _, fheaders := range r.MultipartForm.File {
|
for _, fheaders := range r.MultipartForm.File {
|
||||||
for _, headers := range fheaders {
|
for _, headers := range fheaders {
|
||||||
upload, err := extractImageUpload(headers)
|
upload, err := extractImageUpload(headers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return make([]ImageUpload, 0), fmt.Errorf("Failed to extract photo upload: %w", err)
|
return make([]platform.ImageUpload, 0), fmt.Errorf("Failed to extract photo upload: %w", err)
|
||||||
}
|
}
|
||||||
uploads = append(uploads, upload)
|
uploads = append(uploads, upload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return uploads, nil
|
return uploads, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveImageUploads(ctx context.Context, tx bob.Tx, uploads []ImageUpload) (models.PublicreportImageSlice, error) {
|
|
||||||
images := make(models.PublicreportImageSlice, 0)
|
|
||||||
for _, u := range uploads {
|
|
||||||
image, err := models.PublicreportImages.Insert(&models.PublicreportImageSetter{
|
|
||||||
ContentType: omit.From(u.ContentType),
|
|
||||||
|
|
||||||
Created: omit.From(time.Now()),
|
|
||||||
//Location: psql.Raw("NULL"),
|
|
||||||
Location: omitnull.FromPtr[string](nil),
|
|
||||||
ResolutionX: omit.From(int32(u.Bounds.Max.X)),
|
|
||||||
ResolutionY: omit.From(int32(u.Bounds.Max.Y)),
|
|
||||||
StorageUUID: omit.From(u.UUID),
|
|
||||||
StorageSize: omit.From(int64(u.UploadFilesize)),
|
|
||||||
UploadedFilename: omit.From(u.UploadFilename),
|
|
||||||
}).One(ctx, tx)
|
|
||||||
if err != nil {
|
|
||||||
return images, fmt.Errorf("Failed to create photo records: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: figure out how to do this via the setter...?
|
|
||||||
if u.Exif != nil {
|
|
||||||
if u.Exif.GPS != nil {
|
|
||||||
_, err = psql.Update(
|
|
||||||
um.Table("publicreport.image"),
|
|
||||||
um.SetCol("location").To(fmt.Sprintf("ST_Point(%f, %f, 4326)", u.Exif.GPS.Longitude, u.Exif.GPS.Latitude)),
|
|
||||||
um.Where(psql.Quote("id").EQ(psql.Arg(image.ID))),
|
|
||||||
).Exec(ctx, tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
exif_setters := make([]*models.PublicreportImageExifSetter, 0)
|
|
||||||
for k, v := range u.Exif.Tags {
|
|
||||||
to_save := trimQuotes(v)
|
|
||||||
exif_setters = append(exif_setters, &models.PublicreportImageExifSetter{
|
|
||||||
ImageID: omit.From(image.ID),
|
|
||||||
Name: omit.From(k),
|
|
||||||
Value: omit.From(to_save),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(exif_setters) > 0 {
|
|
||||||
_, err = models.PublicreportImageExifs.Insert(bob.ToMods(exif_setters...)).Exec(ctx, tx)
|
|
||||||
if err != nil {
|
|
||||||
return images, fmt.Errorf("Failed to create photo exif records: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info().Int32("id", image.ID).Int("tags", len(u.Exif.Tags)).Msg("Saved an uploaded file to the database")
|
|
||||||
} else {
|
|
||||||
log.Info().Int32("id", image.ID).Int("tags", 0).Msg("Saved an uploaded file without EXIF data")
|
|
||||||
}
|
|
||||||
images = append(images, image)
|
|
||||||
}
|
|
||||||
return images, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a string like "\"foo\"" return "foo".
|
|
||||||
func trimQuotes(s string) string {
|
|
||||||
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
|
|
||||||
return s[1 : len(s)-1]
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
|
||||||
128
rmo/nuisance.go
128
rmo/nuisance.go
|
|
@ -6,16 +6,11 @@ import (
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Gleipnir-Technology/bob"
|
|
||||||
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
|
||||||
"github.com/Gleipnir-Technology/bob/dialect/psql/um"
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/html"
|
"github.com/Gleipnir-Technology/nidus-sync/html"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/platform/geocode"
|
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/platform/report"
|
"github.com/Gleipnir-Technology/nidus-sync/platform/report"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
|
||||||
"github.com/aarondl/opt/omit"
|
"github.com/aarondl/opt/omit"
|
||||||
"github.com/aarondl/opt/omitnull"
|
"github.com/aarondl/opt/omitnull"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
@ -130,46 +125,6 @@ func postNuisance(w http.ResponseWriter, r *http.Request) {
|
||||||
if slices.Contains(source_locations, "pool-area") {
|
if slices.Contains(source_locations, "pool-area") {
|
||||||
is_location_pool = true
|
is_location_pool = true
|
||||||
}
|
}
|
||||||
//log.Debug().Bool("is_location_backyard", is_location_backyard).Bool("is_location_frontyard", is_location_frontyard).Bool("is_location_garden", is_location_garden).Bool("is_location_other", is_location_other).Bool("is_location_pool", is_location_pool).Msg("parsed")
|
|
||||||
public_id, err := report.GenerateReportID()
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to create report public ID", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
geospatial, err := geospatialFromForm(r)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to handle geospatial data", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
txn, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to create transaction", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer txn.Rollback(ctx)
|
|
||||||
|
|
||||||
// If we've got an address_country value it was set by geocoding so we should save it
|
|
||||||
var address *models.Address
|
|
||||||
if address_country != "" && latlng.Latitude != nil && latlng.Longitude != nil {
|
|
||||||
address, err = geocode.EnsureAddress(ctx, txn, types.Address{
|
|
||||||
Country: address_country,
|
|
||||||
Locality: address_locality,
|
|
||||||
Number: address_number,
|
|
||||||
PostalCode: address_postal_code,
|
|
||||||
Region: address_region,
|
|
||||||
Street: address_street,
|
|
||||||
Unit: "",
|
|
||||||
}, types.Location{
|
|
||||||
Latitude: *latlng.Latitude,
|
|
||||||
Longitude: *latlng.Longitude,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to ensure address: %w", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uploads, err := extractImageUploads(r)
|
uploads, err := extractImageUploads(r)
|
||||||
log.Info().Int("len", len(uploads)).Msg("extracted uploads")
|
log.Info().Int("len", len(uploads)).Msg("extracted uploads")
|
||||||
|
|
@ -177,42 +132,41 @@ func postNuisance(w http.ResponseWriter, r *http.Request) {
|
||||||
respondError(w, "Failed to extract image uploads", err, http.StatusInternalServerError)
|
respondError(w, "Failed to extract image uploads", err, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
images, err := saveImageUploads(ctx, txn, uploads)
|
address := platform.Address{
|
||||||
if err != nil {
|
Country: address_country,
|
||||||
respondError(w, "Failed to save image uploads", err, http.StatusInternalServerError)
|
Locality: address_locality,
|
||||||
return
|
Number: address_number,
|
||||||
|
PostalCode: address_postal_code,
|
||||||
|
Raw: address_raw,
|
||||||
|
Region: address_region,
|
||||||
|
Street: address_street,
|
||||||
|
Unit: "",
|
||||||
}
|
}
|
||||||
var organization_id *int32
|
|
||||||
organization_id, err = matchDistrict(ctx, latlng.Longitude, latlng.Latitude, uploads)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("Failed to match district")
|
|
||||||
}
|
|
||||||
|
|
||||||
setter := models.PublicreportNuisanceSetter{
|
setter := models.PublicreportNuisanceSetter{
|
||||||
AdditionalInfo: omit.From(additional_info),
|
AdditionalInfo: omit.From(additional_info),
|
||||||
//AddressID: omitnull.From(geospatial.Cell.String()),
|
//AddressID: omitnull.From(latlng.Cell.String()),
|
||||||
AddressRaw: omit.From(address_raw),
|
AddressRaw: omit.From(address.Raw),
|
||||||
AddressCountry: omit.From(address_country),
|
AddressCountry: omit.From(address.Country),
|
||||||
AddressNumber: omit.From(address_number),
|
AddressNumber: omit.From(address.Number),
|
||||||
AddressLocality: omit.From(address_locality),
|
AddressLocality: omit.From(address.Locality),
|
||||||
AddressPostalCode: omit.From(address_postal_code),
|
AddressPostalCode: omit.From(address.PostalCode),
|
||||||
AddressRegion: omit.From(address_region),
|
AddressRegion: omit.From(address.Region),
|
||||||
AddressStreet: omit.From(address_street),
|
AddressStreet: omit.From(address.Street),
|
||||||
Created: omit.From(time.Now()),
|
Created: omit.From(time.Now()),
|
||||||
Duration: omit.From(duration),
|
Duration: omit.From(duration),
|
||||||
//H3cell: omitnull.From(geospatial.Cell.String()),
|
//H3cell: omitnull.From(latlng.Cell.String()),
|
||||||
IsLocationBackyard: omit.From(is_location_backyard),
|
IsLocationBackyard: omit.From(is_location_backyard),
|
||||||
IsLocationFrontyard: omit.From(is_location_frontyard),
|
IsLocationFrontyard: omit.From(is_location_frontyard),
|
||||||
IsLocationGarden: omit.From(is_location_garden),
|
IsLocationGarden: omit.From(is_location_garden),
|
||||||
IsLocationOther: omit.From(is_location_other),
|
IsLocationOther: omit.From(is_location_other),
|
||||||
IsLocationPool: omit.From(is_location_pool),
|
IsLocationPool: omit.From(is_location_pool),
|
||||||
LatlngAccuracyType: omit.From(latlng.AccuracyType),
|
LatlngAccuracyType: omit.From(latlng.AccuracyType),
|
||||||
LatlngAccuracyValue: omit.From(latlng.AccuracyValue),
|
LatlngAccuracyValue: omit.From(float32(latlng.AccuracyValue)),
|
||||||
//Location: omitnull.From(fmt.Sprintf("ST_GeometryFromText(Point(%s %s))", longitude, latitude)),
|
//Location: omitnull.From(fmt.Sprintf("ST_GeometryFromText(Point(%s %s))", longitude, latitude)),
|
||||||
Location: omitnull.FromPtr[string](nil),
|
Location: omitnull.FromPtr[string](nil),
|
||||||
MapZoom: omit.From(latlng.MapZoom),
|
MapZoom: omit.From(latlng.MapZoom),
|
||||||
OrganizationID: omitnull.FromPtr(organization_id),
|
//OrganizationID: omitnull.FromPtr(organization_id),
|
||||||
PublicID: omit.From(public_id),
|
//PublicID: omit.From(public_id),
|
||||||
ReporterEmail: omitnull.FromPtr[string](nil),
|
ReporterEmail: omitnull.FromPtr[string](nil),
|
||||||
ReporterName: omitnull.FromPtr[string](nil),
|
ReporterName: omitnull.FromPtr[string](nil),
|
||||||
ReporterPhone: omitnull.FromPtr[string](nil),
|
ReporterPhone: omitnull.FromPtr[string](nil),
|
||||||
|
|
@ -226,42 +180,6 @@ func postNuisance(w http.ResponseWriter, r *http.Request) {
|
||||||
TodEvening: omit.From(tod_evening),
|
TodEvening: omit.From(tod_evening),
|
||||||
TodNight: omit.From(tod_night),
|
TodNight: omit.From(tod_night),
|
||||||
}
|
}
|
||||||
if address != nil {
|
public_id, err := platform.NuisanceCreate(ctx, setter, latlng, address, uploads)
|
||||||
setter.AddressID = omitnull.From(address.ID)
|
|
||||||
}
|
|
||||||
nuisance, err := models.PublicreportNuisances.Insert(&setter).One(ctx, txn)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to create database record", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if geospatial.Populated {
|
|
||||||
_, err = psql.Update(
|
|
||||||
um.Table("publicreport.nuisance"),
|
|
||||||
um.SetCol("h3cell").ToArg(geospatial.Cell),
|
|
||||||
um.SetCol("location").To(geospatial.GeometryQuery),
|
|
||||||
um.Where(psql.Quote("id").EQ(psql.Arg(nuisance.ID))),
|
|
||||||
).Exec(ctx, txn)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to insert publicreport.nuisance geospatial", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info().Str("public_id", public_id).Int32("id", nuisance.ID).Msg("Created nuisance report")
|
|
||||||
if len(images) > 0 {
|
|
||||||
setters := make([]*models.PublicreportNuisanceImageSetter, 0)
|
|
||||||
for _, image := range images {
|
|
||||||
setters = append(setters, &models.PublicreportNuisanceImageSetter{
|
|
||||||
ImageID: omit.From(int32(image.ID)),
|
|
||||||
NuisanceID: omit.From(int32(nuisance.ID)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_, err = models.PublicreportNuisanceImages.Insert(bob.ToMods(setters...)).Exec(ctx, txn)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to save reference to images", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info().Int("len", len(images)).Msg("saved uploads")
|
|
||||||
}
|
|
||||||
txn.Commit(ctx)
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("/submit-complete?report=%s", public_id), http.StatusFound)
|
http.Redirect(w, r, fmt.Sprintf("/submit-complete?report=%s", public_id), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db/sql"
|
"github.com/Gleipnir-Technology/nidus-sync/db/sql"
|
||||||
|
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||||
//"github.com/go-chi/chi/v5"
|
//"github.com/go-chi/chi/v5"
|
||||||
//"github.com/rs/zerolog/log"
|
//"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
@ -67,16 +68,8 @@ func getReportSuggestion(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write(jsonBody)
|
w.Write(jsonBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LatLngForm struct {
|
func parseLatLng(r *http.Request) (platform.LatLng, error) {
|
||||||
Latitude *float64
|
result := platform.LatLng{
|
||||||
Longitude *float64
|
|
||||||
MapZoom float32
|
|
||||||
AccuracyValue float32
|
|
||||||
AccuracyType enums.PublicreportAccuracytype
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLatLng(r *http.Request) (LatLngForm, error) {
|
|
||||||
result := LatLngForm{
|
|
||||||
AccuracyType: enums.PublicreportAccuracytypeNone,
|
AccuracyType: enums.PublicreportAccuracytypeNone,
|
||||||
AccuracyValue: 0.0,
|
AccuracyValue: 0.0,
|
||||||
Latitude: nil,
|
Latitude: nil,
|
||||||
|
|
@ -102,7 +95,7 @@ func parseLatLng(r *http.Request) (LatLngForm, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("Failed to parse latlng_accuracy_value '%s': %w", latlng_accuracy_value_str, err)
|
return result, fmt.Errorf("Failed to parse latlng_accuracy_value '%s': %w", latlng_accuracy_value_str, err)
|
||||||
}
|
}
|
||||||
result.AccuracyValue = float32(t)
|
result.AccuracyValue = float64(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
if latitude_str != "" {
|
if latitude_str != "" {
|
||||||
|
|
|
||||||
85
rmo/water.go
85
rmo/water.go
|
|
@ -5,17 +5,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Gleipnir-Technology/bob"
|
|
||||||
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
|
||||||
"github.com/Gleipnir-Technology/bob/dialect/psql/um"
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/html"
|
"github.com/Gleipnir-Technology/nidus-sync/html"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/platform/report"
|
"github.com/Gleipnir-Technology/nidus-sync/platform"
|
||||||
"github.com/aarondl/opt/omit"
|
"github.com/aarondl/opt/omit"
|
||||||
"github.com/aarondl/opt/omitnull"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContentWater struct {
|
type ContentWater struct {
|
||||||
|
|
@ -65,7 +59,7 @@ func postWater(w http.ResponseWriter, r *http.Request) {
|
||||||
address_country := r.FormValue("address-country")
|
address_country := r.FormValue("address-country")
|
||||||
address_locality := r.FormValue("address-locality")
|
address_locality := r.FormValue("address-locality")
|
||||||
address_number := r.FormValue("address-number")
|
address_number := r.FormValue("address-number")
|
||||||
address_postalcode := r.FormValue("address-postalcode")
|
address_postal_code := r.FormValue("address-postalcode")
|
||||||
address_region := r.FormValue("address-region")
|
address_region := r.FormValue("address-region")
|
||||||
address_street := r.FormValue("address-street")
|
address_street := r.FormValue("address-street")
|
||||||
comments := r.FormValue("comments")
|
comments := r.FormValue("comments")
|
||||||
|
|
@ -85,42 +79,24 @@ func postWater(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
geospatial, err := geospatialFromForm(r)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to handle geospatial data", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
public_id, err := report.GenerateReportID()
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to create water report public ID", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
tx, err := db.PGInstance.BobDB.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to create transaction", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer tx.Rollback(ctx)
|
|
||||||
|
|
||||||
uploads, err := extractImageUploads(r)
|
uploads, err := extractImageUploads(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, "Failed to extract image uploads", err, http.StatusInternalServerError)
|
respondError(w, "Failed to extract image uploads", err, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
images, err := saveImageUploads(r.Context(), tx, uploads)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to save image uploads", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization_id *int32
|
address := platform.Address{
|
||||||
organization_id, err = matchDistrict(ctx, latlng.Longitude, latlng.Latitude, uploads)
|
Country: address_country,
|
||||||
if err != nil {
|
Locality: address_locality,
|
||||||
log.Warn().Err(err).Msg("Failed to match district")
|
Number: address_number,
|
||||||
|
PostalCode: address_postal_code,
|
||||||
|
Raw: address_raw,
|
||||||
|
Region: address_region,
|
||||||
|
Street: address_street,
|
||||||
|
Unit: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
setter := models.PublicreportWaterSetter{
|
setter := models.PublicreportWaterSetter{
|
||||||
AccessComments: omit.From(access_comments),
|
AccessComments: omit.From(access_comments),
|
||||||
AccessDog: omit.From(access_dog),
|
AccessDog: omit.From(access_dog),
|
||||||
|
|
@ -132,7 +108,7 @@ func postWater(w http.ResponseWriter, r *http.Request) {
|
||||||
AddressCountry: omit.From(address_country),
|
AddressCountry: omit.From(address_country),
|
||||||
AddressLocality: omit.From(address_locality),
|
AddressLocality: omit.From(address_locality),
|
||||||
AddressNumber: omit.From(address_number),
|
AddressNumber: omit.From(address_number),
|
||||||
AddressPostalCode: omit.From(address_postalcode),
|
AddressPostalCode: omit.From(address_postal_code),
|
||||||
AddressStreet: omit.From(address_street),
|
AddressStreet: omit.From(address_street),
|
||||||
AddressRegion: omit.From(address_region),
|
AddressRegion: omit.From(address_region),
|
||||||
Comments: omit.From(comments),
|
Comments: omit.From(comments),
|
||||||
|
|
@ -146,50 +122,21 @@ func postWater(w http.ResponseWriter, r *http.Request) {
|
||||||
IsReporterOwner: omit.From(is_reporter_owner),
|
IsReporterOwner: omit.From(is_reporter_owner),
|
||||||
//Location: add later
|
//Location: add later
|
||||||
MapZoom: omit.From(latlng.MapZoom),
|
MapZoom: omit.From(latlng.MapZoom),
|
||||||
OrganizationID: omitnull.FromPtr(organization_id),
|
//OrganizationID: omitnull.FromPtr(organization_id),
|
||||||
OwnerEmail: omit.From(owner_email),
|
OwnerEmail: omit.From(owner_email),
|
||||||
OwnerName: omit.From(owner_name),
|
OwnerName: omit.From(owner_name),
|
||||||
OwnerPhone: omit.From(owner_phone),
|
OwnerPhone: omit.From(owner_phone),
|
||||||
PublicID: omit.From(public_id),
|
//PublicID: omit.From(public_id),
|
||||||
ReporterEmail: omit.From(""),
|
ReporterEmail: omit.From(""),
|
||||||
ReporterName: omit.From(""),
|
ReporterName: omit.From(""),
|
||||||
ReporterPhone: omit.From(""),
|
ReporterPhone: omit.From(""),
|
||||||
Status: omit.From(enums.PublicreportReportstatustypeReported),
|
Status: omit.From(enums.PublicreportReportstatustypeReported),
|
||||||
}
|
}
|
||||||
water, err := models.PublicreportWaters.Insert(&setter).One(ctx, tx)
|
public_id, err := platform.WaterCreate(ctx, setter, latlng, address, uploads)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondError(w, "Failed to create database record", err, http.StatusInternalServerError)
|
respondError(w, "Failed to save new report", err, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if geospatial.Populated {
|
|
||||||
_, err = psql.Update(
|
|
||||||
um.Table("publicreport.water"),
|
|
||||||
um.SetCol("h3cell").ToArg(geospatial.Cell),
|
|
||||||
um.SetCol("location").To(geospatial.GeometryQuery),
|
|
||||||
um.Where(psql.Quote("id").EQ(psql.Arg(water.ID))),
|
|
||||||
).Exec(ctx, tx)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to update publicreport.water geospatial", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.Info().Int32("id", water.ID).Str("public_id", water.PublicID).Msg("Created water report")
|
|
||||||
setters := make([]*models.PublicreportWaterImageSetter, 0)
|
|
||||||
for _, image := range images {
|
|
||||||
setters = append(setters, &models.PublicreportWaterImageSetter{
|
|
||||||
ImageID: omit.From(int32(image.ID)),
|
|
||||||
WaterID: omit.From(int32(water.ID)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(setters) > 0 {
|
|
||||||
_, err = models.PublicreportWaterImages.Insert(bob.ToMods(setters...)).Exec(r.Context(), tx)
|
|
||||||
if err != nil {
|
|
||||||
respondError(w, "Failed to save upload relationships", err, http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tx.Commit(ctx)
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("/submit-complete?report=%s", public_id), http.StatusFound)
|
http.Redirect(w, r, fmt.Sprintf("/submit-complete?report=%s", public_id), http.StatusFound)
|
||||||
}
|
}
|
||||||
func postWaterDistrict(w http.ResponseWriter, r *http.Request) {
|
func postWaterDistrict(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ func Router() chi.Router {
|
||||||
r.Method("GET", "/sudo", authenticatedHandler(getSudo))
|
r.Method("GET", "/sudo", authenticatedHandler(getSudo))
|
||||||
r.Method("POST", "/sudo/email", authenticatedHandlerPost(postSudoEmail))
|
r.Method("POST", "/sudo/email", authenticatedHandlerPost(postSudoEmail))
|
||||||
r.Method("POST", "/sudo/sms", authenticatedHandlerPost(postSudoSMS))
|
r.Method("POST", "/sudo/sms", authenticatedHandlerPost(postSudoSMS))
|
||||||
|
r.Method("POST", "/sudo/sse", authenticatedHandlerPost(postSudoSSE))
|
||||||
r.Method("GET", "/trap/{globalid}", authenticatedHandler(getTrap))
|
r.Method("GET", "/trap/{globalid}", authenticatedHandler(getTrap))
|
||||||
r.Method("GET", "/text/{destination}", authenticatedHandler(getTextMessages))
|
r.Method("GET", "/text/{destination}", authenticatedHandler(getTextMessages))
|
||||||
r.Method("GET", "/tile/gps", auth.NewEnsureAuth(getTileGPS))
|
r.Method("GET", "/tile/gps", auth.NewEnsureAuth(getTileGPS))
|
||||||
|
|
|
||||||
16
sync/sudo.go
16
sync/sudo.go
|
|
@ -84,3 +84,19 @@ func postSudoSMS(ctx context.Context, r *http.Request, u platform.User, sms Form
|
||||||
}
|
}
|
||||||
return "/sudo", nil
|
return "/sudo", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FormSSE struct {
|
||||||
|
Content string `schema:"content"`
|
||||||
|
OrganizationID int32 `schema:"organizationID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func postSudoSSE(ctx context.Context, r *http.Request, u platform.User, sse FormSSE) (string, *nhttp.ErrorWithStatus) {
|
||||||
|
if !u.HasRoot() {
|
||||||
|
return "", &nhttp.ErrorWithStatus{
|
||||||
|
Message: "You must have sudo powers to do this",
|
||||||
|
Status: http.StatusForbidden,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
platform.SudoEvent(sse.OrganizationID, sse.Content)
|
||||||
|
return "/sudo", nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue