31 Commits

Author SHA1 Message Date
7144489afb Bessere Beispiele 2023-10-17 05:25:18 +02:00
76f1fe9588 Großer Umbruch der Architektur, halb fertig, zu viele Details zum Aufzählen, in Zukunft wieder kleine Commits! 2023-10-16 18:51:52 +02:00
726c8b6dcb Login für eingeladene Prüflinge implementiert 2023-10-10 20:57:53 +02:00
608879d008 Generierte UUID in einen asynchronen Speicher schreiben 2023-10-10 19:50:19 +02:00
616df72041 package db zu data umbenannt, außerdem db.Open() zu data.OpenDB() 2023-10-10 19:19:17 +02:00
1597d38d34 UUID-Button repariert 2023-10-10 19:18:34 +02:00
8dbb5f946d UUID-Element zum Ersetzen des "Einladen"-Buttons erstellt 2023-10-10 19:08:59 +02:00
7c7cb5959d GenerateUUID zum erstellen einer pseudozufälligen, 8 Stellen langen Hexadezimalzahl 2023-10-10 19:06:02 +02:00
2beb90a345 Button zum Generieren einer UUID hinzugefügt 2023-10-10 18:55:54 +02:00
f80dca4b10 Bei 82870e10 war question.html nicht gespeichert 2023-10-10 18:55:34 +02:00
bf05bc0be7 Der Participant muss nicht das ganze Questionaire kennen, nur die UUID 2023-10-10 18:52:29 +02:00
f6a073fc39 Alle nötigen types für Questionaire eingefügt 2023-10-10 18:50:54 +02:00
82870e100f question.html erstellt 2023-10-10 18:43:48 +02:00
2b119f6752 Leerzeichen in SQL-Abfragen ergänzt 2023-10-08 15:16:47 +02:00
c04932383e Reihenfolge der ausgegebenen Ergebnisse umgedreht 2023-10-08 15:13:22 +02:00
8ea0c2964a Spalten in Ausgabetabelle umbenannt 2023-10-08 15:12:52 +02:00
519dc82023 Automatisches Suchen ohne Submit-Button 2023-10-08 11:01:57 +02:00
324a1c54d6 Grundlegenden Funktionsumfang geschaffen, dafür einiges umstrukturiert 2023-10-08 10:36:16 +02:00
15675d5e6c HTML-Seiten modularer aufgebaut und table.html hinzugefügt 2023-10-07 18:32:33 +02:00
8ae3019b9c *DB.ReadAll() hinzugefügt 2023-10-07 17:22:55 +02:00
b13eba8008 index.html zu form.html umbenannt 2023-10-07 17:22:18 +02:00
da77201a93 Merge branch 'datenbank_einfügen' 2023-10-07 17:01:58 +02:00
9acc6711fc Nach Suchbegriff aus Datenbank lesen implementiert, außerdem structs mit new() instanziiert 2023-10-07 16:58:34 +02:00
f570950425 In Datenbank schreiben und nach Namen suchen implementiert 2023-10-06 19:07:56 +02:00
5749739761 Versuch, eine übersichtlichere Struktur einzuführen 2023-10-05 19:56:18 +02:00
1bcfbfd325 Verschiedene Typen in package types ausgelagert und Channels zum asynchronen Datentransport eingesetzt 2023-10-05 19:52:11 +02:00
fcb509c9fe Verwendung von Pointern, um Vermischung der Pakete zu vermeiden 2023-10-05 18:27:21 +02:00
1c39b1e471 Code in Pakete aufgeteilt 2023-10-05 18:08:44 +02:00
fdf68adb0d getCredentials() und OpenDB() zum öffnen einer sql.DB erstellt 2023-10-05 17:42:47 +02:00
ea0fdee0e0 i und j sollten jetzt korrekt verwendet werden 2023-10-05 17:01:49 +02:00
61c895d53f Eingabemaske funktioniert soweit
i und j sollten jetzt richtig verwendet werden
2023-10-05 17:01:24 +02:00
19 changed files with 814 additions and 101 deletions

