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 (
"fmt"
"log"
"net/http"
"net/mail"
"officeHours/config"
@ -149,12 +148,12 @@ func (b *BaseHandler) AddOfficeHourHandler(w http.ResponseWriter, req *http.Requ
id, err := b.officeHourRepo.Add(officeHour)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Templates.ExecuteTemplate(w, "addFailure.html", err)
b.serveTemplate(w, "addFailure", err)
return
}
officeHour, _ = b.officeHourRepo.FindById(id)
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 {
w.WriteHeader(http.StatusBadRequest)
}
err := Templates.ExecuteTemplate(w, "addMask.html", 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
}
b.serveTemplate(w, "addMask", data)
}

View file

@ -9,17 +9,15 @@ func (b *BaseHandler) ConfirmRequestHandler(w http.ResponseWriter, req *http.Req
request, err := b.requestRepo.FindBySecret(secret)
if err != nil {
w.WriteHeader(http.StatusNotFound)
Templates.ExecuteTemplate(w, "requestNotFound.html", struct{}{})
b.serveTemplate(w, "requestNotFound", nil)
return
}
err = b.requestRepo.Execute(request)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Templates.ExecuteTemplate(w, "executeFailure.html", err.Error())
b.serveTemplate(w, "executeFailure", err.Error())
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)
}
_, err = b.requestRepo.Add(officeHour, models.RequestDelete)
Templates.ExecuteTemplate(w, "deleteSuccess.html", struct{}{})
b.serveTemplate(w, "deleteSuccess", nil)
} else {
officeHours, _ := b.officeHourRepo.GetAll(true)
timetable, slots := b.GetTimetable(officeHours)

View file

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

View file

@ -1,21 +1,47 @@
package controllers
import (
"fmt"
"html/template"
"net/http"
"officeHours/models"
"os"
)
var Templates, _ = template.Must(template.ParseFiles(
"templates/addFailure.html",
"templates/addMask.html",
"templates/addSuccess.html",
"templates/deleteSuccess.html",
"templates/executeFailure.html",
"templates/executeSuccess.html",
"templates/footer.html",
"templates/head.html",
"templates/index.html",
"templates/officeHourTable.html",
"templates/requestNotFound.html")).
New("").Funcs(template.FuncMap{"DayName": models.DayName,
"divide": func(i int, j int) int { return i / j }}).ParseFiles("templates/confirmationMail", "templates/td.html")
var funcs = template.FuncMap{
"DayName": models.DayName,
"divide": func(i int, j int) int { return i / j },
}
var baseTemplate = template.Must(template.ParseFiles("templates/base.html")).New("base.html").Funcs(funcs)
func (b *BaseHandler) serveTemplate(w http.ResponseWriter, name string, data any) {
full_name := "templates/" + name + ".html"
// check that template exists
info, err := os.Stat(full_name)
if (err != nil && os.IsNotExist(err)) || info.IsDir() {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404: Template nicht gefunden."))
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
DeleteIcons bool
}{OfficeHour: current, MinuteGranularity: b.config.Date.MinuteGranularity, DeleteIcons: deleteIcons}
Templates.ExecuteTemplate(&celldata, "td.html", data)
RawTemplates.ExecuteTemplate(&celldata, "td.html", data)
tableBody += celldata.String()
}
} else {
@ -116,6 +116,6 @@ func (b *BaseHandler) printTimetable(timetable map[models.Date]map[int]models.Of
slots[4],
template.HTML(tableBody),
}
Templates.ExecuteTemplate(&table, "officeHourTable.html", tableData)
RawTemplates.ExecuteTemplate(&table, "officeHourTable.html", tableData)
return template.HTML(table.String())
}

View file

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

View file

@ -1,13 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<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.
<br />
{{.}}
{{template "footer.html" .}}
</body>
</html>
{{define "title"}}Fehler{{end}}
{{define "content"}}
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 />
{{.}}
{{end}}

View file

