Compare commits
14 Commits
online_que
...
3496fe5f86
Author | SHA1 | Date | |
---|---|---|---|
3496fe5f86 | |||
10247722c8 | |||
c7bb630043 | |||
5049db064c | |||
aded71394d | |||
d054b3644b | |||
b78e30d109 | |||
52cd5756d8 | |||
1e6d83de1d | |||
4de7d36385 | |||
35d565ec7d | |||
6359caf3e9 | |||
8622f81f89 | |||
4e0c8ec1ac |
2
.gitignore
vendored
2
.gitignore
vendored
@ -0,0 +1,2 @@
|
|||||||
|
tmp
|
||||||
|
test.sql
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
USE sicherheitsunterweisung;
|
USE sicherheitsunterweisung;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
DROP TABLE IF EXISTS instructors;
|
DROP TABLE IF EXISTS instructors;
|
||||||
DROP TABLE IF EXISTS briefings;
|
DROP TABLE IF EXISTS briefings;
|
||||||
DROP TABLE IF EXISTS participants;
|
DROP TABLE IF EXISTS participants;
|
||||||
DROP TABLE IF EXISTS questions;
|
DROP TABLE IF EXISTS questions;
|
||||||
DROP TABLE IF EXISTS given_answers;
|
DROP TABLE IF EXISTS given_answers;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
CREATE TABLE instructors (
|
CREATE TABLE instructors (
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL,
|
||||||
first_name VARCHAR(32) NOT NULL,
|
first_name VARCHAR(32) NOT NULL,
|
||||||
last_name VARCHAR(32) NOT NULL,
|
last_name VARCHAR(32) NOT NULL,
|
||||||
personnel_id INT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY(id)
|
PRIMARY KEY(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE briefings (
|
CREATE TABLE briefings (
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT AUTO_INCREMENT,
|
||||||
date DATE NOT NULL,
|
date DATE NOT NULL,
|
||||||
time TIME NOT NULL,
|
time TIME NOT NULL,
|
||||||
location VARCHAR(32) NOT NULL,
|
location VARCHAR(32) NOT NULL,
|
||||||
@ -28,7 +30,7 @@ CREATE TABLE briefings (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE participants (
|
CREATE TABLE participants (
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT AUTO_INCREMENT,
|
||||||
first_name VARCHAR(32) NOT NULL,
|
first_name VARCHAR(32) NOT NULL,
|
||||||
last_name VARCHAR(32) NOT NULL,
|
last_name VARCHAR(32) NOT NULL,
|
||||||
company VARCHAR(32) NOT NULL,
|
company VARCHAR(32) NOT NULL,
|
||||||
@ -37,7 +39,7 @@ CREATE TABLE participants (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE questions (
|
CREATE TABLE questions (
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT AUTO_INCREMENT,
|
||||||
question VARCHAR(256) NOT NULL,
|
question VARCHAR(256) NOT NULL,
|
||||||
answer_1 VARCHAR(64) NOT NULL,
|
answer_1 VARCHAR(64) NOT NULL,
|
||||||
answer_2 VARCHAR(64) NOT NULL,
|
answer_2 VARCHAR(64) NOT NULL,
|
||||||
@ -61,17 +63,17 @@ CREATE TABLE given_answers (
|
|||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO instructors
|
INSERT INTO instructors
|
||||||
(first_name, last_name, personnel_id)
|
(id, first_name, last_name)
|
||||||
VALUES
|
VALUES
|
||||||
( 'Jason', 'Streifling', '123456' ),
|
( '123456', 'Jason', 'Streifling' ),
|
||||||
( 'Tim', 'Taler', '123457' ),
|
( '123457', 'Tim', 'Taler' ),
|
||||||
( 'Georg', 'aus dem Jungel', '123458' );
|
( '123458', 'Georg', 'aus dem Jungel' );
|
||||||
|
|
||||||
INSERT INTO briefings (
|
INSERT INTO briefings (
|
||||||
date, time, location, as_of, instructor_id
|
date, time, location, document_name, as_of, instructor_id
|
||||||
) VALUES
|
) VALUES
|
||||||
( '2023-10-16', '17:00:00', 'Werk Langenhagen', 'ICS-2021-LGH', '2021-02-01', '1' ),
|
( '2023-10-16', '17:00:00', 'Werk Langenhagen', 'ICS-2021-LGH', '2021-02-01', '123456' ),
|
||||||
( '2023-10-16', '17:05:00', 'Werk Langenhagen', 'ICS-2021-LGH', '2021-02-01', '2' );
|
( '2023-10-16', '17:05:00', 'Werk Langenhagen', 'ICS-2021-LGH', '2021-02-01', '123457' );
|
||||||
|
|
||||||
INSERT INTO participants (
|
INSERT INTO participants (
|
||||||
first_name, last_name, company
|
first_name, last_name, company
|
||||||
@ -84,7 +86,8 @@ INSERT INTO questions (
|
|||||||
) VALUES
|
) VALUES
|
||||||
( 'Was ist 1+1?', '1', '2', '3', '4', '2' ),
|
( 'Was ist 1+1?', '1', '2', '3', '4', '2' ),
|
||||||
( 'Was ist 1+2?', '1', '2', '3', '4', '3' ),
|
( 'Was ist 1+2?', '1', '2', '3', '4', '3' ),
|
||||||
( 'Was ist 2+2?', '1', '2', '3', '4', '4' );
|
( 'Was ist 2+2?', '1', '2', '3', '4', '4' ),
|
||||||
|
( 'Was ist 0+1?', '1', '2', '3', '4', '1' );
|
||||||
|
|
||||||
INSERT INTO given_answers (
|
INSERT INTO given_answers (
|
||||||
briefing_id, participant_id, question_id, given_answer
|
briefing_id, participant_id, question_id, given_answer
|
||||||
@ -92,6 +95,8 @@ INSERT INTO given_answers (
|
|||||||
( '1', '1', '1', '2' ),
|
( '1', '1', '1', '2' ),
|
||||||
( '1', '1', '2', '3' ),
|
( '1', '1', '2', '3' ),
|
||||||
( '1', '1', '3', '3' ),
|
( '1', '1', '3', '3' ),
|
||||||
|
( '1', '1', '4', '1' ),
|
||||||
( '2', '2', '1', '2' ),
|
( '2', '2', '1', '2' ),
|
||||||
( '2', '2', '2', '3' ),
|
( '2', '2', '2', '3' ),
|
||||||
( '2', '2', '3', '4' );
|
( '2', '2', '3', '4' ),
|
||||||
|
( '2', '2', '4', '1' );
|
||||||
|
1
go.mod
1
go.mod
@ -4,6 +4,7 @@ go 1.21.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/go-sql-driver/mysql v1.7.1
|
github.com/go-sql-driver/mysql v1.7.1
|
||||||
|
github.com/google/uuid v1.3.1
|
||||||
golang.org/x/term v0.13.0
|
golang.org/x/term v0.13.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
2
go.sum
2
go.sum
@ -1,5 +1,7 @@
|
|||||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
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=
|
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
|
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||||
|
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||||
|
53
main.go
53
main.go
@ -1,33 +1,68 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"streifling.com/jason/sicherheitsunterweisung/packages/data"
|
"streifling.com/jason/sicherheitsunterweisung/packages/data"
|
||||||
"streifling.com/jason/sicherheitsunterweisung/packages/server"
|
"streifling.com/jason/sicherheitsunterweisung/packages/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func handleParticipants(mux *http.ServeMux, db *data.DB, cp <-chan *data.Participant, s *session.Session) {
|
||||||
logins := make([]string, 0)
|
for participant := range cp {
|
||||||
|
mux.HandleFunc("/submit-participant/"+fmt.Sprint(s.ID)+"/"+fmt.Sprint(participant.Login)+"/", s.HandleParticipant(participant, &s.Questions, db))
|
||||||
|
for i := range s.Questions {
|
||||||
|
mux.HandleFunc("/submit-answer/"+fmt.Sprint(s.ID)+"/"+fmt.Sprint(participant.Login)+"/"+fmt.Sprint(i+1)+"/", s.HandleAnswer(db, participant, &s.Questions, int64(i+1)))
|
||||||
|
}
|
||||||
|
mux.HandleFunc("/retry/"+fmt.Sprint(s.ID)+"/"+fmt.Sprint(participant.Login)+"/", s.HandleRetry(participant, &s.Questions))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSessions(mux *http.ServeMux, db *data.DB, cs <-chan *session.Session, ss *[]*session.Session) {
|
||||||
|
for s := range cs {
|
||||||
|
(*ss) = append(*ss, s)
|
||||||
|
participantChan := make(chan *data.Participant)
|
||||||
|
questionIDs := make([]string, 4)
|
||||||
|
|
||||||
|
for i := 0; i < len(questionIDs); i++ {
|
||||||
|
questionIDs[i] = fmt.Sprint(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
s.Questions, err = db.GetQuestions(questionIDs)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mux.HandleFunc("/new-briefing/", s.HandleNewBriefing())
|
||||||
|
mux.HandleFunc("/new-participant/"+fmt.Sprint(s.ID)+"/", s.HandleNewParticipant(participantChan))
|
||||||
|
mux.HandleFunc("/submit-form/"+fmt.Sprint(s.ID)+"/", s.HandleBriefingForm(db))
|
||||||
|
|
||||||
|
go handleParticipants(mux, db, participantChan, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
db, err := data.OpenDB("sicherheitsunterweisung")
|
db, err := data.OpenDB("sicherheitsunterweisung")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
sessions := make([]*session.Session, 0)
|
||||||
|
sessionChan := make(chan *session.Session)
|
||||||
|
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static/"))))
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static/"))))
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
template.Must(template.ParseFiles("templates/index.html", "templates/login.html")).Execute(w, nil)
|
template.Must(template.ParseFiles("templates/index.html", "templates/login.html")).Execute(w, nil)
|
||||||
})
|
})
|
||||||
mux.HandleFunc("/search/", server.DisplaySearchResults(db))
|
mux.HandleFunc("/internal-login/", session.HandleInternalLogin(&sessions, sessionChan, db))
|
||||||
mux.HandleFunc("/new-briefing/", server.DisplayInstructorForm())
|
mux.HandleFunc("/external-login/", session.HandleExternalLogin(&sessions))
|
||||||
mux.HandleFunc("/add-participant/", server.AddParticipant(&logins))
|
mux.HandleFunc("/search/", session.HandleSearch(db))
|
||||||
mux.HandleFunc("/submit-form/", server.SubmitBriefingForm(db, &logins))
|
|
||||||
mux.HandleFunc("/internal-login/", server.DisplayTable(db))
|
go handleSessions(mux, db, sessionChan, &sessions)
|
||||||
mux.HandleFunc("/external-login/", server.DisplayParticipantForm(&logins))
|
|
||||||
|
|
||||||
log.Fatalln(http.ListenAndServe(":8080", mux))
|
log.Fatalln(http.ListenAndServe(":8080", mux))
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
package types
|
package data
|
||||||
|
|
||||||
|
import "database/sql"
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
*sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
type Person struct {
|
type Person struct {
|
||||||
ID int64
|
ID int64
|
||||||
@ -11,6 +17,7 @@ type Instructor Person
|
|||||||
type Participant struct {
|
type Participant struct {
|
||||||
Person
|
Person
|
||||||
Company string
|
Company string
|
||||||
|
Login string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Briefing struct {
|
type Briefing struct {
|
@ -1,65 +1,13 @@
|
|||||||
package data
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"streifling.com/jason/sicherheitsunterweisung/packages/types"
|
|
||||||
|
|
||||||
"github.com/go-sql-driver/mysql"
|
"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 OpenDB(dbName string) (*DB, error) {
|
func OpenDB(dbName string) (*DB, error) {
|
||||||
var err error
|
var err error
|
||||||
db := new(DB)
|
db := new(DB)
|
||||||
@ -71,7 +19,6 @@ func OpenDB(dbName string) (*DB, error) {
|
|||||||
return nil, fmt.Errorf("Open: getCredentials(): %v\n", err)
|
return nil, fmt.Errorf("Open: getCredentials(): %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Name = dbName
|
|
||||||
db.DB, err = sql.Open("mysql", cfg.FormatDSN())
|
db.DB, err = sql.Open("mysql", cfg.FormatDSN())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Open: sql.Open(\"mysql\", cfg.FormatDSN()): %v\n", err)
|
return nil, fmt.Errorf("Open: sql.Open(\"mysql\", cfg.FormatDSN()): %v\n", err)
|
||||||
@ -83,7 +30,7 @@ func OpenDB(dbName string) (*DB, error) {
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) WriteBriefing(b *types.Briefing) error {
|
func (db *DB) WriteBriefing(b *Briefing) error {
|
||||||
result, err := db.Exec(`
|
result, err := db.Exec(`
|
||||||
INSERT INTO briefings
|
INSERT INTO briefings
|
||||||
(date, time, location, document_name, as_of, instructor_id)
|
(date, time, location, document_name, as_of, instructor_id)
|
||||||
@ -102,7 +49,7 @@ func (db *DB) WriteBriefing(b *types.Briefing) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) WriteParticipant(p *types.Participant) error {
|
func (db *DB) WriteParticipant(p *Participant) error {
|
||||||
result, err := db.Exec(`
|
result, err := db.Exec(`
|
||||||
INSERT INTO participants
|
INSERT INTO participants
|
||||||
(first_name, last_name, company)
|
(first_name, last_name, company)
|
||||||
@ -121,7 +68,7 @@ func (db *DB) WriteParticipant(p *types.Participant) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) WriteGivenAnswer(b *types.Briefing, p *types.Participant, q *types.Question, g *types.GivenAnswer) error {
|
func (db *DB) WriteGivenAnswer(b *Briefing, p *Participant, q *Question, g int) error {
|
||||||
_, err := db.Exec(`
|
_, err := db.Exec(`
|
||||||
INSERT INTO given_answers
|
INSERT INTO given_answers
|
||||||
(briefing_id, participant_id, question_id, given_answer)
|
(briefing_id, participant_id, question_id, given_answer)
|
||||||
@ -135,7 +82,7 @@ func (db *DB) WriteGivenAnswer(b *types.Briefing, p *types.Participant, q *types
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) WriteAllDataOfBriefing(b *types.Briefing, sp *[]*types.Participant, sq *[]*types.Question, sg *[]*types.GivenAnswer) error {
|
func (db *DB) WriteAllDataOfBriefing(b *Briefing, sp *[]*Participant, sq *[]*Question, sg *[]*GivenAnswer) error {
|
||||||
if err := db.WriteBriefing(b); err != nil {
|
if err := db.WriteBriefing(b); err != nil {
|
||||||
return fmt.Errorf("*DB.WriteAllDataOfBriefing: db.writeBriefing(): %v\n", err)
|
return fmt.Errorf("*DB.WriteAllDataOfBriefing: db.writeBriefing(): %v\n", err)
|
||||||
}
|
}
|
||||||
@ -148,14 +95,14 @@ func (db *DB) WriteAllDataOfBriefing(b *types.Briefing, sp *[]*types.Participant
|
|||||||
|
|
||||||
for _, p := range *sp {
|
for _, p := range *sp {
|
||||||
for i, q := range *sq {
|
for i, q := range *sq {
|
||||||
db.WriteGivenAnswer(b, p, q, (*sg)[i])
|
db.WriteGivenAnswer(b, p, q, i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) GetAllOverviewTableData() (*[]*types.OverviewTableData, error) {
|
func (db *DB) GetAllOverviewTableData() ([]*OverviewTableData, error) {
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
SELECT
|
SELECT
|
||||||
i.first_name,
|
i.first_name,
|
||||||
@ -173,8 +120,12 @@ func (db *DB) GetAllOverviewTableData() (*[]*types.OverviewTableData, error) {
|
|||||||
ON b.id = g.briefing_id
|
ON b.id = g.briefing_id
|
||||||
INNER JOIN participants AS p
|
INNER JOIN participants AS p
|
||||||
ON p.id = g.participant_id
|
ON p.id = g.participant_id
|
||||||
|
INNER JOIN questions AS q
|
||||||
|
ON q.id = g.question_id
|
||||||
INNER JOIN instructors AS i
|
INNER JOIN instructors AS i
|
||||||
ON i.id = b.instructor_id
|
ON i.id = b.instructor_id
|
||||||
|
WHERE
|
||||||
|
q.id = 1
|
||||||
ORDER BY
|
ORDER BY
|
||||||
b.id DESC,
|
b.id DESC,
|
||||||
p.id
|
p.id
|
||||||
@ -184,9 +135,9 @@ func (db *DB) GetAllOverviewTableData() (*[]*types.OverviewTableData, error) {
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
data := make([]*types.OverviewTableData, 0)
|
data := make([]*OverviewTableData, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
otd := new(types.OverviewTableData)
|
otd := new(OverviewTableData)
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&otd.InstructorFirstName,
|
&otd.InstructorFirstName,
|
||||||
@ -207,10 +158,10 @@ func (db *DB) GetAllOverviewTableData() (*[]*types.OverviewTableData, error) {
|
|||||||
data = append(data, otd)
|
data = append(data, otd)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) GetOverviewTableDataByName(n string) (*[]*types.OverviewTableData, error) {
|
func (db *DB) GetOverviewTableDataByName(n string) (*[]*OverviewTableData, error) {
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
SELECT
|
SELECT
|
||||||
i.first_name,
|
i.first_name,
|
||||||
@ -231,6 +182,7 @@ func (db *DB) GetOverviewTableDataByName(n string) (*[]*types.OverviewTableData,
|
|||||||
INNER JOIN instructors AS i
|
INNER JOIN instructors AS i
|
||||||
ON i.id = b.instructor_id
|
ON i.id = b.instructor_id
|
||||||
WHERE
|
WHERE
|
||||||
|
q.id = 1 AND
|
||||||
i.first_name LIKE ? OR
|
i.first_name LIKE ? OR
|
||||||
i.last_name LIKE ? OR
|
i.last_name LIKE ? OR
|
||||||
p.first_name LIKE ? OR
|
p.first_name LIKE ? OR
|
||||||
@ -244,9 +196,9 @@ func (db *DB) GetOverviewTableDataByName(n string) (*[]*types.OverviewTableData,
|
|||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
data := make([]*types.OverviewTableData, 0)
|
data := make([]*OverviewTableData, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
otd := new(types.OverviewTableData)
|
otd := new(OverviewTableData)
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&otd.InstructorFirstName,
|
&otd.InstructorFirstName,
|
||||||
@ -287,23 +239,91 @@ func (db *DB) GetLastID(table string) (int, error) {
|
|||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) GetInstructors() (*[]*types.Instructor, error) {
|
func (db *DB) GetInstructors() ([]*Instructor, error) {
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM instructors
|
FROM instructors
|
||||||
|
ORDER BY
|
||||||
|
last_name,
|
||||||
|
first_name
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("*DB.GetInstructors: db.Query(): %v\n", err)
|
return nil, fmt.Errorf("*DB.GetInstructors: db.Query(): %v\n", err)
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
instructors := make([]*types.Instructor, 0)
|
instructors := make([]*Instructor, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
instructor := new(types.Instructor)
|
instructor := new(Instructor)
|
||||||
if err = rows.Scan(instructor); err != nil {
|
if err = rows.Scan(&instructor.ID, &instructor.FirstName, &instructor.LastName); err != nil {
|
||||||
return nil, fmt.Errorf("*DB.GetInstructors: rows.Scan(): %v\n", err)
|
return nil, fmt.Errorf("*DB.GetInstructors: rows.Scan(): %v\n", err)
|
||||||
}
|
}
|
||||||
instructors = append(instructors, instructor)
|
instructors = append(instructors, instructor)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &instructors, nil
|
return instructors, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetQuestions(nums []string) ([]Question, error) {
|
||||||
|
rows, err := db.Query(`
|
||||||
|
SELECT *
|
||||||
|
FROM questions
|
||||||
|
WHERE id IN (` + strings.Join(nums, ", ") + `)
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("*DB.GetQuestions: db.Query(): %v\n", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
questions := make([]Question, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
q := new(Question)
|
||||||
|
a1 := new(Answer)
|
||||||
|
a2 := new(Answer)
|
||||||
|
a3 := new(Answer)
|
||||||
|
a4 := new(Answer)
|
||||||
|
|
||||||
|
a1.ID = 1
|
||||||
|
a2.ID = 2
|
||||||
|
a3.ID = 3
|
||||||
|
a4.ID = 4
|
||||||
|
|
||||||
|
if err := rows.Scan(&q.ID, &q.Text, &a1.Text, &a2.Text, &a3.Text, &a4.Text, &q.Correct); err != nil {
|
||||||
|
return nil, fmt.Errorf("*DB.GetQuestions: rows.Scan(): %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q.Answers = append(q.Answers, *a1)
|
||||||
|
q.Answers = append(q.Answers, *a2)
|
||||||
|
q.Answers = append(q.Answers, *a3)
|
||||||
|
q.Answers = append(q.Answers, *a4)
|
||||||
|
|
||||||
|
questions = append(questions, *q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return questions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetGivenAnswers(bid, pid int64, sq []Question) ([]int, error) {
|
||||||
|
answers := make([]int, 0)
|
||||||
|
query := `
|
||||||
|
SELECT given_answer
|
||||||
|
FROM given_answers
|
||||||
|
WHERE
|
||||||
|
briefing_id = ? AND
|
||||||
|
participant_id = ? AND
|
||||||
|
question_id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
for _, q := range sq {
|
||||||
|
var answer int
|
||||||
|
|
||||||
|
row := db.QueryRow(query, bid, pid, q.ID)
|
||||||
|
if err := row.Scan(&answer); err != nil {
|
||||||
|
return nil, fmt.Errorf("*DB.GetGivenAnswers: row.Scan(): %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
answers = append(answers, answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return answers, nil
|
||||||
}
|
}
|
52
packages/data/helperFuncs.go
Normal file
52
packages/data/helperFuncs.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,193 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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.GetAllOverviewTableData()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "DisplayTable: *DB.GetAllOverviewTableData(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
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.GetOverviewTableDataByName(r.PostFormValue("search"))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "DisplayResults: db.ReadByName(r.PostFormValue()): "+fmt.Sprint(err), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
template.Must(template.ParseFiles("templates/table.html")).ExecuteTemplate(w, "rows", bs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DisplayInstructorForm() http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
template.Must(template.ParseFiles("templates/briefing.html")).ExecuteTemplate(w, "content", nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateUUID() (string, error) {
|
|
||||||
bs := make([]byte, 4)
|
|
||||||
|
|
||||||
if _, err := rand.Read(bs); err != nil {
|
|
||||||
return "", fmt.Errorf("GenerateUUID: rand.Read(bs): %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hex.EncodeToString(bs), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddParticipant(sl *[]string) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
login, err := generateUUID()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "AddParticipant: generateUUID(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
(*sl) = append(*sl, login)
|
|
||||||
template.Must(template.ParseFiles("templates/briefing.html")).ExecuteTemplate(w, "new", login)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Hier weiter machen, irgendwie die b.ID herausgeben,
|
|
||||||
// am besten hier auch die p.IDs rausgeben, damit diese später verknüpft werden können
|
|
||||||
func SubmitBriefingForm(db *data.DB, sl *[]string) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
now := time.Now()
|
|
||||||
briefing := new(types.Briefing)
|
|
||||||
|
|
||||||
// TODO: Dropdownmenü
|
|
||||||
// instructorFirstName := r.PostFormValue("instructor-first")
|
|
||||||
// instructorLastName := r.PostFormValue("instructor-last")
|
|
||||||
|
|
||||||
briefing.Date = now.Format("2006-01-02")
|
|
||||||
briefing.Time = now.Format("15:04:05")
|
|
||||||
briefing.Location = r.PostFormValue("location")
|
|
||||||
briefing.DocumentName = r.PostFormValue("document") // TODO: in HTML einfügen
|
|
||||||
briefing.AsOf = r.PostFormValue("state") // TODO: Umbenennen
|
|
||||||
// briefing.InstructorID = r.PostFormValue("instructor-id") // TODO: aus Dropdown holen
|
|
||||||
|
|
||||||
db.WriteBriefing(briefing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
var err error
|
|
||||||
p := new(types.Participant)
|
|
||||||
|
|
||||||
p.ID, err = strconv.ParseInt(l, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("newParticipant: strconv.Atoi(idString): %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DisplayParticipantForm(sl *[]string) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if loginIsCorrect(r.PostFormValue("login"), sl) {
|
|
||||||
uuid, err := generateUUID()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "DisplayParticipantForm: generateUUID(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
template.Must(template.ParseFiles("templates/participant.html")).ExecuteTemplate(w, "content", uuid)
|
|
||||||
} 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(): %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)
|
|
||||||
// }
|
|
||||||
// }
|
|
215
packages/session/handlerFuncs.go
Normal file
215
packages/session/handlerFuncs.go
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"streifling.com/jason/sicherheitsunterweisung/packages/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HandleInternalLogin(ss *[]*Session, cs chan<- *Session, db *data.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
instructors, err := db.GetInstructors()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "HandleInternalLogin: db.GetInstructors(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range instructors {
|
||||||
|
if r.PostFormValue("login") == fmt.Sprint(i.ID) {
|
||||||
|
session := new(Session)
|
||||||
|
session.ID = uuid.New()
|
||||||
|
session.Briefing = new(data.Briefing)
|
||||||
|
session.Briefing.InstructorID = i.ID
|
||||||
|
(*ss) = append((*ss), session)
|
||||||
|
cs <- session
|
||||||
|
|
||||||
|
displayTable(w, db)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template.Must(template.ParseFiles("templates/login.html")).ExecuteTemplate(w, "content", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleSearch(db *data.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
bs, err := db.GetOverviewTableDataByName(r.PostFormValue("search"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "DisplayResults: db.ReadByName(r.PostFormValue()): "+fmt.Sprint(err), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
template.Must(template.ParseFiles("templates/table.html")).ExecuteTemplate(w, "rows", bs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) HandleNewBriefing() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := new(briefingHTMLData)
|
||||||
|
data.SessionID = s.ID
|
||||||
|
template.Must(template.ParseFiles("templates/briefing.html")).ExecuteTemplate(w, "content", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) HandleNewParticipant(cp chan<- *data.Participant) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var err error
|
||||||
|
p := new(data.Participant)
|
||||||
|
p.Login, err = generateLogin()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "AddParticipant: generateLogin(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
s.Participants = append(s.Participants, p)
|
||||||
|
cp <- p
|
||||||
|
|
||||||
|
data := new(briefingHTMLData)
|
||||||
|
data.SessionID = s.ID
|
||||||
|
data.Login = p.Login
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "AddParticipant: generateLogin(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
template.Must(template.ParseFiles("templates/briefing.html")).ExecuteTemplate(w, "new", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) HandleBriefingForm(db *data.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
now := time.Now()
|
||||||
|
var err error
|
||||||
|
|
||||||
|
s.Briefing.Date = now.Format("2006-01-02")
|
||||||
|
s.Briefing.Time = now.Format("15:04:05")
|
||||||
|
s.Briefing.Location = r.PostFormValue("location")
|
||||||
|
s.Briefing.DocumentName = r.PostFormValue("document")
|
||||||
|
s.Briefing.AsOf = r.PostFormValue("as-of")
|
||||||
|
|
||||||
|
err = db.WriteBriefing(s.Briefing)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "SubmitBriefingForm: db.WriteBriefing(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
displayTable(w, db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleExternalLogin(ss *[]*Session) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session, participant, loginCorrect := findCorrectLogin(r.PostFormValue("login"), ss)
|
||||||
|
if loginCorrect {
|
||||||
|
data := new(participantHTMLData)
|
||||||
|
data.SessionID = session.ID
|
||||||
|
data.Login = participant.Login
|
||||||
|
|
||||||
|
template.Must(template.ParseFiles("templates/participant.html")).ExecuteTemplate(w, "content", data)
|
||||||
|
} else {
|
||||||
|
template.Must(template.ParseFiles("templates/login.html")).ExecuteTemplate(w, "content", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) HandleParticipant(p *data.Participant, sq *[]data.Question, db *data.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
p.FirstName = r.PostFormValue("first-" + fmt.Sprint(p.Login))
|
||||||
|
p.LastName = r.PostFormValue("last-" + fmt.Sprint(p.Login))
|
||||||
|
p.Company = r.PostFormValue("company-" + fmt.Sprint(p.Login))
|
||||||
|
|
||||||
|
err := db.WriteParticipant(p)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "DisplayQuestion: db.WriteParticipant(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := new(questionHTMLData)
|
||||||
|
data.SessionID = s.ID
|
||||||
|
data.Login = p.Login
|
||||||
|
data.Question = (*sq)[0]
|
||||||
|
data.QuestionID = 1
|
||||||
|
|
||||||
|
template.Must(template.ParseFiles("templates/question.html")).ExecuteTemplate(w, "content", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) HandleAnswer(db *data.DB, p *data.Participant, sq *[]data.Question, i int64) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println(i, len(*sq))
|
||||||
|
if i < int64(len(*sq)) {
|
||||||
|
if err := handleGivenAnswer(s, p, i-1, r, db); err != nil {
|
||||||
|
http.Error(w, "DisplayQuestion: handleGivenAnswer(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := new(questionHTMLData)
|
||||||
|
data.SessionID = s.ID
|
||||||
|
data.Login = p.Login
|
||||||
|
data.Question = (*sq)[i]
|
||||||
|
data.QuestionID = i + 1
|
||||||
|
|
||||||
|
template.Must(template.ParseFiles("templates/question.html")).ExecuteTemplate(w, "content", data)
|
||||||
|
} else {
|
||||||
|
if err := handleGivenAnswer(s, p, i-1, r, db); err != nil {
|
||||||
|
http.Error(w, "DisplayTestResults: handleGivenAnswer(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
givenAnswers, err := db.GetGivenAnswers(s.Briefing.ID, p.ID, s.Questions)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "DisplayTestResults: db.GetGivenAnswers(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
||||||
|
log.Panicln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := new(resultHTMLData)
|
||||||
|
data.SessionID = s.ID
|
||||||
|
data.Login = p.Login
|
||||||
|
data.Incorrect = 0
|
||||||
|
|
||||||
|
data.Questions = make([]htmlQuestion, 0)
|
||||||
|
for i, q := range s.Questions {
|
||||||
|
question := new(htmlQuestion)
|
||||||
|
question.Text = q.Text
|
||||||
|
|
||||||
|
question.Answers = make([]htmlAnswer, 0)
|
||||||
|
for j, a := range q.Answers {
|
||||||
|
answer := new(htmlAnswer)
|
||||||
|
answer.Text = a.Text
|
||||||
|
|
||||||
|
if j+1 == q.Correct {
|
||||||
|
answer.Correct = true
|
||||||
|
} else {
|
||||||
|
answer.Correct = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if j+1 == givenAnswers[i] {
|
||||||
|
answer.Chosen = true
|
||||||
|
} else {
|
||||||
|
answer.Chosen = false
|
||||||
|
}
|
||||||
|
question.Answers = append(question.Answers, *answer)
|
||||||
|
}
|
||||||
|
data.Questions = append(data.Questions, *question)
|
||||||
|
|
||||||
|
if givenAnswers[i] != q.Correct {
|
||||||
|
data.Incorrect++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template.Must(template.ParseFiles("templates/result.html")).ExecuteTemplate(w, "content", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) HandleRetry(p *data.Participant, sq *[]data.Question) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := new(questionHTMLData)
|
||||||
|
data.SessionID = s.ID
|
||||||
|
data.Login = p.Login
|
||||||
|
data.Question = (*sq)[0]
|
||||||
|
data.QuestionID = 1
|
||||||
|
|
||||||
|
template.Must(template.ParseFiles("templates/question.html")).ExecuteTemplate(w, "content", data)
|
||||||
|
}
|
||||||
|
}
|
66
packages/session/helperFuncs.go
Normal file
66
packages/session/helperFuncs.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"streifling.com/jason/sicherheitsunterweisung/packages/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
func displayTable(w http.ResponseWriter, db *data.DB) {
|
||||||
|
bs, err := db.GetAllOverviewTableData()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "displayTable: *DB.GetAllOverviewTableData(): "+fmt.Sprint(err), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
template.Must(template.ParseFiles("templates/table.html")).ExecuteTemplate(w, "content", bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateLogin() (string, error) {
|
||||||
|
bs := make([]byte, 4)
|
||||||
|
|
||||||
|
if _, err := rand.Read(bs); err != nil {
|
||||||
|
return "", fmt.Errorf("generateLogin: rand.Read(bs): %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(bs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCorrectLogin(l string, ss *[]*Session) (*Session, *data.Participant, bool) {
|
||||||
|
for _, session := range *ss {
|
||||||
|
for _, p := range session.Participants {
|
||||||
|
if l == p.Login {
|
||||||
|
return session, p, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParticipant(l string) (*data.Participant, error) {
|
||||||
|
var err error
|
||||||
|
p := new(data.Participant)
|
||||||
|
|
||||||
|
p.ID, err = strconv.ParseInt(l, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("newParticipant: strconv.Atoi(idString): %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGivenAnswer(s *Session, p *data.Participant, i int64, r *http.Request, db *data.DB) error {
|
||||||
|
answer, err := strconv.Atoi(r.PostFormValue("answer"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("handleGivenAnswer: strconv.Atoi(): %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.WriteGivenAnswer(s.Briefing, p, &s.Questions[i], answer); err != nil {
|
||||||
|
return fmt.Errorf("handleGivenAnswer: db.WriteGivenAnswer(): %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
48
packages/session/htmlStructs.go
Normal file
48
packages/session/htmlStructs.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"streifling.com/jason/sicherheitsunterweisung/packages/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
*data.Briefing
|
||||||
|
Participants []*data.Participant
|
||||||
|
Questions []data.Question
|
||||||
|
}
|
||||||
|
|
||||||
|
type briefingHTMLData struct {
|
||||||
|
SessionID uuid.UUID
|
||||||
|
Login string
|
||||||
|
}
|
||||||
|
|
||||||
|
type participantHTMLData struct {
|
||||||
|
SessionID uuid.UUID
|
||||||
|
Login string
|
||||||
|
}
|
||||||
|
|
||||||
|
type questionHTMLData struct {
|
||||||
|
SessionID uuid.UUID
|
||||||
|
Login string
|
||||||
|
Question data.Question
|
||||||
|
QuestionID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type htmlAnswer struct {
|
||||||
|
Text string
|
||||||
|
Correct bool
|
||||||
|
Chosen bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type htmlQuestion struct {
|
||||||
|
Text string
|
||||||
|
Answers []htmlAnswer
|
||||||
|
}
|
||||||
|
|
||||||
|
type resultHTMLData struct {
|
||||||
|
SessionID uuid.UUID
|
||||||
|
Login string
|
||||||
|
Questions []htmlQuestion
|
||||||
|
Incorrect int
|
||||||
|
}
|
7
static/css/style.css
Normal file
7
static/css/style.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.correct {
|
||||||
|
color: #00ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incorrect {
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
@ -1,38 +1,37 @@
|
|||||||
{{ define "add-button" }}
|
{{ define "add-buttons" }}
|
||||||
<button type="button" hx-post="/add-participant/" hx-target="this" hx-swap="outerHTML">
|
<div id="briefing-buttons">
|
||||||
Neuer Teilnehmer
|
<button type="button" hx-post="/new-participant/{{ .SessionID }}/" hx-target="#briefing-buttons" hx-swap="outerHTML">
|
||||||
</button>
|
Neuer Teilnehmer
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="submit" hx-post="/submit-form/{{ .SessionID }}/" hx-target="#content" hx-swap="innerHTML">
|
||||||
|
Fertig
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ define "new" }}
|
{{ define "new" }}
|
||||||
<span>{{ . }}</span>
|
{{ template "add-buttons" . }}
|
||||||
{{ template "add-button" . }}
|
<p>{{ .Login }}</p>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<form>
|
<form>
|
||||||
<div id="instructor">
|
<div>
|
||||||
<label for="instructor-first-input">Unterweiser Vorname</label>
|
<label for="location">Ort</label>
|
||||||
<input type="text" name="instructor-first" id="instructor-first-input" />
|
<input id="location" name="location" required type="text" />
|
||||||
|
|
||||||
<label for="instructor-last-input">Unterweiser Nachname</label>
|
|
||||||
<input type="text" name="instructor-last" id="instructor-last-input" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="location">
|
<div>
|
||||||
<label for="location-input">Ort</label>
|
<label for="document">Dokument</label>
|
||||||
<input type="text" name="location" id="location-input" />
|
<input id="document" name="document" required type="text" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="state">
|
<div>
|
||||||
<label for="state-input">Stand vom</label>
|
<label for="as-of">Stand vom</label>
|
||||||
<input type="date" name="state" id="state-input" />
|
<input id="as-of" name="as-of" required type="date" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ template "add-button" . }}
|
{{ template "add-buttons" . }}
|
||||||
|
|
||||||
<button type="submit" hx-post="/submit-form/" hx-target="#content" hx-swap="innerHTML">
|
|
||||||
OK
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h2>Login</h2>
|
<h2>Anmeldung</h2>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<label for="login-input">Code</label>
|
<input autocomplete="off" id="login-input" name="login" placeholder="Code" required type="text" />
|
||||||
<input type="text" name="login" id="login-input" />
|
|
||||||
|
|
||||||
<button type="submit" hx-post="/external-login/" hx-target="#content">
|
<div>
|
||||||
Anmelden
|
<button type="submit" hx-post="/internal-login/" hx-target="#content">
|
||||||
</button>
|
Intern
|
||||||
|
</button>
|
||||||
|
|
||||||
<button type="submit" hx-post="/internal-login/" hx-target="#content">
|
<button type="submit" hx-post="/external-login/" hx-target="#content">
|
||||||
Intern
|
Gast
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<form id="participant-{{ . }}">
|
<form>
|
||||||
<label for="participant-first-input-{{ . }}">Vorname</label>
|
<label for="first-{{ .Login }}">Vorname</label>
|
||||||
<input type="text" name="participant-first-{{ . }}" id="participant-first-input-{{ . }}" />
|
<input type="text" name="first-{{ .Login }}" id="first-{{ .Login }}" />
|
||||||
|
|
||||||
<label for="participant-last-input-{{ . }}">Nachname</label>
|
<label for="last-{{ .Login }}">Nachname</label>
|
||||||
<input type="text" name="participant-last-{{ . }}" id="participant-last-input-{{ . }}" />
|
<input type="text" name="last-{{ .Login }}" id="last-{{ .Login }}" />
|
||||||
|
|
||||||
<label for="participant-company-input-{{ . }}">Firma</label>
|
<label for="company-{{ .Login }}">Firma</label>
|
||||||
<input type="text" name="participant-company-{{ . }}" id="participant-company-input-{{ . }}" />
|
<input type="text" name="company-{{ .Login }}" id="company-{{ .Login }}" />
|
||||||
|
|
||||||
<button type="button" hx-post="/display-question-{{ . }}-0/" hx-target="#content">
|
<button type="button" hx-post="/submit-participant/{{ .SessionID }}/{{ .Login }}/" hx-target="#content">
|
||||||
Fertig
|
Fertig
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
{{ define "answers" }}
|
{{define "answers"}}
|
||||||
{{ range .Q.Answers }}
|
{{range .Question.Answers}}
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" name="answer" id="answer-{{ .ID }}" value="{{ .ID }}" />
|
<input type="radio" name="answer" id="answer-{{.ID}}" value="{{.ID}}" />
|
||||||
<label for="answer-{{ .ID }}">{{ .Text }}</label>
|
<label for="answer-{{.ID}}">{{.Text}}</label>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{end}}
|
||||||
{{ end }}
|
{{end}}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{define "content"}}
|
||||||
<h2>Frage {{ .I }}</h2>
|
<h2>Frage {{.QuestionID}}</h2>
|
||||||
<p>{{ .Q.Text }}</p>
|
<p>{{.Question.Text}}</p>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
{{ template "answers" . }}
|
{{template "answers" .}}
|
||||||
|
|
||||||
<button type="submit" hx-post="/display-question-{{ .ID }}-{{ .J }}/" hx-target="#content" hx-swap="innerHTML">
|
<button hx-post="/submit-answer/{{.SessionID}}/{{.Login}}/{{.QuestionID}}/" hx-target="#content" type="submit">
|
||||||
Weiter
|
Weiter
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{{ end }}
|
{{end}}
|
||||||
|
18
templates/result.html
Normal file
18
templates/result.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{{define "answers"}}
|
||||||
|
{{range .Answers}}
|
||||||
|
<p class="{{if and .Chosen .Correct}} correct {{else if and .Chosen (not .Correct)}} incorrect {{end}}">
|
||||||
|
{{.Text}}
|
||||||
|
</p>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "content"}}
|
||||||
|
<p>{{.Incorrect}} Fehler</p>
|
||||||
|
{{range .Questions}}
|
||||||
|
<p>{{.Text}}</p>
|
||||||
|
{{template "answers" .}}
|
||||||
|
{{end}}
|
||||||
|
{{if gt .Incorrect 0}}
|
||||||
|
<button hx-post="/retry/{{.SessionID}}/{{.Login}}/" hx-target="#content" type="submit">Wiederholen</button>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
@ -1,11 +0,0 @@
|
|||||||
{{ define "passed" }}
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ define "failed" }}
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ define "content" }}
|
|
||||||
{{ q := range .Participant.Questions }}
|
|
||||||
<p>{{ . }}{{ q.Text }}</p>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
@ -1,17 +1,16 @@
|
|||||||
{{ define "rows" }}
|
{{ define "rows" }}
|
||||||
{{ range . }}
|
{{ range . }}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ .FirstName }}</td>
|
<td>{{ .InstructorFirstName }}</td>
|
||||||
<td>{{ .LastName }}</td>
|
<td>{{ .InstructorLastName }}</td>
|
||||||
<td>{{ .Date }}</td>
|
<td>{{ .BriefingDate }}</td>
|
||||||
<td>{{ .Time }}</td>
|
<td>{{ .BriefingTime }}</td>
|
||||||
<td>{{ .State }}</td>
|
<td>{{ .BriefingLocation }}</td>
|
||||||
<td>{{ .Location }}</td>
|
<td>{{ .BriefingDocumentName }}</td>
|
||||||
{{ range .Participants }}
|
<td>{{ .BriefingAsOf }}</td>
|
||||||
<td>{{ .FirstName }}</td>
|
<td>{{ .ParticipantFirstName }}</td>
|
||||||
<td>{{ .LastName }}</td>
|
<td>{{ .ParticipantLastName }}</td>
|
||||||
<td>{{ .Company }}</td>
|
<td>{{ .ParticipantCompany }}</td>
|
||||||
{{ end }}
|
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@ -35,8 +34,9 @@
|
|||||||
<th colspan="2">Unterweiser</th>
|
<th colspan="2">Unterweiser</th>
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
<th>Uhrzeit</th>
|
<th>Uhrzeit</th>
|
||||||
<th>Stand</th>
|
|
||||||
<th>Ort</th>
|
<th>Ort</th>
|
||||||
|
<th>Dokument</th>
|
||||||
|
<th>Stand</th>
|
||||||
<th colspan="2">Teilnehmer</th>
|
<th colspan="2">Teilnehmer</th>
|
||||||
<th>Firma</th>
|
<th>Firma</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1 +0,0 @@
|
|||||||
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
|
|
Reference in New Issue
Block a user