diff --git a/Dockerfile-repochecker b/Dockerfile-repochecker index 8b30365e..df49d0cb 100644 --- a/Dockerfile-repochecker +++ b/Dockerfile-repochecker @@ -23,7 +23,7 @@ RUN go get -d -v ./repochecker && \ ENV GRAMMALECTE_VERSION 2.1.1 -ADD https://grammalecte.net/zip/Grammalecte-fr-v$GRAMMALECTE_VERSION.zip /srv/grammalecte.zip +ADD https://web.archive.org/web/20240926154729if_/https://grammalecte.net/zip/Grammalecte-fr-v$GRAMMALECTE_VERSION.zip /srv/grammalecte.zip RUN mkdir /srv/grammalecte && cd /srv/grammalecte && unzip /srv/grammalecte.zip && sed -i 's/if sys.version_info.major < (3, 7):/if False:/' /srv/grammalecte/grammalecte-server.py diff --git a/admin/api/exercice.go b/admin/api/exercice.go index cfdb450d..1df894bd 100644 --- a/admin/api/exercice.go +++ b/admin/api/exercice.go @@ -62,6 +62,8 @@ func declareExercicesRoutes(router *gin.RouterGroup) { apiFlagsRoutes.POST("/try", tryExerciceFlag) 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) @@ -77,6 +79,8 @@ func declareExercicesRoutes(router *gin.RouterGroup) { apiQuizRoutes.PUT("", updateExerciceQuiz) apiQuizRoutes.DELETE("", deleteExerciceQuiz) apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps) + apiQuizRoutes.GET("/statistics", showExerciceQuizStats) + apiQuizRoutes.DELETE("/tries", deleteExerciceQuizTries) apiExercicesRoutes.GET("/tags", listExerciceTags) apiExercicesRoutes.POST("/tags", addExerciceTag) @@ -852,6 +856,60 @@ func showExerciceFlagDeps(c *gin.Context) { c.JSON(http.StatusOK, deps) } +func showExerciceFlagStats(c *gin.Context) { + exercice := c.MustGet("exercice").(*fic.Exercice) + flag := c.MustGet("flag-key").(*fic.FlagKey) + + 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) == "flag_found" { + if *hline["secondary"].(*int) == flag.Id { + completed += 1 + } + } + } + + 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, + "teams": teams, + }) +} + +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) @@ -1022,6 +1080,60 @@ 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 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/api/file.go b/admin/api/file.go index b2ca7973..6d94776c 100644 --- a/admin/api/file.go +++ b/admin/api/file.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "os" "path/filepath" "strconv" @@ -143,12 +144,27 @@ func listFiles(c *gin.Context) { } func clearFiles(c *gin.Context) { - _, err := fic.ClearFiles() + err := os.RemoveAll(fic.FilesDir) if err != nil { + log.Println("Unable to remove files:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) return } + err = os.MkdirAll(fic.FilesDir, 0751) + if err != nil { + log.Println("Unable to create FILES:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + _, err = fic.ClearFiles() + if err != nil { + log.Println("Unable to clean DB files:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Les fichiers ont bien été effacés. Mais il n'a pas été possible d'effacer la base de données. Refaites une synchronisation maintenant. " + err.Error()}) + return + } + c.JSON(http.StatusOK, true) } diff --git a/admin/api/repositories.go b/admin/api/repositories.go index 70986aba..9afaea5d 100644 --- a/admin/api/repositories.go +++ b/admin/api/repositories.go @@ -41,5 +41,27 @@ func declareRepositoriesRoutes(router *gin.RouterGroup) { } c.JSON(http.StatusOK, mod) }) + + router.DELETE("/repositories/*repopath", func(c *gin.Context) { + di, ok := sync.GlobalImporter.(sync.DeletableImporter) + if !ok { + c.AbortWithStatusJSON(http.StatusNotImplemented, gin.H{"errmsg": "Not implemented"}) + return + } + + if strings.Contains(c.Param("repopath"), "..") { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Repopath contains invalid characters"}) + return + } + + repopath := strings.TrimPrefix(c.Param("repopath"), "/") + + err := di.DeleteDir(repopath) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + c.JSON(http.StatusOK, true) + }) } } diff --git a/admin/api/settings.go b/admin/api/settings.go index 60065c3c..0ea7f0ea 100644 --- a/admin/api/settings.go +++ b/admin/api/settings.go @@ -8,7 +8,6 @@ import ( "net/http" "os" "path" - "reflect" "strconv" "time" @@ -26,7 +25,6 @@ func declareSettingsRoutes(router *gin.RouterGroup) { router.GET("/challenge.json", getChallengeInfo) router.PUT("/challenge.json", saveChallengeInfo) - router.GET("/settings-ro.json", getROSettings) router.GET("/settings.json", getSettings) router.PUT("/settings.json", saveSettings) router.DELETE("/settings.json", func(c *gin.Context) { @@ -98,24 +96,6 @@ func fullGeneration(c *gin.Context) { }) } -func getROSettings(c *gin.Context) { - syncMtd := "Disabled" - if sync.GlobalImporter != nil { - syncMtd = sync.GlobalImporter.Kind() - } - - var syncId *string - if sync.GlobalImporter != nil { - syncId = sync.GlobalImporter.Id() - } - - c.JSON(http.StatusOK, gin.H{ - "sync-type": reflect.TypeOf(sync.GlobalImporter).Name(), - "sync-id": syncId, - "sync": syncMtd, - }) -} - func GetChallengeInfo() (*settings.ChallengeInfo, error) { var challengeinfo string var err error diff --git a/admin/api/sync.go b/admin/api/sync.go index 113524c8..0bcba713 100644 --- a/admin/api/sync.go +++ b/admin/api/sync.go @@ -7,6 +7,7 @@ import ( "net/url" "os" "path" + "reflect" "strings" "srs.epita.fr/fic-server/admin/generation" @@ -17,6 +18,8 @@ import ( "go.uber.org/multierr" ) +var lastSyncError = "" + func flatifySyncErrors(errs error) (ret []string) { for _, err := range multierr.Errors(errs) { ret = append(ret, err.Error()) @@ -27,12 +30,37 @@ func flatifySyncErrors(errs error) (ret []string) { func declareSyncRoutes(router *gin.RouterGroup) { apiSyncRoutes := router.Group("/sync") + // Return the global sync status + apiSyncRoutes.GET("/status", func(c *gin.Context) { + syncMtd := "Disabled" + if sync.GlobalImporter != nil { + syncMtd = sync.GlobalImporter.Kind() + } + + var syncId *string + if sync.GlobalImporter != nil { + syncId = sync.GlobalImporter.Id() + } + + c.JSON(http.StatusOK, gin.H{ + "sync-type": reflect.TypeOf(sync.GlobalImporter).Name(), + "sync-id": syncId, + "sync": syncMtd, + "pullMutex": !sync.OneGitPullStatus(), + "syncMutex": !sync.OneDeepSyncStatus() && !sync.OneThemeDeepSyncStatus(), + "progress": sync.DeepSyncProgress, + "lastError": lastSyncError, + }) + }) + // Base sync checks if the local directory is in sync with remote one. apiSyncRoutes.POST("/base", func(c *gin.Context) { err := sync.GlobalImporter.Sync() if err != nil { + lastSyncError = err.Error() c.JSON(http.StatusExpectationFailed, gin.H{"errmsg": err.Error()}) } else { + lastSyncError = "" c.JSON(http.StatusOK, true) } }) @@ -45,15 +73,10 @@ func declareSyncRoutes(router *gin.RouterGroup) { }) // Deep sync: a fully recursive synchronization (can be limited by theme). - apiSyncRoutes.GET("/deep", func(c *gin.Context) { - if sync.DeepSyncProgress == 0 { - c.AbortWithStatusJSON(http.StatusTooEarly, gin.H{"errmsg": "Pas de synchronisation en cours"}) - return - } - c.JSON(http.StatusOK, gin.H{"progress": sync.DeepSyncProgress}) - }) apiSyncRoutes.POST("/deep", func(c *gin.Context) { - c.JSON(http.StatusOK, sync.SyncDeep(sync.GlobalImporter)) + r := sync.SyncDeep(sync.GlobalImporter) + lastSyncError = "" + c.JSON(http.StatusOK, r) }) apiSyncDeepRoutes := apiSyncRoutes.Group("/deep/:thid") @@ -66,6 +89,7 @@ func declareSyncRoutes(router *gin.RouterGroup) { } sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false) sync.DeepSyncProgress = 255 + lastSyncError = "" c.JSON(http.StatusOK, st) }) apiSyncDeepRoutes.POST("", func(c *gin.Context) { @@ -79,6 +103,7 @@ func declareSyncRoutes(router *gin.RouterGroup) { } sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false) sync.DeepSyncProgress = 255 + lastSyncError = "" c.JSON(http.StatusOK, st) }) @@ -90,6 +115,7 @@ func declareSyncRoutes(router *gin.RouterGroup) { apiSyncRoutes.POST("/themes", func(c *gin.Context) { _, errs := sync.SyncThemes(sync.GlobalImporter) + lastSyncError = "" c.JSON(http.StatusOK, flatifySyncErrors(errs)) }) @@ -258,10 +284,12 @@ func autoSync(c *gin.Context) { if !IsProductionEnv { if err := sync.GlobalImporter.Sync(); err != nil { + lastSyncError = err.Error() log.Println("Unable to sync.GI.Sync:", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to perform the pull."}) return } + lastSyncError = "" } themes, err := fic.GetThemes() diff --git a/admin/static/js/app.js b/admin/static/js/app.js index 0039770e..24e7b36b 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -230,9 +230,6 @@ angular.module("FICApp") .factory("File", function ($resource) { return $resource("api/files/:fileId", { fileId: '@id' }) }) - .factory("ROSettings", function ($resource) { - return $resource("api/settings-ro.json") - }) .factory("Settings", function ($resource) { return $resource("api/settings.json", null, { 'update': { method: 'PUT' }, @@ -351,6 +348,9 @@ angular.module("FICApp") .factory("ExerciceFlagDeps", function ($resource) { return $resource("api/exercices/:exerciceId/flags/:flagId/dependancies", { exerciceId: '@idExercice', flagId: '@id' }) }) + .factory("ExerciceFlagStats", function ($resource) { + return $resource("api/exercices/:exerciceId/flags/:flagId/statistics", { exerciceId: '@idExercice', flagId: '@id' }) + }) .factory("ExerciceMCQFlag", function ($resource) { return $resource("api/exercices/:exerciceId/quiz/:mcqId", { exerciceId: '@idExercice', mcqId: '@id' }, { update: { method: 'PUT' } @@ -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") @@ -614,9 +617,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) @@ -651,7 +663,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); }); @@ -725,9 +737,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("/"); @@ -739,7 +751,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() @@ -755,7 +767,27 @@ angular.module("FICApp") $http.get("api/repositories").then(function (response) { $scope.repositories = response.data.repositories; }); + + $scope.deleteRepository = function(repo) { + $http.delete("api/repositories/" + repo.path).then(function (response) { + $scope.repositories[$scope.repositories.indexOf(repo)].hash = "- DELETED -"; + }); + }; }) + .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: '<', @@ -781,9 +813,8 @@ angular.module("FICApp") template: `{{ $ctrl.status.hash }} {{ $ctrl.status.text }}` }) - .controller("SyncController", function ($scope, $rootScope, ROSettings, $location, $http, $interval) { + .controller("SyncController", function ($scope, $rootScope, $location, $http, $interval) { $scope.displayDangerousActions = false; - $scope.configro = ROSettings.get(); var needRefreshSyncReportWhenReady = false; var refreshSyncReport = function () { @@ -794,36 +825,34 @@ angular.module("FICApp") }; refreshSyncReport() + $scope.deepSyncInProgress = false; + var progressInterval = $interval(function () { - $http.get("api/sync/deep").then(function (response) { + $http.get("api/sync/status").then(function (response) { if (response.data.progress && response.data.progress != 255) needRefreshSyncReportWhenReady = true; else if (needRefreshSyncReportWhenReady) refreshSyncReport(); if (response.data && response.data.progress) { $scope.syncPercent = Math.floor(response.data.progress * 100 / 255); - $scope.syncProgress = Math.floor(response.data.progress * 100 / 255) + " %"; + $scope.deepSyncInProgress = response.data.pullMutex && response.data.syncMutex; } else { - $scope.syncProgress = response.data; $scope.syncPercent = 0; } + $scope.syncStatus = response.data; }, function (response) { $scope.syncPercent = 0; - if (response.data && response.data.errmsg) - $scope.syncProgress = response.data.errmsg; - else - $scope.syncProgress = response.data; + $scope.syncStatus = response.data; }) }, 1500); $scope.$on('$destroy', function () { $interval.cancel(progressInterval); }); - $scope.deepSyncInProgress = false; $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, '', @@ -839,7 +868,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 () { @@ -852,7 +881,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 () { @@ -865,7 +894,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 () { @@ -1110,7 +1139,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 = [ @@ -1229,8 +1258,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") { @@ -1238,8 +1267,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 !", } } }; @@ -1358,6 +1387,7 @@ angular.module("FICApp") $scope.files = File.query(); $scope.errfnd = null; $scope.errzip = null; + $scope.clearFilesWIP = false; $scope.fields = ["id", "path", "name", "size"]; $scope.clearFiles = function (id) { @@ -1366,6 +1396,21 @@ angular.module("FICApp") $scope.files = []; }); }; + $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.", + function () { + $scope.clearFilesWIP = true; + $http({ + url: "api/files", + method: "DELETE" + }).then(function (response) { + $scope.clearFilesWIP = false; + }, function (response) { + $scope.clearFilesWIP = false; + $scope.addToast('danger', 'An error occurs when trying to clear files:', response.data.errmsg); + }); + }); + }; $scope.gunzipFile = function (f) { f.gunzipWIP = true; $http({ @@ -2260,7 +2305,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); @@ -2329,6 +2374,18 @@ angular.module("FICApp") } }) + .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) { $scope.quiz = ExerciceMCQFlag.query({ exerciceId: $routeParams.exerciceId }); @@ -2366,6 +2423,18 @@ angular.module("FICApp") } }) + .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) { $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 3caf3add..f765d9a9 100644 --- a/admin/static/views/exercice-flags.html +++ b/admin/static/views/exercice-flags.html @@ -73,12 +73,28 @@
{{ syncStatus.lastError }}+ +