qa: Back to the same situation

This commit is contained in:
nemunaire 2022-11-07 01:00:04 +01:00
parent 00f84e43ca
commit 1aa82bb2ef
27 changed files with 1336 additions and 22 deletions

View file

@ -17,6 +17,8 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
exercicesRoutes := router.Group("/exercices/:eid") exercicesRoutes := router.Group("/exercices/:eid")
exercicesRoutes.Use(exerciceHandler) exercicesRoutes.Use(exerciceHandler)
exercicesRoutes.GET("", showExercice) exercicesRoutes.GET("", showExercice)
declareQARoutes(exercicesRoutes)
} }
func exerciceHandler(c *gin.Context) { func exerciceHandler(c *gin.Context) {
@ -42,8 +44,15 @@ func exerciceHandler(c *gin.Context) {
} }
func listExercices(c *gin.Context) { func listExercices(c *gin.Context) {
// List all exercices var exercices []*fic.Exercice
exercices, err := fic.GetExercices() var err error
if theme, ok := c.Get("theme"); ok {
exercices, err = theme.(*fic.Theme).GetExercices()
} else {
// List all exercices
exercices, err = fic.GetExercices()
}
if err != nil { if err != nil {
log.Println("Unable to GetExercices: ", err.Error()) log.Println("Unable to GetExercices: ", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list exercices: %s", err.Error())}) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list exercices: %s", err.Error())})

View file

@ -12,8 +12,7 @@ import (
) )
func declareQARoutes(router *gin.RouterGroup) { func declareQARoutes(router *gin.RouterGroup) {
exercicesRoutes := router.Group("/qa/:eid") exercicesRoutes := router.Group("/qa")
exercicesRoutes.Use(exerciceHandler)
exercicesRoutes.GET("", getExerciceQA) exercicesRoutes.GET("", getExerciceQA)
exercicesRoutes.POST("", createExerciceQA) exercicesRoutes.POST("", createExerciceQA)
@ -30,12 +29,21 @@ func declareQARoutes(router *gin.RouterGroup) {
} }
func qaHandler(c *gin.Context) { func qaHandler(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
var qa *fic.QAQuery var qa *fic.QAQuery
if qid, err := strconv.ParseInt(string(c.Param("qid")), 10, 64); err != nil {
qid, err := strconv.ParseInt(string(c.Param("qid")), 10, 64)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad QA identifier."}) c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad QA identifier."})
return return
} else if qa, err = exercice.GetQAQuery(qid); err != nil { }
if exercice, ok := c.Get("exercice"); ok {
qa, err = exercice.(*fic.Exercice).GetQAQuery(qid)
} else {
qa, err = fic.GetQAQuery(qid)
}
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "QA entry not found."}) c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "QA entry not found."})
return return
} }

View file

@ -6,5 +6,5 @@
"$lib/*": ["src/lib/*"] "$lib/*": ["src/lib/*"]
} }
}, },
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
} }

View file

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

View file

