Compare commits

7 Commits

Author SHA1 Message Date
Bel LaPointe
aacc5d2fc6 img clues in http 2023-04-07 13:53:11 -06:00
Bel LaPointe
08abc8b48f run gui 2023-04-07 13:47:33 -06:00
Bel LaPointe
c960de86f3 http writable 2023-04-07 13:46:29 -06:00
Bel LaPointe
7a464c2f09 test http POST api/questions/QID/answers 2023-04-07 13:44:42 -06:00
Bel LaPointe
c76da12b1a gui done enough 2023-04-07 13:33:09 -06:00
Bel LaPointe
ef25a77254 func q redo 2023-04-07 13:25:40 -06:00
Bel LaPointe
80d987b69e ok ui can rotate questions 2023-04-07 13:22:13 -06:00
4 changed files with 205 additions and 20 deletions

62
http.go
View File

@@ -7,7 +7,10 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"os/signal"
"path"
"strings" "strings"
"syscall"
"time" "time"
) )
@@ -17,17 +20,29 @@ type Context struct {
} }
func HTTP(port int, db DB) error { func HTTP(port int, db DB) error {
ctx, can := signal.NotifyContext(context.Background(), syscall.SIGINT)
defer can()
foo := func(w http.ResponseWriter, r *http.Request) { foo := func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { if r.URL.Path == "/" {
case "/": httpGUI(w, r)
httpRoot(w, r) } else if strings.HasPrefix(r.URL.Path, "/static/") {
default: httpStatic(w, r)
} else if strings.HasPrefix(r.URL.Path, "/api/questions") && strings.HasSuffix(r.URL.Path, "/answers") && r.Method == http.MethodPost {
httpPostQuestionAnswers(w, r)
} else {
http.NotFound(w, r) http.NotFound(w, r)
} }
} }
foo = withAuth(foo) foo = withAuth(foo)
foo = withDB(foo, db) foo = withDB(foo, db)
return http.ListenAndServe(fmt.Sprintf(":%d", port), http.HandlerFunc(foo)) foo = withCtx(foo, ctx)
go func() {
http.ListenAndServe(fmt.Sprintf(":%d", port), http.HandlerFunc(foo))
}()
<-ctx.Done()
return nil
} }
func extract(ctx context.Context) Context { func extract(ctx context.Context) Context {
@@ -40,6 +55,13 @@ func inject(ctx context.Context, v Context) context.Context {
return context.WithValue(ctx, "__context", v) return context.WithValue(ctx, "__context", v)
} }
func withCtx(foo http.HandlerFunc, ctx context.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(ctx)
foo(w, r)
}
}
func withDB(foo http.HandlerFunc, db DB) http.HandlerFunc { func withDB(foo http.HandlerFunc, db DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
c := extract(r.Context()) c := extract(r.Context())
@@ -65,10 +87,10 @@ func withAuth(foo http.HandlerFunc) http.HandlerFunc {
} }
//go:embed public/root.html //go:embed public/root.html
var httpRootHTML string var httpGUIHTML string
func httpRoot(w http.ResponseWriter, r *http.Request) { func httpGUI(w http.ResponseWriter, r *http.Request) {
body := httpRootHTML body := httpGUIHTML
if os.Getenv("DEBUG") != "" { if os.Getenv("DEBUG") != "" {
b, _ := os.ReadFile("public/root.html") b, _ := os.ReadFile("public/root.html")
body = string(b) body = string(b)
@@ -85,6 +107,13 @@ func httpRoot(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(body)) w.Write([]byte(body))
} }
func httpStatic(w http.ResponseWriter, r *http.Request) {
p := strings.TrimPrefix(r.URL.Path, "/static/")
p = strings.ReplaceAll(p, "/..", "/")
p = path.Clean(p)
http.ServeFile(w, r, p)
}
func httpAssignments(ctx context.Context) (interface{}, error) { func httpAssignments(ctx context.Context) (interface{}, error) {
db := extract(ctx).DB db := extract(ctx).DB
user := extract(ctx).User user := extract(ctx).User
@@ -97,3 +126,20 @@ func httpAssignments(ctx context.Context) (interface{}, error) {
} }
return todo, nil return todo, nil
} }
func httpPostQuestionAnswers(w http.ResponseWriter, r *http.Request) {
idq := IDQ(path.Base(strings.Split(r.URL.Path, "/answers")[0]))
var payload struct {
Answer string `json:"answer"`
Passed bool `json:"passed"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx := extract(r.Context())
if err := ctx.DB.PushAnswer(ctx.User, idq, Renderable(payload.Answer), payload.Passed); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

50
http_test.go Normal file
View File

@@ -0,0 +1,50 @@
package main
import (
"net/http"
"net/http/httptest"
"os"
"path"
"strings"
"testing"
"time"
)
func TestHTTPPostQuestionAnswers(t *testing.T) {
p := path.Join(t.TempDir(), "db.yaml")
os.WriteFile(p, []byte("{}"), os.ModePerm)
db, err := newYamlDB(p)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
r := httptest.NewRequest(
http.MethodPost,
"/api/questions/QID/answers",
strings.NewReader(`{"answer":"a", "passed": false}`),
)
r.SetBasicAuth("u", "")
withAuth(withDB(httpPostQuestionAnswers, db))(w, r)
if w.Code != http.StatusOK {
t.Error(w.Code)
}
_, got := db.LastAnswer("u", "QID")
if got == (Answer{}) {
t.Error("no answer pushed:", got)
}
if got.Q != "QID" {
t.Error(got.Q)
} else if got.A != "a" {
t.Error(got.A)
} else if time.Since(time.Unix(0, got.TS)) > time.Minute {
t.Error(got.TS)
} else if got.Author != "u" {
t.Error(got.Author)
} else if got.Pass != false {
t.Error(got.Pass)
}
}

View File

@@ -16,15 +16,16 @@
</header> </header>
<body> <body>
<!--{{USER}}--> <!--{{USER}}-->
<form id="flash" action="" onsubmit="return false; trySolve(this.children.idq.value, this.children.answer.value); return false;"> <form id="flash" action="">
<input type="text" name="idq" readonly=true value="" style="display: none;"> <input type="text" name="idq" readonly=true value="" style="display: none;">
<div name="question"></div> <div name="question"></div>
<div name="clues"></div> <div name="clues"></div>
<div name="solution"></div> <div name="solution"></div>
<input type="button" value="clue"> <input type="button" value="clue">
<input type="text" name="answer"> <input type="text" name="answer">
<input type="submit" value="submit"> <input type="button" value="pass" onclick="passQuestion(this.parentElement)">
<input type="button" value="skip" onclick="nextQuestion(this.parentElement)"> <input type="button" value="fail" onclick="failQuestion(this.parentElement)">
<input type="button" value="skip" onclick="skipQuestion(this.parentElement)">
</form> </form>
</body> </body>
<footer> <footer>
@@ -35,10 +36,54 @@
map((key) => [ map((key) => [
[key, knowledgebase[key]] [key, knowledgebase[key]]
]) ])
console.log(0, knowledgebase)
function passQuestion(form) {
if (!form.children.answer.retake) {
pushAnswer(form.children.idq.value, form.children.answer.value, true)
}
nextQuestion(form)
}
function failQuestion(form) {
if (!form.children.answer.retake) {
pushAnswer(form.children.idq.value, form.children.answer.value, false)
}
queueRedoQuestion(form)
nextQuestion(form)
}
function skipQuestion(form) {
queueRedoQuestion(form)
nextQuestion(form)
}
function queueRedoQuestion(form) {
knowledgebase.push([[
form.children.idq.value,
{
Q: form.children.question.value,
Clues: form.children.clues.value,
Solution: form.children.solution.value,
Retake: true,
},
]])
}
function pushAnswer(idq, answer, passed) {
http("post", `/api/questions/${idq}/answers`, noopcallback,
JSON.stringify(
{
"answer": answer,
"passed": new Boolean(passed),
}
),
)
}
function nextQuestion(form) { function nextQuestion(form) {
form.children.answer.value = "" form.children.answer.value = ""
let todo = knowledgebase.pop() let todo = knowledgebase.shift()
if (!todo) { if (!todo) {
todo = [0] todo = [0]
} }
@@ -50,12 +95,20 @@
form.children.idq.value = todo[0] form.children.idq.value = todo[0]
form.children.question.innerHTML = `<h3>${todo[1].Q}</h3>` form.children.question.innerHTML = `<h3>${todo[1].Q}</h3>`
form.children.question.value = todo[1].Q
let clues = "" let clues = ""
for (var i in todo[1].Clues) { for (var i in todo[1].Clues) {
clues += `<details><summary>clue #${i}</summary>${todo[1].Clues[i]}</details>` clues += `<details><summary>clue #${i}</summary>`
if (todo[1].Clues[i].startsWith("img:")) {
clues += `<img src="static/${todo[1].Clues[i].slice(4, 1000)}"/>`
} else {
clues += todo[1].Clues[i]
}
clues += `</details>`
} }
form.children.clues.innerHTML = clues form.children.clues.innerHTML = clues
form.children.clues.value = todo[1].Clues
let solution = "" let solution = ""
for (var i in todo[1].Solution) { for (var i in todo[1].Solution) {
@@ -65,6 +118,26 @@
solution = `<details><summary>solution</summary>${solution}</details>` solution = `<details><summary>solution</summary>${solution}</details>`
} }
form.children.solution.innerHTML = solution form.children.solution.innerHTML = solution
form.children.solution.value = todo[1].Solution
form.children.answer.retake = todo[1].Retake
}
function noopcallback(responseBody, responseStatus) {
console.log(responseStatus, responseBody)
}
function http(method, remote, callback, body) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function() {
if (xmlhttp.readyState == XMLHttpRequest.DONE) {
callback(xmlhttp.responseText, xmlhttp.status)
}
};
xmlhttp.open(method, remote, true);
if (typeof body == "undefined") {
body = null
}
xmlhttp.send(body);
} }
nextQuestion(document.getElementById("flash")) nextQuestion(document.getElementById("flash"))

