Live: add timer questions
This commit is contained in:
parent
942875536b
commit
628c00b43c
40
direct.go
40
direct.go
@ -14,10 +14,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
WSClients = map[int64][]WSClient{}
|
OffsetQuestionTimer uint = 700
|
||||||
WSClientsMutex = sync.RWMutex{}
|
WSClients = map[int64][]WSClient{}
|
||||||
WSAdmin = []WSClient{}
|
WSClientsMutex = sync.RWMutex{}
|
||||||
WSAdminMutex = sync.RWMutex{}
|
WSAdmin = []WSClient{}
|
||||||
|
WSAdminMutex = sync.RWMutex{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -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) {
|
||||||
@ -271,7 +273,20 @@ func SurveyWSAdmin(w http.ResponseWriter, r *http.Request, ps httprouter.Params,
|
|||||||
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 {
|
} else {
|
||||||
survey.Direct = v.QuestionId
|
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 {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
1
main.go
1
main.go
@ -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(¤tPromo, "current-promo", currentPromo, "Year of the current promotion")
|
flag.UintVar(¤tPromo, "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()
|
||||||
|
|
||||||
|
@ -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,210 +299,218 @@
|
|||||||
</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>
|
||||||
<i class="bi bi-question-diamond"></i>
|
<i class="bi bi-question-diamond"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-light ms-1 float-end"
|
class="btn btn-sm btn-light ms-1 float-end"
|
||||||
on:click={() => { ws.send('{"action":"get_asks", "value": "unanswered"}'); asks = []; }}
|
on:click={() => { ws.send('{"action":"get_asks", "value": "unanswered"}'); asks = []; }}
|
||||||
title="Rafraîchir les réponses, en rapportant les réponses déjà répondues"
|
title="Rafraîchir les réponses, en rapportant les réponses déjà répondues"
|
||||||
>
|
>
|
||||||
<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
|
||||||
Questions
|
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
|
||||||
|
{#if asks.length}
|
||||||
|
<small class="text-muted">
|
||||||
|
{asks.length} question{#if asks.length > 1}s{/if}
|
||||||
|
</small>
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
{#if asks.length}
|
{#if asks.length}
|
||||||
<small class="text-muted">
|
{#each asks as ask (ask.id)}
|
||||||
{asks.length} question{#if asks.length > 1}s{/if}
|
<div class="card mb-3">
|
||||||
</small>
|
<div class="card-body">
|
||||||
{/if}
|
<p class="card-text">
|
||||||
</h3>
|
{ask.content}
|
||||||
{#if asks.length}
|
</p>
|
||||||
{#each asks as ask (ask.id)}
|
</div>
|
||||||
<div class="card mb-3">
|
<div class="card-footer">
|
||||||
<div class="card-body">
|
<button
|
||||||
<p class="card-text">
|
type="button"
|
||||||
{ask.content}
|
class="btn btn-sm btn-success float-end"
|
||||||
</p>
|
title="Marqué comme répondu"
|
||||||
</div>
|
on:click={() => { ws.send('{"action":"mark_answered", "question": ' + ask.id + '}'); asks = asks.filter((e) => e.id != ask.id) }}
|
||||||
<div class="card-footer">
|
>
|
||||||
<button
|
<i class="bi bi-check"></i>
|
||||||
type="button"
|
</button>
|
||||||
class="btn btn-sm btn-success float-end"
|
Par
|
||||||
title="Marqué comme répondu"
|
<a href="users/{ask.userid}" target="_blank">
|
||||||
on:click={() => { ws.send('{"action":"mark_answered", "question": ' + ask.id + '}'); asks = asks.filter((e) => e.id != ask.id) }}
|
{#if users && users[ask.userid]}
|
||||||
>
|
{users[ask.userid].login}
|
||||||
<i class="bi bi-check"></i>
|
{:else}
|
||||||
</button>
|
{ask.userid}
|
||||||
Par
|
{/if}
|
||||||
<a href="users/{ask.userid}" target="_blank">
|
</a>
|
||||||
{#if users && users[ask.userid]}
|
|
||||||
{users[ask.userid].login}
|
|
||||||
{:else}
|
|
||||||
{ask.userid}
|
|
||||||
{/if}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{:else}
|
|
||||||
<div class="text-center text-muted">
|
|
||||||
Pas de question pour l'instant.
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-info ms-1 float-end"
|
|
||||||
on:click={() => { ws.send('{"action":"get_responses"}') }}
|
|
||||||
title="Rafraîchir les réponses"
|
|
||||||
>
|
|
||||||
<i class="bi bi-arrow-counterclockwise"></i>
|
|
||||||
<i class="bi bi-card-checklist"></i>
|
|
||||||
</button>
|
|
||||||
<h3>
|
|
||||||
Réponses
|
|
||||||
</h3>
|
|
||||||
{#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}
|
|
||||||
<h4 id="q{question.id}_res">
|
|
||||||
{question.title}
|
|
||||||
</h4>
|
|
||||||
{#if question.kind == 'ucq'}
|
|
||||||
{#await question.getProposals()}
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
|
||||||
<span>Chargement des propositions …</span>
|
|
||||||
</div>
|
|
||||||
{:then proposals}
|
|
||||||
<div class="card mb-4">
|
|
||||||
<table class="table table-sm table-striped table-hover mb-0">
|
|
||||||
<tbody>
|
|
||||||
{#each proposals as proposal (proposal.id)}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{proposal.label}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{responsesbyid[q].filter((e) => e == proposal.id.toString()).length}/{responsesbyid[q].length}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{Math.trunc(responsesbyid[q].filter((e) => e == proposal.id.toString()).length / responsesbyid[q].length * 1000)/10} %
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
{:else if question.kind == 'mcq'}
|
|
||||||
{#await question.getProposals()}
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
|
||||||
<span>Chargement des propositions …</span>
|
|
||||||
</div>
|
|
||||||
{:then proposals}
|
|
||||||
<div class="card mb-4">
|
|
||||||
<table class="table table-sm table-striped table-hover mb-0">
|
|
||||||
<tbody>
|
|
||||||
{#each proposals as proposal (proposal.id)}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{proposal.label}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length}/{responsesbyid[q].length}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{Math.trunc(responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length / responsesbyid[q].length * 1000)/10} %
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
{:else}
|
|
||||||
<div class="card mb-4">
|
|
||||||
<ul class="list-group list-group-flush">
|
|
||||||
{#each Object.keys(responses[q]) as user, rid (rid)}
|
|
||||||
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
|
||||||
<span>
|
|
||||||
{responses[q][user]}
|
|
||||||
</span>
|
|
||||||
<a href="users/{user}" target="_blank" class="badge bg-dark rounded-pill">
|
|
||||||
{#if users && users[user]}
|
|
||||||
{users[user].login}
|
|
||||||
{:else}
|
|
||||||
{user}
|
|
||||||
{/if}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
{/await}
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-info ms-1 float-end"
|
|
||||||
on:click={() => { ws.send('{"action":"get_stats"}') }}
|
|
||||||
title="Rafraîchir les stats"
|
|
||||||
>
|
|
||||||
<i class="bi bi-arrow-counterclockwise"></i>
|
|
||||||
<i class="bi bi-123"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-primary ms-1 float-end"
|
|
||||||
title="Rafraîchir la liste des utilisateurs"
|
|
||||||
on:click={updateUsers}
|
|
||||||
>
|
|
||||||
<i class="bi bi-arrow-clockwise"></i>
|
|
||||||
<i class="bi bi-people"></i>
|
|
||||||
</button>
|
|
||||||
<h3>
|
|
||||||
Connectés
|
|
||||||
{#if wsstats}
|
|
||||||
<small class="text-muted">{wsstats.nb_clients} utilisateurs</small>
|
|
||||||
{/if}
|
|
||||||
</h3>
|
|
||||||
{#if wsstats}
|
|
||||||
<div class="row row-cols-5 py-3">
|
|
||||||
{#each wsstats.users as login, lid (lid)}
|
|
||||||
<div class="col">
|
|
||||||
<div class="card">
|
|
||||||
<img alt="{login}" src="//photos.cri.epita.fr/thumb/{login}" class="card-img-top">
|
|
||||||
<div class="card-footer text-center text-truncate p-0">
|
|
||||||
<a href="users/{login}" target="_blank">
|
|
||||||
{login}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
{:else}
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
Pas de question pour l'instant.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-info ms-1 float-end"
|
||||||
|
on:click={() => { ws.send('{"action":"get_responses"}') }}
|
||||||
|
title="Rafraîchir les réponses"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i>
|
||||||
|
<i class="bi bi-card-checklist"></i>
|
||||||
|
</button>
|
||||||
|
<h3>
|
||||||
|
Réponses
|
||||||
|
</h3>
|
||||||
|
{#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}
|
||||||
|
<h4 id="q{question.id}_res">
|
||||||
|
{question.title}
|
||||||
|
</h4>
|
||||||
|
{#if question.kind == 'ucq'}
|
||||||
|
{#await question.getProposals()}
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||||
|
<span>Chargement des propositions …</span>
|
||||||
|
</div>
|
||||||
|
{:then proposals}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<table class="table table-sm table-striped table-hover mb-0">
|
||||||
|
<tbody>
|
||||||
|
{#each proposals as proposal (proposal.id)}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{proposal.label}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{responsesbyid[q].filter((e) => e == proposal.id.toString()).length}/{responsesbyid[q].length}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{Math.trunc(responsesbyid[q].filter((e) => e == proposal.id.toString()).length / responsesbyid[q].length * 1000)/10} %
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{:else if question.kind == 'mcq'}
|
||||||
|
{#await question.getProposals()}
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||||
|
<span>Chargement des propositions …</span>
|
||||||
|
</div>
|
||||||
|
{:then proposals}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<table class="table table-sm table-striped table-hover mb-0">
|
||||||
|
<tbody>
|
||||||
|
{#each proposals as proposal (proposal.id)}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{proposal.label}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length}/{responsesbyid[q].length}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{Math.trunc(responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length / responsesbyid[q].length * 1000)/10} %
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{#each Object.keys(responses[q]) as user, rid (rid)}
|
||||||
|
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<span>
|
||||||
|
{responses[q][user]}
|
||||||
|
</span>
|
||||||
|
<a href="users/{user}" target="_blank" class="badge bg-dark rounded-pill">
|
||||||
|
{#if users && users[user]}
|
||||||
|
{users[user].login}
|
||||||
|
{:else}
|
||||||
|
{user}
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-info ms-1 float-end"
|
||||||
|
on:click={() => { ws.send('{"action":"get_stats"}') }}
|
||||||
|
title="Rafraîchir les stats"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i>
|
||||||
|
<i class="bi bi-123"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-primary ms-1 float-end"
|
||||||
|
title="Rafraîchir la liste des utilisateurs"
|
||||||
|
on:click={updateUsers}
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-clockwise"></i>
|
||||||
|
<i class="bi bi-people"></i>
|
||||||
|
</button>
|
||||||
|
<h3>
|
||||||
|
Connectés
|
||||||
|
{#if wsstats}
|
||||||
|
<small class="text-muted">{wsstats.nb_clients} utilisateurs</small>
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
{#if wsstats}
|
||||||
|
<div class="row row-cols-5 py-3">
|
||||||
|
{#each wsstats.users as login, lid (lid)}
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<img alt="{login}" src="//photos.cri.epita.fr/thumb/{login}" class="card-img-top">
|
||||||
|
<div class="card-footer text-center text-truncate p-0">
|
||||||
|
<a href="users/{login}" target="_blank">
|
||||||
|
{login}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{/await}
|
{/await}
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user