package api import ( "fmt" "net/http" "strings" "gardomatic.kleiax.de/internal/platform/validate" "gardomatic.kleiax.de/internal/storage" ) type speciesTaskTemplateInput struct { Title *string `json:"title"` Description *string `json:"description"` TriggerType *storage.TaskTriggerType `json:"trigger_type"` MonthFrom *int `json:"month_from"` DayFrom *int `json:"day_from"` ClearDayFrom bool `json:"clear_day_from"` MonthTo *int `json:"month_to"` DayTo *int `json:"day_to"` ClearDayTo bool `json:"clear_day_to"` OffsetDaysFrom *int `json:"offset_days_from"` OffsetDaysTo *int `json:"offset_days_to"` IntervalDays *int `json:"interval_days"` ClearIntervalDays bool `json:"clear_interval_days"` TriggerOffset *int `json:"trigger_offset"` TriggerOffsetUnit *storage.TaskDurationUnit `json:"trigger_offset_unit"` Duration *int `json:"duration"` DurationUnit *storage.TaskDurationUnit `json:"duration_unit"` Recurrence *storage.TaskRecurrence `json:"recurrence"` RecurrenceInterval *int `json:"recurrence_interval"` Priority *int `json:"priority"` Active *bool `json:"active"` } func (input speciesTaskTemplateInput) apply(template *storage.SpeciesTaskTemplate) { if input.Title != nil { template.Title = strings.TrimSpace(*input.Title) } if input.Description != nil { template.Description = strings.TrimSpace(*input.Description) } if input.TriggerType != nil { template.TriggerType = *input.TriggerType } if input.MonthFrom != nil { template.MonthFrom = positivePointer(input.MonthFrom) } if input.DayFrom != nil { template.DayFrom = positivePointer(input.DayFrom) } if input.ClearDayFrom { template.DayFrom = nil } if input.MonthTo != nil { template.MonthTo = positivePointer(input.MonthTo) } if input.DayTo != nil { template.DayTo = positivePointer(input.DayTo) } if input.ClearDayTo { template.DayTo = nil } if input.OffsetDaysFrom != nil { template.OffsetDaysFrom = input.OffsetDaysFrom } if input.OffsetDaysTo != nil { template.OffsetDaysTo = input.OffsetDaysTo } if input.IntervalDays != nil { template.IntervalDays = positivePointer(input.IntervalDays) } if input.ClearIntervalDays { template.IntervalDays = nil } if input.TriggerOffset != nil { template.TriggerOffset = *input.TriggerOffset } if input.TriggerOffsetUnit != nil { template.TriggerOffsetUnit = *input.TriggerOffsetUnit } if input.Duration != nil { template.Duration = *input.Duration } if input.DurationUnit != nil { template.DurationUnit = *input.DurationUnit } if input.Recurrence != nil { template.Recurrence = *input.Recurrence } if input.RecurrenceInterval != nil { template.RecurrenceInterval = *input.RecurrenceInterval } if input.Priority != nil { template.Priority = *input.Priority } if input.Active != nil { template.Active = *input.Active } if template.TriggerType == storage.TaskTriggerMonthOfYear { template.OffsetDaysFrom, template.OffsetDaysTo = nil, nil } else { template.MonthFrom, template.DayFrom, template.MonthTo, template.DayTo = nil, nil, nil, nil } } func positivePointer(value *int) *int { if value != nil && *value <= 0 { return nil } return value } func (app *application) createSpeciesTaskTemplateHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) speciesID, _ := app.readIDParam(r) if _, ok := app.requireWritableSpecies(w, r, gardenID, speciesID); !ok { return } var input speciesTaskTemplateInput if err := app.readJSON(w, r, &input); err != nil { app.badRequestResponse(w, r, err) return } template := storage.SpeciesTaskTemplate{SpeciesID: speciesID, Origin: storage.TaskTemplateOriginManual, Active: true, TriggerOffsetUnit: storage.TaskDurationDay, DurationUnit: storage.TaskDurationDay, RecurrenceInterval: 1} input.apply(&template) if !app.validateSpeciesTaskTemplate(w, r, template) { return } template, err := app.models.SpeciesTaskTemplates.Insert(template) if err != nil { app.respondToEntityModelError(w, r, err) return } headers := make(http.Header) headers.Set("Location", fmt.Sprintf("/v1/gardens/%d/species/%d/task-templates/%d", gardenID, speciesID, template.ID)) if err := app.writeJSON(w, http.StatusCreated, envelope{"task_template": template}, headers); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) listSpeciesTaskTemplatesHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) speciesID, _ := app.readIDParam(r) if _, err := app.models.Species.Get(gardenID, speciesID); err != nil { app.respondToEntityModelError(w, r, err) return } templates, err := app.models.SpeciesTaskTemplates.GetAllForSpecies(gardenID, speciesID) if err != nil { app.respondToEntityModelError(w, r, err) return } if err := app.writeJSON(w, http.StatusOK, envelope{"task_templates": templates}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) showSpeciesTaskTemplateHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) speciesID, _ := app.readIDParam(r) templateID, err := app.readNamedIDParam(r, "templateID") if err != nil { app.notFoundResponse(w, r) return } template, err := app.models.SpeciesTaskTemplates.Get(gardenID, templateID) if err != nil || template.SpeciesID != speciesID { if err == nil { err = storage.ErrRecordNotFound } app.respondToEntityModelError(w, r, err) return } if err := app.writeJSON(w, http.StatusOK, envelope{"task_template": template}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) updateSpeciesTaskTemplateHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) speciesID, _ := app.readIDParam(r) templateID, err := app.readNamedIDParam(r, "templateID") if err != nil { app.notFoundResponse(w, r) return } species, writable := app.requireWritableSpecies(w, r, gardenID, speciesID) if !writable { return } template, err := app.models.SpeciesTaskTemplates.Get(gardenID, templateID) if err != nil || template.SpeciesID != speciesID { if err == nil { err = storage.ErrRecordNotFound } app.respondToEntityModelError(w, r, err) return } var input speciesTaskTemplateInput if err := app.readJSON(w, r, &input); err != nil { app.badRequestResponse(w, r, err) return } input.apply(&template) if !app.validateSpeciesTaskTemplate(w, r, template) { return } writeScope := gardenID if species.GardenID == nil { writeScope = 0 } template, err = app.models.SpeciesTaskTemplates.Update(writeScope, template) if err != nil { app.respondToEntityModelError(w, r, err) return } if err := app.writeJSON(w, http.StatusOK, envelope{"task_template": template}, nil); err != nil { app.serverErrorResponse(w, r, err) } } func (app *application) deleteSpeciesTaskTemplateHandler(w http.ResponseWriter, r *http.Request) { gardenID, _ := app.readGardenIDParam(r) speciesID, _ := app.readIDParam(r) templateID, err := app.readNamedIDParam(r, "templateID") if err != nil { app.notFoundResponse(w, r) return } species, writable := app.requireWritableSpecies(w, r, gardenID, speciesID) if !writable { return } template, err := app.models.SpeciesTaskTemplates.Get(gardenID, templateID) if err != nil || template.SpeciesID != speciesID { if err == nil { err = storage.ErrRecordNotFound } app.respondToEntityModelError(w, r, err) return } writeScope := gardenID if species.GardenID == nil { writeScope = 0 } if template.Origin != storage.TaskTemplateOriginManual { template.Active = false if _, err := app.models.SpeciesTaskTemplates.Update(writeScope, template); err != nil { app.respondToEntityModelError(w, r, err) return } w.WriteHeader(http.StatusNoContent) return } if err := app.models.SpeciesTaskTemplates.Delete(writeScope, templateID); err != nil { app.respondToEntityModelError(w, r, err) return } w.WriteHeader(http.StatusNoContent) } func (app *application) requireWritableSpecies(w http.ResponseWriter, r *http.Request, gardenID, speciesID int) (storage.Species, bool) { species, err := app.models.Species.Get(gardenID, speciesID) if err != nil { app.respondToEntityModelError(w, r, err) return storage.Species{}, false } if species.GardenID == nil { user, _ := app.contextGetAuthenticatedUser(r) if !user.Can(storage.ApplicationPermissionGlobalSpeciesWrite) { app.permissionDeniedResponse(w, r) return storage.Species{}, false } return species, true } if *species.GardenID != gardenID { app.respondToEntityModelError(w, r, storage.ErrRecordNotFound) return storage.Species{}, false } member, _ := app.contextGetGardenMember(r) if !member.Can(storage.GardenPermissionSpeciesWrite) { app.permissionDeniedResponse(w, r) return storage.Species{}, false } return species, true } func (app *application) validateSpeciesTaskTemplate(w http.ResponseWriter, r *http.Request, template storage.SpeciesTaskTemplate) bool { v := validate.New() storage.ValidateSpeciesTaskTemplate(v, template) if !app.validateConfiguredPriority(w, r, v, template.Priority) { return false } if !v.Valid() { app.failedValidationResponse(w, r, v.Errors) return false } return true }