From 55e829fa64087f474da1bb12eb5d72f13090fab7 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 4 Feb 2025 15:42:22 +0100 Subject: [PATCH 1/6] fickit: Allow admin to remove submissions --- fickit-backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fickit-backend.yml b/fickit-backend.yml index 16a57215..6f640ac3 100644 --- a/fickit-backend.yml +++ b/fickit-backend.yml @@ -237,7 +237,7 @@ services: - /var/lib/fic/generator:/srv/GENERATOR:ro - /var/lib/fic/pki:/srv/PKI - /var/lib/fic/settings:/srv/SETTINGS - - /var/lib/fic/submissions:/srv/submissions:ro + - /var/lib/fic/submissions:/srv/submissions - /var/lib/fic/sync:/srv/SYNC - /var/lib/fic/teams:/srv/TEAMS net: /run/netns/fic-admin From 603b226955423b3b417297e2d93a63473ad51e24 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 4 Feb 2025 15:51:54 +0100 Subject: [PATCH 2/6] fickit: Prepare team registration through checker --- fickit-backend.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fickit-backend.yml b/fickit-backend.yml index 6f640ac3..def65aaa 100644 --- a/fickit-backend.yml +++ b/fickit-backend.yml @@ -278,7 +278,10 @@ services: binds: - /etc/hosts:/etc/hosts:ro - /var/lib/fic/generator:/srv/GENERATOR:ro + # Uncomment this to disallow registrations - /var/lib/fic/teams:/srv/TEAMS:ro + # Uncomment this to allow registrations + #- /var/lib/fic/teams:/srv/TEAMS - /var/lib/fic/secrets/mysql_password:/run/secrets/mysql_password:ro - /var/lib/fic/settingsdist:/srv/SETTINGSDIST:ro - /var/lib/fic/submissions:/srv/submissions From 650f9339934dead2c1ed6c538fae84c713277891 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 4 Feb 2025 17:01:00 +0100 Subject: [PATCH 3/6] admin: duration change impact the expected end --- admin/static/js/app.js | 9 +++++++++ admin/static/views/settings.html | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/admin/static/js/app.js b/admin/static/js/app.js index 2fa150f4..f81bb101 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -614,9 +614,18 @@ angular.module("FICApp") response.enableExerciceDepend = response.unlockedChallengeDepth >= 0; response.disabledsubmitbutton = response.disablesubmitbutton && response.disablesubmitbutton.length > 0; + if (response.end) { + $scope.duration = (response.end - response.start)/60000; + } }) $scope.challenge = SettingsChallenge.get(); $scope.duration = 360; + $scope.durationChange = function(endChanged) { + if (endChanged) + $scope.duration = (new Date($scope.config.end).getTime() - new Date($scope.config.start).getTime())/60000; + else + $scope.config.end = new Date(new Date($scope.config.start).getTime() + $scope.duration * 60000); + } $scope.activateTime = ""; $scope.challenge.$promise.then(function (c) { if (c.duration) diff --git a/admin/static/views/settings.html b/admin/static/views/settings.html index 58035170..61843360 100644 --- a/admin/static/views/settings.html +++ b/admin/static/views/settings.html @@ -35,7 +35,7 @@
- +
@@ -46,14 +46,14 @@
- +
- +
min
From 63b4cdc6229cea53d95dd7f5fd8a0a844b9ecfe3 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 4 Feb 2025 18:42:53 +0100 Subject: [PATCH 4/6] admin: Use non-breakable whitespaces --- admin/static/js/app.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/admin/static/js/app.js b/admin/static/js/app.js index f81bb101..e966b31b 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -660,7 +660,7 @@ angular.module("FICApp") $scope.saveChallengeInfo = function () { this.challenge.duration = $scope.duration; this.challenge.$update(function (response) { - $scope.addToast('success', 'Infos du challenge mises à jour avec succès !'); + $scope.addToast('success', 'Infos du challenge mises à jour avec succès !'); }, function (response) { $scope.addToast('danger', 'An error occurs when saving challenge info:', response.data.errmsg); }); @@ -734,9 +734,9 @@ angular.module("FICApp") "teams": "En validant, vous supprimerez l'ensemble des équipes enregistreées.", "game": "En validant, vous supprimerez toutes les tentatives, les validations, ... faites par les équipes.", } - $scope.addToast('warning', txts[type], 'Êtes-vous sûr de vouloir continuer ?', + $scope.addToast('warning', txts[type], 'Êtes-vous sûr de vouloir continuer ?', function () { - if (confirm("Êtes-vous vraiment sûr ?\n" + txts[type])) { + if (confirm("Êtes-vous vraiment sûr ?\n" + txts[type])) { $http.post("api/reset", { "type": type }).then(function (time) { $scope.addToast('success', type + 'reseted'); $location.url("/"); @@ -748,7 +748,7 @@ angular.module("FICApp") }); }; $scope.switchToProd = function () { - $scope.addToast('warning', "Activer le mode challenge ?", "L'activation du mode challenge est temporaire (vous devriez plutôt relancer le daemon avec l'option `-4real`). Ce mode permet d'éviter les mauvaises manipulations et désactive le hook git de synchronisation automatique. Êtes-vous sûr de vouloir continuer ?", + $scope.addToast('warning', "Activer le mode challenge ?", "L'activation du mode challenge est temporaire (vous devriez plutôt relancer le daemon avec l'option `-4real`). Ce mode permet d'éviter les mauvaises manipulations et désactive le hook git de synchronisation automatique. Êtes-vous sûr de vouloir continuer ?", function () { $http.put("api/prod", true).then(function (time) { $rootScope.refreshSettings() @@ -832,10 +832,10 @@ angular.module("FICApp") $scope.deepSync = function (theme) { if (theme) { - question = 'Faire une synchronisation intégrale du thème ' + theme.name + ' ?' + question = 'Faire une synchronisation intégrale du thème ' + theme.name + ' ?' url = "api/sync/deep/" + theme.id } else { - question = 'Faire une synchronisation intégrale ?' + question = 'Faire une synchronisation intégrale ?' url = "api/sync/deep" } $scope.addToast('warning', question, '', @@ -851,7 +851,7 @@ angular.module("FICApp") }); }; $scope.speedyDeepSync = function () { - $scope.addToast('warning', 'Faire une synchronisation profonde rapide, sans s\'occuper des fichiers ?', '', + $scope.addToast('warning', 'Faire une synchronisation profonde rapide, sans s\'occuper des fichiers ?', '', function () { $scope.deepSyncInProgress = true; $http.post("api/sync/speed").then(function () { @@ -864,7 +864,7 @@ angular.module("FICApp") }); }; $scope.baseSync = function () { - $scope.addToast('warning', 'Tirer les mises à jour du dépôt ?', '', + $scope.addToast('warning', 'Tirer les mises à jour du dépôt ?', '', function () { $scope.deepSyncInProgress = true; $http.post("api/sync/base").then(function () { @@ -877,7 +877,7 @@ angular.module("FICApp") }); }; $scope.syncVideos = function () { - $scope.addToast('warning', 'Synchroniser les vidéos de résolution ?', 'ATTENTION il ne faut pas lancer cette synchronisation durant le challenge. Seulement une fois le challenge terminé, cela permet de rendre les vidéos accessibles dans l\'interface joueurs.', + $scope.addToast('warning', 'Synchroniser les vidéos de résolution ?', 'ATTENTION il ne faut pas lancer cette synchronisation durant le challenge. Seulement une fois le challenge terminé, cela permet de rendre les vidéos accessibles dans l\'interface joueurs.', function () { $scope.deepSyncInProgress = true; $http.post("api/sync/videos").then(function () { @@ -1122,7 +1122,7 @@ angular.module("FICApp") }, { type: "countdown", - params: { color: "success", end: null, lead: "Go, go, go !", title: "Le challenge forensic va bientôt commencer !" }, + params: { color: "success", end: null, lead: "Go, go, go !", title: "Le challenge forensic va bientôt commencer !" }, }, ]; $scope.display.side = [ @@ -1241,8 +1241,8 @@ angular.module("FICApp") show: true, shadow: "#E8CF5C", end: new Date($rootScope.getSrvTime().getTime() + 1802000).toISOString(), - before: "Heure joyeuse : chaque résolution compte double !", - after: "Heure joyeuse terminée !", + before: "Heure joyeuse : chaque résolution compte double !", + after: "Heure joyeuse terminée !", } } else if (scene == "freehintquarter") { @@ -1250,8 +1250,8 @@ angular.module("FICApp") show: true, shadow: "#3DD28F", end: new Date($rootScope.getSrvTime().getTime() + 902000).toISOString(), - before: "Quart d'heure facile : indices dévoilés !", - after: "Quart d'heure facile terminée !", + before: "Quart d'heure facile : indices dévoilés !", + after: "Quart d'heure facile terminée !", } } }; @@ -1380,7 +1380,7 @@ angular.module("FICApp") }); }; $scope.clearFilesDir = function () { - $scope.addToast('warning', 'Êtes-vous sûr de vouloir continuer ?', "Ceci va supprimer tout le contenu du dossier FILES. Il s'agit des fichiers ci-dessous, il faudra refaire une synchronisation ensuite.", + $scope.addToast('warning', 'Êtes-vous sûr de vouloir continuer ?', "Ceci va supprimer tout le contenu du dossier FILES. Il s'agit des fichiers ci-dessous, il faudra refaire une synchronisation ensuite.", function () { $scope.clearFilesWIP = true; $http({ @@ -2288,7 +2288,7 @@ angular.module("FICApp") method: "POST" }).then(function (response) { flag.test_str = ""; - $scope.addToast('success', "Flag Ok !"); + $scope.addToast('success', "Flag Ok !"); }, function (response) { flag.test_str = ""; $scope.addToast('danger', 'An error occurs: ', response.data.errmsg); From b409fa680664cf2d45bfbf269ee41ad6b4f349d3 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 4 Feb 2025 12:33:26 +0100 Subject: [PATCH 5/6] admin: Retrieve stats on exercices --- admin/api/exercice.go | 61 +++++++++++++++++++++++++- admin/static/js/app.js | 23 ++++++++++ admin/static/views/exercice-flags.html | 23 ++++++++-- libfic/db.go | 21 +++++++++ libfic/exercice.go | 51 +++++++++++++++------ libfic/exercice_history.go | 3 +- libfic/flag.go | 2 + libfic/flag_key.go | 25 +++++++++++ libfic/flag_label.go | 8 ++++ libfic/mcq.go | 43 ++++++++++++++++++ 10 files changed, 241 insertions(+), 19 deletions(-) diff --git a/admin/api/exercice.go b/admin/api/exercice.go index c4fdaed9..e77738e8 100644 --- a/admin/api/exercice.go +++ b/admin/api/exercice.go @@ -78,6 +78,7 @@ func declareExercicesRoutes(router *gin.RouterGroup) { apiQuizRoutes.PUT("", updateExerciceQuiz) apiQuizRoutes.DELETE("", deleteExerciceQuiz) apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps) + apiQuizRoutes.GET("/statistics", showExerciceQuizStats) apiExercicesRoutes.GET("/tags", listExerciceTags) apiExercicesRoutes.POST("/tags", addExerciceTag) @@ -864,7 +865,7 @@ func showExerciceFlagStats(c *gin.Context) { return } - var completed, tries, nteams int64 + var completed int64 for _, hline := range history { if hline["kind"].(string) == "flag_found" { @@ -874,10 +875,24 @@ func showExerciceFlagStats(c *gin.Context) { } } + tries, err := flag.NbTries() + if err != nil { + log.Println("Unable to nbTries:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag tries"}) + return + } + + teams, err := flag.TeamsOnIt() + if err != nil { + log.Println("Unable to teamsOnIt:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag related teams"}) + return + } + c.JSON(http.StatusOK, gin.H{ "completed": completed, "tries": tries, - "nteams": nteams, + "teams": teams, }) } @@ -1051,6 +1066,48 @@ func showExerciceQuizDeps(c *gin.Context) { c.JSON(http.StatusOK, deps) } +func showExerciceQuizStats(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + quiz := c.MustGet("flag-quiz").(*fic.MCQ) + + history, err := exercice.GetHistory() + if err != nil { + log.Println("Unable to getExerciceHistory:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"}) + return + } + + var completed int64 + + for _, hline := range history { + if hline["kind"].(string) == "mcq_found" { + if *hline["secondary"].(*int) == quiz.Id { + completed += 1 + } + } + } + + tries, err := quiz.NbTries() + if err != nil { + log.Println("Unable to nbTries:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag tries"}) + return + } + + teams, err := quiz.TeamsOnIt() + if err != nil { + log.Println("Unable to teamsOnIt:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag related teams"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "completed": completed, + "tries": tries, + "teams": teams, + }) +} + func updateExerciceQuiz(c *gin.Context) { quiz := c.MustGet("flag-quiz").(*fic.MCQ) diff --git a/admin/static/js/app.js b/admin/static/js/app.js index e966b31b..39c5aaae 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -358,6 +358,9 @@ angular.module("FICApp") }) .factory("ExerciceMCQDeps", function ($resource) { return $resource("api/exercices/:exerciceId/quiz/:mcqId/dependancies", { exerciceId: '@idExercice', mcqId: '@id' }) + }) + .factory("ExerciceMCQStats", function ($resource) { + return $resource("api/exercices/:exerciceId/quiz/:mcqId/statistics", { exerciceId: '@idExercice', mcqId: '@id' }) }); angular.module("FICApp") @@ -771,6 +774,20 @@ angular.module("FICApp") }); }; }) + .component('teamLink', { + bindings: { + idTeam: '=', + }, + controller: function (Team) { + var ctrl = this; + ctrl.team = {}; + + ctrl.$onInit = function () { + ctrl.team = Team.get({teamId: ctrl.idTeam}); + }; + }, + template: `{{ $ctrl.team.name }} ` + }) .component('repositoryUptodate', { bindings: { repository: '<', @@ -2400,6 +2417,12 @@ angular.module("FICApp") } }) + .controller("ExerciceMCQStatsController", function ($scope, $routeParams, ExerciceMCQStats) { + $scope.init = function (mcq) { + $scope.stats = ExerciceMCQStats.get({ exerciceId: $routeParams.exerciceId, mcqId: mcq.id }); + } + }) + .controller("TeamsListController", function ($scope, $rootScope, Team, $location, $http) { $scope.teams = Team.query(); $scope.fields = ["id", "name"]; diff --git a/admin/static/views/exercice-flags.html b/admin/static/views/exercice-flags.html index b23556ca..217d9feb 100644 --- a/admin/static/views/exercice-flags.html +++ b/admin/static/views/exercice-flags.html @@ -83,9 +83,13 @@
Statistiques
    -
  • Validés : {{ stats["completed"] }}
  • -
  • Tentés : {{ stats["tries"] }}
  • -
  • Équipes : {{ stats["nteams"] }}
  • +
  • Validés : {{ stats["completed"] }}
  • +
  • Tentés : {{ stats["tries"] }}
  • +
  • + Équipes : + aucune + +
@@ -177,6 +181,19 @@ sans +
+
+ Statistiques +
    +
  • Validés : {{ stats["completed"] }}
  • +
  • Tentés : {{ stats["tries"] }}
  • +
  • + Équipes : + aucune + +
  • +
+
diff --git a/libfic/db.go b/libfic/db.go index ae6826ae..0a96120e 100644 --- a/libfic/db.go +++ b/libfic/db.go @@ -414,6 +414,7 @@ CREATE TABLE IF NOT EXISTS exercice_solved( } if _, err := db.Exec(` CREATE TABLE IF NOT EXISTS exercice_tries( + id_try INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, id_exercice INTEGER NOT NULL, id_team INTEGER NOT NULL, time TIMESTAMP NOT NULL, @@ -423,6 +424,26 @@ CREATE TABLE IF NOT EXISTS exercice_tries( FOREIGN KEY(id_exercice) REFERENCES exercices(id_exercice), FOREIGN KEY(id_team) REFERENCES teams(id_team) ) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; +`); err != nil { + return err + } + if _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS exercice_tries_flags( + id_try INTEGER NOT NULL, + id_flag INTEGER NOT NULL, + FOREIGN KEY(id_try) REFERENCES exercice_tries(id_try) ON DELETE CASCADE, + FOREIGN KEY(id_flag) REFERENCES exercice_flags(id_flag) ON DELETE CASCADE +) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; +`); err != nil { + return err + } + if _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS exercice_tries_mcq( + id_try INTEGER NOT NULL, + id_mcq INTEGER NOT NULL, + FOREIGN KEY(id_try) REFERENCES exercice_tries(id_try) ON DELETE CASCADE, + FOREIGN KEY(id_mcq) REFERENCES exercice_mcq(id_mcq) ON DELETE CASCADE +) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; `); err != nil { return err } diff --git a/libfic/exercice.go b/libfic/exercice.go index c9979fcb..d6b38121 100644 --- a/libfic/exercice.go +++ b/libfic/exercice.go @@ -3,6 +3,7 @@ package fic import ( "errors" "fmt" + "log" "math" "time" ) @@ -444,11 +445,25 @@ func (e *Exercice) GetOrdinal() (int, error) { } // NewTry registers a solving attempt for the given Team. -func (e *Exercice) NewTry(t *Team, cksum []byte) error { - if _, err := DBExec("INSERT INTO exercice_tries (id_exercice, id_team, time, cksum) VALUES (?, ?, ?, ?)", e.Id, t.Id, time.Now(), cksum); err != nil { - return err +func (e *Exercice) NewTry(t *Team, cksum []byte, flags ...Flag) (int64, error) { + if res, err := DBExec("INSERT INTO exercice_tries (id_exercice, id_team, time, cksum) VALUES (?, ?, ?, ?)", e.Id, t.Id, time.Now(), cksum); err != nil { + return 0, err } else { - return nil + return res.LastInsertId() + } +} + +func (e *Exercice) NewTryFlag(tryid int64, flags ...Flag) { + for _, flag := range flags { + if fk, ok := flag.(*FlagKey); ok { + if _, err := DBExec("INSERT INTO exercice_tries_flags (id_try, id_flag) VALUES (?, ?)", tryid, fk.Id); err != nil { + log.Println("Unable to add detailed try: ", err.Error()) + } + } else if fm, ok := flag.(*MCQ); ok { + if _, err := DBExec("INSERT INTO exercice_tries_mcq (id_try, id_mcq) VALUES (?, ?)", tryid, fm.Id); err != nil { + log.Println("Unable to add detailed try: ", err.Error()) + } + } } } @@ -550,7 +565,7 @@ func (e *Exercice) MCQSolved() (res []int64) { // CheckResponse, given both flags and MCQ responses, figures out if thoses are correct (or if they are previously solved). // In the meanwhile, CheckResponse registers good answers given (but it does not mark the challenge as solved at the end). func (e *Exercice) CheckResponse(cksum []byte, respflags map[int]string, respmcq map[int]bool, t *Team) (bool, error) { - if err := e.NewTry(t, cksum); err != nil { + if tryId, err := e.NewTry(t, cksum); err != nil { return false, err } else if flags, err := e.GetFlagKeys(); err != nil { return false, err @@ -565,6 +580,10 @@ func (e *Exercice) CheckResponse(cksum []byte, respflags map[int]string, respmcq // Check MCQs for _, mcq := range mcqs { + if mcq.HasOneEntry(respmcq) { + e.NewTryFlag(tryId, mcq) + } + if d := mcq.Check(respmcq); d > 0 { if !PartialValidation || t.HasPartiallySolved(mcq) == nil { valid = false @@ -589,15 +608,21 @@ func (e *Exercice) CheckResponse(cksum []byte, respflags map[int]string, respmcq for _, flag := range flags { if res, ok := respflags[flag.Id]; !ok && (!PartialValidation || t.HasPartiallySolved(flag) == nil) { valid = valid && flag.IsOptionnal() - } else if flag.Check([]byte(res)) != 0 { - if !PartialValidation || t.HasPartiallySolved(flag) == nil { - valid = valid && flag.IsOptionnal() + } else if ok { + if len(res) > 0 { + e.NewTryFlag(tryId, flag) } - } else { - err := flag.FoundBy(t) - if err == nil { - // err is unicity issue, probably flag already found - goodResponses += 1 + + if flag.Check([]byte(res)) != 0 { + if !PartialValidation || t.HasPartiallySolved(flag) == nil { + valid = valid && flag.IsOptionnal() + } + } else { + err := flag.FoundBy(t) + if err == nil { + // err is unicity issue, probably flag already found + goodResponses += 1 + } } } } diff --git a/libfic/exercice_history.go b/libfic/exercice_history.go index 63dab2dd..763c48d7 100644 --- a/libfic/exercice_history.go +++ b/libfic/exercice_history.go @@ -68,7 +68,8 @@ func (e *Exercice) AppendHistoryItem(tId int64, kind string, secondary *int64) e if kind == "tries" { bid := make([]byte, 5) binary.LittleEndian.PutUint32(bid, rand.Uint32()) - return (&Exercice{Id: e.Id}).NewTry(team, bid) + _, err = (&Exercice{Id: e.Id}).NewTry(team, bid) + return err } else if kind == "hint" && secondary != nil { return team.OpenHint(&EHint{Id: *secondary}) } else if kind == "wchoices" && secondary != nil { diff --git a/libfic/flag.go b/libfic/flag.go index 06373495..c4de997e 100644 --- a/libfic/flag.go +++ b/libfic/flag.go @@ -14,6 +14,8 @@ type Flag interface { Check(val interface{}) int IsOptionnal() bool FoundBy(t *Team) error + NbTries() (int64, error) + TeamsOnIt() ([]int64, error) } // GetFlag returns a list of flags comming with the challenge. diff --git a/libfic/flag_key.go b/libfic/flag_key.go index 905d1942..c3a5faae 100644 --- a/libfic/flag_key.go +++ b/libfic/flag_key.go @@ -230,6 +230,31 @@ func (k *FlagKey) RecoverId() (Flag, error) { } } +// NbTries returns the flag resolution statistics. +func (k *FlagKey) NbTries() (tries int64, err error) { + err = DBQueryRow("SELECT COUNT(*) AS tries FROM exercice_tries_flags WHERE id_flag = ?", k.Id).Scan(&tries) + return +} + +func (k *FlagKey) TeamsOnIt() ([]int64, error) { + if rows, err := DBQuery("SELECT DISTINCT M.id_team FROM exercice_tries_flags F INNER JOIN exercice_tries T ON T.id_try = F.id_try INNER JOIN teams M ON M.id_team = T.id_team WHERE id_flag = ?", k.Id); err != nil { + return nil, err + } else { + defer rows.Close() + + teams := []int64{} + for rows.Next() { + var idteam int64 + if err := rows.Scan(&idteam); err != nil { + return nil, err + } + teams = append(teams, idteam) + } + + return teams, nil + } +} + // AddFlagKey creates and fills a new struct Flag, from a hashed flag, and registers it into the database. func (k *FlagKey) Create(e *Exercice) (Flag, error) { // Check the regexp compile diff --git a/libfic/flag_label.go b/libfic/flag_label.go index 6d103f7f..79f2ba04 100644 --- a/libfic/flag_label.go +++ b/libfic/flag_label.go @@ -71,6 +71,14 @@ func (k *FlagLabel) RecoverId() (Flag, error) { } } +func (k *FlagLabel) NbTries() (int64, error) { + return 0, nil +} + +func (k *FlagLabel) TeamsOnIt() ([]int64, error) { + return nil, nil +} + // AddFlagLabel creates and fills a new struct Flag and registers it into the database. func (k *FlagLabel) Create(e *Exercice) (Flag, error) { if res, err := DBExec("INSERT INTO exercice_flag_labels (id_exercice, ordre, label, variant) VALUES (?, ?, ?, ?)", e.Id, k.Order, k.Label, k.Variant); err != nil { diff --git a/libfic/mcq.go b/libfic/mcq.go index 04bf3fd9..0b3ad66b 100644 --- a/libfic/mcq.go +++ b/libfic/mcq.go @@ -136,6 +136,31 @@ func (m *MCQ) RecoverId() (Flag, error) { } } +// NbTries returns the MCQ resolution statistics. +func (m *MCQ) NbTries() (tries int64, err error) { + err = DBQueryRow("SELECT COUNT(*) AS tries FROM exercice_tries_mcq WHERE id_mcq = ?", m.Id).Scan(&tries) + return +} + +func (m *MCQ) TeamsOnIt() ([]int64, error) { + if rows, err := DBQuery("SELECT DISTINCT M.id_team FROM exercice_tries_mcq F INNER JOIN exercice_tries T ON T.id_try = F.id_try INNER JOIN teams M ON M.id_team = T.id_team WHERE id_mcq = ?", m.Id); err != nil { + return nil, err + } else { + defer rows.Close() + + teams := []int64{} + for rows.Next() { + var idteam int64 + if err := rows.Scan(&idteam); err != nil { + return nil, err + } + teams = append(teams, idteam) + } + + return teams, nil + } +} + // Create registers a MCQ into the database and recursively add its entries. func (m *MCQ) Create(e *Exercice) (Flag, error) { if res, err := DBExec("INSERT INTO exercice_mcq (id_exercice, ordre, title) VALUES (?, ?, ?)", e.Id, m.Order, m.Title); err != nil { @@ -319,6 +344,24 @@ func (m *MCQ) IsOptionnal() bool { return false } +// Check if the given vals contains at least a response for the given MCQ. +func (m *MCQ) HasOneEntry(v interface{}) bool { + var vals map[int]bool + if va, ok := v.(map[int]bool); !ok { + return false + } else { + vals = va + } + + for _, n := range m.Entries { + if _, ok := vals[n.Id]; ok { + return true + } + } + + return false +} + // Check if the given vals are the expected ones to validate this flag. func (m *MCQ) Check(v interface{}) int { var vals map[int]bool From 08a31898dff0013ef8d7abdd7401ab4b87daec5e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 4 Feb 2025 19:07:47 +0100 Subject: [PATCH 6/6] admin: New button to delete tries for a flag --- admin/api/exercice.go | 26 ++++++++++++++++++++++++++ admin/static/js/app.js | 16 ++++++++++++++-- admin/static/views/exercice-flags.html | 10 ++++++++-- libfic/flag.go | 1 + libfic/flag_key.go | 23 +++++++++++++++++++++++ libfic/flag_label.go | 4 ++++ libfic/mcq.go | 23 +++++++++++++++++++++++ 7 files changed, 99 insertions(+), 4 deletions(-) diff --git a/admin/api/exercice.go b/admin/api/exercice.go index e77738e8..1df894bd 100644 --- a/admin/api/exercice.go +++ b/admin/api/exercice.go @@ -63,6 +63,7 @@ func declareExercicesRoutes(router *gin.RouterGroup) { apiFlagsRoutes.DELETE("/", deleteExerciceFlag) apiFlagsRoutes.GET("/dependancies", showExerciceFlagDeps) apiFlagsRoutes.GET("/statistics", showExerciceFlagStats) + apiFlagsRoutes.DELETE("/tries", deleteExerciceFlagTries) apiFlagsRoutes.GET("/choices/", listFlagChoices) apiFlagsChoicesRoutes := apiExercicesRoutes.Group("/choices/:cid") apiFlagsChoicesRoutes.Use(FlagChoiceHandler) @@ -79,6 +80,7 @@ func declareExercicesRoutes(router *gin.RouterGroup) { apiQuizRoutes.DELETE("", deleteExerciceQuiz) apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps) apiQuizRoutes.GET("/statistics", showExerciceQuizStats) + apiQuizRoutes.DELETE("/tries", deleteExerciceQuizTries) apiExercicesRoutes.GET("/tags", listExerciceTags) apiExercicesRoutes.POST("/tags", addExerciceTag) @@ -896,6 +898,18 @@ func showExerciceFlagStats(c *gin.Context) { }) } +func deleteExerciceFlagTries(c *gin.Context) { + flag := c.MustGet("flag-key").(*fic.FlagKey) + + err := flag.DeleteTries() + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + c.AbortWithStatusJSON(http.StatusOK, true) +} + func tryExerciceFlag(c *gin.Context) { flag := c.MustGet("flag-key").(*fic.FlagKey) @@ -1108,6 +1122,18 @@ func showExerciceQuizStats(c *gin.Context) { }) } +func deleteExerciceQuizTries(c *gin.Context) { + quiz := c.MustGet("flag-quiz").(*fic.MCQ) + + err := quiz.DeleteTries() + if err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + c.AbortWithStatusJSON(http.StatusOK, true) +} + func updateExerciceQuiz(c *gin.Context) { quiz := c.MustGet("flag-quiz").(*fic.MCQ) diff --git a/admin/static/js/app.js b/admin/static/js/app.js index 39c5aaae..24e7b36b 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -2374,10 +2374,16 @@ angular.module("FICApp") } }) - .controller("ExerciceFlagStatsController", function ($scope, $routeParams, ExerciceFlagStats) { + .controller("ExerciceFlagStatsController", function ($scope, $routeParams, ExerciceFlagStats, $http) { $scope.init = function (flag) { + $scope.flag_id = flag.id; $scope.stats = ExerciceFlagStats.get({ exerciceId: $routeParams.exerciceId, flagId: flag.id }); } + $scope.deleteTries = function () { + $http.delete(`/api/exercices/${$routeParams.exerciceId}/flags/${$scope.flag_id}/tries`).then(function () { + $scope.stats = ExerciceFlagStats.get({ exerciceId: $routeParams.exerciceId, flagId: $scope.flag_id }); + }); + } }) .controller("ExerciceMCQFlagsController", function ($scope, ExerciceMCQFlag, $routeParams, $rootScope) { @@ -2417,10 +2423,16 @@ angular.module("FICApp") } }) - .controller("ExerciceMCQStatsController", function ($scope, $routeParams, ExerciceMCQStats) { + .controller("ExerciceMCQStatsController", function ($scope, $routeParams, ExerciceMCQStats, $http) { $scope.init = function (mcq) { + $scope.mcq_id = mcq.id; $scope.stats = ExerciceMCQStats.get({ exerciceId: $routeParams.exerciceId, mcqId: mcq.id }); } + $scope.deleteTries = function () { + $http.delete(`/api/exercices/${$routeParams.exerciceId}/quiz/${$scope.mcq_id}/tries`).then(function () { + $scope.stats = ExerciceMCQStats.get({ exerciceId: $routeParams.exerciceId, mcqId: $scope.mcq_id }); + }); + } }) .controller("TeamsListController", function ($scope, $rootScope, Team, $location, $http) { diff --git a/admin/static/views/exercice-flags.html b/admin/static/views/exercice-flags.html index 217d9feb..f765d9a9 100644 --- a/admin/static/views/exercice-flags.html +++ b/admin/static/views/exercice-flags.html @@ -84,7 +84,10 @@ Statistiques
  • Validés : {{ stats["completed"] }}
  • -
  • Tentés : {{ stats["tries"] }}
  • +
  • + Tentés : {{ stats["tries"] }} + +
  • Équipes : aucune @@ -186,7 +189,10 @@ Statistiques
    • Validés : {{ stats["completed"] }}
    • -
    • Tentés : {{ stats["tries"] }}
    • +
    • + Tentés : {{ stats["tries"] }} + +
    • Équipes : aucune diff --git a/libfic/flag.go b/libfic/flag.go index c4de997e..2e419bf1 100644 --- a/libfic/flag.go +++ b/libfic/flag.go @@ -16,6 +16,7 @@ type Flag interface { FoundBy(t *Team) error NbTries() (int64, error) TeamsOnIt() ([]int64, error) + DeleteTries() error } // GetFlag returns a list of flags comming with the challenge. diff --git a/libfic/flag_key.go b/libfic/flag_key.go index c3a5faae..40da9348 100644 --- a/libfic/flag_key.go +++ b/libfic/flag_key.go @@ -255,6 +255,29 @@ func (k *FlagKey) TeamsOnIt() ([]int64, error) { } } +func (k *FlagKey) DeleteTries() error { + if rows, err := DBQuery("SELECT id_try FROM exercice_tries_flags WHERE id_flag = ?", k.Id); err != nil { + return err + } else { + defer rows.Close() + + for rows.Next() { + var idtry int64 + err = rows.Scan(&idtry) + if err != nil { + return err + } + + _, err = DBExec("DELETE FROM exercice_tries WHERE id_try = ?", idtry) + if err != nil { + return err + } + } + + return nil + } +} + // AddFlagKey creates and fills a new struct Flag, from a hashed flag, and registers it into the database. func (k *FlagKey) Create(e *Exercice) (Flag, error) { // Check the regexp compile diff --git a/libfic/flag_label.go b/libfic/flag_label.go index 79f2ba04..eec13522 100644 --- a/libfic/flag_label.go +++ b/libfic/flag_label.go @@ -79,6 +79,10 @@ func (k *FlagLabel) TeamsOnIt() ([]int64, error) { return nil, nil } +func (k *FlagLabel) DeleteTries() error { + return nil +} + // AddFlagLabel creates and fills a new struct Flag and registers it into the database. func (k *FlagLabel) Create(e *Exercice) (Flag, error) { if res, err := DBExec("INSERT INTO exercice_flag_labels (id_exercice, ordre, label, variant) VALUES (?, ?, ?, ?)", e.Id, k.Order, k.Label, k.Variant); err != nil { diff --git a/libfic/mcq.go b/libfic/mcq.go index 0b3ad66b..3129ec80 100644 --- a/libfic/mcq.go +++ b/libfic/mcq.go @@ -161,6 +161,29 @@ func (m *MCQ) TeamsOnIt() ([]int64, error) { } } +func (m *MCQ) DeleteTries() error { + if rows, err := DBQuery("SELECT id_try FROM exercice_tries_mcq WHERE id_mcq = ?", m.Id); err != nil { + return err + } else { + defer rows.Close() + + for rows.Next() { + var idtry int64 + err = rows.Scan(&idtry) + if err != nil { + return err + } + + _, err = DBExec("DELETE FROM exercice_tries WHERE id_try = ?", idtry) + if err != nil { + return err + } + } + + return nil + } +} + // Create registers a MCQ into the database and recursively add its entries. func (m *MCQ) Create(e *Exercice) (Flag, error) { if res, err := DBExec("INSERT INTO exercice_mcq (id_exercice, ordre, title) VALUES (?, ?, ?)", e.Id, m.Order, m.Title); err != nil {