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 := 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) {
|
||||||
|
var exercices []*fic.Exercice
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if theme, ok := c.Get("theme"); ok {
|
||||||
|
exercices, err = theme.(*fic.Theme).GetExercices()
|
||||||
|
} else {
|
||||||
// List all exercices
|
// List all exercices
|
||||||
exercices, err := fic.GetExercices()
|
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())})
|
||||||
|
|
18
qa/api/qa.go
18
qa/api/qa.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
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>
|
<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}– Logged as {$auth.name} (team #{$auth.id_team}){/if}
|
||||||
{:then v}
|
|
||||||
v{v.version}
|
|
||||||
{#if v.auth}– Logged as {v.auth.name} (team #{v.auth.id_team}){/if}
|
|
||||||
{/await}
|
|
||||||
</NavItem>
|
</NavItem>
|
||||||
</Nav>
|
</Nav>
|
||||||
</Navbar>
|
</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';
|
} 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 />
|
||||||
|
<Container class="mt-2 mb-5">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
</Container>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:global(body) {
|
:global(body) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
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