44
.air.toml Normal file
View File

@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

0
.gitignore vendored Normal file
View File

96
create_tables.sql Normal file
View File

@ -0,0 +1,96 @@
USE sicherheitsunterweisung;
DROP TABLE IF EXISTS instructors;
DROP TABLE IF EXISTS briefings;
DROP TABLE IF EXISTS participants;
DROP TABLE IF EXISTS questions;
DROP TABLE IF EXISTS given_answers;
CREATE TABLE instructors (
id INT NOT NULL AUTO_INCREMENT,
first_name VARCHAR(32) NOT NULL,
last_name VARCHAR(32) NOT NULL,
personnel_id INT NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE briefings (
id INT NOT NULL AUTO_INCREMENT,
date DATE NOT NULL,
time TIME NOT NULL,
location VARCHAR(32) NOT NULL,
as_of DATE NOT NULL,
instructor_id INT NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(instructor_id) REFERENCES instructors(id)
);
CREATE TABLE participants (
id INT NOT NULL AUTO_INCREMENT,
first_name VARCHAR(32) NOT NULL,
last_name VARCHAR(32) NOT NULL,
company VARCHAR(32) NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE questions (
id INT NOT NULL AUTO_INCREMENT,
question VARCHAR(256) NOT NULL,
answer_1 VARCHAR(64) NOT NULL,
answer_2 VARCHAR(64) NOT NULL,
answer_3 VARCHAR(64) NOT NULL,
answer_4 VARCHAR(64) NOT NULL,
correct_answer INT NOT NULL,
PRIMARY KEY(id)
);
CREATE TABLE given_answers (
briefing_id INT NOT NULL,
participant_id INT NOT NULL,
question_id INT NOT NULL,
given_answer INT NOT NULL,
PRIMARY KEY(briefing_id, participant_id, question_id),
FOREIGN KEY(briefing_id) REFERENCES briefings(id),
FOREIGN KEY(participant_id) REFERENCES participants(id),
FOREIGN KEY(question_id) REFERENCES questions(id)
);
INSERT INTO instructors
(first_name, last_name, personnel_id)
VALUES
( 'Jason', 'Streifling', '123456' ),
( 'Tim', 'Taler', '123457' ),
( 'Georg', 'aus dem Jungel', '123458' );
INSERT INTO briefings (
date, time, location, as_of, instructor_id
) VALUES
( '2023-10-16', '17:00:00', 'Werk Langenhagen', '2021-02-01', '1' ),
( '2023-10-16', '17:05:00', 'Werk Langenhagen', '2021-02-01', '2' );
INSERT INTO participants (
first_name, last_name, company
) VALUES
( 'Peter', 'Enis', 'Körber' ),
( 'Dürüm', 'Döner', 'MP Technic' );
INSERT INTO questions (
question, answer_1, answer_2, answer_3, answer_4, correct_answer
) VALUES
( 'Was ist 1+1?', '1', '2', '3', '4', '2' ),
( 'Was ist 1+2?', '1', '2', '3', '4', '3' ),
( 'Was ist 2+2?', '1', '2', '3', '4', '4' );
INSERT INTO given_answers (
briefing_id, participant_id, question_id, given_answer
) VALUES
( '1', '1', '1', '2' ),
( '1', '1', '2', '3' ),
( '1', '1', '3', '3' ),
( '2', '2', '1', '2' ),
( '2', '2', '2', '3' ),
( '2', '2', '3', '4' );

7
go.mod
View File

@ -1,3 +1,10 @@
module streifling.com/jason/sicherheitsunterweisung
go 1.21.1
require (
github.com/go-sql-driver/mysql v1.7.1
golang.org/x/term v0.13.0
)
require golang.org/x/sys v0.13.0 // indirect

6
go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=

86
main.go
View File

@ -5,64 +5,56 @@ import (
"html/template"
"log"
"net/http"
"streifling.com/jason/sicherheitsunterweisung/packages/data"
"streifling.com/jason/sicherheitsunterweisung/packages/server"
"streifling.com/jason/sicherheitsunterweisung/packages/types"
)
type Person struct {
FirstName string
LastName string
}
func waitForParticipants(sb []*types.Briefing, sp []*types.Participant, cp <-chan *types.Participant, m *http.ServeMux) {
for p := range cp {
p.Questions = data.InitQuestions()
sp = append(sp, p)
type Instructor Person
type Participant struct {
ID int64
Person
Company string
}
type Briefing struct {
Instructor
Date string
Time string
State string
Location string
Participants []Participant
var i int
for i = range p.Questions {
m.HandleFunc("/display-question-"+fmt.Sprintf("%d", p.ID)+"-"+fmt.Sprintf("%d", i)+"/", server.DisplayQuestion(i, p))
}
m.HandleFunc("/display-question-"+fmt.Sprintf("%d", p.ID)+"-"+fmt.Sprintf("%d", i+1)+"/", server.DisplayTestResults(sb, p))
}
}
func main() {
var i, j int64
var b Briefing
logins := make([]string, 0)
participants := make([]*types.Participant, 0)
briefings := make([]*types.Briefing, 0)
mux := http.NewServeMux()
i = 1
db, err := data.OpenDB("sicherheitsunterweisung")
if err != nil {
log.Fatalln(err)
}
var i, j int64
if err := db.GetLastID(&i); err != nil {
log.Fatalln(err)
}
j = i
participantChan := make(chan *types.Participant)
defer close(participantChan)
go waitForParticipants(briefings, participants, participantChan, mux)
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static/"))))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
template.Must(template.ParseFiles("templates/index.html")).Execute(w, i)
})
mux.HandleFunc("/add-participant/", func(w http.ResponseWriter, r *http.Request) {
i++
template.Must(template.ParseFiles("templates/index.html", "templates/participant.html")).ExecuteTemplate(w, "participant", i)
})
mux.HandleFunc("/submit/", func(w http.ResponseWriter, r *http.Request) {
b.Instructor.FirstName = r.PostFormValue("instructor-first")
b.Instructor.LastName = r.PostFormValue("instructor-last")
b.Date = r.PostFormValue("date")
b.Time = r.PostFormValue("time")
b.State = r.PostFormValue("state")
b.Location = r.PostFormValue("location")
for j = 1; j <= i; j++ {
b.Participants = append(b.Participants, Participant{
ID: j,
Person: Person{
FirstName: r.PostFormValue("participant-first-" + fmt.Sprint(j)),
LastName: r.PostFormValue(("participant-last-" + fmt.Sprint(j))),
},
Company: r.PostFormValue(("participant-company-" + fmt.Sprint(j))),
})
}
fmt.Println(b)
template.Must(template.ParseFiles("templates/index.html", "templates/login.html")).Execute(w, nil)
})
mux.HandleFunc("/search/", server.DisplaySearchResults(db))
mux.HandleFunc("/new-briefing/", server.DisplayForm(&i))
mux.HandleFunc("/add-participant/", server.AddParticipant(&i, &logins))
mux.HandleFunc("/submit-form/", server.SubmitForm(db, &i, &j))
mux.HandleFunc("/internal-login/", server.DisplayTable(db))
mux.HandleFunc("/external-login/", server.DisplayParticipantForm(&logins, participantChan))
log.Fatalln(http.ListenAndServe(":8080", mux))
}

192
packages/data/db.go Normal file
View File

@ -0,0 +1,192 @@
package data
import (
"bufio"
"database/sql"
"fmt"
"os"
"strings"
"syscall"
"streifling.com/jason/sicherheitsunterweisung/packages/types"
"github.com/go-sql-driver/mysql"
"golang.org/x/term"
)
type DB struct {
*sql.DB
Name string
}
func getUsername() (string, error) {
user := os.Getenv("DB_USER")
if user == "" {
var err error
fmt.Printf("DB Benutzer: ")
user, err = bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return "", fmt.Errorf("getUsername: bufio.NewReader(os.Stdin).ReadString('\n'): %v", err)
}
}
return strings.TrimSpace(user), nil
}
func getPassword() (string, error) {
pass := os.Getenv("DB_PASS")
if pass == "" {
fmt.Printf("DB Passwort: ")
bytePass, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", fmt.Errorf("getCredentials: term.ReadPassword(int(syscall.Stdin)): %v", err)
}
fmt.Println()
pass = strings.TrimSpace(string(bytePass))
}
return pass, nil
}
func getCredentials() (string, string, error) {
user, err := getUsername()
if err != nil {
return "", "", fmt.Errorf("getCredentials: getUsername(): %v", err)
}
pass, err := getPassword()
if err != nil {
return "", "", fmt.Errorf("getCredentials: getPassword(): %v", err)
}
return user, pass, nil
}
func reverseOrder(bs []*types.Briefing) {
for i, j := 0, len(bs)-1; i < j; i, j = i+1, j-1 {
bs[i], bs[j] = bs[j], bs[i]
}
}
func OpenDB(dbName string) (*DB, error) {
var err error
db := new(DB)
cfg := mysql.NewConfig()
cfg.DBName = dbName
cfg.User, cfg.Passwd, err = getCredentials()
if err != nil {
return nil, fmt.Errorf("Open: getCredentials(): %v\n", err)
}
db.Name = dbName
db.DB, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
return nil, fmt.Errorf("Open: sql.Open(\"mysql\", cfg.FormatDSN()): %v\n", err)
}
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("Open: db.Ping(): %v\n", err)
}
return db, nil
}
func (db *DB) WriteBriefing(b *types.Briefing) error {
for i := 0; i < len(b.Participants); i++ {
result, err := db.Exec("INSERT INTO "+db.Name+" (instructor_first,"+
" instructor_last, date, time, state, location, participant_first,"+
" participant_last, company) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
b.FirstName, b.LastName, b.Date, b.Time, b.State, b.Location,
b.Participants[i].FirstName, b.Participants[i].LastName,
b.Participants[i].Company)
if err != nil {
return fmt.Errorf("*DB.WriteBriefing: db.Exec(\"INSERT INTO"+
" \"+db.Name+\" (instructor_first, instructor_last, date, time, state,"+
" location, participant_first, participant_last, company) VALUES (?, ?,"+
" ?, ?, ?, ?, ?, ?, ?)\", b.FirstName, b.LastName, b.Date, b.Time,"+
" b.State, b.Location, b.Participants[i].FirstName,"+
" b.Participants[i].LastName, b.Participants[i].Company): %v\n", err)
}
_, err = result.LastInsertId()
if err != nil {
return fmt.Errorf("*DB.WriteBriefing: result.LastInsertId(): %v\n", err)
}
}
return nil
}
func (db *DB) ReadAll() ([]*types.Briefing, error) {
bs := make([]*types.Briefing, 0)
rows, err := db.Query("SELECT *" + " FROM " + db.Name)
if err != nil {
return nil, fmt.Errorf("*DB.ReadAll: db.Query(\"SELECT * FROM \"+db.Name): %v\n", err)
}
defer rows.Close()
for rows.Next() {
b := new(types.Briefing)
p := new(types.Participant)
if err := rows.Scan(&p.ID, &b.FirstName, &b.LastName, &b.Date, &b.Time,
&b.State, &b.Location, &p.FirstName, &p.LastName, &p.Company); err != nil {
return nil, fmt.Errorf("*DB.ReadAll: db.Query(): %v\n", err)
}
b.Participants = append(b.Participants, p)
bs = append(bs, b)
}
reverseOrder(bs)
return bs, nil
}
func (db *DB) ReadByName(name string) ([]*types.Briefing, error) {
bs := make([]*types.Briefing, 0)
rows, err := db.Query("SELECT *"+
" FROM "+db.Name+
" WHERE instructor_first LIKE ?"+
" OR instructor_last LIKE ?"+
" OR participant_first LIKE ?"+
" OR participant_last LIKE ?",
"%"+name+"%", "%"+name+"%", "%"+name+"%", "%"+name+"%")
if err != nil {
return nil, fmt.Errorf("*DB.ReadByName: db.Query(\"SELECT *"+
" FROM \"+db.Name+"+
" WHERE instructor_first LIKE ?"+
" OR instructor_last LIKE ?"+
" OR participant_first LIKE ?"+
" OR participant_last LIKE ?\"): %v\n", err)
}
defer rows.Close()
for rows.Next() {
b := new(types.Briefing)
p := new(types.Participant)
if err := rows.Scan(&p.ID, &b.FirstName, &b.LastName, &b.Date, &b.Time, &b.State,
&b.Location, &p.FirstName, &p.LastName, &p.Company); err != nil {
return nil, fmt.Errorf("*DB.ReadByName: rows.Scan(&p.ID, &b.FirstName,"+
" &b.LastName, &b.Date, &b.Time, &b.State, &b.Location, &p.FirstName,"+
" &p.LastName, &p.Company): %v\n", err)
}
b.Participants = append(b.Participants, p)
bs = append(bs, b)
}
reverseOrder(bs)
return bs, nil
}
func (db *DB) GetLastID(i *int64) error {
row := db.QueryRow("SELECT id" +
" FROM " + db.Name +
" ORDER BY id DESC LIMIT 0, 1")
if err := row.Scan(i); err != nil {
return fmt.Errorf("*DB.GetLastID: row.Scan(&i): %v\n", err)
}
return nil
}

