From 5df1cc6e936967303eaa7f26e8e35496dd3adf77 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 29 Jan 2020 15:57:34 +0100 Subject: [PATCH] admin: add some stats about exercices --- admin/api/exercice.go | 31 ++++++++++++++++++++++++--- admin/api/theme.go | 21 +++++++++++++++++++ admin/static/js/app.js | 16 +++++++++----- admin/static/views/exercice.html | 8 +++++++ libfic/exercice.go | 36 ++++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 8 deletions(-) diff --git a/admin/api/exercice.go b/admin/api/exercice.go index 2a4eeb27..babf5919 100644 --- a/admin/api/exercice.go +++ b/admin/api/exercice.go @@ -23,6 +23,7 @@ func init() { router.DELETE("/api/exercices/:eid", apiHandler(exerciceHandler(deleteExercice))) router.GET("/api/exercices/:eid/stats.json", apiHandler(exerciceHandler(getExerciceStats))) + router.GET("/api/exercices_stats.json", apiHandler(getExercicesStats)) router.GET("/api/exercices/:eid/history.json", apiHandler(exerciceHandler(getExerciceHistory))) router.PATCH("/api/exercices/:eid/history.json", apiHandler(exerciceHandler(updateExerciceHistory))) @@ -170,9 +171,12 @@ func getExerciceHistory(exercice fic.Exercice, body []byte) (interface{}, error) } type exerciceStats struct { - TeamTries int64 `json:"team_tries"` - TotalTries int64 `json:"total_tries"` - SolvedCount int64 `json:"solved_count"` + IdExercice int64 `json:"id_exercice,omitempty"` + TeamTries int64 `json:"team_tries"` + TotalTries int64 `json:"total_tries"` + SolvedCount int64 `json:"solved_count"` + FlagSolved []int64 `json:"flag_solved"` + MCQSolved []int64 `json:"mcq_solved"` } func getExerciceStats(e fic.Exercice, body []byte) (interface{}, error) { @@ -180,9 +184,30 @@ func getExerciceStats(e fic.Exercice, body []byte) (interface{}, error) { TeamTries: e.TriedTeamCount(), TotalTries: e.TriedCount(), SolvedCount: e.SolvedCount(), + FlagSolved: e.FlagSolved(), + MCQSolved: e.MCQSolved(), }, nil } +func getExercicesStats(_ httprouter.Params, body []byte) (interface{}, error) { + if exercices, err := fic.GetExercices(); err != nil { + return nil, err + } else { + ret := []exerciceStats{} + for _, e := range exercices { + ret = append(ret, exerciceStats{ + IdExercice: e.Id, + TeamTries: e.TriedTeamCount(), + TotalTries: e.TriedCount(), + SolvedCount: e.SolvedCount(), + FlagSolved: e.FlagSolved(), + MCQSolved: e.MCQSolved(), + }) + } + return ret, nil + } +} + type uploadedExerciceHistory struct { IdTeam int64 `json:"team_id"` Kind string diff --git a/admin/api/theme.go b/admin/api/theme.go index 97832c77..8df3b5ac 100644 --- a/admin/api/theme.go +++ b/admin/api/theme.go @@ -25,6 +25,8 @@ func init() { router.GET("/api/themes/:thid/exercices", apiHandler(themeHandler(listThemedExercices))) router.POST("/api/themes/:thid/exercices", apiHandler(themeHandler(createExercice))) + router.GET("/api/themes/:thid/exercices_stats.json", apiHandler(themeHandler(getThemedExercicesStats))) + router.GET("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(showExercice))) router.PUT("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(updateExercice))) router.DELETE("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(deleteExercice))) @@ -205,3 +207,22 @@ func updateTheme(theme fic.Theme, body []byte) (interface{}, error) { func deleteTheme(theme fic.Theme, _ []byte) (interface{}, error) { return theme.Delete() } + +func getThemedExercicesStats(theme fic.Theme, body []byte) (interface{}, error) { + if exercices, err := theme.GetExercices(); err != nil { + return nil, err + } else { + ret := []exerciceStats{} + for _, e := range exercices { + ret = append(ret, exerciceStats{ + IdExercice: e.Id, + TeamTries: e.TriedTeamCount(), + TotalTries: e.TriedCount(), + SolvedCount: e.SolvedCount(), + FlagSolved: e.FlagSolved(), + MCQSolved: e.MCQSolved(), + }) + } + return ret, nil + } +} diff --git a/admin/static/js/app.js b/admin/static/js/app.js index 128e03dd..bf91546c 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -247,6 +247,9 @@ angular.module("FICApp") .factory("ExerciceHistory", function($resource) { return $resource("/api/exercices/:exerciceId/history.json", { exerciceId: '@id' }) }) + .factory("ExercicesStats", function($resource) { + return $resource("/api/themes/:themeId/exercices_stats.json", { themeId: '@id' }) + }) .factory("ExerciceStats", function($resource) { return $resource("/api/exercices/:exerciceId/stats.json", { exerciceId: '@id' }) }) @@ -519,7 +522,6 @@ angular.module("FICApp") var refreshSyncReport = function() { needRefreshSyncReportWhenReady = false; $http.get("full_import_report.json").then(function(response) { - console.log(response.data); $scope.syncReport = response.data; }) }; @@ -1404,12 +1406,12 @@ angular.module("FICApp") $scope.syncHints = true; $scope.syncFlags = true; }) - .controller("ExercicesListController", function($scope, ThemedExercice, $routeParams, $location, $rootScope, $http) { - $scope.exercices = ThemedExercice.query({ themeId: $routeParams.themeId }); + .controller("ExercicesListController", function($scope, ThemedExercice, $location, $rootScope, $http) { + $scope.exercices = ThemedExercice.query({ themeId: $scope.theme.id }); $scope.fields = ["title", "headline", "issue"]; $scope.show = function(id) { - $location.url("/themes/" + $routeParams.themeId + "/exercices/" + id); + $location.url("/themes/" + $scope.theme.id + "/exercices/" + id); }; $scope.inSync = false; @@ -1420,7 +1422,7 @@ angular.module("FICApp") method: "POST" }).then(function(response) { $scope.inSync = false; - $scope.exercices = ThemedExercice.query({ themeId: $routeParams.themeId }); + $scope.exercices = ThemedExercice.query({ themeId: $scope.theme.id }); $rootScope.staticFilesNeedUpdate++; if (response.data) $rootScope.newBox('warning', null, response.data, -1); @@ -1500,6 +1502,10 @@ angular.module("FICApp") } }) + .controller("ExercicesStatsController", function($scope, ExercicesStats) { + $scope.exercices = ExercicesStats.query({ themeId: $scope.theme.id }); + }) + .controller("ExerciceStatsController", function($scope, ExerciceStats, $routeParams) { $scope.stats = ExerciceStats.get({ exerciceId: $routeParams.exerciceId }); }) diff --git a/admin/static/views/exercice.html b/admin/static/views/exercice.html index 2f0e2b93..6821814b 100644 --- a/admin/static/views/exercice.html +++ b/admin/static/views/exercice.html @@ -64,6 +64,14 @@
Défi validé par
+ +
Drapeaux validés
+
{{ stats.flag_solved.length }}
+
aucun
+ +
QCM validés
+
{{ stats.mcq_solved.length }}
+
aucun
diff --git a/libfic/exercice.go b/libfic/exercice.go index 3709272c..47b3c3dc 100644 --- a/libfic/exercice.go +++ b/libfic/exercice.go @@ -360,6 +360,42 @@ func (e Exercice) TriedCount() int64 { } } +// FlagSolved returns the list of flags solved. +func (e Exercice) FlagSolved() (res []int64) { + if rows, err := DBQuery("SELECT F.id_flag FROM flag_found F INNER JOIN exercice_flags E ON E.id_flag = F.id_flag WHERE E.id_exercice = ? GROUP BY id_flag", e.Id); err != nil { + return + } else { + defer rows.Close() + + for rows.Next() { + var n int64 + if err := rows.Scan(&n); err != nil { + return + } + res = append(res, n) + } + return + } +} + +// MCQSolved returns the list of mcqs solved. +func (e Exercice) MCQSolved() (res []int64) { + if rows, err := DBQuery("SELECT F.id_mcq FROM mcq_found F INNER JOIN exercice_mcq E ON E.id_mcq = F.id_mcq WHERE E.id_exercice = ? GROUP BY id_mcq", e.Id); err != nil { + return + } else { + defer rows.Close() + + for rows.Next() { + var n int64 + if err := rows.Scan(&n); err != nil { + return + } + res = append(res, n) + } + return + } +} + // CheckResponse, given both flags and MCQ responses, figures out if thoses are correct (or if they are previously solved). // In the meanwhile, CheckResponse registers good answers given (but it does not mark the challenge as solved at the end). func (e Exercice) CheckResponse(cksum []byte, respflags map[int64]string, respmcq map[int64]bool, t Team) (bool, error) {