Compare commits

..

No commits in common. "31c807c83b51a382f4a991793d0dea96b0ae3cf2" and "d7d7bfa1035479cf54c9d7d37102960a8098768b" have entirely different histories.

74 changed files with 114 additions and 6371 deletions

View file

@ -19,7 +19,7 @@ steps:
commands: commands:
- apk --no-cache add build-base - apk --no-cache add build-base
- go get -v - go get -v
- go build -v -ldflags="-s -w" - go build -v
environment: environment:
GOARM: 7 GOARM: 7
@ -55,7 +55,7 @@ steps:
commands: commands:
- apk --no-cache add build-base - apk --no-cache add build-base
- go get -v - go get -v
- go build -v -ldflags="-s -w" - go build -v
- name: publish - name: publish
image: plugins/docker image: plugins/docker

View file

@ -9,7 +9,7 @@ COPY htdocs/ ./htdocs/
RUN go generate -v && \ RUN go generate -v && \
go get -d -v && \ go get -d -v && \
go build -v -ldflags="-s -w" go build -v
FROM alpine FROM alpine

View file

@ -1,20 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2019
},
env: {
browser: true,
es2017: true,
node: true
}
};

5
atsebayt/.gitignore vendored
View file

@ -1,5 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package

View file

@ -1,6 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}

View file