30
testdata/sample.yaml vendored Normal file → Executable file
View File

@@ -2,39 +2,55 @@ knowledge:
questions: questions:
uuid10: uuid10:
q: whats a fieldset q: whats a fieldset
clues:
- clue1 of 2
- clue2 of 2
solution: solution:
- solution A - solution A
- solution B - solution B
clues:
- clue1 of 2
- img:testdata/tofugu.d/sa-hiragana-0.png
tags: tags:
- ops - ops
- data-platform - data-platform
uuid11: uuid11:
q: whats a responseset q: whats a responseset
clues:
- clue1 of 2
- clue2 of 2
solution: solution:
- solution A - solution A
- solution B - solution B
clues:
- clue1 of 2
- clue2 of 2
tags: tags:
- ops - ops
- data-platform - data-platform
answers: answers:
4ad47d8f-2e9b-46fe-8a12-67db85efc6f8:
q: uuid11
a: ""
ts: 1680896808930227000
author: breel
pass: false
36648846-4e14-4518-b434-5d4ff3b0127b:
q: uuid10
a: ""
ts: 1680896805896000000
author: breel
pass: false
uuid20: uuid20:
q: uuid10 q: uuid10
a: a schema a: a schema
ts: 123 ts: 123
author: breel author: breel
pass: false
uuid21: uuid21:
q: uuid10 q: uuid10
a: not a schema a: not a schema
ts: 122 ts: 122
author: breel author: breel
pass: false
users: users:
breel: breel:
tags: {assignments: [ops]} tags:
assignments:
- ops
cadence: 5s cadence: 5s
resolution: 1 resolution: 1