diff --git a/cli/main.go b/cli/main.go index 2692f7e..7b826f6 100644 --- a/cli/main.go +++ b/cli/main.go @@ -4,7 +4,7 @@ import ( "log" "os" - "git.andreafazzi.eu/andrea/probo/models" + "git.andreafazzi.eu/andrea/probo/session" "git.andreafazzi.eu/andrea/probo/store/file" "github.com/urfave/cli/v2" ) @@ -36,21 +36,28 @@ func main() { if err != nil { log.Fatalf("An error occurred: %v", err) } - eStore, err := file.NewDefaultExamFileStore() + + session, err := session.NewSession( + "http://localhost:8080/create", + cCtx.Args().First(), + pStore.Storer, + qStore.Storer, + nil, + nil, + ) if err != nil { log.Fatalf("An error occurred: %v", err) } + id, err := session.Push() + if err != nil { + log.Fatalf("An error occurred: %v", err) + } + + log.Printf("Session upload completed with success. URL: https://localhost:8080/%v", id) + for _, p := range pStore.ReadAll() { - e, err := eStore.Create(&models.Exam{ - Name: cCtx.Args().First(), - Participant: p, - Quizzes: qStore.ReadAll(), - }) - if err != nil { - log.Fatalf("An error occurred: %v", err) - } - log.Printf("Created exam %v... in %v", e.ID[:8], eStore.GetPath(e)) + log.Printf("http://localhost:8080/%v/%v", id, p.Token) } return nil diff --git a/go.mod b/go.mod index fef1a2c..2373a0a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.andreafazzi.eu/andrea/probo go 1.21 require ( + github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d github.com/google/uuid v1.3.1 github.com/julienschmidt/httprouter v1.3.0 github.com/sirupsen/logrus v1.8.1 @@ -16,7 +17,6 @@ require ( github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/glebarez/sqlite v1.9.0 // indirect github.com/go-yaml/yaml v2.1.0+incompatible // indirect - github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/kr/pretty v0.2.1 // indirect diff --git a/models/response.go b/models/response.go new file mode 100644 index 0000000..bbdb775 --- /dev/null +++ b/models/response.go @@ -0,0 +1,29 @@ +package models + +import ( + "crypto/sha256" + "encoding/json" + "fmt" +) + +type Response struct { + Meta + QuestionID string + AnswerID string +} + +func (r *Response) String() string { + return fmt.Sprintf("QID: %v, AID:%v", r.QuestionID, r.AnswerID) +} + +func (r *Response) GetHash() string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(r.QuestionID+r.AnswerID))) +} + +func (r *Response) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +func (r *Response) Unmarshal(data []byte) error { + return json.Unmarshal(data, r) +} diff --git a/models/session.go b/models/session.go new file mode 100644 index 0000000..2640e7f --- /dev/null +++ b/models/session.go @@ -0,0 +1 @@ +package models diff --git a/server/main.go b/server/main.go index 9123e9a..4a2ffc2 100644 --- a/server/main.go +++ b/server/main.go @@ -2,7 +2,7 @@ package main import ( "encoding/json" - "log/slog" + "log" "math/rand" "net/http" "os" @@ -10,24 +10,89 @@ import ( "strconv" "strings" "text/template" - "time" "git.andreafazzi.eu/andrea/probo/models" + "git.andreafazzi.eu/andrea/probo/store/file" ) -var Dir = "data" +var ( + DefaultDataDir = "data" + DefaultSessionDir = "sessions" + DefaultTemplateDir = "templates" + DefaultStaticDir = "static" +) + +type Config struct { + SessionDir string + TemplateDir string + StaticDir string +} type ExamSession []*models.Exam -func generateRandomID() string { - id := "" - for i := 0; i < 6; i++ { - id += strconv.Itoa(rand.Intn(9) + 1) - } - return id +type ExamTemplateData struct { + *models.Exam + + SessionID string } -func createExamSessionHandler(w http.ResponseWriter, r *http.Request) { +type Server struct { + config *Config + mux *http.ServeMux + responseStore *file.ResponseFileStore +} + +func GetDefaultSessionDir() string { + return filepath.Join(DefaultDataDir, DefaultSessionDir) +} + +func GetDefaultTemplateDir() string { + return DefaultTemplateDir +} + +func GetDefaultStaticDir() string { + return DefaultStaticDir +} + +func NewServer(config *Config) (*Server, error) { + + _, err := os.Stat(config.SessionDir) + if err != nil { + return nil, err + } + + _, err = os.Stat(config.TemplateDir) + if err != nil { + return nil, err + } + + _, err = os.Stat(config.StaticDir) + if err != nil { + return nil, err + } + + s := &Server{ + config, + http.NewServeMux(), + nil, + } + + s.mux.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir(config.StaticDir)))) + s.mux.HandleFunc("/create", s.createExamSessionHandler) + s.mux.HandleFunc("/", s.getExamHandler) + + return s, nil +} + +func NewDefaultServer() (*Server, error) { + return NewServer(&Config{ + SessionDir: GetDefaultSessionDir(), + TemplateDir: GetDefaultTemplateDir(), + StaticDir: GetDefaultStaticDir(), + }) +} + +func (s *Server) createExamSessionHandler(w http.ResponseWriter, r *http.Request) { var p ExamSession err := json.NewDecoder(r.Body).Decode(&p) if err != nil { @@ -36,7 +101,7 @@ func createExamSessionHandler(w http.ResponseWriter, r *http.Request) { } id := generateRandomID() - path := filepath.Join(Dir, id) + path := filepath.Join(s.config.SessionDir, id) err = os.MkdirAll(path, os.ModePerm) if err != nil { @@ -62,13 +127,13 @@ func createExamSessionHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } -func getExamHandler(w http.ResponseWriter, r *http.Request) { +func (s *Server) getExamHandler(w http.ResponseWriter, r *http.Request) { urlParts := strings.Split(r.URL.Path, "/") examID := urlParts[1] token := urlParts[2] - filePath := filepath.Join(Dir, examID, token+".json") + filePath := filepath.Join(s.config.SessionDir, examID, token+".json") data, err := os.ReadFile(filePath) if err != nil { @@ -83,48 +148,61 @@ func getExamHandler(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "text/html") - tmpl := template.Must(template.New("exam").Parse(` - - -
-Corretto!
")) + return + } + w.Write([]byte("Errato!
")) return } } -func main() { - mux := http.NewServeMux() - mux.HandleFunc("/create", createExamSessionHandler) - mux.HandleFunc("/", getExamHandler) - - slog.Info("Probo server started", "at", time.Now()) - http.ListenAndServe(":8080", mux) +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.mux.ServeHTTP(w, r) +} + +func generateRandomID() string { + id := "" + for i := 0; i < 6; i++ { + id += strconv.Itoa(rand.Intn(9) + 1) + } + return id +} + +func main() { + server, err := NewDefaultServer() + if err != nil { + panic(err) + } + + log.Println("Probo server started.", "Config", server.config) + http.ListenAndServe(":8080", server) } diff --git a/server/server_test.go b/server/server_test.go index a18b276..02a229e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -197,67 +197,81 @@ func TestRunner(t *testing.T) { func (t *serverTestSuite) TestCreate() { - Dir = "testdata" + DefaultDataDir = "testdata" - request, _ := http.NewRequest(http.MethodPost, "/create", strings.NewReader(examPayload)) - response := httptest.NewRecorder() - - handler := http.HandlerFunc(createExamSessionHandler) - - handler.ServeHTTP(response, request) - - t.Equal(http.StatusOK, response.Code) + s, err := NewDefaultServer() + t.Nil(err) if !t.Failed() { - result := map[string]string{} - - err := json.Unmarshal(response.Body.Bytes(), &result) - t.Nil(err) - - path := filepath.Join(Dir, result["id"]) - _, err = os.Stat(path) - defer os.RemoveAll(path) - - files, err := os.ReadDir(path) - t.Nil(err) - - t.Equal(2, len(files)) - - t.Nil(err) - } -} - -func (t *serverTestSuite) TestRead() { - - Dir = "testdata" - - request, _ := http.NewRequest(http.MethodPost, "/create", strings.NewReader(examPayload)) - response := httptest.NewRecorder() - - handler := http.HandlerFunc(createExamSessionHandler) - - handler.ServeHTTP(response, request) - - t.Equal(http.StatusOK, response.Code) - - if !t.Failed() { - result := map[string]string{} - - err := json.Unmarshal(response.Body.Bytes(), &result) - t.Nil(err) - - path := filepath.Join(Dir, result["id"]) - _, err = os.Stat(path) - defer os.RemoveAll(path) - - request, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/%s/%s", result["id"], "111222"), nil) + request, _ := http.NewRequest(http.MethodPost, "/create", strings.NewReader(examPayload)) response := httptest.NewRecorder() - handler := http.HandlerFunc(getExamHandler) + handler := http.HandlerFunc(s.createExamSessionHandler) handler.ServeHTTP(response, request) t.Equal(http.StatusOK, response.Code) + if !t.Failed() { + result := map[string]string{} + + err := json.Unmarshal(response.Body.Bytes(), &result) + t.Nil(err) + + path := filepath.Join(GetDefaultSessionDir(), result["id"]) + _, err = os.Stat(path) + defer os.RemoveAll(path) + + files, err := os.ReadDir(path) + t.Nil(err) + + t.Equal(2, len(files)) + + t.Nil(err) + } + } +} + +func (t *serverTestSuite) TestRead() { + + DefaultDataDir = "testdata" + + s, err := NewDefaultServer() + t.Nil(err) + + if !t.Failed() { + request, _ := http.NewRequest(http.MethodPost, "/create", strings.NewReader(examPayload)) + response := httptest.NewRecorder() + + handler := http.HandlerFunc(s.createExamSessionHandler) + + handler.ServeHTTP(response, request) + + t.Equal(http.StatusOK, response.Code) + + if !t.Failed() { + result := map[string]string{} + + err := json.Unmarshal(response.Body.Bytes(), &result) + t.Nil(err) + + path := filepath.Join(GetDefaultSessionDir(), result["id"]) + _, err = os.Stat(path) + t.Nil(err) + + if !t.Failed() { + defer os.RemoveAll(path) + + request, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/%s/%s", result["id"], "111222"), nil) + response := httptest.NewRecorder() + + handler := http.HandlerFunc(s.getExamHandler) + + handler.ServeHTTP(response, request) + + t.Equal(http.StatusOK, response.Code) + + } + } } } diff --git a/server/static/css/neat.css b/server/static/css/neat.css new file mode 100644 index 0000000..ed85173 --- /dev/null +++ b/server/static/css/neat.css @@ -0,0 +1,144 @@ +:root { + color-scheme: light dark; + --light: #fff; + --lesslight: #efefef; + --dark: #404040; + --moredark: #000; + border-top: 5px solid var(--dark); + line-height: 1.5em; /* This causes wrapping h1's to collapse too small */ + font-family: sans-serif; + font-size: 16px; +} + +* { + box-sizing: border-box; + color: var(--dark); +} + +button, input { + font-size: 1em; /* Override browser default font shrinking*/ +} + +input { + border: 1px solid var(--dark); + background-color: var(--lesslight); + border-radius: .25em; + padding: .5em; +} + +pre { + background-color: var(--lesslight); + margin: 0.5em 0 0.5em 0; + padding: 0.5em; + overflow: auto; +} + +code { + background-color: var(--lesslight); +} + +body { + background-color: var(--light); + margin: 0; + max-width: 800px; + padding: 0 20px 20px 20px; + margin-left: auto; + margin-right: auto; +} + +img { + max-width: 100%; + height: auto; +} + +button, .button, input[type=submit] { + display: inline-block; + background-color: var(--dark); + color: var(--light); + text-align: center; + padding: .5em; + border-radius: .25em; + text-decoration: none; + border: none; + cursor: pointer; +} + +button:hover, .button:hover, input[type=submit]:hover { + color: var(--lesslight); + background-color: var(--moredark); +} + +/* Add a margin between side-by-side buttons */ +button + button, .button + .button, input[type=submit] + input[type=submit] { + margin-left: 1em; +} + +.center { + display: block; + margin-left: auto; + margin-right: auto; + text-align: center; +} + +.bordered { + border: 3px solid; +} + +.home { + display: inline-block; + background-color: var(--dark); + color: var(--light); + margin-top: 20px; + padding: 5px 10px 5px 10px; + text-decoration: none; + font-weight: bold; +} + + +/* Desktop sizes */ +@media only screen and (min-width: 600px) { + ol.twocol { + column-count: 2; + } + + .row { + display: flex; + flex-direction: row; + padding: 0; + width: 100%; + } + + /* Make everything in a row a column */ + .row > * { + display: block; + flex: 1 1 auto; + max-width: 100%; + width: 100%; + } + + .row > *:not(:last-child) { + margin-right: 10px; + } +} + +/* Dark mode overrides (confusingly inverse) */ +@media (prefers-color-scheme: dark) { + :root { + --light: #222; + --lesslight: #333; + --dark: #eee; + --moredark: #fefefe; + } + /* This fixes an odd blue then white shadow on FF in dark mode */ + *:focus { + outline: var(--light); + box-shadow: 0 0 0 .25em royalblue; + } +} + +/* Printing */ +@media print { + .home { + display: none; + } +} diff --git a/server/templates/exam.tpl b/server/templates/exam.tpl new file mode 100644 index 0000000..aa6071e --- /dev/null +++ b/server/templates/exam.tpl @@ -0,0 +1,27 @@ + + + + + +