qa: Back to the same situation
This commit is contained in:
parent
00f84e43ca
commit
1aa82bb2ef
27 changed files with 1336 additions and 22 deletions
|
@ -17,6 +17,8 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
|
|||
exercicesRoutes := router.Group("/exercices/:eid")
|
||||
exercicesRoutes.Use(exerciceHandler)
|
||||
exercicesRoutes.GET("", showExercice)
|
||||
|
||||
declareQARoutes(exercicesRoutes)
|
||||
}
|
||||
|
||||
func exerciceHandler(c *gin.Context) {
|
||||
|
@ -42,8 +44,15 @@ func exerciceHandler(c *gin.Context) {
|
|||
}
|
||||
|
||||
func listExercices(c *gin.Context) {
|
||||
var exercices []*fic.Exercice
|
||||
var err error
|
||||
|
||||
if theme, ok := c.Get("theme"); ok {
|
||||
exercices, err = theme.(*fic.Theme).GetExercices()
|
||||
} else {
|
||||
// List all exercices
|
||||
exercices, err := fic.GetExercices()
|
||||
exercices, err = fic.GetExercices()
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("Unable to GetExercices: ", err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list exercices: %s", err.Error())})
|
||||
|
|
18
qa/api/qa.go
18
qa/api/qa.go
|
@ -12,8 +12,7 @@ import (
|
|||
)
|
||||
|
||||
func declareQARoutes(router *gin.RouterGroup) {
|
||||
exercicesRoutes := router.Group("/qa/:eid")
|
||||
exercicesRoutes.Use(exerciceHandler)
|
||||
exercicesRoutes := router.Group("/qa")
|
||||
exercicesRoutes.GET("", getExerciceQA)
|
||||
exercicesRoutes.POST("", createExerciceQA)
|
||||
|
||||
|
@ -30,12 +29,21 @@ func declareQARoutes(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
func qaHandler(c *gin.Context) {
|
||||
exercice := c.MustGet("exercice").(*fic.Exercice)
|
||||
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."})
|
||||
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."})
|
||||
return
|
||||
}
|
||||
|
|
17
qa/ui/src/lib/components/DateFormat.svelte
Normal file
17
qa/ui/src/lib/components/DateFormat.svelte
Normal 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)}
|
54
qa/ui/src/lib/components/ExerciceQA.svelte
Normal file
54
qa/ui/src/lib/components/ExerciceQA.svelte
Normal 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>
|
|
@ -1,4 +1,6 @@
|
|||
<script>
|
||||
import { page } from '$app/stores'
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
|
@ -20,7 +22,15 @@
|
|||
Row,
|
||||
} 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>
|
||||
|
||||
<Navbar color="dark" dark expand="md">
|
||||
|
@ -30,25 +40,46 @@
|
|||
</NavbarBrand>
|
||||
<Nav navbar>
|
||||
<NavItem>
|
||||
<NavLink href=".">
|
||||
<NavLink
|
||||
href="."
|
||||
active={activemenu === ''}
|
||||
>
|
||||
<Icon name="house-door" />
|
||||
Accueil
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="themes">
|
||||
<NavLink
|
||||
href="themes"
|
||||
active={activemenu === 'themes'}
|
||||
>
|
||||
<Icon name="box-seam" />
|
||||
Scénarios
|
||||
</NavLink>
|
||||
</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" />
|
||||
Équipes
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="repositories">
|
||||
<NavLink
|
||||
href="repositories"
|
||||
active={activemenu === 'repositories'}
|
||||
>
|
||||
<Icon name="archive" />
|
||||
Dépôts
|
||||
</NavLink>
|
||||
|
@ -56,12 +87,8 @@
|
|||
</Nav>
|
||||
<Nav class="ms-auto text-light" navbar>
|
||||
<NavItem class="ms-2">
|
||||
{#await version}
|
||||
veuillez patienter
|
||||
{:then v}
|
||||
v{v.version}
|
||||
{#if v.auth}– Logged as {v.auth.name} (team #{v.auth.id_team}){/if}
|
||||
{/await}
|
||||
v{$version.version}
|
||||
{#if $auth}– Logged as {$auth.name} (team #{$auth.id_team}){/if}
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</Navbar>
|
||||
|
|
93
qa/ui/src/lib/components/MyExercices.svelte
Normal file
93
qa/ui/src/lib/components/MyExercices.svelte
Normal 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>
|
||||
–
|
||||
{/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>
|
91
qa/ui/src/lib/components/MyTodo.svelte
Normal file
91
qa/ui/src/lib/components/MyTodo.svelte
Normal 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>
|
183
qa/ui/src/lib/components/QAItem.svelte
Normal file
183
qa/ui/src/lib/components/QAItem.svelte
Normal 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…
|
||||
</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 : {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>
|
95
qa/ui/src/lib/components/QAItems.svelte
Normal file
95
qa/ui/src/lib/components/QAItems.svelte
Normal 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}
|
76
qa/ui/src/lib/components/QANewItem.svelte
Normal file
76
qa/ui/src/lib/components/QANewItem.svelte
Normal 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 ?
|
||||
</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>
|
66
qa/ui/src/lib/exercices.js
Normal file
66
qa/ui/src/lib/exercices.js
Normal 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
127
qa/ui/src/lib/qa.js
Normal 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);
|
||||
}
|
||||
}
|
29
qa/ui/src/lib/stores/auth.js
Normal file
29
qa/ui/src/lib/stores/auth.js
Normal 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,
|
||||
);
|
39
qa/ui/src/lib/stores/exercices.js
Normal file
39
qa/ui/src/lib/stores/exercices.js
Normal 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;
|
||||
},
|
||||
);
|
39
qa/ui/src/lib/stores/themes.js
Normal file
39
qa/ui/src/lib/stores/themes.js
Normal 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;
|
||||
},
|
||||
);
|
26
qa/ui/src/lib/stores/todo.js
Normal file
26
qa/ui/src/lib/stores/todo.js
Normal 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
39
qa/ui/src/lib/themes.js
Normal 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
61
qa/ui/src/lib/todo.js
Normal 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);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,10 @@
|
|||
} from 'sveltestrap';
|
||||
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import { version } from '$lib/stores/auth';
|
||||
|
||||
version.refresh();
|
||||
setInterval(version.refresh, 30000);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -14,7 +18,9 @@
|
|||
<Styles />
|
||||
|
||||
<Header />
|
||||
<Container class="mt-2 mb-5">
|
||||
<slot></slot>
|
||||
</Container>
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
|
|
|
@ -1,2 +1,23 @@
|
|||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
||||
<script>
|
||||
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>
|
||||
|
|
51
qa/ui/src/routes/exercices/+page.svelte
Normal file
51
qa/ui/src/routes/exercices/+page.svelte
Normal 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}
|
13
qa/ui/src/routes/exercices/[eid]/+page.svelte
Normal file
13
qa/ui/src/routes/exercices/[eid]/+page.svelte
Normal 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}
|
1
qa/ui/src/routes/themes/+layout.svelte
Normal file
1
qa/ui/src/routes/themes/+layout.svelte
Normal file
|
@ -0,0 +1 @@
|
|||
<slot></slot>
|
54
qa/ui/src/routes/themes/+page.svelte
Normal file
54
qa/ui/src/routes/themes/+page.svelte
Normal 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>
|
76
qa/ui/src/routes/themes/[tid]/+page.svelte
Normal file
76
qa/ui/src/routes/themes/[tid]/+page.svelte
Normal 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}
|
13
qa/ui/src/routes/themes/[tid]/[eid]/+page.svelte
Normal file
13
qa/ui/src/routes/themes/[tid]/[eid]/+page.svelte
Normal 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}
|
Reference in a new issue