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 @@