@@ -0,0 +1,243 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gardomatic.kleiax.de/internal/platform/validate"
|
||||
"gardomatic.kleiax.de/internal/storage"
|
||||
)
|
||||
|
||||
type locationInput struct {
|
||||
ParentID *int `json:"parent_id"`
|
||||
ClearParentID bool `json:"clear_parent_id"`
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
ImageData *string `json:"image_data"`
|
||||
ImageID *int `json:"image_id"`
|
||||
Kind *string `json:"kind"`
|
||||
AreaSQM *float64 `json:"area_sqm"`
|
||||
SunExposure *string `json:"sun_exposure"`
|
||||
SoilCondition *string `json:"soil_condition"`
|
||||
SoilReaction *string `json:"soil_reaction"`
|
||||
Attributes json.RawMessage `json:"attributes"`
|
||||
}
|
||||
|
||||
func (input locationInput) apply(location *storage.Location) {
|
||||
if input.ParentID != nil {
|
||||
location.ParentID = input.ParentID
|
||||
}
|
||||
if input.ClearParentID {
|
||||
location.ParentID = nil
|
||||
}
|
||||
if input.Name != nil {
|
||||
location.Name = strings.TrimSpace(*input.Name)
|
||||
}
|
||||
if input.Description != nil {
|
||||
location.Description = strings.TrimSpace(*input.Description)
|
||||
}
|
||||
if input.ImageData != nil {
|
||||
location.ImageData = *input.ImageData
|
||||
}
|
||||
if input.Kind != nil {
|
||||
location.Kind = strings.TrimSpace(*input.Kind)
|
||||
}
|
||||
if input.AreaSQM != nil {
|
||||
location.AreaSQM = input.AreaSQM
|
||||
}
|
||||
if input.SunExposure != nil {
|
||||
assignStringPointer(input.SunExposure, &location.SunExposure)
|
||||
}
|
||||
assignStringPointer(input.SoilCondition, &location.SoilCondition)
|
||||
assignStringPointer(input.SoilReaction, &location.SoilReaction)
|
||||
if len(input.Attributes) != 0 {
|
||||
location.Attributes = input.Attributes
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) createLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
var input locationInput
|
||||
if err := app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
location := storage.Location{GardenID: gardenID, Attributes: json.RawMessage(`{}`), CreatedBy: user.ID, UpdatedBy: user.ID}
|
||||
input.apply(&location)
|
||||
if !app.validateLocationForGarden(w, r, gardenID, location) {
|
||||
return
|
||||
}
|
||||
imageID, err := app.resolveImage(gardenID, location.ImageData, input.ImageID, user.ID, "location")
|
||||
if err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
location.ImageID = imageID
|
||||
location.ImageData = ""
|
||||
location, err = app.models.Locations.Insert(location)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
headers := make(http.Header)
|
||||
headers.Set("Location", fmt.Sprintf("/v1/gardens/%d/locations/%d", gardenID, location.ID))
|
||||
if err := app.writeJSON(w, http.StatusCreated, envelope{"location": location}, headers); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) listLocationsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
locations, err := app.models.Locations.GetAllForGarden(gardenID)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
member, _ := app.contextGetGardenMember(r)
|
||||
visible := locations[:0]
|
||||
for _, location := range locations {
|
||||
if location.CreatedBy == user.ID && member.Can(storage.GardenPermissionLocationReadOwn) || location.CreatedBy != user.ID && member.Can(storage.GardenPermissionLocationReadOther) {
|
||||
visible = append(visible, location)
|
||||
}
|
||||
}
|
||||
if err := app.writeJSON(w, http.StatusOK, envelope{"locations": visible}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) showLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
id, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
location, err := app.models.Locations.Get(gardenID, id)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
if !app.authorizeGardenResource(w, r, location.CreatedBy, storage.GardenPermissionLocationReadOwn, storage.GardenPermissionLocationReadOther) {
|
||||
return
|
||||
}
|
||||
if err := app.writeJSON(w, http.StatusOK, envelope{"location": location}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) updateLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
id, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
location, err := app.models.Locations.Get(gardenID, id)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
if !app.authorizeGardenResource(w, r, location.CreatedBy, storage.GardenPermissionLocationUpdateOwn, storage.GardenPermissionLocationUpdateOther) {
|
||||
return
|
||||
}
|
||||
var input locationInput
|
||||
if err := app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
previousImageID := location.ImageID
|
||||
previousImageData := location.ImageData
|
||||
input.apply(&location)
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
location.UpdatedBy = user.ID
|
||||
if !app.validateLocationForGarden(w, r, gardenID, location) {
|
||||
return
|
||||
}
|
||||
if input.ImageData != nil && *input.ImageData != previousImageData {
|
||||
location.ImageID, err = app.resolveImage(gardenID, *input.ImageData, input.ImageID, user.ID, "location")
|
||||
if err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
location.ImageData = ""
|
||||
location, err = app.models.Locations.Update(gardenID, location)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.recordImageAssignment(storage.ImageAssignment{GardenID: gardenID, EntityType: "location", EntityID: location.ID, PreviousImageID: previousImageID, ImageID: location.ImageID, ChangedBy: user.ID}); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if err := app.writeJSON(w, http.StatusOK, envelope{"location": location}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) deleteLocationHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
id, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
location, err := app.models.Locations.Get(gardenID, id)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
if !app.authorizeGardenResource(w, r, location.CreatedBy, storage.GardenPermissionLocationDeleteOwn, storage.GardenPermissionLocationDeleteOther) {
|
||||
return
|
||||
}
|
||||
if err := app.models.Locations.Delete(gardenID, id); err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (app *application) validateLocationForGarden(w http.ResponseWriter, r *http.Request, gardenID int, location storage.Location) bool {
|
||||
v := validate.New()
|
||||
validateImageData(v, location.ImageData)
|
||||
storage.ValidateLocation(v, location)
|
||||
if location.ParentID != nil && *location.ParentID > 0 && *location.ParentID != location.ID {
|
||||
if _, err := app.models.Locations.Get(gardenID, *location.ParentID); err != nil {
|
||||
if errors.Is(err, storage.ErrRecordNotFound) {
|
||||
v.AddError("parent_id", "must refer to a location in this garden")
|
||||
} else {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if v.Valid() && location.ParentID != nil && location.ID > 0 {
|
||||
locations, err := app.models.Locations.GetAllForGarden(gardenID)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return false
|
||||
}
|
||||
parents := make(map[int]*int, len(locations))
|
||||
for i := range locations {
|
||||
parents[locations[i].ID] = locations[i].ParentID
|
||||
}
|
||||
seen := map[int]bool{}
|
||||
for current := location.ParentID; current != nil; current = parents[*current] {
|
||||
if *current == location.ID || seen[*current] {
|
||||
v.AddError("parent_id", "must not create a cycle")
|
||||
break
|
||||
}
|
||||
seen[*current] = true
|
||||
}
|
||||
}
|
||||
if !v.Valid() {
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user