diff --git a/Dockerfile-repochecker b/Dockerfile-repochecker index 1cb14f26..8f74b1ad 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/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..0753bbb0 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' }, @@ -755,6 +752,12 @@ 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('repositoryUptodate', { bindings: { @@ -781,9 +784,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,30 +796,28 @@ 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 + ' ?' @@ -1358,6 +1358,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 +1367,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({ diff --git a/admin/static/views/file-list.html b/admin/static/views/file-list.html index 4153378f..cd0d36a7 100644 --- a/admin/static/views/file-list.html +++ b/admin/static/views/file-list.html @@ -4,6 +4,7 @@ +

diff --git a/admin/static/views/repositories.html b/admin/static/views/repositories.html index 3946b181..daca48fa 100644 --- a/admin/static/views/repositories.html +++ b/admin/static/views/repositories.html @@ -9,16 +9,20 @@ Chemin Branche - Commit - Plus récent + Commit Plus récent {{ repository.path }} {{ repository.branch }} - {{ repository.hash }} - + + {{ repository.hash }}
+ + + + + diff --git a/admin/static/views/sync.html b/admin/static/views/sync.html index 847c9969..106cc6c3 100644 --- a/admin/static/views/sync.html +++ b/admin/static/views/sync.html @@ -22,7 +22,7 @@
Dernier import : {{ syncReport._updated[syncReport._updated.length-1] | date:"medium" }}
- + Voir les dépôts @@ -32,17 +32,25 @@
Type
-
+
Synchronisation
-
-
ID
-
{{ configro['sync-id'] }}
-
Statut
-
{{ syncProgress }}
+
+
ID
+
+ {{ syncStatus['sync-id'] }} + +
+
Statut
+
+ {{ syncPercent }} % + Pull + Synchronisation +
-
- +
{{ syncStatus.lastError }}
+ +
diff --git a/qa/ui/src/routes/teams/+page.svelte b/qa/ui/src/routes/teams/+page.svelte index 3df8bb60..dfefe4c0 100644 --- a/qa/ui/src/routes/teams/+page.svelte +++ b/qa/ui/src/routes/teams/+page.svelte @@ -22,10 +22,12 @@ goto("teams/" + id) } - let start = 0; - let turns = 3; - let team_prefix = ""; - let team_assistants = ""; + let assignForm = { + start: 0, + turns: 3, + team_prefix: "", + team_assistants: "", + } let assignInProgress = false; async function assignExercices() { @@ -33,12 +35,7 @@ const res = await fetch(`api/qa_assign_work`, { method: 'POST', headers: {'Accept': 'application/json'}, - body: JSON.stringify({ - start, - turns, - team_prefix, - team_assistants, - }), + body: JSON.stringify(assignForm), }) if (res.status == 200) { teams.refresh(); @@ -111,23 +108,26 @@
- +

Incrémenter de 1 pour chaque nouveau challenge blanc, cela décale l'attribution des exercices.

- + - + - + + + +