From ff3dec059cb5d8b0b372eaa7898f2317fcee3feb Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 16 Jan 2019 05:25:20 +0100 Subject: [PATCH] sync: Refactor exercice flags --- admin/sync/exercice_defines.go | 47 +--- admin/sync/exercice_keys.go | 447 ++++++++++++++++++--------------- 2 files changed, 250 insertions(+), 244 deletions(-) diff --git a/admin/sync/exercice_defines.go b/admin/sync/exercice_defines.go index 483c30df..9ac269b4 100644 --- a/admin/sync/exercice_defines.go +++ b/admin/sync/exercice_defines.go @@ -25,54 +25,23 @@ type ExerciceUnlockFile struct { Filename string `toml:",omitempty"` } -// ExerciceFlag holds informations about a "classic" flag. +// ExerciceFlag holds informations about one flag. type ExerciceFlag struct { Id int64 Label string `toml:",omitempty"` + Type string `toml:",omitempty"` Raw interface{} + Value interface{} `toml:",omitempty"` Separator string `toml:",omitempty"` Ordered bool `toml:",omitempty"` IgnoreCase bool `toml:",omitempty"` ValidatorRe string `toml:"validator_regexp,omitempty"` Help string `toml:",omitempty"` + ChoicesCost int64 `toml:"choices_cost,omitempty"` + Choice []ExerciceFlag LockedFile []ExerciceUnlockFile `toml:"unlock_file,omitempty"` NeedFlag []ExerciceDependency `toml:"need_flag,omitempty"` -} - -// ExerciceFlagMCQChoice holds a choice for an MCQ flag. -type ExerciceFlagMCQChoice struct { - Label string - Value interface{} `toml:",omitempty"` - Help string `toml:",omitempty"` -} - -// ExerciceFlagMCQ holds information about a MCQ flag. -type ExerciceFlagMCQ struct { - Label string `toml:",omitempty"` - Choice []ExerciceFlagMCQChoice - NoShuffle bool -} - -// ExerciceFlagUCQChoice holds a choice for an UCQ flag. -type ExerciceFlagUCQChoice struct { - Label string `toml:",omitempty"` - Value string -} - -// ExerciceFlagUCQ holds information about a UCQ flag. -type ExerciceFlagUCQ struct { - Id int64 - Label string `toml:",omitempty"` - Raw string - IgnoreCase bool `toml:",omitempty"` - ValidatorRe string `toml:"validator_regexp,omitempty"` - Help string `toml:",omitempty"` - DisplayAs string `toml:",omitempty"` - ChoicesCost int64 `toml:"choices_cost,omitempty"` - Choice []ExerciceFlagUCQChoice - LockedFile []ExerciceUnlockFile `toml:"unlock_file,omitempty"` - NeedFlag []ExerciceDependency `toml:"need_flag,omitempty"` - NoShuffle bool + NoShuffle bool } // ExerciceParams contains values parsed from defines.txt. @@ -82,8 +51,8 @@ type ExerciceParams struct { Hints []ExerciceHintParams `toml:"hint"` Dependencies []ExerciceDependency `toml:"depend"` Flags []ExerciceFlag `toml:"flag"` - FlagsMCQ []ExerciceFlagMCQ `toml:"flag_mcq"` - FlagsUCQ []ExerciceFlagUCQ `toml:"flag_ucq"` + FlagsMCQ []ExerciceFlag `toml:"flag_mcq"` + FlagsUCQ []ExerciceFlag `toml:"flag_ucq"` } // parseExerciceParams reads challenge definitions from defines.txt and extract usefull data to set up the challenge. diff --git a/admin/sync/exercice_keys.go b/admin/sync/exercice_keys.go index 71b9a3ef..cd80fdfe 100644 --- a/admin/sync/exercice_keys.go +++ b/admin/sync/exercice_keys.go @@ -31,6 +31,57 @@ func validatorRegexp(vre string) (validator_regexp *string) { return } +func getRawKey(flag ExerciceFlag) (raw string, prep string, errs []string) { + // Concatenate array + if f, ok := flag.Raw.([]interface{}); ok { + if len(flag.ValidatorRe) > 0 { + errs = append(errs, "ValidatorRe cannot be defined for this kind of flag.") + flag.ValidatorRe = "" + } + + if len(flag.Separator) == 0 { + flag.Separator = "," + } else if len(flag.Separator) > 1 { + flag.Separator = string(flag.Separator[0]) + errs = append(errs, "separator truncated to %q") + } + + var fitems []string + for _, v := range f { + if g, ok := v.(string); ok { + if strings.Index(g, flag.Separator) != -1 { + errs = append(errs, "flag items cannot contain %q character as it is used as separator. Change the separator attribute for this flag.") + return + } else { + fitems = append(fitems, g) + } + } else { + errs = append(errs, "item %d has an invalid type: can only be string, is %T.") + return + } + } + + ignord := "t" + if flag.Ordered { + sort.Strings(fitems) + ignord = "f" + } + raw = strings.Join(fitems, flag.Separator) + flag.Separator + + prep = "`" + flag.Separator + ignord + } else if f, ok := flag.Raw.(int64); ok { + raw = fmt.Sprintf("%d", f) + } else if f, ok := flag.Value.(string); ok && len(f) > 0 { + raw = f + } else if f, ok := flag.Raw.(string); !ok { + errs = append(errs, fmt.Sprintf("has an invalid type: can only be []string or string, not %T", flag.Raw)) + return + } else { + raw = f + } + return +} + // SyncExerciceFlags reads the content of challenge.txt and import "classic" flags as Key for the given challenge. func SyncExerciceFlags(i Importer, exercice fic.Exercice) (errs []string) { if _, err := exercice.WipeFlags(); err != nil { @@ -44,8 +95,36 @@ func SyncExerciceFlags(i Importer, exercice fic.Exercice) (errs []string) { } else { kmap := map[int64]fic.Flag{} + // Import UCQ flags + for _, flag := range params.FlagsUCQ { + params.Flags = append(params.Flags, ExerciceFlag{ + Id: flag.Id, + Label: flag.Label, + Type: "ucq", + Raw: flag.Raw, + ValidatorRe: flag.ValidatorRe, + Help: flag.Help, + ChoicesCost: flag.ChoicesCost, + Choice: flag.Choice, + LockedFile: flag.LockedFile, + NeedFlag: flag.NeedFlag, + }) + } + + // Import MCQ flags + for _, flag := range params.FlagsMCQ { + params.Flags = append(params.Flags, ExerciceFlag{ + Id: flag.Id, + Label: flag.Label, + Type: "mcq", + ChoicesCost: flag.ChoicesCost, + Choice: flag.Choice, + LockedFile: flag.LockedFile, + NeedFlag: flag.NeedFlag, + }) + } + // Import normal flags - flags: for nline, flag := range params.Flags { if len(flag.Label) == 0 { flag.Label = "Flag" @@ -56,223 +135,181 @@ func SyncExerciceFlags(i Importer, exercice fic.Exercice) (errs []string) { flag.Label = flag.Label[1:] } - // Concatenate array - var raw string - if f, ok := flag.Raw.([]interface{}); ok { - if len(flag.ValidatorRe) > 0 { - errs = append(errs, fmt.Sprintf("%q: flag #%d: ValidatorRe cannot be defined for this kind of flag.", path.Base(exercice.Path), nline + 1)) - flag.ValidatorRe = "" + switch strings.ToLower(flag.Type) { + case "": + flag.Type = "key" + case "key": + flag.Type = "key" + case "ucq": + flag.Type = "ucq" + case "mcq": + flag.Type = "mcq" + default: + errs = append(errs, fmt.Sprintf("%q: flag #%d: invalid type of flag: should be 'key', 'mcq' or 'ucq'.", path.Base(exercice.Path), nline + 1)) + continue + } + + var addedFlag fic.Flag + + if flag.Type == "key" || flag.Type == "ucq" { + raw, prep, terrs := getRawKey(flag) + + if len(terrs) > 0 { + for _, err := range terrs { + errs = append(errs, fmt.Sprintf("%q: flag #%d: %s", path.Base(exercice.Path), nline + 1, err)) + } + continue + } + flag.Label = prep + flag.Label + + if !isFullGraphic(raw) { + errs = append(errs, fmt.Sprintf("%q: WARNING flag #%d: non-printable characters in flag, is this really expected?", path.Base(exercice.Path), nline + 1)) } - if len(flag.Separator) == 0 { - flag.Separator = "," - } else if len(flag.Separator) > 1 { - flag.Separator = string(flag.Separator[0]) - errs = append(errs, fmt.Sprintf("%q: flag #%d: separator truncated to %q", path.Base(exercice.Path), nline + 1, flag.Separator)) - } + if k, err := exercice.AddRawFlagKey(flag.Label, flag.Help, flag.IgnoreCase, validatorRegexp(flag.ValidatorRe), []byte(raw), flag.ChoicesCost); err != nil { + errs = append(errs, fmt.Sprintf("%q: error flag #%d: %s", path.Base(exercice.Path), nline + 1, err)) + continue + } else { + addedFlag = k + if len(flag.Choice) > 0 || flag.Type == "ucq" { + // Import choices + hasOne := false - var fitems []string - for i, v := range f { - if g, ok := v.(string); ok { - if strings.Index(g, flag.Separator) != -1 { - errs = append(errs, fmt.Sprintf("%q: flag #%d: flag items cannot contain %q character as it is used as separator. Change the separator attribute for this flag.", path.Base(exercice.Path), nline + 1, flag.Separator)) - continue flags + if !flag.NoShuffle { + rand.Shuffle(len(flag.Choice), func(i, j int) { + flag.Choice[i], flag.Choice[j] = flag.Choice[j], flag.Choice[i] + }) + } + + for cid, choice := range flag.Choice { + val, prep, terrs := getRawKey(choice) + if len(terrs) > 0 { + for _, err := range terrs { + errs = append(errs, fmt.Sprintf("%q: flag UCQ #%d: %s", path.Base(exercice.Path), nline + 1, err)) + } + continue + } + choice.Label = prep + choice.Label + + if len(choice.Label) == 0 { + choice.Label = val + } + + if _, err := k.AddChoice(choice.Label, val); err != nil { + errs = append(errs, fmt.Sprintf("%q: error in UCQ %d choice %d: %s", path.Base(exercice.Path), nline + 1, cid, err)) + continue + } + + if val == raw { + hasOne = true + } + } + if !hasOne { + errs = append(errs, fmt.Sprintf("%q: error in UCQ %d: no valid answer defined.", path.Base(exercice.Path), nline + 1)) + } + } + } + } else if flag.Type == "mcq" { + if f, err := exercice.AddMCQ(flag.Label); err != nil { + errs = append(errs, fmt.Sprintf("%q: error flag MCQ #%d: %s", path.Base(exercice.Path), nline + 1, err)) + continue + } else { + addedFlag = f + hasOne := false + isJustified := false + + if !flag.NoShuffle { + rand.Shuffle(len(flag.Choice), func(i, j int) { + flag.Choice[i], flag.Choice[j] = flag.Choice[j], flag.Choice[i] + }) + } + + for cid, choice := range flag.Choice { + var val bool + var justify string + + if choice.Value == nil { + val = false + } else if p, ok := choice.Value.(bool); ok { + val = p + if len(choice.Help) > 0 { + errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: help field has to be used only with justified mcq.", path.Base(exercice.Path), nline + 1)) + continue + } + if isJustified { + errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: all true items has to be justified in this MCQ.", path.Base(exercice.Path), nline + 1)) + continue + } } else { - fitems = append(fitems, g) + if choice.Raw == nil && choice.Value != nil { + choice.Raw = choice.Value + choice.Value = nil + } + if len(choice.Help) == 0 { + choice.Help = "Flag correspondant" + } + if hasOne && !isJustified { + errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: all true items has to be justified in this MCQ.", path.Base(exercice.Path), nline + 1)) + continue + } + + var prep string + var terrs []string + val = true + isJustified = true + justify, prep, terrs = getRawKey(choice) + if len(terrs) > 0 { + for _, err := range terrs { + errs = append(errs, fmt.Sprintf("%q: flag MCQ #%d, choice #%d: %s", path.Base(exercice.Path), nline + 1, cid, err)) + } + continue + } + choice.Help = prep + choice.Help } - } else { - errs = append(errs, fmt.Sprintf("%q: flag #%d, item %d has an invalid type: can only be string, is %T.", path.Base(exercice.Path), nline + 1, i, v)) - continue flags - } - } - ignord := "t" - if flag.Ordered { - sort.Strings(fitems) - ignord = "f" - } - - raw = strings.Join(fitems, flag.Separator) + flag.Separator - - flag.Label = "`" + flag.Separator + ignord + flag.Label - } else if f, ok := flag.Raw.(int64); ok { - raw = fmt.Sprintf("%d", f) - } else if f, ok := flag.Raw.(string); !ok { - errs = append(errs, fmt.Sprintf("%q: flag #%d has an invalid type: can only be []string or string, not %T", path.Base(exercice.Path), nline + 1, flag.Raw)) - continue - } else { - raw = f - } - - if !isFullGraphic(raw) { - errs = append(errs, fmt.Sprintf("%q: WARNING flag #%d: non-printable characters in flag, is this really expected?", path.Base(exercice.Path), nline + 1)) - } - - if k, err := exercice.AddRawFlagKey(flag.Label, flag.Help, flag.IgnoreCase, validatorRegexp(flag.ValidatorRe), []byte(raw), 0); err != nil { - errs = append(errs, fmt.Sprintf("%q: error flag #%d: %s", path.Base(exercice.Path), nline + 1, err)) - continue - } else { - if flag.Id != 0 { - kmap[flag.Id] = k - } - - // Import dependency to flag - for _, nf := range flag.NeedFlag { - if rf, ok := kmap[nf.Id]; !ok { - errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to flag id=%d: id not defined, perhaps not available at time of processing", path.Base(exercice.Path), nline + 1, nf.Id)) - continue - } else if err := k.AddDepend(rf); err != nil { - errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), nline + 1, nf.Id, err)) - continue - } - } - - // Import dependency to file - for _, lf := range flag.LockedFile { - if rf, err := exercice.GetFileByFilename(lf.Filename); err != nil { - errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), nline + 1, lf.Filename, err)) - continue - } else if err := rf.AddDepend(k); err != nil { - errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), nline + 1, lf.Filename, err)) - continue - } - } - } - } - - // Import UCQ flags - for nline, flag := range params.FlagsUCQ { - if len(flag.Label) == 0 { - flag.Label = "Flag" - } - - if !isFullGraphic(flag.Raw) { - errs = append(errs, fmt.Sprintf("%q: WARNING flag UCQ #%d: non-printable characters in flag, is this really expected?", path.Base(exercice.Path), nline + 1)) - } - - if k, err := exercice.AddRawFlagKey(flag.Label, flag.Help, flag.IgnoreCase, validatorRegexp(flag.ValidatorRe), []byte(flag.Raw), flag.ChoicesCost); err != nil { - errs = append(errs, fmt.Sprintf("%q: error flag UCQ #%d: %s", path.Base(exercice.Path), nline + 1, err)) - continue - } else { - // Import dependency to file - if flag.Id != 0 { - kmap[flag.Id] = k - } - - // Import dependency to flag - for _, nf := range flag.NeedFlag { - if rf, ok := kmap[nf.Id]; !ok { - errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to flag id=%s: id not defined, perhaps not available at time of processing", path.Base(exercice.Path), nline + 1, nf.Id)) - continue - } else if err := k.AddDepend(rf); err != nil { - errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), nline + 1, nf.Id, err)) - continue - } - } - - // Import dependency to file - for _, lf := range flag.LockedFile { - if rf, err := exercice.GetFileByFilename(lf.Filename); err != nil { - errs = append(errs, fmt.Sprintf("%q: error flag UCQ #%d dependency to %s: %s", path.Base(exercice.Path), nline + 1, lf.Filename, err)) - continue - } else if err := rf.AddDepend(k); err != nil { - errs = append(errs, fmt.Sprintf("%q: error flag UCQ #%d dependency to %s: %s", path.Base(exercice.Path), nline + 1, lf.Filename, err)) - continue - } - } - - // Import choices - hasOne := false - - if !flag.NoShuffle { - rand.Shuffle(len(flag.Choice), func(i, j int) { - flag.Choice[i], flag.Choice[j] = flag.Choice[j], flag.Choice[i] - }) - } - - for cid, choice := range flag.Choice { - if len(choice.Label) == 0 { - choice.Label = choice.Value - } - - if _, err := k.AddChoice(choice.Label, choice.Value); err != nil { - errs = append(errs, fmt.Sprintf("%q: error in UCQ %d choice %d: %s", path.Base(exercice.Path), nline + 1, cid, err)) - continue - } - - if choice.Value == flag.Raw { - hasOne = true - } - } - if !hasOne { - errs = append(errs, fmt.Sprintf("%q: error in UCQ %d: no valid answer defined.", path.Base(exercice.Path), nline + 1)) - } - } - } - - // Import MCQ flags - for nline, quest := range params.FlagsMCQ { - if flag, err := exercice.AddMCQ(quest.Label); err != nil { - errs = append(errs, fmt.Sprintf("%q: error flag MCQ #%d: %s", path.Base(exercice.Path), nline + 1, err)) - continue - } else { - hasOne := false - isJustified := false - - if !quest.NoShuffle { - rand.Shuffle(len(quest.Choice), func(i, j int) { - quest.Choice[i], quest.Choice[j] = quest.Choice[j], quest.Choice[i] - }) - } - - for cid, choice := range quest.Choice { - var val bool - var justify string - - if choice.Value == nil { - val = false - } else if p, ok := choice.Value.(bool); ok { - val = p - if len(choice.Help) > 0 { - errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: help field has to be used only with justified mcq.", path.Base(exercice.Path), nline + 1)) + if e, err := f.AddEntry(choice.Label, val); err != nil { + errs = append(errs, fmt.Sprintf("%q: error in MCQ %d choice %d: %s", path.Base(exercice.Path), nline + 1, cid, err)) continue + } else if len(justify) > 0 { + if _, err := exercice.AddRawFlagKey(fmt.Sprintf("%%%d%%%s", e.Id, choice.Help), "", false, nil, []byte(justify), 0); err != nil { + errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: %s", path.Base(exercice.Path), nline + 1, err)) + continue + } } - if isJustified { - errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: all true items has to be justified in this MCQ.", path.Base(exercice.Path), nline + 1)) - continue - } - } else if p, ok := choice.Value.(string); ok { - val = true - if len(choice.Help) == 0 { - choice.Help = "Flag correspondant" - } - if hasOne && !isJustified { - errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: all true items has to be justified in this MCQ.", path.Base(exercice.Path), nline + 1)) - continue - } - isJustified = true - justify = p - } else { - errs = append(errs, fmt.Sprintf("%q: error in MCQ %d choice %d: has an invalid type. Expected true, false or a string", path.Base(exercice.Path), nline + 1, cid)) - continue - } - if e, err := flag.AddEntry(choice.Label, val); err != nil { - errs = append(errs, fmt.Sprintf("%q: error in MCQ %d choice %d: %s", path.Base(exercice.Path), nline + 1, cid, err)) - continue - } else if len(justify) > 0 { - if _, err := exercice.AddRawFlagKey(fmt.Sprintf("%%%d%%%s", e.Id, choice.Help), "", false, nil, []byte(justify), 0); err != nil { - errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: %s", path.Base(exercice.Path), nline + 1, err)) - continue + if val { + hasOne = true } } - - if val { - hasOne = true + if !hasOne { + errs = append(errs, fmt.Sprintf("%q: warning MCQ %d: no valid answer defined, is this really expected?", path.Base(exercice.Path), nline + 1)) } } - if !hasOne { - errs = append(errs, fmt.Sprintf("%q: warning MCQ %d: no valid answer defined, is this really expected?", path.Base(exercice.Path), nline + 1)) + } + + if flag.Id != 0 { + kmap[flag.Id] = addedFlag + } + + // Import dependency to flag + for _, nf := range flag.NeedFlag { + if rf, ok := kmap[nf.Id]; !ok { + errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to flag id=%d: id not defined, perhaps not available at time of processing", path.Base(exercice.Path), nline + 1, nf.Id)) + continue + } else if err := addedFlag.AddDepend(rf); err != nil { + errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to id=%d: %s", path.Base(exercice.Path), nline + 1, nf.Id, err)) + continue + } + } + + // Import dependency to file + for _, lf := range flag.LockedFile { + if rf, err := exercice.GetFileByFilename(lf.Filename); err != nil { + errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), nline + 1, lf.Filename, err)) + continue + } else if err := rf.AddDepend(addedFlag); err != nil { + errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), nline + 1, lf.Filename, err)) + continue } } }