View File

@ -0,0 +1,42 @@
package data
import "streifling.com/jason/sicherheitsunterweisung/packages/types"
func InitQuestions() []types.Question {
Q := make([]types.Question, 0)
Q = append(Q, types.Question{
Text: "Wie viel ist 1+1?",
Answers: []types.Answer{
{ID: 0, Text: "1"},
{ID: 1, Text: "2"},
{ID: 2, Text: "3"},
{ID: 3, Text: "4"},
},
Correct: 1,
})
Q = append(Q, types.Question{
Text: "Wie viel ist 2+2?",
Answers: []types.Answer{
{ID: 0, Text: "1"},
{ID: 1, Text: "2"},
{ID: 2, Text: "3"},
{ID: 3, Text: "4"},
},
Correct: 3,
})
Q = append(Q, types.Question{
Text: "Wie viel ist 1+2?",
Answers: []types.Answer{
{ID: 0, Text: "1"},
{ID: 1, Text: "2"},
{ID: 2, Text: "3"},
{ID: 3, Text: "4"},
},
Correct: 2,
})
return Q
}

206
packages/server/server.go Normal file
View File

@ -0,0 +1,206 @@
package server
import (
"crypto/rand"
"encoding/hex"
"fmt"
"html/template"
"log"
"net/http"
"strconv"
"strings"
"streifling.com/jason/sicherheitsunterweisung/packages/data"
"streifling.com/jason/sicherheitsunterweisung/packages/types"
)
type questionData struct {
ID int64
Q types.Question
I int
J int
}
func DisplayTable(db *data.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
bs, err := db.ReadAll()
if err != nil {
_ = fmt.Errorf("DisplayTable: %v\n", err)
}
template.Must(template.ParseFiles("templates/table.html")).ExecuteTemplate(w, "content", bs)
}
}
func DisplaySearchResults(db *data.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
bs, err := db.ReadByName(r.PostFormValue("search"))
if err != nil {
_ = fmt.Errorf("DisplayResults: db.ReadByName(r.PostFormValue()): %v\n", err)
}
template.Must(template.ParseFiles("templates/table.html")).ExecuteTemplate(w, "rows", bs)
}
}
func DisplayForm(i *int64) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
template.Must(template.ParseFiles("templates/briefing.html")).ExecuteTemplate(w, "content", i)
}
}
func generateUUID() string {
bs := make([]byte, 2)
if _, err := rand.Read(bs); err != nil {
_ = fmt.Errorf("GenerateUUID: rand.Read(bs): %v\n", err)
return ""
}
return hex.EncodeToString(bs)
}
func AddParticipant(i *int64, ls *[]string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
*i++
login := fmt.Sprintf("%d", *i) + "-" + generateUUID()
(*ls) = append(*ls, login)
template.Must(template.ParseFiles("templates/briefing.html")).ExecuteTemplate(w, "new", login)
}
}
func SubmitForm(db *data.DB, i, j *int64) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
b := new(types.Briefing)
b.FirstName = r.PostFormValue("instructor-first")
b.LastName = r.PostFormValue("instructor-last")
b.Date = r.PostFormValue("date")
b.Time = r.PostFormValue("time")
b.State = r.PostFormValue("state")
b.Location = r.PostFormValue("location")
for ; *j <= *i; *j++ {
b.Participants = append(b.Participants, &types.Participant{
ID: *j,
Person: types.Person{
FirstName: r.PostFormValue("participant-first-" + fmt.Sprint(*j)),
LastName: r.PostFormValue(("participant-last-" + fmt.Sprint(*j))),
},
Company: r.PostFormValue(("participant-company-" + fmt.Sprint(*j))),
})
}
log.Println(b)
db.WriteBriefing(b)
bs, err := db.ReadAll()
if err != nil {
_ = fmt.Errorf("SubmitForm: db.ReadAll(): %v\n", err)
}
template.Must(template.ParseFiles("templates/table.html")).Execute(w, bs)
}
}
// TODO: Make it only serve one purpose
func loginIsCorrect(l string, logins *[]string) bool {
for i, v := range *logins {
if l == v {
(*logins) = append((*logins)[:i], (*logins)[i+1:]...)
return true
}
}
return false
}
func newParticipant(l string) (*types.Participant, error) {
p := new(types.Participant)
idInt, err := strconv.Atoi(strings.Split(l, "-")[0])
if err != nil {
return nil, fmt.Errorf("newParticipant: strconv.Atoi(idString): %v\n", err)
}
p.ID = int64(idInt)
return p, nil
}
func DisplayParticipantForm(ls *[]string, cp chan<- *types.Participant) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := r.PostFormValue("login")
if loginIsCorrect(l, ls) {
p, err := newParticipant(l)
if err != nil {
http.Error(w, "GetParticipantData: newParticipant(l): "+fmt.Sprint(err), http.StatusInternalServerError)
}
cp <- p
template.Must(template.ParseFiles("templates/participant.html")).ExecuteTemplate(w, "content", p.ID)
} else {
template.Must(template.ParseFiles("templates/login.html")).ExecuteTemplate(w, "content", nil)
}
}
}
func readAnswer(r *http.Request, p *types.Participant, i int) error {
v, err := strconv.Atoi(r.PostFormValue("answer"))
if err != nil {
return fmt.Errorf("readAnswer: strconv.Atoi(r.PostFormValue(\"answer\")): %v\n", err)
}
p.Questions[i].Chosen = v
return nil
}
func DisplayQuestion(i int, p *types.Participant) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if i == 0 {
p.FirstName = r.PostFormValue("participant-first-" + fmt.Sprintf("%d", p.ID))
p.LastName = r.PostFormValue("participant-last-" + fmt.Sprintf("%d", p.ID))
p.Company = r.PostFormValue("participant-company-" + fmt.Sprintf("%d", p.ID))
} else {
if err := readAnswer(r, p, i-1); err != nil {
http.Error(w, "DisplayQuestion: readAnswer(r, p, i): "+fmt.Sprint(err), http.StatusInternalServerError)
}
}
data := new(questionData)
data.ID = p.ID
data.Q = p.Questions[i]
data.I = i
data.J = i + 1
template.Must(template.ParseFiles("templates/question.html")).ExecuteTemplate(w, "content", data)
}
}
func DisplayTestResults(b *types.Briefing, p *types.Participant) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
numQuestions := len(p.Questions)
wrongAnswers := make([]int, 0)
fmt.Println(wrongAnswers)
if err := readAnswer(r, p, numQuestions-1); err != nil {
http.Error(w, "DisplayTestResults: readAnswer(r, p, i): "+fmt.Sprint(err), http.StatusInternalServerError)
}
for i, q := range p.Questions {
if q.Chosen != q.Correct {
wrongAnswers = append(wrongAnswers, i)
}
}
if wrongAnswers == nil {
b.Participants = append(b.Participants, p)
} else {
data := new(questionData)
data.ID = p.ID
data.Q = p.Questions[0]
data.I = 0
data.J = data.I + 1
template.Must(template.ParseFiles("templates/question.html")).ExecuteTemplate(w, "content", data)
}
template.Must(template.ParseFiles("templates/results.html")).ExecuteTemplate(w, "content", nil)
}
}

