Working on live surveys

This commit is contained in:
nemunaire 2022-02-28 19:00:30 +01:00
parent 567e170cfd
commit a57e032e59
19 changed files with 1014 additions and 48 deletions

View File

@ -52,6 +52,7 @@
{/if}
{/if}
</QuestionHeader>
<slot></slot>
<div class="card-body">
{#if false && response_history}
<div class="d-flex justify-content-end mb-2">
@ -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}
<p class="card-text alert alert-secondary" style="white-space: pre-line">{value}</p>
{:else if question.kind == 'int'}
<input class="ml-5 col-sm-2 form-control" type="number" bind:value={value} placeholder={question.placeholder}>
<input
class="ml-5 col-sm-2 form-control"
type="number"
bind:value={value}
placeholder={question.placeholder}
on:change={() => { dispatch("change"); }}
>
{:else}
<textarea class="form-control" rows="6" bind:value={value} placeholder={question.placeholder}></textarea>
<textarea
class="form-control"
rows="6"
bind:value={value}
placeholder={question.placeholder}
on:change={() => { dispatch("change"); }}
></textarea>
{/if}
{#if survey.corrected}
{#if survey && survey.corrected}
<ResponseCorrected
response={response_history}
{survey}

View File

@ -1,21 +0,0 @@
<script>
export let proposal = null;
export let kind = 'mcq';
export let value;
let inputType = 'checkbox';
$: {
switch(kind) {
case 'mcq':
inputType = 'checkbox';
break;
default:
inputType = 'radio';
}
}
</script>
<input
type={inputType}
bind:value={value}
{JSON.stringify(proposal)}

View File

@ -1,4 +1,6 @@
<script>
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}
<input
@ -46,6 +50,7 @@
id={prefixid + 'p' + proposal.id}
bind:group={value}
value={proposal.id.toString()}
on:change={() => { dispatch("change"); }}
>
{/if}
{#if edit}

View File

@ -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 @@
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="direct" class="col-form-label col-form-label-sm">Question en direct</label>
</div>
<div class="col-sm-8">
{#await getQuestions(survey.id) then questions}
<select id="direct" class="form-select form-select-sm" bind:value={survey.direct}>
<option value={null}>Pas de direct</option>
<option value={0}>Pause</option>
{#each questions as question (question.id)}
<option value={question.id}>{question.id} - {question.title}</option>
{/each}
</select>
{/await}
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="start_availability" class="col-form-label col-form-label-sm">Date de début</label>

View File

@ -4,7 +4,8 @@
export { className as class };
</script>
{#if survey.startAvailability() > Date.now()}<span class="badge bg-info {className}">Prévu</span>>
{#if survey.direct}<span class="badge bg-danger {className}">Direct</span>
{:else if survey.startAvailability() > Date.now()}<span class="badge bg-info {className}">Prévu</span>
{:else if survey.endAvailability() > Date.now()}<span class="badge bg-warning {className}">En cours</span>
{:else if !survey.corrected}<span class="badge bg-primary text-light {className}">Terminé</span>
{:else}<span class="badge bg-success {className}">Corrigé</span>

View File

@ -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;
}
}
});
</script>
<table class="table table-striped table-hover mb-0">
@ -18,7 +29,7 @@
{/if}
</tr>
</thead>
{#await getSurveys()}
{#await req_surveys}
<tr>
<td colspan="5" class="text-center py-3">
<div class="spinner-border mx-3" role="status"></div>
@ -36,7 +47,7 @@
</th>
</tr>
{/if}
<tr on:click={e => goto($user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}>
<tr on:click={e => goto(survey.direct?`surveys/${survey.id}/live`:$user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}>
<td>
{survey.title}
<SurveyBadge {survey} class="float-end" />

View File

@ -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;

View File

@ -2,6 +2,8 @@
import { user } from '../stores/user';
import SurveyList from '../components/SurveyList.svelte';
import ValidateSubmissions from '../components/ValidateSubmissions.svelte';
let direct = null;
</script>
<div class="card bg-light">
@ -20,6 +22,12 @@
</div>
{/if}
{#if direct}
<div class="alert alert-warning" role="alert">
<strong>Rejoins le cours maintenant&nbsp;!</strong> Il y a actuellement un questionnaire en direct&nbsp;: {direct.title}. <a href="surveys/{direct.id}/live">Clique ici pour le rejoindre</a>.
</div>
{/if}
<p class="lead">Tu as fait les rendus suivants&nbsp;:</p>
</div>
<div class="d-none d-md-block col-md-auto">
@ -38,6 +46,6 @@
Vous devez <a href="auth/CRI" target="_self">vous identifier</a> pour accéder au contenu.
</p>
{/if}
<SurveyList />
<SurveyList bind:direct={direct} />
</div>
</div>

View File

@ -0,0 +1,370 @@
<script context="module">
export async function load({ params, stuff }) {
return {
props: {
surveyP: stuff.survey,
sid: params.sid,
},
};
}
</script>
<script>
import { user } from '../../../stores/user';
import SurveyAdmin from '../../../components/SurveyAdmin.svelte';
import SurveyBadge from '../../../components/SurveyBadge.svelte';
import { getQuestions } from '../../../lib/questions';
import { getUsers } from '../../../lib/users';
export let surveyP;
export let sid;
let survey;
let req_questions;
surveyP.then((s) => {
survey = s;
updateQuestions();
if (survey.direct !== null) {
wsconnect();
}
});
function updateQuestions() {
req_questions = getQuestions(survey.id);
}
let ws = null;
let ws_up = false;
let wsstats = null;
let current_question = null;
let responses = {};
let users = {};
function updateUsers() {
getUsers().then((usr) => {
const tmp = { };
for (const u of usr) {
tmp[u.id.toString()] = u;
}
users = tmp;
});
}
updateUsers();
let responsesbyid = { };
$: {
const tmp = { };
for (const response in responses) {
if (!tmp[response]) tmp[response] = [];
for (const r in responses[response]) {
tmp[response].push(responses[response][r]);
}
}
responsesbyid = tmp;
}
function wsconnect() {
if (ws !== null) return;
ws = new WebSocket((window.location.protocol == 'https'?'wss://':'ws://') + window.location.host + `/api/surveys/${sid}/ws-admin`);
ws.addEventListener("open", () => {
ws_up = true;
ws.send('{"action":"get_responses"}');
ws.send('{"action":"get_stats"}');
});
ws.addEventListener("close", (e) => {
ws_up = false;
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);
ws = null;
setTimeout(function() {
wsconnect();
}, 1500);
});
ws.addEventListener("error", (err) => {
ws_up = false;
console.log('Socket closed due to error.', err.message);
ws = null;
});
ws.addEventListener("message", (message) => {
const data = JSON.parse(message.data);
console.log(data);
if (data.action && data.action == "new_question") {
current_question = data.question;
} else if (data.action && data.action == "stats") {
wsstats = data.stats;
} else if (data.action && data.action == "new_response") {
if (!responses[data.question]) responses[data.question] = {};
responses[data.question][data.user] = data.value;
} else {
current_question = null;
}
});
}
</script>
{#await surveyP then survey}
{#if $user && $user.is_admin}
{#if survey.direct !== null}
<a href="surveys/{survey.id}/live" class="btn btn-danger ms-1 float-end" title="Aller au direct"><i class="bi bi-film"></i></a>
<button
type="button"
class="btn btn-primary ms-1 float-end"
on:click={() => { if (confirm("Sûr ?")) ws.send('{"action":"end"}') }}
>
Terminer
</button>
{/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>
{/if}
<div class="d-flex align-items-center">
<h2>
<a href="surveys/" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
<small class="text-muted">
Administration
</small>
</h2>
{#if survey.direct !== null}
<div
class="badge rounded-pill ms-2"
class:bg-success={ws_up}
class:bg-danger={!ws_up}
>
{#if ws_up}Connecté{:else}Déconnecté{/if}
</div>
{:else}
<SurveyBadge
class="mx-2"
{survey}
/>
{/if}
</div>
{#if survey.direct === null}
<SurveyAdmin
{survey}
/>
{:else}
{#await req_questions}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des questions &hellip;</span>
</div>
{:then questions}
<div class="card my-3">
<table class="table table-hover table-striped mb-0">
<thead>
<tr>
<th>
Question
<button
type="button"
class="btn btn-sm btn-info ms-1"
on:click={updateQuestions}
title="Rafraîchir les questions"
>
<i class="bi bi-arrow-counterclockwise"></i>
</button>
</th>
<th>
Réponses
</th>
<th>
Actions
<button
type="button"
class="btn btn-sm btn-primary"
disabled={!current_question || !ws_up}
on:click={() => { ws.send('{"action":"pause"}')} }
>
Pause
</button>
</th>
</tr>
</thead>
<tbody>
{#each questions as question (question.id)}
<tr>
<td>
{#if responses[question.id]}
<a href="surveys/{sid}/admin#q{question.id}_res">
{question.title}
</a>
{:else}
{question.title}
{/if}
</td>
<td>
{#if responses[question.id]}
{Object.keys(responses[question.id]).length}
{:else}
0
{/if}
{#if wsstats}/ {wsstats.nb_clients}{/if}
</td>
<td>
<button
type="button"
class="btn btn-sm btn-primary"
disabled={question.id === current_question || !ws_up}
on:click={() => { ws.send('{"action":"new_question", "question":' + question.id + '}')} }
>
Lancer cette question
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/await}
{/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 &hellip;</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}&nbsp;%
</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 &hellip;</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}&nbsp;%
</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}
{/await}

View File

@ -11,6 +11,8 @@
</script>
<script lang="ts">
import { goto } from '$app/navigation';
import { user } from '../../../stores/user';
import SurveyAdmin from '../../../components/SurveyAdmin.svelte';
import SurveyBadge from '../../../components/SurveyBadge.svelte';
@ -20,13 +22,26 @@
export let surveyP;
$: {
if (surveyP) {
surveyP.then((survey) => {
if (survey.direct && !$user.is_admin) {
goto(`surveys/${survey.id}/live`);
}
})
}
}
let edit = false;
</script>
{#await surveyP then survey}
{#if $user && $user.is_admin}
<button class="btn btn-primary ms-1 float-end" on:click={() => { edit = !edit; } } title="Éditer" ng-if=" && !edit"><i class="bi bi-pencil"></i></button>
<a href="surveys/{survey.id}/responses" class="btn btn-success ms-1 float-end" title="Voir les réponses" ng-if="user.is_admin"><i class="bi bi-files"></i></a>
<button class="btn btn-primary ms-1 float-end" on:click={() => { edit = !edit; } } title="Éditer"><i class="bi bi-pencil"></i></button>
<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>
{#if survey.direct}
<a href="surveys/{survey.id}/live" class="btn btn-danger ms-1 float-end" title="Aller au direct"><i class="bi bi-film"></i></a>
{/if}
{/if}
<div class="d-flex align-items-center">
<h2>

View File

@ -0,0 +1,118 @@
<script context="module">
export async function load({ params, stuff }) {
return {
props: {
surveyP: stuff.survey,
sid: params.sid,
},
};
}
</script>
<script>
import { user } from '../../../stores/user';
import SurveyBadge from '../../../components/SurveyBadge.svelte';
import QuestionForm from '../../../components/QuestionForm.svelte';
import { getQuestion } from '../../../lib/questions';
export let surveyP;
export let sid;
let survey;
surveyP.then((s) => survey = s);
let ws_up = false;
let show_question = null;
let value;
function wsconnect() {
const ws = new WebSocket((window.location.protocol == 'https'?'wss://':'ws://') + window.location.host + `/api/surveys/${sid}/ws`);
ws.addEventListener("open", () => {
ws_up = true;
});
ws.addEventListener("close", (e) => {
ws_up = false;
show_question = false;
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);
setTimeout(function() {
wsconnect();
}, 1500);
});
ws.addEventListener("error", (err) => {
ws_up = false;
console.log('Socket closed due to error.', err.message);
});
ws.addEventListener("message", (message) => {
const data = JSON.parse(message.data);
console.log(data);
if (data.action && data.action == "new_question") {
show_question = data.question;
} else {
show_question = null;
}
});
}
wsconnect();
function sendValue() {
if (show_question && value) {
survey.submitAnswers([{"id_question": show_question, "value": value}], $user.id_user).then((response) => {
console.log("Vos réponses ont bien étés sauvegardées.");
}, (error) => {
value = null;
console.log("Une erreur s'est produite durant l'envoi de vos réponses : " + error + "<br>Veuillez réessayer dans quelques instants.");
});
}
}
</script>
{#await surveyP then survey}
{#if $user && $user.is_admin}
<a href="surveys/{survey.id}/admin" class="btn btn-primary ms-1 float-end" title="Aller à l'interface d'administration"><i class="bi bi-pencil"></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>
{/if}
<div class="d-flex align-items-center mb-5">
<h2>
<a href="surveys/" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
</h2>
<div
class="badge rounded-pill ms-2"
class:bg-success={ws_up}
class:bg-danger={!ws_up}
>
{#if ws_up}Connecté{:else}Déconnecté{/if}
</div>
</div>
<form on:submit|preventDefault={sendValue}>
{#if show_question}
{#await getQuestion(show_question)}
Please wait
{:then question}
<QuestionForm
qid={show_question}
{question}
bind:value={value}
on:change={sendValue}
>
<!--div class="progress" style="border-radius: 0; height: 4px">
<div class="progress-bar" role="progressbar" style="width: 25%"></div>
</div-->
</QuestionForm>
{/await}
{:else if ws_up}
<h2 class="text-center">
Pas de question actuellement
</h2>
{:else}
<h2 class="text-center">
La session est terminée. <small class="text-muted">On se retrouve une prochaine fois&hellip;</small>
</h2>
{/if}
</form>
{/await}

1
db.go
View File

@ -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

358
direct.go Normal file
View File

@ -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
}
}
}

1
go.mod
View File

@ -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
)

26
go.sum
View File

@ -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=

View File

@ -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)
}
}

View File

@ -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}

View File

@ -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})
}
}
}

View File

@ -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