Compare commits

...

9 commits

31 changed files with 963 additions and 187 deletions

View file

@ -1,9 +1,11 @@
package api
import (
"bytes"
"fmt"
"log"
"net/http"
"path"
"reflect"
"strconv"
"strings"
@ -32,6 +34,8 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
apiExercicesRoutes.PATCH("", partUpdateExercice)
apiExercicesRoutes.DELETE("", deleteExercice)
apiExercicesRoutes.POST("/diff-sync", APIDiffExerciceWithRemote)
apiExercicesRoutes.GET("/stats.json", getExerciceStats)
apiExercicesRoutes.GET("/history.json", getExerciceHistory)
@ -91,8 +95,8 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
// Remote
router.GET("/remote/themes/:thid/exercices/:exid", sync.ApiGetRemoteExercice)
router.GET("/remote/themes/:thid/exercices/:exid/hints", sync.ApiGetRemoteExerciceHints)
router.GET("/remote/themes/:thid/exercices/:exid/flags", sync.ApiGetRemoteExerciceFlags)
router.GET("/remote/themes/:thid/exercices/:exid/hints", sync.ApiGetRemoteExerciceHints)
}
type Exercice struct {
@ -130,7 +134,7 @@ func ExerciceHandler(c *gin.Context) {
c.Set("theme", theme)
} else {
c.Set("theme", &fic.Theme{Path: sync.StandaloneExercicesDirectory})
c.Set("theme", &fic.StandaloneExercicesTheme)
}
}
@ -1275,3 +1279,343 @@ func updateExerciceTags(c *gin.Context) {
exercice.WipeTags()
addExerciceTag(c)
}
type syncDiff struct {
Field string `json:"field"`
Link string `json:"link"`
Before interface{} `json:"be"`
After interface{} `json:"af"`
}
func diffExerciceWithRemote(exercice *fic.Exercice, theme *fic.Theme) ([]syncDiff, error) {
var diffs []syncDiff
// Compare exercice attributes
thid := exercice.Path[:strings.Index(exercice.Path, "/")]
exid := exercice.Path[strings.Index(exercice.Path, "/")+1:]
exercice_remote, err := sync.GetRemoteExercice(thid, exid, theme)
if err != nil {
return nil, err
}
for _, field := range reflect.VisibleFields(reflect.TypeOf(*exercice)) {
if ((field.Name == "Image") && path.Base(reflect.ValueOf(*exercice_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*exercice).FieldByName(field.Name).String())) || ((field.Name == "Depend") && (((exercice_remote.Depend == nil || exercice.Depend == nil) && exercice.Depend != exercice_remote.Depend) || (exercice_remote.Depend != nil && exercice.Depend != nil && *exercice.Depend != *exercice_remote.Depend))) || (field.Name != "Image" && field.Name != "Depend" && !reflect.ValueOf(*exercice_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*exercice).FieldByName(field.Name))) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdTheme" || field.Name == "IssueKind" || field.Name == "Coefficient" || field.Name == "BackgroundColor" {
continue
}
diffs = append(diffs, syncDiff{
Field: field.Name,
Link: fmt.Sprintf("exercices/%d", exercice.Id),
Before: reflect.ValueOf(*exercice).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*exercice_remote).FieldByName(field.Name).Interface(),
})
}
}
// Compare files
files, err := exercice.GetFiles()
if err != nil {
return nil, fmt.Errorf("Unable to GetFiles: %w", err)
}
files_remote, err := sync.GetRemoteExerciceFiles(thid, exid)
if err != nil {
return nil, fmt.Errorf("Unable to GetRemoteFiles: %w", err)
}
for i, file_remote := range files_remote {
if len(files) <= i {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("files[%d]", i),
Link: fmt.Sprintf("exercices/%d", exercice.Id),
Before: nil,
After: file_remote,
})
continue
}
for _, field := range reflect.VisibleFields(reflect.TypeOf(*file_remote)) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
continue
}
if ((field.Name == "Path") && path.Base(reflect.ValueOf(*file_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*files[i]).FieldByName(field.Name).String())) || ((field.Name == "Checksum" || field.Name == "ChecksumShown") && !bytes.Equal(reflect.ValueOf(*file_remote).FieldByName(field.Name).Bytes(), reflect.ValueOf(*files[i]).FieldByName(field.Name).Bytes())) || (field.Name != "Checksum" && field.Name != "ChecksumShown" && field.Name != "Path" && !reflect.ValueOf(*file_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*files[i]).FieldByName(field.Name))) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("files[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d", exercice.Id),
Before: reflect.ValueOf(*files[i]).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*file_remote).FieldByName(field.Name).Interface(),
})
}
}
}
// Compare flags
flags, err := exercice.GetFlags()
if err != nil {
return nil, fmt.Errorf("Unable to GetFlags: %w", err)
}
flags_remote, err := sync.GetRemoteExerciceFlags(thid, exid)
if err != nil {
return nil, fmt.Errorf("Unable to GetRemoteFlags: %w", err)
}
var flags_not_found []interface{}
var flags_extra_found []interface{}
for i, flag_remote := range flags_remote {
if key_remote, ok := flag_remote.(*fic.FlagKey); ok {
found := false
for _, flag := range flags {
if key, ok := flag.(*fic.FlagKey); ok && (key.Label == key_remote.Label || key.Order == key_remote.Order) {
found = true
// Parse flag label
if len(key.Label) > 3 && key.Label[0] == '%' {
spl := strings.Split(key.Label, "%")
key.Label = strings.Join(spl[2:], "%")
}
for _, field := range reflect.VisibleFields(reflect.TypeOf(*key_remote)) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
continue
}
if (field.Name == "Checksum" && !bytes.Equal(key.Checksum, key_remote.Checksum)) || (field.Name == "CaptureRegexp" && ((key.CaptureRegexp == nil || key_remote.CaptureRegexp == nil) && key.CaptureRegexp != key_remote.CaptureRegexp) || (key.CaptureRegexp != nil && key_remote.CaptureRegexp != nil && *key.CaptureRegexp != *key_remote.CaptureRegexp)) || (field.Name != "Checksum" && field.Name != "CaptureRegexp" && !reflect.ValueOf(*key_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*key).FieldByName(field.Name))) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d/flags#flag-%d", exercice.Id, key.Id),
Before: reflect.ValueOf(*key).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*key_remote).FieldByName(field.Name).Interface(),
})
}
}
break
}
}
if !found {
flags_not_found = append(flags_not_found, key_remote)
}
} else if mcq_remote, ok := flag_remote.(*fic.MCQ); ok {
found := false
for _, flag := range flags {
if mcq, ok := flag.(*fic.MCQ); ok && (mcq.Title == mcq_remote.Title || mcq.Order == mcq_remote.Order) {
found = true
for _, field := range reflect.VisibleFields(reflect.TypeOf(*mcq_remote)) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
continue
}
if field.Name == "Entries" {
var not_found []*fic.MCQ_entry
var extra_found []*fic.MCQ_entry
for i, entry_remote := range mcq_remote.Entries {
found := false
for j, entry := range mcq.Entries {
if entry.Label == entry_remote.Label {
for _, field := range reflect.VisibleFields(reflect.TypeOf(*entry_remote)) {
if field.Name == "Id" {
continue
}
if !reflect.ValueOf(*entry_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*entry).FieldByName(field.Name)) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("flags[%d].entries[%d].%s", i, j, field.Name),
Link: fmt.Sprintf("exercices/%d/flags#quiz-%d", exercice.Id, mcq.Id),
Before: reflect.ValueOf(*mcq.Entries[j]).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*entry_remote).FieldByName(field.Name).Interface(),
})
}
}
found = true
break
}
}
if !found {
not_found = append(not_found, entry_remote)
}
}
for _, entry := range mcq.Entries {
found := false
for _, entry_remote := range mcq_remote.Entries {
if entry.Label == entry_remote.Label {
found = true
break
}
}
if !found {
extra_found = append(extra_found, entry)
}
}
if len(not_found) > 0 || len(extra_found) > 0 {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("flags[%d].entries", i, field.Name),
Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
Before: extra_found,
After: not_found,
})
}
} else if !reflect.ValueOf(*mcq_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*mcq).FieldByName(field.Name)) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
Before: reflect.ValueOf(*mcq).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*mcq_remote).FieldByName(field.Name).Interface(),
})
}
}
break
}
}
if !found {
flags_not_found = append(flags_not_found, mcq_remote)
}
} else if label_remote, ok := flag_remote.(*fic.FlagLabel); ok {
found := false
for _, flag := range flags {
if label, ok := flag.(*fic.FlagLabel); ok && (label.Label == label_remote.Label || label.Order == label_remote.Order) {
found = true
for _, field := range reflect.VisibleFields(reflect.TypeOf(*label_remote)) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
continue
}
if !reflect.ValueOf(*label_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*label).FieldByName(field.Name)) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("flags[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d/flags#flag-%d", exercice.Id, label.Id),
Before: reflect.ValueOf(*label).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*label_remote).FieldByName(field.Name).Interface(),
})
}
}
break
}
}
if !found {
flags_not_found = append(flags_not_found, label_remote)
}
} else {
log.Printf("unknown flag type: %T", flag_remote)
}
}
for _, flag := range flags {
if key, ok := flag.(*fic.FlagKey); ok {
found := false
for _, flag_remote := range flags_remote {
if key_remote, ok := flag_remote.(*fic.FlagKey); ok && (key.Label == key_remote.Label || key.Order == key_remote.Order) {
found = true
break
}
}
if !found {
flags_extra_found = append(flags_extra_found, flag)
}
} else if mcq, ok := flag.(*fic.MCQ); ok {
found := false
for _, flag_remote := range flags_remote {
if mcq_remote, ok := flag_remote.(*fic.MCQ); ok && (mcq.Title == mcq_remote.Title || mcq.Order == mcq_remote.Order) {
found = true
break
}
}
if !found {
flags_extra_found = append(flags_extra_found, flag)
}
} else if label, ok := flag.(*fic.FlagLabel); ok {
found := false
for _, flag_remote := range flags_remote {
if label_remote, ok := flag_remote.(*fic.FlagLabel); ok && (label.Label == label_remote.Label || label.Order == label_remote.Order) {
found = true
break
}
}
if !found {
flags_extra_found = append(flags_extra_found, flag)
}
}
}
if len(flags_not_found) > 0 || len(flags_extra_found) > 0 {
diffs = append(diffs, syncDiff{
Field: "flags",
Link: fmt.Sprintf("exercices/%d/flags", exercice.Id),
Before: flags_extra_found,
After: flags_not_found,
})
}
// Compare hints
hints, err := exercice.GetHints()
if err != nil {
return nil, fmt.Errorf("Unable to GetHints: %w", err)
}
hints_remote, err := sync.GetRemoteExerciceHints(thid, exid)
if err != nil {
return nil, fmt.Errorf("Unable to GetRemoteHints: %w", err)
}
for i, hint_remote := range hints_remote {
hint_remote.Hint.TreatHintContent()
for _, field := range reflect.VisibleFields(reflect.TypeOf(*hint_remote.Hint)) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdExercice" {
continue
}
if len(hints) <= i {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("hints[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d", exercice.Id),
Before: nil,
After: reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Interface(),
})
} else if !reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Equal(reflect.ValueOf(*hints[i]).FieldByName(field.Name)) {
diffs = append(diffs, syncDiff{
Field: fmt.Sprintf("hints[%d].%s", i, field.Name),
Link: fmt.Sprintf("exercices/%d", exercice.Id),
Before: reflect.ValueOf(*hints[i]).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*hint_remote.Hint).FieldByName(field.Name).Interface(),
})
}
}
}
return diffs, err
}
func APIDiffExerciceWithRemote(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
exercice := c.MustGet("exercice").(*fic.Exercice)
diffs, err := diffExerciceWithRemote(exercice, theme)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, diffs)
}

