Live: add timer questions

This commit is contained in:
nemunaire 2022-03-01 16:38:52 +01:00
parent 942875536b
commit 628c00b43c
4 changed files with 355 additions and 209 deletions

View File

@ -14,6 +14,7 @@ import (
) )
var ( var (
OffsetQuestionTimer uint = 700
WSClients = map[int64][]WSClient{} WSClients = map[int64][]WSClient{}
WSClientsMutex = sync.RWMutex{} WSClientsMutex = sync.RWMutex{}
WSAdmin = []WSClient{} WSAdmin = []WSClient{}
@ -152,6 +153,7 @@ type WSMessage struct {
Stats map[string]interface{} `json:"stats,omitempty"` Stats map[string]interface{} `json:"stats,omitempty"`
UserId *int64 `json:"user,omitempty"` UserId *int64 `json:"user,omitempty"`
Response string `json:"value,omitempty"` Response string `json:"value,omitempty"`
Timer uint `json:"timer,omitempty"`
} }
func (s *Survey) WSWriteAll(message WSMessage) { func (s *Survey) WSWriteAll(message WSMessage) {
@ -270,8 +272,21 @@ func SurveyWSAdmin(w http.ResponseWriter, r *http.Request, ps httprouter.Params,
if v.Action == "new_question" && v.QuestionId != nil { if v.Action == "new_question" && v.QuestionId != nil {
if survey, err := getSurvey(sid); err != nil { if survey, err := getSurvey(sid); err != nil {
log.Println("Unable to retrieve survey:", err) log.Println("Unable to retrieve survey:", err)
} else {
if v.Timer > 0 {
if *survey.Direct != 0 {
var z int64 = 0
survey.Direct = &z
survey.Update()
}
go func() {
time.Sleep(time.Duration(OffsetQuestionTimer+v.Timer) * time.Millisecond)
survey.WSWriteAll(WSMessage{Action: "pause"})
WSAdminWriteAll(WSMessage{Action: "pause", SurveyId: &survey.Id})
}()
} else { } else {
survey.Direct = v.QuestionId survey.Direct = v.QuestionId
}
_, err = survey.Update() _, err = survey.Update()
if err != nil { if err != nil {
log.Println("Unable to update survey:", err) log.Println("Unable to update survey:", err)
@ -348,6 +363,20 @@ func SurveyWSAdmin(w http.ResponseWriter, r *http.Request, ps httprouter.Params,
log.Println("Unable to update:", err) log.Println("Unable to update:", err)
} }
} }
} else if v.Action == "mark_answered" && v.Response == "all" {
if survey, err := getSurvey(sid); err != nil {
log.Println("Unable to retrieve survey:", err)
} else if asks, err := survey.GetAsks(v.Response == ""); err != nil {
log.Println("Unable to retrieve asks:", err)
} else {
for _, ask := range asks {
ask.Answered = true
err = ask.Update()
if err != nil {
log.Println("Unable to update:", err)
}
}
}
} else { } else {
log.Println("Unknown admin action:", v.Action) log.Println("Unknown admin action:", v.Action)
} }
@ -370,7 +399,6 @@ func (s *Survey) WSAdminWriteAll(message WSMessage) {
defer WSAdminMutex.RUnlock() defer WSAdminMutex.RUnlock()
for _, ws := range WSAdmin { for _, ws := range WSAdmin {
log.Println("snd", message, ws.sid, s.Id)
if ws.sid == s.Id { if ws.sid == s.Id {
ws.c <- message ws.c <- message
} }

View File

@ -63,6 +63,7 @@ func main() {
flag.StringVar(&DevProxy, "dev", DevProxy, "Proxify traffic to this host for static assets") flag.StringVar(&DevProxy, "dev", DevProxy, "Proxify traffic to this host for static assets")
flag.StringVar(&baseURL, "baseurl", baseURL, "URL prepended to each URL") flag.StringVar(&baseURL, "baseurl", baseURL, "URL prepended to each URL")
flag.UintVar(&currentPromo, "current-promo", currentPromo, "Year of the current promotion") flag.UintVar(&currentPromo, "current-promo", currentPromo, "Year of the current promotion")
flag.UintVar(&OffsetQuestionTimer, "offset-question-timer", OffsetQuestionTimer, "Duration to wait before sending pause msg in direct mode (in milliseconds)")
flag.Var(&localAuthUsers, "local-auth-user", "Allow local authentication for this user (bypass OIDC).") flag.Var(&localAuthUsers, "local-auth-user", "Allow local authentication for this user (bypass OIDC).")
flag.Parse() flag.Parse()

View File

@ -13,6 +13,7 @@
import { user } from '../../../stores/user'; import { user } from '../../../stores/user';
import SurveyAdmin from '../../../components/SurveyAdmin.svelte'; import SurveyAdmin from '../../../components/SurveyAdmin.svelte';
import SurveyBadge from '../../../components/SurveyBadge.svelte'; import SurveyBadge from '../../../components/SurveyBadge.svelte';
import { getSurvey } from '../../../lib/surveys';
import { getQuestions } from '../../../lib/questions'; import { getQuestions } from '../../../lib/questions';
import { getUsers } from '../../../lib/users'; import { getUsers } from '../../../lib/users';
@ -29,6 +30,10 @@
} }
}); });
function updateSurvey() {
surveyP = getSurvey(survey.id);
}
function updateQuestions() { function updateQuestions() {
req_questions = getQuestions(survey.id); req_questions = getQuestions(survey.id);
} }
@ -38,6 +43,21 @@
let wsstats = null; let wsstats = null;
let current_question = null; let current_question = null;
let responses = {}; let responses = {};
let timer = 20000;
let timer_end = null;
let timer_remain = 0;
let timer_cancel = null;
function updTimer() {
const now = new Date().getTime();
if (now > timer_end) {
timer_remain = 0;
clearInterval(timer_cancel);
timer_cancel = null;
} else {
timer_remain = Math.floor((timer_end - now) / 100)/10;
}
}
let users = {}; let users = {};
function updateUsers() { function updateUsers() {
@ -80,6 +100,7 @@
ws_up = false; ws_up = false;
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason); console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);
ws = null; ws = null;
updateSurvey();
setTimeout(function() { setTimeout(function() {
wsconnect(); wsconnect();
}, 1500); }, 1500);
@ -96,6 +117,16 @@
console.log(data); console.log(data);
if (data.action && data.action == "new_question") { if (data.action && data.action == "new_question") {
current_question = data.question; current_question = data.question;
if (timer_cancel) {
clearInterval(timer_cancel);
timer_cancel = null;
}
if (data.timer) {
timer_end = new Date().getTime() + data.timer;
timer_cancel = setInterval(updTimer, 250);
} else {
timer_end = null;
}
} else if (data.action && data.action == "stats") { } else if (data.action && data.action == "stats") {
wsstats = data.stats; wsstats = data.stats;
} else if (data.action && data.action == "new_response") { } else if (data.action && data.action == "new_response") {
@ -106,6 +137,11 @@
asks = asks; asks = asks;
} else { } else {
current_question = null; current_question = null;
timer_end = null;
if (timer_cancel) {
clearInterval(timer_cancel);
timer_cancel = null;
}
} }
}); });
} }
@ -118,9 +154,10 @@
<button <button
type="button" type="button"
class="btn btn-primary ms-1 float-end" class="btn btn-primary ms-1 float-end"
title="Terminer le direct"
on:click={() => { if (confirm("Sûr ?")) ws.send('{"action":"end"}') }} on:click={() => { if (confirm("Sûr ?")) ws.send('{"action":"end"}') }}
> >
Terminer <i class="bi bi-align-end"></i>
</button> </button>
{/if} {/if}
<a href="surveys/{survey.id}/responses" class="btn btn-success ms-1 float-end" title="Voir les réponses"><i class="bi bi-files"></i></a> <a href="surveys/{survey.id}/responses" class="btn btn-success ms-1 float-end" title="Voir les réponses"><i class="bi bi-files"></i></a>
@ -132,6 +169,11 @@
<small class="text-muted"> <small class="text-muted">
Administration Administration
</small> </small>
{#if asks.length}
<a href="surveys/{sid}/admin#questions_part">
<i class="bi bi-patch-question-fill text-danger"></i>
</a>
{/if}
</h2> </h2>
{#if survey.direct !== null} {#if survey.direct !== null}
<div <div
@ -152,6 +194,7 @@
{#if survey.direct === null} {#if survey.direct === null}
<SurveyAdmin <SurveyAdmin
{survey} {survey}
on:saved={updateSurvey}
/> />
{:else} {:else}
{#await req_questions} {#await req_questions}
@ -166,6 +209,27 @@
<tr> <tr>
<th> <th>
Question Question
{#if timer_end}
<div class="input-group input-group-sm float-end" style="max-width: 150px;">
<input
type="number"
class="form-control"
disabled
value={timer_remain}
>
<span class="input-group-text">ms</span>
</div>
{:else}
<div class="input-group input-group-sm float-end" style="max-width: 150px;">
<input
type="number"
class="form-control"
bind:value={timer}
placeholder="Valeur du timer"
>
<span class="input-group-text">ms</span>
</div>
{/if}
<button <button
type="button" type="button"
class="btn btn-sm btn-info ms-1" class="btn btn-sm btn-info ms-1"
@ -220,6 +284,14 @@
> >
<i class="bi bi-play-fill"></i> <i class="bi bi-play-fill"></i>
</button> </button>
<button
type="button"
class="btn btn-sm btn-danger"
disabled={question.id === current_question || !ws_up}
on:click={() => { ws.send('{"action":"new_question", "timer": ' + timer + ',"question":' + question.id + '}')} }
>
<i class="bi bi-stopwatch-fill"></i>
</button>
</td> </td>
</tr> </tr>
{/each} {/each}
@ -227,13 +299,12 @@
</table> </table>
</div> </div>
{/await} {/await}
{/if}
<hr> <hr>
<button <button
type="button" type="button"
class="btn btn-sm btn-info ms-1 float-end" class="btn btn-sm btn-info ms-1 float-end"
on:click={() => { ws.send('{"action":"get_asks"}'); asks = []; }} on:click={() => { ws.send('{"action":"get_asks", "value": ""}'); asks = []; }}
title="Rafraîchir les réponses" title="Rafraîchir les réponses"
> >
<i class="bi bi-arrow-counterclockwise"></i> <i class="bi bi-arrow-counterclockwise"></i>
@ -248,7 +319,15 @@
<i class="bi bi-arrow-counterclockwise"></i> <i class="bi bi-arrow-counterclockwise"></i>
<i class="bi bi-question-diamond"></i> <i class="bi bi-question-diamond"></i>
</button> </button>
<h3> <button
type="button"
class="btn btn-sm btn-success float-end"
title="Tout marqué comme répondu"
on:click={() => { ws.send('{"action":"mark_answered", "value": "all"}'); asks = [] }}
>
<i class="bi bi-check-all"></i>
</button>
<h3 id="questions_part">
Questions Questions
{#if asks.length} {#if asks.length}
<small class="text-muted"> <small class="text-muted">
@ -432,5 +511,6 @@
{/each} {/each}
</div> </div>
{/if} {/if}
{/if}
{/await} {/await}

View File

@ -28,6 +28,10 @@
let req_question; let req_question;
let nosend = false; let nosend = false;
let timer_init = null;
let timer_end = null;
let timer = 0;
let timer_cancel = null;
function afterQUpdate(q) { function afterQUpdate(q) {
value = undefined; value = undefined;
@ -46,6 +50,19 @@
} }
} }
function updTimer() {
const now = new Date().getTime();
if (now > timer_end) {
timer = 100;
clearInterval(timer_cancel);
timer_cancel = null;
} else {
const dist1 = timer_end - timer_init;
const dist2 = timer_end - now;
timer = Math.ceil(100-dist2*100/dist1);
}
}
function wsconnect() { function wsconnect() {
const ws = new WebSocket((window.location.protocol == 'https'?'wss://':'ws://') + window.location.host + `/api/surveys/${sid}/ws`); const ws = new WebSocket((window.location.protocol == 'https'?'wss://':'ws://') + window.location.host + `/api/surveys/${sid}/ws`);
@ -72,8 +89,25 @@
console.log(data); console.log(data);
if (data.action && data.action == "new_question") { if (data.action && data.action == "new_question") {
show_question = data.question; show_question = data.question;
if (timer_cancel) {
clearInterval(timer_cancel);
timer_cancel = null;
}
if (data.timer) {
timer_init = new Date().getTime();;
timer_end = timer_init + data.timer;
updTimer();
timer_cancel = setInterval(updTimer, 150);
} else {
timer_init = null;
}
} else { } else {
show_question = null; show_question = null;
if (timer_cancel) {
clearInterval(timer_cancel);
timer_cancel = null;
}
timer_init = null;
} }
}); });
} }
@ -153,12 +187,15 @@
<QuestionForm <QuestionForm
qid={show_question} qid={show_question}
{question} {question}
readonly={timer >= 100}
bind:value={value} bind:value={value}
on:change={sendValue} on:change={sendValue}
> >
<!--div class="progress" style="border-radius: 0; height: 4px"> {#if timer_init}
<div class="progress-bar" role="progressbar" style="width: 25%"></div> <div class="progress" style="border-radius: 0; height: 4px">
</div--> <div class="progress-bar" class:bg-warning={timer > 85 && timer < 100} class:bg-danger={timer >= 100} role="progressbar" style="width: {timer}%"></div>
</div>
{/if}
</QuestionForm> </QuestionForm>
{#if question.kind != 'mcq' && question.kind != 'ucq'} {#if question.kind != 'mcq' && question.kind != 'ucq'}
<button <button