@ -1,50 +1,52 @@
<!DOCTYPE html>
<html lang="de">
<head>
<title>Sprechstunde anlegen</title>
{{template "head.html" .}}
</head>
<body>
<p>
{{range .Errors}}{{.}}<br />{{end}}
</p>
<form method="POST" action="addOfficeHour">
<label for="veranstaltung">Veranstaltung</label>:
<select name="veranstaltung" id="veranstaltung">{{range $course := .Courses}}
<option value="{{$course.Id}}"{{if eq $course.Id $.SelectedCourse}} selected{{end}}>{{$course.Name}}</option>{{end}}
</select><br />
<label for="woche">Woche</label>:
<select name="woche" id="woche">
<option value="0"{{if eq 0 $.Date.Week}} selected{{end}}>Jede</option>
<option value="1"{{if eq 1 $.Date.Week}} selected{{end}}>Ungerade</option>
<option value="2"{{if eq 2 $.Date.Week}} selected{{end}}>Gerade</option>
</select><br />
<label for="tag">Tag</label>: <select name="tag" id="tag">
<option value="0"{{if eq 0 $.Date.Day}} selected{{end}}>Montag</option>
<option value="1"{{if eq 1 $.Date.Day}} selected{{end}}>Dienstag</option>
<option value="2"{{if eq 2 $.Date.Day}} selected{{end}}>Mittwoch</option>
<option value="3"{{if eq 3 $.Date.Day}} selected{{end}}>Donnerstag</option>
<option value="4"{{if eq 4 $.Date.Day}} selected{{end}}>Freitag</option>
</select><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="raum">Raum</label>:
<select name="raum" id="raum">{{range $room := .Rooms}}
<option value="{{$room.Id}}"{{if eq $room.Id $.SelectedRoom}} selected{{end}}>{{$room.Name}}</option>{{end}}
</select><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="email">Email-Adresse</label>:
<input name="email" id="email" type="email" size="50" value="{{.Email}}" required/><br />
<label for="info">Info</label>: <input name="info" id="info" type="text" size="50" value="{{.Info}}"/><br />
<input type="submit">
</form>
{{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
<dl>
{{range $room := .Rooms}}
<dt>{{$room.Name}}</dt><dd>{{$room.MaxOccupy}} Sprechstunde{{if gt $room.MaxOccupy 1}}n{{end}}</dd>{{end}}
</dl>
{{template "footer.html" .}}
</body>
</html>
<{{define "title"}}Sprechstunde anlegen{{end}}
{{define "content"}}
<p>
{{range .Errors}}{{.}}<br />{{end}}
</p>
<form method="POST" action="addOfficeHour">
<label for="veranstaltung">Veranstaltung</label>:
<select name="veranstaltung" id="veranstaltung">
{{range $course := .Courses}}
<option value="{{$course.Id}}"{{if eq $course.Id $.SelectedCourse}} selected{{end}}>{{$course.Name}}</option>
{{end}}
</select><br />
<label for="woche">Woche</label>:
<select name="woche" id="woche">
<option value="0"{{if eq 0 $.Date.Week}} selected{{end}}>Jede</option>
<option value="1"{{if eq 1 $.Date.Week}} selected{{end}}>Ungerade</option>
<option value="2"{{if eq 2 $.Date.Week}} selected{{end}}>Gerade</option>
</select><br />
<label for="tag">Tag</label>: <select name="tag" id="tag">
<option value="0"{{if eq 0 $.Date.Day}} selected{{end}}>Montag</option>
<option value="1"{{if eq 1 $.Date.Day}} selected{{end}}>Dienstag</option>
<option value="2"{{if eq 2 $.Date.Day}} selected{{end}}>Mittwoch</option>
<option value="3"{{if eq 3 $.Date.Day}} selected{{end}}>Donnerstag</option>
<option value="4"{{if eq 4 $.Date.Day}} selected{{end}}>Freitag</option>
</select><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="raum">Raum</label>:
<select name="raum" id="raum">
{{range $room := .Rooms}}
<option value="{{$room.Id}}"{{if eq $room.Id $.SelectedRoom}} selected{{end}}>{{$room.Name}}</option>
{{end}}
</select><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="email">Email-Adresse</label>:
<input name="email" id="email" type="email" size="50" value="{{.Email}}" required/><br />
<label for="info">Info</label>: <input name="info" id="info" type="text" size="50" value="{{.Info}}"/><br />
<input type="submit">
</form>
{{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
<dl>
{{range $room := .Rooms}}
<dt>{{$room.Name}}</dt>
<dd>{{$room.MaxOccupy}} Sprechstunde{{if gt $room.MaxOccupy 1}}n{{end}}</dd>
{{end}}
</dl>
{{end}}

View file

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

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

View file

@ -1,13 +1,7 @@
<!DOCTYPE html>
<html lang="de">
<head>
<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.
<br />
{{.}}
{{template "footer.html" .}}
</body>
</html>
{{define "title"}}Anfrage ausführen fehlgeschlagen{{end}}
{{define "content"}}
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 />
{{.}}
{{end}}

View file

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

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

View file

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