admin: Add exercice's tags: sync, api, interface done

This commit is contained in:
nemunaire 2018-11-18 22:44:23 +01:00 committed by Pierre-Olivier Mercier
parent 665fd301c6
commit f183985982
10 changed files with 166 additions and 20 deletions

View File

@ -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)
}

View File

@ -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 });

View File

@ -24,8 +24,8 @@
</div>
</form>
<div class="col-md-4" ng-controller="ExerciceFilesController" ng-show="exercice.id">
<div class="card border-secondary">
<div class="col-md-4" ng-show="exercice.id">
<div class="card border-secondary" ng-controller="ExerciceFilesController">
<div class="card-header bg-secondary text-light">
<h4 class="m-0" ng-click="toggleDownloads()"><small class="glyphicon" ng-class="{'glyphicon-chevron-right': !showDownloads, 'glyphicon-chevron-down': showDownloads}" aria-hidden="true"></small> Téléchargements</h4>
</div>
@ -49,7 +49,7 @@
</div>
</div>
<div class="mt-2 card border-info" ng-controller="ExerciceHintsController" ng-show="exercice.id">
<div class="mt-2 card border-info" ng-controller="ExerciceHintsController">
<div class="card-header bg-info text-light">
<button type="button" ng-click="addHint()" class="float-right btn btn-sm btn-primary ml-2"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
<button type="button" ng-click="syncHints()" class="float-right btn btn-sm btn-light ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span></button>
@ -79,7 +79,7 @@
</div>
</div>
<div class="mt-2 card border-success" ng-controller="ExerciceFlagsController" ng-show="exercice.id">
<div class="mt-2 card border-success" ng-controller="ExerciceFlagsController">
<div class="card-header bg-success border-success text-light">
<button type="button" ng-click="addFlag()" class="float-right btn btn-sm btn-primary ml-2"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
<button type="button" ng-click="syncFlags()" class="float-right btn btn-sm btn-light ml-2"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span></button>
@ -114,7 +114,7 @@
</div>
</div>
<div class="mt-2 card border-success" ng-controller="ExerciceMCQFlagsController" ng-show="exercice.id">
<div class="mt-2 card border-success" ng-controller="ExerciceMCQFlagsController">
<div class="card-header bg-success text-light">
<button type="button" ng-click="addQuiz()" class="float-right btn btn-sm btn-primary ml-2"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
<h4 class="m-0" ng-click="toggleQuizz()"><small class="glyphicon" ng-class="{'glyphicon-chevron-right': !showQuizz, 'glyphicon-chevron-down': showQuizz}" aria-hidden="true"></small> Quizz</h4>
@ -152,6 +152,22 @@
</div>
</div>
<div class="mt-2 card border-warning" ng-controller="ExerciceTagsController">
<div class="card-header bg-warning text-light">
<button type="button" ng-click="addTag()" class="float-right btn btn-sm btn-primary ml-2"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
<button type="button" ng-click="saveTags()" class="float-right btn btn-sm btn-success ml-2" ng-show="showTags"><span class="glyphicon glyphicon-ok" aria-hidden="true"></span></button>
<h4 class="m-0" ng-click="toggleTags()"><small class="glyphicon" ng-class="{'glyphicon-chevron-right': !showTags, 'glyphicon-chevron-down': showTags}" aria-hidden="true"></small> Tags</h4>
</div>
<div class="list-group" ng-show="showTags">
<form ng-submit="saveTags()" class="list-group-item bg-light text-dark">
<div class="row form-group" ng-repeat="(k, tag) in tags track by $index">
<input type="text" ng-model="tags[k]" class="col form-control form-control-sm" placeholder="#tag">
<button type="button" ng-click="deleteTag()" class="btn btn-sm btn-danger col-auto"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></button>
</div>
</form>
</div>
</div>
</div>
</div>

View File

@ -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) ;

View File

@ -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"`

View File

@ -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
}
}
}

View File

@ -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
}

52
libfic/exercice_tag.go Normal file
View File

@ -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
}
}

View File

@ -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.

View File

@ -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(),