Update dependency node to v18 #15
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ui/node_modules
|
||||||
|
ui/build
|
36
.drone.yml
36
.drone.yml
@ -8,18 +8,28 @@ platform:
|
|||||||
arch: arm
|
arch: arm
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: build front
|
||||||
|
image: node:18-alpine
|
||||||
|
commands:
|
||||||
|
- mkdir deploy
|
||||||
|
- apk --no-cache add python2 build-base
|
||||||
|
- cd ui
|
||||||
|
- npm install --network-timeout=100000
|
||||||
|
- npm run build
|
||||||
|
- tar chjf ../deploy/static.tar.bz2 build
|
||||||
|
|
||||||
- name: vet
|
- name: vet
|
||||||
image: golang:alpine
|
image: golang:1-alpine
|
||||||
commands:
|
commands:
|
||||||
- apk --no-cache add build-base
|
- apk --no-cache add build-base
|
||||||
- go vet -v
|
- go vet -v -buildvcs=false
|
||||||
|
|
||||||
- name: backend armv7
|
- name: backend armv7
|
||||||
image: golang:alpine
|
image: golang:1-alpine
|
||||||
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 -buildvcs=false -ldflags="-s -w"
|
||||||
environment:
|
environment:
|
||||||
GOARM: 7
|
GOARM: 7
|
||||||
|
|
||||||
@ -44,18 +54,28 @@ platform:
|
|||||||
arch: arm64
|
arch: arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: build front
|
||||||
|
image: node:18-alpine
|
||||||
|
commands:
|
||||||
|
- mkdir deploy
|
||||||
|
- apk --no-cache add python2 build-base
|
||||||
|
- cd ui
|
||||||
|
- npm install --network-timeout=100000
|
||||||
|
- npm run build
|
||||||
|
- tar chjf ../deploy/static.tar.bz2 build
|
||||||
|
|
||||||
- name: vet
|
- name: vet
|
||||||
image: golang:alpine
|
image: golang:1-alpine
|
||||||
commands:
|
commands:
|
||||||
- apk --no-cache add build-base
|
- apk --no-cache add build-base
|
||||||
- go vet -v
|
- go vet -v -buildvcs=false
|
||||||
|
|
||||||
- name: backend
|
- name: backend
|
||||||
image: golang:alpine
|
image: golang:1-alpine
|
||||||
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 -buildvcs=false -ldflags="-s -w"
|
||||||
|
|
||||||
- name: publish
|
- name: publish
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
|
24
Dockerfile
24
Dockerfile
@ -1,18 +1,30 @@
|
|||||||
FROM golang:alpine as gobuild
|
FROM node:18-alpine as nodebuild
|
||||||
|
|
||||||
|
WORKDIR /ui
|
||||||
|
|
||||||
|
RUN apk --no-cache add python2 build-base
|
||||||
|
|
||||||
|
COPY ui/ .
|
||||||
|
|
||||||
|
RUN npm install --network-timeout=100000 && \
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
|
||||||
|
FROM golang:1-alpine as gobuild
|
||||||
|
|
||||||
RUN apk add --no-cache go-bindata
|
RUN apk add --no-cache go-bindata
|
||||||
|
|
||||||
WORKDIR /go/src/git.nemunai.re/atsebay.t
|
WORKDIR /go/src/git.nemunai.re/atsebay.t
|
||||||
|
|
||||||
COPY *.go go.mod go.sum ./
|
COPY *.go go.mod go.sum ./
|
||||||
COPY htdocs/ ./htdocs/
|
COPY --from=nodebuild /ui/build ui/build
|
||||||
|
|
||||||
RUN go generate -v && \
|
RUN go get -d -v && \
|
||||||
go get -d -v && \
|
go generate -v && \
|
||||||
go build -v -ldflags="-s -w"
|
go build -v -buildvcs=false -ldflags="-s -w" -o atsebay.t
|
||||||
|
|
||||||
|
|
||||||
FROM alpine
|
FROM alpine:3.15
|
||||||
|
|
||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
|
|
||||||
|
105
asks.go
Normal file
105
asks.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
router.POST("/api/surveys/:sid/ask", apiAuthHandler(surveyAuthHandler(func(s Survey, u *User, body []byte) HTTPResponse {
|
||||||
|
var ask Ask
|
||||||
|
if err := json.Unmarshal(body, &ask); err != nil {
|
||||||
|
return APIErrorResponse{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
a, err := s.NewAsk(u.Id, ask.Content)
|
||||||
|
if err != nil {
|
||||||
|
return APIErrorResponse{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Direct != nil {
|
||||||
|
s.WSAdminWriteAll(WSMessage{Action: "new_ask", UserId: &u.Id, QuestionId: &a.Id, Response: ask.Content})
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatApiResponse(a, err)
|
||||||
|
}), loggedUser))
|
||||||
|
router.GET("/api/surveys/:sid/ask", apiHandler(surveyHandler(
|
||||||
|
func(s Survey, _ []byte) HTTPResponse {
|
||||||
|
return formatApiResponse(s.GetAsks(true))
|
||||||
|
}), adminRestricted))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ask struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
IdSurvey int64 `json:"id_survey"`
|
||||||
|
IdUser int64 `json:"id_user"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Answered bool `json:"answered,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Survey) GetAsks(unansweredonly bool) (asks []Ask, err error) {
|
||||||
|
cmp := ""
|
||||||
|
if unansweredonly {
|
||||||
|
cmp = " AND answered = 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows, errr := DBQuery("SELECT id_ask, id_survey, id_user, date, content, answered FROM survey_asks WHERE id_survey=?"+cmp, s.Id); errr != nil {
|
||||||
|
return nil, errr
|
||||||
|
} else {
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var a Ask
|
||||||
|
if err = rows.Scan(&a.Id, &a.IdSurvey, &a.IdUser, &a.Date, &a.Content, &a.Answered); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
asks = append(asks, a)
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAsk(id int) (a Ask, err error) {
|
||||||
|
err = DBQueryRow("SELECT id_ask, id_survey, id_user, date, content, answered FROM survey_asks WHERE id_ask = ?", id).Scan(&a.Id, &a.IdSurvey, &a.IdUser, &a.Date, &a.Content, &a.Answered)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Survey) NewAsk(id_user int64, content string) (Ask, error) {
|
||||||
|
if res, err := DBExec("INSERT INTO survey_asks (id_survey, id_user, date, content) VALUES (?, ?, ?, ?)", s.Id, id_user, time.Now(), content); err != nil {
|
||||||
|
return Ask{}, err
|
||||||
|
} else if aid, err := res.LastInsertId(); err != nil {
|
||||||
|
return Ask{}, err
|
||||||
|
} else {
|
||||||
|
return Ask{aid, s.Id, id_user, time.Now(), content, false}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Ask) Update() error {
|
||||||
|
_, err := DBExec("UPDATE survey_asks SET id_survey = ?, id_user = ?, date = ?, content = ?, answered = ? WHERE id_ask = ?", a.IdSurvey, a.IdUser, a.Date, a.Content, a.Answered, a.Id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Ask) Delete() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM survey_asks WHERE id_ask = ?", a.Id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearAsks() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM survey_asks"); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
@ -10,15 +10,15 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed htdocs
|
//go:embed ui/build/* ui/build/_app/* ui/build/_app/assets/pages/* ui/build/_app/pages/* ui/build/_app/pages/grades/* ui/build/_app/pages/surveys/* ui/build/_app/pages/surveys/_sid_/* ui/build/_app/pages/surveys/_sid_/responses/* ui/build/_app/pages/users/* ui/build/_app/pages/users/_uid_/* ui/build/_app/pages/users/_uid_/surveys/*
|
||||||
var _assets embed.FS
|
var _assets embed.FS
|
||||||
|
|
||||||
var Assets http.FileSystem
|
var Assets http.FileSystem
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
sub, err := fs.Sub(_assets, "htdocs")
|
sub, err := fs.Sub(_assets, "ui/build")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Unable to cd to htdocs/ directory:", err)
|
log.Fatal("Unable to cd to ui/build/ directory:", err)
|
||||||
}
|
}
|
||||||
Assets = http.FS(sub)
|
Assets = http.FS(sub)
|
||||||
}
|
}
|
||||||
|
@ -1,167 +0,0 @@
|
|||||||
<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>
|
|
@ -1,21 +0,0 @@
|
|||||||
<script>
|
|
||||||
export let proposal = null;
|
|
||||||
export let kind = 'mcq';
|
|
||||||
export let value;
|
|
||||||
|
|
||||||
let inputType = 'checkbox';
|
|
||||||
$: {
|
|
||||||
switch(kind) {
|
|
||||||
case 'mcq':
|
|
||||||
inputType = 'checkbox';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
inputType = 'radio';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type={inputType}
|
|
||||||
bind:value={value}
|
|
||||||
{JSON.stringify(proposal)}
|
|
@ -1,25 +0,0 @@
|
|||||||
<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>
|
|
@ -1,29 +0,0 @@
|
|||||||
<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>
|
|
@ -1,62 +0,0 @@
|
|||||||
<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}
|
|
7
auth.go
7
auth.go
@ -26,11 +26,16 @@ 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 {
|
if u == nil {
|
||||||
return APIErrorResponse{status: http.StatusUnauthorized, err: fmt.Errorf("Not connected")}
|
return APIErrorResponse{status: http.StatusUnauthorized, err: fmt.Errorf("Not connected")}
|
||||||
} else {
|
} else {
|
||||||
return APIResponse{u}
|
return APIResponse{authToken{u, currentPromo}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ func init() {
|
|||||||
return APIErrorResponse{err: err}
|
return APIErrorResponse{err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.Score, new.ScoreExplaination))
|
return formatApiResponse(q.NewCorrectionTemplate(new.Label, new.RegExp, 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,6 +51,48 @@ 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 {
|
||||||
@ -75,6 +117,14 @@ 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))
|
||||||
@ -112,19 +162,20 @@ 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, score, score_explanation FROM correction_templates WHERE id_question=?", q.Id); errr != nil {
|
if rows, errr := DBQuery("SELECT id_template, id_question, label, re, 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.Score, &c.ScoreExplaination); err != nil {
|
if err = rows.Scan(&c.Id, &c.IdQuestion, &c.Label, &c.RegExp, &c.Score, &c.ScoreExplaination); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ct = append(ct, c)
|
ct = append(ct, c)
|
||||||
@ -138,27 +189,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, 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)
|
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)
|
||||||
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, score, score_explanation FROM correction_templates WHERE id_template=?", id).Scan(&c.Id, &c.IdQuestion, &c.Label, &c.Score, &c.ScoreExplaination)
|
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)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Question) NewCorrectionTemplate(label string, score int, score_explaination string) (CorrectionTemplate, error) {
|
func (q *Question) NewCorrectionTemplate(label string, regexp string, score int, score_explaination string) (CorrectionTemplate, error) {
|
||||||
if res, err := DBExec("INSERT INTO correction_templates (id_question, label, score, score_explanation) VALUES (?, ?, ?, ?)", q.Id, label, score, score_explaination); err != nil {
|
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 {
|
||||||
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, score, score_explaination}, nil
|
return CorrectionTemplate{cid, q.Id, label, regexp, score, score_explaination}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *CorrectionTemplate) Update() error {
|
func (t *CorrectionTemplate) Update() error {
|
||||||
_, 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)
|
_, 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)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,6 +320,26 @@ 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
|
||||||
|
31
db.go
31
db.go
@ -83,6 +83,7 @@ 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
|
||||||
@ -124,6 +125,7 @@ CREATE TABLE IF NOT EXISTS survey_responses(
|
|||||||
score_explanation TEXT,
|
score_explanation TEXT,
|
||||||
id_corrector INTEGER,
|
id_corrector INTEGER,
|
||||||
time_scored TIMESTAMP NULL DEFAULT NULL,
|
time_scored TIMESTAMP NULL DEFAULT NULL,
|
||||||
|
time_reported TIMESTAMP NULL DEFAULT NULL,
|
||||||
FOREIGN KEY(id_question) REFERENCES survey_quests(id_question),
|
FOREIGN KEY(id_question) REFERENCES survey_quests(id_question),
|
||||||
FOREIGN KEY(id_user) REFERENCES users(id_user),
|
FOREIGN KEY(id_user) REFERENCES users(id_user),
|
||||||
FOREIGN KEY(id_corrector) REFERENCES users(id_user)
|
FOREIGN KEY(id_corrector) REFERENCES users(id_user)
|
||||||
@ -136,6 +138,7 @@ 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)
|
||||||
@ -155,7 +158,33 @@ 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 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;
|
CREATE TABLE IF NOT EXISTS survey_asks(
|
||||||
|
id_ask INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
id_survey INTEGER NOT NULL,
|
||||||
|
id_user INTEGER NOT NULL,
|
||||||
|
date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
answered BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
FOREIGN KEY(id_user) REFERENCES users(id_user),
|
||||||
|
FOREIGN KEY(id_survey) REFERENCES surveys(id_survey)
|
||||||
|
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_need_help(
|
||||||
|
id_need_help INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
id_user INTEGER NOT NULL,
|
||||||
|
date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
comment TEXT NOT NULL,
|
||||||
|
date_treated TIMESTAMP NULL,
|
||||||
|
FOREIGN KEY(id_user) REFERENCES users(id_user)
|
||||||
|
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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;
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
406
direct.go
Normal file
406
direct.go
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"nhooyr.io/websocket"
|
||||||
|
"nhooyr.io/websocket/wsjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
OffsetQuestionTimer uint = 700
|
||||||
|
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"`
|
||||||
|
Timer uint `json:"timer,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 {
|
||||||
|
if v.Timer > 0 {
|
||||||
|
if *survey.Direct != 0 {
|
||||||
|
var z int64 = 0
|
||||||
|
survey.Direct = &z
|
||||||
|
survey.Update()
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
time.Sleep(time.Duration(OffsetQuestionTimer+v.Timer) * time.Millisecond)
|
||||||
|
survey.WSWriteAll(WSMessage{Action: "pause"})
|
||||||
|
WSAdminWriteAll(WSMessage{Action: "pause", SurveyId: &survey.Id})
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
survey.Direct = v.QuestionId
|
||||||
|
}
|
||||||
|
_, err = survey.Update()
|
||||||
|
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 if v.Action == "get_asks" {
|
||||||
|
if survey, err := getSurvey(sid); err != nil {
|
||||||
|
log.Println("Unable to retrieve survey:", err)
|
||||||
|
} else if asks, err := survey.GetAsks(v.Response == ""); err != nil {
|
||||||
|
log.Println("Unable to retrieve asks:", err)
|
||||||
|
} else {
|
||||||
|
for _, a := range asks {
|
||||||
|
wsjson.Write(context.Background(), ws, WSMessage{Action: "new_ask", UserId: &a.IdUser, QuestionId: &a.Id, Response: a.Content})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if v.Action == "mark_answered" && v.QuestionId != nil {
|
||||||
|
if asks, err := GetAsk(int(*v.QuestionId)); err != nil {
|
||||||
|
log.Println("Unable to retrieve ask:", err)
|
||||||
|
} else {
|
||||||
|
asks.Answered = true
|
||||||
|
err = asks.Update()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Unable to update:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if v.Action == "mark_answered" && v.Response == "all" {
|
||||||
|
if survey, err := getSurvey(sid); err != nil {
|
||||||
|
log.Println("Unable to retrieve survey:", err)
|
||||||
|
} else if asks, err := survey.GetAsks(v.Response == ""); err != nil {
|
||||||
|
log.Println("Unable to retrieve asks:", err)
|
||||||
|
} else {
|
||||||
|
for _, ask := range asks {
|
||||||
|
ask.Answered = true
|
||||||
|
err = ask.Update()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Unable to update:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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 {
|
||||||
|
if ws.sid == s.Id {
|
||||||
|
ws.c <- message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
go.mod
4
go.mod
@ -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/net v0.0.0-20210525063256-abc453219eb5 // indirect
|
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
|
||||||
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
|
||||||
)
|
)
|
||||||
|
78
go.sum
78
go.sum
@ -41,17 +41,35 @@ 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=
|
||||||
@ -90,6 +108,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.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=
|
||||||
@ -104,6 +123,8 @@ 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=
|
||||||
@ -115,21 +136,32 @@ 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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
|
||||||
|
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||||
github.com/kr/pretty v0.1.0/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=
|
||||||
@ -139,9 +171,14 @@ 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=
|
||||||
@ -215,27 +252,17 @@ 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-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
|
||||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
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-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
|
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg=
|
||||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||||
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 h1:Ati8dO7+U7mxpkPSxBZQEvzHVUYB/MqCklCN8ig5w/o=
|
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM=
|
||||||
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||||
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=
|
||||||
@ -257,6 +284,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-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=
|
||||||
@ -270,14 +298,17 @@ 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-20210423082822-04245dca01da/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-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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
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=
|
||||||
@ -405,12 +436,13 @@ 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=
|
||||||
@ -420,6 +452,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-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=
|
||||||
|
@ -76,6 +76,12 @@ func eraseCookie(w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func rawHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params, []byte), access ...func(*User, *http.Request) *APIErrorResponse) func(http.ResponseWriter, *http.Request, httprouter.Params) {
|
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
|
||||||
@ -136,7 +142,7 @@ func rawHandler(f func(http.ResponseWriter, *http.Request, httprouter.Params, []
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f(w, r, ps, body)
|
f(w, r, ps, user, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
56
help.go
Normal file
56
help.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
router.POST("/api/help", apiAuthHandler(func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
|
||||||
|
return formatApiResponse(u.NewNeedHelp())
|
||||||
|
}, loggedUser))
|
||||||
|
}
|
||||||
|
|
||||||
|
type NeedHelp struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
IdUser int64 `json:"id_user"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
Comment string `json:"comment,omitempty"`
|
||||||
|
DateTreated *time.Time `json:"treated,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) NewNeedHelp() (NeedHelp, error) {
|
||||||
|
if res, err := DBExec("INSERT INTO user_need_help (id_user, comment) VALUES (?, ?)", u.Id, ""); err != nil {
|
||||||
|
return NeedHelp{}, err
|
||||||
|
} else if hid, err := res.LastInsertId(); err != nil {
|
||||||
|
return NeedHelp{}, err
|
||||||
|
} else {
|
||||||
|
return NeedHelp{hid, u.Id, time.Now(), "Ton appel a bien été entendu.", nil}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NeedHelp) Update() error {
|
||||||
|
_, err := DBExec("UPDATE user_need_help SET id_user = ?, date = ?, comment = ?, date_treated = ? WHERE id_need_help = ?", h.IdUser, h.Date, h.Comment, h.DateTreated, h.Id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *NeedHelp) Delete() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM user_need_help WHERE id_need_help = ?", h.Id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearNeedHelp() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM user_need_help"); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
1
main.go
1
main.go
@ -63,6 +63,7 @@ func main() {
|
|||||||
flag.StringVar(&DevProxy, "dev", DevProxy, "Proxify traffic to this host for static assets")
|
flag.StringVar(&DevProxy, "dev", DevProxy, "Proxify traffic to this host for static assets")
|
||||||
flag.StringVar(&baseURL, "baseurl", baseURL, "URL prepended to each URL")
|
flag.StringVar(&baseURL, "baseurl", baseURL, "URL prepended to each URL")
|
||||||
flag.UintVar(¤tPromo, "current-promo", currentPromo, "Year of the current promotion")
|
flag.UintVar(¤tPromo, "current-promo", currentPromo, "Year of the current promotion")
|
||||||
|
flag.UintVar(&OffsetQuestionTimer, "offset-question-timer", OffsetQuestionTimer, "Duration to wait before sending pause msg in direct mode (in milliseconds)")
|
||||||
flag.Var(&localAuthUsers, "local-auth-user", "Allow local authentication for this user (bypass OIDC).")
|
flag.Var(&localAuthUsers, "local-auth-user", "Allow local authentication for this user (bypass OIDC).")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
17
questions.go
17
questions.go
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@ -38,10 +39,18 @@ 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", apiHandler(questionHandler(
|
router.GET("/api/questions/:qid", apiAuthHandler(questionAuthHandler(
|
||||||
func(s Question, _ []byte) HTTPResponse {
|
func(q Question, u *User, _ []byte) HTTPResponse {
|
||||||
return APIResponse{s}
|
if u.IsAdmin {
|
||||||
}), adminRestricted))
|
return APIResponse{q}
|
||||||
|
} else if s, err := getSurvey(int(q.IdSurvey)); err != nil {
|
||||||
|
return APIErrorResponse{err: err}
|
||||||
|
} else if s.Shown || (s.Direct != nil && *s.Direct == q.Id) {
|
||||||
|
return APIResponse{q}
|
||||||
|
} else {
|
||||||
|
return APIErrorResponse{err: fmt.Errorf("Not authorized"), status: http.StatusForbidden}
|
||||||
|
}
|
||||||
|
}), loggedUser))
|
||||||
router.GET("/api/surveys/:sid/questions/:qid", apiHandler(questionHandler(
|
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}
|
||||||
|
131
responses.go
131
responses.go
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -25,10 +26,25 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, response := range responses {
|
for _, response := range responses {
|
||||||
if len(response.Answer) > 0 {
|
if !s.Shown && (s.Direct == nil || *s.Direct != response.IdQuestion) {
|
||||||
|
return APIErrorResponse{err: fmt.Errorf("Cette question n'est pas disponible")}
|
||||||
|
} else if len(response.Answer) > 0 {
|
||||||
|
// Check if the response has changed
|
||||||
|
if response.Id != 0 {
|
||||||
|
if res, err := s.GetResponse(int(response.Id)); err == nil {
|
||||||
|
if res.IdUser == u.Id && res.Answer == response.Answer {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +60,15 @@ func init() {
|
|||||||
|
|
||||||
for _, response := range responses {
|
for _, response := range responses {
|
||||||
if len(response.Answer) > 0 {
|
if len(response.Answer) > 0 {
|
||||||
|
// Check if the response has changed
|
||||||
|
if response.Id != 0 {
|
||||||
|
if res, err := s.GetResponse(int(response.Id)); err == nil {
|
||||||
|
if res.IdUser == u.Id && res.Answer == response.Answer {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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}
|
||||||
}
|
}
|
||||||
@ -58,6 +83,10 @@ 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 {
|
||||||
@ -69,6 +98,24 @@ func init() {
|
|||||||
func(r Response, _ *User, _ []byte) HTTPResponse {
|
func(r Response, _ *User, _ []byte) HTTPResponse {
|
||||||
return APIResponse{r}
|
return APIResponse{r}
|
||||||
}), adminRestricted))
|
}), adminRestricted))
|
||||||
|
router.POST("/api/surveys/:sid/responses/:rid/report", apiAuthHandler(surveyResponseAuthHandler(
|
||||||
|
func(s *Survey, r Response, u *User, _ []byte) HTTPResponse {
|
||||||
|
if s == nil || !s.Corrected || r.IdUser != u.Id {
|
||||||
|
return APIErrorResponse{err: fmt.Errorf("Cette action est impossible pour l'instant"), status: http.StatusForbidden}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.TimeScored == nil || r.TimeReported == nil || r.TimeReported.Before(*r.TimeScored) {
|
||||||
|
now := time.Now()
|
||||||
|
r.TimeReported = &now
|
||||||
|
} else {
|
||||||
|
r.TimeReported = nil
|
||||||
|
}
|
||||||
|
if _, err := r.Update(); err != nil {
|
||||||
|
return APIErrorResponse{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
return APIResponse{r}
|
||||||
|
}), loggedUser))
|
||||||
router.GET("/api/surveys/:sid/questions/:qid/responses", apiAuthHandler(questionAuthHandler(
|
router.GET("/api/surveys/:sid/questions/:qid/responses", apiAuthHandler(questionAuthHandler(
|
||||||
func(q Question, u *User, _ []byte) HTTPResponse {
|
func(q Question, u *User, _ []byte) HTTPResponse {
|
||||||
return formatApiResponse(q.GetResponses())
|
return formatApiResponse(q.GetResponses())
|
||||||
@ -85,6 +132,30 @@ 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
|
||||||
|
|
||||||
|
if _, ok := _score_cache[current.IdUser]; ok {
|
||||||
|
if surveyId, err := current.GetSurveyId(); err == nil {
|
||||||
|
if _, ok = _score_cache[current.IdUser][surveyId]; ok {
|
||||||
|
delete(_score_cache[current.IdUser], surveyId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
new.Id = current.Id
|
new.Id = current.Id
|
||||||
new.IdUser = current.IdUser
|
new.IdUser = current.IdUser
|
||||||
return formatApiResponse(new.Update())
|
return formatApiResponse(new.Update())
|
||||||
@ -92,6 +163,14 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func responseHandler(f func(Response, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
|
func responseHandler(f func(Response, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
|
||||||
|
return func(ps httprouter.Params, body []byte) HTTPResponse {
|
||||||
|
return surveyResponseHandler(func(s *Survey, r Response, b []byte) HTTPResponse {
|
||||||
|
return f(r, b)
|
||||||
|
})(ps, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func surveyResponseHandler(f func(*Survey, Response, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse {
|
||||||
return func(ps httprouter.Params, body []byte) HTTPResponse {
|
return func(ps httprouter.Params, body []byte) HTTPResponse {
|
||||||
var survey *Survey = nil
|
var survey *Survey = nil
|
||||||
|
|
||||||
@ -107,18 +186,26 @@ func responseHandler(f func(Response, []byte) HTTPResponse) func(httprouter.Para
|
|||||||
if response, err := getResponse(rid); err != nil {
|
if response, err := getResponse(rid); err != nil {
|
||||||
return APIErrorResponse{err: err}
|
return APIErrorResponse{err: err}
|
||||||
} else {
|
} else {
|
||||||
return f(response, body)
|
return f(survey, response, body)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if response, err := survey.GetResponse(rid); err != nil {
|
if response, err := survey.GetResponse(rid); err != nil {
|
||||||
return APIErrorResponse{err: err}
|
return APIErrorResponse{err: err}
|
||||||
} else {
|
} else {
|
||||||
return f(response, body)
|
return f(survey, response, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func surveyResponseAuthHandler(f func(*Survey, Response, *User, []byte) HTTPResponse) func(*User, httprouter.Params, []byte) HTTPResponse {
|
||||||
|
return func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
|
||||||
|
return surveyResponseHandler(func(s *Survey, r Response, body []byte) HTTPResponse {
|
||||||
|
return f(s, r, u, body)
|
||||||
|
})(ps, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func responseAuthHandler(f func(Response, *User, []byte) HTTPResponse) func(*User, httprouter.Params, []byte) HTTPResponse {
|
func responseAuthHandler(f func(Response, *User, []byte) HTTPResponse) func(*User, httprouter.Params, []byte) HTTPResponse {
|
||||||
return func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
|
return func(u *User, ps httprouter.Params, body []byte) HTTPResponse {
|
||||||
return responseHandler(func(r Response, body []byte) HTTPResponse {
|
return responseHandler(func(r Response, body []byte) HTTPResponse {
|
||||||
@ -137,17 +224,18 @@ type Response struct {
|
|||||||
ScoreExplaination *string `json:"score_explaination,omitempty"`
|
ScoreExplaination *string `json:"score_explaination,omitempty"`
|
||||||
IdCorrector *int64 `json:"id_corrector,omitempty"`
|
IdCorrector *int64 `json:"id_corrector,omitempty"`
|
||||||
TimeScored *time.Time `json:"time_scored,omitempty"`
|
TimeScored *time.Time `json:"time_scored,omitempty"`
|
||||||
|
TimeReported *time.Time `json:"time_reported,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Survey) GetResponses() (responses []Response, err error) {
|
func (s *Survey) GetResponses() (responses []Response, err error) {
|
||||||
if rows, errr := DBQuery("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 INNER JOIN survey_quests Q ON Q.id_question = R.id_question WHERE Q.id_survey=?", s.Id); errr != nil {
|
if rows, errr := DBQuery("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, R.time_reported FROM survey_responses R INNER JOIN survey_quests Q ON Q.id_question = R.id_question WHERE Q.id_survey=?", s.Id); errr != nil {
|
||||||
return nil, errr
|
return nil, errr
|
||||||
} else {
|
} else {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var r Response
|
var r Response
|
||||||
if err = rows.Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored); err != nil {
|
if err = rows.Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored, &r.TimeReported); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
responses = append(responses, r)
|
responses = append(responses, r)
|
||||||
@ -161,14 +249,14 @@ func (s *Survey) GetResponses() (responses []Response, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Survey) GetMyResponses(u *User, showScore bool) (responses []Response, err error) {
|
func (s *Survey) GetMyResponses(u *User, showScore bool) (responses []Response, err error) {
|
||||||
if rows, errr := DBQuery("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 INNER JOIN survey_quests Q ON Q.id_question = R.id_question WHERE Q.id_survey=? AND R.id_user=? ORDER BY time_submit DESC", s.Id, u.Id); errr != nil {
|
if rows, errr := DBQuery("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, R.time_reported FROM survey_responses R INNER JOIN survey_quests Q ON Q.id_question = R.id_question WHERE Q.id_survey=? AND R.id_user=? ORDER BY time_submit DESC", s.Id, u.Id); errr != nil {
|
||||||
return nil, errr
|
return nil, errr
|
||||||
} else {
|
} else {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var r Response
|
var r Response
|
||||||
if err = rows.Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored); err != nil {
|
if err = rows.Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored, &r.TimeReported); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !showScore {
|
if !showScore {
|
||||||
@ -185,15 +273,24 @@ 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, R.time_reported 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, &r.TimeReported)
|
||||||
|
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, time_reported 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
|
||||||
} else {
|
} else {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var r Response
|
var r Response
|
||||||
if err = rows.Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored); err != nil {
|
if err = rows.Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored, &r.TimeReported); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
responses = append(responses, r)
|
responses = append(responses, r)
|
||||||
@ -207,12 +304,12 @@ func (q *Question) GetResponses() (responses []Response, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getResponse(id int) (r Response, err error) {
|
func getResponse(id int) (r Response, err error) {
|
||||||
err = DBQueryRow("SELECT id_response, id_question, id_user, answer, time_submit, score, score_explanation, id_corrector, time_scored FROM survey_responses WHERE id_response=?", id).Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored)
|
err = DBQueryRow("SELECT id_response, id_question, id_user, answer, time_submit, score, score_explanation, id_corrector, time_scored, time_reported FROM survey_responses WHERE id_response=?", id).Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored, &r.TimeReported)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Survey) GetResponse(id int) (r Response, err error) {
|
func (s *Survey) GetResponse(id int) (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 INNER JOIN survey_quests Q ON Q.id_question = R.id_question WHERE R.id_response=? AND Q.id_survey=?", id, s.Id).Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored)
|
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, R.time_reported FROM survey_responses R INNER JOIN survey_quests Q ON Q.id_question = R.id_question WHERE R.id_response=? AND Q.id_survey=?", id, s.Id).Scan(&r.Id, &r.IdQuestion, &r.IdUser, &r.Answer, &r.TimeSubmit, &r.Score, &r.ScoreExplaination, &r.IdCorrector, &r.TimeScored, &r.TimeReported)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,12 +319,20 @@ func (s *Survey) NewResponse(id_question int64, id_user int64, response string)
|
|||||||
} else if rid, err := res.LastInsertId(); err != nil {
|
} else if rid, err := res.LastInsertId(); err != nil {
|
||||||
return Response{}, err
|
return Response{}, err
|
||||||
} else {
|
} else {
|
||||||
return Response{rid, id_question, id_user, response, time.Now(), nil, nil, nil, nil}, nil
|
return Response{rid, id_question, id_user, response, time.Now(), nil, nil, nil, nil, nil}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Response) GetSurveyId() (int64, error) {
|
||||||
|
if q, err := getQuestion(int(r.IdQuestion)); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return q.IdSurvey, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Response) Update() (Response, error) {
|
func (r Response) Update() (Response, error) {
|
||||||
_, err := DBExec("UPDATE survey_responses SET id_question = ?, id_user = ?, answer = ?, time_submit = ?, score = ?, score_explanation = ?, id_corrector = ?, time_scored = ? WHERE id_response = ?", r.IdQuestion, r.IdUser, r.Answer, r.TimeSubmit, r.Score, r.ScoreExplaination, r.IdCorrector, r.TimeScored, r.Id)
|
_, err := DBExec("UPDATE survey_responses SET id_question = ?, id_user = ?, answer = ?, time_submit = ?, score = ?, score_explanation = ?, id_corrector = ?, time_scored = ?, time_reported = ? WHERE id_response = ?", r.IdQuestion, r.IdUser, r.Answer, r.TimeSubmit, r.Score, r.ScoreExplaination, r.IdCorrector, r.TimeScored, r.TimeReported, r.Id)
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
static.go
18
static.go
@ -50,16 +50,16 @@ func serveOrReverse(forced_url string) func(w http.ResponseWriter, r *http.Reque
|
|||||||
func init() {
|
func init() {
|
||||||
Router().GET("/@fs/*_", serveOrReverse(""))
|
Router().GET("/@fs/*_", serveOrReverse(""))
|
||||||
Router().GET("/", serveOrReverse(""))
|
Router().GET("/", serveOrReverse(""))
|
||||||
Router().GET("/auth", serveOrReverse(""))
|
Router().GET("/_app/*_", serveOrReverse(""))
|
||||||
Router().GET("/bug-bounty", serveOrReverse(""))
|
Router().GET("/auth", serveOrReverse("/"))
|
||||||
Router().GET("/grades", serveOrReverse(""))
|
Router().GET("/bug-bounty", serveOrReverse("/"))
|
||||||
Router().GET("/surveys", serveOrReverse(""))
|
Router().GET("/grades", serveOrReverse("/"))
|
||||||
Router().GET("/surveys/*_", serveOrReverse(""))
|
Router().GET("/help", serveOrReverse("/"))
|
||||||
Router().GET("/users", serveOrReverse(""))
|
Router().GET("/surveys", serveOrReverse("/"))
|
||||||
Router().GET("/users/*_", serveOrReverse(""))
|
Router().GET("/surveys/*_", 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(""))
|
||||||
Router().GET("/js/*_", serveOrReverse(""))
|
|
||||||
Router().GET("/views/*_", serveOrReverse(""))
|
|
||||||
}
|
}
|
||||||
|
76
surveys.go
76
surveys.go
@ -6,20 +6,37 @@ 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 AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC", currentPromo)))
|
return formatApiResponse(getSurveys(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND NOW() > start_availability AND promo = %d ORDER BY start_availability ASC", currentPromo)))
|
||||||
} else if u.IsAdmin {
|
} 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 {
|
||||||
return formatApiResponse(getSurveys(fmt.Sprintf("WHERE shown = TRUE AND promo = %d ORDER BY start_availability ASC", u.Promo)))
|
surveys, err := getSurveys(fmt.Sprintf("WHERE (shown = TRUE OR direct IS NOT NULL) AND promo = %d ORDER BY start_availability ASC", u.Promo))
|
||||||
|
if err != nil {
|
||||||
|
return APIErrorResponse{err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
@ -32,11 +49,16 @@ func init() {
|
|||||||
new.Promo = currentPromo
|
new.Promo = currentPromo
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatApiResponse(NewSurvey(new.Title, new.Promo, new.Shown, new.StartAvailability, new.EndAvailability))
|
return formatApiResponse(NewSurvey(new.Title, new.Promo, new.Group, new.Shown, new.Direct, 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.Shown) || (u != nil && u.IsAdmin) {
|
if u == nil {
|
||||||
|
return APIErrorResponse{
|
||||||
|
status: http.StatusUnauthorized,
|
||||||
|
err: errors.New("Veuillez vous connecter pour accéder à cette page."),
|
||||||
|
}
|
||||||
|
} else if (s.Promo == u.Promo && (s.Group == "" || strings.Contains(u.Groups, ","+s.Group+",") && s.Shown)) || u.IsAdmin {
|
||||||
return APIResponse{s}
|
return APIResponse{s}
|
||||||
} else {
|
} else {
|
||||||
return APIErrorResponse{
|
return APIErrorResponse{
|
||||||
@ -52,6 +74,22 @@ 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(
|
||||||
@ -125,21 +163,23 @@ 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, shown, corrected, start_availability, end_availability FROM surveys "+cnd, param...); errr != nil {
|
if rows, errr := DBQuery("SELECT id_survey, title, promo, grp, shown, direct, corrected, start_availability, end_availability FROM surveys "+cnd, param...); errr != nil {
|
||||||
return nil, errr
|
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.Shown, &s.Corrected, &s.StartAvailability, &s.EndAvailability); err != nil {
|
if err = rows.Scan(&s.Id, &s.Title, &s.Promo, &s.Group, &s.Shown, &s.Direct, &s.Corrected, &s.StartAvailability, &s.EndAvailability); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
surveys = append(surveys, s)
|
surveys = append(surveys, s)
|
||||||
@ -153,24 +193,32 @@ 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, 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)
|
err = DBQueryRow("SELECT id_survey, title, promo, grp, shown, direct, corrected, start_availability, end_availability FROM surveys WHERE id_survey=?", id).Scan(&s.Id, &s.Title, &s.Promo, &s.Group, &s.Shown, &s.Direct, &s.Corrected, &s.StartAvailability, &s.EndAvailability)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSurvey(title string, promo uint, shown bool, startAvailability time.Time, endAvailability time.Time) (*Survey, error) {
|
func NewSurvey(title string, promo uint, group string, shown bool, direct *int64, startAvailability time.Time, endAvailability time.Time) (*Survey, error) {
|
||||||
if res, err := DBExec("INSERT INTO surveys (title, promo, shown, start_availability, end_availability) VALUES (?, ?, ?, ?, ?)", title, promo, shown, startAvailability, endAvailability); err != nil {
|
if res, err := DBExec("INSERT INTO surveys (title, promo, grp, shown, direct, start_availability, end_availability) VALUES (?, ?, ?, ?, ?, ?, ?)", title, promo, group, shown, direct, startAvailability, endAvailability); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if sid, err := res.LastInsertId(); err != nil {
|
} else if sid, err := res.LastInsertId(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return &Survey{sid, title, promo, shown, false, startAvailability, endAvailability}, nil
|
return &Survey{sid, title, promo, group, shown, direct, false, startAvailability, endAvailability}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Survey) GetScore(u *User) (score *float64, err error) {
|
func (s Survey) GetScore(u *User) (score *float64, err error) {
|
||||||
err = DBQueryRow("SELECT SUM(score)/COUNT(*) FROM student_scores WHERE id_survey=? AND id_user=?", s.Id, u.Id).Scan(&score)
|
if _, ok := _score_cache[u.Id]; !ok {
|
||||||
if score != nil {
|
_score_cache[u.Id] = map[int64]*float64{}
|
||||||
*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
|
||||||
}
|
}
|
||||||
@ -201,7 +249,7 @@ func (s Survey) GetScores() (scores map[int64]*float64, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Survey) Update() (*Survey, error) {
|
func (s *Survey) Update() (*Survey, error) {
|
||||||
if _, 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 {
|
if _, err := DBExec("UPDATE surveys SET title = ?, promo = ?, grp = ?, shown = ?, direct = ?, corrected = ?, start_availability = ?, end_availability = ? WHERE id_survey = ?", s.Title, s.Promo, s.Group, s.Shown, s.Direct, s.Corrected, s.StartAvailability, s.EndAvailability, s.Id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return s, err
|
return s, err
|
||||||
|
0
atsebayt/.gitignore → ui/.gitignore
vendored
0
atsebayt/.gitignore → ui/.gitignore
vendored
957
atsebayt/package-lock.json → ui/package-lock.json
generated
957
atsebayt/package-lock.json → ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,19 +11,20 @@
|
|||||||
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/kit": "^1.0.0-next.266",
|
"@sveltejs/adapter-static": "^1.0.0-next.28",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
"@sveltejs/kit": "^1.0.0-next.288",
|
||||||
"@typescript-eslint/parser": "^4.33.0",
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
"eslint": "^7.32.0",
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint": "^8.0.0",
|
||||||
"eslint-plugin-svelte3": "^3.4.0",
|
"eslint-config-prettier": "^8.4.0",
|
||||||
|
"eslint-plugin-svelte3": "^3.4.1",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"prettier-plugin-svelte": "^2.6.0",
|
"prettier-plugin-svelte": "^2.6.0",
|
||||||
"svelte": "^3.46.4",
|
"svelte": "^3.46.4",
|
||||||
"svelte-check": "^2.4.3",
|
"svelte-check": "^2.4.5",
|
||||||
"svelte-preprocess": "^4.10.3",
|
"svelte-preprocess": "^4.10.4",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"typescript": "^4.5.5"
|
"typescript": "^4.6.2"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<link href="css/bootstrap.min.css" type="text/css" rel="stylesheet">
|
<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>
|
<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">
|
<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>
|
<script async defer data-website-id="7f459249-ea9f-43a6-a0d9-60bd9fa86c16" src="https://pythagore.p0m.fr/pythagore.js"></script>
|
47
ui/src/components/Correction.svelte
Normal file
47
ui/src/components/Correction.svelte
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<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…</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}
|
112
ui/src/components/CorrectionReference.svelte
Normal file
112
ui/src/components/CorrectionReference.svelte
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<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} %
|
||||||
|
{: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>
|
124
ui/src/components/CorrectionResponseFooter.svelte
Normal file
124
ui/src/components/CorrectionResponseFooter.svelte
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<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} %
|
||||||
|
</strong>
|
||||||
|
<div class="col">
|
||||||
|
{my_correction.score_explaination}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
129
ui/src/components/CorrectionResponses.svelte
Normal file
129
ui/src/components/CorrectionResponses.svelte
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
<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 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 <= r.time_reported || !r.time_scored) && (!filter || ((filter[0] == '!' && !r.value.match(filter.substring(1))) || r.value.match(filter))));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyCorrections() {
|
||||||
|
for (const r of filteredResponses) {
|
||||||
|
const my_correction = { };
|
||||||
|
|
||||||
|
for (const tpl of templates) {
|
||||||
|
if (!tpl.regexp && tpl.label) continue;
|
||||||
|
|
||||||
|
if (tpl.regexp && (tpl.regexp[0] == '!' && !r.value.match(tpl.regexp.substring(1))) || 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…</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}
|
142
ui/src/components/QuestionForm.svelte
Normal file
142
ui/src/components/QuestionForm.svelte
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<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 type="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 type="button" class="btn btn-sm btn-success ms-1 float-end" on:click={saveQuestion}>
|
||||||
|
<i class="bi bi-check"></i>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button type="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 :
|
||||||
|
<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 if question.kind}
|
||||||
|
{#if !question.id}
|
||||||
|
Veuillez enregistrer la question pour pouvoir ajouter des propositions.
|
||||||
|
{: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}
|
||||||
|
on:change={() => { dispatch("change"); }}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
{/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}
|
||||||
|
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}
|
||||||
|
</div>
|
||||||
|
</div>
|
44
ui/src/components/QuestionHeader.svelte
Normal file
44
ui/src/components/QuestionHeader.svelte
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<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 :</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>
|
@ -1,21 +1,25 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
import { QuestionProposal } from '../lib/questions';
|
import { QuestionProposal } from '../lib/questions';
|
||||||
|
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
export let proposals = [];
|
export let proposals = [];
|
||||||
export let kind = 'mcq';
|
export let kind = 'mcq';
|
||||||
|
export let prefixid = '';
|
||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
export let id_question = 0;
|
export let id_question = 0;
|
||||||
export let value;
|
export let value;
|
||||||
|
|
||||||
let valueCheck = [];
|
let valueCheck = [];
|
||||||
$: {
|
$: {
|
||||||
console.log(value);
|
|
||||||
if (value) {
|
if (value) {
|
||||||
valueCheck = value.split(',');
|
valueCheck = value.split(',');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
function addProposal() {
|
function addProposal() {
|
||||||
const p = new QuestionProposal();
|
const p = new QuestionProposal();
|
||||||
p.id_question = id_question;
|
p.id_question = id_question;
|
||||||
@ -31,21 +35,22 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
name={'proposal' + proposal.id_question}
|
name={prefixid + 'proposal' + proposal.id_question}
|
||||||
id={'p' + proposal.id}
|
id={prefixid + 'p' + proposal.id}
|
||||||
bind:group={valueCheck}
|
bind:group={valueCheck}
|
||||||
value={String(proposal.id)}
|
value={proposal.id?proposal.id.toString():''}
|
||||||
on:change={() => { value = valueCheck.join(',')}}
|
on:change={() => { value = valueCheck.join(','); dispatch("change"); }}
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
name={'proposal' + proposal.id_question}
|
name={prefixid + 'proposal' + proposal.id_question}
|
||||||
id={'p' + proposal.id}
|
id={prefixid + 'p' + proposal.id}
|
||||||
bind:group={value}
|
bind:group={value}
|
||||||
value={String(proposal.id)}
|
value={proposal.id?proposal.id.toString():''}
|
||||||
|
on:change={() => { dispatch("change"); }}
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if edit}
|
{#if edit}
|
||||||
@ -80,7 +85,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<label
|
<label
|
||||||
class="form-check-label"
|
class="form-check-label"
|
||||||
for={'p' + proposal.id}
|
for={prefixid + 'p' + proposal.id}
|
||||||
>
|
>
|
||||||
{proposal.label}
|
{proposal.label}
|
||||||
</label>
|
</label>
|
||||||
@ -106,6 +111,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-link"
|
class="btn btn-sm btn-link"
|
||||||
|
disabled={proposals.length > 0 && !proposals[proposals.length-1].id}
|
||||||
on:click={addProposal}
|
on:click={addProposal}
|
||||||
>
|
>
|
||||||
ajouter
|
ajouter
|
125
ui/src/components/ResponseCorrected.svelte
Normal file
125
ui/src/components/ResponseCorrected.svelte
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<script>
|
||||||
|
import { user } from '../stores/user';
|
||||||
|
import { ToastsStore } from '../stores/toasts';
|
||||||
|
|
||||||
|
export let response = null;
|
||||||
|
export let survey = null;
|
||||||
|
let reportInProgress = false;
|
||||||
|
|
||||||
|
function report() {
|
||||||
|
reportInProgress = true;
|
||||||
|
response.report(survey).then((res) => {
|
||||||
|
reportInProgress = false;
|
||||||
|
response.time_reported = res.time_reported;
|
||||||
|
if (res.time_reported >= res.time_scored) {
|
||||||
|
ToastsStore.addToast({
|
||||||
|
msg: "Ton signalement a bien été pris en compte.",
|
||||||
|
color: "success",
|
||||||
|
title: "Signaler une erreur de correction",
|
||||||
|
});
|
||||||
|
} else if (!res.time_reported) {
|
||||||
|
ToastsStore.addToast({
|
||||||
|
msg: "La correction de ta réponse n'est maintenant plus signalée, signalement annulé.",
|
||||||
|
color: "info",
|
||||||
|
title: "Signaler une erreur de correction",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ToastsStore.addErrorToast({
|
||||||
|
msg: "Quelque chose s'est mal passé lors du signalement du problème.\nSi le problème persiste, contacte directement ton professeur.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, (error) => {
|
||||||
|
reportInProgress = false;
|
||||||
|
ToastsStore.addErrorToast({
|
||||||
|
msg: "Une erreur s'est produite durant le signalement du problème : " + error + "\nSi le problème persiste, contacte directement ton professeur.",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</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} %, ce qui correspond à {Math.trunc(response.score*10/5)/10}/20."
|
||||||
|
>
|
||||||
|
{response.score} %
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
{#if response.id_user == $user.id}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="d-block btn btn-sm float-end"
|
||||||
|
class:btn-outline-success={!response.time_reported && response.score >= 95}
|
||||||
|
class:btn-outline-info={!response.time_reported && response.score < 95 && response.score >= 70}
|
||||||
|
class:btn-outline-warning={!response.time_reported && response.score < 70 && response.score >= 45}
|
||||||
|
class:btn-outline-danger={!response.time_reported && response.score < 45}
|
||||||
|
class:btn-success={response.time_reported && response.score >= 95}
|
||||||
|
class:btn-info={response.time_reported && response.score < 95 && response.score >= 70}
|
||||||
|
class:btn-warning={response.time_reported && response.score < 70 && response.score >= 45}
|
||||||
|
class:btn-danger={response.time_reported && response.score < 45}
|
||||||
|
title="Signaler un problème avec la correction"
|
||||||
|
disabled={reportInProgress}
|
||||||
|
on:click={report}
|
||||||
|
>
|
||||||
|
{#if reportInProgress}
|
||||||
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||||
|
{:else if response.time_reported > response.time_scored}
|
||||||
|
<i class="bi bi-exclamation-octagon-fill"></i>
|
||||||
|
{:else}
|
||||||
|
<i class="bi bi-exclamation-octagon"></i>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{:else if $user.is_admin && response.time_reported}
|
||||||
|
{#if response.time_reported > response.time_scored}
|
||||||
|
<i
|
||||||
|
class="float-end bi bi-exclamation-octagon-fill"
|
||||||
|
class:text-warning={response.score < 45}
|
||||||
|
class:text-danger={response.score >= 45}
|
||||||
|
></i>
|
||||||
|
{:else}
|
||||||
|
<i
|
||||||
|
class="float-end bi bi-exclamation-octagon"
|
||||||
|
class:text-warning={response.score < 45}
|
||||||
|
class:text-danger={response.score >= 45}
|
||||||
|
></i>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#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 !</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é ?
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
77
ui/src/components/StudentGrades.svelte
Normal file
77
ui/src/components/StudentGrades.svelte
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<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…
|
||||||
|
</div>
|
||||||
|
{:then surveys}
|
||||||
|
{#await getGrades()}
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<div class="spinner-border me-2" role="status"></div>
|
||||||
|
Chargement des notes…
|
||||||
|
</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…
|
||||||
|
</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] && grades[user.id][survey.id]?grades[user.id][survey.id]:""}</td>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{/await}
|
@ -2,14 +2,19 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import { getQuestions } from '../lib/questions';
|
||||||
|
import { ToastsStore } from '../stores/toasts';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
export let survey = null;
|
export let survey = null;
|
||||||
|
|
||||||
function saveSurvey() {
|
function saveSurvey() {
|
||||||
survey.save().then((response) => {
|
survey.save().then((response) => {
|
||||||
dispatch('saved');
|
dispatch('saved', response);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
console.log(error)
|
ToastsStore.addErrorToast({
|
||||||
|
msg: error.errmsg,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +22,9 @@
|
|||||||
survey.delete().then((response) => {
|
survey.delete().then((response) => {
|
||||||
goto(`surveys`);
|
goto(`surveys`);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
console.log(error)
|
ToastsStore.addErrorToast({
|
||||||
|
msg: error.errmsg,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,7 +32,9 @@
|
|||||||
survey.duplicate().then((response) => {
|
survey.duplicate().then((response) => {
|
||||||
goto(`surveys/${response.id}`);
|
goto(`surveys/${response.id}`);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.log(error)
|
ToastsStore.addErrorToast({
|
||||||
|
msg: error.errmsg,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,11 +42,22 @@
|
|||||||
|
|
||||||
<form on:submit|preventDefault={saveSurvey}>
|
<form on:submit|preventDefault={saveSurvey}>
|
||||||
|
|
||||||
|
{#if survey.id}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-3 text-sm-end">
|
||||||
|
<label for="title" class="col-form-label col-form-label-sm">Identifiant du questionnaire</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
<input type="text" class="form-control-plaintext form-control-sm" id="title" value={survey.id}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 text-sm-end">
|
<div class="col-sm-3 text-sm-end">
|
||||||
<label for="title" class="col-form-label col-form-label-sm">Titre du questionnaire</label>
|
<label for="title" class="col-form-label col-form-label-sm">Titre du questionnaire</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-8">{survey.id}
|
<div class="col-sm-8">
|
||||||
<input type="text" class="form-control form-control-sm" id="title" bind:value={survey.title}>
|
<input type="text" class="form-control form-control-sm" id="title" bind:value={survey.title}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -51,6 +71,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
|
{#if survey.id}
|
||||||
|
<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>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 text-sm-end">
|
<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>
|
<label for="start_availability" class="col-form-label col-form-label-sm">Date de début</label>
|
@ -4,8 +4,10 @@
|
|||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if survey.startAvailability() > Date.now()}<span class="badge bg-info {className}">Prévu</span>>
|
{#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.endAvailability() > Date.now()}<span class="badge bg-warning {className}">En cours</span>
|
||||||
|
{:else if !survey.__start_availability}<span class="badge bg-dark {className}">Nouveau</span>
|
||||||
{:else if !survey.corrected}<span class="badge bg-primary text-light {className}">Terminé</span>
|
{:else if !survey.corrected}<span class="badge bg-primary text-light {className}">Terminé</span>
|
||||||
{:else}<span class="badge bg-success {className}">Corrigé</span>
|
{:else}<span class="badge bg-success {className}">Corrigé</span>
|
||||||
{/if}
|
{/if}
|
@ -6,6 +6,17 @@
|
|||||||
import SurveyBadge from '../components/SurveyBadge.svelte';
|
import SurveyBadge from '../components/SurveyBadge.svelte';
|
||||||
import { getSurveys } from '../lib/surveys';
|
import { getSurveys } from '../lib/surveys';
|
||||||
import { getScore } from '../lib/users';
|
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>
|
</script>
|
||||||
|
|
||||||
<table class="table table-striped table-hover mb-0">
|
<table class="table table-striped table-hover mb-0">
|
||||||
@ -18,7 +29,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{#await getSurveys()}
|
{#await req_surveys}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="text-center py-3">
|
<td colspan="5" class="text-center py-3">
|
||||||
<div class="spinner-border mx-3" role="status"></div>
|
<div class="spinner-border mx-3" role="status"></div>
|
||||||
@ -28,7 +39,7 @@
|
|||||||
{:then surveys}
|
{:then surveys}
|
||||||
<tbody style="cursor: pointer;">
|
<tbody style="cursor: pointer;">
|
||||||
{#each surveys as survey, sid (survey.id)}
|
{#each surveys as survey, sid (survey.id)}
|
||||||
{#if survey.shown && (!$user || (!$user.was_admin || $user.promo == survey.promo) || $user.is_admin)}
|
{#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)}
|
{#if $user && $user.is_admin && (sid == 0 || surveys[sid-1].promo != survey.promo)}
|
||||||
<tr class="bg-info text-light">
|
<tr class="bg-info text-light">
|
||||||
<th colspan="5" class="fw-bold">
|
<th colspan="5" class="fw-bold">
|
||||||
@ -36,33 +47,34 @@
|
|||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
{/if}
|
||||||
<tr on:click={e => goto(`surveys/${survey.id}`)}>
|
<tr on:click={e => goto(survey.direct != null ?`surveys/${survey.id}/live`:$user.is_admin?`surveys/${survey.id}/responses`:`surveys/${survey.id}`)}>
|
||||||
<td>
|
<td>
|
||||||
{survey.title}
|
{survey.title}
|
||||||
<SurveyBadge {survey} class="float-end" />
|
<SurveyBadge {survey} class="float-end" />
|
||||||
</td>
|
</td>
|
||||||
{#if survey.start_availability > Date.now()}
|
{#if survey.startAvailability() > Date.now()}
|
||||||
<td>
|
<td>
|
||||||
<DateFormat date={survey.start_availability} dateStyle="medium" timeStyle="medium" />
|
<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>
|
<i class="bi bi-arrow-bar-right"></i>
|
||||||
</td>
|
</td>
|
||||||
{:else}
|
{:else}
|
||||||
<td>
|
<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>
|
<i class="bi bi-arrow-bar-left"></i>
|
||||||
<DateFormat date={survey.end_availability} dateStyle="medium" timeStyle="medium" />
|
<DateFormat date={survey.end_availability} dateStyle="medium" timeStyle="medium" />
|
||||||
</td>
|
</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !$user}
|
{#if $user}
|
||||||
{:else if !survey.corrected}
|
{#if !survey.corrected}
|
||||||
<td>N/A</td>
|
<td>N/A</td>
|
||||||
{:else}
|
{:else}
|
||||||
<td>
|
<td>
|
||||||
{#await getScore(survey)}
|
{#await getScore(survey)}
|
||||||
<div class="spinner-border spinner-border-sm" role="status"></div>
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
||||||
{:then score}
|
{:then score}
|
||||||
{score.score}
|
{score.score}
|
||||||
{/await}
|
{/await}
|
||||||
</td>
|
</td>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</tr>
|
</tr>
|
||||||
{/if}
|
{/if}
|
@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { user } from '../stores/user';
|
import { user } from '../stores/user';
|
||||||
|
import { ToastsStore } from '../stores/toasts';
|
||||||
import QuestionForm from '../components/QuestionForm.svelte';
|
import QuestionForm from '../components/QuestionForm.svelte';
|
||||||
import { Question } from '../lib/questions';
|
import { Question } from '../lib/questions';
|
||||||
|
|
||||||
@ -14,15 +15,25 @@
|
|||||||
|
|
||||||
const res = [];
|
const res = [];
|
||||||
for (const r in responses) {
|
for (const r in responses) {
|
||||||
res.push({"id_question": responses[r].id_question, "value": String(responses[r].value)})
|
res.push({
|
||||||
|
"id": responses[r].id,
|
||||||
|
"id_question": responses[r].id_question,
|
||||||
|
"value": String(responses[r].value)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
survey.submitAnswers(res, id_user).then((response) => {
|
survey.submitAnswers(res, id_user).then((response) => {
|
||||||
submitInProgress = false;
|
submitInProgress = false;
|
||||||
console.log("Vos réponses ont bien étés sauvegardées.");
|
ToastsStore.addToast({
|
||||||
|
msg: "Vos réponses ont bien étés sauvegardées.",
|
||||||
|
color: "success",
|
||||||
|
title: "Questionnaire",
|
||||||
|
});
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
submitInProgress = false;
|
submitInProgress = false;
|
||||||
console.log("Une erreur s'est produite durant l'envoi de vos réponses : " + error + "<br>Veuillez réessayer dans quelques instants.");
|
ToastsStore.addErrorToast({
|
||||||
|
msg: "Une erreur s'est produite durant l'envoi de vos réponses : " + error + "\nVeuillez réessayer dans quelques instants.",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +79,7 @@
|
|||||||
<form class="mb-5" on:submit|preventDefault={submitAnswers}>
|
<form class="mb-5" on:submit|preventDefault={submitAnswers}>
|
||||||
{#each questions as question, qid (question.id)}
|
{#each questions as question, qid (question.id)}
|
||||||
<QuestionForm
|
<QuestionForm
|
||||||
|
{survey}
|
||||||
qid={qid}
|
qid={qid}
|
||||||
question={question}
|
question={question}
|
||||||
response_history={responses[question.id]}
|
response_history={responses[question.id]}
|
||||||
@ -86,12 +98,14 @@
|
|||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<button type="submit" class="btn btn-primary" disabled={submitInProgress || (survey.isFinished() && !$user.is_admin)}>
|
{#if !survey.corrected || $user.is_admin}
|
||||||
{#if submitInProgress}
|
<button type="submit" class="btn btn-primary" disabled={submitInProgress || (survey.isFinished() && !$user.is_admin)}>
|
||||||
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
|
{#if submitInProgress}
|
||||||
{/if}
|
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
|
||||||
Soumettre les réponses
|
{/if}
|
||||||
</button>
|
Soumettre les réponses
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{#if $user && $user.is_admin}
|
{#if $user && $user.is_admin}
|
||||||
<button type="button" class="btn btn-info" on:click={addQuestion}>
|
<button type="button" class="btn btn-info" on:click={addQuestion}>
|
||||||
Ajouter une question
|
Ajouter une question
|
18
ui/src/components/Toaster.svelte
Normal file
18
ui/src/components/Toaster.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<script>
|
||||||
|
import { ToastsStore } from '../stores/toasts';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="toast-container position-fixed 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"> </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>
|
@ -5,7 +5,7 @@
|
|||||||
let className = '';
|
let className = '';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
const rendus_baseurl = "https://virli.nemunai.re/rendus/";
|
const rendus_baseurl = "https://adlin.nemunai.re/rendus/";
|
||||||
|
|
||||||
async function getUserRendus() {
|
async function getUserRendus() {
|
||||||
const res = await fetch(`${rendus_baseurl}${$user.login}.json`)
|
const res = await fetch(`${rendus_baseurl}${$user.login}.json`)
|
88
ui/src/lib/correctionTemplates.js
Normal file
88
ui/src/lib/correctionTemplates.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Response } from './response';
|
||||||
|
|
||||||
export class QuestionProposal {
|
export class QuestionProposal {
|
||||||
constructor(res) {
|
constructor(res) {
|
||||||
if (res) {
|
if (res) {
|
||||||
@ -66,7 +68,36 @@ export class Question {
|
|||||||
headers: {'Accept': 'application/json'},
|
headers: {'Accept': 'application/json'},
|
||||||
});
|
});
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
return (await res.json()).map((p) => new QuestionProposal(p))
|
const data = await res.json();
|
||||||
|
if (data === null) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return (data).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 {
|
} else {
|
||||||
throw new Error((await res.json()).errmsg);
|
throw new Error((await res.json()).errmsg);
|
||||||
}
|
}
|
||||||
@ -102,10 +133,24 @@ export class Question {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getQuestions(sid) {
|
export async function getQuestion(qid) {
|
||||||
const res = await fetch(`api/surveys/${sid}/questions`, {headers: {'Accept': 'application/json'}})
|
const res = await fetch(`api/questions/${qid}`, {headers: {'Accept': 'application/json'}})
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
return (await res.json()).map((e) => new Question(e))
|
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) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data === null) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return (data).map((q) => new Question(q))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error((await res.json()).errmsg);
|
throw new Error((await res.json()).errmsg);
|
||||||
}
|
}
|
49
ui/src/lib/response.js
Normal file
49
ui/src/lib/response.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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, time_reported }) {
|
||||||
|
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;
|
||||||
|
this.time_reported = time_reported;
|
||||||
|
}
|
||||||
|
|
||||||
|
async report(survey) {
|
||||||
|
const res = await fetch(`api/surveys/${survey.id}/responses/${this.id}/report`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Accept': 'application/json'},
|
||||||
|
});
|
||||||
|
if (res.status == 200) {
|
||||||
|
const data = await res.json();
|
||||||
|
this.update(data);
|
||||||
|
return data;
|
||||||
|
} else {
|
||||||
|
throw new Error((await res.json()).errmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,20 @@
|
|||||||
import { getQuestions } from './questions';
|
import { getQuestions } from './questions';
|
||||||
|
import { Response } from './response';
|
||||||
|
|
||||||
|
export class Survey {
|
||||||
class Survey {
|
|
||||||
constructor(res) {
|
constructor(res) {
|
||||||
if (res) {
|
if (res) {
|
||||||
this.update(res);
|
this.update(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update({ id, title, promo, shown, corrected, start_availability, end_availability }) {
|
update({ id, title, promo, group, shown, direct, corrected, start_availability, end_availability }) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.promo = promo;
|
this.promo = promo;
|
||||||
|
this.group = group;
|
||||||
this.shown = shown;
|
this.shown = shown;
|
||||||
|
this.direct = direct;
|
||||||
this.corrected = corrected;
|
this.corrected = corrected;
|
||||||
if (this.start_availability != start_availability) {
|
if (this.start_availability != start_availability) {
|
||||||
this.start_availability = start_availability;
|
this.start_availability = start_availability;
|
||||||
@ -48,7 +50,7 @@ class Survey {
|
|||||||
headers: {'Accept': 'application/json'},
|
headers: {'Accept': 'application/json'},
|
||||||
});
|
});
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
return await res.json();
|
return (await res.json()).map((r) => new Response(r));
|
||||||
} else {
|
} else {
|
||||||
throw new Error((await res.json()).errmsg);
|
throw new Error((await res.json()).errmsg);
|
||||||
}
|
}
|
||||||
@ -110,7 +112,6 @@ class Survey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete() {
|
async delete() {
|
||||||
console.log("delete", this.id)
|
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
const res = await fetch(`api/surveys/${this.id}`, {
|
const res = await fetch(`api/surveys/${this.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
@ -25,6 +25,14 @@ export async function getUser(uid) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
export async function getUserGrade(uid, survey) {
|
||||||
const res = await fetch(`api/users/${uid}/surveys/${survey.id}/grades`, {headers: {'Accept': 'application/json'}})
|
const res = await fetch(`api/users/${uid}/surveys/${survey.id}/grades`, {headers: {'Accept': 'application/json'}})
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
@ -42,6 +42,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Toaster from '../components/Toaster.svelte';
|
||||||
|
|
||||||
export let rroute = '';
|
export let rroute = '';
|
||||||
|
|
||||||
function switchAdminMode() {
|
function switchAdminMode() {
|
||||||
@ -59,13 +61,17 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>SRS: MCQ and others courses related stuff</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-sm navbar-dark bg-primary">
|
<nav class="navbar navbar-expand-sm navbar-dark bg-primary">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href=".">
|
<a class="navbar-brand" href=".">
|
||||||
SRS
|
SRS
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#adminMenu" aria-controls="adminMenu" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#loggedMenu" aria-controls="loggedMenu" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="loggedMenu">
|
<div class="collapse navbar-collapse" id="loggedMenu">
|
||||||
@ -104,7 +110,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">
|
<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">
|
<img class="rounded-circle" src="//photos.cri.epita.fr/square/{$user.login}" alt="Menu" style="margin: -0.75em 0; max-height: 2.5em; border: 2px solid white;">
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<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 === 'help'} href="help">Besoin d'aide ?</a></li>
|
||||||
@ -136,3 +142,5 @@
|
|||||||
<div class="container mt-3">
|
<div class="container mt-3">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Toaster />
|
@ -12,10 +12,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/stores'
|
||||||
|
|
||||||
let auth = { username: "", password: "" };
|
let auth = { username: "", password: "" };
|
||||||
let pleaseWait = false;
|
let pleaseWait = false;
|
||||||
|
|
||||||
|
export let next = $page.url.searchParams.get('next');
|
||||||
|
|
||||||
function logmein() {
|
function logmein() {
|
||||||
pleaseWait = true;
|
pleaseWait = true;
|
||||||
fetch('api/auth', {
|
fetch('api/auth', {
|
||||||
@ -26,14 +29,18 @@
|
|||||||
response.json().then((auth) => {
|
response.json().then((auth) => {
|
||||||
pleaseWait = false;
|
pleaseWait = false;
|
||||||
$session = auth;
|
$session = auth;
|
||||||
goto(".");
|
if (next && next.indexOf('//') === -1) {
|
||||||
|
goto(next)
|
||||||
|
} else {
|
||||||
|
goto(".");
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((response) => {
|
||||||
pleaseWait = false;
|
pleaseWait = false;
|
||||||
if (response.data)
|
if (response.data)
|
||||||
addToast({
|
ToastsStore.addToast({
|
||||||
variant: "danger",
|
color: "danger",
|
||||||
title: "Connexion impossible",
|
title: "Connexion impossible",
|
||||||
msg: (response.data ? response.data.errmsg : "Impossible de contacter le serveur"),
|
msg: (response.data ? response.data.errmsg : "Impossible de contacter le serveur"),
|
||||||
});
|
});
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<ul>
|
<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 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 maîtriser les outils que vous utilisez :</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>
|
<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>
|
</ul>
|
||||||
|
|
63
ui/src/routes/help.svelte
Normal file
63
ui/src/routes/help.svelte
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<script>
|
||||||
|
import { user } from '../stores/user';
|
||||||
|
import { ToastsStore } from '../stores/toasts';
|
||||||
|
|
||||||
|
function needhelp() {
|
||||||
|
fetch('api/help', {
|
||||||
|
method: 'POST',
|
||||||
|
}).then((response) => {
|
||||||
|
response.json().then((data) => {
|
||||||
|
ToastsStore.addToast({
|
||||||
|
color: "info",
|
||||||
|
title: "Assistance",
|
||||||
|
msg: data.comment,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}, (response) => {
|
||||||
|
pleaseWait = false;
|
||||||
|
if (response.data)
|
||||||
|
ToastsStore.addToast({
|
||||||
|
color: "danger",
|
||||||
|
title: "Une erreur s'est produite",
|
||||||
|
msg: (response.data ? response.data.errmsg : "Impossible de contacter le serveur"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2>Besoin d'aide ?</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é 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é·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{#if $user} ;
|
||||||
|
je suis aussi joignable <a href="mailto:nemunaire@nemunai.re" class="umami--click--need-help-mail">par e-mail</a> ou bien <a href="https://matrix.to/#/@nemunaire:nemunai.re" class="umami--click--need-help-matrix">sur Matrix</a> ou Teams{/if}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if $user}
|
||||||
|
<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 :
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-primary umami--click--need-help"
|
||||||
|
on:click={needhelp}
|
||||||
|
>
|
||||||
|
Clique ce bouton
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mb-5"></div>
|
53
ui/src/routes/index.svelte
Normal file
53
ui/src/routes/index.svelte
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<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} !
|
||||||
|
</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} ?</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 !</strong> Il y a actuellement un questionnaire en direct : {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 :</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 :</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>
|
||||||
|
|
||||||
|
<div class="mb-5"></div>
|
39
ui/src/routes/surveys/[sid]/__layout.svelte
Normal file
39
ui/src/routes/surveys/[sid]/__layout.svelte
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<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 …</span>
|
||||||
|
</div>
|
||||||
|
{:then}
|
||||||
|
<slot></slot>
|
||||||
|
{: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}
|
516
ui/src/routes/surveys/[sid]/admin.svelte
Normal file
516
ui/src/routes/surveys/[sid]/admin.svelte
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
<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 { getSurvey } from '../../../lib/surveys';
|
||||||
|
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 updateSurvey() {
|
||||||
|
surveyP = getSurvey(survey.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateQuestions() {
|
||||||
|
req_questions = getQuestions(survey.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ws = null;
|
||||||
|
let ws_up = false;
|
||||||
|
let wsstats = null;
|
||||||
|
let current_question = null;
|
||||||
|
let responses = {};
|
||||||
|
let timer = 20000;
|
||||||
|
let timer_end = null;
|
||||||
|
let timer_remain = 0;
|
||||||
|
let timer_cancel = null;
|
||||||
|
|
||||||
|
function updTimer() {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (now > timer_end) {
|
||||||
|
timer_remain = 0;
|
||||||
|
clearInterval(timer_cancel);
|
||||||
|
timer_cancel = null;
|
||||||
|
} else {
|
||||||
|
timer_remain = Math.floor((timer_end - now) / 100)/10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let users = {};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let asks = [];
|
||||||
|
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.send('{"action":"get_asks"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("close", (e) => {
|
||||||
|
ws_up = false;
|
||||||
|
console.log('Socket is closed. Reconnect will be attempted in 1 second.', e.reason);
|
||||||
|
ws = null;
|
||||||
|
updateSurvey();
|
||||||
|
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;
|
||||||
|
if (timer_cancel) {
|
||||||
|
clearInterval(timer_cancel);
|
||||||
|
timer_cancel = null;
|
||||||
|
}
|
||||||
|
if (data.timer) {
|
||||||
|
timer_end = new Date().getTime() + data.timer;
|
||||||
|
timer_cancel = setInterval(updTimer, 250);
|
||||||
|
} else {
|
||||||
|
timer_end = null;
|
||||||
|
}
|
||||||
|
} else if (data.action && data.action == "stats") {
|
||||||
|
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 if (data.action && data.action == "new_ask") {
|
||||||
|
asks.push({"id": data.question, "content": data.value, "userid": data.user});
|
||||||
|
asks = asks;
|
||||||
|
} else {
|
||||||
|
current_question = null;
|
||||||
|
timer_end = null;
|
||||||
|
if (timer_cancel) {
|
||||||
|
clearInterval(timer_cancel);
|
||||||
|
timer_cancel = 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"
|
||||||
|
title="Terminer le direct"
|
||||||
|
on:click={() => { if (confirm("Sûr ?")) ws.send('{"action":"end"}') }}
|
||||||
|
>
|
||||||
|
<i class="bi bi-align-end"></i>
|
||||||
|
</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"><</a>
|
||||||
|
{survey.title}
|
||||||
|
<small class="text-muted">
|
||||||
|
Administration
|
||||||
|
</small>
|
||||||
|
{#if asks.length}
|
||||||
|
<a href="surveys/{sid}/admin#questions_part">
|
||||||
|
<i class="bi bi-patch-question-fill text-danger"></i>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</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}
|
||||||
|
on:saved={updateSurvey}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
{#await req_questions}
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||||
|
<span>Chargement des questions …</span>
|
||||||
|
</div>
|
||||||
|
{:then questions}
|
||||||
|
<div class="card my-3">
|
||||||
|
<table class="table table-hover table-striped mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
Question
|
||||||
|
{#if timer_end}
|
||||||
|
<div class="input-group input-group-sm float-end" style="max-width: 150px;">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
disabled
|
||||||
|
value={timer_remain}
|
||||||
|
>
|
||||||
|
<span class="input-group-text">ms</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="input-group input-group-sm float-end" style="max-width: 150px;">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={timer}
|
||||||
|
placeholder="Valeur du timer"
|
||||||
|
>
|
||||||
|
<span class="input-group-text">ms</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
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", "timer": 0, "question":' + question.id + '}')} }
|
||||||
|
>
|
||||||
|
<i class="bi bi-play-fill"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-danger"
|
||||||
|
disabled={question.id === current_question || !ws_up}
|
||||||
|
on:click={() => { ws.send('{"action":"new_question", "timer": ' + timer + ',"question":' + question.id + '}')} }
|
||||||
|
>
|
||||||
|
<i class="bi bi-stopwatch-fill"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-info ms-1 float-end"
|
||||||
|
on:click={() => { ws.send('{"action":"get_asks", "value": ""}'); asks = []; }}
|
||||||
|
title="Rafraîchir les réponses"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i>
|
||||||
|
<i class="bi bi-question-diamond"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-light ms-1 float-end"
|
||||||
|
on:click={() => { ws.send('{"action":"get_asks", "value": "unanswered"}'); asks = []; }}
|
||||||
|
title="Rafraîchir les réponses, en rapportant les réponses déjà répondues"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i>
|
||||||
|
<i class="bi bi-question-diamond"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-success float-end"
|
||||||
|
title="Tout marqué comme répondu"
|
||||||
|
on:click={() => { ws.send('{"action":"mark_answered", "value": "all"}'); asks = [] }}
|
||||||
|
>
|
||||||
|
<i class="bi bi-check-all"></i>
|
||||||
|
</button>
|
||||||
|
<h3 id="questions_part">
|
||||||
|
Questions
|
||||||
|
{#if asks.length}
|
||||||
|
<small class="text-muted">
|
||||||
|
{asks.length} question{#if asks.length > 1}s{/if}
|
||||||
|
</small>
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
{#if asks.length}
|
||||||
|
{#each asks as ask (ask.id)}
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="card-text">
|
||||||
|
{ask.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-success float-end"
|
||||||
|
title="Marqué comme répondu"
|
||||||
|
on:click={() => { ws.send('{"action":"mark_answered", "question": ' + ask.id + '}'); asks = asks.filter((e) => e.id != ask.id) }}
|
||||||
|
>
|
||||||
|
<i class="bi bi-check"></i>
|
||||||
|
</button>
|
||||||
|
Par
|
||||||
|
<a href="users/{ask.userid}" target="_blank">
|
||||||
|
{#if users && users[ask.userid]}
|
||||||
|
{users[ask.userid].login}
|
||||||
|
{:else}
|
||||||
|
{ask.userid}
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
Pas de question pour l'instant.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-info ms-1 float-end"
|
||||||
|
on:click={() => { ws.send('{"action":"get_responses"}') }}
|
||||||
|
title="Rafraîchir les réponses"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i>
|
||||||
|
<i class="bi bi-card-checklist"></i>
|
||||||
|
</button>
|
||||||
|
<h3>
|
||||||
|
Réponses
|
||||||
|
</h3>
|
||||||
|
{#if Object.keys(responses).length}
|
||||||
|
{#each Object.keys(responses) as q, qid (qid)}
|
||||||
|
{#await req_questions then questions}
|
||||||
|
{#each questions as question}
|
||||||
|
{#if question.id == q}
|
||||||
|
<h4 id="q{question.id}_res">
|
||||||
|
{question.title}
|
||||||
|
</h4>
|
||||||
|
{#if question.kind == 'ucq'}
|
||||||
|
{#await question.getProposals()}
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||||
|
<span>Chargement des propositions …</span>
|
||||||
|
</div>
|
||||||
|
{:then proposals}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<table class="table table-sm table-striped table-hover mb-0">
|
||||||
|
<tbody>
|
||||||
|
{#each proposals as proposal (proposal.id)}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{proposal.label}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{responsesbyid[q].filter((e) => e == proposal.id.toString()).length}/{responsesbyid[q].length}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{Math.trunc(responsesbyid[q].filter((e) => e == proposal.id.toString()).length / responsesbyid[q].length * 1000)/10} %
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{:else if question.kind == 'mcq'}
|
||||||
|
{#await question.getProposals()}
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary mx-3" role="status"></div>
|
||||||
|
<span>Chargement des propositions …</span>
|
||||||
|
</div>
|
||||||
|
{:then proposals}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<table class="table table-sm table-striped table-hover mb-0">
|
||||||
|
<tbody>
|
||||||
|
{#each proposals as proposal (proposal.id)}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{proposal.label}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length}/{responsesbyid[q].length}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{Math.trunc(responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length / responsesbyid[q].length * 1000)/10} %
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
<div class="card mb-4">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{#each Object.keys(responses[q]) as user, rid (rid)}
|
||||||
|
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||||
|
<span>
|
||||||
|
{responses[q][user]}
|
||||||
|
</span>
|
||||||
|
<a href="users/{user}" target="_blank" class="badge bg-dark rounded-pill">
|
||||||
|
{#if users && users[user]}
|
||||||
|
{users[user].login}
|
||||||
|
{:else}
|
||||||
|
{user}
|
||||||
|
{/if}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-info ms-1 float-end"
|
||||||
|
on:click={() => { ws.send('{"action":"get_stats"}') }}
|
||||||
|
title="Rafraîchir les stats"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i>
|
||||||
|
<i class="bi bi-123"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-primary ms-1 float-end"
|
||||||
|
title="Rafraîchir la liste des utilisateurs"
|
||||||
|
on:click={updateUsers}
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-clockwise"></i>
|
||||||
|
<i class="bi bi-people"></i>
|
||||||
|
</button>
|
||||||
|
<h3>
|
||||||
|
Connectés
|
||||||
|
{#if wsstats}
|
||||||
|
<small class="text-muted">{wsstats.nb_clients} utilisateurs</small>
|
||||||
|
{/if}
|
||||||
|
</h3>
|
||||||
|
{#if wsstats}
|
||||||
|
<div class="row row-cols-5 py-3">
|
||||||
|
{#each wsstats.users as login, lid (lid)}
|
||||||
|
<div class="col">
|
||||||
|
<div class="card">
|
||||||
|
<img alt="{login}" src="//photos.cri.epita.fr/thumb/{login}" class="card-img-top">
|
||||||
|
<div class="card-footer text-center text-truncate p-0">
|
||||||
|
<a href="users/{login}" target="_blank">
|
||||||
|
{login}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{/await}
|
76
ui/src/routes/surveys/[sid]/index.svelte
Normal file
76
ui/src/routes/surveys/[sid]/index.svelte
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<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"><</a>
|
||||||
|
{survey.title}
|
||||||
|
</h2>
|
||||||
|
<SurveyBadge class="ms-2" {survey} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $user && $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} />
|
||||||
|
{:catch error}
|
||||||
|
<div class="row mt-5">
|
||||||
|
<div class="d-none d-sm-block col-sm">
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<h3 class="col-sm-auto text-center text-muted mb-3"><label for="askquestion">Ce questionnaire n'est pas accessible</label></h3>
|
||||||
|
<div class="d-none d-sm-block col-sm">
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{/await}
|
263
ui/src/routes/surveys/[sid]/live.svelte
Normal file
263
ui/src/routes/surveys/[sid]/live.svelte
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
<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;
|
||||||
|
let timer_init = null;
|
||||||
|
let timer_end = null;
|
||||||
|
let timer = 0;
|
||||||
|
let timer_cancel = null;
|
||||||
|
|
||||||
|
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 updTimer() {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (now > timer_end) {
|
||||||
|
timer = 100;
|
||||||
|
clearInterval(timer_cancel);
|
||||||
|
timer_cancel = null;
|
||||||
|
} else {
|
||||||
|
const dist1 = timer_end - timer_init;
|
||||||
|
const dist2 = timer_end - now;
|
||||||
|
timer = Math.ceil(100-dist2*100/dist1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wsconnect() {
|
||||||
|
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;
|
||||||
|
if (timer_cancel) {
|
||||||
|
clearInterval(timer_cancel);
|
||||||
|
timer_cancel = null;
|
||||||
|
}
|
||||||
|
if (data.timer) {
|
||||||
|
timer_init = new Date().getTime();;
|
||||||
|
timer_end = timer_init + data.timer;
|
||||||
|
updTimer();
|
||||||
|
timer_cancel = setInterval(updTimer, 150);
|
||||||
|
} else {
|
||||||
|
timer_init = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
show_question = null;
|
||||||
|
if (timer_cancel) {
|
||||||
|
clearInterval(timer_cancel);
|
||||||
|
timer_cancel = null;
|
||||||
|
}
|
||||||
|
timer_init = 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.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let myQuestion = "";
|
||||||
|
let submitQuestionInProgress = false;
|
||||||
|
function askQuestion() {
|
||||||
|
if (!myQuestion) {
|
||||||
|
ToastsStore.addErrorToast({
|
||||||
|
msg: "Quel est ta question ?",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitQuestionInProgress = true;
|
||||||
|
fetch(`api/surveys/${survey.id}/ask`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Accept': 'application/json'},
|
||||||
|
body: JSON.stringify({"content": myQuestion}),
|
||||||
|
}).then((r) => {
|
||||||
|
submitQuestionInProgress = false;
|
||||||
|
myQuestion = "";
|
||||||
|
ToastsStore.addToast({
|
||||||
|
msg: "Ta question a bien été envoyée.",
|
||||||
|
title: survey.title,
|
||||||
|
color: "success",
|
||||||
|
});
|
||||||
|
}, (error) => {
|
||||||
|
ToastsStore.addErrorToast({
|
||||||
|
msg: "Un problème est survenu : " + error.errmsg,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</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-3 mb-md-4 mb-lg-5">
|
||||||
|
<h2>
|
||||||
|
<a href="surveys/" class="text-muted" style="text-decoration: none"><</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 …</span>
|
||||||
|
</div>
|
||||||
|
{:then question}
|
||||||
|
<QuestionForm
|
||||||
|
{question}
|
||||||
|
readonly={timer >= 100}
|
||||||
|
bind:value={value}
|
||||||
|
on:change={sendValue}
|
||||||
|
>
|
||||||
|
{#if timer_init}
|
||||||
|
<div class="progress" style="border-radius: 0; height: 4px">
|
||||||
|
<div class="progress-bar" class:bg-warning={timer > 85 && timer < 100} class:bg-danger={timer >= 100} role="progressbar" style="width: {timer}%"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</QuestionForm>
|
||||||
|
{#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 mb-4">
|
||||||
|
Pas de question actuellement.
|
||||||
|
</h2>
|
||||||
|
<form on:submit|preventDefault={askQuestion}>
|
||||||
|
<div class="row">
|
||||||
|
<div class="d-none d-sm-block col-sm">
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<h3 class="col-sm-auto text-center text-muted mb-3"><label for="askquestion">Vous avez une question ?</label></h3>
|
||||||
|
<div class="d-none d-sm-block col-sm">
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="offset-md-1 col-md-10 offset-lg-2 col-lg-8 offset-xl-3 col-xl-6 mb-4">
|
||||||
|
<div class="input-group">
|
||||||
|
<textarea
|
||||||
|
id="askquestion"
|
||||||
|
class="form-control"
|
||||||
|
bind:value={myQuestion}
|
||||||
|
autofocus
|
||||||
|
placeholder="Remarques, soucis, choses pas claires ? Demandez !"
|
||||||
|
></textarea>
|
||||||
|
<button
|
||||||
|
class="d-sm-none btn btn-primary"
|
||||||
|
disabled={!myQuestion || submitQuestionInProgress}
|
||||||
|
>
|
||||||
|
{#if submitQuestionInProgress}
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
|
||||||
|
{/if}
|
||||||
|
Poser cette question
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if myQuestion}
|
||||||
|
<div class="d-none d-sm-block text-center mb-4">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
disabled={submitQuestionInProgress}
|
||||||
|
>
|
||||||
|
{#if submitQuestionInProgress}
|
||||||
|
<div class="spinner-border spinner-border-sm me-1" role="status"></div>
|
||||||
|
{/if}
|
||||||
|
Poser cette question
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<h2 class="text-center">
|
||||||
|
La session est terminée. <small class="text-muted">On se retrouve une prochaine fois…</small>
|
||||||
|
</h2>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
{/await}
|
156
ui/src/routes/surveys/[sid]/responses/[rid].svelte
Normal file
156
ui/src/routes/surveys/[sid]/responses/[rid].svelte
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<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…</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…</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"><</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>
|
85
ui/src/routes/surveys/[sid]/responses/index.svelte
Normal file
85
ui/src/routes/surveys/[sid]/responses/index.svelte
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<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"><</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 …</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 …</span>
|
||||||
|
</td>
|
||||||
|
{:then responses}
|
||||||
|
<td>
|
||||||
|
{#if responses}
|
||||||
|
{responses.filter((r) => !r.time_scored || (r.time_reported && r.time_reported >= 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}
|
||||||
|
-- %
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
{/await}
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">Moyenne</th>
|
||||||
|
<th><!--{mean}--> %</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{/await}
|
@ -6,3 +6,5 @@
|
|||||||
<div class="card bg-light">
|
<div class="card bg-light">
|
||||||
<SurveyList />
|
<SurveyList />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5"></div>
|
22
ui/src/routes/surveys/new.svelte
Normal file
22
ui/src/routes/surveys/new.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script>
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
import { user } from '../../stores/user';
|
||||||
|
import SurveyAdmin from '../../components/SurveyAdmin.svelte';
|
||||||
|
import SurveyBadge from '../../components/SurveyBadge.svelte';
|
||||||
|
import { Survey } from '../../lib/surveys';
|
||||||
|
|
||||||
|
let survey = new Survey();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<h2>
|
||||||
|
<a href="surveys/" class="text-muted" style="text-decoration: none"><</a>
|
||||||
|
Nouveau questionnaire
|
||||||
|
</h2>
|
||||||
|
<SurveyBadge class="ms-2" {survey} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $user && $user.is_admin}
|
||||||
|
<SurveyAdmin {survey} on:saved={(e) => { goto(`surveys/${e.detail.id}`)}} />
|
||||||
|
{/if}
|
41
ui/src/stores/toasts.js
Normal file
41
ui/src/stores/toasts.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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();
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@ -1,3 +1,4 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-static';
|
||||||
import preprocess from 'svelte-preprocess';
|
import preprocess from 'svelte-preprocess';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
@ -7,6 +8,9 @@ const config = {
|
|||||||
preprocess: preprocess(),
|
preprocess: preprocess(),
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
fallback: 'index.html'
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Reference in New Issue
Block a user