oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendar
2
3import (
4 "fmt"
5 "html/template"
6 "strconv"
7)
8
9// appFuncs are this app's own template helpers, on top of rastrillo's.
10//
11// Each one exists because the alternative is arithmetic or branching
12// inside a template, and a template is the one place in this app that
13// has no tests and no compiler. Anything with a decision in it is
14// computed in Go and printed here.
15var appFuncs = template.FuncMap{
16 "if_eq": ifEq,
17 "repeatOptions": repeatOptions,
18 "calendarOptions": calendarOptions,
19 "eventCancelHref": eventCancelHref,
20 "zoneOptions": zoneOptions,
21 "weekStartOptions": weekStartOptions,
22 "minutesLabel": minutesLabel,
23 "duration": humanDuration,
24 "feedURL": feedURL,
25 "deleteAllLabel": deleteAllLabel,
26 "canWrite": CanWrite,
27}
28
29// deleteAllLabel names the expensive option with its own count, so the
30// warning is in the label rather than in a sentence beside it.
31//
32// "All 52 of them" is a different decision from "All". A series with no
33// end has no count to give, and says that instead of inventing one.
34func deleteAllLabel(count int, forever bool) string {
35 if forever || count <= 0 {
36 return "Every occurrence, including all the future ones"
37 }
38 if count == 1 {
39 return "The only occurrence"
40 }
41 return fmt.Sprintf("All %d of them", count)
42}
43
44// ifEq returns value when field matches want, and "" otherwise. It is
45// how a shared error string is shown against exactly the field it
46// belongs to, without the form data carrying one error slot per field.
47func ifEq(field, want, value string) string {
48 if field == want {
49 return value
50 }
51 return ""
52}
53
54// repeatOptions builds the repeat picker, labelled against this event's
55// own start date so it says "Every Wednesday" rather than "Weekly".
56func repeatOptions(startLocal, tz string, allDay bool, selected string) []map[string]any {
57 if selected == "" {
58 selected = RepeatNone
59 }
60 var out []map[string]any
61 for _, c := range RepeatChoicesFor(startLocal, tz, allDay) {
62 out = append(out, map[string]any{
63 "Value": c.Value,
64 "Label": c.Label,
65 "Selected": c.Value == selected,
66 })
67 }
68 return out
69}
70
71func calendarOptions(cals []CalendarView, selected int64) []map[string]any {
72 var out []map[string]any
73 for _, c := range cals {
74 out = append(out, map[string]any{
75 "Value": strconv.FormatInt(c.ID, 10),
76 "Label": c.Name,
77 "Selected": c.ID == selected,
78 })
79 }
80 return out
81}
82
83// eventCancelHref sends Cancel back where it came from: the event's own
84// page when editing, and the calendar when creating, because a new
85// event has no page to return to.
86func eventCancelHref(eventID int64) string {
87 if eventID == 0 {
88 return "/today"
89 }
90 return "/events/" + strconv.FormatInt(eventID, 10)
91}
92
93func zoneOptions(selected string) []map[string]any {
94 var out []map[string]any
95 found := false
96 for _, z := range CommonZones {
97 if z == selected {
98 found = true
99 }
100 out = append(out, map[string]any{"Value": z, "Label": z, "Selected": z == selected})
101 }
102 // Somebody in a zone the short list omits must not have it silently
103 // replaced by the first option the moment they save anything else.
104 if !found && selected != "" {
105 out = append(out, map[string]any{"Value": selected, "Label": selected, "Selected": true})
106 }
107 return out
108}
109
110func weekStartOptions(selected int) []map[string]any {
111 return []map[string]any{
112 {"Value": strconv.Itoa(WeekStartMonday), "Label": "Monday", "Selected": selected == WeekStartMonday},
113 {"Value": strconv.Itoa(WeekStartSaturday), "Label": "Saturday", "Selected": selected == WeekStartSaturday},
114 {"Value": strconv.Itoa(WeekStartSunday), "Label": "Sunday", "Selected": selected == WeekStartSunday},
115 }
116}
117
118// minutesLabel writes a reminder offset the way somebody would say it.
119func minutesLabel(min int) string {
120 switch {
121 case min == 0:
122 return "at the time it starts"
123 case min < 0:
124 return fmt.Sprintf("%s afterwards", humanDuration(-min))
125 case min%(24*60) == 0:
126 d := min / (24 * 60)
127 if d == 1 {
128 return "one day before"
129 }
130 return fmt.Sprintf("%d days before", d)
131 default:
132 return humanDuration(min) + " before"
133 }
134}
135
136// feedURL builds the absolute subscription URL. Absolute because a
137// calendar client is given this by copy and paste and has no page to
138// resolve a relative path against.
139func feedURL(origin, token string) string {
140 return origin + "/feed/" + token + ".ics"
141}
142