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}
+
{value}
{:else if question.kind == 'int'} - + { dispatch("change"); }} + > {:else} - + {/if} - {#if survey.corrected} + {#if survey && survey.corrected}@@ -36,7 +47,7 @@ | ||||||||||||
{survey.title}
@@ -20,6 +22,12 @@
{/if}
+ {#if direct}
+
+ Rejoins le cours maintenant ! Il y a actuellement un questionnaire en direct : {direct.title}. Clique ici pour le rejoindre.
+
+ {/if}
+
Tu as fait les rendus suivants : 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} +
+
+
+ {#if survey.direct === null}
+ + < + {survey.title} + + Administration + ++ {#if survey.direct !== null} +
+ {#if ws_up}Connecté{:else}Déconnecté{/if}
+
+ {:else}
+
+
+ Chargement des questions …
+
+ {:then questions}
+
+
+ {/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}
+
+
+ {/await}
+ {:else if question.kind == 'mcq'}
+ {#await question.getProposals()}
+
+
+ Chargement des propositions …
+
+ {:then proposals}
+
+
+ {/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)}
+
+ {/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}
+
+ {/each}
+
+
+
+
+
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}
+
+{/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
+ < + {survey.title} ++
+ {#if ws_up}Connecté{:else}Déconnecté{/if}
+
+ |