@@ -0,0 +1,396 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gardomatic.kleiax.de/internal/platform/validate"
|
||||
"gardomatic.kleiax.de/internal/storage"
|
||||
)
|
||||
|
||||
type speciesInput struct {
|
||||
Global bool `json:"global"`
|
||||
Tags []string `json:"tags"`
|
||||
CommonName *string `json:"common_name"`
|
||||
Cultivar *string `json:"cultivar"`
|
||||
BotanicalName *string `json:"botanical_name"`
|
||||
CategoryID *int `json:"category_id"`
|
||||
ClearCategoryID bool `json:"clear_category_id"`
|
||||
SunExposure *string `json:"sun_exposure"`
|
||||
SoilCondition *string `json:"soil_condition"`
|
||||
SoilReaction *string `json:"soil_reaction"`
|
||||
WinterProtection *string `json:"winter_protection"`
|
||||
SpacingCM *int `json:"spacing_cm"`
|
||||
HeightCM *int `json:"height_cm"`
|
||||
SowMonthFrom *int `json:"sow_month_from"`
|
||||
SowDayFrom *int `json:"sow_day_from"`
|
||||
ClearSowDayFrom bool `json:"clear_sow_day_from"`
|
||||
SowMonthTo *int `json:"sow_month_to"`
|
||||
SowDayTo *int `json:"sow_day_to"`
|
||||
ClearSowDayTo bool `json:"clear_sow_day_to"`
|
||||
PlantingMonthFrom *int `json:"planting_month_from"`
|
||||
PlantingDayFrom *int `json:"planting_day_from"`
|
||||
ClearPlantingDayFrom bool `json:"clear_planting_day_from"`
|
||||
PlantingMonthTo *int `json:"planting_month_to"`
|
||||
PlantingDayTo *int `json:"planting_day_to"`
|
||||
ClearPlantingDayTo bool `json:"clear_planting_day_to"`
|
||||
HarvestMonthFrom *int `json:"harvest_month_from"`
|
||||
HarvestDayFrom *int `json:"harvest_day_from"`
|
||||
ClearHarvestDayFrom bool `json:"clear_harvest_day_from"`
|
||||
HarvestMonthTo *int `json:"harvest_month_to"`
|
||||
HarvestDayTo *int `json:"harvest_day_to"`
|
||||
ClearHarvestDayTo bool `json:"clear_harvest_day_to"`
|
||||
ClearSowRange bool `json:"clear_sow_range"`
|
||||
ClearPlantingRange bool `json:"clear_planting_range"`
|
||||
ClearHarvestRange bool `json:"clear_harvest_range"`
|
||||
Notes *string `json:"notes"`
|
||||
ImageData *string `json:"image_data"`
|
||||
ImageID *int `json:"image_id"`
|
||||
Attributes json.RawMessage `json:"attributes"`
|
||||
}
|
||||
|
||||
func (input speciesInput) apply(species *storage.Species) {
|
||||
assignTrimmed(input.CommonName, &species.CommonName)
|
||||
assignTrimmed(input.Cultivar, &species.Cultivar)
|
||||
assignTrimmed(input.BotanicalName, &species.BotanicalName)
|
||||
assignIntPointer(input.CategoryID, &species.CategoryID)
|
||||
if input.ClearCategoryID {
|
||||
species.CategoryID = nil
|
||||
species.Category = ""
|
||||
}
|
||||
assignTrimmed(input.Notes, &species.Notes)
|
||||
if input.ImageData != nil {
|
||||
species.ImageData = *input.ImageData
|
||||
}
|
||||
assignStringPointer(input.SunExposure, &species.SunExposure)
|
||||
assignStringPointer(input.SoilCondition, &species.SoilCondition)
|
||||
assignStringPointer(input.SoilReaction, &species.SoilReaction)
|
||||
assignStringPointer(input.WinterProtection, &species.WinterProtection)
|
||||
assignIntPointer(input.SpacingCM, &species.SpacingCM)
|
||||
assignIntPointer(input.HeightCM, &species.HeightCM)
|
||||
assignIntPointer(input.SowMonthFrom, &species.SowMonthFrom)
|
||||
assignIntPointer(input.SowDayFrom, &species.SowDayFrom)
|
||||
assignIntPointer(input.SowMonthTo, &species.SowMonthTo)
|
||||
assignIntPointer(input.SowDayTo, &species.SowDayTo)
|
||||
assignIntPointer(input.PlantingMonthFrom, &species.PlantingMonthFrom)
|
||||
assignIntPointer(input.PlantingDayFrom, &species.PlantingDayFrom)
|
||||
assignIntPointer(input.PlantingMonthTo, &species.PlantingMonthTo)
|
||||
assignIntPointer(input.PlantingDayTo, &species.PlantingDayTo)
|
||||
assignIntPointer(input.HarvestMonthFrom, &species.HarvestMonthFrom)
|
||||
assignIntPointer(input.HarvestDayFrom, &species.HarvestDayFrom)
|
||||
assignIntPointer(input.HarvestMonthTo, &species.HarvestMonthTo)
|
||||
assignIntPointer(input.HarvestDayTo, &species.HarvestDayTo)
|
||||
if input.ClearSowRange {
|
||||
species.SowMonthFrom, species.SowDayFrom, species.SowMonthTo, species.SowDayTo = nil, nil, nil, nil
|
||||
}
|
||||
if input.ClearHarvestRange {
|
||||
species.HarvestMonthFrom, species.HarvestDayFrom, species.HarvestMonthTo, species.HarvestDayTo = nil, nil, nil, nil
|
||||
}
|
||||
if input.ClearPlantingRange {
|
||||
species.PlantingMonthFrom, species.PlantingDayFrom, species.PlantingMonthTo, species.PlantingDayTo = nil, nil, nil, nil
|
||||
}
|
||||
if input.ClearSowDayFrom {
|
||||
species.SowDayFrom = nil
|
||||
}
|
||||
if input.ClearSowDayTo {
|
||||
species.SowDayTo = nil
|
||||
}
|
||||
if input.ClearPlantingDayFrom {
|
||||
species.PlantingDayFrom = nil
|
||||
}
|
||||
if input.ClearPlantingDayTo {
|
||||
species.PlantingDayTo = nil
|
||||
}
|
||||
if input.ClearHarvestDayFrom {
|
||||
species.HarvestDayFrom = nil
|
||||
}
|
||||
if input.ClearHarvestDayTo {
|
||||
species.HarvestDayTo = nil
|
||||
}
|
||||
if len(input.Attributes) != 0 {
|
||||
species.Attributes = input.Attributes
|
||||
}
|
||||
}
|
||||
|
||||
func assignTrimmed(input *string, destination *string) {
|
||||
if input != nil {
|
||||
*destination = strings.TrimSpace(*input)
|
||||
}
|
||||
}
|
||||
|
||||
func assignStringPointer(input *string, destination **string) {
|
||||
if input != nil {
|
||||
value := strings.TrimSpace(*input)
|
||||
if value == "" {
|
||||
*destination = nil
|
||||
} else {
|
||||
*destination = &value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assignIntPointer(input *int, destination **int) {
|
||||
if input != nil {
|
||||
*destination = input
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) createSpeciesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
var input speciesInput
|
||||
if err := app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
member, _ := app.contextGetGardenMember(r)
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
if input.Global {
|
||||
if !user.Can(storage.ApplicationPermissionGlobalSpeciesWrite) {
|
||||
app.permissionDeniedResponse(w, r)
|
||||
return
|
||||
}
|
||||
} else if !member.Can(storage.GardenPermissionSpeciesWrite) {
|
||||
app.permissionDeniedResponse(w, r)
|
||||
return
|
||||
}
|
||||
var ownerGardenID *int
|
||||
if !input.Global {
|
||||
ownerGardenID = &gardenID
|
||||
}
|
||||
species := storage.Species{GardenID: ownerGardenID, Attributes: json.RawMessage(`{}`), CreatedBy: user.ID, UpdatedBy: user.ID}
|
||||
input.apply(&species)
|
||||
changedCategoryID := input.CategoryID
|
||||
if input.ClearCategoryID {
|
||||
changedCategoryID = nil
|
||||
}
|
||||
if !app.resolveSpeciesCategory(w, r, &species, changedCategoryID, nil) {
|
||||
return
|
||||
}
|
||||
species.Tags = storage.NormalizeTags(input.Tags)
|
||||
v := validate.New()
|
||||
validateImageData(v, species.ImageData)
|
||||
if storage.ValidateSpecies(v, species); !v.Valid() {
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
return
|
||||
}
|
||||
imageID, err := app.resolveImage(gardenID, species.ImageData, input.ImageID, user.ID, "species")
|
||||
if err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
species.ImageID = imageID
|
||||
species.ImageData = ""
|
||||
species, err = app.models.Species.Insert(species)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.syncSeasonTaskTemplates(gardenID, species); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
tagScope := gardenID
|
||||
if species.GardenID == nil {
|
||||
tagScope = 0
|
||||
}
|
||||
species.Tags, err = app.saveTags(tagScope, storage.TagEntitySpecies, species.ID, species.Tags)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
headers := make(http.Header)
|
||||
headers.Set("Location", fmt.Sprintf("/v1/gardens/%d/species/%d", gardenID, species.ID))
|
||||
if err := app.writeJSON(w, http.StatusCreated, envelope{"species": species}, headers); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) listSpeciesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
allSpecies, err := app.models.Species.GetAllForGarden(gardenID)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
for i := range allSpecies {
|
||||
tagScope := gardenID
|
||||
if allSpecies[i].GardenID == nil {
|
||||
tagScope = 0
|
||||
}
|
||||
allSpecies[i].Tags, err = app.loadTags(tagScope, storage.TagEntitySpecies, allSpecies[i].ID)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := app.writeJSON(w, http.StatusOK, envelope{"species": allSpecies}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) showSpeciesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
id, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
species, err := app.models.Species.Get(gardenID, id)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
tagScope := gardenID
|
||||
if species.GardenID == nil {
|
||||
tagScope = 0
|
||||
}
|
||||
species.Tags, err = app.loadTags(tagScope, storage.TagEntitySpecies, species.ID)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if err := app.writeJSON(w, http.StatusOK, envelope{"species": species}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) updateSpeciesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
id, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
species, err := app.models.Species.Get(gardenID, id)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
member, _ := app.contextGetGardenMember(r)
|
||||
user, _ := app.contextGetAuthenticatedUser(r)
|
||||
if species.GardenID == nil {
|
||||
if !user.Can(storage.ApplicationPermissionGlobalSpeciesWrite) {
|
||||
app.permissionDeniedResponse(w, r)
|
||||
return
|
||||
}
|
||||
} else if *species.GardenID != gardenID {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
} else if !member.Can(storage.GardenPermissionSpeciesWrite) {
|
||||
app.permissionDeniedResponse(w, r)
|
||||
return
|
||||
}
|
||||
var input speciesInput
|
||||
if err := app.readJSON(w, r, &input); err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
previousCategoryID := species.CategoryID
|
||||
previousImageID := species.ImageID
|
||||
previousImageData := species.ImageData
|
||||
input.apply(&species)
|
||||
species.UpdatedBy = user.ID
|
||||
changedCategoryID := input.CategoryID
|
||||
if input.ClearCategoryID {
|
||||
changedCategoryID = nil
|
||||
}
|
||||
if !app.resolveSpeciesCategory(w, r, &species, changedCategoryID, previousCategoryID) {
|
||||
return
|
||||
}
|
||||
if input.Tags != nil {
|
||||
species.Tags = storage.NormalizeTags(input.Tags)
|
||||
}
|
||||
v := validate.New()
|
||||
validateImageData(v, species.ImageData)
|
||||
if storage.ValidateSpecies(v, species); !v.Valid() {
|
||||
app.failedValidationResponse(w, r, v.Errors)
|
||||
return
|
||||
}
|
||||
if input.ImageData != nil && *input.ImageData != previousImageData {
|
||||
species.ImageID, err = app.resolveImage(gardenID, *input.ImageData, input.ImageID, user.ID, "species")
|
||||
if err != nil {
|
||||
app.badRequestResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
species.ImageData = ""
|
||||
updateScope := gardenID
|
||||
if species.GardenID == nil {
|
||||
updateScope = 0
|
||||
}
|
||||
species, err = app.models.Species.Update(updateScope, species)
|
||||
if err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.syncSeasonTaskTemplates(gardenID, species); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if err = app.recordImageAssignment(storage.ImageAssignment{GardenID: gardenID, EntityType: "species", EntityID: species.ID, PreviousImageID: previousImageID, ImageID: species.ImageID, ChangedBy: user.ID}); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
if input.Tags != nil {
|
||||
species.Tags, err = app.saveTags(updateScope, storage.TagEntitySpecies, species.ID, species.Tags)
|
||||
if err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := app.writeJSON(w, http.StatusOK, envelope{"species": species}, nil); err != nil {
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) resolveSpeciesCategory(w http.ResponseWriter, r *http.Request, species *storage.Species, changedID, existingID *int) bool {
|
||||
if changedID == nil {
|
||||
return true
|
||||
}
|
||||
category, err := app.models.SpeciesCategories.Get(*changedID)
|
||||
keepsInactiveCategory := existingID != nil && *existingID == *changedID
|
||||
if err != nil || !category.Active && !keepsInactiveCategory {
|
||||
app.failedValidationResponse(w, r, map[string]string{"category_id": "must reference an active category"})
|
||||
return false
|
||||
}
|
||||
species.Category = category.Name
|
||||
return true
|
||||
}
|
||||
|
||||
func (app *application) deleteSpeciesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, _ := app.readGardenIDParam(r)
|
||||
id, err := app.readIDParam(r)
|
||||
if err != nil {
|
||||
app.notFoundResponse(w, r)
|
||||
return
|
||||
}
|
||||
species, writable := app.requireWritableSpecies(w, r, gardenID, id)
|
||||
if !writable {
|
||||
return
|
||||
}
|
||||
deleteScope := gardenID
|
||||
if species.GardenID == nil {
|
||||
deleteScope = 0
|
||||
}
|
||||
if err := app.models.Species.Delete(deleteScope, id); err != nil {
|
||||
app.respondToEntityModelError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (app *application) respondToEntityModelError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
switch {
|
||||
case errors.Is(err, storage.ErrRecordNotFound):
|
||||
app.notFoundResponse(w, r)
|
||||
case errors.Is(err, storage.ErrEditConflict):
|
||||
app.editConflictResponse(w, r)
|
||||
case errors.Is(err, storage.ErrConflict):
|
||||
app.conflictResponse(w, r)
|
||||
default:
|
||||
app.serverErrorResponse(w, r, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user