36
packages/types/types.go Normal file
View File

@ -0,0 +1,36 @@
package types
type Person struct {
FirstName string
LastName string
}
type Instructor Person
type Answer struct {
ID int
Text string
}
type Question struct {
Text string
Answers []Answer
Chosen int
Correct int
}
type Participant struct {
ID int64
Person
Company string
Questions []Question
}
type Briefing struct {
Instructor
Date string
Time string
State string
Location string
Participants []*Participant
}

38
templates/briefing.html Normal file
View File

@ -0,0 +1,38 @@
{{ define "add-button" }}
<button type="button" hx-post="/add-participant/" hx-target="this" hx-swap="outerHTML">
Neuer Teilnehmer
</button>
{{ end }}
{{ define "new" }}
<span>{{ . }}</span>
{{ template "add-button" . }}
{{ end }}
{{ define "content" }}
<form>
<div id="instructor">
<label for="instructor-first-input">Unterweiser Vorname</label>
<input type="text" name="instructor-first" id="instructor-first-input" />
<label for="instructor-last-input">Unterweiser Nachname</label>
<input type="text" name="instructor-last" id="instructor-last-input" />
</div>
<div id="location">
<label for="location-input">Ort</label>
<input type="text" name="location" id="location-input" />
</div>
<div id="state">
<label for="state-input">Stand vom</label>
<input type="date" name="state" id="state-input" />
</div>
{{ template "add-button" . }}
<button type="submit" hx-post="/submit-form/" hx-target="#content" hx-swap="innerHTML">
OK
</button>
</form>
{{ end }}

