From cd64fc90bf9806328d5aeecdd9b8204754df839e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 25 Jul 2023 11:17:40 +0200 Subject: [PATCH] qa: Managers can view team and manage theirs todo list --- libfic/qa.go | 11 ++ qa/api/router.go | 1 + qa/api/team.go | 53 ++++++ qa/api/todo.go | 76 +++++++- qa/static.go | 2 + qa/ui/src/lib/components/MyExercices.svelte | 13 +- qa/ui/src/lib/components/MyTodo.svelte | 15 +- qa/ui/src/lib/stores/teams.js | 39 ++++ qa/ui/src/lib/stores/todo.js | 6 +- qa/ui/src/lib/teams.js | 49 +++++ qa/ui/src/lib/todo.js | 35 +++- qa/ui/src/routes/teams/+page.svelte | 53 ++++++ qa/ui/src/routes/teams/[tid]/+page.svelte | 197 ++++++++++++++++++++ 13 files changed, 526 insertions(+), 24 deletions(-) create mode 100644 qa/api/team.go create mode 100644 qa/ui/src/lib/stores/teams.js create mode 100644 qa/ui/src/lib/teams.js create mode 100644 qa/ui/src/routes/teams/+page.svelte create mode 100644 qa/ui/src/routes/teams/[tid]/+page.svelte diff --git a/libfic/qa.go b/libfic/qa.go index 45087f7d..0042efe4 100644 --- a/libfic/qa.go +++ b/libfic/qa.go @@ -242,6 +242,17 @@ func (t *Team) NewQATodo(idExercice int64) (*QATodo, error) { } } +// Delete the comment in the database. +func (t *QATodo) Delete() (int64, error) { + if res, err := DBExec("DELETE FROM teams_qa_todo WHERE id_todo = ?", t.Id); err != nil { + return 0, err + } else if nb, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return nb, err + } +} + // QAView func (t *Team) GetQAView() (res []*QATodo, err error) { diff --git a/qa/api/router.go b/qa/api/router.go index 5fc188ed..a59013b8 100644 --- a/qa/api/router.go +++ b/qa/api/router.go @@ -29,4 +29,5 @@ func DeclareRoutes(router *gin.RouterGroup) { })) declareTodoManagerRoutes(apiManagerRoutes) + declareTeamsRoutes(apiManagerRoutes) } diff --git a/qa/api/team.go b/qa/api/team.go new file mode 100644 index 00000000..0286c0c3 --- /dev/null +++ b/qa/api/team.go @@ -0,0 +1,53 @@ +package api + +import ( + "fmt" + "log" + "net/http" + "strconv" + + "srs.epita.fr/fic-server/libfic" + + "github.com/gin-gonic/gin" +) + +func declareTeamsRoutes(router *gin.RouterGroup) { + router.GET("/teams", listTeams) + + teamsRoutes := router.Group("/teams/:tid") + teamsRoutes.Use(teamHandler) + teamsRoutes.GET("", showTeam) + + declareTodoRoutes(teamsRoutes) + declareTodoManagerRoutes(teamsRoutes) +} + +func teamHandler(c *gin.Context) { + var team *fic.Team + if tid, err := strconv.ParseInt(string(c.Param("tid")), 10, 64); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad team identifier."}) + return + } else if team, err = fic.GetTeam(tid); err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Team not found."}) + return + } + + c.Set("team", team) + + c.Next() +} + +func listTeams(c *gin.Context) { + teams, err := fic.GetTeams() + if err != nil { + log.Println("Unable to GetTeams: ", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to list teams: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, teams) +} + +func showTeam(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("team")) +} diff --git a/qa/api/todo.go b/qa/api/todo.go index 75aab427..9a5ff270 100644 --- a/qa/api/todo.go +++ b/qa/api/todo.go @@ -2,6 +2,7 @@ package api import ( "net/http" + "strconv" "srs.epita.fr/fic-server/libfic" @@ -18,12 +19,22 @@ func declareTodoRoutes(router *gin.RouterGroup) { func declareTodoManagerRoutes(router *gin.RouterGroup) { router.POST("/qa_my_exercices.json", addQAView) router.POST("/qa_work.json", createQATodo) + + todosRoutes := router.Group("/todo/:wid") + todosRoutes.Use(todoHandler) + todosRoutes.GET("", showTodo) + todosRoutes.DELETE("", deleteQATodo) } type exerciceTested map[int64]string func getExerciceTested(c *gin.Context) { - teamid := c.MustGet("LoggedTeam").(int64) + var teamid int64 + if team, ok := c.Get("team"); ok { + teamid = team.(*fic.Team).Id + } else { + teamid = c.MustGet("LoggedTeam").(int64) + } team, err := fic.GetTeam(teamid) if err != nil { @@ -55,7 +66,12 @@ func getExerciceTested(c *gin.Context) { } func getQAView(c *gin.Context) { - teamid := c.MustGet("LoggedTeam").(int64) + var teamid int64 + if team, ok := c.Get("team"); ok { + teamid = team.(*fic.Team).Id + } else { + teamid = c.MustGet("LoggedTeam").(int64) + } team, err := fic.GetTeam(teamid) if err != nil { @@ -73,7 +89,12 @@ func getQAView(c *gin.Context) { } func getQAWork(c *gin.Context) { - teamid := c.MustGet("LoggedTeam").(int64) + var teamid int64 + if team, ok := c.Get("team"); ok { + teamid = team.(*fic.Team).Id + } else { + teamid = c.MustGet("LoggedTeam").(int64) + } team, err := fic.GetTeam(teamid) if err != nil { @@ -91,7 +112,12 @@ func getQAWork(c *gin.Context) { } func getQATodo(c *gin.Context) { - teamid := c.MustGet("LoggedTeam").(int64) + var teamid int64 + if team, ok := c.Get("team"); ok { + teamid = team.(*fic.Team).Id + } else { + teamid = c.MustGet("LoggedTeam").(int64) + } team, err := fic.GetTeam(teamid) if err != nil { @@ -167,3 +193,45 @@ func addQAView(c *gin.Context) { c.JSON(http.StatusOK, view) } + +func todoHandler(c *gin.Context) { + team := c.MustGet("team").(*fic.Team) + + var wid int64 + var todos []*fic.QATodo + var err error + + if wid, err = strconv.ParseInt(string(c.Param("wid")), 10, 64); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad todo identifier."}) + return + } + + if todos, err = team.GetQATodo(); err != nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Todo not found."}) + return + } + + for _, t := range todos { + if t.Id == wid { + c.Set("todo", t) + c.Next() + return + } + } + + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Unable to find the requested QA Todo"}) +} + +func showTodo(c *gin.Context) { + c.JSON(http.StatusOK, c.MustGet("todo")) +} + +func deleteQATodo(c *gin.Context) { + todo := c.MustGet("todo").(*fic.QATodo) + + if _, err := todo.Delete(); err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + } else { + c.Status(http.StatusOK) + } +} diff --git a/qa/static.go b/qa/static.go index f5c2170a..83507c3c 100644 --- a/qa/static.go +++ b/qa/static.go @@ -121,6 +121,8 @@ func declareStaticRoutes(router *gin.RouterGroup, baseURL string) { router.GET("/exercices", serveOrReverse("/", baseURL)) router.GET("/exercices/*_", serveOrReverse("/", baseURL)) router.GET("/export", serveOrReverse("/", baseURL)) + router.GET("/teams", serveOrReverse("/", baseURL)) + router.GET("/teams/*_", serveOrReverse("/", baseURL)) router.GET("/themes", serveOrReverse("/", baseURL)) router.GET("/themes/*_", serveOrReverse("/", baseURL)) router.GET("/_app/*_", serveOrReverse("", baseURL)) diff --git a/qa/ui/src/lib/components/MyExercices.svelte b/qa/ui/src/lib/components/MyExercices.svelte index cb4975d9..d6a11e87 100644 --- a/qa/ui/src/lib/components/MyExercices.svelte +++ b/qa/ui/src/lib/components/MyExercices.svelte @@ -9,11 +9,12 @@ 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 = ''; + export let team = null; + function show(id) { goto("exercices/" + id) } @@ -23,7 +24,7 @@ setInterval(() => my_exercicesP = update_exercices(), 62000); async function update_exercices() { - const view = await getQAView(); + const view = await getQAView(team); my_exercices = []; for (const v of view) { @@ -50,7 +51,13 @@ > ↻ -

