427 lines
11 KiB
Go
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
|
|
}
|