View file

@ -310,6 +310,7 @@ func ApplySettings(config *settings.Settings) {
fic.SubmissionCostBase = config.SubmissionCostBase
fic.SubmissionUniqueness = config.SubmissionUniqueness
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
fic.QuestionGainRatio = config.QuestionGainRatio
if config.DiscountedFactor != fic.DiscountedFactor {
fic.DiscountedFactor = config.DiscountedFactor
@ -329,6 +330,7 @@ func ResetSettings() error {
WChoiceCurCoefficient: 1,
GlobalScoreCoefficient: 1,
DiscountedFactor: 0,
QuestionGainRatio: 0,
UnlockedStandaloneExercices: 10,
UnlockedStandaloneExercicesByThemeStepValidation: 1,
UnlockedStandaloneExercicesByStandaloneExerciceValidation: 0,

View file

@ -79,12 +79,14 @@ func declareSyncRoutes(router *gin.RouterGroup) {
c.JSON(http.StatusOK, r)
})
apiSyncRoutes.POST("/local-diff", APIDiffDBWithRemote)
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)) {
for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, &fic.StandaloneExercicesTheme, 0, 250, nil)) {
st = append(st, se.Error())
}
sync.EditDeepReport(&sync.SyncReport{Exercices: st}, false)
@ -378,3 +380,32 @@ func autoSync(c *gin.Context) {
c.JSON(http.StatusOK, st)
}
func diffDBWithRemote() (map[string][]syncDiff, error) {
diffs := map[string][]syncDiff{}
themes, err := fic.GetThemesExtended()
if err != nil {
return nil, err
}
// Compare inner themes
for _, theme := range themes {
diffs[theme.Name], err = diffThemeWithRemote(theme)
if err != nil {
return nil, fmt.Errorf("Unable to diffThemeWithRemote: %w", err)
}
}
return diffs, err
}
func APIDiffDBWithRemote(c *gin.Context) {
diffs, err := diffDBWithRemote()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, diffs)
}

View file

@ -5,7 +5,9 @@ import (
"log"
"net/http"
"path"
"reflect"
"strconv"
"strings"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
@ -48,6 +50,8 @@ func declareThemesRoutes(router *gin.RouterGroup) {
apiThemesRoutes.PUT("", updateTheme)
apiThemesRoutes.DELETE("", deleteTheme)
apiThemesRoutes.POST("/diff-sync", APIDiffThemeWithRemote)
apiThemesRoutes.GET("/exercices_stats.json", getThemedExercicesStats)
declareExercicesRoutes(apiThemesRoutes)
@ -70,13 +74,17 @@ func ThemeHandler(c *gin.Context) {
return
}
theme, err := fic.GetTheme(thid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
return
}
if thid == 0 {
c.Set("theme", &fic.StandaloneExercicesTheme)
} else {
theme, err := fic.GetTheme(thid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Theme not found"})
return
}
c.Set("theme", theme)
c.Set("theme", theme)
}
c.Next()
}
@ -127,6 +135,10 @@ func listThemes(c *gin.Context) {
return
}
if has, _ := fic.HasStandaloneExercice(); has {
themes = append([]*fic.Theme{&fic.StandaloneExercicesTheme}, themes...)
}
c.JSON(http.StatusOK, themes)
}
@ -255,3 +267,110 @@ func getThemedExercicesStats(c *gin.Context) {
}
c.JSON(http.StatusOK, ret)
}
func diffThemeWithRemote(theme *fic.Theme) ([]syncDiff, error) {
var diffs []syncDiff
// Compare theme attributes
theme_remote, err := sync.GetRemoteTheme(theme.Path)
if err != nil {
return nil, err
}
for _, field := range reflect.VisibleFields(reflect.TypeOf(*theme)) {
if ((field.Name == "Image") && path.Base(reflect.ValueOf(*theme_remote).FieldByName(field.Name).String()) != path.Base(reflect.ValueOf(*theme).FieldByName(field.Name).String())) || (field.Name != "Image" && !reflect.ValueOf(*theme_remote).FieldByName(field.Name).Equal(reflect.ValueOf(*theme).FieldByName(field.Name))) {
if !field.IsExported() || field.Name == "Id" || field.Name == "IdTheme" || field.Name == "IssueKind" || field.Name == "BackgroundColor" {
continue
}
diffs = append(diffs, syncDiff{
Field: field.Name,
Link: fmt.Sprintf("themes/%d", theme.Id),
Before: reflect.ValueOf(*theme).FieldByName(field.Name).Interface(),
After: reflect.ValueOf(*theme_remote).FieldByName(field.Name).Interface(),
})
}
}
// Compare exercices list
exercices, err := theme.GetExercices()
if err != nil {
return nil, fmt.Errorf("Unable to GetExercices: %w", err)
}
exercices_remote, err := sync.ListRemoteExercices(theme.Path)
if err != nil {
return nil, fmt.Errorf("Unable to ListRemoteExercices: %w", err)
}
var not_found []string
var extra_found []string
for _, exercice_remote := range exercices_remote {
found := false
for _, exercice := range exercices {
if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote {
found = true
break
}
}
if !found {
not_found = append(not_found, exercice_remote)
}
}
for _, exercice := range exercices {
found := false
for _, exercice_remote := range exercices_remote {
if exercice.Path[strings.Index(exercice.Path, "/")+1:] == exercice_remote {
found = true
break
}
}
if !found {
extra_found = append(extra_found, exercice.Path[strings.Index(exercice.Path, "/")+1:])
}
}
if len(not_found) > 0 || len(extra_found) > 0 {
diffs = append(diffs, syncDiff{
Field: "theme.Exercices",
Link: fmt.Sprintf("themes/%d", theme.Id),
Before: strings.Join(extra_found, ", "),
After: strings.Join(not_found, ", "),
})
}
// Compare inner exercices
for i, exercice := range exercices {
exdiffs, err := diffExerciceWithRemote(exercice, theme)
if err != nil {
return nil, fmt.Errorf("Unable to diffExerciceWithRemote: %w", err)
}
for _, exdiff := range exdiffs {
if theme.Id == 0 {
exdiff.Field = fmt.Sprintf("exercices[%d].%s", exercice.Id, exdiff.Field)
} else {
exdiff.Field = fmt.Sprintf("exercices[%d].%s", i, exdiff.Field)
}
diffs = append(diffs, exdiff)
}
}
return diffs, err
}
func APIDiffThemeWithRemote(c *gin.Context) {
theme := c.MustGet("theme").(*fic.Theme)
diffs, err := diffThemeWithRemote(theme)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, diffs)
}

