@@ -0,0 +1,426 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user