From f183985982f022796b42ed274472be01583c0d0c Mon Sep 17 00:00:00 2001 From: nemunaire Date: Sun, 18 Nov 2018 22:44:23 +0100 Subject: [PATCH] admin: Add exercice's tags: sync, api, interface done --- admin/api/exercice.go | 30 ++++++++++++++++++ admin/static/js/app.js | 26 ++++++++++++++++ admin/static/views/exercice.html | 26 +++++++++++++--- admin/sync/README.md | 1 + admin/sync/exercice_defines.go | 1 + admin/sync/exercices.go | 24 ++++++++++----- libfic/db.go | 9 ++++++ libfic/exercice_tag.go | 52 ++++++++++++++++++++++++++++++++ libfic/reset.go | 2 +- libfic/theme_export.go | 15 +++++---- 10 files changed, 166 insertions(+), 20 deletions(-) create mode 100644 libfic/exercice_tag.go diff --git a/admin/api/exercice.go b/admin/api/exercice.go index 87d28900..249de0b1 100644 --- a/admin/api/exercice.go +++ b/admin/api/exercice.go @@ -41,6 +41,10 @@ func init() { router.PUT("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(updateExerciceQuiz))) router.DELETE("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(deleteExerciceQuiz))) + router.GET("/api/exercices/:eid/tags", apiHandler(exerciceHandler(listExerciceTags))) + router.POST("/api/exercices/:eid/tags", apiHandler(exerciceHandler(addExerciceTag))) + router.PUT("/api/exercices/:eid/tags", apiHandler(exerciceHandler(updateExerciceTags))) + // Synchronize router.POST("/api/sync/exercices/:eid/files", apiHandler(exerciceHandler( func(exercice fic.Exercice, _ []byte) (interface{}, error) { @@ -352,3 +356,29 @@ func showExerciceFile(file fic.EFile, body []byte) (interface{}, error) { func deleteExerciceFile(file fic.EFile, _ []byte) (interface{}, error) { return file.Delete() } + + +func listExerciceTags(exercice fic.Exercice, _ []byte) (interface{}, error) { + return exercice.GetTags() +} + +func addExerciceTag(exercice fic.Exercice, body []byte) (interface{}, error) { + var ut []string + if err := json.Unmarshal(body, &ut); err != nil { + return nil, err + } + + // TODO: a DB transaction should be done here: on error we should rollback + for _, t := range ut { + if _, err := exercice.AddTag(t); err != nil { + return nil, err + } + } + + return ut, nil +} + +func updateExerciceTags(exercice fic.Exercice, body []byte) (interface{}, error) { + exercice.WipeTags() + return addExerciceTag(exercice, body) +} diff --git a/admin/static/js/app.js b/admin/static/js/app.js index b56d5268..9372bb69 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -208,6 +208,11 @@ angular.module("FICApp") update: {method: 'PUT'} }) }) + .factory("ExerciceTags", function($resource) { + return $resource("/api/exercices/:exerciceId/tags", { exerciceId: '@idExercice'}, { + update: {method: 'PUT'} + }) + }) .factory("ExerciceFile", function($resource) { return $resource("/api/exercices/:exerciceId/files/:fileId", { exerciceId: '@idExercice', fileId: '@id' }, { update: {method: 'PUT'} @@ -1023,6 +1028,11 @@ angular.module("FICApp") $scope.exercices = Exercice.query(); $scope.fields = ["title", "urlid", "statement", "overview", "depend", "gain", "coefficient", "videoURI"]; + $scope.showTags = false; + $scope.toggleTags = function(val) { + $scope.showTags = val ||!$scope.showTags; + } + $scope.showDownloads = false; $scope.toggleDownloads = function(val) { $scope.showDownloads = val ||!$scope.showDownloads; @@ -1054,6 +1064,22 @@ angular.module("FICApp") } }) + .controller("ExerciceTagsController", function($scope, ExerciceTags, $routeParams, $rootScope, $http) { + $scope.tags = ExerciceTags.query({ exerciceId: $routeParams.exerciceId }); + + $scope.addTag = function() { + $scope.toggleTags(true); + $scope.tags.push(""); + } + $scope.deleteTag = function() { + $scope.tags.splice($scope.tags.indexOf(this.tag), 1); + return $scope.saveTags(); + } + $scope.saveTags = function() { + ExerciceTags.update({ exerciceId: $routeParams.exerciceId }, this.tags); + } + }) + .controller("ExerciceFilesController", function($scope, ExerciceFile, $routeParams, $rootScope, $http) { $scope.files = ExerciceFile.query({ exerciceId: $routeParams.exerciceId }); diff --git a/admin/static/views/exercice.html b/admin/static/views/exercice.html index 7b3092d2..4119e4a1 100644 --- a/admin/static/views/exercice.html +++ b/admin/static/views/exercice.html @@ -24,8 +24,8 @@ -
-
+
+