View File

@ -11,57 +11,9 @@
<body>
<h1>Sicherheitsunterweisung</h1>
<form>
<div id="instructor">
<label for="instructor-first-input">Unterweiser Vorname</label>
<input type="text" name="instructor-first" id="instructor-first-input" />
<label for="instructor-last-input">Unterweiser Nachname</label>
<input type="text" name="instructor-last" id="instructor-last-input" />
</div>
<div id="date">
<label for="date-input">Datum</label>
<input type="date" name="date" id="date-input" />
</div>
<div id="time">
<label for="time-input">Uhrzeit</label>
<input type="time" name="time" id="time-input" />
</div>
<div id="state">
<label for="state-input">Stand vom</label>
<input type="date" name="state" id="state-input" />
</div>
<div id="location">
<label for="location-input">Ort</label>
<input type="text" name="location" id="location-input" />
</div>
<div id="participants">
<button type="button" hx-post="/add-participant/" hx-target="#participants" hx-swap="beforeend"
hx-trigger="click">
+
</button>
<div id="participant-{{ . }}">
<label for="participant-first-input-{{ . }}">Vorname</label>
<input type="text" name="participant-first-{{ . }}" id="participant-first-input-{{ . }}" />
<label for="participant-last-input-{{ . }}">Nachname</label>
<input type="text" name="participant-last-{{ . }}" id="participant-last-input-{{ . }}" />
<label for="participant-company-input-{{ . }}">Firma</label>
<input type="text" name="participant-company-{{ . }}" id="participant-company-input-{{ . }}" />
</div>
</div>
<button type="submit" hx-post="/submit/">
Senden
</button>
</form>
<div id="content">
{{ template "content" . }}
</div>
<script src="/static/js/htmx.min.js" type="text/javascript"></script>
</body>

