preparations for styling: refactor template rendering #1

Merged
Gonne merged 7 commits from styling into main 2022-09-26 10:46:05 +00:00
18 changed files with 199 additions and 212 deletions
Showing only changes of commit d60b715d96 - Show all commits

View file

@ -2,7 +2,6 @@ package controllers
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"net/mail" "net/mail"
"officeHours/config" "officeHours/config"
@ -149,12 +148,12 @@ func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Requ
id, err := b.officeHourRepo.Add(officeHour) id, err := b.officeHourRepo.Add(officeHour)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
Templates.ExecuteTemplate(w, "addFailure.html", err) b.serveTemplate(w, "addFailure", err)
return return
} }
officeHour, _ = b.officeHourRepo.FindById(id) officeHour, _ = b.officeHourRepo.FindById(id)
b.requestRepo.Add(officeHour, models.RequestActivate) b.requestRepo.Add(officeHour, models.RequestActivate)
Templates.ExecuteTemplate(w, "addSuccess.html", struct{}{}) b.serveTemplate(w, "addSuccess", nil)
} }
} }
@ -165,10 +164,5 @@ func (b *BaseHandler) writeAddOfficeHourMask(w http.ResponseWriter, req *http.Re
if len(data.Errors) != 0 { if len(data.Errors) != 0 {
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
} }
err := Templates.ExecuteTemplate(w, "addMask.html", data) b.serveTemplate(w, "addMask", data)
if err != nil {
log.Printf("Template addMask.html could not be parsed: %s", err.Error())
w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", err.Error())))
return
}
} }

View file

@ -9,17 +9,15 @@ func (b *BaseHandler) ConfirmRequestHandler(w http.ResponseWriter, req *http.Req
request, err := b.requestRepo.FindBySecret(secret) request, err := b.requestRepo.FindBySecret(secret)
if err != nil { if err != nil {
w.WriteHeader(http.StatusNotFound) b.serveTemplate(w, "requestNotFound", nil)
Templates.ExecuteTemplate(w, "requestNotFound.html", struct{}{})
return return
} }
err = b.requestRepo.Execute(request) err = b.requestRepo.Execute(request)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) b.serveTemplate(w, "executeFailure", err.Error())
Templates.ExecuteTemplate(w, "executeFailure.html", err.Error())
return return
} }
Templates.ExecuteTemplate(w, "executeSuccess.html", struct{}{}) b.serveTemplate(w, "executeSuccess", nil)
} }

View file

