Compare commits
9 commits
d26333c5e2
...
0edf71107a
Author | SHA1 | Date | |
---|---|---|---|
0edf71107a | |||
f841d9c11c | |||
bf2be00f15 | |||
71120c1c89 | |||
5ba86d0c5f | |||
8e196136c3 | |||
4ca2bc106a | |||
74f388a2b9 | |||
5e262b75a3 |
31 changed files with 963 additions and 187 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -23,7 +23,7 @@ func NewThemeError(theme *fic.Theme, err error) *ThemeError {
|
|||
if theme == nil {
|
||||
return &ThemeError{
|
||||
error: err,
|
||||
ThemePath: StandaloneExercicesDirectory,
|
||||
ThemePath: fic.StandaloneExercicesDirectory,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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]}
|
||||
>
|
||||
|
|
|
@ -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} :{/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}
|
||||
|
|
|
@ -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> {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> × <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> × {#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} %</span> ÷ <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> − <span title="Pourcentage des points déjà obtenu au travers des réponses aux questions">{Math.trunc($settings.questionGainRatio * 1000)/10} %</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}
|
||||
|
|
|
@ -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} % 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 :<br>
|
||||
20 × {Math.trunc($settings.questionGainRatio * 1000)/10} % ÷ 5 × 3 = {Math.trunc(20 * $settings.questionGainRatio / 5 * 3 * 100)/100} points.
|
||||
</p>
|
||||
<p>
|
||||
Les {Math.trunc(1000 - $settings.questionGainRatio * 1000)/10} % 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 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 : 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} % la cote de l'exercice.
|
||||
Chaque validation réduit de {$settings.discountedFactor*100} % la cote de l'exercice.
|
||||
</p>
|
||||
<p>
|
||||
Ainsi, pour un exercice d'une valeur initiale de {10*$settings.globalScoreCoefficient} points :
|
||||
|
@ -192,10 +208,12 @@
|
|||
défi.
|
||||
</p>
|
||||
|
||||
<h4>Prem's</h4>
|
||||
<p>
|
||||
Un bonus de +{$settings.firstBlood * 100} % 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} % 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>
|
||||
|
|
|
@ -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...")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"`
|
||||
|
|
Reference in a new issue