WIP Svelte
This commit is contained in:
parent
38180f8afd
commit
ded0e8e1c8
20
atsebayt/.eslintrc.cjs
Normal file
20
atsebayt/.eslintrc.cjs
Normal file
@ -0,0 +1,20 @@
|
||||
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
Normal file
5
atsebayt/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
6
atsebayt/.prettierrc
Normal file
6
atsebayt/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100
|
||||
}
|
38
atsebayt/README.md
Normal file
38
atsebayt/README.md
Normal file
@ -0,0 +1,38 @@
|
||||
# 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.
|
1880
atsebayt/package-lock.json
generated
Normal file
1880
atsebayt/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
atsebayt/package.json
Normal file
29
atsebayt/package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"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"
|
||||
}
|
18
atsebayt/src/app.html
Normal file
18
atsebayt/src/app.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!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>
|
17
atsebayt/src/components/DateFormat.svelte
Normal file
17
atsebayt/src/components/DateFormat.svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<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)}
|
167
atsebayt/src/components/QuestionForm.svelte
Normal file
167
atsebayt/src/components/QuestionForm.svelte
Normal file
@ -0,0 +1,167 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import QuestionProposals from '../components/QuestionProposals.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 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}">
|
||||
<div class="card-header">
|
||||
{#if $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}
|
||||
|
||||
{#if edit}
|
||||
<div class="card-title row">
|
||||
<label for="q{qid}title" class="col-auto col-form-label font-weight-bold">Titre :</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">{qid + 1}. {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>
|
||||
<div class="card-body">
|
||||
{#if false && response_history}
|
||||
<div class="d-flex justify-content-end mb-2">
|
||||
<div class="col-auto">
|
||||
Historique :
|
||||
<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 …</span>
|
||||
</div>
|
||||
{:then proposals}
|
||||
<QuestionProposals
|
||||
edit
|
||||
id_question={question.id}
|
||||
kind={question.kind}
|
||||
{proposals}
|
||||
readonly
|
||||
bind:value={value}
|
||||
/>
|
||||
{/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 …</span>
|
||||
</div>
|
||||
{:then proposals}
|
||||
<QuestionProposals
|
||||
kind={question.kind}
|
||||
{proposals}
|
||||
{readonly}
|
||||
bind:value={value}
|
||||
/>
|
||||
{/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}>
|
||||
{:else}
|
||||
<textarea class="form-control" rows="6" bind:value={value} placeholder={question.placeholder}></textarea>
|
||||
{/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} %</strong>
|
||||
</div>
|
||||
<p class="col mb-0" style="white-space: pre-line">{question.response.score_explaination}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
21
atsebayt/src/components/QuestionProposal.svelte
Normal file
21
atsebayt/src/components/QuestionProposal.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script>
|
||||
export let proposal = null;
|
||||
export let kind = 'mcq';
|
||||
export let value;
|
||||
|
||||
let inputType = 'checkbox';
|
||||
$: {
|
||||
switch(kind) {
|
||||
case 'mcq':
|
||||
inputType = 'checkbox';
|
||||
break;
|
||||
default:
|
||||
inputType = 'radio';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<input
|
||||
type={inputType}
|
||||
bind:value={value}
|
||||
{JSON.stringify(proposal)}
|
113
atsebayt/src/components/QuestionProposals.svelte
Normal file
113
atsebayt/src/components/QuestionProposals.svelte
Normal file
@ -0,0 +1,113 @@
|
||||
<script>
|
||||
import { QuestionProposal } from '../lib/questions';
|
||||
|
||||
export let edit = false;
|
||||
export let proposals = [];
|
||||
export let kind = 'mcq';
|
||||
export let readonly = false;
|
||||
export let id_question = 0;
|
||||
export let value;
|
||||
|
||||
let valueCheck = [];
|
||||
$: {
|
||||
console.log(value);
|
||||
if (value) {
|
||||
valueCheck = value.split(',');
|
||||
}
|
||||
}
|
||||
|
||||
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={'proposal' + proposal.id_question}
|
||||
id={'p' + proposal.id}
|
||||
bind:group={valueCheck}
|
||||
value={String(proposal.id)}
|
||||
on:change={() => { value = valueCheck.join(',')}}
|
||||
>
|
||||
{:else}
|
||||
<input
|
||||
type="radio"
|
||||
class="form-check-input"
|
||||
disabled={readonly}
|
||||
name={'proposal' + proposal.id_question}
|
||||
id={'p' + proposal.id}
|
||||
bind:group={value}
|
||||
value={String(proposal.id)}
|
||||
>
|
||||
{/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={'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}
|
25
atsebayt/src/components/StudentGrades.svelte
Normal file
25
atsebayt/src/components/StudentGrades.svelte
Normal file
@ -0,0 +1,25 @@
|
||||
<script>
|
||||
export let promo = null;
|
||||
</script>
|
||||
|
||||
<h2>
|
||||
Étudiants {#if promo}{promo}{/if}
|
||||
<small class="text-muted">Notes</small>
|
||||
</h2>
|
||||
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Login</th>
|
||||
<th ng-repeat="(sid,survey) in surveys" ng-if="survey.corrected">{ survey.title }</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="(uid,user) in users" ng-click="showUser(user)">
|
||||
<td>{ user.id }</td>
|
||||
<td>{ user.login }</td>
|
||||
<td ng-repeat="(sid,survey) in surveys" ng-if="survey.corrected">{ grades[user.id][survey.id] }</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
99
atsebayt/src/components/SurveyAdmin.svelte
Normal file
99
atsebayt/src/components/SurveyAdmin.svelte
Normal file
@ -0,0 +1,99 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let survey = null;
|
||||
|
||||
function saveSurvey() {
|
||||
survey.save().then((response) => {
|
||||
dispatch('saved');
|
||||
}, (error) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
function deleteSurvey() {
|
||||
survey.delete().then((response) => {
|
||||
goto(`surveys`);
|
||||
}, (error) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
function duplicateSurvey() {
|
||||
survey.duplicate().then((response) => {
|
||||
goto(`surveys/${response.id}`);
|
||||
}).catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
</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="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>
|
11
atsebayt/src/components/SurveyBadge.svelte
Normal file
11
atsebayt/src/components/SurveyBadge.svelte
Normal file
@ -0,0 +1,11 @@
|
||||
<script>
|
||||
export let survey;
|
||||
let className = '';
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
{#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}
|
81
atsebayt/src/components/SurveyList.svelte
Normal file
81
atsebayt/src/components/SurveyList.svelte
Normal file
@ -0,0 +1,81 @@
|
||||
<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';
|
||||
</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 getSurveys()}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-3">
|
||||
<div class="spinner-border mx-3" role="status"></div>
|
||||
<span>Chargement des questionnaires …</span>
|
||||
</td>
|
||||
</tr>
|
||||
{:then surveys}
|
||||
<tbody style="cursor: pointer;">
|
||||
{#each surveys as survey, sid (survey.id)}
|
||||
{#if survey.shown && (!$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(`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>
|
101
atsebayt/src/components/SurveyQuestions.svelte
Normal file
101
atsebayt/src/components/SurveyQuestions.svelte
Normal file
@ -0,0 +1,101 @@
|
||||
<script>
|
||||
import { user } from '../stores/user';
|
||||
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;
|
||||
console.log("Vos réponses ont bien étés sauvegardées.");
|
||||
}, (error) => {
|
||||
submitInProgress = false;
|
||||
console.log("Une erreur s'est produite durant l'envoi de vos réponses : " + error + "<br>Veuillez 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
|
||||
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">
|
||||
<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 $user && $user.is_admin}
|
||||
<button type="button" class="btn btn-info" on:click={addQuestion}>
|
||||
Ajouter une question
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
61
atsebayt/src/components/UserSurveys.svelte
Normal file
61
atsebayt/src/components/UserSurveys.svelte
Normal file
@ -0,0 +1,61 @@
|
||||
<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…
|
||||
</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} %
|
||||
</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>
|
60
atsebayt/src/components/ValidateSubmissions.svelte
Normal file
60
atsebayt/src/components/ValidateSubmissions.svelte
Normal file
@ -0,0 +1,60 @@
|
||||
<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}
|
||||
–
|
||||
{/if}
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</tbody>
|
||||
{/await}
|
||||
</table>
|
||||
|
||||
<style>
|
||||
.hash {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
1
atsebayt/src/global.d.ts
vendored
Normal file
1
atsebayt/src/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
6
atsebayt/src/hooks.js
Normal file
6
atsebayt/src/hooks.js
Normal file
@ -0,0 +1,6 @@
|
||||
export async function handle({ event, resolve }) {
|
||||
const response = await resolve(event, {
|
||||
ssr: false,
|
||||
});
|
||||
return response;
|
||||
}
|
112
atsebayt/src/lib/questions.js
Normal file
112
atsebayt/src/lib/questions.js
Normal file
@ -0,0 +1,112 @@
|
||||
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 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 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);
|
||||
}
|
||||
}
|
144
atsebayt/src/lib/surveys.js
Normal file
144
atsebayt/src/lib/surveys.js
Normal file
@ -0,0 +1,144 @@
|
||||
import { getQuestions } from './questions';
|
||||
|
||||
|
||||
class Survey {
|
||||
constructor(res) {
|
||||
if (res) {
|
||||
this.update(res);
|
||||
}
|
||||
}
|
||||
|
||||
update({ id, title, promo, shown, corrected, start_availability, end_availability }) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.promo = promo;
|
||||
this.shown = shown;
|
||||
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() {
|
||||
console.log("delete", this.id)
|
||||
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);
|
||||
}
|
||||
}
|
53
atsebayt/src/lib/users.js
Normal file
53
atsebayt/src/lib/users.js
Normal file
@ -0,0 +1,53 @@
|
||||
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 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);
|
||||
}
|
||||
}
|
138
atsebayt/src/routes/__layout.svelte
Normal file
138
atsebayt/src/routes/__layout.svelte
Normal file
@ -0,0 +1,138 @@
|
||||
<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>
|
||||
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>
|
||||
|
||||
<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 ?</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>
|
71
atsebayt/src/routes/auth.svelte
Normal file
71
atsebayt/src/routes/auth.svelte
Normal file
@ -0,0 +1,71 @@
|
||||
<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>
|
89
atsebayt/src/routes/bug-bounty.svelte
Normal file
89
atsebayt/src/routes/bug-bounty.svelte
Normal file
@ -0,0 +1,89 @@
|
||||
<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 :</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 :
|
||||
</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 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>
|
17
atsebayt/src/routes/grades/[promo].svelte
Normal file
17
atsebayt/src/routes/grades/[promo].svelte
Normal file
@ -0,0 +1,17 @@
|
||||
<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} />
|
5
atsebayt/src/routes/grades/index.svelte
Normal file
5
atsebayt/src/routes/grades/index.svelte
Normal file
@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import StudentGrades from '../../components/StudentGrades.svelte';
|
||||
</script>
|
||||
|
||||
<StudentGrades />
|
29
atsebayt/src/routes/index.svelte
Normal file
29
atsebayt/src/routes/index.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { user } from '../stores/user';
|
||||
import SurveyList from '../components/SurveyList.svelte';
|
||||
import ValidateSubmissions from '../components/ValidateSubmissions.svelte';
|
||||
</script>
|
||||
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
{#if $user}
|
||||
<img class="float-end img-thumbnail" src="https://photos.cri.epita.fr/thumb/{$user.login}" alt="avatar {$user.login}" style="max-height: 150px">
|
||||
<h1 class="card-text">
|
||||
Bienvenue {$user.firstname} !
|
||||
</h1>
|
||||
<hr class="my-4">
|
||||
|
||||
<p class="lead">Tu as fait les rendus suivants :</p>
|
||||
<ValidateSubmissions />
|
||||
<p class="lead">Voici la liste des questionnaires :</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 />
|
||||
</div>
|
||||
</div>
|
62
atsebayt/src/routes/surveys/[sid].svelte
Normal file
62
atsebayt/src/routes/surveys/[sid].svelte
Normal file
@ -0,0 +1,62 @@
|
||||
<script context="module">
|
||||
export async function load({ params }) {
|
||||
return {
|
||||
props: {
|
||||
sid: params.sid,
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
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 sid;
|
||||
|
||||
let edit = false;
|
||||
</script>
|
||||
|
||||
{#await getSurvey(sid)}
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||
<span>Chargement du questionnaire …</span>
|
||||
</div>
|
||||
{:then survey}
|
||||
{#if $user && $user.is_admin}
|
||||
<button class="btn btn-primary ms-1 float-end" on:click={() => { edit = !edit; } } title="Éditer" ng-if=" && !edit"><i class="bi bi-pencil"></i></button>
|
||||
<a href="surveys/{survey.id}/responses" class="btn btn-success ms-1 float-end" title="Voir les réponses" ng-if="user.is_admin"><i class="bi bi-files"></i></a>
|
||||
{/if}
|
||||
<div class="d-flex align-items-center">
|
||||
<h2>
|
||||
<a href="surveys/" class="text-muted" style="text-decoration: none"><</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 …</span>
|
||||
</div>
|
||||
{:then questions}
|
||||
<SurveyQuestions {survey} {questions} />
|
||||
{/await}
|
||||
{:catch error}
|
||||
<div class="text-center">
|
||||
<h2>
|
||||
<a href="surveys/" class="text-muted" style="text-decoration: none"><</a>
|
||||
Questionnaire introuvable
|
||||
</h2>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/await}
|
8
atsebayt/src/routes/surveys/index.svelte
Normal file
8
atsebayt/src/routes/surveys/index.svelte
Normal file
@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { user } from '../../stores/user';
|
||||
import SurveyList from '../../components/SurveyList.svelte';
|
||||
</script>
|
||||
|
||||
<div class="card bg-light">
|
||||
<SurveyList />
|
||||
</div>
|
111
atsebayt/src/routes/users/[uid]/index.svelte
Normal file
111
atsebayt/src/routes/users/[uid]/index.svelte
Normal file
@ -0,0 +1,111 @@
|
||||
<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étails…
|
||||
</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}
|
64
atsebayt/src/routes/users/[uid]/surveys/[sid].svelte
Normal file
64
atsebayt/src/routes/users/[uid]/surveys/[sid].svelte
Normal file
@ -0,0 +1,64 @@
|
||||
<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étails…
|
||||
</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 …</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 …</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"><</a>
|
||||
Questionnaire introuvable
|
||||
</h2>
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
{/await}
|
||||
{/await}
|
64
atsebayt/src/routes/users/[uid]/surveys/index.svelte
Normal file
64
atsebayt/src/routes/users/[uid]/surveys/index.svelte
Normal file
@ -0,0 +1,64 @@
|
||||
<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étails…
|
||||
</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}
|
79
atsebayt/src/routes/users/index.svelte
Normal file
79
atsebayt/src/routes/users/index.svelte
Normal file
@ -0,0 +1,79 @@
|
||||
<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 étudiants …</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}
|
27
atsebayt/src/stores/user.js
Normal file
27
atsebayt/src/stores/user.js
Normal file
@ -0,0 +1,27 @@
|
||||
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();
|
12
atsebayt/static/css/bootstrap.min.css
vendored
Normal file
12
atsebayt/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
atsebayt/static/favicon.png
Normal file
BIN
atsebayt/static/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
atsebayt/static/img/srstamps.png
Normal file
BIN
atsebayt/static/img/srstamps.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
13
atsebayt/svelte.config.js
Normal file
13
atsebayt/svelte.config.js
Normal file
@ -0,0 +1,13 @@
|
||||
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;
|
31
atsebayt/tsconfig.json
Normal file
31
atsebayt/tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
33
auth.go
33
auth.go
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@ -26,33 +27,28 @@ func init() {
|
||||
}
|
||||
|
||||
func validateAuthToken(u *User, _ httprouter.Params, _ []byte) HTTPResponse {
|
||||
if u == nil {
|
||||
return APIErrorResponse{status: http.StatusUnauthorized, err: fmt.Errorf("Not connected")}
|
||||
} else {
|
||||
return APIResponse{u}
|
||||
}
|
||||
}
|
||||
|
||||
func logout(w http.ResponseWriter, ps httprouter.Params, body []byte) HTTPResponse {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth",
|
||||
Value: "",
|
||||
Path: baseURL + "/",
|
||||
Expires: time.Unix(0, 0),
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
|
||||
eraseCookie(w)
|
||||
return APIResponse{true}
|
||||
}
|
||||
|
||||
func completeAuth(w http.ResponseWriter, username string, email string, firstname string, lastname string, groups string, session *Session) (err error) {
|
||||
var usr User
|
||||
func completeAuth(w http.ResponseWriter, username string, email string, firstname string, lastname string, groups string, session *Session) (usr User, err error) {
|
||||
if !userExists(username) {
|
||||
if usr, err = NewUser(username, email, firstname, lastname, groups); err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
} else if usr, err = getUserByLogin(username); err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
if len(groups) > 0 {
|
||||
if len(groups) > 255 {
|
||||
groups = groups[:255]
|
||||
}
|
||||
@ -60,6 +56,7 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
|
||||
usr.Groups = groups
|
||||
usr.Update()
|
||||
}
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
var s Session
|
||||
@ -70,7 +67,7 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
@ -78,12 +75,12 @@ func completeAuth(w http.ResponseWriter, username string, email string, firstnam
|
||||
Value: base64.StdEncoding.EncodeToString(session.Id),
|
||||
Path: baseURL + "/",
|
||||
Expires: time.Now().Add(30 * 24 * time.Hour),
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
//Secure: true,
|
||||
})
|
||||
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
func dummyAuth(w http.ResponseWriter, _ httprouter.Params, body []byte) (interface{}, error) {
|
||||
@ -92,5 +89,5 @@ func dummyAuth(w http.ResponseWriter, _ httprouter.Params, body []byte) (interfa
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]string{"status": "OK"}, completeAuth(w, lf["login"], lf["email"], lf["firstname"], lf["lastname"], "", nil)
|
||||
return completeAuth(w, lf["username"], lf["email"], lf["firstname"], lf["lastname"], "", nil)
|
||||
}
|
||||
|
@ -73,6 +73,6 @@ func checkAuthKrb5(w http.ResponseWriter, _ httprouter.Params, body []byte) (int
|
||||
}
|
||||
return nil, err
|
||||
} else {
|
||||
return dummyAuth(w, nil, body)
|
||||
return completeAuth(w, lf.Login, lf.Login+"@epita.fr", "", "", "", nil)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
return
|
||||
}
|
||||
|
25
handler.go
25
handler.go
@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
@ -18,7 +19,6 @@ func Router() *httprouter.Router {
|
||||
return router
|
||||
}
|
||||
|
||||
|
||||
type HTTPResponse interface {
|
||||
WriteResponse(http.ResponseWriter)
|
||||
}
|
||||
@ -62,9 +62,19 @@ func (r APIErrorResponse) WriteResponse(w http.ResponseWriter) {
|
||||
http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", r.err.Error()), r.status)
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||
if addr := r.Header.Get("X-Forwarded-For"); addr != "" {
|
||||
@ -76,16 +86,19 @@ func rawHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params, []
|
||||
var user *User = nil
|
||||
if cookie, err := r.Cookie("auth"); err == nil {
|
||||
if sessionid, err := base64.StdEncoding.DecodeString(cookie.Value); err != nil {
|
||||
eraseCookie(w)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusNotAcceptable)
|
||||
return
|
||||
} else if session, err := getSession(sessionid); err != nil {
|
||||
eraseCookie(w)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusUnauthorized)
|
||||
return
|
||||
} else if session.IdUser == nil {
|
||||
user = nil
|
||||
} else if std, err := getUser(int(*session.IdUser)); err != nil {
|
||||
eraseCookie(w)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
http.Error(w, fmt.Sprintf(`{"errmsg": %q}`, err.Error()), http.StatusUnauthorized)
|
||||
return
|
||||
@ -134,15 +147,15 @@ 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) {
|
||||
return rawHandler(func (w http.ResponseWriter, r *http.Request, ps httprouter.Params, b []byte) {
|
||||
formatResponseHandler(func (_ *http.Request, ps httprouter.Params, b []byte) HTTPResponse {
|
||||
return rawHandler(func(w http.ResponseWriter, r *http.Request, ps httprouter.Params, b []byte) {
|
||||
formatResponseHandler(func(_ *http.Request, ps httprouter.Params, b []byte) HTTPResponse {
|
||||
return f(w, ps, b)
|
||||
})(w, r, ps, b)
|
||||
}, access...)
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -157,7 +170,7 @@ 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) {
|
||||
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 {
|
||||
return f(nil, ps, b)
|
||||
} else if sessionid, err := base64.StdEncoding.DecodeString(cookie.Value); err != nil {
|
||||
|
5
main.go
5
main.go
@ -59,6 +59,7 @@ func StripPrefix(prefix string, h http.Handler) http.Handler {
|
||||
func main() {
|
||||
var bind = flag.String("bind", ":8081", "Bind port/socket")
|
||||
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.UintVar(¤tPromo, "current-promo", currentPromo, "Year of the current promotion")
|
||||
@ -77,6 +78,10 @@ func main() {
|
||||
baseURL = ""
|
||||
}
|
||||
|
||||
if dummyauth != nil && *dummyauth == true {
|
||||
LocalAuthFunc = dummyAuth
|
||||
}
|
||||
|
||||
if DevProxy != "" {
|
||||
Router().GET("/.svelte-kit/*_", serveOrReverse(""))
|
||||
Router().GET("/node_modules/*_", serveOrReverse(""))
|
||||
|
14
static.go
14
static.go
@ -48,13 +48,15 @@ func serveOrReverse(forced_url string) func(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
func init() {
|
||||
Router().GET("/@fs/*_", serveOrReverse(""))
|
||||
Router().GET("/", serveOrReverse(""))
|
||||
Router().GET("/auth", serveOrReverse("/"))
|
||||
Router().GET("/grades", serveOrReverse("/"))
|
||||
Router().GET("/surveys", serveOrReverse("/"))
|
||||
Router().GET("/surveys/*_", serveOrReverse("/"))
|
||||
Router().GET("/users", serveOrReverse("/"))
|
||||
Router().GET("/users/*_", serveOrReverse("/"))
|
||||
Router().GET("/auth", serveOrReverse(""))
|
||||
Router().GET("/bug-bounty", serveOrReverse(""))
|
||||
Router().GET("/grades", serveOrReverse(""))
|
||||
Router().GET("/surveys", serveOrReverse(""))
|
||||
Router().GET("/surveys/*_", serveOrReverse(""))
|
||||
Router().GET("/users", serveOrReverse(""))
|
||||
Router().GET("/users/*_", serveOrReverse(""))
|
||||
Router().GET("/css/*_", serveOrReverse(""))
|
||||
Router().GET("/fonts/*_", serveOrReverse(""))
|
||||
Router().GET("/img/*_", serveOrReverse(""))
|
||||
|
27
users.go
27
users.go
@ -11,6 +11,10 @@ import (
|
||||
var currentPromo uint = 0
|
||||
|
||||
func init() {
|
||||
router.GET("/api/promos", apiHandler(
|
||||
func(httprouter.Params, []byte) HTTPResponse {
|
||||
return formatApiResponse(getPromos())
|
||||
}, adminRestricted))
|
||||
router.GET("/api/users", apiHandler(
|
||||
func(httprouter.Params, []byte) HTTPResponse {
|
||||
return formatApiResponse(getUsers())
|
||||
@ -55,7 +59,7 @@ type User struct {
|
||||
}
|
||||
|
||||
func getUsers() (users []User, err error) {
|
||||
if rows, errr := DBQuery("SELECT id_user, login, email, firstname, lastname, time, promo, groups, is_admin FROM users"); errr != nil {
|
||||
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 {
|
||||
return nil, errr
|
||||
} else {
|
||||
defer rows.Close()
|
||||
@ -75,6 +79,27 @@ 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) {
|
||||
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
|
||||
|
Reference in New Issue
Block a user