@@ -0,0 +1,392 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gardomatic.kleiax.de/lib/client"
|
||||
)
|
||||
|
||||
type speciesForm struct {
|
||||
Global bool `form:"global"`
|
||||
CommonName string `form:"common_name"`
|
||||
Cultivar string `form:"cultivar"`
|
||||
BotanicalName string `form:"botanical_name"`
|
||||
CategoryID int `form:"category_id"`
|
||||
SunExposure string `form:"sun_exposure"`
|
||||
SoilCondition string `form:"soil_condition"`
|
||||
SoilReaction string `form:"soil_reaction"`
|
||||
WinterProtection string `form:"winter_protection"`
|
||||
SpacingCM int `form:"spacing_cm"`
|
||||
HeightCM int `form:"height_cm"`
|
||||
Notes string `form:"notes"`
|
||||
ImageData string `form:"image_data"`
|
||||
ImageID int `form:"image_id"`
|
||||
Attributes string `form:"attributes"`
|
||||
TemplateSpeciesID int `form:"template_species_id"`
|
||||
Keywords string `form:"keywords"`
|
||||
SowMonthFrom int `form:"sow_month_from"`
|
||||
SowDayFrom int `form:"sow_day_from"`
|
||||
SowMonthTo int `form:"sow_month_to"`
|
||||
SowDayTo int `form:"sow_day_to"`
|
||||
PlantingMonthFrom int `form:"planting_month_from"`
|
||||
PlantingDayFrom int `form:"planting_day_from"`
|
||||
PlantingMonthTo int `form:"planting_month_to"`
|
||||
PlantingDayTo int `form:"planting_day_to"`
|
||||
HarvestMonthFrom int `form:"harvest_month_from"`
|
||||
HarvestDayFrom int `form:"harvest_day_from"`
|
||||
HarvestMonthTo int `form:"harvest_month_to"`
|
||||
HarvestDayTo int `form:"harvest_day_to"`
|
||||
SowDuration int `form:"sow_duration"`
|
||||
SowDurationUnit string `form:"sow_duration_unit"`
|
||||
PlantingDuration int `form:"planting_duration"`
|
||||
PlantingDurationUnit string `form:"planting_duration_unit"`
|
||||
HarvestDuration int `form:"harvest_duration"`
|
||||
HarvestDurationUnit string `form:"harvest_duration_unit"`
|
||||
Errors map[string]string
|
||||
}
|
||||
|
||||
func (app *application) speciesList(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, err := app.readPathID(r, "gardenID")
|
||||
if err != nil {
|
||||
app.notFound(w)
|
||||
return
|
||||
}
|
||||
apiClient := client.FromContext(r.Context())
|
||||
garden, _, err := apiClient.Garden(r.Context(), gardenID)
|
||||
if err != nil {
|
||||
app.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
species, _, err := apiClient.SpeciesForGarden(r.Context(), gardenID)
|
||||
if err != nil {
|
||||
app.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
data := app.newTemplateData(r)
|
||||
data.Filters = collectionFilters(r, "q", "origin", "sort")
|
||||
data.Garden, data.Species = &garden, filterAndSortSpecies(species, data.Filters)
|
||||
data.Species, data.Pagination = paginateCollection(r, data.Species)
|
||||
app.render(w, http.StatusOK, "species.tmpl", data)
|
||||
}
|
||||
func (app *application) speciesCreate(w http.ResponseWriter, r *http.Request) {
|
||||
form := speciesForm{SowDayFrom: 1, PlantingDayFrom: 1, HarvestDayFrom: 1, SowDuration: 1, PlantingDuration: 1, HarvestDuration: 1, SowDurationUnit: "week", PlantingDurationUnit: "week", HarvestDurationUnit: "week", Errors: map[string]string{}}
|
||||
if templateID, _ := strconv.Atoi(r.URL.Query().Get("template_id")); templateID > 0 {
|
||||
gardenID, err := app.readPathID(r, "gardenID")
|
||||
if err == nil {
|
||||
if source, _, loadErr := client.FromContext(r.Context()).Species(r.Context(), gardenID, templateID); loadErr == nil {
|
||||
form = speciesFormFromSpecies(source)
|
||||
form.CommonName = ""
|
||||
form.Global = false
|
||||
form.TemplateSpeciesID = source.ID
|
||||
} else {
|
||||
app.handleAPIError(w, r, loadErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
app.renderSpeciesForm(w, r, form, http.StatusOK, 0)
|
||||
}
|
||||
func (app *application) speciesEdit(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, err := app.readPathID(r, "gardenID")
|
||||
if err != nil {
|
||||
app.notFound(w)
|
||||
return
|
||||
}
|
||||
speciesID, err := app.readPathID(r, "speciesID")
|
||||
if err != nil {
|
||||
app.notFound(w)
|
||||
return
|
||||
}
|
||||
value, _, err := client.FromContext(r.Context()).Species(r.Context(), gardenID, speciesID)
|
||||
if err != nil {
|
||||
app.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
if value.GardenID != nil && *value.GardenID != gardenID {
|
||||
app.notFound(w)
|
||||
return
|
||||
}
|
||||
form := speciesFormFromSpecies(value)
|
||||
app.renderSpeciesForm(w, r, form, http.StatusOK, speciesID)
|
||||
}
|
||||
|
||||
func speciesFormFromSpecies(value client.Species) speciesForm {
|
||||
form := speciesForm{Global: value.GardenID == nil, CommonName: value.CommonName, Cultivar: value.Cultivar, BotanicalName: value.BotanicalName, Notes: value.Notes, ImageData: value.ImageData, Attributes: string(value.Attributes), Keywords: strings.Join(value.Tags, ", "), Errors: map[string]string{}}
|
||||
if value.ImageID != nil {
|
||||
form.ImageID = *value.ImageID
|
||||
}
|
||||
if value.SunExposure != nil {
|
||||
form.SunExposure = *value.SunExposure
|
||||
}
|
||||
if value.SoilCondition != nil {
|
||||
form.SoilCondition = *value.SoilCondition
|
||||
}
|
||||
if value.SoilReaction != nil {
|
||||
form.SoilReaction = *value.SoilReaction
|
||||
}
|
||||
if value.WinterProtection != nil {
|
||||
form.WinterProtection = *value.WinterProtection
|
||||
}
|
||||
copyOptionalInt(value.SpacingCM, &form.SpacingCM)
|
||||
copyOptionalInt(value.HeightCM, &form.HeightCM)
|
||||
copyOptionalInt(value.CategoryID, &form.CategoryID)
|
||||
copyOptionalInt(value.SowMonthFrom, &form.SowMonthFrom)
|
||||
copyOptionalInt(value.SowDayFrom, &form.SowDayFrom)
|
||||
copyOptionalInt(value.SowMonthTo, &form.SowMonthTo)
|
||||
copyOptionalInt(value.SowDayTo, &form.SowDayTo)
|
||||
copyOptionalInt(value.PlantingMonthFrom, &form.PlantingMonthFrom)
|
||||
copyOptionalInt(value.PlantingDayFrom, &form.PlantingDayFrom)
|
||||
copyOptionalInt(value.PlantingMonthTo, &form.PlantingMonthTo)
|
||||
copyOptionalInt(value.PlantingDayTo, &form.PlantingDayTo)
|
||||
copyOptionalInt(value.HarvestMonthFrom, &form.HarvestMonthFrom)
|
||||
copyOptionalInt(value.HarvestDayFrom, &form.HarvestDayFrom)
|
||||
copyOptionalInt(value.HarvestMonthTo, &form.HarvestMonthTo)
|
||||
copyOptionalInt(value.HarvestDayTo, &form.HarvestDayTo)
|
||||
form.SowDuration, form.SowDurationUnit = rangeDuration(value.SowMonthFrom, value.SowDayFrom, value.SowMonthTo, value.SowDayTo)
|
||||
form.PlantingDuration, form.PlantingDurationUnit = rangeDuration(value.PlantingMonthFrom, value.PlantingDayFrom, value.PlantingMonthTo, value.PlantingDayTo)
|
||||
form.HarvestDuration, form.HarvestDurationUnit = rangeDuration(value.HarvestMonthFrom, value.HarvestDayFrom, value.HarvestMonthTo, value.HarvestDayTo)
|
||||
return form
|
||||
}
|
||||
func (app *application) speciesSave(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, err := app.readPathID(r, "gardenID")
|
||||
if err != nil {
|
||||
app.notFound(w)
|
||||
return
|
||||
}
|
||||
speciesID := 0
|
||||
if id, readErr := app.readPathID(r, "speciesID"); readErr == nil {
|
||||
speciesID = id
|
||||
}
|
||||
var form speciesForm
|
||||
if err := app.decodePostForm(r, &form); err != nil {
|
||||
app.clientError(w, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
form.CommonName, form.Cultivar, form.BotanicalName, form.Notes = strings.TrimSpace(form.CommonName), strings.TrimSpace(form.Cultivar), strings.TrimSpace(form.BotanicalName), strings.TrimSpace(form.Notes)
|
||||
form.Errors = map[string]string{}
|
||||
if form.CommonName == "" {
|
||||
form.Errors["common_name"] = "Ein Name ist erforderlich."
|
||||
}
|
||||
input := speciesInput(form)
|
||||
if len(form.Errors) == 0 {
|
||||
apiClient := client.FromContext(r.Context())
|
||||
var saveErr error
|
||||
savedSpeciesID := speciesID
|
||||
if speciesID > 0 {
|
||||
_, _, saveErr = apiClient.UpdateSpecies(r.Context(), gardenID, speciesID, input)
|
||||
} else {
|
||||
var created client.Species
|
||||
created, _, saveErr = apiClient.CreateSpecies(r.Context(), gardenID, input)
|
||||
savedSpeciesID = created.ID
|
||||
if saveErr == nil && form.TemplateSpeciesID > 0 {
|
||||
saveErr = app.copySpeciesTaskTemplates(r, apiClient, gardenID, form.TemplateSpeciesID, created.ID)
|
||||
if saveErr != nil {
|
||||
_, _ = apiClient.DeleteSpecies(r.Context(), gardenID, created.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if saveErr == nil {
|
||||
if r.FormValue("continue") == "care" {
|
||||
http.Redirect(w, r, pathWithQuery(webPath("species.edit", gardenID, savedSpeciesID), "step", "care"), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webPath("species", gardenID), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
var apiError *client.APIError
|
||||
if errors.As(saveErr, &apiError) && apiError.StatusCode == http.StatusUnprocessableEntity {
|
||||
form.Errors = apiError.Validation
|
||||
} else {
|
||||
app.handleAPIError(w, r, saveErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
app.renderSpeciesForm(w, r, form, http.StatusUnprocessableEntity, speciesID)
|
||||
}
|
||||
|
||||
func speciesInput(form speciesForm) client.SpeciesInput {
|
||||
input := client.SpeciesInput{Global: form.Global, CommonName: &form.CommonName, Cultivar: &form.Cultivar, BotanicalName: &form.BotanicalName, Notes: &form.Notes, ImageData: &form.ImageData, Tags: parseKeywords(form.Keywords)}
|
||||
if form.ImageID > 0 {
|
||||
imageID := form.ImageID
|
||||
input.ImageID = &imageID
|
||||
}
|
||||
input.SunExposure, input.SoilCondition, input.SoilReaction, input.WinterProtection = &form.SunExposure, &form.SoilCondition, &form.SoilReaction, &form.WinterProtection
|
||||
if form.SpacingCM > 0 {
|
||||
input.SpacingCM = &form.SpacingCM
|
||||
}
|
||||
if form.HeightCM > 0 {
|
||||
input.HeightCM = &form.HeightCM
|
||||
}
|
||||
if json.Valid([]byte(form.Attributes)) {
|
||||
input.Attributes = json.RawMessage(form.Attributes)
|
||||
}
|
||||
if form.CategoryID > 0 {
|
||||
input.CategoryID = &form.CategoryID
|
||||
} else {
|
||||
input.ClearCategoryID = true
|
||||
}
|
||||
assignPositive := func(value int) *int {
|
||||
if value > 0 {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
form.SowMonthTo, form.SowDayTo = rangeEnd(form.SowMonthFrom, form.SowDayFrom, form.SowDuration, form.SowDurationUnit)
|
||||
form.PlantingMonthTo, form.PlantingDayTo = rangeEnd(form.PlantingMonthFrom, form.PlantingDayFrom, form.PlantingDuration, form.PlantingDurationUnit)
|
||||
form.HarvestMonthTo, form.HarvestDayTo = rangeEnd(form.HarvestMonthFrom, form.HarvestDayFrom, form.HarvestDuration, form.HarvestDurationUnit)
|
||||
input.SowMonthFrom, input.SowDayFrom, input.SowMonthTo, input.SowDayTo = assignPositive(form.SowMonthFrom), assignPositive(form.SowDayFrom), assignPositive(form.SowMonthTo), assignPositive(form.SowDayTo)
|
||||
input.PlantingMonthFrom, input.PlantingDayFrom, input.PlantingMonthTo, input.PlantingDayTo = assignPositive(form.PlantingMonthFrom), assignPositive(form.PlantingDayFrom), assignPositive(form.PlantingMonthTo), assignPositive(form.PlantingDayTo)
|
||||
input.HarvestMonthFrom, input.HarvestDayFrom, input.HarvestMonthTo, input.HarvestDayTo = assignPositive(form.HarvestMonthFrom), assignPositive(form.HarvestDayFrom), assignPositive(form.HarvestMonthTo), assignPositive(form.HarvestDayTo)
|
||||
input.ClearSowRange = form.SowMonthFrom == 0 && form.SowMonthTo == 0
|
||||
input.ClearPlantingRange = form.PlantingMonthFrom == 0 && form.PlantingMonthTo == 0
|
||||
input.ClearHarvestRange = form.HarvestMonthFrom == 0 && form.HarvestMonthTo == 0
|
||||
input.ClearSowDayFrom, input.ClearSowDayTo = form.SowDayFrom == 0, form.SowDayTo == 0
|
||||
input.ClearPlantingDayFrom, input.ClearPlantingDayTo = form.PlantingDayFrom == 0, form.PlantingDayTo == 0
|
||||
input.ClearHarvestDayFrom, input.ClearHarvestDayTo = form.HarvestDayFrom == 0, form.HarvestDayTo == 0
|
||||
return input
|
||||
}
|
||||
|
||||
func rangeEnd(month, day, duration int, unit string) (int, int) {
|
||||
if month < 1 {
|
||||
return 0, 0
|
||||
}
|
||||
if day < 1 {
|
||||
day = 1
|
||||
}
|
||||
start := time.Date(2024, time.Month(month), day, 0, 0, 0, 0, time.UTC)
|
||||
switch unit {
|
||||
case "month":
|
||||
start = start.AddDate(0, duration, 0)
|
||||
case "week":
|
||||
start = start.AddDate(0, 0, 7*duration)
|
||||
default:
|
||||
start = start.AddDate(0, 0, duration)
|
||||
}
|
||||
return int(start.Month()), start.Day()
|
||||
}
|
||||
func rangeDuration(fromMonth, fromDay, toMonth, toDay *int) (int, string) {
|
||||
if fromMonth == nil || toMonth == nil {
|
||||
return 1, "week"
|
||||
}
|
||||
fd, td := 1, 1
|
||||
if fromDay != nil {
|
||||
fd = *fromDay
|
||||
}
|
||||
if toDay != nil {
|
||||
td = *toDay
|
||||
}
|
||||
from := time.Date(2024, time.Month(*fromMonth), fd, 0, 0, 0, 0, time.UTC)
|
||||
to := time.Date(2024, time.Month(*toMonth), td, 0, 0, 0, 0, time.UTC)
|
||||
if to.Before(from) {
|
||||
to = to.AddDate(1, 0, 0)
|
||||
}
|
||||
days := int(to.Sub(from).Hours() / 24)
|
||||
if days%7 == 0 && days > 0 {
|
||||
return days / 7, "week"
|
||||
}
|
||||
return days, "day"
|
||||
}
|
||||
|
||||
func (app *application) copySpeciesTaskTemplates(r *http.Request, apiClient *client.Client, gardenID, sourceID, targetID int) error {
|
||||
templates, _, err := apiClient.SpeciesTaskTemplates(r.Context(), gardenID, sourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range templates {
|
||||
if item.Origin != "" && item.Origin != "manual" {
|
||||
continue
|
||||
}
|
||||
title, description, triggerType := item.Title, item.Description, item.TriggerType
|
||||
triggerOffset, triggerOffsetUnit := item.TriggerOffset, item.TriggerOffsetUnit
|
||||
duration, durationUnit := item.Duration, item.DurationUnit
|
||||
recurrence, recurrenceInterval, priority, active := item.Recurrence, item.RecurrenceInterval, item.Priority, item.Active
|
||||
input := client.SpeciesTaskTemplateInput{
|
||||
Title: &title, Description: &description, TriggerType: &triggerType,
|
||||
MonthFrom: item.MonthFrom, DayFrom: item.DayFrom, MonthTo: item.MonthTo, DayTo: item.DayTo,
|
||||
OffsetDaysFrom: item.OffsetDaysFrom, OffsetDaysTo: item.OffsetDaysTo, IntervalDays: item.IntervalDays,
|
||||
TriggerOffset: &triggerOffset, TriggerOffsetUnit: &triggerOffsetUnit,
|
||||
Duration: &duration, DurationUnit: &durationUnit, Recurrence: &recurrence,
|
||||
RecurrenceInterval: &recurrenceInterval, Priority: &priority, Active: &active,
|
||||
}
|
||||
if _, _, err = apiClient.CreateSpeciesTaskTemplate(r.Context(), gardenID, targetID, input); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (app *application) speciesDelete(w http.ResponseWriter, r *http.Request) {
|
||||
gardenID, err := app.readPathID(r, "gardenID")
|
||||
if err != nil {
|
||||
app.notFound(w)
|
||||
return
|
||||
}
|
||||
speciesID, err := app.readPathID(r, "speciesID")
|
||||
if err != nil {
|
||||
app.notFound(w)
|
||||
return
|
||||
}
|
||||
if _, err := client.FromContext(r.Context()).DeleteSpecies(r.Context(), gardenID, speciesID); err != nil {
|
||||
app.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webPath("species", gardenID), http.StatusSeeOther)
|
||||
}
|
||||
func (app *application) renderSpeciesForm(w http.ResponseWriter, r *http.Request, form speciesForm, status, speciesID int) {
|
||||
gardenID, err := app.readPathID(r, "gardenID")
|
||||
if err != nil {
|
||||
app.notFound(w)
|
||||
return
|
||||
}
|
||||
apiClient := client.FromContext(r.Context())
|
||||
garden, _, err := apiClient.Garden(r.Context(), gardenID)
|
||||
if err != nil {
|
||||
app.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
data := app.newTemplateData(r)
|
||||
data.Garden, data.Form, data.SpeciesID, data.SpeciesGlobal = &garden, form, speciesID, form.Global
|
||||
data.WizardStep = r.URL.Query().Get("step")
|
||||
if speciesID > 0 && data.WizardStep == "" {
|
||||
data.WizardStep = "all"
|
||||
} else if data.WizardStep != "general" && data.WizardStep != "care" && data.WizardStep != "tasks" {
|
||||
data.WizardStep = "general"
|
||||
}
|
||||
data.Images, err = app.loadImageLibrary(r, gardenID)
|
||||
if err != nil {
|
||||
app.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
data.TagSuggestions = app.loadTagSuggestions(r, gardenID)
|
||||
categories, _, err := apiClient.SpeciesCategories(r.Context())
|
||||
if err != nil {
|
||||
app.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
data.SpeciesCategories = categories
|
||||
if speciesID == 0 {
|
||||
data.Species, _, err = apiClient.SpeciesForGarden(r.Context(), gardenID)
|
||||
if err != nil {
|
||||
app.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if speciesID > 0 {
|
||||
templates, _, err := apiClient.SpeciesTaskTemplates(r.Context(), gardenID, speciesID)
|
||||
if err != nil {
|
||||
app.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
data.TaskTemplates = map[int][]client.SpeciesTaskTemplate{speciesID: templates}
|
||||
data.CareInstructions, _, err = apiClient.CareInstructions(r.Context(), gardenID, speciesID)
|
||||
if err != nil {
|
||||
app.handleAPIError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
app.render(w, status, "species_form.tmpl", data)
|
||||
}
|
||||
Reference in New Issue
Block a user