diff --git a/Dockerfile-admin b/Dockerfile-admin index fabc84a5..d43c2888 100644 --- a/Dockerfile-admin +++ b/Dockerfile-admin @@ -20,7 +20,7 @@ RUN go get -d -v ./admin && \ go build -v -buildmode=plugin -o repochecker/videos-rules.so ./repochecker/videos -FROM alpine:3.20 +FROM alpine:3.21 RUN apk add --no-cache \ ca-certificates \ diff --git a/Dockerfile-checker b/Dockerfile-checker index 9f2eb06c..1343fbd2 100644 --- a/Dockerfile-checker +++ b/Dockerfile-checker @@ -13,7 +13,7 @@ RUN go get -d -v ./checker && \ go build -v -buildvcs=false -o checker/checker ./checker -FROM alpine:3.20 +FROM alpine:3.21 WORKDIR /srv diff --git a/Dockerfile-dashboard b/Dockerfile-dashboard index a94c85ec..7c24e279 100644 --- a/Dockerfile-dashboard +++ b/Dockerfile-dashboard @@ -13,7 +13,7 @@ RUN go get -d -v ./dashboard && \ go build -v -buildvcs=false -o dashboard/dashboard ./dashboard -FROM alpine:3.20 +FROM alpine:3.21 EXPOSE 8082 diff --git a/Dockerfile-deploy b/Dockerfile-deploy index c6d672fc..99765543 100644 --- a/Dockerfile-deploy +++ b/Dockerfile-deploy @@ -1,4 +1,4 @@ -FROM alpine:3.20 +FROM alpine:3.21 EXPOSE 67/udp EXPOSE 69/udp diff --git a/Dockerfile-evdist b/Dockerfile-evdist index 3c42fc6d..1b9bffec 100644 --- a/Dockerfile-evdist +++ b/Dockerfile-evdist @@ -12,7 +12,7 @@ RUN go get -d -v ./evdist && \ go build -v -buildvcs=false -o evdist/evdist ./evdist -FROM alpine:3.20 +FROM alpine:3.21 WORKDIR /srv diff --git a/Dockerfile-generator b/Dockerfile-generator index 3ff63ecf..6b4c6a1d 100644 --- a/Dockerfile-generator +++ b/Dockerfile-generator @@ -13,7 +13,7 @@ RUN go get -d -v ./generator && \ go build -v -buildvcs=false -o generator/generator ./generator -FROM alpine:3.20 +FROM alpine:3.21 WORKDIR /srv diff --git a/Dockerfile-get-remote-files b/Dockerfile-get-remote-files index 19f88472..6a71caa4 100644 --- a/Dockerfile-get-remote-files +++ b/Dockerfile-get-remote-files @@ -15,7 +15,7 @@ RUN go get -d -v ./admin && \ go build -v -o get-remote-files ./admin/get-remote-files -FROM alpine:3.20 +FROM alpine:3.21 RUN apk add --no-cache \ ca-certificates diff --git a/Dockerfile-qa b/Dockerfile-qa index eeae2953..7e2e6488 100644 --- a/Dockerfile-qa +++ b/Dockerfile-qa @@ -25,7 +25,7 @@ RUN go get -d -v ./qa && \ go build -v -buildvcs=false -o qa/qa ./qa -FROM alpine:3.20 +FROM alpine:3.21 EXPOSE 8083 diff --git a/Dockerfile-receiver b/Dockerfile-receiver index 52d98620..49d15871 100644 --- a/Dockerfile-receiver +++ b/Dockerfile-receiver @@ -13,7 +13,7 @@ RUN go get -d -v ./receiver && \ go build -v -buildvcs=false -o ./receiver/receiver ./receiver -FROM alpine:3.20 +FROM alpine:3.21 EXPOSE 8080 diff --git a/Dockerfile-remote-challenge-sync-airbus b/Dockerfile-remote-challenge-sync-airbus index f5dfeca2..516dd6d1 100644 --- a/Dockerfile-remote-challenge-sync-airbus +++ b/Dockerfile-remote-challenge-sync-airbus @@ -13,7 +13,7 @@ RUN go get -d -v ./remote/challenge-sync-airbus && \ go build -v -buildvcs=false -o ./challenge-sync-airbus ./remote/challenge-sync-airbus -FROM alpine:3.20 +FROM alpine:3.21 RUN apk add --no-cache openssl ca-certificates diff --git a/Dockerfile-remote-scores-sync-zqds b/Dockerfile-remote-scores-sync-zqds index 772f7f53..1a5c68f8 100644 --- a/Dockerfile-remote-scores-sync-zqds +++ b/Dockerfile-remote-scores-sync-zqds @@ -13,7 +13,7 @@ RUN go get -d -v ./remote/scores-sync-zqds && \ go build -v -buildvcs=false -o ./scores-sync-zqds ./remote/scores-sync-zqds -FROM alpine:3.20 +FROM alpine:3.21 RUN apk add --no-cache openssl ca-certificates diff --git a/Dockerfile-repochecker b/Dockerfile-repochecker index 1cb14f26..df49d0cb 100644 --- a/Dockerfile-repochecker +++ b/Dockerfile-repochecker @@ -23,11 +23,11 @@ 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 -FROM alpine:3.19 +FROM alpine:3.21 ENTRYPOINT ["/usr/bin/repochecker", "--rules-plugins=/usr/lib/epita-rules.so", "--rules-plugins=/usr/lib/file-inspector.so", "--rules-plugins=/usr/lib/grammalecte-rules.so", "--rules-plugins=/usr/lib/pcap-inspector.so", "--rules-plugins=/usr/lib/videos-rules.so"] 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 }}+ +