diff --git a/admin/api/exercice.go b/admin/api/exercice.go index f93e9c18..d5b9e71f 100644 --- a/admin/api/exercice.go +++ b/admin/api/exercice.go @@ -44,6 +44,8 @@ func init() { func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceHints(sync.GlobalImporter, exercice), nil }))) router.GET("/api/sync/exercices/:eid/keys", apiHandler(exerciceHandler( func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceKeys(sync.GlobalImporter, exercice), nil }))) + router.GET("/api/sync/exercices/:eid/quiz", apiHandler(exerciceHandler( + func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceMCQ(sync.GlobalImporter, exercice), nil }))) } func listExercices(_ httprouter.Params, body []byte) (interface{}, error) { diff --git a/admin/api/theme.go b/admin/api/theme.go index c339b226..6e6ec069 100644 --- a/admin/api/theme.go +++ b/admin/api/theme.go @@ -57,6 +57,8 @@ func init() { func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceHints(sync.GlobalImporter, exercice), nil }))) router.GET("/api/sync/themes/:thid/exercices/:eid/keys", apiHandler(exerciceHandler( func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceKeys(sync.GlobalImporter, exercice), nil }))) + router.GET("/api/sync/themes/:thid/exercices/:eid/quiz", apiHandler(exerciceHandler( + func(exercice fic.Exercice, _ []byte) (interface{}, error) { return sync.SyncExerciceMCQ(sync.GlobalImporter, exercice), nil }))) } func bindingFiles(_ httprouter.Params, body []byte) (interface{}, error) { diff --git a/admin/api/version.go b/admin/api/version.go index 61b0a6c7..d99314f8 100644 --- a/admin/api/version.go +++ b/admin/api/version.go @@ -9,5 +9,5 @@ func init() { } func showVersion(_ httprouter.Params, body []byte) (interface{}, error) { - return map[string]interface{}{"version": 0.3}, nil + return map[string]interface{}{"version": 0.4}, nil } diff --git a/admin/sync/exercice_keys.go b/admin/sync/exercice_keys.go index 72735ec5..91da833d 100644 --- a/admin/sync/exercice_keys.go +++ b/admin/sync/exercice_keys.go @@ -44,3 +44,87 @@ func SyncExerciceKeys(i Importer, exercice fic.Exercice) []string { } return errs } + +func SyncExerciceMCQ(i Importer, exercice fic.Exercice) []string { + var errs []string + + if _, err := exercice.WipeMCQs(); err != nil { + errs = append(errs, err.Error()) + return errs + } + + // Unique Choice Questions (checkbox) + if ucq, err := getFileContent(i, path.Join(exercice.Path, "flags-ucq.txt")); err != nil { + errs = append(errs, fmt.Sprintf("%q: unable to read ucq: %s", path.Base(exercice.Path), err)) + } else if flag, err := exercice.AddMCQ("", "checkbox"); err != nil { + errs = append(errs, fmt.Sprintf("%q: unable to add ucq: %s", path.Base(exercice.Path), err)) + } else { + for nline, quest := range strings.Split(ucq, "\n") { + if len(quest) == 0 { + continue + } + + if len(quest) < 2 { + errs = append(errs, fmt.Sprintf("%q: error in ucq file at line %d: missing response", path.Base(exercice.Path), nline + 1)) + continue + } + + // Expect 0 or 1 + if quest[0] != 48 && quest[0] != 49 { + errs = append(errs, fmt.Sprintf("%q: error in ucq file at line %d: invalid format: first character has to be either 0 or 1", path.Base(exercice.Path), nline + 1)) + continue + } + + if _, err := flag.AddEntry(quest[1:], quest[0] == 49); err != nil { + errs = append(errs, fmt.Sprintf("%q: error in ucq file at line %d: %s", path.Base(exercice.Path), nline + 1, err)) + } + } + } + + // Multiple Choice Questions (radio) + if mcq, err := getFileContent(i, path.Join(exercice.Path, "flags-mcq.txt")); err != nil { + errs = append(errs, fmt.Sprintf("%q: unable to read mcq: %s", path.Base(exercice.Path), err)) + } else { + for nline, quest := range strings.Split(mcq, "\n") { + quest_splt := strings.Split(string(quest), "\t") + if len(quest_splt) < 2 { + errs = append(errs, fmt.Sprintf("%q: error in mcq file at line %d: not enough responses", path.Base(exercice.Path), nline + 1)) + continue + } + + if flag, err := exercice.AddMCQ(quest_splt[0], "radio"); err != nil { + errs = append(errs, fmt.Sprintf("%q: error in mcq file at line %d: %s", path.Base(exercice.Path), nline + 1, err)) + continue + } else { + hasOne := false + for cid, choice := range quest_splt[1:] { + // Expect 0 or 1 + if choice[0] != 48 && choice[0] != 49 { + errs = append(errs, fmt.Sprintf("%q: error in mcq file at line %d,%d: invalid format: first character has to be either 0 or 1", path.Base(exercice.Path), nline + 1, cid)) + continue + } + + if _, err := flag.AddEntry(choice[1:], choice[0] == 49); err != nil { + errs = append(errs, fmt.Sprintf("%q: error in mcq file at line %d,%d: %s", path.Base(exercice.Path), nline + 1, cid, err)) + continue + } + + if choice[0] == 49 { + if hasOne { + flag.Kind = "checkbox" + flag.Update() + errs = append(errs, fmt.Sprintf("%q: warning in mcq file at line %d: multiple expected response, switching to ucq-like quiz; is this really expected?", path.Base(exercice.Path), nline + 1)) + } else { + hasOne = true + } + } + } + if !hasOne { + errs = append(errs, fmt.Sprintf("%q: warning in mcq file at line %d: no valid answer defined, is this really expected?", path.Base(exercice.Path), nline + 1)) + } + } + } + } + + return errs +} diff --git a/libfic/mcq.go b/libfic/mcq.go index 59170a5d..3487b71a 100644 --- a/libfic/mcq.go +++ b/libfic/mcq.go @@ -119,11 +119,21 @@ func (n MCQ_entry) Delete() (int64, error) { } } +func (e Exercice) WipeMCQs() (int64, error) { + if res, err := DBExec("DELETE FROM exercice_mcq, mcq_found, mcq_entries USING exercice_mcq NATURAL JOIN mcq_entries NATURAL JOIN mcq_found WHERE exercice_mcq.id_exercice = ?", e.Id); err != nil { + return 0, err + } else if nb, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return nb, err + } +} + func (m MCQ) Check(vals map[int64]bool) int { diff := 0 for _, n := range m.Entries { - if v, ok := vals[n.Id]; ok && v == n.Response { + if v, ok := vals[n.Id]; (ok || !n.Response) && v == n.Response { continue } diff += 1