16
templates/login.html Normal file
View File

@ -0,0 +1,16 @@
{{ define "content" }}
<h2>Login</h2>
<form>
<label for="login-input">Code</label>
<input type="text" name="login" id="login-input" />
<button type="submit" hx-post="/external-login/" hx-target="#content">
Anmelden
</button>
<button type="submit" hx-post="/internal-login/" hx-target="#content">
Intern
</button>
</form>
{{ end }}

View File

@ -1,5 +1,5 @@
{{ define "participant" }}
<div id="participant-{{ . }}">
{{ define "content" }}
<form id="participant-{{ . }}">
<label for="participant-first-input-{{ . }}">Vorname</label>
<input type="text" name="participant-first-{{ . }}" id="participant-first-input-{{ . }}" />
@ -8,5 +8,9 @@
<label for="participant-company-input-{{ . }}">Firma</label>
<input type="text" name="participant-company-{{ . }}" id="participant-company-input-{{ . }}" />
</div>
<button type="button" hx-post="/display-question-{{ . }}-0/" hx-target="#content">
Fertig
</button>
</form>
{{ end }}

21
templates/question.html Normal file
View File

@ -0,0 +1,21 @@
{{ define "answers" }}
{{ range .Q.Answers }}
<div>
<input type="radio" name="answer" id="answer-{{ .ID }}" value="{{ .ID }}" />
<label for="answer-{{ .ID }}">{{ .Text }}</label>
</div>
{{ end }}
{{ end }}
{{ define "content" }}
<h2>Frage {{ .I }}</h2>
<p>{{ .Q.Text }}</p>
<form>
{{ template "answers" . }}
<button type="submit" hx-post="/display-question-{{ .ID }}-{{ .J }}/" hx-target="#content" hx-swap="innerHTML">
Weiter
</button>
</form>
{{ end }}