@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte);
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm init svelte@next
# create a new project in my-app
npm init svelte@next my-app
```
> Note: the `@next` is temporary
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then:
```bash
npm run build
```
> You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production.

File diff suppressed because it is too large Load diff

View file

@ -1,29 +0,0 @@
{
"name": "atsebayt",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"preview": "svelte-kit preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
},
"devDependencies": {
"@sveltejs/kit": "^1.0.0-next.266",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^3.4.0",
"prettier": "^2.5.1",
"prettier-plugin-svelte": "^2.6.0",
"svelte": "^3.46.4",
"svelte-check": "^2.4.3",
"svelte-preprocess": "^4.10.3",
"tslib": "^2.3.1",
"typescript": "^4.5.5"
},
"type": "module"
}

View file

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="/">
<link href="css/bootstrap.min.css" type="text/css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
<script async defer data-website-id="7f459249-ea9f-43a6-a0d9-60bd9fa86c16" src="https://pythagore.p0m.fr/pythagore.js"></script>
%svelte.head%
</head>
<body>
<div style="position: fixed; bottom: 20px; right: 20px; z-index: -1; background-image: url('img/srstamps.png'); background-size: cover; width: 125px; height: 125px;">
</div>
<div id="svelte">%svelte.body%</div>
</body>
</html>

View file

@ -1,47 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte';
import CorrectionResponses from './CorrectionResponses.svelte';
export let question = null;
export let cts = null;
export let child = null;
export let filter = "";
export let notCorrected = false;
export let showStudent = false;
export let templates = [];
const dispatch = createEventDispatcher();
</script>
{#if question && (question.kind == 'mcq' || question.kind == 'ucq')}
{#await question.getProposals()}
<div class="text-center mt-4">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Récupération des propositions&hellip;</span>
</div>
{:then proposals}
<CorrectionResponses
{cts}
bind:this={child}
{filter}
{question}
{notCorrected}
{proposals}
{showStudent}
{templates}
on:nb_responses={(v) => dispatch('nb_responses', v.detail)}
/>
{/await}
{:else}
<CorrectionResponses
{cts}
bind:this={child}
{filter}
{question}
{notCorrected}
{showStudent}
{templates}
on:nb_responses={(v) => dispatch('nb_responses', v.detail)}
/>
{/if}

View file

@ -1,112 +0,0 @@
<script>
import { CorrectionTemplate } from '../lib/correctionTemplates';
let className = '';
export { className as class };
export let cts = null;
export let nb_responses = 0;
export let question = null;
export let templates = [];
export let filter = "";
function addTemplate() {
const ct = new CorrectionTemplate()
if (question) {
ct.id_question = question.id;
}
templates.push(ct);
templates = templates;
}
function delTemplate(tpl) {
tpl.delete().then(() => {
const idx = templates.findIndex((e) => e.id === tpl.id);
if (idx >= 0) {
templates.splice(idx, 1);
}
templates = templates;
});
}
function submitTemplate(tpl) {
tpl.save().then(() => {
templates = templates;
});
}
</script>
<div class="{className}">
{#each templates as template (template.id)}
<form class="row mb-2" on:submit|preventDefault={() => submitTemplate(template)}>
<div class="col-2">
<div class="input-group">
<input
placeholder="RegExp"
class="form-control"
class:bg-warning={template.regexp && template.regexp === filter}
bind:value={template.regexp}
>
<button
type="button"
class="btn btn-sm"
class:btn-outline-secondary={!template.regexp || template.regexp !== filter}
class:btn-outline-warning={template.regexp && template.regexp === filter}
on:click={() => { if (filter == template.regexp) filter = ''; else filter = template.regexp; } }
>
<i class="bi bi-filter"></i>
</button>
</div>
</div>
<div class="col-2">
<input placeholder="Intitulé" class="form-control" bind:value={template.label}>
</div>
<div class="col-1">
<input
type="number"
placeholder="-12"
class="form-control"
bind:value={template.score}
>
</div>
<div class="col">
<textarea
placeholder="Explication pour l'étudiant"
class="form-control form-control-sm"
bind:value={template.score_explaination}
></textarea>
</div>
<div class="col-1 d-flex flex-column">
<div class="text-end">
{#if cts && template.id && cts[template.id.toString()]}
{Math.trunc(Object.keys(cts[template.id.toString()]).length/nb_responses*1000)/10}&nbsp;%
{:else}
N/A
{/if}
</div>
<div class="d-flex justify-content-between">
<button
type="button"
class="btn btn-sm btn-danger"
on:click={() => delTemplate(template)}
>
<i class="bi bi-trash"></i>
</button>
<button
class="btn btn-sm btn-success"
>
<i class="bi bi-check"></i>
</button>
</div>
</div>
</form>
{/each}
<button
type="button"
class="btn btn-info me-1"
on:click={addTemplate}
disabled={templates.length > 0 && !templates[templates.length-1].id}
>
<i class="bi bi-plus"></i> Ajouter un template
</button>
</div>

View file

@ -1,124 +0,0 @@
<script>
import { user } from '../stores/user';
import { autoCorrection } from '../lib/correctionTemplates';
export let cts = null;
export let rid = 0;
export let response = null;
export let templates = [];
let my_tpls = { };
let my_correction = null;
function submitCorrection() {
if (response.score === undefined || response.score === null) {
if (my_correction && my_correction.score !== undefined) {
response.score = my_correction.score;
} else {
response.score = 100;
}
}
if (response.score_explaination === undefined || response.score_explaination === null) {
if (my_correction && my_correction.score_explaination !== undefined) {
response.score_explaination = my_correction.score_explaination;
}
}
response.id_corrector = $user.id
response.time_scored = (new Date()).toISOString()
response.save().then((res) => {
});
}
$: {
if (cts && templates && response && response.id_user) {
for (const t of templates) {
if (my_tpls[t.id] === undefined && cts[t.id.toString()]) {
my_tpls[t.id] = cts[t.id.toString()][response.id_user] !== undefined;
}
}
}
}
</script>
<form
class="row"
on:submit|preventDefault={submitCorrection}
>
<div class="col-auto">
<button
class="btn btn-success me-1"
class:mt-4={rid%2}
>
<i class="bi bi-check"></i>
</button>
</div>
<div class="col-7">
<div class="row row-cols-3">
{#each templates as template (template.id)}
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="r{response.id}t{template.id}"
on:change={() => {my_tpls[template.id] = !my_tpls[template.id]; autoCorrection(response.id_user, my_tpls).then((r) => my_correction = r); }}
checked={my_tpls[template.id]}
>
<label
class="form-check-label"
for="r{response.id}t{template.id}"
>
{template.label}
</label>
</div>
{/each}
</div>
</div>
<div class="col">
<div class="input-group mb-2">
<input
type="number"
class="form-control"
placeholder="Score"
bind:value={response.score}
>
{#if my_correction}
<button
type="button"
class="btn btn-light"
on:click={() => { response.score = my_correction.score; response.score_explaination = my_correction.score_explaination; }}
>
{my_correction.score}
</button>
{/if}
<span class="input-group-text">/100</span>
</div>
<textarea
class="form-control mb-2"
placeholder="Appréciation"
bind:value={response.score_explaination}
></textarea>
</div>
</form>
{#if my_correction}
<div
class="alert row mt-1 mb-0"
class:bg-success={my_correction.score > 100}
class:alert-success={my_correction.score >= 95 && my_correction.score <= 100}
class:alert-info={my_correction.score < 95 && my_correction.score >= 70}
class:alert-warning={my_correction.score < 70 && my_correction.score >= 45}
class:alert-danger={my_correction.score < 45}
>
<strong class="col-auto">
{my_correction.score}&nbsp;%
</strong>
<div class="col">
{my_correction.score_explaination}
</div>
</div>
{/if}

View file

@ -1,130 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte';
import QuestionProposals from './QuestionProposals.svelte';
import ResponseCorrected from './ResponseCorrected.svelte';
import CorrectionResponseFooter from './CorrectionResponseFooter.svelte';
import { autoCorrection } from '../lib/correctionTemplates';
import { getUser } from '../lib/users';
export let cts = null;
export let filter = "";
export let hilights = "";
export let question = null;
export let proposals = null;
export let notCorrected = false;
export let showStudent = false;
export let templates = false;
function refreshResponses() {
let req = question.getResponses();
req.then((res) => {
responses = res;
dispatch('nb_responses', res.length);
});
return req;
}
const dispatch = createEventDispatcher();
let req_responses = refreshResponses();
let responses = [];
let filteredResponses = [];
$:{
filteredResponses = responses.filter((r) => (notCorrected || !r.time_scored) && (!filter || r.value.match(filter)));
}
export async function applyCorrections() {
for (const r of filteredResponses) {
const my_correction = { };
for (const tpl of templates) {
if (!tpl.regexp) continue;
if (r.value.match(tpl.regexp)) {
my_correction[tpl.id] = true;
} else {
my_correction[tpl.id] = false;
}
}
const auto = await autoCorrection(r.id_user, my_correction);
r.score = auto.score;
r.score_explaination = auto.score_explaination;
await r.save();
}
req_responses = refreshResponses();
}
</script>
{#await req_responses}
<div class="text-center mt-4">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Récupération des réponses&hellip;</span>
</div>
{:then}
{#each filteredResponses as response, rid (response.id)}
<div class="row">
<div class="col">
<div class="card mt-3">
<div class="card-body">
{#if question.kind == 'mcq' || question.kind == 'ucq'}
{#if !proposals}
<div class="alert bg-danger">
Une erreur s'est produite, aucune proposition n'a été chargée
</div>
{:else}
<QuestionProposals
kind={question.kind}
prefixid={'r' + response.id}
{proposals}
readonly
value={response.value}
/>
{/if}
{:else}
<p
class="card-text"
style="white-space: pre-line"
>
{response.value}
</p>
{/if}
<ResponseCorrected
{response}
/>
</div>
<div class="card-footer">
<CorrectionResponseFooter
{cts}
{rid}
bind:response={response}
{templates}
/>
</div>
</div>
</div>
{#if showStudent}
<div class="col-auto">
<div class="text-center mt-2" style="max-width: 110px">
{#await getUser(response.id_user)}
<div class="spinner-border text-primary mx-3" role="status"></div>
{:then user}
<a href="/users/{user.login}">
<img class="img-thumbnail" src="https://photos.cri.epita.fr/thumb/{user.login}" alt="avatar {user.login}">
<div
class="text-truncate"
title={user.login}
>
{user.login}
</div>
</a>
{/await}
</div>
</div>
{/if}
</div>
{/each}
{/await}

View file

@ -1,17 +0,0 @@
<script>
export let date;
export let dateStyle = "long";
export let timeStyle = "long";
function formatDate(input, dateStyle, timeStyle) {
if (typeof input === 'string') {
input = new Date(input);
}
return new Intl.DateTimeFormat(undefined, {
dateStyle,
timeStyle,
}).format(input);
}
</script>
{formatDate(date, dateStyle, timeStyle)}

View file

@ -1,170 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte';
import QuestionHeader from './QuestionHeader.svelte';
import QuestionProposals from './QuestionProposals.svelte';
import ResponseCorrected from './ResponseCorrected.svelte';
import { user } from '../stores/user';
const dispatch = createEventDispatcher();
let className = '';
export { className as class };
export let question;
export let qid;
export let response_history = null;
export let readonly = false;
export let survey = null;
export let value = "";
export let edit = false;
function saveQuestion() {
question.save().then((response) => {
question.description = response.description;
question = question;
edit = false;
})
}
function editQuestion() {
edit = true;
}
</script>
<div class="card my-3 {className}">
<QuestionHeader
bind:question={question}
{qid}
{edit}
>
{#if $user && $user.is_admin}
<button class="btn btn-sm btn-danger ms-1 float-end" on:click={() => dispatch('delete')}>
<i class="bi bi-trash-fill"></i>
</button>
{#if edit}
<button class="btn btn-sm btn-success ms-1 float-end" on:click={saveQuestion}>
<i class="bi bi-check"></i>
</button>
{:else}
<button class="btn btn-sm btn-primary ms-1 float-end" on:click={editQuestion}>
<i class="bi bi-pencil"></i>
</button>
{/if}
{/if}
</QuestionHeader>
<slot></slot>
<div class="card-body">
{#if false && response_history}
<div class="d-flex justify-content-end mb-2">
<div class="col-auto">
Historique&nbsp;:
<select class="form-select">
<option value="new">Actuel</option>
{#each response_history as history (history.id)}
<option value={history.id}>{new Intl.DateTimeFormat('default', { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric'}).format(new Date(history.time_submit))}</option>
{/each}
</select>
</div>
</div>
{/if}
{#if edit}
{#if question.kind == 'text' || question.kind == 'int'}
<div class="form-group row">
<label class="col-2 col-form-label" for="q{qid}placeholder">Placeholder</label>
<div class="col">
<input class="form-control" id="q{qid}placeholder" bind:value={question.placeholder}>
</div>
</div>
{:else}
{#await question.getProposals()}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des choix &hellip;</span>
</div>
{:then proposals}
<QuestionProposals
edit
id_question={question.id}
kind={question.kind}
{proposals}
readonly
bind:value={value}
on:change={() => { dispatch("change"); }}
/>
{/await}
{/if}
{:else if question.kind == 'mcq' || question.kind == 'ucq'}
{#await question.getProposals()}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des choix &hellip;</span>
</div>
{:then proposals}
<QuestionProposals
kind={question.kind}
{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}
on:change={() => { dispatch("change"); }}
>
{:else}
<textarea
class="form-control"
rows="6"
bind:value={value}
placeholder={question.placeholder}
on:change={() => { dispatch("change"); }}
></textarea>
{/if}
{#if survey && survey.corrected}
<ResponseCorrected
response={response_history}
{survey}
/>
{/if}
{#if false}
<div ng-controller="ProposalsController" ng-if="question.kind == 'ucq' || question.kind == 'mcq'">
<div class="form-group form-check" ng-if="!question.edit && question.kind == 'mcq'" ng-repeat="proposal in proposals">
<input type="checkbox" class="form-check-input" id="p{proposal.id}" ng-model="question['p' + proposal.id]" disabled={readonly}>
<label class="form-check-label" for="p{proposal.id}">{proposal.label}</label>
</div>
<div class="form-group form-check" ng-if="!question.edit && question.kind == 'ucq'" ng-repeat="proposal in proposals">
<input type="radio" class="form-check-input" name="proposals{question.id}" id="p{proposal.id}" ng-model="question.value" value="{proposal.id}" disabled={survey.readonly}>
<label class="form-check-label" for="p{proposal.id}">{proposal.label}</label>
</div>
<div class="form-group row" ng-if="question.edit" ng-repeat="proposal in proposals">
<div class="col">
<input type="text" class="form-control" id="pi{proposal.id}" placeholder="Label" ng-model="proposal.label">
</div>
<div class="col-auto">
<button type="button" class="btn btn-success ml-1" ng-click="saveProposal()" ng-if="question.edit && (question.kind == 'ucq' || question.kind == 'mcq')"><i class="bi bi-check" ></i></button>
<button type="button" class="btn btn-danger ml-1" ng-click="deleteProposal()" ng-if="question.edit && (question.kind == 'ucq' || question.kind == 'mcq')"><i class="bi bi-trash-fill"></i></button>
</div>
</div>
<button type="button" class="btn btn-info ml-1" ng-click="addProposal()" ng-if="question.edit && (question.kind == 'ucq' || question.kind == 'mcq')" ng-disabled="!question.id"><i class="bi bi-plus"></i> Ajouter des proposals
</button><span ng-show="question.edit && (question.kind == 'ucq' || question.kind == 'mcq') && !question.id" class="ml-2" style="font-style:italic"> Créez la question pour ajouter des propositions</span>
</div>
<div class="ml-3 card-text alert alert-success" ng-if="!question.edit && (question.response.score_explaination || question.response.score)">
<div class="row">
<div class="col-auto">
<strong>{question.response.score}&nbsp;%</strong>
</div>
<p class="col mb-0" style="white-space: pre-line">{question.response.score_explaination}</p>
</div>
</div>
{/if}
</div>
</div>

View file

@ -1,44 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte';
import { user } from '../stores/user';
const dispatch = createEventDispatcher();
let className = '';
export { className as class };
export let question = null;
export let qid = null;
export let edit = false;
</script>
<div class="card-header {className}">
<slot></slot>
{#if edit}
<div class="card-title row">
<label for="q{qid}title" class="col-auto col-form-label font-weight-bold">Titre&nbsp;:</label>
<div class="col"><input id="q{qid}title" class="form-control" bind:value={question.title}></div>
</div>
{:else}
<h4 class="card-title mb-0">{#if qid !== null}{qid + 1}. {/if}{question.title}</h4>
{/if}
{#if edit}
<div class="form-group row">
<label class="col-2 col-form-label" for="q{qid}kind">Type de réponse</label>
<div class="col">
<select class="form-select" id="q{qid}kind" bind:value={question.kind}>
<option value="text">Texte</option>
<option value="int">Entier</option>
<option value="ucq">QCU</option>
<option value="mcq">QCM</option>
</select>
</div>
</div>
<textarea class="form-control mb-2" bind:value={question.desc_raw} placeholder="Description de la question"></textarea>
{:else if question.description}
<p class="card-text mt-2">{@html question.description}</p>
{/if}
</div>

View file

@ -1,118 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte';
import { QuestionProposal } from '../lib/questions';
export let edit = false;
export let proposals = [];
export let kind = 'mcq';
export let prefixid = '';
export let readonly = false;
export let id_question = 0;
export let value;
let valueCheck = [];
$: {
if (value) {
valueCheck = value.split(',');
}
}
const dispatch = createEventDispatcher();
function addProposal() {
const p = new QuestionProposal();
p.id_question = id_question;
proposals.push(p);
proposals = proposals;
}
</script>
{#each proposals as proposal, pid (proposal.id)}
<div class="form-check">
{#if kind == 'mcq'}
<input
type="checkbox"
class="form-check-input"
disabled={readonly}
name={prefixid + 'proposal' + proposal.id_question}
id={prefixid + 'p' + proposal.id}
bind:group={valueCheck}
value={proposal.id.toString()}
on:change={() => { value = valueCheck.join(','); dispatch("change"); }}
>
{:else}
<input
type="radio"
class="form-check-input"
disabled={readonly}
name={prefixid + 'proposal' + proposal.id_question}
id={prefixid + 'p' + proposal.id}
bind:group={value}
value={proposal.id.toString()}
on:change={() => { dispatch("change"); }}
>
{/if}
{#if edit}
<form on:submit|preventDefault={() => { proposal.save().then(() => proposal = proposal); } }>
<div class="input-group input-group-sm mb-2">
<input
type="text"
class="form-control"
bind:value={proposal.label}
on:input={() => proposal.changed = true}
>
<button
type="button"
class="btn btn-outline-danger"
tabindex="-1"
disabled={!proposal.id}
on:click={() => { proposal.delete().then(() => { proposals.splice(pid, 1); proposals = proposals; }); }}
>
<i class="bi bi-trash"></i>
</button>
<button
type="submit"
class="btn"
class:btn-success={proposal.changed}
class:btn-outline-success={!proposal.changed}
disabled={!proposal.changed}
>
<i class="bi bi-check"></i>
</button>
</div>
</form>
{:else}
<label
class="form-check-label"
for={prefixid + 'p' + proposal.id}
>
{proposal.label}
</label>
{/if}
</div>
{/each}
{#if edit}
{#if kind == 'mcq'}
<input
type="checkbox"
class="form-check-input"
disabled
checked
>
{:else}
<input
type="radio"
class="form-check-input"
disabled
checked
>
{/if}
<button
type="button"
class="btn btn-sm btn-link"
on:click={addProposal}
>
ajouter
</button>
{/if}

View file

@ -1,52 +0,0 @@
<script>
export let response = null;
export let survey = null;
</script>
{#if response.score !== undefined}
<div
class="alert row mb-0"
class:alert-success={response.score >= 95}
class:alert-info={response.score < 95 && response.score >= 70}
class:alert-warning={response.score < 70 && response.score >= 45}
class:alert-danger={response.score < 45}
>
<div class="col-auto">
<strong
title="Tu as obtenu un score de {response.score}&nbsp;%, ce qui correspond à {Math.trunc(response.score*10/5)/10}/20."
>
{response.score}&nbsp;%
</strong>
</div>
<div class="col">
{#if response.score_explaination}
{response.score_explaination}
{:else if response.score === 100}
<i class="bi bi-check"></i>
{/if}
</div>
</div>
{:else if response && survey}
{#if response.value}
<div class="alert alert-dark text-danger row mb-0">
<div class="col-auto" style="margin: -0.4em; font-size: 2em;">
🤯
</div>
<div class="col">
<strong>Oups, tu sembles être passé entre les mailles du filet&nbsp;!</strong>
Cette question a bien été corrigée, mais une erreur s'est produite dans la correction de ta réponse.
<a href="mailto:nemunaire@nemunai.re?subject=Question non corrigée (questionnaire {survey.id})">Contacte ton enseignant</a> au plus vite.
</div>
</div>
{:else}
<div class="alert alert-danger row mb-0">
<div class="col-auto" style="margin: -0.4em; font-size: 2em;">
😟
</div>
<div class="col">
<strong>Tu n'as pas répondu à cette question.</strong>
Que s'est-il passé&nbsp;?
</div>
</div>
{/if}
{/if}

View file

@ -1,77 +0,0 @@
<script>
import { getSurveys } from '../lib/surveys';
import { getUsers, getGrades, getPromos } from '../lib/users';
export let promo = null;
</script>
{#await getPromos() then promos}
<div class="float-end me-2">
<select class="form-select" bind:value={promo}>
<option value={null}>tous</option>
{#each promos as promo, pid (pid)}
<option value={promo}>{promo}</option>
{/each}
</select>
</div>
{/await}
<h2>
Étudiants {#if promo !== null}{promo}{/if}
<small class="text-muted">Notes</small>
</h2>
{#await getSurveys()}
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des questionnaires corrigés&hellip;
</div>
{:then surveys}
{#await getGrades()}
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des notes&hellip;
</div>
{:then grades}
<div class="card mb-5">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>ID</th>
<th>Login</th>
{#each surveys as survey (survey.id)}
{#if survey.corrected && (promo === null || survey.promo == promo)}
<th><a href="surveys/{survey.id}" style="text-decoration: none">{survey.title}</a></th>
{/if}
{/each}
</tr>
</thead>
<tbody>
{#await getUsers()}
<tr>
<td colspan="20">
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des étudiants&hellip;
</div>
</td>
</tr>
{:then users}
{#each users as user (user.id)}
{#if promo === null || user.promo === promo}
<tr>
<td><a href="users/{user.id}" style="text-decoration: none">{user.id}</a></td>
<td><a href="users/{user.login}" style="text-decoration: none">{user.login}</a></td>
{#each surveys as survey (survey.id)}
{#if survey.corrected && (promo === null || survey.promo == promo)}
<td>{grades[user.id][survey.id]?grades[user.id][survey.id]:"N/A"}</td>
{/if}
{/each}
</tr>
{/if}
{/each}
{/await}
</tbody>
</table>
</div>
{/await}
{/await}

View file

@ -1,134 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
import { getQuestions } from '../lib/questions';
import { ToastsStore } from '../stores/toasts';
const dispatch = createEventDispatcher();
export let survey = null;
function saveSurvey() {
survey.save().then((response) => {
dispatch('saved');
}, (error) => {
ToastsStore.addErrorToast({
msg: error.errmsg,
});
})
}
function deleteSurvey() {
survey.delete().then((response) => {
goto(`surveys`);
}, (error) => {
ToastsStore.addErrorToast({
msg: error.errmsg,
});
})
}
function duplicateSurvey() {
survey.duplicate().then((response) => {
goto(`surveys/${response.id}`);
}).catch((error) => {
ToastsStore.addErrorToast({
msg: error.errmsg,
});
})
}
</script>
<form on:submit|preventDefault={saveSurvey}>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="title" class="col-form-label col-form-label-sm">Titre du questionnaire</label>
</div>
<div class="col-sm-8">{survey.id}
<input type="text" class="form-control form-control-sm" id="title" bind:value={survey.title}>
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="promo" class="col-form-label col-form-label-sm">Promo</label>
</div>
<div class="col-sm-8 col-md-4 col-lg-2">
<input type="number" step="1" min="0" max="2068" class="form-control form-control-sm" id="promo" bind:value={survey.promo}>
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="group" class="col-form-label col-form-label-sm">Restreindre au groupe</label>
</div>
<div class="col-sm-8 col-md-4 col-lg-2">
<input class="form-control form-control-sm" id="group" bind:value={survey.group}>
</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>
</div>
<div class="col-sm-8">
<input type="text" class="form-control form-control-sm" id="start_availability" bind:value={survey.start_availability}>
</div>
</div>
<div class="row">
<div class="col-sm-3 text-sm-end">
<label for="end_availability" class="col-form-label col-form-label-sm">Date de fin</label>
</div>
<div class="col-8">
<input type="text" class="form-control form-control-sm" id="end_availability" bind:value={survey.end_availability}>
</div>
</div>
<div class="row row-cols-3 mx-1 my-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="shown" bind:checked={survey.shown}>
<label class="form-check-label" for="shown">
Afficher le questionnaire
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="corrected" bind:checked={survey.corrected}>
<label class="form-check-label" for="corrected">
Marqué comme corrigé
</label>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Enregistrer</button>
{#if survey.id}
<button type="button" class="btn btn-danger" on:click={deleteSurvey}>Supprimer</button>
<button type="button" class="btn btn-secondary" on:click={duplicateSurvey}>Dupliquer avec ces nouveaux paramètres</button>
{/if}
</div>
</div>
</form>
<hr>

View file

@ -1,12 +0,0 @@
<script>
export let survey;
let className = '';
export { className as class };
</script>
{#if survey.direct != null}<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>
{/if}

View file

@ -1,92 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { user } from '../stores/user';
import DateFormat from '../components/DateFormat.svelte';
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 != null) {
direct = survey;
}
}
});
</script>
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>Intitulé</th>
<th>Date</th>
{#if $user}
<th>Score</th>
{/if}
</tr>
</thead>
{#await req_surveys}
<tr>
<td colspan="5" class="text-center py-3">
<div class="spinner-border mx-3" role="status"></div>
<span>Chargement des questionnaires &hellip;</span>
</td>
</tr>
{:then surveys}
<tbody style="cursor: pointer;">
{#each surveys as survey, sid (survey.id)}
{#if (survey.shown || survey.direct != null) && (!$user || (!$user.was_admin || $user.promo == survey.promo) || $user.is_admin)}
{#if $user && $user.is_admin && (sid == 0 || surveys[sid-1].promo != survey.promo)}
<tr class="bg-info text-light">
<th colspan="5" class="fw-bold">
{survey.promo}
</th>
</tr>
{/if}
<tr on:click={e => goto(survey.direct != null ?`surveys/${survey.id}/live`:$user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}>
<td>
{survey.title}
<SurveyBadge {survey} class="float-end" />
</td>
{#if survey.start_availability > Date.now()}
<td>
<DateFormat date={survey.start_availability} dateStyle="medium" timeStyle="medium" />
<svg class="bi bi-arrow-bar-right" width="1em" height="1em" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.146 6.646a.5.5 0 01.708 0l3 3a.5.5 0 010 .708l-3 3a.5.5 0 01-.708-.708L14.793 10l-2.647-2.646a.5.5 0 010-.708z" clip-rule="evenodd"></path><path fill-rule="evenodd" d="M8 10a.5.5 0 01.5-.5H15a.5.5 0 010 1H8.5A.5.5 0 018 10zm-2.5 6a.5.5 0 01-.5-.5v-11a.5.5 0 011 0v11a.5.5 0 01-.5.5z" clip-rule="evenodd"></path></svg>
</td>
{:else}
<td>
<svg class="bi bi-arrow-bar-left" width="1em" height="1em" viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.854 6.646a.5.5 0 00-.708 0l-3 3a.5.5 0 000 .708l3 3a.5.5 0 00.708-.708L5.207 10l2.647-2.646a.5.5 0 000-.708z" clip-rule="evenodd"></path><path fill-rule="evenodd" d="M12 10a.5.5 0 00-.5-.5H5a.5.5 0 000 1h6.5a.5.5 0 00.5-.5zm2.5 6a.5.5 0 01-.5-.5v-11a.5.5 0 011 0v11a.5.5 0 01-.5.5z" clip-rule="evenodd"></path></svg>
<DateFormat date={survey.end_availability} dateStyle="medium" timeStyle="medium" />
</td>
{/if}
{#if !$user}
{:else if !survey.corrected}
<td>N/A</td>
{:else}
<td>
{#await getScore(survey)}
<div class="spinner-border spinner-border-sm" role="status"></div>
{:then score}
{score.score}
{/await}
</td>
{/if}
</tr>
{/if}
{/each}
</tbody>
{/await}
{#if $user && $user.is_admin}
<tfoot>
<tr>
<td colspan="4">
<a href="surveys/new" class="btn btn-sm btn-primary">Ajouter un questionnaire</a>
</td>
</tr>
</tfoot>
{/if}
</table>

View file

@ -1,111 +0,0 @@
<script>
import { user } from '../stores/user';
import { ToastsStore } from '../stores/toasts';
import QuestionForm from '../components/QuestionForm.svelte';
import { Question } from '../lib/questions';
export let survey = null;
export let id_user = null;
export let questions = [];
let newquestions = [];
let submitInProgress = false;
function submitAnswers() {
submitInProgress = true;
const res = [];
for (const r in responses) {
res.push({"id_question": responses[r].id_question, "value": String(responses[r].value)})
}
survey.submitAnswers(res, id_user).then((response) => {
submitInProgress = false;
ToastsStore.addToast({
msg: "Vos réponses ont bien étés sauvegardées.",
color: "success",
title: "Questionnaire",
});
}, (error) => {
submitInProgress = false;
ToastsStore.addErrorToast({
msg: "Une erreur s'est produite durant l'envoi de vos réponses : " + error + "\nVeuillez réessayer dans quelques instants.",
});
});
}
function addQuestion() {
const q = new Question();
q.id_survey = survey.id;
newquestions.push(q);
newquestions = newquestions;
}
function deleteQuestion(question, qid) {
question.delete().then(() => {
questions.splice(qid, 1);
questions = questions;
})
}
function deleteNewQuestion(question, qid) {
if (question.id) {
question.delete().then(() => {
newquestions.splice(qid, 1);
newquestions = newquestions;
})
} else {
newquestions.splice(qid, 1);
newquestions = newquestions;
}
}
let responses = {};
for (const q of questions) {
responses[q.id] = {id_question: q.id, value: ""};
}
survey.retrieveAnswers(id_user).then((response) => {
if (response) {
for (const res of response.reverse()) {
responses[res.id_question] = res;
}
}
})
</script>
<form class="mb-5" on:submit|preventDefault={submitAnswers}>
{#each questions as question, qid (question.id)}
<QuestionForm
{survey}
qid={qid}
question={question}
response_history={responses[question.id]}
readonly={survey.isFinished() && !$user.is_admin}
on:delete={() => deleteQuestion(question, qid)}
bind:value={responses[question.id].value}
/>
{/each}
{#each newquestions as question, qid (qid)}
<QuestionForm
qid={questions.length + qid}
question={question}
edit
on:delete={() => deleteNewQuestion(question, qid)}
/>
{/each}
<div class="d-flex justify-content-between">
{#if !survey.corrected || $user.is_admin}
<button type="submit" class="btn btn-primary" disabled={submitInProgress || (survey.isFinished() && !$user.is_admin)}>
{#if submitInProgress}
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
{/if}
Soumettre les réponses
</button>
{/if}
{#if $user && $user.is_admin}
<button type="button" class="btn btn-info" on:click={addQuestion}>
Ajouter une question
</button>
{/if}
</div>
</form>

View file

@ -1,18 +0,0 @@
<script>
import { ToastsStore } from '../stores/toasts';
</script>
<div class="toast-container position-absolute top-0 end-0 p-3">
{#each $ToastsStore.toasts as toast}
<div class="toast show" role="alert">
<div class="toast-header">
<div class="bg-{toast.color} rounded me-2">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</div>
<strong>{#if toast.title}{toast.title}{:else}Questionnaire{/if}</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
{toast.msg}
</div>
</div>
{/each}
</div>

View file

@ -1,61 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { getSurveys } from '../lib/surveys';
import { getUser, getUserGrade, getUserScore } from '../lib/users';
export let student = null;
export let allPromos = false;
</script>
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>ID</th>
<th>Titre</th>
<th>Promo</th>
<th>Avancement</th>
<th>Note</th>
</tr>
</thead>
<tbody>
{#await getSurveys()}
<tr>
<td colspan="4">
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des questionnaires&hellip;
</div>
</td>
</tr>
{:then surveys}
{#each surveys as survey, sid (survey.id)}
{#if allPromos || survey.promo === student.promo}
<tr on:click={e => goto(`users/${student.id}/surveys/${survey.id}`)}>
<td>{survey.id}</td>
<td>{survey.title}</td>
<td>{survey.promo}</td>
{#await getUserGrade(student.id, survey)}
<td>
<div class="spinner-border spinner-border-sm" role="status"></div>
</td>
{:then gr}
<td title="{gr.grades}">
{gr.avancement * 100}&nbsp;%
</td>
{/await}
{#await getUserScore(student.id, survey)}
<td>
<div class="spinner-border spinner-border-sm" role="status"></div>
</td>
{:then score}
<td>
{score.score}{#if score.score >= 0}/20{/if}
</td>
{/await}
</tr>
{/if}
{/each}
{/await}
</tbody>
</table>

View file

@ -1,60 +0,0 @@
<script>
import { user } from '../stores/user';
import DateFormat from '../components/DateFormat.svelte';
let className = '';
export { className as class };
const rendus_baseurl = "https://virli.nemunai.re/rendus/";
async function getUserRendus() {
const res = await fetch(`${rendus_baseurl}${$user.login}.json`)
if (res.status == 200) {
return await res.json();
} else {
return null;
}
}
</script>
<table class="table {className}">
{#await getUserRendus()}
Please wait...
{:then rendus}
<thead>
<tr>
{#each Object.keys(rendus) as renduname, rid (rid)}
<th>{renduname}</th>
{/each}
</tr>
</thead>
<tbody>
<tr>
{#each Object.keys(rendus) as renduname, rid (rid)}
<th
class:bg-danger={!rendus[renduname]}
class:text-center={!rendus[renduname]}
class:bg-success={rendus[renduname]}
>
{#if rendus[renduname]}
<DateFormat date={rendus[renduname].date} dateStyle="medium" timeStyle="medium" /><br>
<span class="hash" title={rendus[renduname].hash}>{rendus[renduname].hash}</span>
{:else}
&ndash;
{/if}
</th>
{/each}
</tr>
</tbody>
{/await}
</table>
<style>
.hash {
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
}
</style>

View file

@ -1 +0,0 @@
/// <reference types="@sveltejs/kit" />

View file

@ -1,6 +0,0 @@
export async function handle({ event, resolve }) {
const response = await resolve(event, {
ssr: false,
});
return response;
}

View file

@ -1,88 +0,0 @@
export class CorrectionTemplate {
constructor(res) {
if (res) {
this.update(res);
}
}
update({ id, id_question, label, regexp, score, score_explaination }) {
this.id = id;
this.id_question = id_question;
this.regexp = regexp;
this.label = label;
this.score = score;
this.score_explaination = score_explaination;
}
async getCorrections() {
if (this.id) {
const res = await fetch(`api/questions/${this.id_question}/corrections/${this.id}`, {
method: 'GET',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
}
async save() {
const res = await fetch(this.id?`api/questions/${this.id_question}/corrections/${this.id}`:`api/questions/${this.id_question}/corrections`, {
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);
}
}
async delete() {
if (this.id) {
const res = await fetch(`api/questions/${this.id_question}/corrections/${this.id}`, {
method: 'DELETE',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return true;
} else {
throw new Error((await res.json()).errmsg);
}
} else {
return true;
}
}
}
export async function getCorrectionTemplates(qid) {
const res = await fetch(`api/questions/${qid}/corrections`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
const data = await res.json();
if (data === null) {
return [];
} else {
return (data).map((c) => new CorrectionTemplate(c))
}
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function autoCorrection(id_user, my_tpls) {
const res = await fetch(`api/users/${id_user}/corrections`, {
method: 'PUT',
headers: {'Accept': 'application/json'},
body: JSON.stringify(my_tpls),
});
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}

View file

@ -1,147 +0,0 @@
import { Response } from './response';
export class QuestionProposal {
constructor(res) {
if (res) {
this.update(res);
}
}
update({ id, id_question, label }) {
this.id = id;
this.id_question = id_question;
this.label = label;
if (this.changed !== undefined)
delete this.changed;
}
async save() {
const res = await fetch(this.id?`api/questions/${this.id_question}/proposals/${this.id}`:`api/questions/${this.id_question}/proposals`, {
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);
}
}
async delete() {
if (this.id) {
const res = await fetch(`api/questions/${this.id_question}/proposals/${this.id}`, {
method: 'DELETE',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return true;
} else {
throw new Error((await res.json()).errmsg);
}
}
}
}
export class Question {
constructor(res) {
if (res) {
this.update(res);
}
}
update({ id, id_survey, title, description, desc_raw, placeholder, kind }) {
this.id = id;
this.id_survey = id_survey;
this.title = title;
this.description = description;
this.desc_raw = desc_raw;
this.placeholder = placeholder;
this.kind = kind;
}
async getProposals() {
const res = await fetch(`api/questions/${this.id}/proposals`, {
method: 'GET',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return (await res.json()).map((p) => new QuestionProposal(p))
} else {
throw new Error((await res.json()).errmsg);
}
}
async getMyResponse() {
const res = await fetch(`api/questions/${this.id}/response`, {
method: 'GET',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return new Response(await res.json());
} else {
throw new Error((await res.json()).errmsg);
}
}
async getResponses() {
const res = await fetch(`api/surveys/${this.id_survey}/questions/${this.id}/responses`, {
method: 'GET',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return (await res.json()).map((r) => new Response(r))
} else {
throw new Error((await res.json()).errmsg);
}
}
async save() {
const res = await fetch(this.id?`api/questions/${this.id}`:`api/surveys/${this.id_survey}/questions`, {
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);
}
}
async delete() {
if (this.id) {
const res = await fetch(`api/questions/${this.id}`, {
method: 'DELETE',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return true;
} else {
throw new Error((await res.json()).errmsg);
}
}
}
}
export async function getQuestion(qid) {
const res = await fetch(`api/questions/${qid}`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return new Question(await res.json());
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getQuestions(sid) {
const res = await fetch(`api/surveys/${sid}/questions`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return (await res.json()).map((e) => new Question(e))
} else {
throw new Error((await res.json()).errmsg);
}
}

View file

@ -1,34 +0,0 @@
export class Response {
constructor(res) {
if (res) {
this.update(res);
}
}
update({ id, id_question, id_user, value, time_submit, score, score_explaination, id_corrector, time_scored }) {
this.id = id;
this.id_question = id_question;
this.id_user = id_user;
this.value = value;
this.time_submit = time_submit;
this.score = score;
this.score_explaination = score_explaination;
this.id_corrector = id_corrector;
this.time_scored = time_scored;
}
async save() {
const res = await fetch(`api/questions/${this.id_question}/responses/${this.id}`, {
method: 'PUT',
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);
}
}
}

View file

@ -1,145 +0,0 @@
import { getQuestions } from './questions';
class Survey {
constructor(res) {
if (res) {
this.update(res);
}
}
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;
delete this.__start_availability;
}
if (this.end_availability != end_availability) {
this.end_availability = end_availability;
delete this.__end_availability;
}
}
startAvailability() {
if (!this.__start_availability) {
this.__start_availability = new Date(this.start_availability)
}
return this.__start_availability
}
endAvailability() {
if (!this.__end_availability) {
this.__end_availability = new Date(this.end_availability)
}
return this.__end_availability
}
isFinished() {
return this.endAvailability() < new Date();
}
async retrieveAnswers(id_user=null) {
const res = await fetch(id_user?`api/users/${id_user}/surveys/${this.id}/responses`:`api/surveys/${this.id}/responses`, {
method: 'GET',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
async submitAnswers(answer, id_user=null) {
const res = await fetch(id_user?`api/users/${id_user}/surveys/${this.id}`:`api/surveys/${this.id}`, {
method: 'POST',
headers: {'Accept': 'application/json'},
body: JSON.stringify(answer),
});
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
async save() {
const res = await fetch(this.id?`api/surveys/${this.id}`:'api/surveys', {
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);
}
}
async duplicate() {
if (this.id) {
const oldSurveyId = this.id;
delete this.id;
const res = await fetch(`api/surveys`, {
method: 'POST',
headers: {'Accept': 'application/json'},
body: JSON.stringify(this),
});
if (res.status == 200) {
const response = await res.json();
// Now recopy questions
const questions = await getQuestions(oldSurveyId);
for (const q of questions) {
delete q.id;
q.id_survey = response.id;
q.save();
}
return response;
} else {
throw new Error((await res.json()).errmsg);
}
}
}
async delete() {
if (this.id) {
const res = await fetch(`api/surveys/${this.id}`, {
method: 'DELETE',
headers: {'Accept': 'application/json'},
});
if (res.status == 200) {
return true;
} else {
throw new Error((await res.json()).errmsg);
}
}
}
}
export async function getSurveys() {
const res = await fetch(`api/surveys`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return (await res.json()).map((s) => new Survey(s));
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getSurvey(sid) {
const res = await fetch(`api/surveys/${sid}`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return new Survey(await res.json());
} else {
throw new Error((await res.json()).errmsg);
}
}

View file

@ -1,61 +0,0 @@
export async function getPromos() {
const res = await fetch('api/promos', {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getUsers() {
const res = await fetch('api/users', {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getUser(uid) {
const res = await fetch(`api/users/${uid}`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getGrades(uid, survey) {
const res = await fetch(`api/grades`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getUserGrade(uid, survey) {
const res = await fetch(`api/users/${uid}/surveys/${survey.id}/grades`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getUserScore(uid, survey) {
const res = await fetch(`api/users/${uid}/surveys/${survey.id}/score`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getScore(survey) {
const res = await fetch(`api/surveys/${survey.id}/score`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}

View file

@ -1,146 +0,0 @@
<script context="module">
import { user } from '../stores/user';
let stop_refresh = false;
let refresh_interval_auth = null;
async function refresh_auth(cb=null, interval=null) {
if (refresh_interval_auth)
clearInterval(refresh_interval_auth);
if (interval === null) {
interval = Math.floor(Math.random() * 200000) + 200000;
}
if (stop_refresh) {
return;
}
refresh_interval_auth = setInterval(refresh_auth, interval);
const res = await fetch('api/auth', {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
const auth = await res.json();
user.set(auth);
} else {
user.set(null);
}
}
export async function load({ props, stuff, url }) {
refresh_auth();
const rroutes = url.pathname.split('/');
return {
props: {
...props,
rroute: rroutes.length>1?rroutes[1]:'',
},
stuff: {
...stuff,
refresh_auth,
}
};
}
</script>
<script>
import Toaster from '../components/Toaster.svelte';
export let rroute = '';
function switchAdminMode() {
var tmp = $user.is_admin;
$user.is_admin = $user.was_admin || false;
$user.was_admin = tmp;
}
function disconnectCurrentUser() {
fetch('api/auth/logout', {
method: 'POST'
}).then((response) => {
refresh_auth();
});
}
</script>
<svelte:head>
<title>SRS: MCQ and others courses related stuff</title>
</svelte:head>
<nav class="navbar navbar-expand-sm navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href=".">
SRS
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#adminMenu" aria-controls="adminMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="loggedMenu">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="adlin" target="_self">AdLin</a>
</li>
<li class="nav-item">
<a class="nav-link" href="fic" target="_self">FIC</a>
</li>
<li class="nav-item">
<a class="nav-link" class:active={rroute === 'surveys'} href="surveys">
Questionnaires
</a>
</li>
{#if $user && $user.is_admin}
<li class="nav-item"><a class="nav-link" class:active={rroute === 'users'} href="users">Étudiants</a></li>
{/if}
<li class="nav-item"><a class="nav-link" href="virli" target="_self">VIRLI</a></li>
</ul>
<ul class="navbar-nav ms-auto">
{#if $user}
{#if $user.was_admin}
<li class="nav-item">
<button class="btn btn-dark" on:click={switchAdminMode}>
Vue admin
</button>
</li>
{:else if $user.is_admin}
<li class="nav-item">
<button class="btn btn-light" on:click={switchAdminMode}>
Vue étudiant
</button>
</li>
{/if}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">
<img class="rounded-circle" src="//photos.cri.epita.fr/square/{$user.login}" alt="Menu" style="margin: -0.75em 0; max-height: 2.5em">
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" class:active={rroute === 'help'} href="help">Besoin d'aide&nbsp;?</a></li>
<li><a class="dropdown-item" class:active={rroute === 'bug-bounty'} href="bug-bounty">Bug Bounty</a></li>
<li><hr class="dropdown-divider"></li>
<li>
<button class="dropdown-item" on:click={disconnectCurrentUser}>
Se déconnecter
</button>
</li>
</ul>
</li>
{:else if $user === undefined}
<li class="nav-item">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</li>
{:else}
<li class="nav-item">
<a href="auth/CRI" target="_self" class="btn btn-dark">
Se connecter
</a>
</li>
{/if}
</ul>
</div>
</div>
</nav>
<div class="container mt-3">
<slot></slot>
</div>
<Toaster />

View file

@ -1,71 +0,0 @@
<script context="module">
import { session } from '$app/stores';
export function load() {
if (session.id) {
return { redirect: '/', status: 302 };
}
return { };
}
</script>
<script>
import { goto } from '$app/navigation';
let auth = { username: "", password: "" };
let pleaseWait = false;
function logmein() {
pleaseWait = true;
fetch('api/auth', {
method: 'POST',
body: JSON.stringify(auth),
})
.then((response) => {
response.json().then((auth) => {
pleaseWait = false;
$session = auth;
goto(".");
})
})
.catch((response) => {
pleaseWait = false;
if (response.data)
addToast({
variant: "danger",
title: "Connexion impossible",
msg: (response.data ? response.data.errmsg : "Impossible de contacter le serveur"),
});
});
}
</script>
<div class="row">
<form class="col" on:submit|preventDefault={logmein}>
<h2>Accès à votre compte</h2>
<div class="mb-3">
<label for="login" class="form-label">CRI login</label>
<input class="form-control" id="login" bind:value={auth.username} placeholder="Entrer votre login" autofocus>
</div>
<div class="mb-3">
<label for="password" class="form-label">Mot de passe</label>
<input type="password" class="form-control" id="password" bind:value={auth.password} placeholder="Mot de passe">
</div>
<button type="submit" class="btn btn-info">
{#if pleaseWait}
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
{/if}
Me connecter
</button>
</form>
<div class="col">
<h2>OpenId Connect</h2>
<div class="text-center">
<a href="auth/CRI" class="btn btn-primary" target="_self">
Me connecter avec mon compte CRI
</a>
</div>
</div>
</div>

View file

@ -1,89 +0,0 @@
<h2>Bug Bounty</h2>
<p class="lead">
Comme tous les services accessibles en ligne, ce site présente un certain nombre de bugs et de vulnérabilités qui ne font pas partie des fonctionnalités attendues.
</p>
<p>
Par exception aux règles qui vous ont été données, vous êtes autorisés à rechercher des vulnérabilités sur tous les services (et leurs infrastructures afférentes) qui vous sont mis à disposition sur <code>nemunai.re</code>, selon les conditions suivantes :
</p>
<ul>
<li><strong>vous ne devez pas <ins>entraver volontairement</ins> la progression de vos camarades ou le fonctionnement d'une partie de l'infrastructure ;</strong></li>
<li><strong>vous devez maîtriser les outils que vous utiliser&nbsp;:</strong> certains outils mal maîtrisés peuvent bombarder de requêtes un service au point de le faire tomber. Les services mis à votre disposition ne constituent pas une plateforme d'entraînement à l'utilisation de ces outils, vous avez des machines virtuelles pour cela ;</li>
<li><strong>vous devez rapporter rapidement à <a href="mailto:bounty@nemunai.re">bounty@nemunai.re</a> tous les bugs ou vulnérabilités que vous découvrez.</strong></li>
</ul>
<p>
En plus du maintien des bénéfices que vous avez éventuellement pu obtenir (par exemple changer une note), vous obtiendrez également un bonus sur votre note finale.
À titre indicatif, voici ce à quoi vous pouvez vous attendre&nbsp;:
</p>
<div class="card mb-2">
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th>Exemples de vulnérabilités</th>
<th>Points</th>
</tr>
</thead>
<tbody>
<tr>
<td>Faute d'orthographe/grammaire, non conformité, bug, ...</td>
<td>crédité dans le commit</td>
</tr>
<tr>
<td>XSS, CSRF, injection SQL/LDAP, ...</td>
<td>1 point</td>
</tr>
<tr>
<td>Violation de permission, fuite d'informations, ...</td>
<td>2 points</td>
</tr>
<tr>
<td>Exécution arbitraire de code</td>
<td>3 points</td>
</tr>
<tr>
<td>Exécution arbitraire de code sur l'hôte</td>
<td>5 points</td>
</tr>
</tbody>
</table>
</div>
<p>
Lorsque vous découvrez une vulnérabilité en groupe, précisez les noms et le rôle que chacun a eu dans la découverte.
</p>
<div class="alert alert-warning d-flex">
<i class="bi bi-exclamation-triangle me-3"></i>
<span>
À toute fin utile, l'usage et la non-divulgation d'une vulnérabilité sont <a href="https://www.legifrance.gouv.fr/codes/id/LEGISCTA000006149839/" target="_blank">sanctionnables</a>.
</span>
</div>
<h3 class="mt-5 mb-3">Hall of Fame</h3>
<div class="card mb-3">
<div class="card-header">
Il était toujours possible de répondre aux questionnaires après l'heure de clôture.
<span class="badge bg-success shadow-lg">+2&nbsp;pts</span>
</div>
<div class="card-body">
<div class="row row-cols-6">
<img class="img-thumbnail" src="//photos.cri.epita.fr/mahe.charpy" alt="mahe.charpy">
<img class="img-thumbnail" src="//photos.cri.epita.fr/albin.parou" alt="albin.parou">
<img class="img-thumbnail" src="//photos.cri.epita.fr/sebastien.januszczak" alt="sebastien.januszczak">
<img class="img-thumbnail" src="//photos.cri.epita.fr/clement.lanata" alt="clement.lanata">
<img class="img-thumbnail" src="//photos.cri.epita.fr/alexandre.delorme" alt="alexandre.delorme">
<img class="img-thumbnail" src="//photos.cri.epita.fr/justin.puchelle" alt="justin.puchelle">
</div>
<p class="card-text mt-3">
Divulguée et corrigée le 19 novembre 2021.
<a href="https://git.nemunai.re/srs/atsebay.t/commit/5c53d2eaea9e7233bc8a08de2f40c040c0700c3e" target="_blank">Commit</a>
</p>
</div>
</div>
<div class="mb-5"></div>

View file

@ -1,17 +0,0 @@
<script context="module">
export async function load({ params }) {
return {
props: {
promo: params.promo,
}
};
}
</script>
<script>
import StudentGrades from '../../components/StudentGrades.svelte';
export let promo;
</script>
<StudentGrades {promo} />

View file

@ -1,5 +0,0 @@
<script>
import StudentGrades from '../../components/StudentGrades.svelte';
</script>
<StudentGrades />

View file

@ -1,40 +0,0 @@
<script>
function needhelp() {
}
</script>
<h2>Besoin d'aide&nbsp;?</h2>
<p class="lead">
Vous êtes nombreux et l'on n'est malheureusement pas en mesure de vous suivre régulièrement individuellement.
Nous restons néanmoins toujours disponibles lorsque vous avez besoin de notre aide.
</p>
<p>
D'une manière générale, si vous avez des problèmes, n'hésitez pas à contacter le professeur, que ce soit en cours ou par mail.
Il vaut mieux mettre des mots soi-même sur un problème que l'on rencontre plutôt que d'attendre le dernier moment, en se disant qu'on aura le temps de trouver une solution.
</p>
<p>
Peut-être que tu as raté&middot;e plusieurs rendus, ou peut-être que tu ne te sens plus le courage de continuer les projets.
</p>
<p>
Si tu souhaites me parler d'une situation qui t'a troublé&middot;e, d'un problème que tu rencontres ou me faire une remarque,
n'hésite pas à venir me voir lors d'un cours, par exemple à la pause ou à la fin&nbsp;;
je suis aussi joignable <a href="mailto:nemunaire@nemunai.re">par e-mail</a> ou bien <a href="https://matrix.to/#/@nemunaire:nemunai.re">sur Matrix</a> ou Teams.
</p>
<p class="mt-4">
Si tu souhaites juste avoir un peu plus d'attention, soit parce que tu te sens à l'écart, en difficulté ou autre&nbsp;:
<button
type="button"
class="btn btn-sm btn-primary"
on:click={needhelp}
>
Clique ce bouton
</button>
</p>
<div class="mb-5"></div>

View file

@ -1,51 +0,0 @@
<script lang="ts">
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">
<div class="card-body">
{#if $user}
<div class="row">
<div class="col-md">
<h1 class="card-text">
Bienvenue {$user.firstname}&nbsp;!
</h1>
<hr class="my-4">
{#if $user.promo != $user.current_promo}
<div class="alert alert-primary" role="alert">
<strong>Es-tu un {$user.current_promo}&nbsp;?</strong> Tu es actuellement enregistré comme un {$user.promo}, ce qui ne te permet pas d'accéder aux questionnaires de la promo {$user.current_promo}. <a href="mailto:nemunaire@nemunai.re?subject=Mauvaise promotion sur srs.nemunai.re&body=Bonjour, Je ne suis pas enregistré dans la bonne promotion sur le site srs.nemunai.re. Cordialement,">Contacte-moi</a> pour corriger cela.
</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">
<img class="img-thumbnail" src="https://photos.cri.epita.fr/thumb/{$user.login}" alt="avatar {$user.login}" style="max-height: 150px">
</div>
</div>
<ValidateSubmissions />
<p class="lead">Voici la liste des questionnaires&nbsp;:</p>
{:else}
<p class="card-text lead">
Vous voici arrivés sur le site dédié aux cours d'<a href="https://adlin.nemunai.re/">Administration Linux avancée</a>, du <a href="https://srs.nemunai.re/fic/">FIC</a> et de <a href="https://virli.nemunai.re/">Virtualisation légère</a>.
</p>
<p class="card-text">
Vous devez <a href="auth/CRI" target="_self">vous identifier</a> pour accéder au contenu.
</p>
{/if}
<SurveyList bind:direct={direct} />
</div>
</div>

View file

@ -1,39 +0,0 @@
<script context="module">
import { getSurvey } from '../../../lib/surveys';
export async function load({ params, stuff }) {
const survey = getSurvey(params.sid);
return {
props: {
survey,
},
stuff: {
...stuff,
survey,
}
};
}
</script>
<script lang="ts">
export let survey;
</script>
{#await survey}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement du questionnaire &hellip;</span>
</div>
{:then}
<slot></slot>
{:catch error}
<div class="text-center">
<h2>
<a href="surveys/" class="text-muted" style="text-decoration: none">&lt;</a>
Questionnaire introuvable
</h2>
<span>{error}</span>
</div>
{/await}

View file

@ -1,370 +0,0 @@
<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"}')} }
>
<i class="bi bi-pause-fill"></i>
</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 + '}')} }
>
<i class="bi bi-play-fill"></i>
</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

@ -1,66 +0,0 @@
<script context="module">
import { getSurvey } from '../../../lib/surveys';
export async function load({ params, stuff }) {
return {
props: {
surveyP: stuff.survey,
},
};
}
</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';
import SurveyQuestions from '../../../components/SurveyQuestions.svelte';
import { getSurvey } from '../../../lib/surveys';
import { getQuestions } from '../../../lib/questions';
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"><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>
<a href="surveys/" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
</h2>
<SurveyBadge class="ms-2" {survey} />
</div>
{#if $user.is_admin && edit}
<SurveyAdmin {survey} on:saved={() => edit = false} />
{/if}
{#await getQuestions(survey.id)}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des questions &hellip;</span>
</div>
{:then questions}
<SurveyQuestions {survey} {questions} />
{/await}
{/await}

View file

@ -1,151 +0,0 @@
<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 { ToastsStore } from '../../../stores/toasts';
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;
let req_question;
let nosend = false;
function afterQUpdate(q) {
value = undefined;
if (q) {
q.getMyResponse().then((response) => {
if (response && response.value)
value = response.value;
})
}
}
$: {
if (show_question) {
req_question = getQuestion(show_question);
req_question.then(afterQUpdate);
}
}
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 && !nosend) {
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;
ToastsStore.addErrorToast({
msg: "Une erreur s'est produite durant l'envoi de vos réponses : " + error + "\nVeuillez 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 req_question}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement d'une nouvelle question &hellip;</span>
</div>
{: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>
{#if question.kind != 'mcq' && question.kind != 'ucq'}
<button
class="btn btn-primary"
>
Soumettre la réponse
</button>
{/if}
{/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}

View file

@ -1,156 +0,0 @@
<script context="module">
import { getSurvey } from '../../../../lib/surveys';
export async function load({ params, stuff }) {
return {
props: {
surveyP: stuff.survey,
rid: params.rid,
},
};
}
</script>
<script lang="ts">
import Correction from '../../../../components/Correction.svelte';
import CorrectionReference from '../../../../components/CorrectionReference.svelte';
import SurveyBadge from '../../../../components/SurveyBadge.svelte';
import QuestionHeader from '../../../../components/QuestionHeader.svelte';
import { getCorrectionTemplates } from '../../../../lib/correctionTemplates';
import { getQuestion } from '../../../../lib/questions';
export let surveyP;
export let rid;
let showResponses = false;
let showStudent = false;
let notCorrected = false;
let nb_responses = 0;
let child;
let waitApply = false;
let ctpls = getCorrectionTemplates(rid);
let filter = "";
let cts = { };
ctpls.then((ctpls) => {
for (const tpl of ctpls) {
cts[tpl.id] = { };
tpl.getCorrections().then((c) => {
if (c) {
for (const d of c) {
cts[tpl.id][d.id_user] = d;
}
}
})
}
cts = cts;
});
</script>
{#await surveyP then survey}
{#await getQuestion(rid)}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement de la question&hellip;</span>
</div>
{:then question}
{#await ctpls}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement de la question&hellip;</span>
</div>
{:then correctionTemplates}
<div class="float-end">
<input
class="form-control"
placeholder="filtre"
bind:value={filter}
>
</div>
<div class="d-flex align-items-center">
<h2>
<a href="surveys/{survey.id}/responses" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
<small class="text-muted">Corrections</small>
</h2>
<SurveyBadge class="ms-2" {survey} />
</div>
<div class="card sticky-top">
<QuestionHeader
{question}
>
<button
class="btn btn-sm btn-link float-start"
on:click={() => showResponses = !showResponses}
>
<i
class="bi"
class:bi-chevron-right={!showResponses}
class:bi-chevron-down={showResponses}
></i>
</button>
{#if showResponses}
<button
type="button"
class="btn btn-sm btn-success float-end ms-3 me-1"
title="Appliquer les corrections par regexp"
on:click={() => {waitApply = true; child.applyCorrections().then(() => { waitApply = false; })} }
disabled={waitApply}
>
{#if waitApply}
<div class="spinner-border spinner-border-sm" role="status"></div>
{:else}
<i class="bi bi-check-all"></i>
{/if}
</button>
{/if}
<button
type="button"
class="btn btn-sm float-end mx-1"
class:btn-outline-info={!showStudent}
class:btn-info={showStudent}
on:click={() => showStudent = !showStudent}
title="Afficher les étudiants"
>
<i class="bi bi-people"></i>
</button>
<button
type="button"
class="btn btn-sm float-end mx-1"
class:btn-outline-info={!notCorrected}
class:btn-info={notCorrected}
on:click={() => notCorrected = !notCorrected}
title="Afficher les réponses corrigées"
>
<i class="bi bi-files"></i>
</button>
</QuestionHeader>
{#if showResponses}
<CorrectionReference
class="card-body"
{cts}
bind:filter={filter}
{nb_responses}
{question}
templates={correctionTemplates}
/>
{/if}
</div>
<Correction
{cts}
{filter}
{question}
{showStudent}
{notCorrected}
bind:child={child}
templates={correctionTemplates}
on:nb_responses={(v) => { nb_responses = v.detail; } }
/>
{/await}
{/await}
{/await}
<div class="mb-5"></div>

View file

@ -1,85 +0,0 @@
<script context="module">
import { getSurvey } from '../../../../lib/surveys';
export async function load({ params, stuff }) {
return {
props: {
surveyP: stuff.survey,
},
};
}
</script>
<script lang="ts">
import SurveyBadge from '../../../../components/SurveyBadge.svelte';
import SurveyQuestions from '../../../../components/SurveyQuestions.svelte';
import { getSurvey } from '../../../../lib/surveys';
import { getQuestions } from '../../../../lib/questions';
export let surveyP;
</script>
{#await surveyP then survey}
<div class="d-flex align-items-center">
<h2>
<a href="surveys/{survey.id}" class="text-muted" style="text-decoration: none">&lt;</a>
{survey.title}
<small class="text-muted">Corrections</small>
</h2>
<SurveyBadge class="ms-2" {survey} />
</div>
{#await getQuestions(survey.id)}
<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 mt-3 mb-5">
<table class="table table-hover table-striped mb-0">
<thead>
<tr>
<th>Question</th>
<th>Réponses</th>
<th>Moyenne</th>
</tr>
</thead>
<tbody ng-controller="SurveyGradesController">
{#each questions as question (question.id)}
<tr ng-click="showResponses()" ng-controller="ResponsesController">
<td><a href="surveys/{survey.id}/responses/{question.id}">{question.title}</a></td>
{#await question.getResponses()}
<td colspan="2" class="text-center">
<div class="spinner-border mx-3" role="status"></div>
<span>Chargement &hellip;</span>
</td>
{:then responses}
<td>
{#if responses}
{responses.filter((r) => !r.time_scored).length} /
{responses.length}
{:else}
0
{/if}
</td>
<td>
{#if responses && responses.filter((r) => r.time_scored).length}
{responses.reduce((p, c) => (p + c.score?c.score:0), 0)/responses.filter((r) => r.time_scored).length}
{:else}
--&nbsp;%
{/if}
</td>
{/await}
</tr>
{/each}
</tbody>
<tfoot>
<tr>
<th colspan="2">Moyenne</th>
<th><!--{mean}-->&nbsp;%</th>
</tr>
</tfoot>
</table>
</div>
{/await}
{/await}

View file

@ -1,8 +0,0 @@
<script lang="ts">
import { user } from '../../stores/user';
import SurveyList from '../../components/SurveyList.svelte';
</script>
<div class="card bg-light">
<SurveyList />
</div>

View file

@ -1,111 +0,0 @@
<script context="module">
export async function load({ params }) {
return {
props: {
uid: params.uid,
}
};
}
</script>
<script lang="ts">
import UserSurveys from '../../../components/UserSurveys.svelte';
import { user } from '../../../stores/user';
import { getSurveys } from '../../../lib/surveys';
import { getUser, getUserGrade, getUserScore } from '../../../lib/users';
export let uid;
let allPromos = false;
</script>
{#await getUser(uid)}
<h2>
Étudiant
</h2>
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des d&eacute;tails&hellip;
</div>
{:then student}
<div class="card mb-5">
<div class="card-header d-flex justify-content-center align-items-center">
<h2 class="card-title text-center">
<i class="bi bi-person-fill"></i>
{#if student.firstname}
{student.firstname} {student.lastname}
{:else}
{student.login}
{/if}
</h2>
{#if student.promo}
<span class="badge bg-success ms-1">{student.promo}</span>
{/if}
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3 text-center">
<a href="https://photos.cri.epita.fr/{student.login}" target="_blank">
<img src="https://photos.cri.epita.fr/thumb/{student.login}" alt="avatar" class="img-thumbnail" style="max-height: 250px">
</a>
</div>
<div class="col">
<dl class="row">
<dt class="col-2">ID</dt>
<dd class="col-10">{student.id}</dd>
<dt class="col-2">Login</dt>
<dd class="col-10">
<a href="//cri.epita.fr/users/{student.login}" target="_blank">
{student.login}
</a>
</dd>
<dt class="col-2">E-mail</dt>
<dd class="col-10"><a href="mailto:{student.email}">{student.email}</a></dd>
<dt class="col-2">Nom</dt>
<dd class="col-10">{student.lastname}</dd>
<dt class="col-2">Prénom</dt>
<dd class="col-10">{student.firstname}</dd>
<dt class="col-2">Inscription</dt>
<dd class="col-10">{student.time}</dd>
<dt class="col-2">Groupes</dt>
<dd class="col-10">
<ul ng-if="student.groups">
{#each student.groups.split(',').slice(1) as g, gid (gid)}
<li>
<a href="https://cri.epita.fr/group/{g}/">{g}</a>
</li>
{/each}
</ul>
</dd>
<dt class="col-2">Admin</dt>
<dd class="col-10">{student.is_admin?"Oui":"Non"}</dd>
</dl>
</div>
</div>
</div>
<div class="card-header">
<button
class="btn btn-secondary float-end"
class:active={allPromos}
on:click={e => allPromos = !allPromos}
>
Toutes les promos
</button>
<h3 class="card-title">
Questionnaires
</h3>
</div>
<UserSurveys
{student}
{allPromos}
/>
</div>
{/await}

View file

@ -1,64 +0,0 @@
<script context="module">
export async function load({ params }) {
return {
props: {
sid: params.sid,
uid: params.uid,
}
};
}
</script>
<script lang="ts">
import SurveyBadge from '../../../../components/SurveyBadge.svelte';
import SurveyQuestions from '../../../../components/SurveyQuestions.svelte';
import { getSurvey } from '../../../../lib/surveys';
import { getQuestions } from '../../../../lib/questions';
import { getUser } from '../../../../lib/users';
export let sid;
export let uid;
</script>
{#await getUser(uid)}
<h2>
Étudiant
</h2>
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des d&eacute;tails&hellip;
</div>
{:then student}
{#await getSurvey(sid)}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement du questionnaire &hellip;</span>
</div>
{:then survey}
<div class="d-flex align-items-center">
<h2>
<a href="users/{student.id}/surveys/" class="text-muted" style="text-decoration: none">{student.login}</a> /
{survey.title}
</h2>
<SurveyBadge class="ms-2" {survey} />
</div>
{#await getQuestions(survey.id)}
<div class="text-center">
<div class="spinner-border text-primary mx-3" role="status"></div>
<span>Chargement des questions &hellip;</span>
</div>
{:then questions}
<SurveyQuestions {survey} {questions} id_user={uid} />
{/await}
{:catch error}
<div class="text-center">
<h2>
<a href="surveys/" class="text-muted" style="text-decoration: none">&lt;</a>
Questionnaire introuvable
</h2>
<span>{error}</span>
</div>
{/await}
{/await}

View file

@ -1,64 +0,0 @@
<script context="module">
export async function load({ params }) {
return {
props: {
uid: params.uid,
}
};
}
</script>
<script lang="ts">
import UserSurveys from '../../../../components/UserSurveys.svelte';
import { user } from '../../../../stores/user';
import { getSurveys } from '../../../../lib/surveys';
import { getUser, getUserGrade, getUserScore } from '../../../../lib/users';
export let uid;
let allPromos = false;
</script>
{#await getUser(uid)}
<h2>
Étudiant
</h2>
<div class="d-flex justify-content-center">
<div class="spinner-border me-2" role="status"></div>
Chargement des d&eacute;tails&hellip;
</div>
{:then student}
<div class="card mb-5">
<div class="card-header d-flex justify-content-center align-items-center">
<h2 class="card-title text-center">
<a href="users/{student.id}"><i class="bi bi-chevron-left"></i></a>
<i class="bi bi-person-fill"></i>
{#if student.firstname}
{student.firstname} {student.lastname}
{:else}
{student.login}
{/if}
</h2>
{#if student.promo}
<span class="badge bg-success ms-1">{student.promo}</span>
{/if}
</div>
<div class="card-header">
<button
class="btn btn-secondary float-end"
class:active={allPromos}
on:click={e => allPromos = !allPromos}
>
Toutes les promos
</button>
<h3 class="card-title">
Questionnaires
</h3>
</div>
<UserSurveys
{student}
{allPromos}
/>
</div>
{/await}

View file

@ -1,79 +0,0 @@
<script>
import { goto } from '$app/navigation';
import { user } from '../../stores/user';
import DateFormat from '../../components/DateFormat.svelte';
import { getUsers, getPromos } from '../../lib/users';
function showUser(user) {
goto(`users/${user.id}`)
}
let filterPromo = "";
</script>
{#if $user && $user.is_admin}
<a href="grades" class="btn btn-success ml-1 float-end" title="Notes">
<i class="bi bi-files"></i>
</a>
{#await getPromos() then promos}
<div class="float-end me-2">
<select class="form-select" bind:value={filterPromo}>
<option value="">-</option>
{#each promos as promo, pid (pid)}
<option value={promo}>{promo}</option>
{/each}
</select>
</div>
{/await}
{/if}
<h2>
Étudiants
</h2>
{#await getUsers()}
<div class="text-center">
<div class="spinner-border text-danger mx-3" role="status"></div>
<span>Chargement des &eacute;tudiants &hellip;</span>
</div>
{:then users}
<table class="table table-sm table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Login</th>
<th>E-mail</th>
<th>Nom</th>
<th>Prénom</th>
<th>Date d'inscription</th>
<th>Promo</th>
</tr>
</thead>
<tbody>
{#each users.filter((u) => (filterPromo === "" || filterPromo === u.promo)) as u, uid (u.id)}
<tr
class:bg-danger={u.is_admin}
>
<td>
{u.id}
<div class="d-inline-block float-end" style="max-height: 1.5em">
<img src="//photos.cri.epita.fr/square/{u.login}" alt={u.login} style="max-height: 2.5em; margin-top: -0.33em">
</div>
</td>
<td>
<a href="users/{u.login}">{u.login}</a>
</td>
<td>
<a href="mailto:{u.email}">{u.email}</a>
</td>
<td>{u.lastname}</td>
<td>{u.firstname}</td>
<td>
<DateFormat date={u.time} dateStyle="short" timeStyle="medium" />
</td>
<td>{u.promo}</td>
</tr>
{/each}
</tbody>
</table>
{/await}

View file

@ -1,41 +0,0 @@
import { writable } from 'svelte/store';
function createToastsStore() {
const { subscribe, set, update } = writable({toasts: []});
const addToast = (o) => {
o.timestamp = new Date();
o.close = () => {
update((i) => {
i.toasts = i.toasts.filter((j) => {
return !(j.title === o.title && j.msg === o.msg && j.timestamp === o.timestamp)
});
return i;
});
}
update((i) => {
i.toasts.unshift(o);
return i;
});
o.cancel = setTimeout(o.close, o.dismiss?o.dismiss:5000);
};
const addErrorToast = (o) => {
if (!o.title) o.title = 'Une erreur est survenue !';
if (!o.color) o.color = 'danger';
return addToast(o);
};
return {
subscribe,
addToast,
addErrorToast,
};
}
export const ToastsStore = createToastsStore();

View file

@ -1,27 +0,0 @@
import { writable } from 'svelte/store';
function createUserStore() {
const { subscribe, set, update } = writable(undefined);
return {
subscribe,
set: (auth) => {
update((m) => auth);
},
update: (res_auth, cb=null) => {
if (res_auth.status === 200) {
res_auth.json().then((auth) => {
update((m) => (Object.assign(m?m:{}, auth)));
if (cb) {
cb(my);
}
});
} else if (res_auth.status >= 400 && res_auth.status < 500) {
update((m) => (null));
}
},
};
}
export const user = createUserStore();

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,13 +0,0 @@
import preprocess from 'svelte-preprocess';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
}
};
export default config;

View file

@ -1,31 +0,0 @@
{
"compilerOptions": {
"moduleResolution": "node",
"module": "es2020",
"lib": ["es2020", "DOM"],
"target": "es2019",
/**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
to enforce using \`import type\` instead of \`import\` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
To have warnings/errors of the Svelte compiler at the correct position,
enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"allowJs": true,
"checkJs": true,
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
}

52
auth.go
View file

@ -3,7 +3,6 @@ package main
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"time" "time"
@ -26,41 +25,40 @@ func init() {
router.POST("/api/auth/logout", apiRawHandler(logout)) router.POST("/api/auth/logout", apiRawHandler(logout))
} }
type authToken struct {
*User
CurrentPromo uint `json:"current_promo"`
}
func validateAuthToken(u *User, _ httprouter.Params, _ []byte) HTTPResponse { func validateAuthToken(u *User, _ httprouter.Params, _ []byte) HTTPResponse {
if u == nil { return APIResponse{u}
return APIErrorResponse{status: http.StatusUnauthorized, err: fmt.Errorf("Not connected")}
} else {
return APIResponse{authToken{u, currentPromo}}
}
} }
func logout(w http.ResponseWriter, ps httprouter.Params, body []byte) HTTPResponse { func logout(w http.ResponseWriter, ps httprouter.Params, body []byte) HTTPResponse {
eraseCookie(w) http.SetCookie(w, &http.Cookie{
Name: "auth",
Value: "",
Path: baseURL + "/",
Expires: time.Unix(0, 0),
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
return APIResponse{true} return APIResponse{true}
} }
func completeAuth(w http.ResponseWriter, username string, email string, firstname string, lastname string, groups string, session *Session) (usr User, err error) { func completeAuth(w http.ResponseWriter, username string, email string, firstname string, lastname string, groups string, session *Session) (err error) {
var usr User
if !userExists(username) { if !userExists(username) {
if usr, err = NewUser(username, email, firstname, lastname, groups); err != nil { if usr, err = NewUser(username, email, firstname, lastname, groups); err != nil {
return return err
} }
} else if usr, err = getUserByLogin(username); err != nil { } else if usr, err = getUserByLogin(username); err != nil {
return return err
} }
if len(groups) > 0 { if len(groups) > 255 {
if len(groups) > 255 { groups = groups[:255]
groups = groups[:255] }
} if usr.Groups != groups {
if usr.Groups != groups { usr.Groups = groups
usr.Groups = groups usr.Update()
usr.Update()
}
} }
if session == nil { if session == nil {
@ -72,7 +70,7 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
} }
if err != nil { if err != nil {
return return err
} }
http.SetCookie(w, &http.Cookie{ http.SetCookie(w, &http.Cookie{
@ -80,12 +78,12 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
Value: base64.StdEncoding.EncodeToString(session.Id), Value: base64.StdEncoding.EncodeToString(session.Id),
Path: baseURL + "/", Path: baseURL + "/",
Expires: time.Now().Add(30 * 24 * time.Hour), Expires: time.Now().Add(30 * 24 * time.Hour),
Secure: true,
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteStrictMode, SameSite: http.SameSiteStrictMode,
//Secure: true,
}) })
return return nil
} }
func dummyAuth(w http.ResponseWriter, _ httprouter.Params, body []byte) (interface{}, error) { func dummyAuth(w http.ResponseWriter, _ httprouter.Params, body []byte) (interface{}, error) {
@ -94,5 +92,5 @@ func dummyAuth(w http.ResponseWriter, _ httprouter.Params, body []byte) (interfa
return nil, err return nil, err
} }
return completeAuth(w, lf["username"], lf["email"], lf["firstname"], lf["lastname"], "", nil) return map[string]string{"status": "OK"}, completeAuth(w, lf["login"], lf["email"], lf["firstname"], lf["lastname"], "", nil)
} }

View file

@ -73,6 +73,6 @@ func checkAuthKrb5(w http.ResponseWriter, _ httprouter.Params, body []byte) (int
} }
return nil, err return nil, err
} else { } else {
return completeAuth(w, lf.Login, lf.Login+"@epita.fr", "", "", "", nil) return dummyAuth(w, nil, body)
} }
} }

View file

@ -116,7 +116,7 @@ func OIDC_CRI_complete(w http.ResponseWriter, r *http.Request, ps httprouter.Par
} }
} }
if _, err := completeAuth(w, claims.Username, claims.Email, claims.Firstname, claims.Lastname, groups, &session); err != nil { if err := completeAuth(w, claims.Username, claims.Email, claims.Firstname, claims.Lastname, groups, &session); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }

View file

@ -23,7 +23,7 @@ func init() {
return APIErrorResponse{err: err} return APIErrorResponse{err: err}
} }
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.RegExp, new.Score, new.ScoreExplaination)) return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.Score, new.ScoreExplaination))
}), adminRestricted)) }), adminRestricted))
router.GET("/api/surveys/:sid/questions/:qid/corrections/:cid", apiHandler(correctionHandler( router.GET("/api/surveys/:sid/questions/:qid/corrections/:cid", apiHandler(correctionHandler(
@ -51,48 +51,6 @@ func init() {
return formatApiResponse(ct.Delete()) return formatApiResponse(ct.Delete())
}), adminRestricted)) }), adminRestricted))
router.GET("/api/questions/:qid/corrections", apiHandler(questionHandler(
func(q Question, _ []byte) HTTPResponse {
if cts, err := q.GetCorrectionTemplates(); err != nil {
return APIErrorResponse{err: err}
} else {
return APIResponse{cts}
}
}), adminRestricted))
router.POST("/api/questions/:qid/corrections", apiHandler(questionHandler(func(q Question, body []byte) HTTPResponse {
var new CorrectionTemplate
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.RegExp, new.Score, new.ScoreExplaination))
}), adminRestricted))
router.GET("/api/questions/:qid/corrections/:cid", apiHandler(correctionHandler(
func(ct CorrectionTemplate, _ []byte) HTTPResponse {
if users, err := ct.GetUserCorrected(); err != nil {
return APIErrorResponse{err: err}
} else {
return APIResponse{users}
}
}), adminRestricted))
router.PUT("/api/questions/:qid/corrections/:cid", apiHandler(correctionHandler(func(current CorrectionTemplate, body []byte) HTTPResponse {
var new CorrectionTemplate
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
new.Id = current.Id
if err := new.Update(); err != nil {
return APIErrorResponse{err: err}
} else {
return APIResponse{new}
}
}), adminRestricted))
router.DELETE("/api/questions/:qid/corrections/:cid", apiHandler(correctionHandler(func(ct CorrectionTemplate, body []byte) HTTPResponse {
return formatApiResponse(ct.Delete())
}), adminRestricted))
router.GET("/api/users/:uid/questions/:qid", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse { router.GET("/api/users/:uid/questions/:qid", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return userHandler(func(u User, _ []byte) HTTPResponse { return userHandler(func(u User, _ []byte) HTTPResponse {
if qid, err := strconv.Atoi(string(ps.ByName("qid"))); err != nil { if qid, err := strconv.Atoi(string(ps.ByName("qid"))); err != nil {
@ -117,14 +75,6 @@ func init() {
return formatApiResponse(u.NewCorrection(new.IdTemplate)) return formatApiResponse(u.NewCorrection(new.IdTemplate))
}), adminRestricted)) }), adminRestricted))
router.PUT("/api/users/:uid/corrections", apiHandler(userHandler(func(u User, body []byte) HTTPResponse {
var new map[int64]bool
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
return formatApiResponse(u.EraseCorrections(new))
}), adminRestricted))
router.DELETE("/api/users/:uid/corrections/:cid", apiHandler(userCorrectionHandler(func(u User, uc UserCorrection, body []byte) HTTPResponse { router.DELETE("/api/users/:uid/corrections/:cid", apiHandler(userCorrectionHandler(func(u User, uc UserCorrection, body []byte) HTTPResponse {
return formatApiResponse(uc.Delete(u)) return formatApiResponse(uc.Delete(u))
}), adminRestricted)) }), adminRestricted))
@ -162,20 +112,19 @@ type CorrectionTemplate struct {
Id int64 `json:"id"` Id int64 `json:"id"`
IdQuestion int64 `json:"id_question"` IdQuestion int64 `json:"id_question"`
Label string `json:"label"` Label string `json:"label"`
RegExp string `json:"regexp"`
Score int `json:"score"` Score int `json:"score"`
ScoreExplaination string `json:"score_explaination,omitempty"` ScoreExplaination string `json:"score_explaination,omitempty"`
} }
func (q *Question) GetCorrectionTemplates() (ct []CorrectionTemplate, err error) { func (q *Question) GetCorrectionTemplates() (ct []CorrectionTemplate, err error) {
if rows, errr := DBQuery("SELECT id_template, id_question, label, re, score, score_explanation FROM correction_templates WHERE id_question=?", q.Id); errr != nil { if rows, errr := DBQuery("SELECT id_template, id_question, label, score, score_explanation FROM correction_templates WHERE id_question=?", q.Id); errr != nil {
return nil, errr return nil, errr
} else { } else {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var c CorrectionTemplate var c CorrectionTemplate
if err = rows.Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination); err != nil { if err = rows.Scan(&c.Id, &c.IdQuestion, &c.Label, &c.Score, &c.ScoreExplaination); err != nil {
return return
} }
ct = append(ct, c) ct = append(ct, c)
@ -189,27 +138,27 @@ func (q *Question) GetCorrectionTemplates() (ct []CorrectionTemplate, err error)
} }
func (q *Question) GetCorrectionTemplate(id int) (c CorrectionTemplate, err error) { func (q *Question) GetCorrectionTemplate(id int) (c CorrectionTemplate, err error) {
err = DBQueryRow("SELECT id_template, id_question, label, re, score, score_explanation FROM correction_templates WHERE id_question=? AND id_template=?", q.Id, id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination) err = DBQueryRow("SELECT id_template, id_question, label, score, score_explanation FROM correction_templates WHERE id_question=? AND id_template=?", q.Id, id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.Score, &c.ScoreExplaination)
return return
} }
func GetCorrectionTemplate(id int64) (c CorrectionTemplate, err error) { func GetCorrectionTemplate(id int64) (c CorrectionTemplate, err error) {
err = DBQueryRow("SELECT id_template, id_question, label, re, score, score_explanation FROM correction_templates WHERE id_template=?", id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination) err = DBQueryRow("SELECT id_template, id_question, label, score, score_explanation FROM correction_templates WHERE id_template=?", id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.Score, &c.ScoreExplaination)
return return
} }
func (q *Question) NewCorrectionTemplate(label string, regexp string, score int, score_explaination string) (CorrectionTemplate, error) { func (q *Question) NewCorrectionTemplate(label string, score int, score_explaination string) (CorrectionTemplate, error) {
if res, err := DBExec("INSERT INTO correction_templates (id_question, label, re, score, score_explanation) VALUES (?, ?, ?, ?, ?)", q.Id, label, regexp, score, score_explaination); err != nil { if res, err := DBExec("INSERT INTO correction_templates (id_question, label, score, score_explanation) VALUES (?, ?, ?, ?)", q.Id, label, score, score_explaination); err != nil {
return CorrectionTemplate{}, err return CorrectionTemplate{}, err
} else if cid, err := res.LastInsertId(); err != nil { } else if cid, err := res.LastInsertId(); err != nil {
return CorrectionTemplate{}, err return CorrectionTemplate{}, err
} else { } else {
return CorrectionTemplate{cid, q.Id, label, regexp, score, score_explaination}, nil return CorrectionTemplate{cid, q.Id, label, score, score_explaination}, nil
} }
} }
func (t *CorrectionTemplate) Update() error { func (t *CorrectionTemplate) Update() error {
_, err := DBExec("UPDATE correction_templates SET id_question = ?, label = ?, re = ?, score = ?, score_explanation = ? WHERE id_template = ?", t.IdQuestion, t.Label, t.RegExp, t.Score, t.ScoreExplaination, t.Id) _, err := DBExec("UPDATE correction_templates SET id_question = ?, label = ?, score = ?, score_explanation = ? WHERE id_template = ?", t.IdQuestion, t.Label, t.Score, t.ScoreExplaination, t.Id)
return err return err
} }
@ -320,26 +269,6 @@ func (u *User) NewCorrection(id int64) (*UserCorrectionSummary, error) {
} }
} }
func (u *User) EraseCorrections(ids map[int64]bool) (*UserCorrectionSummary, error) {
var lastid int64
for id, st := range ids {
lastid = id
if st {
DBExec("INSERT INTO student_corrected (id_user, id_template) VALUES (?, ?)", u.Id, id)
} else {
DBExec("DELETE FROM student_corrected WHERE id_user = ? AND id_template = ?", u.Id, id)
}
}
if ucs, err := u.ComputeScoreQuestion(lastid); err != nil {
return nil, err
} else {
return ucs, nil
}
}
func (c *UserCorrection) Delete(u User) (*UserCorrectionSummary, error) { func (c *UserCorrection) Delete(u User) (*UserCorrectionSummary, error) {
if res, err := DBExec("DELETE FROM student_corrected WHERE id_correction = ?", c.Id); err != nil { if res, err := DBExec("DELETE FROM student_corrected WHERE id_correction = ?", c.Id); err != nil {
return nil, err return nil, err

4
db.go
View file

@ -83,7 +83,6 @@ CREATE TABLE IF NOT EXISTS surveys(
title VARCHAR(255), title VARCHAR(255),
promo MEDIUMINT NOT NULL, promo MEDIUMINT NOT NULL,
shown BOOLEAN NOT NULL DEFAULT FALSE, shown BOOLEAN NOT NULL DEFAULT FALSE,
direct INTEGER DEFAULT NULL,
corrected BOOLEAN NOT NULL DEFAULT FALSE, corrected BOOLEAN NOT NULL DEFAULT FALSE,
start_availability TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, start_availability TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
end_availability TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP end_availability TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
@ -137,7 +136,6 @@ CREATE TABLE IF NOT EXISTS correction_templates(
id_template INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, id_template INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
id_question INTEGER NOT NULL, id_question INTEGER NOT NULL,
label VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL,
re VARCHAR(255) NOT NULL,
score INTEGER, score INTEGER,
score_explanation TEXT, score_explanation TEXT,
FOREIGN KEY(id_question) REFERENCES survey_quests(id_question) FOREIGN KEY(id_question) REFERENCES survey_quests(id_question)
@ -157,7 +155,7 @@ CREATE TABLE IF NOT EXISTS student_corrected(
return err return err
} }
if _, err := db.Exec(` if _, err := db.Exec(`
CREATE VIEW IF NOT EXISTS student_scores AS SELECT T.id_user, T.id_survey, Q.id_question, MAX(R.score) AS score FROM (SELECT DISTINCT R.id_user, S.id_survey FROM survey_responses R INNER JOIN survey_quests Q ON R.id_question = Q.id_question INNER JOIN surveys S ON Q.id_survey = S.id_survey) T LEFT OUTER JOIN survey_quests Q ON T.id_survey = Q.id_survey LEFT OUTER JOIN survey_responses R ON R.id_user = T.id_user AND Q.id_question = R.id_question GROUP BY id_user, id_survey, id_question; CREATE VIEW IF NOT EXISTS student_scores AS SELECT U.id_user, id_survey, Q.id_question, MAX(R.score) as score FROM survey_quests Q CROSS JOIN users U LEFT JOIN survey_responses R ON Q.id_question = R.id_question AND R.id_user = U.id_user GROUP BY Q.id_question, U.id_user;
`); err != nil { `); err != nil {
return err return err
} }

358
direct.go
View file

@ -1,358 +0,0 @@
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
}
}
}

4
go.mod
View file

@ -11,8 +11,8 @@ require (
github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 github.com/russross/blackfriday/v2 v2.1.0
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect
nhooyr.io/websocket v1.8.7
) )

76
go.sum
View file

@ -41,35 +41,17 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= 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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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.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.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/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/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
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 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-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-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 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
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 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
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/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-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -108,7 +90,6 @@ 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.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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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 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/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= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -123,8 +104,6 @@ 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/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/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/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
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 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 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= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -136,32 +115,21 @@ github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA= github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA=
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= 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 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
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.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/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 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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/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/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
@ -171,14 +139,9 @@ 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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 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/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
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.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/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= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -252,15 +215,27 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 h1:Ati8dO7+U7mxpkPSxBZQEvzHVUYB/MqCklCN8ig5w/o=
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210817223510-7df4dd6e12ab h1:llrcWN/wOwO+6gAyfBzxb5hZ+c3mriU/0+KNgYu6adA=
golang.org/x/oauth2 v0.0.0-20210817223510-7df4dd6e12ab/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 h1:B333XXssMuKQeBwiNODx4TupZy7bf4sxFZnN2ZOcvUE=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5 h1:v79phzBz03tsVCUTbvTBmmC3CUXF5mKYt7DA4ZVldpM=
golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -282,7 +257,6 @@ 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-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-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-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-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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -296,17 +270,14 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -434,13 +405,12 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 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 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@ -450,8 +420,6 @@ 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-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.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/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/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/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View file

@ -8,7 +8,6 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"time"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
) )
@ -19,6 +18,7 @@ func Router() *httprouter.Router {
return router return router
} }
type HTTPResponse interface { type HTTPResponse interface {
WriteResponse(http.ResponseWriter) WriteResponse(http.ResponseWriter)
} }
@ -62,26 +62,10 @@ func (r APIErrorResponse) WriteResponse(w http.ResponseWriter) {
http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", r.err.Error()), r.status) http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", r.err.Error()), r.status)
} }
type DispatchFunction func(httprouter.Params, []byte) HTTPResponse type DispatchFunction func(httprouter.Params, []byte) HTTPResponse
func eraseCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: "auth",
Value: "",
Path: baseURL + "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
}
func rawHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params, []byte), access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) { 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) { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if addr := r.Header.Get("X-Forwarded-For"); addr != "" { if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
r.RemoteAddr = addr r.RemoteAddr = addr
@ -92,19 +76,16 @@ func rawAuthHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params
var user *User = nil var user *User = nil
if cookie, err := r.Cookie("auth"); err == nil { if cookie, err := r.Cookie("auth"); err == nil {
if sessionid, err := base64.StdEncoding.DecodeString(cookie.Value); err != nil { if sessionid, err := base64.StdEncoding.DecodeString(cookie.Value); err != nil {
eraseCookie(w)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusNotAcceptable) http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusNotAcceptable)
return return
} else if session, err := getSession(sessionid); err != nil { } else if session, err := getSession(sessionid); err != nil {
eraseCookie(w)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusUnauthorized) http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusUnauthorized)
return return
} else if session.IdUser == nil { } else if session.IdUser == nil {
user = nil user = nil
} else if std, err := getUser(int(*session.IdUser)); err != nil { } else if std, err := getUser(int(*session.IdUser)); err != nil {
eraseCookie(w)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusUnauthorized) http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusUnauthorized)
return return
@ -142,7 +123,7 @@ func rawAuthHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params
} }
} }
f(w, r, ps, user, body) f(w, r, ps, body)
} }
} }
@ -153,22 +134,22 @@ func formatResponseHandler(f func(*http.Request, httprouter.Params, []byte) HTTP
} }
func apiRawHandler(f func(http.ResponseWriter, httprouter.Params, []byte) HTTPResponse, access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) { func apiRawHandler(f func(http.ResponseWriter, httprouter.Params, []byte) HTTPResponse, access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return rawHandler(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params, b []byte) { return rawHandler(func (w http.ResponseWriter, r *http.Request, ps httprouter.Params, b []byte) {
formatResponseHandler(func(_ *http.Request, ps httprouter.Params, b []byte) HTTPResponse { formatResponseHandler(func (_ *http.Request, ps httprouter.Params, b []byte) HTTPResponse {
return f(w, ps, b) return f(w, ps, b)
})(w, r, ps, b) })(w, r, ps, b)
}, access...) }, access...)
} }
func apiHandler(f DispatchFunction, access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) { func apiHandler(f DispatchFunction, access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return rawHandler(formatResponseHandler(func(_ *http.Request, ps httprouter.Params, b []byte) HTTPResponse { return f(ps, b) }), access...) return rawHandler(formatResponseHandler(func (_ *http.Request, ps httprouter.Params, b []byte) HTTPResponse { return f(ps, b) }), access...)
} }
func formatApiResponse(i interface{}, err error) HTTPResponse { func formatApiResponse(i interface{}, err error) HTTPResponse {
if err != nil { if err != nil {
return APIErrorResponse{ return APIErrorResponse{
status: http.StatusBadRequest, status: http.StatusBadRequest,
err: err, err: err,
} }
} else { } else {
return APIResponse{i} return APIResponse{i}
@ -176,25 +157,25 @@ func formatApiResponse(i interface{}, err error) HTTPResponse {
} }
func apiAuthHandler(f func(*User, httprouter.Params, []byte) HTTPResponse, access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) { func apiAuthHandler(f func(*User, httprouter.Params, []byte) HTTPResponse, access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return rawHandler(formatResponseHandler(func(r *http.Request, ps httprouter.Params, b []byte) HTTPResponse { return rawHandler(formatResponseHandler(func (r *http.Request, ps httprouter.Params, b []byte) HTTPResponse {
if cookie, err := r.Cookie("auth"); err != nil { if cookie, err := r.Cookie("auth"); err != nil {
return f(nil, ps, b) return f(nil, ps, b)
} else if sessionid, err := base64.StdEncoding.DecodeString(cookie.Value); err != nil { } else if sessionid, err := base64.StdEncoding.DecodeString(cookie.Value); err != nil {
return APIErrorResponse{ return APIErrorResponse{
status: http.StatusBadRequest, status: http.StatusBadRequest,
err: err, err: err,
} }
} else if session, err := getSession(sessionid); err != nil { } else if session, err := getSession(sessionid); err != nil {
return APIErrorResponse{ return APIErrorResponse{
status: http.StatusBadRequest, status: http.StatusBadRequest,
err: err, err: err,
} }
} else if session.IdUser == nil { } else if session.IdUser == nil {
return f(nil, ps, b) return f(nil, ps, b)
} else if std, err := getUser(int(*session.IdUser)); err != nil { } else if std, err := getUser(int(*session.IdUser)); err != nil {
return APIErrorResponse{ return APIErrorResponse{
status: http.StatusInternalServerError, status: http.StatusInternalServerError,
err: err, err: err,
} }
} else { } else {
return f(&std, ps, b) return f(&std, ps, b)
@ -208,7 +189,7 @@ func loggedUser(u *User, r *http.Request) *APIErrorResponse {
} else { } else {
ret := &APIErrorResponse{ ret := &APIErrorResponse{
status: http.StatusForbidden, status: http.StatusForbidden,
err: errors.New("Permission Denied"), err: errors.New("Permission Denied"),
} }
return ret return ret
} }
@ -220,7 +201,7 @@ func adminRestricted(u *User, r *http.Request) *APIErrorResponse {
} else { } else {
ret := &APIErrorResponse{ ret := &APIErrorResponse{
status: http.StatusForbidden, status: http.StatusForbidden,
err: errors.New("Permission Denied"), err: errors.New("Permission Denied"),
} }
return ret return ret
} }

14
main.go
View file

@ -59,8 +59,6 @@ func StripPrefix(prefix string, h http.Handler) http.Handler {
func main() { func main() {
var bind = flag.String("bind", ":8081", "Bind port/socket") var bind = flag.String("bind", ":8081", "Bind port/socket")
var dsn = flag.String("dsn", DSNGenerator(), "DSN to connect to the MySQL server") var dsn = flag.String("dsn", DSNGenerator(), "DSN to connect to the MySQL server")
var dummyauth = flag.Bool("dummy-auth", false, "If set, allow any authentication credentials")
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.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).")
@ -78,18 +76,6 @@ func main() {
baseURL = "" baseURL = ""
} }
if dummyauth != nil && *dummyauth == true {
LocalAuthFunc = dummyAuth
}
if DevProxy != "" {
Router().GET("/.svelte-kit/*_", serveOrReverse(""))
Router().GET("/node_modules/*_", serveOrReverse(""))
Router().GET("/@vite/*_", serveOrReverse(""))
Router().GET("/__vite_ping", serveOrReverse(""))
Router().GET("/src/*_", serveOrReverse(""))
}
initializeOIDC() initializeOIDC()
// Initialize contents // Initialize contents

View file

@ -12,27 +12,6 @@ func init() {
func(q Question, u *User, _ []byte) HTTPResponse { func(q Question, u *User, _ []byte) HTTPResponse {
return formatApiResponse(q.GetProposals()) return formatApiResponse(q.GetProposals())
}), loggedUser)) }), loggedUser))
router.POST("/api/questions/:qid/proposals", apiAuthHandler(questionAuthHandler(func(q Question, u *User, body []byte) HTTPResponse {
var new Proposal
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
return formatApiResponse(q.NewProposal(new.Label))
}), adminRestricted))
router.PUT("/api/questions/:qid/proposals/:pid", apiAuthHandler(proposalAuthHandler(func(current Proposal, u *User, body []byte) HTTPResponse {
var new Proposal
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
new.Id = current.Id
return formatApiResponse(new.Update())
}), adminRestricted))
router.DELETE("/api/questions/:qid/proposals/:pid", apiAuthHandler(proposalAuthHandler(func(p Proposal, u *User, body []byte) HTTPResponse {
return formatApiResponse(p.Delete())
}), adminRestricted))
router.GET("/api/surveys/:sid/questions/:qid/proposals", apiAuthHandler(questionAuthHandler( router.GET("/api/surveys/:sid/questions/:qid/proposals", apiAuthHandler(questionAuthHandler(
func(q Question, u *User, _ []byte) HTTPResponse { func(q Question, u *User, _ []byte) HTTPResponse {
return formatApiResponse(q.GetProposals()) return formatApiResponse(q.GetProposals())

View file

@ -3,7 +3,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@ -39,18 +38,10 @@ func init() {
return formatApiResponse(s.NewQuestion(new.Title, new.DescriptionRaw, new.Placeholder, new.Kind)) return formatApiResponse(s.NewQuestion(new.Title, new.DescriptionRaw, new.Placeholder, new.Kind))
}), adminRestricted)) }), adminRestricted))
router.GET("/api/questions/:qid", apiAuthHandler(questionAuthHandler( router.GET("/api/questions/:qid", apiHandler(questionHandler(
func(q Question, u *User, _ []byte) HTTPResponse { func(s Question, _ []byte) HTTPResponse {
if u.IsAdmin { return APIResponse{s}
return APIResponse{q} }), adminRestricted))
} 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( router.GET("/api/surveys/:sid/questions/:qid", apiHandler(questionHandler(
func(s Question, _ []byte) HTTPResponse { func(s Question, _ []byte) HTTPResponse {
return APIResponse{s} return APIResponse{s}

View file

@ -20,21 +20,15 @@ func init() {
now := time.Now() now := time.Now()
if now.Before(s.StartAvailability) { if now.Before(s.StartAvailability) {
return APIErrorResponse{err: fmt.Errorf("Le questionnaire n'a pas encore commencé")} return APIErrorResponse{err: fmt.Errorf("Le questionnaire n'a pas encore commencé")}
} else if now.After(s.EndAvailability.Add(5 * time.Minute)) { } else if now.After(s.EndAvailability) {
return APIErrorResponse{err: fmt.Errorf("Le questionnaire n'est plus ouvert")} return APIErrorResponse{err: fmt.Errorf("Le questionnaire n'est plus ouvert")}
} }
for _, response := range responses { for _, response := range responses {
if !s.Shown && (s.Direct == nil || *s.Direct != response.IdQuestion) { if len(response.Answer) > 0 {
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 { if _, err := s.NewResponse(response.IdQuestion, u.Id, response.Answer); err != nil {
return APIErrorResponse{err: err} return APIErrorResponse{err: err}
} }
if s.Direct != nil {
s.WSAdminWriteAll(WSMessage{Action: "new_response", UserId: &u.Id, QuestionId: &response.IdQuestion, Response: response.Answer})
}
} }
} }
@ -64,10 +58,6 @@ func init() {
func(s Survey, u *User, _ []byte) HTTPResponse { func(s Survey, u *User, _ []byte) HTTPResponse {
return formatApiResponse(s.GetMyResponses(u, s.Corrected)) return formatApiResponse(s.GetMyResponses(u, s.Corrected))
}), loggedUser)) }), loggedUser))
router.GET("/api/questions/:qid/response", apiAuthHandler(questionAuthHandler(
func(q Question, u *User, _ []byte) HTTPResponse {
return formatApiResponse(q.GetMyResponse(u, false))
}), loggedUser))
router.GET("/api/users/:uid/surveys/:sid/responses", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse { router.GET("/api/users/:uid/surveys/:sid/responses", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
return surveyAuthHandler(func(s Survey, u *User, _ []byte) HTTPResponse { return surveyAuthHandler(func(s Survey, u *User, _ []byte) HTTPResponse {
return userHandler(func(u User, _ []byte) HTTPResponse { return userHandler(func(u User, _ []byte) HTTPResponse {
@ -95,22 +85,6 @@ func init() {
new.TimeScored = &now new.TimeScored = &now
} }
new.Id = current.Id
new.IdUser = current.IdUser
return formatApiResponse(new.Update())
}), adminRestricted))
router.PUT("/api/questions/:qid/responses/:rid", apiAuthHandler(responseAuthHandler(func(current Response, u *User, body []byte) HTTPResponse {
var new Response
if err := json.Unmarshal(body, &new); err != nil {
return APIErrorResponse{err: err}
}
if new.Score != nil && (current.Score == nil || *new.Score != *current.Score) {
now := time.Now()
new.IdCorrector = &u.Id
new.TimeScored = &now
}
new.Id = current.Id new.Id = current.Id
new.IdUser = current.IdUser new.IdUser = current.IdUser
return formatApiResponse(new.Update()) return formatApiResponse(new.Update())
@ -211,15 +185,6 @@ func (s *Survey) GetMyResponses(u *User, showScore bool) (responses []Response,
} }
} }
func (q *Question) GetMyResponse(u *User, showScore bool) (r Response, err error) {
err = DBQueryRow("SELECT R.id_response, R.id_question, R.id_user, R.answer, R.time_submit, R.score, R.score_explanation, R.id_corrector, R.time_scored FROM survey_responses R WHERE R.id_question=? AND R.id_user=? ORDER BY time_submit DESC LIMIT 1", q.Id, u.Id).Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored)
if !showScore {
r.Score = nil
r.ScoreExplaination = nil
}
return
}
func (q *Question) GetResponses() (responses []Response, err error) { func (q *Question) GetResponses() (responses []Response, err error) {
if rows, errr := DBQuery("SELECT id_response, id_question, S.id_user, answer, S.time_submit, score, score_explanation, id_corrector, time_scored FROM (SELECT id_user, MAX(time_submit) AS time_submit FROM survey_responses WHERE id_question=? GROUP BY id_user) R INNER JOIN survey_responses S ON S.id_user = R.id_user AND S.time_submit = R.time_submit AND S.id_question=?", q.Id, q.Id); errr != nil { if rows, errr := DBQuery("SELECT id_response, id_question, S.id_user, answer, S.time_submit, score, score_explanation, id_corrector, time_scored FROM (SELECT id_user, MAX(time_submit) AS time_submit FROM survey_responses WHERE id_question=? GROUP BY id_user) R INNER JOIN survey_responses S ON S.id_user = R.id_user AND S.time_submit = R.time_submit AND S.id_question=?", q.Id, q.Id); errr != nil {
return nil, errr return nil, errr

View file

@ -1,63 +1,28 @@
package main package main
import ( import (
"io"
"net/http" "net/http"
"net/url"
"path"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
) )
var DevProxy string
func serveOrReverse(forced_url string) func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { func serveOrReverse(forced_url string) func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if DevProxy != "" { if forced_url != "" {
if u, err := url.Parse(DevProxy); err != nil { r.URL.Path = forced_url
http.Error(w, err.Error(), http.StatusInternalServerError)
} else {
if forced_url != "" {
u.Path = path.Join(u.Path, forced_url)
} else {
u.Path = path.Join(u.Path, r.URL.Path)
}
if r, err := http.NewRequest(r.Method, u.String(), r.Body); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} else if resp, err := http.DefaultClient.Do(r); err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
} else {
defer resp.Body.Close()
for key := range resp.Header {
w.Header().Add(key, resp.Header.Get(key))
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
}
} else {
if forced_url != "" {
r.URL.Path = forced_url
}
http.FileServer(Assets).ServeHTTP(w, r)
} }
http.FileServer(Assets).ServeHTTP(w, r)
} }
} }
func init() { func init() {
Router().GET("/@fs/*_", serveOrReverse(""))
Router().GET("/", serveOrReverse("")) Router().GET("/", serveOrReverse(""))
Router().GET("/auth", serveOrReverse("")) Router().GET("/auth", serveOrReverse("/"))
Router().GET("/bug-bounty", serveOrReverse("")) Router().GET("/grades", serveOrReverse("/"))
Router().GET("/grades", serveOrReverse("")) Router().GET("/surveys", serveOrReverse("/"))
Router().GET("/help", serveOrReverse("")) Router().GET("/surveys/*_", serveOrReverse("/"))
Router().GET("/surveys", serveOrReverse("")) Router().GET("/users", serveOrReverse("/"))
Router().GET("/surveys/*_", serveOrReverse("")) Router().GET("/users/*_", serveOrReverse("/"))
Router().GET("/users", serveOrReverse(""))
Router().GET("/users/*_", serveOrReverse(""))
Router().GET("/css/*_", serveOrReverse("")) Router().GET("/css/*_", serveOrReverse(""))
Router().GET("/fonts/*_", serveOrReverse("")) Router().GET("/fonts/*_", serveOrReverse(""))
Router().GET("/img/*_", serveOrReverse("")) Router().GET("/img/*_", serveOrReverse(""))

View file

@ -6,37 +6,20 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
) )
var (
_score_cache = map[int64]map[int64]*float64{}
)
func init() { func init() {
router.GET("/api/surveys", apiAuthHandler( router.GET("/api/surveys", apiAuthHandler(
func(u *User, _ httprouter.Params, _ []byte) HTTPResponse { func(u *User, _ httprouter.Params, _ []byte) HTTPResponse {
if u == nil { if u == nil {
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))) return formatApiResponse(getSurveys(fmt.Sprintf("WHERE shown = TRUE AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC", currentPromo)))
} else if u.IsAdmin { } else if u.IsAdmin {
return formatApiResponse(getSurveys("ORDER BY promo DESC, start_availability ASC")) return formatApiResponse(getSurveys("ORDER BY promo DESC, start_availability ASC"))
} else { } else {
surveys, err := getSurveys(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND promo = %d ORDER BY start_availability ASC", u.Promo)) return formatApiResponse(getSurveys(fmt.Sprintf("WHERE shown = TRUE AND promo = %d ORDER BY start_availability ASC", u.Promo)))
if err != nil {
return APIErrorResponse{err: err}
}
var response []Survey
for _, s := range surveys {
if s.Group == "" || strings.Contains(u.Groups, ","+s.Group+",") {
response = append(response, s)
}
}
return formatApiResponse(response, nil)
} }
})) }))
router.POST("/api/surveys", apiHandler(func(_ httprouter.Params, body []byte) HTTPResponse { router.POST("/api/surveys", apiHandler(func(_ httprouter.Params, body []byte) HTTPResponse {
@ -49,11 +32,11 @@ func init() {
new.Promo = currentPromo new.Promo = currentPromo
} }
return formatApiResponse(NewSurvey(new.Title, new.Promo, new.Group, new.Shown, new.Direct, new.StartAvailability, new.EndAvailability)) return formatApiResponse(NewSurvey(new.Title, new.Promo, new.Shown, new.StartAvailability, new.EndAvailability))
}, adminRestricted)) }, adminRestricted))
router.GET("/api/surveys/:sid", apiAuthHandler(surveyAuthHandler( router.GET("/api/surveys/:sid", apiAuthHandler(surveyAuthHandler(
func(s Survey, u *User, _ []byte) HTTPResponse { func(s Survey, u *User, _ []byte) HTTPResponse {
if (s.Promo == u.Promo && (s.Group == "" || (u != nil && strings.Contains(u.Groups, ","+s.Group+",")) && s.Shown)) || (u != nil && u.IsAdmin) { if (s.Promo == u.Promo && s.Shown) || (u != nil && u.IsAdmin) {
return APIResponse{s} return APIResponse{s}
} else { } else {
return APIErrorResponse{ return APIErrorResponse{
@ -69,22 +52,6 @@ func init() {
} }
new.Id = current.Id 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()) return formatApiResponse(new.Update())
}), adminRestricted)) }), adminRestricted))
router.DELETE("/api/surveys/:sid", apiHandler(surveyHandler( router.DELETE("/api/surveys/:sid", apiHandler(surveyHandler(
@ -158,23 +125,21 @@ type Survey struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Promo uint `json:"promo"` Promo uint `json:"promo"`
Group string `json:"group"`
Shown bool `json:"shown"` Shown bool `json:"shown"`
Direct *int64 `json:"direct"`
Corrected bool `json:"corrected"` Corrected bool `json:"corrected"`
StartAvailability time.Time `json:"start_availability"` StartAvailability time.Time `json:"start_availability"`
EndAvailability time.Time `json:"end_availability"` EndAvailability time.Time `json:"end_availability"`
} }
func getSurveys(cnd string, param ...interface{}) (surveys []Survey, err error) { func getSurveys(cnd string, param ...interface{}) (surveys []Survey, err error) {
if rows, errr := DBQuery("SELECT id_survey, title, promo, grp, shown, direct, corrected, start_availability, end_availability FROM surveys "+cnd, param...); errr != nil { if rows, errr := DBQuery("SELECT id_survey, title, promo, shown, corrected, start_availability, end_availability FROM surveys "+cnd, param...); errr != nil {
return nil, errr return nil, errr
} else { } else {
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var s Survey var s Survey
if err = rows.Scan(&s.Id, &s.Title, &s.Promo, &s.Group, &s.Shown, &s.Direct, &s.Corrected, &s.StartAvailability, &s.EndAvailability); err != nil { if err = rows.Scan(&s.Id, &s.Title, &s.Promo, &s.Shown, &s.Corrected, &s.StartAvailability, &s.EndAvailability); err != nil {
return return
} }
surveys = append(surveys, s) surveys = append(surveys, s)
@ -188,32 +153,24 @@ func getSurveys(cnd string, param ...interface{}) (surveys []Survey, err error)
} }
func getSurvey(id int) (s Survey, err error) { func getSurvey(id int) (s Survey, err error) {
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) err = DBQueryRow("SELECT id_survey, title, promo, shown, corrected, start_availability, end_availability FROM surveys WHERE id_survey=?", id).Scan(&s.Id, &s.Title, &s.Promo, &s.Shown, &s.Corrected, &s.StartAvailability, &s.EndAvailability)
return return
} }
func NewSurvey(title string, promo uint, group string, shown bool, direct *int64, startAvailability time.Time, endAvailability time.Time) (*Survey, error) { func NewSurvey(title string, promo uint, shown bool, 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 { if res, err := DBExec("INSERT INTO surveys (title, promo, shown, start_availability, end_availability) VALUES (?, ?, ?, ?, ?)", title, promo, shown, startAvailability, endAvailability); err != nil {
return nil, err return Survey{}, err
} else if sid, err := res.LastInsertId(); err != nil { } else if sid, err := res.LastInsertId(); err != nil {
return nil, err return Survey{}, err
} else { } else {
return &Survey{sid, title, promo, group, shown, direct, false, startAvailability, endAvailability}, nil return Survey{sid, title, promo, shown, false, startAvailability, endAvailability}, nil
} }
} }
func (s Survey) GetScore(u *User) (score *float64, err error) { func (s Survey) GetScore(u *User) (score *float64, err error) {
if _, ok := _score_cache[u.Id]; !ok { err = DBQueryRow("SELECT SUM(score)/COUNT(*) FROM student_scores WHERE id_survey=? AND id_user=?", s.Id, u.Id).Scan(&score)
_score_cache[u.Id] = map[int64]*float64{} if score != nil {
} *score = *score / 5.0
if v, ok := _score_cache[u.Id][s.Id]; ok {
score = v
} else {
err = DBQueryRow("SELECT SUM(score)/COUNT(*) FROM student_scores WHERE id_survey=? AND id_user=?", s.Id, u.Id).Scan(&score)
if score != nil {
*score = *score / 5.0
}
_score_cache[u.Id][s.Id] = score
} }
return return
} }
@ -243,11 +200,13 @@ func (s Survey) GetScores() (scores map[int64]*float64, err error) {
return return
} }
func (s *Survey) Update() (*Survey, error) { func (s Survey) Update() (int64, error) {
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 { if res, err := DBExec("UPDATE surveys SET title = ?, promo = ?, shown = ?, corrected = ?, start_availability = ?, end_availability = ? WHERE id_survey = ?", s.Title, s.Promo, s.Shown, s.Corrected, s.StartAvailability, s.EndAvailability, s.Id); err != nil {
return nil, err return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else { } else {
return s, err return nb, err
} }
} }

View file

@ -11,10 +11,6 @@ import (
var currentPromo uint = 0 var currentPromo uint = 0
func init() { func init() {
router.GET("/api/promos", apiHandler(
func(httprouter.Params, []byte) HTTPResponse {
return formatApiResponse(getPromos())
}, adminRestricted))
router.GET("/api/users", apiHandler( router.GET("/api/users", apiHandler(
func(httprouter.Params, []byte) HTTPResponse { func(httprouter.Params, []byte) HTTPResponse {
return formatApiResponse(getUsers()) return formatApiResponse(getUsers())
@ -59,7 +55,7 @@ type User struct {
} }
func getUsers() (users []User, err error) { func getUsers() (users []User, err error) {
if rows, errr := DBQuery("SELECT id_user, login, email, firstname, lastname, time, promo, groups, is_admin FROM users ORDER BY promo DESC, id_user DESC"); errr != nil { if rows, errr := DBQuery("SELECT id_user, login, email, firstname, lastname, time, promo, groups, is_admin FROM users"); errr != nil {
return nil, errr return nil, errr
} else { } else {
defer rows.Close() defer rows.Close()
@ -79,27 +75,6 @@ func getUsers() (users []User, err error) {
} }
} }
func getPromos() (promos []uint, err error) {
if rows, errr := DBQuery("SELECT DISTINCT promo FROM users ORDER BY promo DESC"); errr != nil {
return nil, errr
} else {
defer rows.Close()
for rows.Next() {
var p uint
if err = rows.Scan(&p); err != nil {
return
}
promos = append(promos, p)
}
if err = rows.Err(); err != nil {
return
}
return
}
}
func getUser(id int) (u User, err error) { func getUser(id int) (u User, err error) {
err = DBQueryRow("SELECT id_user, login, email, firstname, lastname, time, promo, groups, is_admin FROM users WHERE id_user=?", id).Scan(&u.Id, &u.Login, &u.Email, &u.Firstname, &u.Lastname, &u.Time, &u.Promo, &u.Groups, &u.IsAdmin) err = DBQueryRow("SELECT id_user, login, email, firstname, lastname, time, promo, groups, is_admin FROM users WHERE id_user=?", id).Scan(&u.Id, &u.Login, &u.Email, &u.Firstname, &u.Lastname, &u.Time, &u.Promo, &u.Groups, &u.IsAdmin)
return return