@ -18,7 +18,7 @@ func (b *BaseHandler) DeleteOfficeHourHandler(w http.ResponseWriter, req *http.R
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
} }
_, err = b.requestRepo.Add(officeHour, models.RequestDelete) _, err = b.requestRepo.Add(officeHour, models.RequestDelete)
Templates.ExecuteTemplate(w, "deleteSuccess.html", struct{}{}) b.serveTemplate(w, "deleteSuccess", nil)
} else { } else {
officeHours, _ := b.officeHourRepo.GetAll(true) officeHours, _ := b.officeHourRepo.GetAll(true)
timetable, slots := b.GetTimetable(officeHours) timetable, slots := b.GetTimetable(officeHours)

View file

@ -1,7 +1,6 @@
package controllers package controllers
import ( import (
"fmt"
"html/template" "html/template"
"net/http" "net/http"
"officeHours/models" "officeHours/models"
@ -28,6 +27,7 @@ func (b *BaseHandler) GetByCourseHandler(w http.ResponseWriter, req *http.Reques
courseid, err := strconv.Atoi(req.FormValue("veranstaltung")) courseid, err := strconv.Atoi(req.FormValue("veranstaltung"))
if err != nil { if err != nil {
b.RootHandler(w, req) b.RootHandler(w, req)
return
} }
course, err := b.courseRepo.FindById(courseid) course, err := b.courseRepo.FindById(courseid)
if err != nil { if err != nil {
@ -51,9 +51,5 @@ func (b *BaseHandler) writeTimetablePage(w http.ResponseWriter, req *http.Reques
SelectedRoom int SelectedRoom int
SelectedCourse int SelectedCourse int
}{courses, rooms, timetable, selectedRoom, selectedCourse} }{courses, rooms, timetable, selectedRoom, selectedCourse}
err := Templates.ExecuteTemplate(w, "index.html", data) b.serveTemplate(w, "index", data)
if err != nil {
w.Write([]byte(fmt.Sprintf("Template konnte nicht geparst werden : %s", err.Error())))
return
}
} }

View file

@ -1,21 +1,47 @@
package controllers package controllers
import ( import (
"fmt"
"html/template" "html/template"
"net/http"
"officeHours/models" "officeHours/models"
"os"
) )
var Templates, _ = template.Must(template.ParseFiles( var funcs = template.FuncMap{
"templates/addFailure.html", "DayName": models.DayName,
"templates/addMask.html", "divide": func(i int, j int) int { return i / j },
"templates/addSuccess.html", }
"templates/deleteSuccess.html", var baseTemplate = template.Must(template.ParseFiles("templates/base.html")).New("base.html").Funcs(funcs)
"templates/executeFailure.html",
"templates/executeSuccess.html", func (b *BaseHandler) serveTemplate(w http.ResponseWriter, name string, data any) {
"templates/footer.html", full_name := "templates/" + name + ".html"
"templates/head.html", // check that template exists
"templates/index.html", info, err := os.Stat(full_name)
"templates/officeHourTable.html", if (err != nil && os.IsNotExist(err)) || info.IsDir() {
"templates/requestNotFound.html")). w.WriteHeader(http.StatusNotFound)
New("").Funcs(template.FuncMap{"DayName": models.DayName, w.Write([]byte("404: Template nicht gefunden."))
"divide": func(i int, j int) int { return i / j }}).ParseFiles("templates/confirmationMail", "templates/td.html") return
}
Outdated
Review

Here we should consider checking whether the name is restricted to something like [a-zA-Z0-9] to prevent directory traversal. Possibly it is sufficient to forbid / and/or .

Here we should consider checking whether the name is restricted to something like `[a-zA-Z0-9]` to prevent directory traversal. Possibly it is sufficient to forbid `/` and/or `.`

This is not longer relevant, since all available templates are listed in one place inside our code and we do not parse filenames from anywhere else.

This is not longer relevant, since all available templates are listed in one place inside our code and we do not parse filenames from anywhere else.
tmpl, err := baseTemplate.ParseFiles(full_name)
if err != nil {
// TODO: log.Printf
w.WriteHeader(http.StatusInternalServerError)
johannes marked this conversation as resolved Outdated
Outdated
Review

This could be an actual error using fmt.Errorf.
Furthermore I began logging in English. We should make that consistent.

This could be an actual error using `fmt.Errorf`. Furthermore I began logging in English. We should make that consistent.
w.Write([]byte(fmt.Sprintf("500: Template "+full_name+" konnte nicht geparst werden : %s", err.Error())))
return
johannes marked this conversation as resolved Outdated
Outdated
Review

I think, that StatusNotFound is for user supplied references (e.g. a course or a room id), but this should be an internal server error.

I think, that StatusNotFound is for user supplied references (e.g. a course or a room id), but this should be an internal server error.
}
// TODO: cache templates in parsed state, but not executed
err = template.Must(tmpl.Clone()).Execute(w, data)
if err != nil {
// TODO: log.Printf
Outdated
Review

Using this construction each template is parsed again and again, while it would be sufficient to parse them all on startup and later just execute them.

Using this construction each template is parsed again and again, while it would be sufficient to parse them all on startup and later just execute them.

Fixed (and spent too much time implementing this ^^)

Fixed (and spent too much time implementing this ^^)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("500: Template "+full_name+" konnte nicht ausgeführt werden : %s", err.Error())))
Outdated
Review

This could be an actual error using fmt.Errorf.
Furthermore I began logging in English. We should make that consistent.

Furthermore error wrapping is something cool I just learnt.

This could be an actual error using `fmt.Errorf`. Furthermore I began logging in English. We should make that consistent. Furthermore [error wrapping](https://blog.boot.dev/golang/wrapping-errors-in-go-how-to-handle-nested-errors/) is something cool I just learnt.

Error wrapping looks nice. I started doing that.

In general it seems best to wrap errors which occur during runtime from the beginning of a http request handler, which then logs the error and returns an error page to the user. (I added an inline TODO about that)

Errors on startup however can just abort the whole program, like I implemented in main.go

Error wrapping looks nice. I started doing that. In general it seems best to wrap errors which occur during runtime from the beginning of a http request handler, which then logs the error and returns an error page to the user. (I added an inline TODO about that) Errors on startup however can just abort the whole program, like I implemented in `main.go`
return
}
}
var RawTemplates = template.Must(template.New("").Funcs(funcs).ParseFiles(
"templates/confirmationMail",
"templates/td.html",
"templates/officeHourTable.html"))

View file

@ -84,7 +84,7 @@ func (b *BaseHandler) printTimetable(timetable map[models.Date]map[int]models.Of
MinuteGranularity int MinuteGranularity int
DeleteIcons bool DeleteIcons bool
}{OfficeHour: current, MinuteGranularity: b.config.Date.MinuteGranularity, DeleteIcons: deleteIcons} }{OfficeHour: current, MinuteGranularity: b.config.Date.MinuteGranularity, DeleteIcons: deleteIcons}
Templates.ExecuteTemplate(&celldata, "td.html", data) RawTemplates.ExecuteTemplate(&celldata, "td.html", data)
tableBody += celldata.String() tableBody += celldata.String()
} }
} else { } else {
@ -116,6 +116,6 @@ func (b *BaseHandler) printTimetable(timetable map[models.Date]map[int]models.Of
slots[4], slots[4],
template.HTML(tableBody), template.HTML(tableBody),
} }
Templates.ExecuteTemplate(&table, "officeHourTable.html", tableData) RawTemplates.ExecuteTemplate(&table, "officeHourTable.html", tableData)
return template.HTML(table.String()) return template.HTML(table.String())
} }

