Able to sync and export standalone exercices
This commit is contained in:
parent
76f830b332
commit
adb0e36dd4
@ -113,6 +113,7 @@ func ExerciceHandler(c *gin.Context) {
|
||||
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."})
|
||||
@ -120,6 +121,9 @@ func ExerciceHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Set("theme", theme)
|
||||
} else {
|
||||
c.Set("theme", &fic.Theme{Path: sync.StandaloneExercicesDirectory})
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("exercice", exercice)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -1840,11 +1840,18 @@ angular.module("FICApp")
|
||||
url: "api/themes.json",
|
||||
method: "GET"
|
||||
}).then(function(response) {
|
||||
$scope.themes = response.data
|
||||
$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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
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)
|
||||
|
@ -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 {
|
||||
|
@ -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)})
|
||||
|
@ -175,9 +175,9 @@ func treatSubmission(pathname string, team *fic.Team, exercice_id string) {
|
||||
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeams})
|
||||
} else {
|
||||
if theme == nil {
|
||||
log.Printf("%s Team %d submit an invalid solution for exercice %d (%s : %s)\n", id, team.Id, exercice.Id, theme.Name, exercice.Title)
|
||||
} else {
|
||||
log.Printf("%s Team %d submit an invalid solution for exercice %d (%s)\n", id, team.Id, exercice.Id, exercice.Title)
|
||||
} else {
|
||||
log.Printf("%s Team %d submit an invalid solution for exercice %d (%s : %s)\n", id, team.Id, exercice.Id, theme.Name, exercice.Title)
|
||||
}
|
||||
|
||||
// Write event (only on first try)
|
||||
|
@ -91,17 +91,44 @@ func GetExercice(id int64) (*Exercice, error) {
|
||||
|
||||
// GetExercice retrieves the challenge with the given id.
|
||||
func (t *Theme) GetExercice(id int) (*Exercice, error) {
|
||||
return getExercice("exercices", "WHERE id_theme = ? AND id_exercice = ?", t.Id, id)
|
||||
query := "WHERE id_exercice = ? AND id_theme = ?"
|
||||
args := []interface{}{id}
|
||||
|
||||
if t.GetId() == nil {
|
||||
query = "WHERE id_exercice = ? AND id_theme IS NULL"
|
||||
} else {
|
||||
args = append(args, t.GetId())
|
||||
}
|
||||
|
||||
return getExercice("exercices", query, args...)
|
||||
}
|
||||
|
||||
// GetExerciceByTitle retrieves the challenge with the given title.
|
||||
func (t *Theme) GetExerciceByTitle(title string) (*Exercice, error) {
|
||||
return getExercice("exercices", "WHERE id_theme = ? AND title = ?", t.Id, title)
|
||||
query := "WHERE title = ? AND id_theme = ?"
|
||||
args := []interface{}{title}
|
||||
|
||||
if t.GetId() == nil {
|
||||
query = "WHERE title = ? AND id_theme IS NULL"
|
||||
} else {
|
||||
args = append(args, t.GetId())
|
||||
}
|
||||
|
||||
return getExercice("exercices", query, args...)
|
||||
}
|
||||
|
||||
// GetExerciceByPath retrieves the challenge with the given path.
|
||||
func (t *Theme) GetExerciceByPath(epath string) (*Exercice, error) {
|
||||
return getExercice("exercices", "WHERE id_theme = ? AND path = ?", t.Id, epath)
|
||||
query := "WHERE path = ? AND id_theme = ?"
|
||||
args := []interface{}{epath}
|
||||
|
||||
if t.GetId() == nil {
|
||||
query = "WHERE path = ? AND id_theme IS NULL"
|
||||
} else {
|
||||
args = append(args, t.GetId())
|
||||
}
|
||||
|
||||
return getExercice("exercices", query, args...)
|
||||
}
|
||||
|
||||
// GetDiscountedExercice retrieves the challenge with the given id.
|
||||
@ -153,7 +180,15 @@ func GetDiscountedExercices() ([]*Exercice, error) {
|
||||
|
||||
// GetExercices returns the list of all challenges in the Theme.
|
||||
func (t *Theme) GetExercices() ([]*Exercice, error) {
|
||||
if rows, err := DBQuery("SELECT id_exercice, id_theme, title, authors, image, disabled, url_id, path, statement, overview, headline, issue, issue_kind, depend, gain, coefficient_cur, video_uri, resolution, seealso, finished FROM exercices WHERE id_theme = ? ORDER BY path ASC", t.Id); err != nil {
|
||||
query := "SELECT id_exercice, id_theme, title, authors, image, disabled, url_id, path, statement, overview, headline, issue, issue_kind, depend, gain, coefficient_cur, video_uri, resolution, seealso, finished FROM exercices WHERE id_theme IS NULL ORDER BY path ASC"
|
||||
args := []interface{}{}
|
||||
|
||||
if t.GetId() != nil {
|
||||
query = "SELECT id_exercice, id_theme, title, authors, image, disabled, url_id, path, statement, overview, headline, issue, issue_kind, depend, gain, coefficient_cur, video_uri, resolution, seealso, finished FROM exercices WHERE id_theme = ? ORDER BY path ASC"
|
||||
args = append(args, t.GetId())
|
||||
}
|
||||
|
||||
if rows, err := DBQuery(query, args...); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer rows.Close()
|
||||
@ -224,7 +259,7 @@ func (t *Theme) addExercice(e *Exercice) (err error) {
|
||||
if e.WIP {
|
||||
wip = "%"
|
||||
}
|
||||
if res, err := DBExec("INSERT INTO exercices (id_theme, title, authors, image, disabled, url_id, path, statement, overview, finished, headline, issue, depend, gain, video_uri, resolution, seealso, issue_kind, coefficient_cur) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "+ik+", "+cc+")", t.Id, wip+e.Title, e.Authors, e.Image, e.Disabled, e.URLId, e.Path, e.Statement, e.Overview, e.Finished, e.Headline, e.Issue, e.Depend, e.Gain, e.VideoURI, e.Resolution, e.SeeAlso); err != nil {
|
||||
if res, err := DBExec("INSERT INTO exercices (id_theme, title, authors, image, disabled, url_id, path, statement, overview, finished, headline, issue, depend, gain, video_uri, resolution, seealso, issue_kind, coefficient_cur) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "+ik+", "+cc+")", t.GetId(), wip+e.Title, e.Authors, e.Image, e.Disabled, e.URLId, e.Path, e.Statement, e.Overview, e.Finished, e.Headline, e.Issue, e.Depend, e.Gain, e.VideoURI, e.Resolution, e.SeeAlso); err != nil {
|
||||
return err
|
||||
} else if eid, err := res.LastInsertId(); err != nil {
|
||||
return err
|
||||
|
@ -140,7 +140,9 @@ func MyJSONTeam(t *Team, started bool) (interface{}, error) {
|
||||
exercice := myTeamExercice{}
|
||||
exercice.Disabled = e.Disabled
|
||||
exercice.WIP = e.WIP
|
||||
if e.IdTheme != nil {
|
||||
exercice.ThemeId = *e.IdTheme
|
||||
}
|
||||
|
||||
exercice.Statement = strings.Replace(e.Statement, "$FILES$", FilesDir, -1)
|
||||
|
||||
|
@ -19,6 +19,14 @@ type Theme struct {
|
||||
PartnerText string `json:"partner_txt,omitempty"`
|
||||
}
|
||||
|
||||
func (t *Theme) GetId() *int64 {
|
||||
if t.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &t.Id
|
||||
}
|
||||
|
||||
// CmpTheme returns true if given Themes are identicals.
|
||||
func CmpTheme(t1 *Theme, t2 *Theme) bool {
|
||||
return t1 != nil && t2 != nil && !(t1.Name != t2.Name || t1.URLId != t2.URLId || t1.Path != t2.Path || t1.Authors != t2.Authors || t1.Intro != t2.Intro || t1.Headline != t2.Headline || t1.Image != t2.Image || t1.PartnerImage != t2.PartnerImage || t1.PartnerLink != t2.PartnerLink || t1.PartnerText != t2.PartnerText)
|
||||
|
@ -25,12 +25,12 @@ type exportedExercice struct {
|
||||
|
||||
// exportedTheme is a structure representing a Theme, as exposed to players.
|
||||
type exportedTheme struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name,omitempty"`
|
||||
URLId string `json:"urlid"`
|
||||
Locked bool `json:"locked,omitempty"`
|
||||
Authors string `json:"authors"`
|
||||
Authors string `json:"authors,omitempty"`
|
||||
Headline string `json:"headline,omitempty"`
|
||||
Intro string `json:"intro"`
|
||||
Intro string `json:"intro,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
PartnerImage string `json:"partner_img,omitempty"`
|
||||
PartnerLink string `json:"partner_href,omitempty"`
|
||||
@ -43,12 +43,17 @@ func ExportThemes() (interface{}, error) {
|
||||
if themes, err := GetThemes(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
themes = append(themes, &Theme{URLId: "_", Path: "exercices"})
|
||||
|
||||
ret := map[string]exportedTheme{}
|
||||
for _, theme := range themes {
|
||||
exos := []exportedExercice{}
|
||||
|
||||
if exercices, err := theme.GetExercices(); err != nil {
|
||||
return nil, err
|
||||
} else if theme.URLId == "_" && len(exercices) == 0 {
|
||||
// If no standalone exercices, don't append them
|
||||
continue
|
||||
} else {
|
||||
for _, exercice := range exercices {
|
||||
if exercice.Disabled && theme.Locked {
|
||||
|
Loading…
x
Reference in New Issue
Block a user