View file

@ -199,10 +199,10 @@ func main() {
}
log.Println("Using", sync.GlobalImporter.Kind())
// Update distributed challenge.json
if _, err := os.Stat(path.Join(settings.SettingsDir, settings.ChallengeFile)); os.IsNotExist(err) {
challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile)
if err == nil {
challengeinfo, err := sync.GetFileContent(sync.GlobalImporter, settings.ChallengeFile)
if err == nil {
// Initial distribution of challenge.json
if _, err := os.Stat(path.Join(settings.SettingsDir, settings.ChallengeFile)); os.IsNotExist(err) {
if fd, err := os.Create(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
log.Fatal("Unable to open SETTINGS/challenge.json:", err)
} else {
@ -213,6 +213,10 @@ func main() {
}
}
}
if ci, err := settings.ReadChallengeInfo(challengeinfo); err == nil {
fic.StandaloneExercicesTheme.Authors = ci.Authors
}
}
}

View file

@ -127,7 +127,6 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
if st, err := os.Stat(filepath); os.IsNotExist(err) || st.Size() == 0 {
if st, err := os.Stat(filepath + ".gz"); err == nil {
if fd, err := os.Open(filepath + ".gz"); err == nil {
log.Println(filepath + ".gz")
c.DataFromReader(http.StatusOK, st.Size(), "application/octet-stream", fd, map[string]string{
"Content-Encoding": "gzip",
})
@ -136,7 +135,6 @@ func declareStaticRoutes(router *gin.RouterGroup, cfg *settings.Settings, baseUR
}
}
log.Println(filepath)
c.File(filepath)
})
router.GET("/submissions/*_", func(c *gin.Context) {

View file

@ -923,6 +923,21 @@ angular.module("FICApp")
});
});
};
$scope.diffWithRepo = function () {
$scope.diff = null;
$http({
url: "api/sync/local-diff",
method: "POST"
}).then(function (response) {
$scope.diff = response.data;
if (response.data === null) {
$scope.addToast('success', 'Changements par rapport au dépôt', "Tout est pareil !");
}
}, function (response) {
$scope.diff = null;
$scope.addToast('danger', 'An error occurs when synchronizing exercice:', response.data.errmsg);
});
};
})
.controller("AuthController", function ($scope, $http) {
@ -1819,6 +1834,21 @@ angular.module("FICApp")
$scope.addToast('danger', 'An error occurs when trying to delete theme:', response.data.errmsg);
});
}
$scope.checkExoSync = function () {
$scope.diff = null;
$http({
url: "api/themes/" + $scope.theme.id + "/diff-sync",
method: "POST"
}).then(function (response) {
$scope.diff = response.data;
if (response.data === null) {
$scope.addToast('success', 'Changements par rapport au dépôt', "Tout est pareil !");
}
}, function (response) {
$scope.diff = null;
$scope.addToast('danger', 'An error occurs when synchronizing exercice:', response.data.errmsg);
});
};
})
.controller("TagsListController", function ($scope, $http) {
@ -1998,6 +2028,21 @@ angular.module("FICApp")
$scope.addToast('danger', 'An error occurs when synchronizing exercice:', response.data.errmsg);
});
};
$scope.checkExoSync = function () {
$scope.diff = null;
$http({
url: ($scope.exercice.id_theme ? ("api/themes/" + $scope.exercice.id_theme + "/exercices/" + $routeParams.exerciceId) : ("api/exercices/" + $routeParams.exerciceId)) + "/diff-sync",
method: "POST"
}).then(function (response) {
$scope.diff = response.data;
if (response.data === null) {
$scope.addToast('success', 'Changements par rapport au dépôt', "Tout est pareil !");
}
}, function (response) {
$scope.diff = null;
$scope.addToast('danger', 'An error occurs when synchronizing exercice:', response.data.errmsg);
});
};
$scope.deleteExercice = function () {
var tid = $scope.exercice.id_theme;

View file

@ -10,11 +10,27 @@
<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>
<a href="exercices/{{exercice.id}}/flags" class="ml-2 btn btn-sm btn-success"><span class="glyphicon glyphicon-flag" aria-hidden="true"></span> Flags</a>
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="ml-2 btn btn-sm btn-light"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
<div class="btn-group ml-2" role="group">
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
<button type="button" ng-click="checkExoSync()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light" title="Exporter l'exercice actuel"><span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span></button>
</div>
<a href="{{exercice.forge_link}}" target="_blank" class="ml-2 btn btn-sm btn-dark" ng-if="exercice.forge_link"><span class="glyphicon glyphicon-folder-open" aria-hidden="true"></span> Voir sur la forge</a>
</div>
</div>
<div ng-if="diff">
<h3>Différences par rapport au dépôt</h3>
<div ng-repeat="diffline in diff" class="row">
<a ng-href="{{ diffline.link }}" class="col-2 d-flex justify-content-end align-items-center text-monospace" title="{{ diffline.field }}">{{ diffline.field }}</a>
<div class="col">
<div class="text-danger"><span class="text-monospace">-</span>{{ diffline.be }}</div>
<div class="text-success"><span class="text-monospace">+</span>{{ diffline.af }}</div>
</div>
</div>
</div>
<hr ng-if="diff" class="my-3">
<div class="row mb-5">
<form class="col-md-8" ng-submit="saveExercice()">

View file

@ -64,46 +64,60 @@
<hr>
<div class="form-group row">
<label for="globalScoreCoefficient" class="col-sm-2 col-form-label col-form-label-sm" ng-class="{'text-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}"><strong>Coefficients</strong></label>
<div class="col-sm-1">
<input type="text" class="form-control form-control-sm" id="globalScoreCoefficient" ng-model="config.globalScoreCoefficient" float title="Coefficient multiplicateur global du score final (le coefficient est appliqué dans la fonction et vaut pour tout le challenge, présent/passé/futur, sans effet de bord)" ng-class="{'border-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}">
<div class="col-sm row">
<label for="globalScoreCoefficient" class="col-form-label col-form-label-sm" ng-class="{'text-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}"><strong>Coefficients</strong></label>
<div class="col">
<input type="text" class="form-control form-control-sm" id="globalScoreCoefficient" ng-model="config.globalScoreCoefficient" float title="Coefficient multiplicateur global du score final (le coefficient est appliqué dans la fonction et vaut pour tout le challenge, présent/passé/futur, sans effet de bord)" ng-class="{'border-primary': config.globalScoreCoefficient != dist_config.globalScoreCoefficient}">
</div>
</div>
<label for="hintcoefficient" class="col-sm-2 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">incides</label>
<div class="col-sm-1">
<input type="text" class="form-control form-control-sm" id="hintcoefficient" ng-model="config.hintCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre un indice (le coefficient est enregistré au moment où l'équipe demande un indice, ce n'est pas global)" ng-class="{'border-primary': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">
<div class="col-sm row">
<label for="hintcoefficient" class="col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">incides</label>
<div class="col">
<input type="text" class="form-control form-control-sm" id="hintcoefficient" ng-model="config.hintCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre un indice (le coefficient est enregistré au moment où l'équipe demande un indice, ce n'est pas global)" ng-class="{'border-primary': config.hintCurrentCoefficient != dist_config.hintCurrentCoefficient}">
</div>
</div>
<label for="wchoicescoefficient" class="col-sm-2 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">WChoices</label>
<div class="col-sm-1">
<input type="text" class="form-control form-control-sm" id="wchoicescoefficient" ng-model="config.wchoiceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre une demande de liste de choix (le coefficient est enregistré au moment où l'équipe demande la liste de choix, ce n'est pas global)" ng-class="{'border-primary': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">
<div class="col-sm row">
<label for="wchoicescoefficient" class="col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">WChoices</label>
<div class="col">
<input type="text" class="form-control form-control-sm" id="wchoicescoefficient" ng-model="config.wchoiceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait perdre une demande de liste de choix (le coefficient est enregistré au moment où l'équipe demande la liste de choix, ce n'est pas global)" ng-class="{'border-primary': config.wchoiceCurrentCoefficient != dist_config.wchoiceCurrentCoefficient}">
</div>
</div>
<label for="exercicecurcoefficient" class="col-sm-2 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">défis</label>
<div class="col-sm-1">
<input type="text" class="form-control form-control-sm" id="exercicecurcoefficient" ng-model="config.exerciceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait gagner un exercice validé (le coefficient est enregistré au moment où l'équipe valide l'exercice, ce n'est pas global)" ng-class="{'border-primary': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">
<div class="col-sm row">
<label for="exercicecurcoefficient" class="col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">défis</label>
<div class="col">
<input type="text" class="form-control form-control-sm" id="exercicecurcoefficient" ng-model="config.exerciceCurrentCoefficient" float title="Coefficient multiplicateur temporaire du nombre de points que fait gagner un exercice validé (le coefficient est enregistré au moment où l'équipe valide l'exercice, ce n'est pas global)" ng-class="{'border-primary': config.exerciceCurrentCoefficient != dist_config.exerciceCurrentCoefficient}">
</div>
</div>
</div>
<div class="form-group row">
<div class="form-group row" title="Attribuer ce pourcentage de points bonus supplémentaire à la première équipe qui valide un exercice">
<div class="col-sm row">
<label for="firstBlood" class="col-sm-8 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.firstBlood != dist_config.firstBlood}">Bonus premier sang</label>
<label for="firstBlood" class="col-sm-8 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.firstBlood != dist_config.firstBlood}">Premier sang</label>
<div class="col-sm-4">
<input type="text" class="form-control form-control-sm" id="firstBlood" ng-model="config.firstBlood" float ng-class="{'border-primary': config.firstBlood != dist_config.firstBlood}">
</div>
</div>
<div class="col-sm row">
<label for="discountFactor" class="col-sm-8 col-form-label col-form-label-sm" ng-class="{'text-primary font-weight-bold': config.discountedFactor != dist_config.discountedFactor}">Décote des exercices</label>
<div class="col-sm row" title="Pour chaque validation supplémentaire d'un exercice donné, on retire ce pourcentage de points à l'exercice. Les points rapportés par un exercice sont alors dynamiques : ils baissent pour toutes les équipes y compris celles ayant validé cet exercice il y a longtemps.">
<label for="discountFactor" class="col-sm-8 col-form-label col-form-label-sm text-truncate" ng-class="{'text-primary font-weight-bold': config.discountedFactor != dist_config.discountedFactor}">Décote exercices</label>
<div class="col-sm-4">
<input type="text" class="form-control form-control-sm" id="discountFactor" ng-model="config.discountedFactor" float ng-class="{'border-primary': config.discountedFactor != dist_config.discountedFactor}">
</div>
</div>
<div class="col-sm row">
<label for="submissionCostBase" class="col-sm-8 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.submissionCostBase != dist_config.submissionCostBase}">Coût de base tentative</label>
<div class="col-sm row" title="Coefficient de base retiré pour chaque soumission invalide au delà de 10 soumissions">
<label for="submissionCostBase" class="col-sm-8 col-form-label col-form-label-sm text-right" ng-class="{'text-primary font-weight-bold': config.submissionCostBase != dist_config.submissionCostBase}">Coût tentative</label>
<div class="col-sm-4">
<input type="text" class="form-control form-control-sm" id="submissionCostBase" ng-model="config.submissionCostBase" float ng-class="{'border-primary': config.submissionCostBase != dist_config.submissionCostBase}">
</div>
</div>
<div class="col-sm row" title="Accorder des points aux exercices partiellement résolu, par questions validée. Ce champ est le pourcentage de points que peut rapporter la complétion de toutes les questions d'un exercice. Par exemple avec 25%, un exercice avec 10 questions, chaque question validée rapportera GAIN * 25% / 10">
<label for="questionGainRatio" class="col-sm-8 col-form-label col-form-label-sm text-right text-truncate" ng-class="{'text-primary font-weight-bold': config.questionGainRatio != dist_config.questionGainRatio}">Gain par questions</label>
<div class="col-sm-4">
<input type="text" class="form-control form-control-sm" id="questionGainRatio" ng-model="config.questionGainRatio" float ng-class="{'border-primary': config.questionGainRatio != dist_config.questionGainRatio}">
</div>
</div>
</div>
<hr>

View file

@ -120,3 +120,41 @@
</ul>
</div>
</div>
<div class="card mb-5" ng-controller="ThemesListController">
<div class="card-header">
<button type="button" class="btn btn-primary float-right mx-1" ng-click="diffWithRepo()" title="Calculer les différences avec le dépôt">
<span class="glyphicon glyphicon-refresh" aria-hidden="true"></span>
</button>
<h3 class="mb-0">
Différences avec le dépôts
</h3>
</div>
<div class="card-body" ng-if="!diff">
<div class="alert alert-info">Lancez la génération du rapport pour lister les différences.</div>
</div>
<div ng-repeat="(th, lines) in diff" class="card-body" ng-if="diff">
<div class="d-flex">
<h3>
{{ th }}
</h3>
<div class="d-inline-block" ng-repeat="theme in themes" ng-if="theme.name == th">
<a href="themes/{{ theme.id }}" class="btn btn-link" title="Voir le thème">
<span class="glyphicon glyphicon-hand-right" aria-hidden="true"></span>
</a>
<button class="btn btn-light" title="Resynchroniser uniquement ce thème" ng-click="deepSync(theme)" ng-if="settings.wip || !timeProgression || displayDangerousActions">
<span class="glyphicon glyphicon-hdd" aria-hidden="true"></span>
</button>
</div>
</div>
<ul>
<li ng-repeat="diffline in lines" class="row">
<a ng-href="{{ diffline.link }}" class="col-2 d-flex align-items-center text-truncate text-monospace" title="{{ diffline.field }}">{{ diffline.field }}</a>
<div class="col">
<div class="text-danger"><span class="text-monospace">-</span>{{ diffline.be }}</div>
<div class="text-success"><span class="text-monospace">+</span>{{ diffline.af }}</div>
</div>
</li>
</ul>
</div>
</div>

View file

@ -13,7 +13,7 @@
<th>Date</th>
</thead>
<tbody>
<tr ng-repeat="row in scores | filter: query | orderBy:'time'" ng-class="{'bg-danger': row.reason == 'Bonus flag', 'bg-ffound': row.reason == 'First blood', 'bg-wchoices': row.reason == 'Display choices', 'bg-success': row.reason == 'Validation', 'bg-info': row.reason == 'Hint', 'bg-warning': row.reason == 'Tries'}">
<tr ng-repeat="row in scores | filter: query | orderBy:'time'" ng-class="{'bg-danger': row.reason == 'Bonus flag', 'bg-ffound': row.reason == 'First blood', 'bg-wchoices': row.reason == 'Display choices', 'bg-success': row.reason == 'Validation', 'bg-info': row.reason == 'Hint', 'bg-secondary': row.reason.startsWith('Response '), 'bg-warning': row.reason == 'Tries'}">
<td>
<a ng-repeat="exercice in exercices" ng-if="exercice.id == row.id_exercice" href="exercices/{{ row.id_exercice }}">{{ exercice.title }}</a>
</td>
@ -23,9 +23,12 @@
<td>
{{ row.points * row.coeff }}
</td>
<td>
<td ng-if="!row.reason.startsWith('Response ')">
{{ row.points }} * {{ row.coeff }}
</td>
<td ng-if="row.reason.startsWith('Response ')">
{{ row.points }} * {{ settings.questionGainRatio }} / {{ settings.questionGainRatio / row.coeff }}
</td>
<td>
<nobr title="{{ row.time }}">{{ row.time | date:"mediumTime" }}</nobr>
</td>

View file

@ -7,8 +7,21 @@
</div>
</div>
<div ng-if="diff">
<h3>Différences par rapport au dépôt</h3>
<div ng-repeat="diffline in diff" class="row">
<a ng-href="{{ diffline.link }}" class="col-3 d-flex align-items-center text-truncate text-monospace" title="{{ diffline.field }}">{{ diffline.field }}</a>
<div class="col">
<div class="text-danger"><span class="text-monospace">-</span>{{ diffline.be }}</div>
<div class="text-success"><span class="text-monospace">+</span>{{ diffline.af }}</div>
</div>
</div>
</div>
<hr ng-if="diff" class="my-3">
<div class="row">
<form ng-submit="saveTheme()" class="col-4">
<form ng-submit="saveTheme()" class="col-4" ng-if="!(theme.id === 0 && theme.path)">
<div ng-class="{'form-group': field != 'locked', 'form-check': field == 'locked'}" ng-repeat="field in fields">
<input type="checkbox" class="form-check-input" id="{{ field }}" ng-model="theme[field]" ng-if="field == 'locked'">
<label for="{{ field }}">{{ field | capitalize }}</label>
@ -25,11 +38,14 @@
</div>
</form>
<div ng-if="theme.id" class="col-md-8" ng-controller="ExercicesListController">
<div ng-if="theme.id || theme.path" class="col-md-8" ng-class="{'offset-md-2': theme.id === 0 && theme.path}" ng-controller="ExercicesListController">
<h3>
Exercices ({{ exercices.length }})
<button type="button" ng-click="show('new')" class="float-right btn btn-sm btn-primary ml-2"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter un exercice</button>
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="float-right btn btn-sm btn-light ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
<div class="float-right btn-group ml-2" role="group">
<button type="button" ng-click="syncExo()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Synchroniser</button>
<button type="button" ng-click="checkExoSync()" ng-class="{'disabled': inSync}" class="btn btn-sm btn-light" title="Exporter l'exercice actuel"><span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span></button>
</div>
</h3>
<p><input type="search" class="form-control form-control-sm" placeholder="Search" ng-model="query" autofocus></p>

View file

@ -23,7 +23,7 @@ func NewThemeError(theme *fic.Theme, err error) *ThemeError {
if theme == nil {
return &ThemeError{
error: err,
ThemePath: StandaloneExercicesDirectory,
ThemePath: fic.StandaloneExercicesDirectory,
}
}

View file

@ -420,36 +420,67 @@ func SyncExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExce
return
}
func GetRemoteExerciceFiles(thid, exid string) ([]*fic.EFile, error) {
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, errs
}
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, exid), nil, exceptions)
if exercice == nil {
return nil, errs
}
files, digests, errs := BuildFilesListInto(GlobalImporter, exercice, "files")
if files == nil {
return nil, errs
}
var ret []*fic.EFile
for _, fname := range files {
fPath := path.Join(exercice.Path, "files", fname)
fSize, _ := GetFileSize(GlobalImporter, fPath)
file := fic.EFile{
Path: fPath,
Name: fname,
Checksum: digests[fname],
Size: fSize,
Published: true,
}
if d, exists := digests[strings.TrimSuffix(file.Name, ".gz")]; exists {
file.Name = strings.TrimSuffix(file.Name, ".gz")
file.Path = strings.TrimSuffix(file.Path, ".gz")
file.ChecksumShown = d
}
ret = append(ret, &file)
}
// Complete with attributes
if paramsFiles, err := GetExerciceFilesParams(GlobalImporter, exercice); err == nil {
for _, file := range ret {
if f, ok := paramsFiles[file.Name]; ok {
file.Published = !f.Hidden
if disclaimer, err := ProcessMarkdown(GlobalImporter, fixnbsp(f.Disclaimer), exercice.Path); err == nil {
file.Disclaimer = disclaimer
}
}
}
}
return ret, nil
}
// ApiGetRemoteExerciceFiles is an accessor to remote exercice files list.
func ApiGetRemoteExerciceFiles(c *gin.Context) {
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)
if exercice != nil {
files, digests, errs := BuildFilesListInto(GlobalImporter, exercice, "files")
if files != nil {
var ret []*fic.EFile
for _, fname := range files {
fPath := path.Join(exercice.Path, "files", fname)
fSize, _ := GetFileSize(GlobalImporter, fPath)
ret = append(ret, &fic.EFile{
Path: fPath,
Name: fname,
Checksum: digests[fname],
Size: fSize,
})
}
c.JSON(http.StatusOK, ret)
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
} else {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": fmt.Errorf("%q", errs)})
files, err := GetRemoteExerciceFiles(c.Params.ByName("thid"), c.Params.ByName("exid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, files)
}

View file

@ -169,25 +169,32 @@ func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int
return
}
func GetRemoteExerciceHints(thid, exid string) ([]importHint, error) {
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, errs
}
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, exid), nil, exceptions)
if exercice == nil {
return nil, errs
}
hints, errs := CheckExerciceHints(GlobalImporter, exercice, eexceptions)
if hints == nil {
return nil, errs
}
return hints, nil
}
// ApiListRemoteExerciceHints is an accessor letting foreign packages to access remote exercice hints.
func ApiGetRemoteExerciceHints(c *gin.Context) {
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
if exercice != nil {
hints, errs := CheckExerciceHints(GlobalImporter, exercice, eexceptions)
if hints != nil {
c.JSON(http.StatusOK, hints)
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
hints, errs := GetRemoteExerciceHints(c.Params.ByName("thid"), c.Params.ByName("exid"))
if hints != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
c.JSON(http.StatusOK, hints)
}

View file

@ -699,26 +699,32 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExce
return
}
func GetRemoteExerciceFlags(thid, exid string) ([]fic.Flag, error) {
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, errs
}
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, exid), nil, exceptions)
if exercice == nil {
return nil, errs
}
flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{}, eexceptions)
if flags == nil {
return nil, errs
}
return flags, nil
}
// ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags.
func ApiGetRemoteExerciceFlags(c *gin.Context) {
theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
if exercice != nil {
flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{}, eexceptions)
if flags != nil {
c.JSON(http.StatusOK, flags)
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
flags, err := GetRemoteExerciceFlags(c.Params.ByName("thid"), c.Params.ByName("exid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
c.JSON(http.StatusOK, flags)
}

View file

@ -63,10 +63,17 @@ func buildDependancyMap(i Importer, theme *fic.Theme) (dmap map[int64]*fic.Exerc
continue
}
// ename can be overrride by title.txt
if i.Exists(path.Join(theme.Path, edir, "title.txt")) {
if myTitle, err := GetFileContent(i, path.Join(theme.Path, edir, "title.txt")); err == nil {
ename = strings.TrimSpace(myTitle)
}
}
var e *fic.Exercice
e, err = theme.GetExerciceByTitle(ename)
if err != nil {
return
return dmap, fmt.Errorf("unable to GetExerciceByTitle(ename=%q, tid=%d): %w", ename, theme.Id, err)
}
dmap[int64(eid)] = e
@ -468,59 +475,57 @@ func SyncExercices(i Importer, theme *fic.Theme, exceptions *CheckExceptions) (e
return
}
func ListRemoteExercices(thid string) ([]string, error) {
if thid == "_" {
return GetExercices(GlobalImporter, &fic.StandaloneExercicesTheme)
}
theme, _, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, errs
}
return GetExercices(GlobalImporter, theme)
}
// 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)
exercices, err := ListRemoteExercices(c.Params.ByName("thid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
theme, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if theme != nil {
exercices, err := GetExercices(GlobalImporter, theme)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, exercices)
} else {
c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
return
}
c.JSON(http.StatusOK, exercices)
}
// ApiListRemoteExercice is an accessor letting foreign packages to access remote exercice attributes.
func GetRemoteExercice(thid, exid string, inTheme *fic.Theme) (*fic.Exercice, error) {
if thid == fic.StandaloneExercicesDirectory || thid == "_" {
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, nil, path.Join(fic.StandaloneExercicesDirectory, exid), nil, nil)
return exercice, errs
}
theme, exceptions, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, fmt.Errorf("Theme not found")
}
if inTheme == nil {
inTheme = theme
}
exercice, _, _, _, _, errs := BuildExercice(GlobalImporter, inTheme, path.Join(theme.Path, exid), nil, exceptions)
return exercice, errs
}
// ApiGetRemoteExercice 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)
if exercice != nil {
c.JSON(http.StatusOK, exercice)
return
} else {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
exercice, err := GetRemoteExercice(c.Params.ByName("thid"), c.Params.ByName("exid"), nil)
if exercice != nil {
c.JSON(http.StatusOK, exercice)
return
} else {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
}

View file

@ -75,8 +75,8 @@ func SpeedySyncDeep(i Importer) (errs SyncReport) {
if themes, err := fic.GetThemes(); err == nil {
DeepSyncProgress = 2
if i.Exists(StandaloneExercicesDirectory) {
themes = append(themes, &fic.Theme{Path: StandaloneExercicesDirectory})
if i.Exists(fic.StandaloneExercicesDirectory) {
themes = append(themes, &fic.StandaloneExercicesTheme)
}
var themeStep uint8 = uint8(250) / uint8(len(themes))
@ -147,8 +147,8 @@ func SyncDeep(i Importer) (errs SyncReport) {
DeepSyncProgress = 2
// Also synchronize standalone exercices
if i.Exists(StandaloneExercicesDirectory) {
themes = append(themes, &fic.Theme{Path: StandaloneExercicesDirectory})
if i.Exists(fic.StandaloneExercicesDirectory) {
themes = append(themes, &fic.StandaloneExercicesTheme)
}
var themeStep uint8 = uint8(250) / uint8(len(themes))

View file

@ -22,15 +22,13 @@ 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, "_") && dir != StandaloneExercicesDirectory {
if !strings.HasPrefix(dir, ".") && !strings.HasPrefix(dir, "_") && dir != fic.StandaloneExercicesDirectory {
if _, err := i.ListDir(dir); err == nil {
themes = append(themes, dir)
}
@ -180,8 +178,10 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
th.URLId = fic.ToURLid(th.Name)
if authors, err := getAuthors(i, tdir); err != nil {
errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get AUTHORS.txt: %w", err)))
return nil, nil, errs
if tdir != fic.StandaloneExercicesDirectory {
errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get AUTHORS.txt: %w", err)))
return nil, nil, errs
}
} else {
// Format authors
th.Authors = strings.Join(authors, ", ")
@ -355,18 +355,34 @@ func ApiListRemoteThemes(c *gin.Context) {
c.JSON(http.StatusOK, themes)
}
func GetRemoteTheme(thid string) (*fic.Theme, error) {
if thid == fic.StandaloneExercicesTheme.URLId || thid == fic.StandaloneExercicesDirectory {
return &fic.StandaloneExercicesTheme, nil
}
theme, _, errs := BuildTheme(GlobalImporter, thid)
if theme == nil {
return nil, errs
}
return theme, nil
}
// 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)
var theme *fic.Theme
var err error
if c.Params.ByName("thid") == fic.StandaloneExercicesTheme.URLId {
theme, err = GetRemoteTheme(fic.StandaloneExercicesDirectory)
} else {
theme, err = GetRemoteTheme(c.Params.ByName("thid"))
}
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
r, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
if r == nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
return
}
c.JSON(http.StatusOK, r)
c.JSON(http.StatusOK, theme)
}

View file

@ -90,6 +90,7 @@ func reloadSettings(config *settings.Settings) {
fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
fic.DiscountedFactor = config.DiscountedFactor
fic.QuestionGainRatio = config.QuestionGainRatio
}
func main() {

View file

@ -28,11 +28,13 @@
export let readonly = false;
export let forcesolved = false;
let last_submission = { };
let responses = { };
async function submitFlags() {
waitInProgress.set(true);
sberr = "";
message = "";
last_submission = JSON.parse(JSON.stringify(responses));
if ($my && $my.team_id === 0) {
let allGoodResponse = true;
@ -119,6 +121,11 @@
mcqs: { },
justifications: { },
};
last_submission = {
flags: { },
mcqs: { },
justifications: { },
};
}
let last_exercice = null;
@ -152,10 +159,12 @@
{#if exercice.tries || exercice.solved_time || exercice.submitted || sberr || $timeouted}
<ListGroup class="border-dark">
{#if exercice.solved_time || exercice.tries}
<ListGroupItem class="text-warning rounded-0">
{#if exercice.tries > 0}{exercice.tries} {exercice.tries==1?"tentative effectuée":"tentatives effectuées"}.{/if}
Dernière solution envoyée à <DateFormat date={exercice.solved_time} />.
</ListGroupItem>
<div class="d-flex align-items-center">
<ListGroupItem class="rounded-0 {$waitInProgress?'text-secondary':'text-warning'}">
{#if exercice.tries > 0}{exercice.tries} {exercice.tries==1?"tentative effectuée":"tentatives effectuées"}.{/if}
Dernière solution envoyée le <span class:placeholder={$waitInProgress} class:placeholder-glow={$waitInProgress}><DateFormat date={exercice.solved_time} /></span>.
</ListGroupItem>
</div>
{/if}
{#if exercice.solve_dist}
<ListGroupItem class="rounded-0">
@ -191,6 +200,7 @@
<FlagMCQ
exercice_id={exercice.id}
{flag}
previous_values={$waitInProgress ? { justifications: { }, mcqs: { } } : last_submission}
bind:values={responses.mcqs}
bind:justifications={responses.justifications}
/>
@ -199,6 +209,7 @@
class="mb-3"
exercice_id={exercice.id}
{flag}
previous_value={$waitInProgress ? "" : last_submission.flags[flag.id]}
bind:value={responses.flags[flag.id]}
/>
{/if}

View file

@ -16,6 +16,7 @@
export let exercice_id = 0;
export let flag = { };
export let no_label = false;
export let previous_value = "";
export let value = "";
let values = [""];
@ -141,6 +142,7 @@
<input
type="number"
class="form-control flag"
class:is-invalid={previous_value && previous_value == value}
id="sol_{flag.type}{flag.id}_{index}"
autocomplete="off"
bind:value={values[index]}
@ -154,6 +156,7 @@
<input
type="text"
class="form-control flag"
class:is-invalid={previous_value && previous_value == value}
id="sol_{flag.type}{flag.id}_{index}"
autocomplete="off"
bind:value={values[index]}
@ -164,6 +167,7 @@
{:else}
<textarea
class="form-control flag"
class:is-invalid={previous_value && previous_value == value}
id="sol_{flag.type}{flag.id}_{index}"
autocomplete="off"
bind:value={values[index]}
@ -208,6 +212,7 @@
value={l}
bind:group={values[index]}
class="form-check-input"
class:is-invalid={previous_value && previous_value == value}
>
<label
class="form-check-label"
@ -220,6 +225,7 @@
{:else}
<select
class="form-select"
class:is-invalid={previous_value && previous_value == value}
id="sol_{flag.type}{flag.id}_{index}"
bind:value={values[index]}
>

View file

@ -8,6 +8,7 @@
export let exercice_id = 0;
export let flag = { };
export let previous_values = { justifications: { }, mcqs: { } };
export let values = { };
export let justifications = { };
</script>
@ -24,7 +25,7 @@
{#each Object.keys(flag.choices) as cid, index}
<div class="form-check ms-3">
{#if typeof flag.choices[cid] != "object"}
<input class="form-check-input" type="checkbox" id="mcq_{flag.id}_{cid}" bind:checked={values[Number(cid)]} disabled={flag.found || flag.part_solved}>
<input class="form-check-input" class:is-invalid={previous_values.mcqs && Object.keys(flag.choices).reduce((acc, cur) => acc + (previous_values.mcqs[Number(cur)] !== undefined ? 1 : 0), 0) > 0 && Object.keys(flag.choices).reduce((acc, cur) => acc && previous_values.mcqs[Number(cur)] == values[Number(cur)], true)} type="checkbox" id="mcq_{flag.id}_{cid}" bind:checked={values[Number(cid)]} disabled={flag.found || flag.part_solved}>
<label class="form-check-label" for="mcq_{flag.id}_{cid}">
{flag.choices[cid]}{#if values[Number(cid)] && flag.justify}&nbsp;:{/if}
</label>
@ -34,6 +35,7 @@
{exercice_id}
flag={{id: cid, placeholder: "Flag correspondant"}}
no_label={true}
previous_value={previous_values.justifications && previous_values.justifications[cid]}
bind:value={justifications[cid]}
/>
{/if}
@ -43,6 +45,7 @@
class={flag.choices[cid].justification.found?"":"mb-3"}
{exercice_id}
flag={flag.choices[cid].justification}
previous_value={previous_values.justifications[cid]}
bind:value={justifications[cid]}
/>
{/if}

View file

@ -10,6 +10,7 @@
import DateFormat from '$lib/components/DateFormat.svelte';
import { my } from '$lib/stores/my.js';
import { settings } from '$lib/stores/settings.js';
import { themes, exercices_idx } from '$lib/stores/themes.js';
let req = null;
@ -55,6 +56,10 @@
{:else if row.reason == "Display choices"}
<Badge color="secondary"><Icon name="info-square" /></Badge>
Échange champ de texte contre liste de choix
{:else if row.reason.startsWith("Response ")}
{@const fields = row.reason.split(" ")}
<Badge class="bg-success-subtle text-dark"><Icon name="clipboard2-check" /></Badge>
Validation {fields[1]} n<sup>o</sup>&nbsp;{fields[3]}
{:else}
<Badge color="primary"><Icon name="question" /></Badge>
{row.reason}
@ -66,7 +71,7 @@
{/if}
</Column>
<Column header="Détail">
<span title="Valeur initiale (cette valeur est fixe)">{Math.trunc(10*row.points)/10}</span> &times; <span title="Coefficient multiplicateur (il varie selon les événements en cours sur la plateforme)">{row.coeff}</span>
<span title="Valeur initiale (cette valeur est fixe)">{Math.trunc(10*row.points)/10}</span> &times; {#if row.reason.startsWith("Response ")}<span title="Pourcentage des points accordé pour avoir répondu aux questions d'un défi, sans avoir validé entièrement le défi">{Math.trunc($settings.questionGainRatio * 1000)/10}&nbsp;&percnt;</span> &divide; <span title="Nombre de questions du défi">{$settings.questionGainRatio / row.coeff}</span>{:else if row.reason == "Validation" && $settings.questionGainRatio != 0}(<span title="Coefficient multiplicateur (il varie selon les événements en cours sur la plateforme)">{row.coeff + $settings.questionGainRatio}</span> &minus; <span title="Pourcentage des points déjà obtenu au travers des réponses aux questions">{Math.trunc($settings.questionGainRatio * 1000)/10}&nbsp;&percnt;</span>){:else}<span title="Coefficient multiplicateur (il varie selon les événements en cours sur la plateforme)">{row.coeff}</span>{/if}
</Column>
<Column header="Points">
{Math.trunc(10*row.points * row.coeff)/10}

View file

@ -21,7 +21,7 @@
<div class="card-group text-justify mb-5">
<div class="card niceborder">
<div class="card-body text-indent">
<div class="card-body text-indent text-white">
<h2>Débloquage des challenges</h2>
<p>
Au début, seul le premier défi de chaque scénario est
@ -31,7 +31,7 @@
{#if $settings.unlockedStandaloneExercicesByThemeStepValidation > 0 || $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 0}
<p>
Vous avez également accès à {$settings.unlockedStandaloneExercices} défis indépendants.
Ces défis sont débloqués
D'autres défis sont débloqués
{#if $settings.unlockedStandaloneExercicesByThemeStepValidation > 0}{#if $settings.unlockedStandaloneExercicesByThemeStepValidation < 1} toutes les {1/$settings.unlockedStandaloneExercicesByThemeStepValidation} étape{#if 1/$settings.unlockedStandaloneExercicesByThemeStepValidation > 1}s{/if} de scénario que vous validez{:else}par {$settings.unlockedStandaloneExercicesByThemeStepValidation} défis pour chaque étape de scénario validée{/if}{/if}
{#if $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 0}{#if $settings.unlockedStandaloneExercicesByStandaloneExerciceValidation < 1} tous les {1/$settings.unlockedStandaloneExercicesByStandaloneExerciceValidation} défi{#if 1/$settings.unlockedStandaloneExercicesByStandaloneExerciceValidation > 1}s{/if} indépendant que vous validez{:else}par {$settings.unlockedStandaloneExercicesByStandaloneExerciceValidation} exercice indépendant validé{/if}{/if}
</p>
@ -55,6 +55,21 @@
proposés. Plus le challenge est compliqué, plus il rapporte de points.
</p>
{#if $settings.questionGainRatio != 0}
<p>
Même si vous n'arrivez pas à valider un défi, toutes les questions validées augmentent votre score.
{Math.trunc($settings.questionGainRatio * 1000)/10}&nbsp;&percnt; des points du défi sont répartis à parts égales entre toutes les questions.
</p>
<p>
Par exemple, pour un défi de 5 questions valant 20 points, en ayant répondu à 3 questions sur les 5, votre score sera augmenté de&nbsp;:<br>
20 &times; {Math.trunc($settings.questionGainRatio * 1000)/10}&nbsp;&percnt; &divide; 5 &times; 3 &equals; {Math.trunc(20 * $settings.questionGainRatio / 5 * 3 * 100)/100}&nbsp;points.
</p>
<p>
Les {Math.trunc(1000 - $settings.questionGainRatio * 1000)/10}&nbsp;&percnt; restants sont obtenus à la validation complète du défi.
</p>
{/if}
{#if $settings.submissionCostBase != 0}
<h3>Coût des tentatives</h3>
<p>
Vous disposez de 10&nbsp;tentatives pour trouver la/les solutions d'un
@ -114,20 +129,21 @@
parmi ce nombre de tentatives.
</p>
{/if}
{/if}
</div>
</div>
<div class="card niceborder">
<div class="card-body text-indent">
<div class="card-body text-indent text-white">
{#if $settings.discountedFactor > 0}
<h3>Décote des gains</h3>
<p>
Une validation d'étape ne vous garanti pas un solde de points fixe.
Une validation d'étape ne vous garantit pas un solde de points fixe.
</p>
<p>
Selon le nombre d'équipe qui valident un challenge donné, sa cote diminue et vous rapporte alors moins de points. Le gain est donc indépendemment du fait que vous ayez validé l'étape avant une autre équipe : le gain affiché est un gain maximum, entendu si aucune autre équipe ne le valide.
Selon le nombre d'équipes qui valident un challenge donné, sa cote diminue et vous rapporte alors moins de points. Le gain final est donc indépendant du fait que vous ayez validé l'étape avant une autre équipe&nbsp;: le gain affiché est un gain maximum que vous obtiendriez si aucune autre équipe ne valide cette étape.
</p>
<p>
Chaque validation réduit de {$settings.discountedFactor*100}&nbsp;% la cote de l'exercice.
Chaque validation réduit de {$settings.discountedFactor*100}&nbsp;&percnt; la cote de l'exercice.
</p>
<p>
Ainsi, pour un exercice d'une valeur initiale de {10*$settings.globalScoreCoefficient}&nbsp;points&nbsp;:
@ -192,10 +208,12 @@
défi.
</p>
<h4>Prem's</h4>
<p>
Un bonus de +{$settings.firstBlood * 100}&nbsp;% est attribué à la première équipe qui résout un défi.
</p>
{#if $settings.firstBlood}
<h4>Prem's</h4>
<p>
Un bonus de +{$settings.firstBlood * 100}&nbsp;&percnt; est attribué à la première équipe qui résout un défi.
</p>
{/if}
<h4>Bonus temporaires <small><Icon name="gift" aria-hidden="true" title="Des
bonus existent pour au moins un challenge de ce thème" /></small></h4>

View file

@ -30,7 +30,7 @@ func reloadSettings(config *settings.Settings) {
fic.WChoiceCoefficient = config.WChoiceCurCoefficient
fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient
ChStarted = config.Start.Unix() > 0 && time.Since(config.Start) >= 0
if allowRegistration != config.AllowRegistration || fic.PartialValidation != config.PartialValidation || fic.UnlockedChallengeDepth != config.UnlockedChallengeDepth || fic.UnlockedStandaloneExercices != config.UnlockedStandaloneExercices || fic.UnlockedStandaloneExercicesByThemeStepValidation != config.UnlockedStandaloneExercicesByThemeStepValidation || fic.UnlockedStandaloneExercicesByStandaloneExerciceValidation != config.UnlockedStandaloneExercicesByStandaloneExerciceValidation || fic.UnlockedChallengeUpTo != config.UnlockedChallengeUpTo || fic.DisplayAllFlags != config.DisplayAllFlags || fic.FirstBlood != config.FirstBlood || fic.SubmissionCostBase != config.SubmissionCostBase || fic.SubmissionUniqueness != config.SubmissionUniqueness || fic.DiscountedFactor != config.DiscountedFactor || fic.HideCaseSensitivity != config.HideCaseSensitivity {
if allowRegistration != config.AllowRegistration || fic.PartialValidation != config.PartialValidation || fic.UnlockedChallengeDepth != config.UnlockedChallengeDepth || fic.UnlockedStandaloneExercices != config.UnlockedStandaloneExercices || fic.UnlockedStandaloneExercicesByThemeStepValidation != config.UnlockedStandaloneExercicesByThemeStepValidation || fic.UnlockedStandaloneExercicesByStandaloneExerciceValidation != config.UnlockedStandaloneExercicesByStandaloneExerciceValidation || fic.UnlockedChallengeUpTo != config.UnlockedChallengeUpTo || fic.DisplayAllFlags != config.DisplayAllFlags || fic.FirstBlood != config.FirstBlood || fic.SubmissionCostBase != config.SubmissionCostBase || fic.SubmissionUniqueness != config.SubmissionUniqueness || fic.DiscountedFactor != config.DiscountedFactor || fic.QuestionGainRatio != config.QuestionGainRatio || fic.HideCaseSensitivity != config.HideCaseSensitivity {
allowRegistration = config.AllowRegistration
fic.PartialValidation = config.PartialValidation
@ -48,6 +48,7 @@ func reloadSettings(config *settings.Settings) {
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
fic.HideCaseSensitivity = config.HideCaseSensitivity
fic.DiscountedFactor = config.DiscountedFactor
fic.QuestionGainRatio = config.QuestionGainRatio
if !skipInitialGeneration {
log.Println("Generating files...")

View file

@ -645,3 +645,14 @@ func (e *Exercice) IsSolved() (int, *time.Time) {
return *nb, tm
}
}
func HasStandaloneExercice() (bool, error) {
var nb int
err := DBQueryRow("SELECT COUNT(id_exercice) FROM exercices WHERE id_theme IS NULL").Scan(&nb)
if err != nil {
return false, err
} else {
return nb > 0, nil
}
}

View file

@ -46,6 +46,10 @@ func GetHint(id int64) (*EHint, error) {
return h, nil
}
func (h *EHint) TreatHintContent() {
treatHintContent(h)
}
// GetHint retrieves the hint with the given id.
func (e *Exercice) GetHint(id int64) (*EHint, error) {
h := &EHint{}

View file

@ -23,6 +23,9 @@ var CountOnlyNotGoodTries = false
// DiscountedFactor stores the percentage of the exercice's gain lost on each validation.
var DiscountedFactor = 0.0
// QuestionGainRatio is the ratio given to a partially solved exercice in the final scoreboard.
var QuestionGainRatio = 0.0
func exoptsQuery(whereExo string) string {
tries_table := "exercice_tries"
if SubmissionUniqueness {
@ -38,9 +41,21 @@ func exoptsQuery(whereExo string) string {
exercices_table = "exercices_discounted"
}
questionGainQuery := ""
if QuestionGainRatio != 0.0 {
questionGainQuery = `SELECT id_team, F.id_exercice AS id_exercice, time, ` + fmt.Sprintf("%f", QuestionGainRatio) + ` / (COALESCE(T.total_flags, 0) + COALESCE(TMCQ.total_mcqs, 0)) AS coeff, CONCAT("Response flag ", F.id_flag, " ", F.ordre) AS reason FROM flag_found B INNER JOIN exercice_flags F ON F.id_flag = B.id_flag LEFT JOIN (SELECT id_exercice, COUNT(*) AS total_flags FROM exercice_flags GROUP BY id_exercice) T ON F.id_exercice = T.id_exercice LEFT JOIN (SELECT id_exercice, COUNT(*) AS total_mcqs FROM exercice_mcq GROUP BY id_exercice) TMCQ ON F.id_exercice = TMCQ.id_exercice WHERE F.bonus_gain = 0 UNION
SELECT id_team, F.id_exercice AS id_exercice, time, ` + fmt.Sprintf("%f", QuestionGainRatio) + ` / (COALESCE(T.total_flags, 0) + COALESCE(TMCQ.total_mcqs, 0)) AS coeff, CONCAT("Response MCQ ", F.id_mcq, " ", F.ordre) AS reason FROM mcq_found B INNER JOIN exercice_mcq F ON F.id_mcq = B.id_mcq LEFT JOIN (SELECT id_exercice, COUNT(*) AS total_flags FROM exercice_flags GROUP BY id_exercice) T ON F.id_exercice = T.id_exercice LEFT JOIN (SELECT id_exercice, COUNT(*) AS total_mcqs FROM exercice_mcq GROUP BY id_exercice) TMCQ ON F.id_exercice = TMCQ.id_exercice UNION`
}
firstBloodQuery := ""
if FirstBlood != 0.0 {
firstBloodQuery = `SELECT id_team, id_exercice, time, ` + fmt.Sprintf("%f", FirstBlood) + ` AS coeff, "First blood" AS reason FROM exercice_solved JOIN (SELECT id_exercice, MIN(time) time FROM exercice_solved GROUP BY id_exercice) d1 USING (id_exercice, time) UNION`
}
query := `SELECT S.id_team, S.time, E.gain AS points, coeff, S.reason, S.id_exercice FROM (
SELECT id_team, id_exercice, time, ` + fmt.Sprintf("%f", FirstBlood) + ` AS coeff, "First blood" AS reason FROM exercice_solved JOIN (SELECT id_exercice, MIN(time) time FROM exercice_solved GROUP BY id_exercice) d1 USING (id_exercice, time) UNION
SELECT id_team, id_exercice, time, coefficient AS coeff, "Validation" AS reason FROM exercice_solved
` + questionGainQuery + `
` + firstBloodQuery + `
SELECT id_team, id_exercice, time, coefficient - ` + fmt.Sprintf("%f", QuestionGainRatio) + ` AS coeff, "Validation" AS reason FROM exercice_solved
) S INNER JOIN ` + exercices_table + ` E ON S.id_exercice = E.id_exercice UNION ALL
SELECT B.id_team, B.time, F.bonus_gain AS points, 1 AS coeff, "Bonus flag" AS reason, F.id_exercice FROM flag_found B INNER JOIN exercice_flags F ON F.id_flag = B.id_flag WHERE F.bonus_gain != 0 HAVING points != 0 UNION ALL
SELECT id_team, MAX(time) AS time, (FLOOR(COUNT(*)/10 - 1) * (FLOOR(COUNT(*)/10)))/0.2 + (FLOOR(COUNT(*)/10) * (COUNT(*)%10)) AS points, ` + fmt.Sprintf("%f", SubmissionCostBase*-1) + ` AS coeff, "Tries" AS reason, id_exercice FROM ` + tries_table + ` S GROUP BY id_exercice, id_team`

View file

@ -2,6 +2,14 @@ package fic
import ()
const StandaloneExercicesDirectory = "exercices"
var StandaloneExercicesTheme = Theme{
Name: "Défis indépendants",
URLId: "_",
Path: StandaloneExercicesDirectory,
}
// Theme represents a group of challenges, to display to players
type Theme struct {
Id int64 `json:"id"`
@ -62,11 +70,7 @@ func GetThemesExtended() ([]*Theme, error) {
return nil, err
} else {
// Append standalone exercices fake-themes
stdthm := &Theme{
Name: "Défis indépendants",
URLId: "_",
Path: "exercices",
}
stdthm := &StandaloneExercicesTheme
if exercices, err := stdthm.GetExercices(); err == nil && len(exercices) > 0 {
themes = append(themes, stdthm)

View file

@ -46,6 +46,8 @@ type Settings struct {
GlobalScoreCoefficient float64 `json:"globalScoreCoefficient"`
// DiscountedFactor stores the percentage of the exercice's gain lost on each validation.
DiscountedFactor float64 `json:"discountedFactor,omitempty"`
// QuestionGainRatio is the ratio given to a partially solved exercice in the final scoreboard.
QuestionGainRatio float64 `json:"questionGainRatio,omitempty"`
// AllowRegistration permits unregistered Team to register themselves.
AllowRegistration bool `json:"allowRegistration,omitempty"`