Able to sync and export standalone exercices

This commit is contained in:
nemunaire 2024-03-15 17:46:50 +01:00
commit adb0e36dd4
15 changed files with 159 additions and 31 deletions

View file

@ -113,13 +113,17 @@ func ExerciceHandler(c *gin.Context) {
return
}
theme, err = exercice.GetTheme()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to find the attached theme."})
return
}
if exercice.IdTheme != nil {
theme, err = exercice.GetTheme()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to find the attached theme."})
return
}
c.Set("theme", theme)
c.Set("theme", theme)
} else {
c.Set("theme", &fic.Theme{Path: sync.StandaloneExercicesDirectory})
}
}
c.Set("exercice", exercice)

View file

@ -58,6 +58,16 @@ func declareSyncRoutes(router *gin.RouterGroup) {
apiSyncDeepRoutes := apiSyncRoutes.Group("/deep/:thid")
apiSyncDeepRoutes.Use(ThemeHandler)
// Special route to handle standalone exercices
apiSyncRoutes.POST("/deep/0", func(c *gin.Context) {
var st []string
for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, &fic.Theme{Path: sync.StandaloneExercicesDirectory}, 0, 250, nil)) {
st = append(st, se.Error())
}
sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false)
sync.DeepSyncProgress = 255
c.JSON(http.StatusOK, st)
})
apiSyncDeepRoutes.POST("", func(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)

View file

@ -1840,11 +1840,18 @@ angular.module("FICApp")
url: "api/themes.json",
method: "GET"
}).then(function(response) {
$scope.themes = response.data
for (var k in $scope.themes[$scope.exercice.id_theme].exercices) {
var exercice = $scope.themes[$scope.exercice.id_theme].exercices[k];
$scope.my_ex_num[exercice.id] = k;
}
$scope.themes = response.data;
if ($scope.exercice.id_theme) {
for (var k in $scope.themes[$scope.exercice.id_theme].exercices) {
var exercice = $scope.themes[$scope.exercice.id_theme].exercices[k];
$scope.my_ex_num[exercice.id] = k;
}
} else {
for (var k in $scope.themes["0"].exercices) {
var exercice = $scope.themes["0"].exercices[k];
$scope.my_ex_num[exercice.id] = k;
}
}
});
$scope.exercices = Exercice.query();
$scope.fields = ["title", "urlid", "authors", "disabled", "statement", "headline", "overview", "finished", "depend", "gain", "coefficient", "videoURI", "image", "resolution", "issue", "issuekind", "wip"];
@ -1853,7 +1860,7 @@ angular.module("FICApp")
$scope.syncExo = function() {
$scope.inSync = true;
$http({
url: "api/sync/themes/" + $scope.exercice.id_theme + "/exercices/" + $routeParams.exerciceId,
url: $scope.exercice.id_theme?("api/sync/themes/" + $scope.exercice.id_theme + "/exercices/" + $routeParams.exerciceId):("api/sync/exercices/" + $routeParams.exerciceId),
method: "POST"
}).then(function(response) {
$scope.inSync = false;

View file

@ -3,9 +3,9 @@
{{exercice.title}}
<small ng-if="themes && themes[exercice.id_theme]"><a href="themes/{{ exercice.id_theme }}" title="{{themes[exercice.id_theme].authors | stripHTML}}">{{themes[exercice.id_theme].name}}</a></small>
</h2>
<div class="btn-group" role="group" ng-if="themes[exercice.id_theme].exercices">
<a href="exercices/{{ themes[exercice.id_theme].exercices[my_ex_num[exercice.id]-1].id }}" title="Exercice précédent" ng-class="{'disabled': !themes[exercice.id_theme].exercices[my_ex_num[exercice.id]-1]}" class="btn btn-sm btn-light"><span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span></a>
<a href="exercices/{{ themes[exercice.id_theme].exercices[my_ex_num[exercice.id]-1+2].id }}" title="Exercice suivant" ng-class="{'disabled': !themes[exercice.id_theme].exercices[my_ex_num[exercice.id]-1+2]}" class="btn btn-sm btn-light"><span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span></a>
<div class="btn-group" role="group" ng-if="themes[exercice.id_theme?exercice.id_theme:'0'].exercices">
<a href="exercices/{{ themes[exercice.id_theme?exercice.id_theme:'0'].exercices[my_ex_num[exercice.id]-1].id }}" title="Exercice précédent" ng-class="{'disabled': !themes[exercice.id_theme?exercice.id_theme:'0'].exercices[my_ex_num[exercice.id]-1]}" class="btn btn-sm btn-light"><span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span></a>
<a href="exercices/{{ themes[exercice.id_theme?exercice.id_theme:'0'].exercices[my_ex_num[exercice.id]-1+2].id }}" title="Exercice suivant" ng-class="{'disabled': !themes[exercice.id_theme?exercice.id_theme:'0'].exercices[my_ex_num[exercice.id]-1+2]}" class="btn btn-sm btn-light"><span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span></a>
</div>
<div class="ml-auto d-flex flex-row-reverse text-nowrap">
<a href="exercices/{{exercice.id}}/resolution" ng-disabled="!exercice.videoURI" class="ml-2 btn btn-sm btn-info"><span class="glyphicon glyphicon-facetime-video" aria-hidden="true"></span> Vidéo</a>

View file

@ -50,6 +50,7 @@
</button>
<div class="dropdown-menu" ng-controller="ThemesListController" style="max-height: 45vh; overflow: auto">
<a class="dropdown-item" ng-click="deepSync(theme)" ng-repeat="theme in themes" ng-bind="theme.name"></a>
<a class="dropdown-item" ng-click="deepSync({name: 'Exercices indépendants', id: 0})">Exercices indépendants</a>
</div>
</div>
<button type="button" class="btn btn-secondary" ng-click="speedyDeepSync()" ng-disabled="deepSyncInProgress"><span class="glyphicon glyphicon-import" aria-hidden="true"></span> Synchronisation sans fichiers</button>

View file

@ -20,6 +20,13 @@ type ThemeError struct {
}
func NewThemeError(theme *fic.Theme, err error) *ThemeError {
if theme == nil {
return &ThemeError{
error: err,
ThemePath: StandaloneExercicesDirectory,
}
}
return &ThemeError{
error: err,
ThemeId: theme.Id,

View file

@ -100,6 +100,10 @@ func ParseExceptionString(fexcept string, exceptions *CheckExceptions) *CheckExc
}
func LoadThemeException(i Importer, th *fic.Theme) (exceptions *CheckExceptions) {
if th == nil {
return
}
if fexcept, err := GetFileContent(i, filepath.Join(th.Path, "repochecker-ack.txt")); err == nil {
return ParseExceptionString(fexcept, nil)
}

View file

@ -114,7 +114,9 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
exceptions = LoadExerciceException(i, theme, e, exceptions_in)
//log.Printf("Kept repochecker exceptions for this exercice: %v", exceptions)
e.Language = theme.Language
if theme != nil {
e.Language = theme.Language
}
// Overwrite language if language.txt exists
if language, err := GetFileContent(i, path.Join(epath, "language.txt")); err == nil {
language = strings.TrimSpace(language)
@ -251,7 +253,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
e.Image = path.Join(epath, "heading.jpg")
} else if i.Exists(path.Join(epath, "heading.png")) {
e.Image = path.Join(epath, "heading.png")
} else if theme.Image == "" {
} else if theme == nil || theme.Image == "" {
errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("heading.jpg: No such file")))
}
@ -283,7 +285,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
// Handle dependency
if len(p.Dependencies) > 0 {
if len(p.Dependencies[0].Theme) > 0 && p.Dependencies[0].Theme != theme.Name {
if len(p.Dependencies[0].Theme) > 0 && (theme == nil || p.Dependencies[0].Theme != theme.Name) {
errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("unable to treat dependency to another theme (%q): not implemented.", p.Dependencies[0].Theme), theme))
} else {
if dmap == nil {
@ -455,6 +457,17 @@ func SyncExercices(i Importer, theme *fic.Theme, exceptions *CheckExceptions) (e
// ApiListRemoteExercices is an accessor letting foreign packages to access remote exercices list.
func ApiListRemoteExercices(c *gin.Context) {
if c.Params.ByName("thid") == "_" {
exercices, err := GetExercices(GlobalImporter, &fic.Theme{Path: StandaloneExercicesDirectory})
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, exercices)
return
}
theme, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercices, err := GetExercices(GlobalImporter, theme)
@ -472,6 +485,17 @@ func ApiListRemoteExercices(c *gin.Context) {
// ApiListRemoteExercice is an accessor letting foreign packages to access remote exercice attributes.
func ApiGetRemoteExercice(c *gin.Context) {
if c.Params.ByName("thid") == "_" {
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, nil, path.Join(StandaloneExercicesDirectory, c.Params.ByName("exid")), nil, nil)
if exercice != nil {
c.JSON(http.StatusOK, exercice)
return
} else {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
}
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)

View file

@ -32,6 +32,7 @@ type SyncReport struct {
SyncId string `json:"_id,omitempty"`
ThemesSync []string `json:"_themes,omitempty"`
Themes map[string][]string `json:"themes"`
Exercices []string `json:"exercices,omitempty"`
}
// SpeedySyncDeep performs a recursive synchronisation without importing files.
@ -57,6 +58,11 @@ func SpeedySyncDeep(i Importer) (errs SyncReport) {
if themes, err := fic.GetThemes(); err == nil {
DeepSyncProgress = 2
if i.Exists(StandaloneExercicesDirectory) {
themes = append([]*fic.Theme{}, &fic.Theme{Path: StandaloneExercicesDirectory})
}
var themeStep uint8 = uint8(250) / uint8(len(themes))
for tid, theme := range themes {
@ -113,14 +119,22 @@ func SyncDeep(i Importer) (errs SyncReport) {
startTime := time.Now()
// Import all themes
errs.DateStart = startTime
exceptions, sterrs := SyncThemes(i)
for _, sterr := range multierr.Errors(sterrs) {
errs.ThemesSync = append(errs.ThemesSync, sterr.Error())
}
if themes, err := fic.GetThemes(); err == nil && len(themes) > 0 {
// Synchronize themes
if themes, err := fic.GetThemes(); err == nil {
DeepSyncProgress = 2
// Also synchronize standalone exercices
if i.Exists(StandaloneExercicesDirectory) {
themes = append(themes, &fic.Theme{Path: StandaloneExercicesDirectory})
}
var themeStep uint8 = uint8(250) / uint8(len(themes))
for tid, theme := range themes {

View file

@ -21,13 +21,15 @@ import (
"srs.epita.fr/fic-server/libfic"
)
const StandaloneExercicesDirectory = "exercices"
// GetThemes returns all theme directories in the base directory.
func GetThemes(i Importer) (themes []string, err error) {
if dirs, err := i.ListDir("/"); err != nil {
return nil, err
} else {
for _, dir := range dirs {
if !strings.HasPrefix(dir, ".") && !strings.HasPrefix(dir, "_") {
if !strings.HasPrefix(dir, ".") && !strings.HasPrefix(dir, "_") && dir != StandaloneExercicesDirectory {
if _, err := i.ListDir(dir); err == nil {
themes = append(themes, dir)
}
@ -310,6 +312,11 @@ func ApiListRemoteThemes(c *gin.Context) {
// ApiListRemoteTheme is an accessor letting foreign packages to access remote main theme attributes.
func ApiGetRemoteTheme(c *gin.Context) {
if c.Params.ByName("thid") == "_" {
c.Status(http.StatusNoContent)
return
}
r, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if r == nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})