package web import ( "bytes" "fmt" "html/template" "io/fs" "path/filepath" "strings" "time" "gardomatic.kleiax.de/lib/client" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" ) type locationNode struct { Location client.Location Children []locationNode } func buildLocationTree(locations []client.Location) []locationNode { children := make(map[int][]client.Location) known := make(map[int]bool, len(locations)) for _, location := range locations { known[location.ID] = true } for _, location := range locations { parent := 0 if location.ParentID != nil && known[*location.ParentID] { parent = *location.ParentID } children[parent] = append(children[parent], location) } var build func(int, map[int]bool) []locationNode build = func(parent int, ancestors map[int]bool) []locationNode { result := make([]locationNode, 0, len(children[parent])) for _, location := range children[parent] { if ancestors[location.ID] { continue } next := make(map[int]bool, len(ancestors)+1) for id := range ancestors { next[id] = true } next[location.ID] = true result = append(result, locationNode{Location: location, Children: build(location.ID, next)}) } return result } return build(0, map[int]bool{}) } func flattenLocationTree(nodes []locationNode) []client.Location { locations := make([]client.Location, 0) var appendNodes func([]locationNode) appendNodes = func(items []locationNode) { for _, node := range items { locations = append(locations, node.Location) appendNodes(node.Children) } } appendNodes(nodes) return locations } func humanDate(t time.Time) string { if t.IsZero() { return "" } return t.UTC().Format("02 Jan 2006 at 15:04") } func speciesName(species []client.Species, id *int) string { if id == nil { return "Ohne Artzuordnung" } for _, item := range species { if item.ID == *id { if item.Cultivar != "" { return item.CommonName + " · " + item.Cultivar } return item.CommonName } } return "Unbekannte Art" } func plantName(plants []client.Plant, id *int) string { if id == nil { return "" } for _, plant := range plants { if plant.ID == *id { return plant.Name } } return "Unbekannte Pflanze" } func locationName(locations []client.Location, id *int) string { if id == nil { return "" } for _, location := range locations { if location.ID == *id { return location.Name } } return "Unbekannter Ort" } func taskDue(task client.Task) string { format := func(value *time.Time) string { if value == nil { return "" } return value.Local().Format("02.01.2006 15:04") } start, end := format(task.DueAtStart), format(task.DueAtEnd) if start != "" && end != "" { return start + " - " + end } if end != "" { return "bis " + end } if start != "" { return "ab " + start } return "Ohne Fälligkeit" } func taskDueDate(task client.Task) string { format := func(value *time.Time) string { if value == nil { return "" } return value.Local().Format("02.01.2006") } start, end := format(task.DueAtStart), format(task.DueAtEnd) if start != "" && end != "" { if start == end { return start } return start + " - " + end } if end != "" { return "bis " + end } if start != "" { return "ab " + start } return "Ohne Fälligkeit" } func priorityName(priority int) string { switch { case priority >= 5: return "Hoch" case priority > 0: return "Erhöht" case priority < 0: return "Niedrig" default: return "Normal" } } func configuredPriorityName(priorities []client.TaskPriority, priority int) string { for _, option := range priorities { if option.Value == priority { return option.Name } } return priorityName(priority) } type statusOption struct { Value string Label string } func plantStatuses() []statusOption { return []statusOption{ {Value: "alive", Label: "Lebendig"}, {Value: "dead", Label: "Tot"}, {Value: "removed", Label: "Entfernt"}, {Value: "infested", Label: "Befallen"}, {Value: "harvested", Label: "Geerntet"}, } } func plantStatusName(status string) string { for _, option := range plantStatuses() { if option.Value == status { return option.Label } } return status } func careStatuses() []statusOption { return []statusOption{{Value: "good", Label: "Gut"}, {Value: "bad", Label: "Schlecht"}, {Value: "untested", Label: "Ungetestet"}, {Value: "testing", Label: "In Testung"}, {Value: "planned", Label: "Geplant"}} } func lifecycleName(value *string) string { if value == nil { return "" } switch *value { case "annual": return "Einjährig" case "biennial": return "Zweijährig" case "perennial": return "Mehrjährig" default: return "" } } type monthOption struct { Value int Label string } func months() []monthOption { return []monthOption{ {Value: 1, Label: "Januar"}, {Value: 2, Label: "Februar"}, {Value: 3, Label: "März"}, {Value: 4, Label: "April"}, {Value: 5, Label: "Mai"}, {Value: 6, Label: "Juni"}, {Value: 7, Label: "Juli"}, {Value: 8, Label: "August"}, {Value: 9, Label: "September"}, {Value: 10, Label: "Oktober"}, {Value: 11, Label: "November"}, {Value: 12, Label: "Dezember"}, } } func recurrenceName(recurrence string) string { switch recurrence { case "daily": return "Täglich" case "weekly": return "Wöchentlich" case "monthly": return "Monatlich" case "yearly": return "Jährlich" default: return "" } } func recurrenceDescription(recurrence string, interval int) string { if recurrence == "" { return "" } if interval < 1 { interval = 1 } units := map[string][2]string{"daily": {"Tag", "Tage"}, "weekly": {"Woche", "Wochen"}, "monthly": {"Monat", "Monate"}, "yearly": {"Jahr", "Jahre"}} unit, ok := units[recurrence] if !ok { return "" } label := unit[1] if interval == 1 { label = unit[0] } return fmt.Sprintf("Alle %d %s", interval, label) } func durationDescription(amount int, unit string) string { units := map[string][2]string{"day": {"Tag", "Tage"}, "week": {"Woche", "Wochen"}, "month": {"Monat", "Monate"}} labels, ok := units[unit] if !ok { return "" } label := labels[1] if amount == 1 { label = labels[0] } return fmt.Sprintf("%d %s", amount, label) } func plantTaskData(garden *client.Garden, task plantTaskForm) *templateData { return &templateData{commonTemplateData: commonTemplateData{Garden: garden}, plantTemplateData: plantTemplateData{PlantTasks: []plantTaskForm{task}}} } var functions = template.FuncMap{ "webPath": webPath, "pathWithQuery": pathWithQuery, "gardenAwarePath": gardenAwarePath, "dict": func(values ...any) map[string]any { result := map[string]any{} for i := 0; i+1 < len(values); i += 2 { key, _ := values[i].(string) result[key] = values[i+1] } return result }, "humanDate": humanDate, "journalDate": func(value time.Time) string { return value.Local().Format("02.01.2006 · 15:04 Uhr") }, "fileSize": func(value int64) string { if value >= 1<<20 { return fmt.Sprintf("%.1f MB", float64(value)/(1<<20)) } if value >= 1<<10 { return fmt.Sprintf("%.1f kB", float64(value)/(1<<10)) } return fmt.Sprintf("%d B", value) }, "hasPrefix": strings.HasPrefix, "markdown": func(value string) template.HTML { var output bytes.Buffer parser := goldmark.New(goldmark.WithExtensions(extension.GFM)) if err := parser.Convert([]byte(value), &output); err != nil { return template.HTML(template.HTMLEscapeString(value)) } return template.HTML(output.String()) }, "speciesName": speciesName, "plantName": plantName, "locationName": locationName, "taskDue": taskDue, "taskDueDate": taskDueDate, "priorityName": priorityName, "configuredPriorityName": configuredPriorityName, "plantStatuses": plantStatuses, "plantStatusName": plantStatusName, "careStatuses": careStatuses, "lifecycleName": lifecycleName, "months": months, "recurrenceName": recurrenceName, "recurrenceDescription": recurrenceDescription, "durationDescription": durationDescription, "plantTaskData": plantTaskData, "calendarDate": func(value time.Time) string { return value.Format("02.01.2006") }, "dateValue": func(value time.Time) string { return value.Format("2006-01-02") }, "eqInt": func(left, right int) bool { return left == right }, "neInt": func(left, right int) bool { return left != right }, "containsInt": func(values []int, target int) bool { for _, value := range values { if value == target { return true } } return false }, "containsString": func(values []string, target string) bool { for _, value := range values { if value == target { return true } } return false }, "add": func(left, right int) int { return left + right }, "sub": func(left, right int) int { return left - right }, "eqString": func(left, right string) bool { return left == right }, "neString": func(left, right string) bool { return left != right }, "stringValue": func(value *string) string { if value == nil { return "" } return *value }, "canGarden": func(garden *client.Garden, permission string) bool { return garden != nil && garden.Can(permission) }, "canUser": func(user *client.User, permission string) bool { return user != nil && user.Can(permission) }, "canGardenResource": func(garden *client.Garden, user *client.User, createdBy int, own, other string) bool { if garden == nil { return false } if user == nil { return garden.Can(other) } if createdBy == user.ID { return garden.Can(own) } return garden.Can(other) }, "isAppAdmin": func(user *client.User) bool { return user != nil && user.IsAdmin() }, "canGlobalSpecies": func(user *client.User) bool { return user != nil && user.Can("global_species:write") }, "canEditSpecies": func(garden *client.Garden, user *client.User, global bool, speciesID int) bool { if speciesID == 0 { return garden != nil && garden.Can("species:write") || user != nil && user.Can("global_species:write") } if global { return user != nil && user.Can("global_species:write") } return garden != nil && garden.Can("species:write") }, } func newTemplateCache() (map[string]*template.Template, error) { cache := map[string]*template.Template{} pages, err := fs.Glob(files, "templates/pages/*.tmpl") if err != nil { return nil, err } for _, page := range pages { name := filepath.Base(page) patterns := []string{ "templates/layout/base.tmpl", "templates/partials/*.tmpl", "templates/fragments/*.tmpl", page, } ts, err := template.New(name).Funcs(functions).ParseFS(files, patterns...) if err != nil { return nil, err } cache[name] = ts } return cache, nil }