Vos étapes

+

+ {#if team} + Étapes de l'équipe + {:else} + Vos étapes + {/if} +

{#await my_exercicesP}
diff --git a/qa/ui/src/lib/components/MyTodo.svelte b/qa/ui/src/lib/components/MyTodo.svelte index b81ce43f..6f70405b 100644 --- a/qa/ui/src/lib/components/MyTodo.svelte +++ b/qa/ui/src/lib/components/MyTodo.svelte @@ -13,10 +13,15 @@ export { className as class }; let className = ''; - let exo_doneP = getExerciceTested(); + export let team = null; + + let teamtodos = todos; + $: teamtodos = team ? team.todos : todos; + + let exo_doneP = getExerciceTested(team); let tododone = { }; - getQAWork().then((queries) => { + getQAWork(team).then((queries) => { for (const q of queries) { tododone[q.id_exercice] = q; } @@ -30,12 +35,12 @@

Étapes à tester et valider

- {#await todos.refresh()} + {#await teamtodos.refresh()}
@@ -54,7 +59,7 @@ - {#each $todos as todo (todo.id)} + {#each $teamtodos as todo (todo.id)} { + update((m) => Object.assign(m, v)); + }, + + update, + + refresh: async () => { + const list = await getTeams(); + update((m) => list); + return list; + }, + }; + +} + +export const teams = createTeamsStore(); + +export const teamsIdx = derived( + teams, + $teams => { + const teams_idx = { }; + + for (const e of $teams) { + teams_idx[e.id] = e; + } + + return teams_idx; + }, +); diff --git a/qa/ui/src/lib/stores/todo.js b/qa/ui/src/lib/stores/todo.js index 3adf65fe..cc24082d 100644 --- a/qa/ui/src/lib/stores/todo.js +++ b/qa/ui/src/lib/stores/todo.js @@ -2,7 +2,7 @@ import { writable, derived } from 'svelte/store'; import { getQAView, getQATodo, getQAWork } from '$lib/todo'; -function createTodosStore() { +export function createTodosStore(team) { const { subscribe, set, update } = writable([]); return { @@ -15,8 +15,8 @@ function createTodosStore() { update, refresh: async () => { - const list = await getQATodo(); - list.push(...await getQAWork()); + const list = await getQATodo(team); + list.push(...await getQAWork(team)); update((m) => list); return list; }, diff --git a/qa/ui/src/lib/teams.js b/qa/ui/src/lib/teams.js new file mode 100644 index 00000000..c39c2409 --- /dev/null +++ b/qa/ui/src/lib/teams.js @@ -0,0 +1,49 @@ +import { createTodosStore } from '$lib/stores/todo.js' + +export const fieldsTeams = ["name", "color", "active", "external_id"]; + +export class Team { + constructor(res) { + if (res) { + this.update(res); + } + + this.todos = createTodosStore(this); + } + + update({ id, name, color, active, external_id }) { + this.id = id; + this.name = name; + this.color = color; + this.active = active; + this.external_id = external_id; + } + + toHexColor() { + let num = this.color; + num >>>= 0; + let b = num & 0xFF, + g = (num & 0xFF00) >>> 8, + r = (num & 0xFF0000) >>> 16, + a = ( (num & 0xFF000000) >>> 24 ) / 255 ; + return "#" + r.toString(16) + g.toString(16) + b.toString(16); + } +} + +export async function getTeams() { + const res = await fetch(`api/teams`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + return (await res.json()).map((t) => new Team(t)); + } else { + throw new Error((await res.json()).errmsg); + } +} + +export async function getTeam(tid) { + const res = await fetch(`api/teams/${tid}`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + return new Team(await res.json()); + } else { + throw new Error((await res.json()).errmsg); + } +} diff --git a/qa/ui/src/lib/todo.js b/qa/ui/src/lib/todo.js index 69ddd5a7..4ddd769f 100644 --- a/qa/ui/src/lib/todo.js +++ b/qa/ui/src/lib/todo.js @@ -12,19 +12,36 @@ export class QATodo { this.id_team = id_team; this.id_exercice = id_exercice; } + + async delete(team) { + const res = await fetch(team?`api/teams/${team.id}/todo/${this.id}`:`api/teams/${this.id_team}/todo/${this.id}`, { + method: 'DELETE', + headers: {'Accept': 'application/json'} + }); + if (res.status < 300) { + return true; + } else { + throw new Error((await res.json()).errmsg); + } + } } -export async function getQATodo() { - const res = await fetch(`api/qa_work.json`, {headers: {'Accept': 'application/json'}}) +export async function getQATodo(team) { + const res = await fetch(team?`api/teams/${team.id}/qa_work.json`:`api/qa_work.json`, {headers: {'Accept': 'application/json'}}) if (res.status == 200) { - return (await res.json()).map((t) => new QATodo(t)); + const data = await res.json(); + if (data === null) { + return [] + } else { + return data.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'}}) +export async function getQAWork(team) { + const res = await fetch(team?`api/teams/${team.id}/qa_mywork.json`:`api/qa_mywork.json`, {headers: {'Accept': 'application/json'}}) if (res.status == 200) { const data = await res.json() if (data) { @@ -37,8 +54,8 @@ export async function getQAWork() { } } -export async function getQAView() { - const res = await fetch(`api/qa_myexercices.json`, {headers: {'Accept': 'application/json'}}) +export async function getQAView(team) { + const res = await fetch(team?`api/teams/${team.id}/qa_myexercices.json`:`api/qa_myexercices.json`, {headers: {'Accept': 'application/json'}}) if (res.status == 200) { const data = await res.json() if (data) { @@ -51,8 +68,8 @@ export async function getQAView() { } } -export async function getExerciceTested() { - const res = await fetch(`api/qa_exercices.json`, {headers: {'Accept': 'application/json'}}) +export async function getExerciceTested(team) { + const res = await fetch(team?`api/teams/${team.id}/qa_exercices.json`:`api/qa_exercices.json`, {headers: {'Accept': 'application/json'}}) if (res.status == 200) { return await res.json(); } else { diff --git a/qa/ui/src/routes/teams/+page.svelte b/qa/ui/src/routes/teams/+page.svelte new file mode 100644 index 00000000..e4ae06e1 --- /dev/null +++ b/qa/ui/src/routes/teams/+page.svelte @@ -0,0 +1,53 @@ + + + +

+ Équipes +

+ +

+ +

+ + + + {#each fields as field} + + {/each} + + + + {#each $teams as team (team.id)} + {#if team.name.indexOf(query) >= 0} + show(team.id)}> + {#each fields as field} + + {/each} + + {/if} + {/each} + +
+ {field} +
+ {team[field]} +
+
diff --git a/qa/ui/src/routes/teams/[tid]/+page.svelte b/qa/ui/src/routes/teams/[tid]/+page.svelte new file mode 100644 index 00000000..f1f75347 --- /dev/null +++ b/qa/ui/src/routes/teams/[tid]/+page.svelte @@ -0,0 +1,197 @@ + + +{#await teamP} + +
+ +
+
+{:then team} + +

+
+ {team.name} +

+ + + +

Tâches de l'équipe

+ {#await todosP} +
+ +
+ {:then} + + + + + + + + + + {#each $teamtodos as todo (todo.id)} + + + + + + {/each} + + + + + + + + + +
ScénarioDéfiAct
+ {#if $exercicesIdx.length == 0 && $themesIdx.length == 0} + + {:else} + + {$themesIdx[$exercicesIdx[todo.id_exercice].id_theme].name} + + {/if} + + {#if $exercicesIdx.length == 0} + + {:else} + {$exercicesIdx[todo.id_exercice].title} + {/if} + + +
+
submitNewTodo(team)} + > +
+ + + +
+
+
+
submitNewThemeTodo(team)} + > +
+ + + +
+
+
+ {/await} + + + + + +
+
+{/await}