diff --git a/atsebayt/src/components/QuestionForm.svelte b/atsebayt/src/components/QuestionForm.svelte index 71b9449..27d7144 100644 --- a/atsebayt/src/components/QuestionForm.svelte +++ b/atsebayt/src/components/QuestionForm.svelte @@ -52,6 +52,7 @@ {/if} {/if} +
{#if false && response_history}
@@ -88,6 +89,7 @@ {proposals} readonly bind:value={value} + on:change={() => { dispatch("change"); }} /> {/await} {/if} @@ -103,17 +105,30 @@ {proposals} {readonly} bind:value={value} + on:change={() => { dispatch("change"); }} /> {/await} {:else if readonly}

{value}

{:else if question.kind == 'int'} - + { dispatch("change"); }} + > {:else} - + {/if} - {#if survey.corrected} + {#if survey && survey.corrected} - export let proposal = null; - export let kind = 'mcq'; - export let value; - - let inputType = 'checkbox'; - $: { - switch(kind) { - case 'mcq': - inputType = 'checkbox'; - break; - default: - inputType = 'radio'; - } - } - - - + import { createEventDispatcher } from 'svelte'; + import { QuestionProposal } from '../lib/questions'; export let edit = false; @@ -16,6 +18,8 @@ } } + const dispatch = createEventDispatcher(); + function addProposal() { const p = new QuestionProposal(); p.id_question = id_question; @@ -35,7 +39,7 @@ id={prefixid + 'p' + proposal.id} bind:group={valueCheck} value={proposal.id.toString()} - on:change={() => { value = valueCheck.join(',')}} + on:change={() => { value = valueCheck.join(','); dispatch("change"); }} > {:else} { dispatch("change"); }} > {/if} {#if edit} diff --git a/atsebayt/src/components/SurveyAdmin.svelte b/atsebayt/src/components/SurveyAdmin.svelte index dc28e7d..0ab89cc 100644 --- a/atsebayt/src/components/SurveyAdmin.svelte +++ b/atsebayt/src/components/SurveyAdmin.svelte @@ -2,6 +2,8 @@ import { createEventDispatcher } from 'svelte'; import { goto } from '$app/navigation'; + import { getQuestions } from '../lib/questions'; + const dispatch = createEventDispatcher(); export let survey = null; @@ -60,6 +62,23 @@
+
+
+ +
+
+ {#await getQuestions(survey.id) then questions} + + {/await} +
+
+
diff --git a/atsebayt/src/components/SurveyBadge.svelte b/atsebayt/src/components/SurveyBadge.svelte index 4b28e0d..2d87b5a 100644 --- a/atsebayt/src/components/SurveyBadge.svelte +++ b/atsebayt/src/components/SurveyBadge.svelte @@ -4,7 +4,8 @@ export { className as class }; -{#if survey.startAvailability() > Date.now()}Prévu> +{#if survey.direct}Direct +{:else if survey.startAvailability() > Date.now()}Prévu {:else if survey.endAvailability() > Date.now()}En cours {:else if !survey.corrected}Terminé {:else}Corrigé diff --git a/atsebayt/src/components/SurveyList.svelte b/atsebayt/src/components/SurveyList.svelte index 807f079..cd16519 100644 --- a/atsebayt/src/components/SurveyList.svelte +++ b/atsebayt/src/components/SurveyList.svelte @@ -6,6 +6,17 @@ import SurveyBadge from '../components/SurveyBadge.svelte'; import { getSurveys } from '../lib/surveys'; import { getScore } from '../lib/users'; + + let req_surveys = getSurveys(); + export let direct = null; + + req_surveys.then((surveys) => { + for (const survey of surveys) { + if (survey.direct) { + direct = survey; + } + } + }); @@ -18,7 +29,7 @@ {/if} - {#await getSurveys()} + {#await req_surveys} {/if} - goto($user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}> + goto(survey.direct?`surveys/${survey.id}/live`:$user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}>
@@ -36,7 +47,7 @@
{survey.title} diff --git a/atsebayt/src/lib/surveys.js b/atsebayt/src/lib/surveys.js index 1fe9040..9a9fc62 100644 --- a/atsebayt/src/lib/surveys.js +++ b/atsebayt/src/lib/surveys.js @@ -8,12 +8,13 @@ class Survey { } } - update({ id, title, promo, group, shown, corrected, start_availability, end_availability }) { + update({ id, title, promo, group, shown, direct, corrected, start_availability, end_availability }) { this.id = id; this.title = title; this.promo = promo; this.group = group; this.shown = shown; + this.direct = direct; this.corrected = corrected; if (this.start_availability != start_availability) { this.start_availability = start_availability; diff --git a/atsebayt/src/routes/index.svelte b/atsebayt/src/routes/index.svelte index e1e991d..270af16 100644 --- a/atsebayt/src/routes/index.svelte +++ b/atsebayt/src/routes/index.svelte @@ -2,6 +2,8 @@ import { user } from '../stores/user'; import SurveyList from '../components/SurveyList.svelte'; import ValidateSubmissions from '../components/ValidateSubmissions.svelte'; + + let direct = null;
@@ -20,6 +22,12 @@
{/if} + {#if direct} + + {/if} +

Tu as fait les rendus suivants :

@@ -38,6 +46,6 @@ Vous devez vous identifier pour accéder au contenu.

{/if} - +
diff --git a/atsebayt/src/routes/surveys/[sid]/admin.svelte b/atsebayt/src/routes/surveys/[sid]/admin.svelte new file mode 100644 index 0000000..3abb015 --- /dev/null +++ b/atsebayt/src/routes/surveys/[sid]/admin.svelte @@ -0,0 +1,370 @@ + + + + +{#await surveyP then survey} + {#if $user && $user.is_admin} + {#if survey.direct !== null} + + + {/if} + + {/if} +
+

+ < + {survey.title} + + Administration + +

+ {#if survey.direct !== null} +
+ {#if ws_up}Connecté{:else}Déconnecté{/if} +
+ {:else} + + {/if} +
+ + {#if survey.direct === null} + + {:else} + {#await req_questions} +
+
+ Chargement des questions … +
+ {:then questions} +
+ + + + + + + + + + {#each questions as question (question.id)} + + + + + + {/each} + +
+ Question + + + Réponses + + Actions + +
+ {#if responses[question.id]} + + {question.title} + + {:else} + {question.title} + {/if} + + {#if responses[question.id]} + {Object.keys(responses[question.id]).length} + {:else} + 0 + {/if} + {#if wsstats}/ {wsstats.nb_clients}{/if} + + +
+
+ {/await} + {/if} + +
+ +

+ Réponses +

+ {#if Object.keys(responses).length} + {#each Object.keys(responses) as q, qid (qid)} + {#await req_questions then questions} + {#each questions as question} + {#if question.id == q} +

+ {question.title} +

+ {#if question.kind == 'ucq'} + {#await question.getProposals()} +
+
+ Chargement des propositions … +
+ {:then proposals} +
+ + + {#each proposals as proposal (proposal.id)} + + + + + + {/each} + +
+ {proposal.label} + + {responsesbyid[q].filter((e) => e == proposal.id.toString()).length}/{responsesbyid[q].length} + + {Math.trunc(responsesbyid[q].filter((e) => e == proposal.id.toString()).length / responsesbyid[q].length * 1000)/10} % +
+
+ {/await} + {:else if question.kind == 'mcq'} + {#await question.getProposals()} +
+
+ Chargement des propositions … +
+ {:then proposals} +
+ + + {#each proposals as proposal (proposal.id)} + + + + + + {/each} + +
+ {proposal.label} + + {responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length}/{responsesbyid[q].length} + + {Math.trunc(responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length / responsesbyid[q].length * 1000)/10} % +
+
+ {/await} + {:else} +
+ +
+ {/if} + {/if} + {/each} + {/await} + {/each} + {/if} + +
+ + +

+ Connectés + {#if wsstats} + {wsstats.nb_clients} utilisateurs + {/if} +

+ {#if wsstats} +
+ {#each wsstats.users as login, lid (lid)} +
+
+ {login} + +
+
+ {/each} +
+ {/if} + +{/await} diff --git a/atsebayt/src/routes/surveys/[sid]/index.svelte b/atsebayt/src/routes/surveys/[sid]/index.svelte index 02899f4..8df6da5 100644 --- a/atsebayt/src/routes/surveys/[sid]/index.svelte +++ b/atsebayt/src/routes/surveys/[sid]/index.svelte @@ -11,6 +11,8 @@ {#await surveyP then survey} {#if $user && $user.is_admin} - - + + + {#if survey.direct} + + {/if} {/if}

diff --git a/atsebayt/src/routes/surveys/[sid]/live.svelte b/atsebayt/src/routes/surveys/[sid]/live.svelte new file mode 100644 index 0000000..1aa027f --- /dev/null +++ b/atsebayt/src/routes/surveys/[sid]/live.svelte @@ -0,0 +1,118 @@ + + + + +{#await surveyP then survey} + {#if $user && $user.is_admin} + + + {/if} +
+

+ < + {survey.title} +

+
+ {#if ws_up}Connecté{:else}Déconnecté{/if} +
+
+ +
+ {#if show_question} + {#await getQuestion(show_question)} + Please wait + {:then question} + + + + {/await} + {:else if ws_up} +

+ Pas de question actuellement +

+ {:else} +

+ La session est terminée. On se retrouve une prochaine fois… +

+ {/if} +
+{/await} diff --git a/db.go b/db.go index 3175ecc..4aee092 100644 --- a/db.go +++ b/db.go @@ -84,6 +84,7 @@ CREATE TABLE IF NOT EXISTS surveys( promo MEDIUMINT NOT NULL, grp VARCHAR(255) NOT NULL, shown BOOLEAN NOT NULL DEFAULT FALSE, + direct INTEGER DEFAULT NULL, corrected BOOLEAN NOT NULL DEFAULT FALSE, start_availability TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, end_availability TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP diff --git a/direct.go b/direct.go new file mode 100644 index 0000000..2e45835 --- /dev/null +++ b/direct.go @@ -0,0 +1,358 @@ +package main + +import ( + "context" + "log" + "net/http" + "strconv" + "sync" + "time" + + "github.com/julienschmidt/httprouter" + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" +) + +var ( + WSClients = map[int64][]WSClient{} + WSClientsMutex = sync.RWMutex{} + WSAdmin = []WSClient{} + WSAdminMutex = sync.RWMutex{} +) + +func init() { + router.GET("/api/surveys/:sid/ws", rawAuthHandler(SurveyWS, loggedUser)) + router.GET("/api/surveys/:sid/ws-admin", rawAuthHandler(SurveyWSAdmin, adminRestricted)) + + router.GET("/api/surveys/:sid/ws/stats", apiHandler(surveyHandler(func(s Survey, body []byte) HTTPResponse { + return APIResponse{ + WSSurveyStats(s.Id), + } + }), adminRestricted)) +} + +func WSSurveyStats(sid int64) map[string]interface{} { + var users []string + var nb int + + WSClientsMutex.RLock() + defer WSClientsMutex.RUnlock() + if w, ok := WSClients[sid]; ok { + nb = len(w) + for _, ws := range w { + users = append(users, ws.u.Login) + } + } + + return map[string]interface{}{ + "nb_clients": nb, + "users": users, + } +} + +type WSClient struct { + ws *websocket.Conn + c chan WSMessage + u *User + sid int64 +} + +func SurveyWS_run(ws *websocket.Conn, c chan WSMessage, sid int64, u *User) { + for { + msg, ok := <-c + if !ok { + break + } + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + err := wsjson.Write(ctx, ws, msg) + if err != nil { + log.Println("Error on WebSocket:", err) + ws.Close(websocket.StatusInternalError, "error on write") + break + } + } + ws.Close(websocket.StatusNormalClosure, "end") + + WSClientsMutex.Lock() + defer WSClientsMutex.Unlock() + + for i, clt := range WSClients[sid] { + if clt.ws == ws { + WSClients[sid][i] = WSClients[sid][len(WSClients[sid])-1] + WSClients[sid] = WSClients[sid][:len(WSClients[sid])-1] + break + } + } + log.Println(u.Login, "disconnected") +} + +func msgCurrentState(survey *Survey) (msg WSMessage) { + if *survey.Direct == 0 { + msg = WSMessage{ + Action: "pause", + } + } else { + msg = WSMessage{ + Action: "new_question", + QuestionId: survey.Direct, + } + } + return +} + +func SurveyWS(w http.ResponseWriter, r *http.Request, ps httprouter.Params, u *User, body []byte) { + if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err != nil { + http.Error(w, "{\"errmsg\": \"Invalid survey identifier\"}", http.StatusBadRequest) + return + } else if survey, err := getSurvey(sid); err != nil { + http.Error(w, "{\"errmsg\": \"Survey not found\"}", http.StatusNotFound) + return + } else if survey.Direct == nil { + http.Error(w, "{\"errmsg\": \"Not a direct survey\"}", http.StatusBadRequest) + return + } else { + ws, err := websocket.Accept(w, r, nil) + if err != nil { + log.Fatal("error get connection", err) + } + + log.Println(u.Login, "is now connected to WS", sid) + + c := make(chan WSMessage, 1) + + WSClientsMutex.Lock() + defer WSClientsMutex.Unlock() + WSClients[survey.Id] = append(WSClients[survey.Id], WSClient{ws, c, u, survey.Id}) + + // Send current state + c <- msgCurrentState(&survey) + + go SurveyWS_run(ws, c, survey.Id, u) + } +} + +func WSWriteAll(message WSMessage) { + WSClientsMutex.RLock() + defer WSClientsMutex.RUnlock() + + for _, wss := range WSClients { + for _, ws := range wss { + ws.c <- message + } + } +} + +type WSMessage struct { + Action string `json:"action"` + SurveyId *int64 `json:"survey,omitempty"` + QuestionId *int64 `json:"question,omitempty"` + Stats map[string]interface{} `json:"stats,omitempty"` + UserId *int64 `json:"user,omitempty"` + Response string `json:"value,omitempty"` +} + +func (s *Survey) WSWriteAll(message WSMessage) { + WSClientsMutex.RLock() + defer WSClientsMutex.RUnlock() + + if wss, ok := WSClients[s.Id]; ok { + for _, ws := range wss { + ws.c <- message + } + } +} + +func (s *Survey) WSCloseAll(message string) { + WSClientsMutex.RLock() + defer WSClientsMutex.RUnlock() + + if wss, ok := WSClients[s.Id]; ok { + for _, ws := range wss { + close(ws.c) + } + } +} + +// Admin ////////////////////////////////////////////////////////////// + +func SurveyWSAdmin_run(ctx context.Context, ws *websocket.Conn, c chan WSMessage, sid int64, u *User) { + ct := time.Tick(25000 * time.Millisecond) +loopadmin: + for { + select { + case <-ct: + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + err := wsjson.Write(ctx, ws, WSMessage{ + Action: "stats", + Stats: WSSurveyStats(sid), + }) + if err != nil { + log.Println("Error on WebSocket:", err) + ws.Close(websocket.StatusInternalError, "error on write") + break + } + + case msg, ok := <-c: + if !ok { + break loopadmin + } + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + err := wsjson.Write(ctx, ws, msg) + if err != nil { + log.Println("Error on WebSocket:", err) + ws.Close(websocket.StatusInternalError, "error on write") + break + } + } + } + ws.Close(websocket.StatusNormalClosure, "end") + + WSAdminMutex.Lock() + defer WSAdminMutex.Unlock() + + for i, clt := range WSAdmin { + if clt.ws == ws { + WSAdmin[i] = WSAdmin[len(WSAdmin)-1] + WSAdmin = WSAdmin[:len(WSAdmin)-1] + break + } + } + log.Println(u.Login, "admin disconnected") +} + +func SurveyWSAdmin(w http.ResponseWriter, r *http.Request, ps httprouter.Params, u *User, body []byte) { + if sid, err := strconv.Atoi(string(ps.ByName("sid"))); err != nil { + http.Error(w, "{\"errmsg\": \"Invalid survey identifier\"}", http.StatusBadRequest) + return + } else if survey, err := getSurvey(sid); err != nil { + http.Error(w, "{\"errmsg\": \"Survey not found\"}", http.StatusNotFound) + return + } else if survey.Direct == nil { + http.Error(w, "{\"errmsg\": \"Not a direct survey\"}", http.StatusBadRequest) + return + } else { + ws, err := websocket.Accept(w, r, nil) + if err != nil { + log.Fatal("error get connection", err) + } + + log.Println(u.Login, "is now connected to WS-admin", sid) + + c := make(chan WSMessage, 2) + + WSAdminMutex.Lock() + defer WSAdminMutex.Unlock() + WSAdmin = append(WSAdmin, WSClient{ws, c, u, survey.Id}) + + // Send current state + c <- msgCurrentState(&survey) + + go SurveyWSAdmin_run(r.Context(), ws, c, survey.Id, u) + go func(c chan WSMessage, sid int) { + var v WSMessage + var err error + for { + err = wsjson.Read(context.Background(), ws, &v) + if err != nil { + log.Println("Error when receiving message:", err) + close(c) + break + } + + if v.Action == "new_question" && v.QuestionId != nil { + if survey, err := getSurvey(sid); err != nil { + log.Println("Unable to retrieve survey:", err) + } else { + survey.Direct = v.QuestionId + _, err = survey.Update() + if err != nil { + log.Println("Unable to update survey:", err) + } + + survey.WSWriteAll(v) + v.SurveyId = &survey.Id + WSAdminWriteAll(v) + } + } else if v.Action == "pause" { + if survey, err := getSurvey(sid); err != nil { + log.Println("Unable to retrieve survey:", err) + } else { + var u int64 = 0 + survey.Direct = &u + _, err = survey.Update() + if err != nil { + log.Println("Unable to update survey:", err) + } + + survey.WSWriteAll(v) + v.SurveyId = &survey.Id + WSAdminWriteAll(v) + } + } else if v.Action == "end" { + if survey, err := getSurvey(sid); err != nil { + log.Println("Unable to retrieve survey:", err) + } else { + survey.Direct = nil + _, err = survey.Update() + if err != nil { + log.Println("Unable to update survey:", err) + } + + survey.WSCloseAll("Fin du live") + v.SurveyId = &survey.Id + WSAdminWriteAll(v) + } + } else if v.Action == "get_stats" { + err = wsjson.Write(context.Background(), ws, WSMessage{Action: "stats", Stats: WSSurveyStats(int64(sid))}) + } else if v.Action == "get_responses" { + if survey, err := getSurvey(sid); err != nil { + log.Println("Unable to retrieve survey:", err) + } else if questions, err := survey.GetQuestions(); err != nil { + log.Println("Unable to retrieve questions:", err) + } else { + for _, q := range questions { + if responses, err := q.GetResponses(); err != nil { + log.Println("Unable to retrieve questions:", err) + } else { + for _, r := range responses { + wsjson.Write(context.Background(), ws, WSMessage{Action: "new_response", UserId: &r.IdUser, QuestionId: &q.Id, Response: r.Answer}) + } + } + } + } + } else { + log.Println("Unknown admin action:", v.Action) + } + } + }(c, sid) + } +} + +func WSAdminWriteAll(message WSMessage) { + WSAdminMutex.RLock() + defer WSAdminMutex.RUnlock() + + for _, ws := range WSAdmin { + ws.c <- message + } +} + +func (s *Survey) WSAdminWriteAll(message WSMessage) { + WSAdminMutex.RLock() + defer WSAdminMutex.RUnlock() + + for _, ws := range WSAdmin { + log.Println("snd", message, ws.sid, s.Id) + if ws.sid == s.Id { + ws.c <- message + } + } +} diff --git a/go.mod b/go.mod index 530f842..0723079 100644 --- a/go.mod +++ b/go.mod @@ -15,4 +15,5 @@ require ( golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 google.golang.org/appengine v1.6.7 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect + nhooyr.io/websocket v1.8.7 ) diff --git a/go.sum b/go.sum index 03ec421..98853f3 100644 --- a/go.sum +++ b/go.sum @@ -43,15 +43,25 @@ github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjs github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -90,6 +100,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -104,6 +115,7 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -120,16 +132,23 @@ github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJz github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= @@ -139,9 +158,12 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -257,6 +279,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -411,6 +434,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -420,6 +444,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/handler.go b/handler.go index d85cc61..c146cf2 100644 --- a/handler.go +++ b/handler.go @@ -76,6 +76,12 @@ func eraseCookie(w http.ResponseWriter) { } func rawHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params, []byte), access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) { + return rawAuthHandler(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params, _ *User, body []byte) { + f(w, r, ps, body) + }, access...) +} + +func rawAuthHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params, *User, []byte), access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { if addr := r.Header.Get("X-Forwarded-For"); addr != "" { r.RemoteAddr = addr @@ -136,7 +142,7 @@ func rawHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params, [] } } - f(w, r, ps, body) + f(w, r, ps, user, body) } } diff --git a/questions.go b/questions.go index eca2959..62181e9 100644 --- a/questions.go +++ b/questions.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "errors" + "fmt" "net/http" "strconv" "time" @@ -38,10 +39,18 @@ func init() { return formatApiResponse(s.NewQuestion(new.Title, new.DescriptionRaw, new.Placeholder, new.Kind)) }), adminRestricted)) - router.GET("/api/questions/:qid", apiHandler(questionHandler( - func(s Question, _ []byte) HTTPResponse { - return APIResponse{s} - }), adminRestricted)) + router.GET("/api/questions/:qid", apiAuthHandler(questionAuthHandler( + func(q Question, u *User, _ []byte) HTTPResponse { + if u.IsAdmin { + return APIResponse{q} + } else if s, err := getSurvey(int(q.IdSurvey)); err != nil { + return APIErrorResponse{err: err} + } else if s.Shown || (s.Direct != nil && *s.Direct == q.Id) { + return APIResponse{q} + } else { + return APIErrorResponse{err: fmt.Errorf("Not authorized"), status: http.StatusForbidden} + } + }), loggedUser)) router.GET("/api/surveys/:sid/questions/:qid", apiHandler(questionHandler( func(s Question, _ []byte) HTTPResponse { return APIResponse{s} diff --git a/responses.go b/responses.go index d89f13a..7fa8055 100644 --- a/responses.go +++ b/responses.go @@ -25,10 +25,16 @@ func init() { } for _, response := range responses { - if len(response.Answer) > 0 { + if !s.Shown && (s.Direct == nil || *s.Direct != response.IdQuestion) { + return APIErrorResponse{err: fmt.Errorf("Cette question n'est pas disponible")} + } else if len(response.Answer) > 0 { if _, err := s.NewResponse(response.IdQuestion, u.Id, response.Answer); err != nil { return APIErrorResponse{err: err} } + + if s.Direct != nil { + s.WSAdminWriteAll(WSMessage{Action: "new_response", UserId: &u.Id, QuestionId: &response.IdQuestion, Response: response.Answer}) + } } } diff --git a/surveys.go b/surveys.go index 872d971..e880652 100644 --- a/surveys.go +++ b/surveys.go @@ -20,11 +20,11 @@ func init() { router.GET("/api/surveys", apiAuthHandler( func(u *User, _ httprouter.Params, _ []byte) HTTPResponse { if u == nil { - return formatApiResponse(getSurveys(fmt.Sprintf("WHERE shown = TRUE AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC", currentPromo))) + return formatApiResponse(getSurveys(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC", currentPromo))) } else if u.IsAdmin { return formatApiResponse(getSurveys("ORDER BY promo DESC, start_availability ASC")) } else { - surveys, err := getSurveys(fmt.Sprintf("WHERE shown = TRUE AND promo = %d ORDER BY start_availability ASC", u.Promo)) + surveys, err := getSurveys(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND promo = %d ORDER BY start_availability ASC", u.Promo)) if err != nil { return APIErrorResponse{err: err} } @@ -49,7 +49,7 @@ func init() { new.Promo = currentPromo } - return formatApiResponse(NewSurvey(new.Title, new.Promo, new.Group, new.Shown, new.StartAvailability, new.EndAvailability)) + return formatApiResponse(NewSurvey(new.Title, new.Promo, new.Group, new.Shown, new.Direct, new.StartAvailability, new.EndAvailability)) }, adminRestricted)) router.GET("/api/surveys/:sid", apiAuthHandler(surveyAuthHandler( func(s Survey, u *User, _ []byte) HTTPResponse { @@ -69,6 +69,22 @@ func init() { } new.Id = current.Id + + if new.Direct != current.Direct { + if new.Direct == nil { + current.WSCloseAll("") + } else if *new.Direct == 0 { + current.WSWriteAll(WSMessage{ + Action: "pause", + }) + } else { + current.WSWriteAll(WSMessage{ + Action: "new_question", + QuestionId: new.Direct, + }) + } + } + return formatApiResponse(new.Update()) }), adminRestricted)) router.DELETE("/api/surveys/:sid", apiHandler(surveyHandler( @@ -144,20 +160,21 @@ type Survey struct { Promo uint `json:"promo"` Group string `json:"group"` Shown bool `json:"shown"` + Direct *int64 `json:"direct"` Corrected bool `json:"corrected"` StartAvailability time.Time `json:"start_availability"` EndAvailability time.Time `json:"end_availability"` } func getSurveys(cnd string, param ...interface{}) (surveys []Survey, err error) { - if rows, errr := DBQuery("SELECT id_survey, title, promo, grp, shown, corrected, start_availability, end_availability FROM surveys "+cnd, param...); errr != nil { + if rows, errr := DBQuery("SELECT id_survey, title, promo, grp, shown, direct, corrected, start_availability, end_availability FROM surveys "+cnd, param...); errr != nil { return nil, errr } else { defer rows.Close() for rows.Next() { var s Survey - if err = rows.Scan(&s.Id, &s.Title, &s.Promo, &s.Group, &s.Shown, &s.Corrected, &s.StartAvailability, &s.EndAvailability); err != nil { + if err = rows.Scan(&s.Id, &s.Title, &s.Promo, &s.Group, &s.Shown, &s.Direct, &s.Corrected, &s.StartAvailability, &s.EndAvailability); err != nil { return } surveys = append(surveys, s) @@ -171,17 +188,17 @@ func getSurveys(cnd string, param ...interface{}) (surveys []Survey, err error) } func getSurvey(id int) (s Survey, err error) { - err = DBQueryRow("SELECT id_survey, title, promo, grp, shown, corrected, start_availability, end_availability FROM surveys WHERE id_survey=?", id).Scan(&s.Id, &s.Title, &s.Promo, &s.Group, &s.Shown, &s.Corrected, &s.StartAvailability, &s.EndAvailability) + err = DBQueryRow("SELECT id_survey, title, promo, grp, shown, direct, corrected, start_availability, end_availability FROM surveys WHERE id_survey=?", id).Scan(&s.Id, &s.Title, &s.Promo, &s.Group, &s.Shown, &s.Direct, &s.Corrected, &s.StartAvailability, &s.EndAvailability) return } -func NewSurvey(title string, promo uint, group string, shown bool, startAvailability time.Time, endAvailability time.Time) (*Survey, error) { - if res, err := DBExec("INSERT INTO surveys (title, promo, grp, shown, start_availability, end_availability) VALUES (?, ?, ?, ?, ?, ?)", title, promo, group, shown, startAvailability, endAvailability); err != nil { +func NewSurvey(title string, promo uint, group string, shown bool, direct *int64, startAvailability time.Time, endAvailability time.Time) (*Survey, error) { + if res, err := DBExec("INSERT INTO surveys (title, promo, grp, shown, direct, start_availability, end_availability) VALUES (?, ?, ?, ?, ?, ?, ?)", title, promo, group, shown, direct, startAvailability, endAvailability); err != nil { return nil, err } else if sid, err := res.LastInsertId(); err != nil { return nil, err } else { - return &Survey{sid, title, promo, group, shown, false, startAvailability, endAvailability}, nil + return &Survey{sid, title, promo, group, shown, direct, false, startAvailability, endAvailability}, nil } } @@ -227,7 +244,7 @@ func (s Survey) GetScores() (scores map[int64]*float64, err error) { } func (s *Survey) Update() (*Survey, error) { - if _, err := DBExec("UPDATE surveys SET title = ?, promo = ?, grp = ?, shown = ?, corrected = ?, start_availability = ?, end_availability = ? WHERE id_survey = ?", s.Title, s.Promo, s.Group, s.Shown, s.Corrected, s.StartAvailability, s.EndAvailability, s.Id); err != nil { + if _, err := DBExec("UPDATE surveys SET title = ?, promo = ?, grp = ?, shown = ?, direct = ?, corrected = ?, start_availability = ?, end_availability = ? WHERE id_survey = ?", s.Title, s.Promo, s.Group, s.Shown, s.Direct, s.Corrected, s.StartAvailability, s.EndAvailability, s.Id); err != nil { return nil, err } else { return s, err