11
templates/results.html Normal file
View File

@ -0,0 +1,11 @@
{{ define "passed" }}
{{ end }}
{{ define "failed" }}
{{ end }}
{{ define "content" }}
{{ q := range .Participant.Questions }}
<p>{{ . }}{{ q.Text }}</p>
{{ end }}
{{ end }}

49
templates/table.html Normal file
View File

@ -0,0 +1,49 @@
{{ define "rows" }}
{{ range . }}
<tr>
<td>{{ .FirstName }}</td>
<td>{{ .LastName }}</td>
<td>{{ .Date }}</td>
<td>{{ .Time }}</td>
<td>{{ .State }}</td>
<td>{{ .Location }}</td>
{{ range .Participants }}
<td>{{ .FirstName }}</td>
<td>{{ .LastName }}</td>
<td>{{ .Company }}</td>
{{ end }}
</tr>
{{ end }}
{{ end }}
{{ define "content" }}
<form>
<label for="search-input">Suche</label>
<input type="text" name="search" id="search-input" hx-post="/search/" hx-target="#results" hx-swap="innerHTML"
hx-trigger="keyup changed delay:200ms" />
</form>
<form>
<button type="submit" hx-post="/new-briefing/" hx-target="#content" hx-swap="innerHTML">
Neue Unterweisung
</button>
</form>
<table>
<thead>
<tr>
<th colspan="2">Unterweiser</th>
<th>Datum</th>
<th>Uhrzeit</th>
<th>Stand</th>
<th>Ort</th>
<th colspan="2">Teilnehmer</th>
<th>Firma</th>
</tr>
</thead>
<tbody id="results">
{{ template "rows" . }}
</tbody>
</table>
{{ end }}

1
tmp/build-errors.log Normal file
View File

@ -0,0 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

BIN
tmp/main Executable file

Binary file not shown.