hivedav

A curlable free/busy scheduler with CalDAV integration
git clone https://git.in0rdr.ch/hivedav.git
Log | Files | Refs | Pull requests | README | LICENSE

commit d08def228f6b05aeec76130dcd285f6b9f24312c
parent 6306164c020de6df16af94ec2a60a964eae67b5b
Author: Andreas Gruhler <andreas.gruhler@adfinis.com>
Date:   Sun, 17 Sep 2023 11:39:22 +0200

fix: dayOfISOWeek loop and html tmpl

Diffstat:
Acss/index.css | 33+++++++++++++++++++++++++++++++++
Acss/style.css | 27+++++++++++++++++++++++++++
Mmain.go | 1+
Mserver.go | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Atemplates/index.html | 32++++++++++++++++++++++++++++++++
5 files changed, 217 insertions(+), 62 deletions(-)

diff --git a/css/index.css b/css/index.css @@ -0,0 +1,33 @@ +* { + padding: 0; + margin: 0; + font-family: monospace; + font-size: 18px; +} + +a { + color: black; +} + +h1, h2, p { + padding: 10px 20px; +} + +button, input { + width: 95%; + padding: 5px 10px; + border-radius: 3px; + box-shadow: 0 1px #b1b0b6; + background: white; + border: 1px solid #f3f2f7; + color: black; +} + +button { + cursor: pointer; + text-align: center; +} + +button:disabled { + display: none; +} diff --git a/css/style.css b/css/style.css @@ -0,0 +1,27 @@ +table { + border-collapse: collapse; + width: 100%; +} + +th, td { + padding: 5px 20px; + text-align: left; + border-right: 1px solid #b0b0b6; +} + +th { + font-weight: normal; + font-size: 0.8em; + text-transform: uppercase; + color: #b0b0b6; +} + +tbody tr:nth-child(even):not(.input) { + background-color: #f9f9fc; +} + +footer { + margin-top: 5px; + padding: 5px; + flex-wrap: wrap; +} diff --git a/main.go b/main.go @@ -68,6 +68,7 @@ func main() { router := httprouter.New() router.GET("/", server.Index) router.GET("/week/:week", server.Week) + router.ServeFiles("/css/*filepath", http.Dir("./css")) log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", conf.ListenAddress, conf.ListenPort), router)) } diff --git a/server.go b/server.go @@ -10,6 +10,7 @@ import ( "hivedav/caldav" "fmt" "github.com/pquerna/termchalk/prettytable" + "html/template" _ "github.com/mattn/go-sqlite3" "database/sql" "io" @@ -24,6 +25,14 @@ type Server struct { db *sql.DB } +type TableData struct { + TableHead []string + Rows [][]string + Week int + Year int + Version string +} + var drop = `DROP TABLE IF EXISTS availability; DROP TABLE IF EXISTS availability_1;` var create_availability = `CREATE TABLE IF NOT EXISTS availability ( @@ -68,32 +77,44 @@ func (s *Server) NewServer(c *config.Config) (*Server, error) { return s, nil } +// Returns Time with requested weekday and week. If out of bounds, the returned +// week is set to the maximum for the specific year. // https://xferion.com/golang-reverse-isoweek-get-the-date-of-the-first-day-of-iso-week -func dayOfISOWeek(weekday int, week int, year int) time.Time { - timeNow := time.Now() - yearNow, weekNow:= timeNow.ISOWeek() +func dayOfISOWeek(weekday int, week int, year int) (timeIt time.Time, weekNow int) { + // check out of bounds week (last week of the year) + limit := time.Date(year, 12, 31, 0, 0, 0, 0, time.Local) + _, limitWeek := limit.ISOWeek() + + if week > limitWeek { + // set week to maximum + week = limitWeek + } + + // start time iterator in week 0 + timeIt = time.Date(year, 0, 0, 0, 0, 0, 0, time.Local) + yearNow, weekNow := timeIt.ISOWeek() // iterate back to the weekday - for timeNow.Weekday() != time.Weekday(weekday) { + for timeIt.Weekday() != time.Weekday(weekday) { // years, months, days - timeNow = timeNow.AddDate(0, 0, -1) - yearNow, weekNow = timeNow.ISOWeek() + timeIt = timeIt.AddDate(0, 0, -1) + yearNow, weekNow = timeIt.ISOWeek() } // if we iterated back into the old year, iterate forward in weekly // steps (7) until we reach the weekday of the first week in yearNow for yearNow < year { - timeNow = timeNow.AddDate(0, 0, 7) - yearNow, weekNow = timeNow.ISOWeek() + timeIt = timeIt.AddDate(0, 0, 7) + yearNow, weekNow = timeIt.ISOWeek() } // iterate forward to the first day of the given ISO week for weekNow < week { - timeNow = timeNow.AddDate(0, 0, 7) - yearNow, weekNow = timeNow.ISOWeek() + timeIt = timeIt.AddDate(0, 0, 7) + yearNow, weekNow = timeIt.ISOWeek() } - return timeNow + return timeIt, weekNow } func (s *Server) available(start string, end string) (bool, error) { @@ -133,76 +154,117 @@ func (s *Server) Week(w http.ResponseWriter, req *http.Request, ps httprouter.Pa // overwrite with requested week week, err = strconv.Atoi(ps.ByName("week")) if err != nil { - log.Printf("Cannot serve requested week '%s'") + log.Printf("Cannot serve requested week '%d'\n", week) w.WriteHeader(http.StatusBadRequest) + // TODO: Print human readable error to w return } + monday, week := dayOfISOWeek(1, week, year) log.Printf("Current weekday is '%d'\n", weekday) log.Printf("Serving week '%v' of year '%v'\n", week, year) - - monday := dayOfISOWeek(1, week, year) log.Printf("Monday is '%v'\n", monday) + sqlDateFmt := "2006-01-02 15:04" + rows := make([][]string, 9) + for i := range rows { + rows[i] = make([]string, 6) + } + + // Define the table header + // TODO: Make heading date format configurable, start of week, etc.. + tableHead := []string{ + "Time ", + fmt.Sprintf("Mon %s", monday.Format("02.01.")), + fmt.Sprintf("Tue %s", monday.Add(time.Hour).Format("02.01.")), + fmt.Sprintf("Wed %s", monday.Add(time.Hour*24*2).Format("02.01.")), + fmt.Sprintf("Thu %s", monday.Add(time.Hour*24*3).Format("02.01.")), + fmt.Sprintf("Fri %s", monday.Add(time.Hour*24*4).Format("02.01.")), + } + + tableData := TableData { + TableHead: tableHead, + Rows: rows, + Week: week, + Year: year, + Version: hivedavVersion, + } + + // TODO: use timeIt to go through the loops below, remove the static arrays + timeIt := time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, localLocation) + //dayEnd := time.Date(monday.Year(), monday.Month(), monday.Day(), 17, 0, 0, 0, localLocation) + //weekEnd := dayEnd.Add(time.Hour * 24*5) + + // Working hours - Eight to Five + //for h := 8; h < 17; h++ { + for _, h := range []int{8,9,10,11,12,13,14,15,16} { + var availability [5]string + // Working days - Monday through Friday + //for d := 0; d < 5; d++ { + for _, d := range []int{1,2,3,4,5} { + // add/remove 1 minute to make the sql time query in available() below behave correctly + // convert to read/compare in UTC from db + start := timeIt.Add(time.Hour * time.Duration((d-1)*24 + h)).Add(1 * time.Minute).UTC().Format(sqlDateFmt) + end := timeIt.Add(time.Hour * time.Duration((d-1)*24 + h+1)).Add(-1 * time.Minute).UTC().Format(sqlDateFmt) + avi, err := s.available(start, end) + if err != nil { + log.Printf("Error getting availability on day '%d' hour '%d'\n", d, h) + return + } + if avi { + availability[d-1] = "" + } else { + availability[d-1] = "X" + } + //timeIt = timeIt.Add(time.Hour * 24) + } + tableData.Rows[h-8] = []string{ + fmt.Sprintf("%02d:00 - %02d:00", h, h+1), + availability[0], + availability[1], + availability[2], + availability[3], + availability[4], + } + //timeIt = timeIt.Add(time.Hour) + } + if strings.Contains(userAgent, "curl") { w.Header().Set("Content-Type", "text/plain") io.WriteString(w, fmt.Sprintf("Serving week %d of year %d\n", week, year)) - // TODO: Make heading date format configurable, start of week, etc.. - pt := prettytable.New([]string{ - "Time ", - fmt.Sprintf("Mon %s", monday.Format("02.01.")), - fmt.Sprintf("Tue %s", monday.Add(time.Hour).Format("02.01.")), - fmt.Sprintf("Wed %s", monday.Add(time.Hour*24*2).Format("02.01.")), - fmt.Sprintf("Thu %s", monday.Add(time.Hour*24*3).Format("02.01.")), - fmt.Sprintf("Fri %s", monday.Add(time.Hour*24*4).Format("02.01.")), - }) - - monTime:= dayOfISOWeek(1, week, year) - // TODO: use timeIt to go through the loops below, remove the static arrays - timeIt := time.Date(monTime.Year(), monTime.Month(), monTime.Day(), 0, 0, 0, 0, localLocation) - //dayEnd := time.Date(monTime.Year(), monTime.Month(), monTime.Day(), 17, 0, 0, 0, localLocation) - //weekEnd := dayEnd.Add(time.Hour * 24*5) - - // Working hours - Eight to Five - //for h := 8; h < 17; h++ { - for _, h := range []int{8,9,10,11,12,13,14,15,16} { - var availability [5]string - // Working days - Monday through Friday - //for d := 0; d < 5; d++ { - for _, d := range []int{1,2,3,4,5} { - // add/remove 1 minute to make the sql time query in available() below behave correctly - // convert to read/compare in UTC from db - start := timeIt.Add(time.Hour * time.Duration((d-1)*24 + h)).Add(1 * time.Minute).UTC().Format(sqlDateFmt) - end := timeIt.Add(time.Hour * time.Duration((d-1)*24 + h+1)).Add(-1 * time.Minute).UTC().Format(sqlDateFmt) - avi, err := s.available(start, end) - if err != nil { - log.Printf("Error getting availability on day '%d' hour '%d'\n", d, h) - return - } - if avi { - availability[d-1] = "" - } else { - availability[d-1] = "X" - } - //timeIt = timeIt.Add(time.Hour * 24) + pt := prettytable.New(tableData.TableHead) + for _, r := range tableData.Rows { + // convert to slice of interface + // https://go.dev/doc/faq#convert_slice_of_interface + s := make([]interface{}, len(r)) + for i, v := range r { + s[i] = v } - pt.AddRow(fmt.Sprintf("%02d:00 - %02d:00", h, h+1), - availability[0], - availability[1], - availability[2], - availability[3], - availability[4]) - //timeIt = timeIt.Add(time.Hour) + // unpack the string array into row arguments + pt.AddRow(s...) } - io.WriteString(w, fmt.Sprintf("HiveDAV %s\n", hivedavVersion)) io.WriteString(w, fmt.Sprint(pt)) + io.WriteString(w, fmt.Sprintf("HiveDAV %s 🍯\n", hivedavVersion)) } else { w.Header().Set("Content-Type", "text/html") - io.WriteString(w, "<h1>Welcome to the hive!</h1>") - io.WriteString(w, "<p>HTML Calendar here</p>") + + tmpl, err := template.ParseFiles("./templates/index.html") + if err != nil { + log.Printf("Error parsing html template: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + // TODO: Print human readable error to w + return + } + err = tmpl.Execute(w, tableData) + if err != nil { + log.Printf("Error executing html template: %v\n", err) + w.WriteHeader(http.StatusInternalServerError) + // TODO: Print human readable error to w + return + } } } diff --git a/templates/index.html b/templates/index.html @@ -0,0 +1,32 @@ +<html> + <head> + <title>HiveDAV - Week {{ .Week }}, Year {{ .Year }}</title> + <link rel="stylesheet" href="/css/index.css"> + <link rel="stylesheet" href="/css/style.css"> + </head> + <body> + <p> + Serving week <i>{{ .Week }}</i> of year <i>{{ .Year }}</i> + </p> + <table> + <tr> + {{ range $i, $h := .TableHead }} + <th>{{ $h }}</th> + {{ end }} + </tr> + {{ range $i, $r := .Rows }} + <tr> + <td>{{ index $r 0 }}</td> + <td>{{ index $r 1 }}</td> + <td>{{ index $r 2 }}</td> + <td>{{ index $r 3 }}</td> + <td>{{ index $r 4 }}</td> + <td>{{ index $r 5 }}</td> + </tr> + {{ end }} + </table> + <footer> + HiveDAV <a href="https://code.in0rdr.ch/hivedav/refs.html">{{ .Version }}</a> &#127855; + </footer> + </body> +</html>