Files
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

427 lines
11 KiB
Go

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
}