@ -0,0 +1,54 @@
<script>
import { goto } from '$app/navigation';
import {
Button,
Container,
Icon,
Table,
} from 'sveltestrap';
import QAItems from '$lib/components/QAItems.svelte';
import QANewItem from '$lib/components/QANewItem.svelte';
import { themes, themesIdx } from '$lib/stores/themes';
if ($themes.length == 0) {
themes.refresh();
}
export let exercice = {};
let countCreation = 0;
</script>
<h2>
{exercice.title}
{#if $themes.length && $themesIdx[exercice.id_theme]}
<small>
<a href="themes/{exercice.id_theme}" title={$themesIdx[exercice.id_theme].authors}>{$themesIdx[exercice.id_theme].name}</a>
</small>
{#if $themesIdx[exercice.id_theme].exercices && $themesIdx[exercice.id_theme].exercices[exercice.id]}
<div class="btn-group" role="group">
<a href="exercices/{$themesIdx[exercice.id_theme].exercices[exercice.id].previous}" title="Exercice précédent" class:disabled={!$themesIdx[exercice.id_theme].exercices[exercice.id].previous} class="btn btn-sm btn-light"><Icon name="chevron-left" /></a>
<a href="exercices/{$themesIdx[exercice.id_theme].exercices[exercice.id].next}" title="Exercice suivant" class:disabled={!$themesIdx[exercice.id_theme].exercices[exercice.id].next} class="btn btn-sm btn-light"><Icon name="chevron-right" /></a>
</div>
{/if}
<a href="../{$themesIdx[exercice.id_theme].urlid}/{exercice.urlid}" target="_self" class="float-right ml-2 btn btn-sm btn-info"><Icon name="play-fill" /> Site du challenge</a>
{/if}
</h2>
<div class="row mb-3">
<div class="col-md-6">{@html exercice.statement}</div>
<div class="col-md-6">{@html exercice.overview}</div>
</div>
<div class="mb-5">
<QANewItem
{exercice}
on:new-query={() => countCreation++}
/>
{#key countCreation}
<QAItems
{exercice}
/>
{/key}
</div>

View file

@ -1,4 +1,6 @@
<script> <script>
import { page } from '$app/stores'
import { import {
Badge, Badge,
Button, Button,
@ -20,7 +22,15 @@
Row, Row,
} from 'sveltestrap'; } from 'sveltestrap';
const version = fetch('api/version', {headers: {'Accept': 'application/json'}}).then((res) => res.json()) import { auth, version } from '$lib/stores/auth';
export let activemenu = "";
$: {
const path = $page.url.pathname.split("/");
if (path.length > 1) {
activemenu = path[1];
}
}
</script> </script>
<Navbar color="dark" dark expand="md"> <Navbar color="dark" dark expand="md">
@ -30,25 +40,46 @@
</NavbarBrand> </NavbarBrand>
<Nav navbar> <Nav navbar>
<NavItem> <NavItem>
<NavLink href="."> <NavLink
href="."
active={activemenu === ''}
>
<Icon name="house-door" /> <Icon name="house-door" />
Accueil Accueil
</NavLink> </NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink href="themes"> <NavLink
href="themes"
active={activemenu === 'themes'}
>
<Icon name="box-seam" /> <Icon name="box-seam" />
Scénarios Scénarios
</NavLink> </NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink href="teams"> <NavLink
href="exercices"
active={activemenu === 'exercices'}
>
<Icon name="bar-chart-steps" />
Étapes
</NavLink>
</NavItem>
<NavItem>
<NavLink
href="teams"
active={activemenu === 'teams'}
>
<Icon name="people" /> <Icon name="people" />
Équipes Équipes
</NavLink> </NavLink>
</NavItem> </NavItem>
<NavItem> <NavItem>
<NavLink href="repositories"> <NavLink
href="repositories"
active={activemenu === 'repositories'}
>
<Icon name="archive" /> <Icon name="archive" />
Dépôts Dépôts
</NavLink> </NavLink>
@ -56,12 +87,8 @@
</Nav> </Nav>
<Nav class="ms-auto text-light" navbar> <Nav class="ms-auto text-light" navbar>
<NavItem class="ms-2"> <NavItem class="ms-2">
{#await version} v{$version.version}
veuillez patienter {#if $auth}&ndash; Logged as {$auth.name} (team #{$auth.id_team}){/if}
{:then v}
v{v.version}
{#if v.auth}&ndash; Logged as {v.auth.name} (team #{v.auth.id_team}){/if}
{/await}
</NavItem> </NavItem>
</Nav> </Nav>
</Navbar> </Navbar>

View file

@ -0,0 +1,93 @@
<script>
import { goto } from '$app/navigation';
import {
Spinner,
} from 'sveltestrap';
import { getQAView } from '$lib/todo';
import { getExerciceQA } from '$lib/qa';
import { exercicesIdx } from '$lib/stores/exercices';
import { themesIdx } from '$lib/stores/themes';
import { todos } from '$lib/stores/todo';
export { className as class };
let className = '';
function show(id) {
goto("exercices/" + id)
}
let my_exercices = [];
let my_exercicesP = update_exercices()
async function update_exercices() {
const view = await getQAView();
my_exercices = [];
for (const v of view) {
v.queries = null;
v.queriesNSolved = 0;
v.queriesNClosed = 0;
getExerciceQA(v.id_exercice).then((queries) => {
v.queries = queries;
for (const q of queries) {
if (q.solved == null) v.queriesNSolved++;
if (q.closed == null) v.queriesNClosed++;
}
my_exercices.push(v);
my_exercices = my_exercices;
});
}
}
</script>
<div class={className}>
<h3>Vos étapes</h3>
{#await my_exercicesP}
{:then}
<table class="table table-stripped table-hover">
<thead>
<tr>
<th>Défi</th>
<th>Requêtes</th>
</tr>
</thead>
<tbody>
{#each my_exercices as todo (todo.id)}
<tr
class:table-success={todo.queries && todo.queries.length > 0}
class:table-warning={todo.queriesNSolved > 0}
on:click={() => show(todo.id_exercice)}
>
<td>
{#if $exercicesIdx.length == 0 && $themesIdx.length == 0}
<Spinner size="sm" />
{:else if $themesIdx[$exercicesIdx[todo.id_exercice]]}
<a href="themes/{$exercicesIdx[todo.id_exercice].id_theme}">
{$themesIdx[$exercicesIdx[todo.id_exercice].id_theme].name}
</a>
&ndash;
{/if}
{#if $exercicesIdx.length == 0}
<Spinner size="sm" />
{:else if $exercicesIdx[todo.id_exercice]}
{$exercicesIdx[todo.id_exercice].title}
{#if $exercicesIdx[todo.id_exercice].wip}
<Icon name="cone-striped" />
{/if}
{/if}
</td>
<td>
{#if todo.queries && todo.queries.length}
{todo.queriesNSolved} / {todo.queriesNClosed}
{:else}
0
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
{/await}
</div>

View file

@ -0,0 +1,91 @@
<script>
import { goto } from '$app/navigation';
import {
Spinner,
} from 'sveltestrap';
import { getExerciceTested, getQAWork } from '$lib/todo'
import { exercicesIdx } from '$lib/stores/exercices'
import { themesIdx } from '$lib/stores/themes'
import { todos } from '$lib/stores/todo'
export { className as class };
let className = '';
let exo_doneP = getExerciceTested();
let tododone = { };
getQAWork().then((queries) => {
for (const q of queries) {
tododone[q.id_exercice] = q;
}
})
function show(id) {
goto("exercices/" + id)
}
</script>
<div class={className}>
<h3>Étapes à tester et valider</h3>
{#await todos.refresh()}
{:then}
{#await exo_doneP}
{:then exo_done}
<table class="table table-stripped table-hover">
<thead>
<tr>
<th>Avancement</th>
<th>Scénario</th>
<th>Défi</th>
</tr>
</thead>
<tbody>
{#each $todos as todo (todo.id)}
<tr
style:cursor="pointer"
class:text-light={!tododone[todo.id_exercice] && !exo_done[todo.id_exercice]}
class:table-dark={!tododone[todo.id_exercice] && !exo_done[todo.id_exercice]}
class:table-warning={!tododone[todo.id_exercice] && exo_done[todo.id_exercice] == 'access'}
class:table-info={!tododone[todo.id_exercice] && (exo_done[todo.id_exercice] == 'tried' || exo_done[todo.id_exercice] == 'solved')}
class:table-success={tododone[todo.id_exercice]} on:click={() => show(todo.id_exercice)}
>
<td>
{#if tododone[todo.id_exercice]}
Commenté
{#if !exo_done[todo.id_exercice] || exo_done[todo.id_exercice] != 'solved'}
mais pas testé/terminé
{/if}
{:else if exo_done[todo.id_exercice] && exo_done[todo.id_exercice] != 'access'}
À commenter
{:else}
À tester
{/if}
</td>
<td>
{#if $exercicesIdx.length == 0 && $themesIdx.length == 0}
<Spinner size="sm" />
{:else}
<a href="themes/{$exercicesIdx[todo.id_exercice].id_theme}">
{$themesIdx[$exercicesIdx[todo.id_exercice].id_theme].name}
</a>
{/if}
</td>
<td>
{#if $exercicesIdx.length == 0}
<Spinner size="sm" />
{:else}
{$exercicesIdx[todo.id_exercice].title}
{#if $exercicesIdx[todo.id_exercice].wip}
<Icon name="cone-striped" />
{/if}
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
{/await}
{/await}
</div>

View file

@ -0,0 +1,183 @@
<script>
import { createEventDispatcher } from 'svelte';
import {
Button,
Card,
CardBody,
CardHeader,
Icon,
Spinner,
} from 'sveltestrap';
import DateFormat from '$lib/components/DateFormat.svelte';
import { getQAComments, QAComment } from '$lib/qa.js';
import { auth } from '$lib/stores/auth';
const dispatch = createEventDispatcher();
export let query_selected;
let query_commentsP;
$: query_commentsP = getQAComments(query_selected.id);
let newComment = new QAComment();
let submissionInProgress = false;
function addComment() {
submissionInProgress = true;
newComment.save(query_selected.id).then(() => {
query_commentsP = getQAComments(query_selected.id);
newComment = new QAComment();
submissionInProgress = false;
})
}
function updateQA() {
query_selected.save().then(() => {
dispatch("update-queries");
})
}
function solveQA() {
query_selected.solved = new Date();
query_selected.save().then(() => {
dispatch("update-queries");
})
}
function reopenQA() {
query_selected.solved = null;
query_selected.save().then(() => {
dispatch("update-queries");
})
}
function closeQA() {
query_selected.closed = new Date();
query_selected.save().then(() => {
dispatch("update-queries");
})
}
function deleteQA() {
query_selected.delete().then(() => {
query_selected = null;
dispatch("update-queries");
})
}
</script>
<Card>
<CardHeader>
<div class="d-flex justify-content-between">
<h4 class="card-title mb-0">{query_selected.subject}</h4>
<div>
{#if $auth && $auth.id_team == query_selected.id_team}
{#if query_selected.solved && !query_selected.closed}
<Button on:click={closeQA} color="success">
<Icon name="check" />
Valider la résolution
</Button>
<Button on:click={reopenQA} color="danger">
<Icon name="x" />
Réouvrir
</Button>
{/if}
{#if !query_selected.solved}
<Button on:click={deleteQA} color="danger">
<Icon name="trash-fill" />
Supprimer
</Button>
{/if}
{:else if $auth && !query_selected.solved}
<Button on:click={solveQA} color="success">
<Icon name="check" />
Marquer comme résolu
</Button>
{/if}
</div>
</div>
</CardHeader>
<CardBody>
<div class="row">
<dl class="col row">
<dt class="col-3">Qui ?</dt>
<dd class="col-9">{query_selected.user} (team #{query_selected.id_team})</dd>
<dt class="col-3">État</dt>
<dd class="col-9">{query_selected.state}</dd>
<dt class="col-3">Date de création</dt>
<dd class="col-9">
<DateFormat date={query_selected.creation} dateStyle="long" timeStyle="medium" />
</dd>
<dt class="col-3">Date de résolution</dt>
<dd class="col-9">
{#if query_selected.solved}
<DateFormat date={query_selected.solved} dateStyle="long" timeStyle="medium" />
{:else}
-
{/if}
</dd>
<dt class="col-3">Date de clôture</dt>
<dd class="col-9">
{#if query_selected.closed}
<DateFormat date={query_selected.closed} dateStyle="long" timeStyle="medium" />
{:else}
-
{/if}
</dd>
</dl>
<div class="col-auto">
{#if $auth && $auth.id_team == query_selected.id_team}
<Button on:click={updateQA} color="primary">
<Icon name="upload" />
Mettre à jour
</Button>
{/if}
</div>
</div>
{#await query_commentsP}
<div class="d-flex">
<Spinner />
<div>
Chargement des commentaires en cours&hellip;
</div>
</div>
{:then query_comments}
<table class="table table-striped">
{#each query_comments as comment (comment.id)}
<tr>
<td style="white-space: pre-line">
Le <DateFormat date={comment.date} dateStyle="medium" timeStyle="short" />, <strong>{comment.user}</strong> a écrit&nbsp;: {comment.content}
</td>
</tr>
{/each}
</table>
{/await}
<form on:submit|preventDefault={addComment}>
<label for="newComment">Répondre :</label>
<textarea
class="form-control"
placeholder="Ajouter un commentaire"
rows="2"
id="newComment"
bind:value={newComment.content}
></textarea>
<Button
type="submit"
color="primary"
class="mt-1 float-right"
disabled={!newComment.content || newComment.content.length == 0 || submissionInProgress}
>
{#if submissionInProgress}
<Spinner size="sm" />
{/if}
Ajouter le commentaire
</Button>
</form>
</CardBody>
</Card>

View file

@ -0,0 +1,95 @@
<script>
import {
Button,
Icon,
Spinner,
} from 'sveltestrap';
import { getExerciceQA, QAComment } from '$lib/qa.js';
import QAItem from '$lib/components/QAItem.svelte';
export let exercice = { };
export let query_selected = null;
const fields = [ "state", "subject" ];
let queriesP;
$: queriesP = getExerciceQA(exercice.id);
let thumbInProgress = null;
function thumbUp(qid) {
thumbInProgress = qid;
const thumb = new QAComment({
content: "+1",
});
thumb.save(qid).then(() => {
thumbInProgress = null;
})
}
function updateQueries() {
queriesP = getExerciceQA(exercice.id);
}
</script>
{#await queriesP}
{:then queries}
<table
class="table table-bordered table-striped"
class:table-hover={queries.length}
class:table-sm={queries.length}
>
<thead class="thead-dark">
<tr>
{#each fields as field}
<th>
{ field }
</th>
{/each}
<th></th>
</tr>
</thead>
{#if queries.length}
<tbody>
{#each queries as q (q.id)}
<tr on:click={() => query_selected = q} class:bg-warning={query_selected && q.id == query_selected.id}>
{#each fields as field}
<td>
{@html q[field]}
</td>
{/each}
<td>
<button
type="button"
class="btn btn-sm btn-light"
disabled={thumbInProgress !== null}
on:click|preventDefault={() => thumbUp(q.id)}
>
{#if thumbInProgress == q.id}
<Spinner size="sm" />
{:else}
<Icon name="hand-thumbs-up" />
{/if}
</button>
</td>
</tr>
{/each}
</tbody>
{:else}
<tbody>
<tr>
<td colspan={fields.length+1} class="font-weight-bold text-info text-center">
Aucune requête enregistrée
</td>
</tr>
</tbody>
{/if}
</table>
{#if query_selected}
<QAItem
bind:query_selected={query_selected}
on:update-queries={updateQueries}
/>
{/if}
{/await}

View file

@ -0,0 +1,76 @@
<script>
import { createEventDispatcher } from 'svelte';
import {
Button,
Spinner,
} from 'sveltestrap';
import { QAQuery, QAStates } from '$lib/qa.js';
const dispatch = createEventDispatcher();
export let exercice = {};
let newQuery = new QAQuery();
let creationInProgress = false;
function saveQuery() {
newQuery.id_exercice = exercice.id;
creationInProgress = true;
newQuery.save().then((res) => {
dispatch("new-query");
newQuery = new QAQuery();
creationInProgress = false;
})
}
</script>
<form on:submit|preventDefault={saveQuery} class="card mb-3">
<div class="card-header">
Qu'avez-vous pensé de cette étape&nbsp;?
</div>
<div class="card-body">
<div class="mb-3 row">
<label for="state" class="col-2 col-form-label col-form-label-sm">État</label>
<div class="col-10">
<select class="form-select form-select-sm" id="state" bind:value={newQuery.state}>
{#each Object.keys(QAStates) as state}
<option value={state}>
{QAStates[state]}
</option>
{/each}
</select>
</div>
</div>
<div class="mb-3 row">
<label for="subject" class="col-2 col-form-label col-form-label-sm">Sujet</label>
<div class="col-10">
<input
type="text"
class="form-control form-control-sm"
id="subject"
placeholder="Le pcap ne contient pas l'IP attendue"
bind:value={newQuery.subject}
>
</div>
</div>
<div class="mb-3 row">
<label for="description" class="col-2 col-form-label col-form-label-sm">Description</label>
<div class="col-10">
<textarea class="form-control form-control-sm" placeholder="Ajouter un commentaire" rows="2" id="description" bind:value={newQuery.content}></textarea>
</div>
</div>
<Button
type="submit"
color="primary"
class="float-end"
disabled={creationInProgress}
>
{#if creationInProgress}
<Spinner size="sm" />
{/if}
Soumettre
</Button>
</div>
</form>

View file

@ -0,0 +1,66 @@
export const fieldsExercices = ["title", "headline"];
export class Exercice {
constructor(res) {
if (res) {
this.update(res);
}
}
update({ id, id_theme, title, wip, urlid, path, statement, overview, headline, finished, issue, issuekind, depend, gain, coefficient, videoURI, resolution, seealso }) {
this.id = id;
this.id_theme = id_theme;
this.title = title;
this.wip = wip
this.urlid = urlid;
this.path = path;
this.statement = statement;
this.overview = overview;
this.headline = headline;
this.finished = finished;
this.issue = issue;
this.issuekind = issuekind;
this.depend = depend;
this.gain = gain;
this.coefficient = coefficient;
this.videoURI = videoURI;
this.resolution = resolution;
this.seealso = seealso;
}
}
export async function getExercices() {
const res = await fetch(`api/exercices`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return (await res.json()).map((t) => new Exercice(t));
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getThemedExercices(tid) {
const res = await fetch(`api/themes/${tid}/exercices`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return (await res.json()).map((t) => new Exercice(t));
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getExercice(eid) {
const res = await fetch(`api/exercices/${eid}`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return new Exercice(await res.json());
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getThemedExercice(tid, eid) {
const res = await fetch(`api/themes/${tid}/exercices/${eid}`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return new Exercice(await res.json());
} else {
throw new Error((await res.json()).errmsg);
}
}

127
qa/ui/src/lib/qa.js Normal file
View file

@ -0,0 +1,127 @@
export const QAStates = {
"ok": "OK",
"orthograph": "Orthographe et grammaire",
"issue-statement": "Pas compris",
"issue-flag": "Problème de flag",
"issue-mcq": "Problème de QCM/QCU",
"issue-hint": "Problème d'indice",
"issue-file": "Problème de fichier",
"issue": "Problème autre",
"suggest": "Suggestion",
"too-hard": "Trop dur",
"too-easy": "Trop facile",
};
export class QAQuery {
constructor(res) {
if (res) {
this.update(res);
}
}
update({ id, id_exercice, id_team, user, creation, state, subject, solved, closed }) {
this.id = id;
this.id_team = id_team;
this.id_exercice = id_exercice;
this.user = user;
this.creation = creation;
this.state = state;
this.subject = subject;
this.solved = solved;
this.closed = closed;
}
async delete() {
const res = await fetch(`api/exercices/${this.id_exercice}/qa/${this.id}`, {
method: 'DELETE',
headers: {'Accept': 'application/json'}
});
if (res.status < 300) {
return true;
} else {
throw new Error((await res.json()).errmsg);
}
}
async save() {
const res = await fetch(this.id?`api/exercices/${this.id_exercice}/qa/${this.id}`:`api/exercices/${this.id_exercice}/qa`, {
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);
}
}
}
export async function getExerciceQA(eid) {
const res = await fetch(`api/exercices/${eid}/qa`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
const data = await res.json();
if (data == null)
return [];
return data.map((t) => new QAQuery(t));
} else {
throw new Error((await res.json()).errmsg);
}
}
export class QAComment {
constructor(res) {
if (res) {
this.update(res);
}
}
update({ id, id_team, user, date, content }) {
this.id = id;
this.id_team = id_team;
this.user = user;
this.date = date;
this.content = content;
}
async delete(qid) {
const res = await fetch(`api/qa/${qid}/comments/${this.id}`, {
method: 'DELETE',
headers: {'Accept': 'application/json'}
});
if (res.status == 200) {
return true;
} else {
throw new Error((await res.json()).errmsg);
}
}
async save(qid) {
const res = await fetch(this.id?`api/qa/${qid}/comments/${this.id}`:`api/qa/${qid}/comments`, {
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);
}
}
}
export async function getQAComments(qid) {
const res = await fetch(`api/qa/${qid}/comments`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
const data = await res.json();
if (data == null)
return [];
return data.map((t) => new QAComment(t));
} else {
throw new Error((await res.json()).errmsg);
}
}

View file

@ -0,0 +1,29 @@
import { writable, derived } from 'svelte/store';
function createVersionStore() {
const { subscribe, set, update } = writable({"auth":null});
return {
subscribe,
set: (v) => {
update((m) => Object.assign(m, v));
},
update,
refresh: async () => {
const version = await (await fetch('api/version', {headers: {'Accept': 'application/json'}})).json()
update((m) => version);
return version;
},
};
}
export const version = createVersionStore();
export const auth = derived(
version,
$version => $version.auth,
);

View file

@ -0,0 +1,39 @@
import { writable, derived } from 'svelte/store';
import { getExercices } from '$lib/exercices'
function createExercicesStore() {
const { subscribe, set, update } = writable([]);
return {
subscribe,
set: (v) => {
update((m) => Object.assign(m, v));
},
update,
refresh: async () => {
const list = await getExercices();
update((m) => list);
return list;
},
};
}
export const exercices = createExercicesStore();
export const exercicesIdx = derived(
exercices,
$exercices => {
const exercices_idx = { };
for (const e of $exercices) {
exercices_idx[e.id] = e;
}
return exercices_idx;
},
);

View file

@ -0,0 +1,39 @@
import { writable, derived } from 'svelte/store';
import { getThemes } from '$lib/themes'
function createThemesStore() {
const { subscribe, set, update } = writable([]);
return {
subscribe,
set: (v) => {
update((m) => Object.assign(m, v));
},
update,
refresh: async () => {
const list = await getThemes();
update((m) => list);
return list;
},
};
}
export const themes = createThemesStore();
export const themesIdx = derived(
themes,
$themes => {
const themes_idx = { };
for (const t of $themes) {
themes_idx[t.id] = t;
}
return themes_idx;
},
);

View file

@ -0,0 +1,26 @@
import { writable } from 'svelte/store';
import { getQAWork } from '$lib/todo'
function createTodosStore() {
const { subscribe, set, update } = writable([]);
return {
subscribe,
set: (v) => {
update((m) => Object.assign(m, v));
},
update,
refresh: async () => {
const list = await getQAWork();
update((m) => list);
return list;
},
};
}
export const todos = createTodosStore();

39
qa/ui/src/lib/themes.js Normal file
View file

@ -0,0 +1,39 @@
export class Theme {
constructor(res) {
if (res) {
this.update(res);
}
}
update({ id, name, urlid, path, authors, intro, headline, image, partner_img, partner_href, partner_txt }) {
this.id = id;
this.name = name;
this.urlid = urlid;
this.path = path;
this.authors = authors;
this.intro = intro;
this.headline = headline;
this.image = image;
this.partner_img = partner_img;
this.partner_href = partner_href;
this.partner_txt = partner_txt;
}
}
export async function getThemes() {
const res = await fetch(`api/themes`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return (await res.json()).map((t) => new Theme(t));
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getTheme(tid) {
const res = await fetch(`api/themes/${tid}`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return new Theme(await res.json());
} else {
throw new Error((await res.json()).errmsg);
}
}

61
qa/ui/src/lib/todo.js Normal file
View file

@ -0,0 +1,61 @@
import { QAQuery } from './qa';
export class QATodo {
constructor(res) {
if (res) {
this.update(res);
}
}
update({ id, id_team, id_exercice }) {
this.id = id;
this.id_team = id_team;
this.id_exercice = id_exercice;
}
}
export async function getQATodo() {
const res = await fetch(`api/qa_work.json`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return (await res.json()).map((t) => new QATodo(t));
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getQAWork() {
const res = await fetch(`api/qa_mywork.json`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
const data = await res.json()
if (data) {
return data.map((t) => new QAQuery(t));
} else {
return [];
}
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getQAView() {
const res = await fetch(`api/qa_myexercices.json`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
const data = await res.json()
if (data) {
return data.map((t) => new QATodo(t));
} else {
return [];
}
} else {
throw new Error((await res.json()).errmsg);
}
}
export async function getExerciceTested() {
const res = await fetch(`api/qa_exercices.json`, {headers: {'Accept': 'application/json'}})
if (res.status == 200) {
return await res.json();
} else {
throw new Error((await res.json()).errmsg);
}
}

View file

@ -5,6 +5,10 @@
} from 'sveltestrap'; } from 'sveltestrap';
import Header from '$lib/components/Header.svelte'; import Header from '$lib/components/Header.svelte';
import { version } from '$lib/stores/auth';
version.refresh();
setInterval(version.refresh, 30000);
</script> </script>
<svelte:head> <svelte:head>
@ -14,7 +18,9 @@
<Styles /> <Styles />
<Header /> <Header />
<slot></slot> <Container class="mt-2 mb-5">
<slot></slot>
</Container>
<style> <style>
:global(body) { :global(body) {

View file

@ -1,2 +1,23 @@
<h1>Welcome to SvelteKit</h1> <script>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p> import {
Container,
Row,
} from 'sveltestrap';
import MyExercices from '$lib/components/MyExercices.svelte';
import MyTodo from '$lib/components/MyTodo.svelte';
import { exercices } from '$lib/stores/exercices';
import { themes } from '$lib/stores/themes';
if ($exercices.length == 0) {
exercices.refresh();
}
if ($themes.length == 0) {
themes.refresh();
}
</script>
<Row>
<MyTodo class="col-6" />
<MyExercices class="col-6" />
</Row>

View file

@ -0,0 +1,51 @@
<script>
import { goto } from '$app/navigation';
import {
Icon,
Table,
} from 'sveltestrap';
import { fieldsExercices, getExercices } from '$lib/exercices';
let query = "";
function show(id) {
goto("exercices/" + id)
}
</script>
<h2>
Étapes
</h2>
{#await getExercices()}
{:then exercices}
<p>
<input type="search" class="form-control form-control-sm" placeholder="Search" bind:value={query} autofocus>
</p>
<Table class="table-hover table-bordered table-striped table-sm">
<thead class="thead-dark">
<tr>
{#each fieldsExercices as field}
<th>
{field}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each exercices as exercice (exercice.id)}
{#if exercice.title.indexOf(query) >= 0}
<tr on:click={() => show(exercice.id)}>
{#each fieldsExercices as field}
<td>
{@html exercice[field]}
</td>
{/each}
</tr>
{/if}
{/each}
</tbody>
</Table>
{/await}

View file

@ -0,0 +1,13 @@
<script>
import { page } from '$app/stores';
import { getExercice } from '$lib/exercices';
import ExerciceQA from '$lib/components/ExerciceQA.svelte';
let exerciceP = getExercice($page.params.eid);
</script>
{#await exerciceP}
{:then exercice}
<ExerciceQA {exercice} />
{/await}

View file

@ -0,0 +1 @@
<slot></slot>

View file

@ -0,0 +1,54 @@
<script>
import { goto } from '$app/navigation';
import { themes } from '$lib/stores/themes';
import {
Table,
} from 'sveltestrap';
themes.refresh();
let query = "";
const fields = ["name", "authors", "headline", "image"];
function show(id) {
goto("themes/" + id)
}
</script>
<h2>
Scénarios
</h2>
<p>
<input type="search" class="form-control" placeholder="Filtrer" bind:value={query} autofocus>
</p>
<Table class="table-hover table-bordered table-striped">
<thead class="thead-dark">
<tr>
{#each fields as field}
<th>
{field}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each $themes as theme (theme.id)}
{#if theme.name.indexOf(query) >= 0 || theme.authors.indexOf(query) >= 0 || theme.intro.indexOf(query) >= 0}
<tr on:click={() => show(theme.id)}>
{#each fields as field}
<td>
{#if field == "image"}
<img src={"../files" + theme[field]} alt="Image du scénario">
{:else}
{@html theme[field]}
{/if}
</td>
{/each}
</tr>
{/if}
{/each}
</tbody>
</Table>

View file

@ -0,0 +1,76 @@
<script>
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import {
Button,
Container,
Icon,
Table,
} from 'sveltestrap';
import { getTheme } from '$lib/themes';
import { fieldsExercices, getThemedExercices } from '$lib/exercices';
let query = "";
function show(id) {
goto("themes/" + $page.params.tid + "/" + id)
}
</script>
{#await getTheme($page.params.tid)}
{:then theme}
<div class="d-flex align-items-end">
<Button
class="align-self-center"
color="link"
on:click={() => goto('themes/')}
>
<Icon name="chevron-left" />
</Button>
<h2>
{theme.name}
</h2>
<small class="m-2 mb-3 text-muted text-truncate">{@html theme.authors}</small>
</div>
<Container class="text-muted">
{@html theme.intro}
</Container>
{#await getThemedExercices($page.params.tid)}
{:then exercices}
<h3>
Défis ({exercices.length})
</h3>
<p>
<input type="search" class="form-control form-control-sm" placeholder="Search" bind:value={query} autofocus>
</p>
<Table class="table-hover table-bordered table-striped table-sm">
<thead class="thead-dark">
<tr>
{#each fieldsExercices as field}
<th>
{field}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each exercices as exercice (exercice.id)}
{#if exercice.title.indexOf(query) >= 0}
<tr on:click={() => show(exercice.id)}>
{#each fieldsExercices as field}
<td>
{@html exercice[field]}
</td>
{/each}
</tr>
{/if}
{/each}
</tbody>
</Table>
{/await}
{/await}

View file

@ -0,0 +1,13 @@
<script>
import { page } from '$app/stores';
import { getThemedExercice } from '$lib/exercices';
import ExerciceQA from '$lib/components/ExerciceQA.svelte';
let exerciceP = getThemedExercice($page.params.tid, $page.params.eid);
</script>
{#await exerciceP}
{:then exercice}
<ExerciceQA {exercice} />
{/await}