Téléchargements

@@ -49,7 +49,7 @@
-
+
@@ -79,7 +79,7 @@
-
+
@@ -114,7 +114,7 @@
-
+

Quizz

@@ -152,6 +152,22 @@
+
+
+ + +

Tags

+
+
+
+
+ + +
+
+
+
+
diff --git a/admin/sync/README.md b/admin/sync/README.md index 846cf8e9..428a9e6b 100644 --- a/admin/sync/README.md +++ b/admin/sync/README.md @@ -11,6 +11,7 @@ Tous les textes doivent utiliser l'encodage UTF8. + `statement.txt` contenant le scénario du challenge, tel qu'il sera affiché sur le site, à destination des participants + `challenge.txt` définitions des paramètres de votre challenge (au format [toml](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)) : - `gain = 42` : nombre de points que rapporte cet exercice ; + - `tags = ["Android", "RAT", "ROM"]` : mots-clefs de l'exercice ; - `[[depend]]` : dépendance à un autre exercice : * `id = CHID` : identifiant du challenge ; * `theme = "NomDuTheme"` : (facultatif) nom du thème dans lequel aller chercher l'identifiant (par défaut, on prend le thème courant) ; diff --git a/admin/sync/exercice_defines.go b/admin/sync/exercice_defines.go index 8947cf5f..641984e5 100644 --- a/admin/sync/exercice_defines.go +++ b/admin/sync/exercice_defines.go @@ -69,6 +69,7 @@ type ExerciceFlagUCQ struct { // ExerciceParams contains values parsed from defines.txt. type ExerciceParams struct { Gain int64 + Tags []string Hints []ExerciceHintParams `toml:"hint"` Dependencies []ExerciceDependency `toml:"depend"` Flags []ExerciceFlag `toml:"flag"` diff --git a/admin/sync/exercices.go b/admin/sync/exercices.go index 0e6b5258..b32873c8 100644 --- a/admin/sync/exercices.go +++ b/admin/sync/exercices.go @@ -69,6 +69,7 @@ func SyncExercices(i Importer, theme fic.Theme) []string { // Handle score gain var gain int64 var depend *fic.Exercice + var tags []string if p, err := parseExerciceParams(i, path.Join(theme.Path, edir)); err != nil { errs = append(errs, fmt.Sprintf("%q: challenge.txt: %s", edir, err)) continue @@ -76,6 +77,7 @@ func SyncExercices(i Importer, theme fic.Theme) []string { errs = append(errs, fmt.Sprintf("%q: challenge.txt: Undefined gain for challenge", edir)) } else { gain = p.Gain + tags = p.Tags // Handle dependency if len(p.Dependencies) > 0 { @@ -110,12 +112,11 @@ func SyncExercices(i Importer, theme fic.Theme) []string { statement = string(blackfriday.Run([]byte(statement))) overview = string(blackfriday.Run([]byte(overview))) - if e, err := theme.GetExerciceByTitle(ename); err != nil { - if ex, err := theme.AddExercice(ename, fic.ToURLid(ename), path.Join(theme.Path, edir), statement, overview, depend, gain, videoURI); err != nil { + e, err := theme.GetExerciceByTitle(ename) + if err != nil { + if e, err = theme.AddExercice(ename, fic.ToURLid(ename), path.Join(theme.Path, edir), statement, overview, depend, gain, videoURI); err != nil { errs = append(errs, fmt.Sprintf("%q: error on exercice add: %s", edir, err)) continue - } else { - dmap[int64(eid)] = ex } } else if e.Title != ename || e.URLId == "" || e.Statement != statement || e.Overview != overview || e.Gain != gain || e.VideoURI != videoURI { e.Title = ename @@ -127,11 +128,18 @@ func SyncExercices(i Importer, theme fic.Theme) []string { if _, err := e.Update(); err != nil { errs = append(errs, fmt.Sprintf("%q: error on exercice update: %s", edir, err)) continue - } else { - dmap[int64(eid)] = e } - } else { - dmap[int64(eid)] = e + } + dmap[int64(eid)] = e + + if _, err := e.WipeTags(); err != nil { + errs = append(errs, fmt.Sprintf("%q: Unable to wipe tags: %s", edir, err)) + } + for _, tag := range tags { + if _, err := e.AddTag(tag); err != nil { + errs = append(errs, fmt.Sprintf("%q: Unable to add tag: %s", edir, err)) + continue + } } } diff --git a/libfic/db.go b/libfic/db.go index 0fbd4fa4..31d66816 100644 --- a/libfic/db.go +++ b/libfic/db.go @@ -246,6 +246,15 @@ CREATE TABLE IF NOT EXISTS exercice_tries( FOREIGN KEY(id_exercice) REFERENCES exercices(id_exercice), FOREIGN KEY(id_team) REFERENCES teams(id_team) ) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; +`); err != nil { + return err + } + if _, err := db.Exec(` +CREATE TABLE IF NOT EXISTS exercice_tags( + id_exercice INTEGER NOT NULL, + tag VARCHAR(255) NOT NULL, + FOREIGN KEY(id_exercice) REFERENCES exercices(id_exercice) +) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; `); err != nil { return err } diff --git a/libfic/exercice_tag.go b/libfic/exercice_tag.go new file mode 100644 index 00000000..22c4088b --- /dev/null +++ b/libfic/exercice_tag.go @@ -0,0 +1,52 @@ +package fic + +// GetTags returns tags associated with this exercice. +func (e Exercice) GetTags() (tags []string, err error) { + if rows, errr := DBQuery("SELECT tag FROM exercice_tags WHERE id_exercice = ?", e.Id); errr != nil { + return nil, errr + } else { + defer rows.Close() + + tags = make([]string, 0) + for rows.Next() { + var t string + if err = rows.Scan(&t); err != nil { + return + } + tags = append(tags, t) + } + err = rows.Err() + return + } +} + +// AddTag assign a new tag to the exercice and registers it into the database. +func (e Exercice) AddTag(tag string) (string, error) { + if _, err := DBExec("INSERT INTO exercice_tags (id_exercice, tag) VALUES (?, ?)", e.Id, tag); err != nil { + return "", err + } else { + return tag, nil + } +} + +// DeleteTag delete a tag assigned to the current exercice from the database. +func (e Exercice) DeleteTag(tag string) (int64, error) { + if res, err := DBExec("DELETE FROM exercice_tags WHERE id_exercice = ? AND tag = ?", e.Id, tag); err != nil { + return 0, err + } else if nb, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return nb, err + } +} + +// WipeTags delete all tag assigned to the current exercice from the database. +func (e Exercice) WipeTags() (int64, error) { + if res, err := DBExec("DELETE FROM exercice_tags WHERE id_exercice = ?", e.Id); err != nil { + return 0, err + } else if nb, err := res.RowsAffected(); err != nil { + return 0, err + } else { + return nb, err + } +} diff --git a/libfic/reset.go b/libfic/reset.go index 0c0f03b5..a738e077 100644 --- a/libfic/reset.go +++ b/libfic/reset.go @@ -34,7 +34,7 @@ func ResetGame() (error) { // ResetExercices wipes out all challenges (both attempts and statements). func ResetExercices() (error) { - return truncateTable("team_hints", "exercice_files_deps", "exercice_files", "flag_found", "exercice_flags", "exercice_solved", "exercice_tries", "exercice_hints", "mcq_found", "mcq_entries", "exercice_mcq", "exercices", "themes") + return truncateTable("team_hints", "exercice_files_deps", "exercice_files", "flag_found", "exercice_flags", "exercice_solved", "exercice_tries", "exercice_hints", "mcq_found", "mcq_entries", "exercice_mcq", "exercice_tags", "exercices", "themes") } // ResetTeams wipes out all teams, incluings members and attempts. diff --git a/libfic/theme_export.go b/libfic/theme_export.go index 62fea57f..c691e883 100644 --- a/libfic/theme_export.go +++ b/libfic/theme_export.go @@ -6,12 +6,13 @@ import ( // exportedExercice is a structure representing a challenge, as exposed to players. type exportedExercice struct { - Title string `json:"title"` - URLId string `json:"urlid"` - Gain int64 `json:"gain"` - Coeff float64 `json:"curcoeff"` - Solved int64 `json:"solved"` - Tried int64 `json:"tried"` + Title string `json:"title"` + URLId string `json:"urlid"` + Tags []string `json:"tags"` + Gain int64 `json:"gain"` + Coeff float64 `json:"curcoeff"` + Solved int64 `json:"solved"` + Tried int64 `json:"tried"` } // exportedTheme is a structure representing a Theme, as exposed to players. @@ -35,9 +36,11 @@ func ExportThemes() (interface{}, error) { } else { exos := map[string]exportedExercice{} for _, exercice := range exercices { + tags, _ := exercice.GetTags() exos[fmt.Sprintf("%d", exercice.Id)] = exportedExercice{ exercice.Title, exercice.URLId, + tags, exercice.Gain, exercice.Coefficient, exercice.SolvedCount(),