View file

@ -128,7 +128,7 @@ func (r *RequestRepo) sendConfirmationMail(request models.Request) error {
Config config.Config Config config.Config
Request models.Request Request models.Request
}{r.config, request} }{r.config, request}
err := controllers.Templates.ExecuteTemplate(&message, "confirmationMail", data) err := controllers.RawTemplates.ExecuteTemplate(&message, "confirmationMail", data)
if err != nil { if err != nil {
log.Printf("Error parsing confirmation Mail: %s", err.Error()) log.Printf("Error parsing confirmation Mail: %s", err.Error())
return err return err

View file

@ -1,13 +1,7 @@
<!DOCTYPE html> {{define "title"}}Fehler{{end}}
<html lang="de">
<head> {{define "content"}}
<title>Sprechstunde anlegen</title>
{{template "head.html" .}}
</head>
<body>
Irgendetwas ist schief gegangen. Bitte sende folgende Daten an <a href="mailto:sprechstundentool@mathebau.de">sprechstundentool@mathebau.de</a> mit einer Beschreibung, was du tun wolltest. Irgendetwas ist schief gegangen. Bitte sende folgende Daten an <a href="mailto:sprechstundentool@mathebau.de">sprechstundentool@mathebau.de</a> mit einer Beschreibung, was du tun wolltest.
<br /> <br />
{{.}} {{.}}
{{template "footer.html" .}} {{end}}
</body>
</html>

View file

@ -1,17 +1,15 @@
<!DOCTYPE html> <{{define "title"}}Sprechstunde anlegen{{end}}
<html lang="de">
<head> {{define "content"}}
<title>Sprechstunde anlegen</title>
{{template "head.html" .}}
</head>
<body>
<p> <p>
{{range .Errors}}{{.}}<br />{{end}} {{range .Errors}}{{.}}<br />{{end}}
</p> </p>
<form method="POST" action="addOfficeHour"> <form method="POST" action="addOfficeHour">
<label for="veranstaltung">Veranstaltung</label>: <label for="veranstaltung">Veranstaltung</label>:
<select name="veranstaltung" id="veranstaltung">{{range $course := .Courses}} <select name="veranstaltung" id="veranstaltung">
<option value="{{$course.Id}}"{{if eq $course.Id $.SelectedCourse}} selected{{end}}>{{$course.Name}}</option>{{end}} {{range $course := .Courses}}
<option value="{{$course.Id}}"{{if eq $course.Id $.SelectedCourse}} selected{{end}}>{{$course.Name}}</option>
{{end}}
</select><br /> </select><br />
<label for="woche">Woche</label>: <label for="woche">Woche</label>:
<select name="woche" id="woche"> <select name="woche" id="woche">
@ -29,8 +27,10 @@
<label for="startzeit">Startzeit</label>: <input type="time" name="startzeit" id="startzeit" min="08:00" max="17:30" {{if gt $.Date.Hour 7}}value="{{printf "%02d" $.Date.Hour}}:{{printf "%02d" $.Date.Minute}}"{{end}} required/><br /> <label for="startzeit">Startzeit</label>: <input type="time" name="startzeit" id="startzeit" min="08:00" max="17:30" {{if gt $.Date.Hour 7}}value="{{printf "%02d" $.Date.Hour}}:{{printf "%02d" $.Date.Minute}}"{{end}} required/><br />
<label for="dauer">Dauer in Minuten</label>: <input name="dauer" id="dauer" type="number" min="{{.MinuteGranularity}}" max="120" step="{{.MinuteGranularity}}" value="{{.Duration}}" required/><br /> <label for="dauer">Dauer in Minuten</label>: <input name="dauer" id="dauer" type="number" min="{{.MinuteGranularity}}" max="120" step="{{.MinuteGranularity}}" value="{{.Duration}}" required/><br />
<label for="raum">Raum</label>: <label for="raum">Raum</label>:
<select name="raum" id="raum">{{range $room := .Rooms}} <select name="raum" id="raum">
<option value="{{$room.Id}}"{{if eq $room.Id $.SelectedRoom}} selected{{end}}>{{$room.Name}}</option>{{end}} {{range $room := .Rooms}}
<option value="{{$room.Id}}"{{if eq $room.Id $.SelectedRoom}} selected{{end}}>{{$room.Name}}</option>
{{end}}
</select><br /> </select><br />
<label for="raumname">Raumname (für Sonderräume)</label>: <input type="text" name="raumname" id="raumname" value="{{.Roomname}}"/><br /> <label for="raumname">Raumname (für Sonderräume)</label>: <input type="text" name="raumname" id="raumname" value="{{.Roomname}}"/><br />
<label for="name">Name</label>: <input name="name" id="name" type="text" size="50" value="{{.Name}}" required/><br /> <label for="name">Name</label>: <input name="name" id="name" type="text" size="50" value="{{.Name}}" required/><br />
@ -39,12 +39,14 @@
<label for="info">Info</label>: <input name="info" id="info" type="text" size="50" value="{{.Info}}"/><br /> <label for="info">Info</label>: <input name="info" id="info" type="text" size="50" value="{{.Info}}"/><br />
<input type="submit"> <input type="submit">
</form> </form>
{{if ne .Config.Tutor.MailSuffix ""}}Du musst hier eine Email-Adresse angeben, die auf „{{.Config.Tutor.MailSuffix}}“ endet.<br />{{end}} {{if ne .Config.Tutor.MailSuffix ""}}
Du musst hier eine Email-Adresse angeben, die auf „{{.Config.Tutor.MailSuffix}}“ endet.<br />
{{end}}
Außerdem dürfen in Räumen nur begrenzt viele Sprechstunden gleichzeitig stattfinden, nämlich Außerdem dürfen in Räumen nur begrenzt viele Sprechstunden gleichzeitig stattfinden, nämlich
<dl> <dl>
{{range $room := .Rooms}} {{range $room := .Rooms}}
<dt>{{$room.Name}}</dt><dd>{{$room.MaxOccupy}} Sprechstunde{{if gt $room.MaxOccupy 1}}n{{end}}</dd>{{end}} <dt>{{$room.Name}}</dt>
<dd>{{$room.MaxOccupy}} Sprechstunde{{if gt $room.MaxOccupy 1}}n{{end}}</dd>
{{end}}
</dl> </dl>
{{template "footer.html" .}} {{end}}
</body>
</html>

View file

@ -1,12 +1,5 @@
<!DOCTYPE html> {{define "title"}}Sprechstunde anlegen{{end}}
<html lang="de">
<head> {{define "content"}}
<title>Sprechstunde anlegen</title>
{{template "head.html" .}}
</head>
<body>
Die Sprechstunde wurde angelegt. Du solltest eine Mail mit einem Aktivierungslink erhalten haben. Die Sprechstunde wurde angelegt. Du solltest eine Mail mit einem Aktivierungslink erhalten haben.
<br /> {{end}}
{{template "footer.html" .}}
</body>
</html>

27
templates/base.html Normal file
View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="keywords" content="Mathebau, Sprechstunde, Sprechstunden, Mathe, Mathematik, technische, Universität, Darmstadt, TU, Fachschaft">
<meta name="description" content="Eine Übersicht der Sprechstunden, die in den offenen Arbeitsräumen der Fachschaft Mathematik, TU Darmstadt, angeboten werden">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<title>{{block "title" .}}Start{{end}} Sprechstunden</title>
</head>
<body>
<div class="container">
{{block "content" .}}Du solltest dies nicht sehen.{{end}}
</div>
<footer class="container">
<a href="/">Startseite</a><br />
<a href="/addOfficeHour">Sprechstunde anlegen</a><br />
<a href="/deleteOfficeHour">Sprechstunde löschen</a><br />
Technische Fragen an <a href="mailto:sprechstundentool@mathebau.de">sprechstundentool@mathebau.de</a>
</footer>
<script src="/static/bootstrap/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View file

@ -1,13 +1,7 @@
<!DOCTYPE html> {{define "title"}}Sprechstunde löschen{{end}}
<html lang="de">
<head> {{define "content"}}
<title>Sprechstunde löschen</title>
{{template "head.html" .}}
</head>
<body>
Du solltest eine Mail mit einem Bestätigungslink erhalten haben. <br /> Du solltest eine Mail mit einem Bestätigungslink erhalten haben. <br />
Sie wurde an die Adresse geschickt, mit der die Sprechstunde angelegt wurde. Sie wurde an die Adresse geschickt, mit der die Sprechstunde angelegt wurde.
<br /> <br />
{{template "footer.html" .}} {{end}}
</body>
</html>

View file

@ -1,13 +1,7 @@
<!DOCTYPE html> {{define "title"}}Anfrage ausführen fehlgeschlagen{{end}}
<html lang="de">
<head> {{define "content"}}
<title>Anfrage ausführen fehlgeschlagen</title>
{{template "head.html" .}}
</head>
<body>
Irgendetwas ist schief gegangen. Bitte sende folgende Daten an <a href="mailto:sprechstundentool@mathebau.de">sprechstundentool@mathebau.de</a> mit einer Beschreibung, was du tun wolltest. Irgendetwas ist schief gegangen. Bitte sende folgende Daten an <a href="mailto:sprechstundentool@mathebau.de">sprechstundentool@mathebau.de</a> mit einer Beschreibung, was du tun wolltest.
<br /> <br />
{{.}} {{.}}
{{template "footer.html" .}} {{end}}
</body>
</html>

View file

@ -1,11 +1,5 @@
<!DOCTYPE html> {{define "title"}}Anfrage ausgeführt{{end}}
<html lang="de">
<head> {{define "content"}}
<title>Anfrage ausgeführt</title> Deine Anfrage wurde ausgeführt.
{{template "head.html" .}} {{end}}
</head>
<body>
Deine Anfrage wurde ausgeführt. <br />
{{template "footer.html" .}}
</body>
</html>

View file

@ -1,6 +0,0 @@
<footer>
<a href="/">Startseite</a><br />
<a href="/addOfficeHour">Sprechstunde anlegen</a><br />
<a href="/deleteOfficeHour">Sprechstunde löschen</a><br />
Technische Fragen an <a href="mailto:sprechstundentool@mathebau.de">sprechstundentool@mathebau.de</a>
</footer>

View file

@ -1,9 +0,0 @@
<meta charset="UTF-8">
<meta name="keywords" content="Mathebau, Sprechstunde, Sprechstunden, Mathe, Mathematik, technische, Universität, Darmstadt, TU, Fachschaft">
<meta name="description" content="Eine Übersicht der Sprechstunden, die in den offenen Arbeitsräumen der Fachschaft Mathematik, TU Darmstadt, angeboten werden">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<!-- TODO: include this at the very bottom of <body> after refactoring to base template
<script src="/static/bootstrap/js/bootstrap.bundle.min.js"></script>
-->

View file

@ -1,16 +1,13 @@
<!DOCTYPE html> {{define "title"}}Übersicht{{end}}
<html lang="de">
<head> {{define "content"}}
<title>Sprechstunden</title>
{{template "head.html" .}}
</head>
<body>
<form method="GET" action="/getByCourse"> <form method="GET" action="/getByCourse">
<label for="veranstaltung">Veranstaltung: </label> <label for="veranstaltung">Veranstaltung: </label>
<select name="veranstaltung" id="veranstaltung" size="1" onchange="document.forms[0].submit()"> <select name="veranstaltung" id="veranstaltung" size="1" onchange="document.forms[0].submit()">
<option value="">Alle</option> <option value="">Alle</option>
{{range $course := .Courses}} {{range $course := .Courses}}
<option value="{{$course.Id}}"{{if eq $course.Id $.SelectedCourse}} selected{{end}}>{{$course.Name}}</option>{{end}} <option value="{{$course.Id}}"{{if eq $course.Id $.SelectedCourse}} selected{{end}}>{{$course.Name}}</option>
{{end}}
</select> </select>
<input type="submit" value="Auswählen" /> <input type="submit" value="Auswählen" />
</form> </form>
@ -19,11 +16,10 @@
<select name="raum" id="raum" size="1" onchange="document.forms[1].submit()"> <select name="raum" id="raum" size="1" onchange="document.forms[1].submit()">
<option value="">Alle</option> <option value="">Alle</option>
{{range $room := .Rooms}} {{range $room := .Rooms}}
<option value="{{$room.Id}}"{{if eq $room.Id $.SelectedRoom}} selected{{end}}>{{$room.Name}}</option>{{end}} <option value="{{$room.Id}}"{{if eq $room.Id $.SelectedRoom}} selected{{end}}>{{$room.Name}}</option>
{{end}}
</select> </select>
<input type="submit" value="Auswählen" /> <input type="submit" value="Auswählen" />
</form> </form>
{{.Timetable}} {{.Timetable}}
{{template "footer.html" .}} {{end}}
</body>
</html>

View file

@ -1,10 +1,6 @@
<!DOCTYPE html> {{define "title"}}Anfrage bestätigen fehlgeschlagen{{end}}
<html lang="de">
<head> {{define "content"}}
<title>Anfrage bestätigen fehlgeschlagen</title>
{{template "head.html" .}}
</head>
<body>
<p> <p>
Dieser Bestätigungscode ist nicht verfügbar. <br /> Dieser Bestätigungscode ist nicht verfügbar. <br />
Bitte gib deinen Bestätigungscode hier ein. Bitte gib deinen Bestätigungscode hier ein.
@ -13,6 +9,4 @@
<label for="code">Bestätigungscode</label>: <input type="text" name="code" id="code"/> <label for="code">Bestätigungscode</label>: <input type="text" name="code" id="code"/>
<input type="submit" /> <input type="submit" />
</form> </form>
{{template "footer.html" .}} {{end}}
</body>
</html>