From 6c1ca2a23bf39822a7eeebe3d4fdf2332339b3d0 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sat, 9 Jul 2022 19:42:00 +0200 Subject: [PATCH 01/14] Use gin-gonic instead of httprouter --- responses.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/responses.go b/responses.go index 4db50bb..27f21f4 100644 --- a/responses.go +++ b/responses.go @@ -27,7 +27,7 @@ func declareAPIAuthResponsesRoutes(router *gin.RouterGroup) { } var responses []Response - if err := c.ShouldBindJSON(responses); err != nil { + if err := c.ShouldBindJSON(&responses); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } From bb7c4f32e4654161e2c5bfa40b9c9e5e465908a0 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Sep 2022 13:30:10 +0200 Subject: [PATCH 02/14] Add a route to update NeedHelp entries --- help.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/help.go b/help.go index 53e85ca..39f24dc 100644 --- a/help.go +++ b/help.go @@ -1,8 +1,10 @@ package main import ( + "fmt" "log" "net/http" + "strconv" "time" "github.com/gin-gonic/gin" @@ -19,6 +21,29 @@ func declareAPIAdminHelpRoutes(router *gin.RouterGroup) { c.JSON(http.StatusOK, nhs) }) + + needhelpsRoutes := router.Group("/help/:hid") + needhelpsRoutes.Use(needHelpHandler) + + needhelpsRoutes.PUT("", func(c *gin.Context) { + current := c.MustGet("needhelp").(*NeedHelp) + + var new NeedHelp + if err := c.ShouldBindJSON(&new); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + new.Id = current.Id + + if err := new.Update(); err != nil { + log.Println("Unable to Update needhelp:", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during needhelp entry updation: %s", err.Error())}) + return + } else { + c.JSON(http.StatusOK, new) + } + }) } func declareAPIAuthHelpRoutes(router *gin.RouterGroup) { @@ -36,6 +61,19 @@ func declareAPIAuthHelpRoutes(router *gin.RouterGroup) { }) } +func needHelpHandler(c *gin.Context) { + if hid, err := strconv.Atoi(string(c.Param("hid"))); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad need help identifier."}) + return + } else if nh, err := getNeedHelp(hid); err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Need help entry not found."}) + return + } else { + c.Set("needhelp", nh) + c.Next() + } +} + type NeedHelp struct { Id int64 `json:"id"` IdUser int64 `json:"id_user"` @@ -65,6 +103,12 @@ func getNeedHelps() (nh []NeedHelp, err error) { } } +func getNeedHelp(id int) (n *NeedHelp, err error) { + n = new(NeedHelp) + err = DBQueryRow("SELECT id_need_help, id_user, date, comment, date_treated FROM user_need_help WHERE id_need_help=?", id).Scan(&n.Id, &n.IdUser, &n.Date, &n.Comment, &n.DateTreated) + return +} + func (u *User) NewNeedHelp() (NeedHelp, error) { if res, err := DBExec("INSERT INTO user_need_help (id_user, comment) VALUES (?, ?)", u.Id, ""); err != nil { return NeedHelp{}, err From 9c0e35b1e241cc4752d0b92b5048320070d4ea10 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Sep 2022 14:10:05 +0200 Subject: [PATCH 03/14] Can mark need help as treated --- help.go | 6 +++--- ui/src/lib/users.js | 43 +++++++++++++++++++++++++++++++++++++- ui/src/routes/index.svelte | 25 ++++++++++++++++++++-- 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/help.go b/help.go index 39f24dc..181ae7e 100644 --- a/help.go +++ b/help.go @@ -12,7 +12,7 @@ import ( func declareAPIAdminHelpRoutes(router *gin.RouterGroup) { router.GET("/help", func(c *gin.Context) { - nhs, err := getNeedHelps() + nhs, err := getNeedHelps("WHERE date_treated IS NULL") if err != nil { log.Println("Unable to getNeedHelps:", err) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during need helps retrieval. Please retry."}) @@ -82,8 +82,8 @@ type NeedHelp struct { DateTreated *time.Time `json:"treated,omitempty"` } -func getNeedHelps() (nh []NeedHelp, err error) { - if rows, errr := DBQuery("SELECT id_need_help, id_user, date, comment, date_treated FROM user_need_help"); errr != nil { +func getNeedHelps(cond string) (nh []NeedHelp, err error) { + if rows, errr := DBQuery("SELECT id_need_help, id_user, date, comment, date_treated FROM user_need_help " + cond); errr != nil { return nil, errr } else { defer rows.Close() diff --git a/ui/src/lib/users.js b/ui/src/lib/users.js index 70deb9d..b70104c 100644 --- a/ui/src/lib/users.js +++ b/ui/src/lib/users.js @@ -51,10 +51,51 @@ export async function getUserScore(uid, survey) { } } +export class UserNeedingHelp { + constructor(res) { + if (res) { + this.update(res); + } + } + + update({ id, id_user, date, comment, treated }) { + this.id = id; + this.id_user = id_user; + this.date = new Date(date); + this.comment = comment; + if (treated) { + this.treated = new Date(treated); + } else { + this.treated = null; + } + } + + mark_treated() { + this.treated = new Date(); + } + + async save() { + const res = await fetch(this.id?`api/help/${this.id}`:'api/help', { + method: this.id?'PUT':'POST', + headers: {'Accept': 'application/json'}, + body: JSON.stringify(this), + }); + if (res.status == 200) { + const data = await res.json() + this.update(data); + return data; + } else { + throw new Error((await res.json()).errmsg); + } + } +} + export async function getUserNeedingHelp() { const res = await fetch(`api/help`, {headers: {'Accept': 'application/json'}}) if (res.status == 200) { - return await res.json(); + return (await res.json()).map((nh) => { + return new UserNeedingHelp(nh) + }); } else { throw new Error((await res.json()).errmsg); } diff --git a/ui/src/routes/index.svelte b/ui/src/routes/index.svelte index de5e9aa..1da957e 100644 --- a/ui/src/routes/index.svelte +++ b/ui/src/routes/index.svelte @@ -1,10 +1,24 @@
@@ -29,7 +43,7 @@ {#if $user.is_admin}

Demande d'aide :

- {#await getUserNeedingHelp()} + {#await users_needing_help} {:then nhs}
    @@ -40,7 +54,14 @@ {:then u} {u.login} {/await} - ({user.date}) + () + {/each}
From cfbd00beef3fc048724520c64bc73ece26ca2766 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Sep 2022 17:15:37 +0200 Subject: [PATCH 04/14] Don't consider no response as error --- responses.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/responses.go b/responses.go index 27f21f4..103a0cb 100644 --- a/responses.go +++ b/responses.go @@ -1,6 +1,8 @@ package main import ( + "database/sql" + "errors" "log" "net/http" "strconv" @@ -142,7 +144,7 @@ func declareAPIAuthQuestionResponsesRoutes(router *gin.RouterGroup) { q := c.MustGet("question").(*Question) res, err := q.GetMyResponse(u, false) - if err != nil { + if err != nil && !errors.Is(err, sql.ErrNoRows) { log.Printf("Unable to GetMyResponse(uid=%d;qid=%d;false): %s", u.Id, q.Id, err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during response retrieval."}) return From f0bd1b1e70e1ac7121ff8b6d1cfb2f5319e648e7 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Sep 2022 17:16:00 +0200 Subject: [PATCH 05/14] Disallow responding to corrected survey --- responses.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/responses.go b/responses.go index 103a0cb..9e0e809 100644 --- a/responses.go +++ b/responses.go @@ -47,7 +47,7 @@ func declareAPIAuthResponsesRoutes(router *gin.RouterGroup) { } for _, response := range responses { - if !uauth.IsAdmin && !s.Shown && (s.Direct == nil || *s.Direct != response.IdQuestion) { + if !uauth.IsAdmin && !s.Shown && (s.Corrected || s.Direct == nil || *s.Direct != response.IdQuestion) { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Cette question n'est pas disponible"}) return } else if len(response.Answer) > 0 { From ab3c94119d017faff7d57b9cde579256f4d25795 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Sep 2022 19:11:26 +0200 Subject: [PATCH 06/14] Use bars for MCQ, add mean for int responses --- ui/src/components/CorrectionPieChart.svelte | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ui/src/components/CorrectionPieChart.svelte b/ui/src/components/CorrectionPieChart.svelte index f203a23..461cf49 100644 --- a/ui/src/components/CorrectionPieChart.svelte +++ b/ui/src/components/CorrectionPieChart.svelte @@ -32,20 +32,24 @@ } let req_proposals = null; let req_responses = null; + let mean = null; if (question.kind == "int") { req_responses = question.getResponses(); req_responses.then((responses) => { + const values = []; const proposal_idx = { }; for (const res of responses) { if (proposal_idx[res.value]) { data.datasets[0].values[proposal_idx[res.value]] += 1; + values.push(Number(res.value)); } else { data.labels.push(res.value); data.datasets[0].values.push(1); proposal_idx[res.value] = new String(data.labels.length - 1); } } + mean = Math.trunc(values.reduce((p, e) => p + e) / values.length*10)/10; }); } else { req_proposals = refreshProposals(); @@ -74,7 +78,16 @@ Récupération des réponses…
{:then} - + {#if mean !== null} +
+ Moyenne représentative : {mean} +
+ {/if} + {#if question.kind === "mcq"} + + {:else} + + {/if} {/await} {/await} From 9d95360e990f8982356e148fe919d7cd7e836e27 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Sep 2022 19:11:53 +0200 Subject: [PATCH 07/14] Add the ability to generate response templates, based on proposals --- ui/src/components/CorrectionReference.svelte | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/ui/src/components/CorrectionReference.svelte b/ui/src/components/CorrectionReference.svelte index 83b62fd..f1c51e1 100644 --- a/ui/src/components/CorrectionReference.svelte +++ b/ui/src/components/CorrectionReference.svelte @@ -19,6 +19,35 @@ templates = templates; } + function genTemplates() { + question.getProposals().then((proposals) => { + let i = 0; + for (const p of proposals) { + // Search proposal in templates + let found = false; + for (const tpl of templates) { + if (tpl.regexp.indexOf(p.id.toString()) !== -1) { + found = true; + break; + } + } + + if (!found) { + const ct = new CorrectionTemplate() + ct.id_question = question.id; + ct.regexp = p.id.toString(); + ct.label = String.fromCharCode(97 + i); + ct.save().then((ct) => { + templates.push(ct); + templates = templates; + }); + } + + i++; + } + }) + } + function delTemplate(tpl) { tpl.delete().then(() => { const idx = templates.findIndex((e) => e.id === tpl.id); @@ -109,4 +138,13 @@ > Ajouter un template + {#if question.kind == "mcq" || question.kind == "ucq"} + + {/if} From 48a7ccc9791f198ffa71c6cfab9199f88e2f96ad Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Sep 2022 19:13:19 +0200 Subject: [PATCH 08/14] Remove debug print --- ui/src/routes/surveys/[sid]/admin.svelte | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ui/src/routes/surveys/[sid]/admin.svelte b/ui/src/routes/surveys/[sid]/admin.svelte index 64e45b8..9060c9f 100644 --- a/ui/src/routes/surveys/[sid]/admin.svelte +++ b/ui/src/routes/surveys/[sid]/admin.svelte @@ -98,7 +98,7 @@ ws.addEventListener("close", (e) => { ws_up = false; - console.log('Socket is closed. Reconnect will be attempted in 1 second.', e); + console.log('Socket is closed. Reconnect will be attempted in 1 second.'); ws = null; updateSurvey(); setTimeout(function() { @@ -106,10 +106,6 @@ }, 1500); }); - ws.onerror((evt) => { - console.log('onerror', evt) - }) - ws.addEventListener("error", (err) => { ws_up = false; console.log('Socket closed due to error.', err); @@ -118,7 +114,6 @@ ws.addEventListener("message", (message) => { const data = JSON.parse(message.data); - console.log(data); if (data.action && data.action == "new_question") { current_question = data.question; if (timer_cancel) { From 99441d54f365d760ccbc576a4ffc49db15667286 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Sep 2022 19:13:55 +0200 Subject: [PATCH 09/14] Use seconds instead of miliseconds --- ui/src/routes/surveys/[sid]/admin.svelte | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/routes/surveys/[sid]/admin.svelte b/ui/src/routes/surveys/[sid]/admin.svelte index 9060c9f..334d034 100644 --- a/ui/src/routes/surveys/[sid]/admin.svelte +++ b/ui/src/routes/surveys/[sid]/admin.svelte @@ -43,7 +43,7 @@ let wsstats = null; let current_question = null; let responses = {}; - let timer = 20000; + let timer = 20; let timer_end = null; let timer_remain = 0; let timer_cancel = null; @@ -216,7 +216,7 @@ disabled value={timer_remain} > - ms + s {:else}
@@ -226,7 +226,7 @@ bind:value={timer} placeholder="Valeur du timer" > - ms + s
{/if} From 89f61d10469e1873ebf2d86e3e9839fb0d541c43 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Sep 2022 19:14:57 +0200 Subject: [PATCH 10/14] Can display corrections in live sessions --- direct.go | 21 ++++++++++++++---- ui/src/components/QuestionForm.svelte | 2 +- ui/src/routes/surveys/[sid]/admin.svelte | 27 ++++++++++++++++++++++-- ui/src/routes/surveys/[sid]/live.svelte | 6 ++++-- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/direct.go b/direct.go index 02eb44e..32f1498 100644 --- a/direct.go +++ b/direct.go @@ -101,6 +101,7 @@ func msgCurrentState(survey *Survey) (msg WSMessage) { msg = WSMessage{ Action: "new_question", QuestionId: survey.Direct, + Corrected: survey.Corrected, } } return @@ -152,6 +153,7 @@ type WSMessage struct { Stats map[string]interface{} `json:"stats,omitempty"` UserId *int64 `json:"user,omitempty"` Response string `json:"value,omitempty"` + Corrected bool `json:"corrected,omitempty"` Timer uint `json:"timer,omitempty"` } @@ -274,15 +276,26 @@ func SurveyWSAdmin(c *gin.Context) { if *survey.Direct != 0 { var z int64 = 0 survey.Direct = &z + survey.Corrected = false survey.Update() } - go func() { + go func(corrected bool) { time.Sleep(time.Duration(OffsetQuestionTimer+v.Timer) * time.Millisecond) - survey.WSWriteAll(WSMessage{Action: "pause"}) - WSAdminWriteAll(WSMessage{Action: "pause", SurveyId: &survey.Id}) - }() + + if corrected { + survey.Corrected = v.Corrected + survey.Update() + + survey.WSWriteAll(WSMessage{Action: "new_question", QuestionId: v.QuestionId, Corrected: true}) + } else { + survey.WSWriteAll(WSMessage{Action: "pause"}) + WSAdminWriteAll(WSMessage{Action: "pause", SurveyId: &survey.Id}) + } + }(v.Corrected) + v.Corrected = false } else { survey.Direct = v.QuestionId + survey.Corrected = v.Corrected } _, err = survey.Update() if err != nil { diff --git a/ui/src/components/QuestionForm.svelte b/ui/src/components/QuestionForm.svelte index 6a11e9f..7944148 100644 --- a/ui/src/components/QuestionForm.svelte +++ b/ui/src/components/QuestionForm.svelte @@ -132,7 +132,7 @@ > {/if} - {#if survey && survey.corrected} + {#if survey && survey.corrected && response_history} { ws.send('{"action":"pause"}')} } + title="Passer sur une scène sans question" > + @@ -277,9 +290,11 @@ @@ -291,6 +306,14 @@ > + + + {/each} diff --git a/ui/src/routes/surveys/[sid]/live.svelte b/ui/src/routes/surveys/[sid]/live.svelte index cb43ea3..7a932b6 100644 --- a/ui/src/routes/surveys/[sid]/live.svelte +++ b/ui/src/routes/surveys/[sid]/live.svelte @@ -89,6 +89,7 @@ console.log(data); if (data.action && data.action == "new_question") { show_question = data.question; + survey.corrected = data.corrected; if (timer_cancel) { clearInterval(timer_cancel); timer_cancel = null; @@ -157,7 +158,7 @@ } -{#await surveyP then survey} +{#await surveyP then unused} {#if $user && $user.is_admin} @@ -185,8 +186,9 @@ {:then question} = 100} + readonly={timer >= 100 || survey.corrected} bind:value={value} on:change={sendValue} > From ed42f2eaeb3f6b5b8feec47c4dfb2c083717d8fc Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Sep 2022 19:15:53 +0200 Subject: [PATCH 11/14] In live session, radio are big buttons --- ui/src/components/QuestionForm.svelte | 2 ++ ui/src/components/QuestionProposals.svelte | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ui/src/components/QuestionForm.svelte b/ui/src/components/QuestionForm.svelte index 7944148..4a3576e 100644 --- a/ui/src/components/QuestionForm.svelte +++ b/ui/src/components/QuestionForm.svelte @@ -91,6 +91,7 @@ kind={question.kind} {proposals} readonly + live={survey.direct !== null} bind:value={value} on:change={() => { dispatch("change"); }} /> @@ -108,6 +109,7 @@ kind={question.kind} {proposals} {readonly} + live={survey.direct !== null} bind:value={value} on:change={() => { dispatch("change"); }} /> diff --git a/ui/src/components/QuestionProposals.svelte b/ui/src/components/QuestionProposals.svelte index 3219574..910c63e 100644 --- a/ui/src/components/QuestionProposals.svelte +++ b/ui/src/components/QuestionProposals.svelte @@ -5,6 +5,7 @@ export let edit = false; export let proposals = []; + export let live = false; export let kind = 'mcq'; export let prefixid = ''; export let readonly = false; @@ -28,6 +29,7 @@ } +
{#each proposals as proposal, pid (proposal.id)}
{#if kind == 'mcq'} @@ -44,7 +46,8 @@ {:else} {:else}
{/each} +
{#if edit} {#if kind == 'mcq'} Date: Thu, 1 Sep 2022 19:17:26 +0200 Subject: [PATCH 12/14] Change some texts --- ui/src/routes/surveys/[sid]/live.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/routes/surveys/[sid]/live.svelte b/ui/src/routes/surveys/[sid]/live.svelte index 7a932b6..e5e2538 100644 --- a/ui/src/routes/surveys/[sid]/live.svelte +++ b/ui/src/routes/surveys/[sid]/live.svelte @@ -202,7 +202,7 @@ {/if} {/await} @@ -228,7 +228,7 @@ class="form-control" bind:value={myQuestion} autofocus - placeholder="Remarques, soucis, choses pas claires ? Demandez !" + placeholder="Remarques, soucis, choses pas claires ? Levez la main ou écrivez ici !" >