diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..ad439e1 --- /dev/null +++ b/.air.toml @@ -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 diff --git a/.gitignore b/.gitignore index c8a398a..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +0,0 @@ -create_tables.sql diff --git a/create_tables.sql b/create_tables.sql new file mode 100644 index 0000000..92fabeb --- /dev/null +++ b/create_tables.sql @@ -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', '300484'), + ('Tim', 'Matzuga', '300483'), + ('Georg', 'Bränzel', '300485'); + +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 + ( 'Jan', 'Schiele', 'Körber' ), + ( 'Murat', 'Zorlu', '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' ); diff --git a/main.go b/main.go index 3d98fdb..ca55fdb 100644 --- a/main.go +++ b/main.go @@ -1,23 +1,33 @@ package main import ( + "fmt" "html/template" "log" "net/http" + "streifling.com/jason/sicherheitsunterweisung/packages/data" "streifling.com/jason/sicherheitsunterweisung/packages/server" + "streifling.com/jason/sicherheitsunterweisung/packages/types" ) -func addUUIDs(uc *chan string, uuids *[]string) { - for uuid := range *uc { - (*uuids) = append(*uuids, uuid) +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) + + 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 - uuids := make([]string, 0) - i, j = 1, 1 + logins := make([]string, 0) + participants := make([]*types.Participant, 0) + briefings := make([]*types.Briefing, 0) mux := http.NewServeMux() db, err := data.OpenDB("sicherheitsunterweisung") @@ -25,21 +35,26 @@ func main() { log.Fatalln(err) } - uuidChan := make(chan string) - defer close(uuidChan) - go addUUIDs(&uuidChan, &uuids) + 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", "templates/login.html")).Execute(w, nil) }) - mux.HandleFunc("/search/", server.DisplayResults(db)) + mux.HandleFunc("/search/", server.DisplaySearchResults(db)) mux.HandleFunc("/new-briefing/", server.DisplayForm(&i)) - mux.HandleFunc("/add-participant/", server.AddParticipant(&i)) + mux.HandleFunc("/add-participant/", server.AddParticipant(&i, &logins)) mux.HandleFunc("/submit-form/", server.SubmitForm(db, &i, &j)) - mux.HandleFunc("/generate-uuid/", server.GenerateUUID(uuidChan)) mux.HandleFunc("/internal-login/", server.DisplayTable(db)) - mux.HandleFunc("/external-login/", server.DisplayQuestionsIfOK(&uuids)) + mux.HandleFunc("/external-login/", server.DisplayParticipantForm(&logins, participantChan)) log.Fatalln(http.ListenAndServe(":8080", mux)) } diff --git a/packages/data/db.go b/packages/data/db.go index 9f56b53..ea22a9c 100644 --- a/packages/data/db.go +++ b/packages/data/db.go @@ -19,27 +19,50 @@ type DB struct { Name string } -func getCredentials() (string, string, error) { - fmt.Printf("DB Benutzer: ") - user, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err != nil { - return "", "", fmt.Errorf("getCredentials: bufio.NewReader(os.Stdin).ReadString('\n'): %v", err) +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) + } } - - 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 := string(bytePass) - - return strings.TrimSpace(user), strings.TrimSpace(pass), nil + return strings.TrimSpace(user), 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 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] } } @@ -92,8 +115,8 @@ func (db *DB) WriteBriefing(b *types.Briefing) error { return nil } -func (db *DB) ReadAll() (*[]types.Briefing, error) { - bs := make([]types.Briefing, 0) +func (db *DB) ReadAll() ([]*types.Briefing, error) { + bs := make([]*types.Briefing, 0) rows, err := db.Query("SELECT *" + " FROM " + db.Name) if err != nil { @@ -110,16 +133,16 @@ func (db *DB) ReadAll() (*[]types.Briefing, error) { return nil, fmt.Errorf("*DB.ReadAll: db.Query(): %v\n", err) } - b.Participants = append(b.Participants, *p) - bs = append(bs, *b) + b.Participants = append(b.Participants, p) + bs = append(bs, b) } - reverseOrder(&bs) - return &bs, nil + reverseOrder(bs) + return bs, nil } -func (db *DB) ReadByName(name string) (*[]types.Briefing, error) { - bs := make([]types.Briefing, 0) +func (db *DB) ReadByName(name string) ([]*types.Briefing, error) { + bs := make([]*types.Briefing, 0) rows, err := db.Query("SELECT *"+ " FROM "+db.Name+ @@ -148,10 +171,22 @@ func (db *DB) ReadByName(name string) (*[]types.Briefing, error) { " &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) + b.Participants = append(b.Participants, p) + bs = append(bs, b) } - reverseOrder(&bs) - return &bs, nil + 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 } diff --git a/packages/data/questionaires.go b/packages/data/questionaires.go index 0ad59c2..5f1baee 100644 --- a/packages/data/questionaires.go +++ b/packages/data/questionaires.go @@ -1 +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 +} diff --git a/packages/server/server.go b/packages/server/server.go index ca44e63..e94937e 100644 --- a/packages/server/server.go +++ b/packages/server/server.go @@ -7,21 +7,31 @@ import ( "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/index.html", "templates/table.html")).Execute(w, bs) + template.Must(template.ParseFiles("templates/table.html")).ExecuteTemplate(w, "content", bs) } } -func DisplayResults(db *data.DB) http.HandlerFunc { +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 { @@ -33,14 +43,27 @@ func DisplayResults(db *data.DB) http.HandlerFunc { func DisplayForm(i *int64) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - template.Must(template.ParseFiles("templates/form.html")).ExecuteTemplate(w, "content", i) + template.Must(template.ParseFiles("templates/briefing.html")).ExecuteTemplate(w, "content", i) } } -func AddParticipant(i *int64) http.HandlerFunc { +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++ - template.Must(template.ParseFiles("templates/form.html")).ExecuteTemplate(w, "participant", i) + login := fmt.Sprintf("%d", *i) + "-" + generateUUID() + (*ls) = append(*ls, login) + template.Must(template.ParseFiles("templates/briefing.html")).ExecuteTemplate(w, "new", login) } } @@ -56,7 +79,7 @@ func SubmitForm(db *data.DB, i, j *int64) http.HandlerFunc { b.Location = r.PostFormValue("location") for ; *j <= *i; *j++ { - b.Participants = append(b.Participants, types.Participant{ + b.Participants = append(b.Participants, &types.Participant{ ID: *j, Person: types.Person{ FirstName: r.PostFormValue("participant-first-" + fmt.Sprint(*j)), @@ -77,38 +100,107 @@ func SubmitForm(db *data.DB, i, j *int64) http.HandlerFunc { } } -func GenerateUUID(ch chan<- string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - bs := make([]byte, 4) - - _, err := rand.Read(bs) - if err != nil { - _ = fmt.Errorf("GenerateUUID: rand.Read(bs): %v\n", err) +// 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 } - - uuid := hex.EncodeToString(bs) - ch <- uuid - - template.Must(template.ParseFiles("templates/form.html")).ExecuteTemplate(w, "uuid", uuid) } + return false } -func DisplayQuestionsIfOK(uuids *[]string) http.HandlerFunc { +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) { - if uuidIsOK(r.PostFormValue("login"), uuids) { - template.Must(template.ParseFiles("templates/question.html")).ExecuteTemplate(w, "content", nil) + 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) } } } -// TODO: Delete uuid from uuids -func uuidIsOK(uuid string, uuids *[]string) bool { - for _, u := range *uuids { - if uuid == u { - return true - } +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) } - return false } diff --git a/packages/types/types.go b/packages/types/types.go index 63492c9..8c1bb5d 100644 --- a/packages/types/types.go +++ b/packages/types/types.go @@ -7,28 +7,23 @@ type Person struct { type Instructor Person -type QAElement struct { +type Answer struct { ID int Text string } -type Answer QAElement - type Question struct { - QAElement + Text string Answers []Answer -} - -type Questionaire struct { - UUID string - Questions []Question + Chosen int + Correct int } type Participant struct { ID int64 Person - Company string - QuestionaireUUID string + Company string + Questions []Question } type Briefing struct { @@ -37,5 +32,5 @@ type Briefing struct { Time string State string Location string - Participants []Participant + Participants []*Participant } diff --git a/templates/briefing.html b/templates/briefing.html new file mode 100644 index 0000000..ec3e343 --- /dev/null +++ b/templates/briefing.html @@ -0,0 +1,38 @@ +{{ define "add-button" }} + +{{ end }} + +{{ define "new" }} +{{ . }} +{{ template "add-button" . }} +{{ end }} + +{{ define "content" }} +
+{{ end }} diff --git a/templates/form.html b/templates/form.html deleted file mode 100644 index 55dfb54..0000000 --- a/templates/form.html +++ /dev/null @@ -1,64 +0,0 @@ -{{ define "uuid" }} -{{ . }} -{{ end }} - -{{ define "participant" }} -{{ .Question.Text }}
+{{ .Q.Text }}
-{{ template "answers" . }} + +{{ end }} diff --git a/templates/results.html b/templates/results.html new file mode 100644 index 0000000..a275c3b --- /dev/null +++ b/templates/results.html @@ -0,0 +1,11 @@ +{{ define "passed" }} +{{ end }} + +{{ define "failed" }} +{{ end }} + +{{ define "content" }} +{{ q := range .Participant.Questions }} +{{ . }}{{ q.Text }}
+{{ end }} +{{ end }} diff --git a/tmp/build-errors.log b/tmp/build-errors.log new file mode 100644 index 0000000..c215497 --- /dev/null +++ b/tmp/build-errors.log @@ -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 \ No newline at end of file diff --git a/tmp/main b/tmp/main new file mode 100755 index 0000000..c450226 Binary files /dev/null and b/tmp/main differ