admin: Retrieve stats on exercices

This commit is contained in:
nemunaire 2025-02-04 12:33:26 +01:00
parent 63b4cdc622
commit b409fa6806
10 changed files with 241 additions and 19 deletions

View File

@ -78,6 +78,7 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
apiQuizRoutes.PUT("", updateExerciceQuiz)
apiQuizRoutes.DELETE("", deleteExerciceQuiz)
apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps)
apiQuizRoutes.GET("/statistics", showExerciceQuizStats)
apiExercicesRoutes.GET("/tags", listExerciceTags)
apiExercicesRoutes.POST("/tags", addExerciceTag)
@ -864,7 +865,7 @@ func showExerciceFlagStats(c *gin.Context) {
return
}
var completed, tries, nteams int64
var completed int64
for _, hline := range history {
if hline["kind"].(string) == "flag_found" {
@ -874,10 +875,24 @@ func showExerciceFlagStats(c *gin.Context) {
}
}
tries, err := flag.NbTries()
if err != nil {
log.Println("Unable to nbTries:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag tries"})
return
}
teams, err := flag.TeamsOnIt()
if err != nil {
log.Println("Unable to teamsOnIt:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag related teams"})
return
}
c.JSON(http.StatusOK, gin.H{
"completed": completed,
"tries": tries,
"nteams": nteams,
"teams": teams,
})
}
@ -1051,6 +1066,48 @@ func showExerciceQuizDeps(c *gin.Context) {
c.JSON(http.StatusOK, deps)
}
func showExerciceQuizStats(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
quiz := c.MustGet("flag-quiz").(*fic.MCQ)
history, err := exercice.GetHistory()
if err != nil {
log.Println("Unable to getExerciceHistory:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"})
return
}
var completed int64
for _, hline := range history {
if hline["kind"].(string) == "mcq_found" {
if *hline["secondary"].(*int) == quiz.Id {
completed += 1
}
}
}
tries, err := quiz.NbTries()
if err != nil {
log.Println("Unable to nbTries:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag tries"})
return
}
teams, err := quiz.TeamsOnIt()
if err != nil {
log.Println("Unable to teamsOnIt:", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag related teams"})
return
}
c.JSON(http.StatusOK, gin.H{
"completed": completed,
"tries": tries,
"teams": teams,
})
}
func updateExerciceQuiz(c *gin.Context) {
quiz := c.MustGet("flag-quiz").(*fic.MCQ)

View File

@ -358,6 +358,9 @@ angular.module("FICApp")
})
.factory("ExerciceMCQDeps", function ($resource) {
return $resource("api/exercices/:exerciceId/quiz/:mcqId/dependancies", { exerciceId: '@idExercice', mcqId: '@id' })
})
.factory("ExerciceMCQStats", function ($resource) {
return $resource("api/exercices/:exerciceId/quiz/:mcqId/statistics", { exerciceId: '@idExercice', mcqId: '@id' })
});
angular.module("FICApp")
@ -771,6 +774,20 @@ angular.module("FICApp")
});
};
})
.component('teamLink', {
bindings: {
idTeam: '=',
},
controller: function (Team) {
var ctrl = this;
ctrl.team = {};
ctrl.$onInit = function () {
ctrl.team = Team.get({teamId: ctrl.idTeam});
};
},
template: `<a href="/teams/{{ $ctrl.idTeam }}">{{ $ctrl.team.name }}</a> `
})
.component('repositoryUptodate', {
bindings: {
repository: '<',
@ -2400,6 +2417,12 @@ angular.module("FICApp")
}
})
.controller("ExerciceMCQStatsController", function ($scope, $routeParams, ExerciceMCQStats) {
$scope.init = function (mcq) {
$scope.stats = ExerciceMCQStats.get({ exerciceId: $routeParams.exerciceId, mcqId: mcq.id });
}
})
.controller("TeamsListController", function ($scope, $rootScope, Team, $location, $http) {
$scope.teams = Team.query();
$scope.fields = ["id", "name"];

View File

@ -83,9 +83,13 @@
<div ng-controller="ExerciceFlagStatsController" ng-init="init(flag)">
<strong>Statistiques</strong>
<ul>
<li>Validés : {{ stats["completed"] }}</li>
<li>Tentés : {{ stats["tries"] }}</li>
<li>Équipes : {{ stats["nteams"] }}</li>
<li>Validés: {{ stats["completed"] }}</li>
<li>Tentés: {{ stats["tries"] }}</li>
<li>
Équipes:
<span ng-if="stats['teams'].length == 0">aucune</span>
<team-link ng-repeat="team in stats['teams']" id-team="team"></team-link>
</li>
</ul>
</div>
</div>
@ -177,6 +181,19 @@
<dependancy ng-repeat="dep in deps" dep="dep"></dependancy>
</ul>
<span ng-if="deps.length == 0"> sans</span>
<hr>
<div ng-controller="ExerciceMCQStatsController" ng-init="init(q)">
<strong>Statistiques</strong>
<ul>
<li>Validés: {{ stats["completed"] }}</li>
<li>Tentés: {{ stats["tries"] }}</li>
<li>
Équipes:
<span ng-if="stats['teams'].length == 0">aucune</span>
<team-link ng-repeat="team in stats['teams']" id-team="team"></team-link>
</li>
</ul>
</div>
</div>
</form>
</div>

View File

@ -414,6 +414,7 @@ CREATE TABLE IF NOT EXISTS exercice_solved(
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS exercice_tries(
id_try INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
id_exercice INTEGER NOT NULL,
id_team INTEGER NOT NULL,
time TIMESTAMP NOT NULL,
@ -423,6 +424,26 @@ 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_tries_flags(
id_try INTEGER NOT NULL,
id_flag INTEGER NOT NULL,
FOREIGN KEY(id_try) REFERENCES exercice_tries(id_try) ON DELETE CASCADE,
FOREIGN KEY(id_flag) REFERENCES exercice_flags(id_flag) ON DELETE CASCADE
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
`); err != nil {
return err
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS exercice_tries_mcq(
id_try INTEGER NOT NULL,
id_mcq INTEGER NOT NULL,
FOREIGN KEY(id_try) REFERENCES exercice_tries(id_try) ON DELETE CASCADE,
FOREIGN KEY(id_mcq) REFERENCES exercice_mcq(id_mcq) ON DELETE CASCADE
) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
`); err != nil {
return err
}

View File

@ -3,6 +3,7 @@ package fic
import (
"errors"
"fmt"
"log"
"math"
"time"
)
@ -444,11 +445,25 @@ func (e *Exercice) GetOrdinal() (int, error) {
}
// NewTry registers a solving attempt for the given Team.
func (e *Exercice) NewTry(t *Team, cksum []byte) error {
if _, err := DBExec("INSERT INTO exercice_tries (id_exercice, id_team, time, cksum) VALUES (?, ?, ?, ?)", e.Id, t.Id, time.Now(), cksum); err != nil {
return err
func (e *Exercice) NewTry(t *Team, cksum []byte, flags ...Flag) (int64, error) {
if res, err := DBExec("INSERT INTO exercice_tries (id_exercice, id_team, time, cksum) VALUES (?, ?, ?, ?)", e.Id, t.Id, time.Now(), cksum); err != nil {
return 0, err
} else {
return nil
return res.LastInsertId()
}
}
func (e *Exercice) NewTryFlag(tryid int64, flags ...Flag) {
for _, flag := range flags {
if fk, ok := flag.(*FlagKey); ok {
if _, err := DBExec("INSERT INTO exercice_tries_flags (id_try, id_flag) VALUES (?, ?)", tryid, fk.Id); err != nil {
log.Println("Unable to add detailed try: ", err.Error())
}
} else if fm, ok := flag.(*MCQ); ok {
if _, err := DBExec("INSERT INTO exercice_tries_mcq (id_try, id_mcq) VALUES (?, ?)", tryid, fm.Id); err != nil {
log.Println("Unable to add detailed try: ", err.Error())
}
}
}
}
@ -550,7 +565,7 @@ func (e *Exercice) MCQSolved() (res []int64) {
// 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[int]string, respmcq map[int]bool, t *Team) (bool, error) {
if err := e.NewTry(t, cksum); err != nil {
if tryId, err := e.NewTry(t, cksum); err != nil {
return false, err
} else if flags, err := e.GetFlagKeys(); err != nil {
return false, err
@ -565,6 +580,10 @@ func (e *Exercice) CheckResponse(cksum []byte, respflags map[int]string, respmcq
// Check MCQs
for _, mcq := range mcqs {
if mcq.HasOneEntry(respmcq) {
e.NewTryFlag(tryId, mcq)
}
if d := mcq.Check(respmcq); d > 0 {
if !PartialValidation || t.HasPartiallySolved(mcq) == nil {
valid = false
@ -589,15 +608,21 @@ func (e *Exercice) CheckResponse(cksum []byte, respflags map[int]string, respmcq
for _, flag := range flags {
if res, ok := respflags[flag.Id]; !ok && (!PartialValidation || t.HasPartiallySolved(flag) == nil) {
valid = valid && flag.IsOptionnal()
} else if flag.Check([]byte(res)) != 0 {
if !PartialValidation || t.HasPartiallySolved(flag) == nil {
valid = valid && flag.IsOptionnal()
} else if ok {
if len(res) > 0 {
e.NewTryFlag(tryId, flag)
}
} else {
err := flag.FoundBy(t)
if err == nil {
// err is unicity issue, probably flag already found
goodResponses += 1
if flag.Check([]byte(res)) != 0 {
if !PartialValidation || t.HasPartiallySolved(flag) == nil {
valid = valid && flag.IsOptionnal()
}
} else {
err := flag.FoundBy(t)
if err == nil {
// err is unicity issue, probably flag already found
goodResponses += 1
}
}
}
}

View File

@ -68,7 +68,8 @@ func (e *Exercice) AppendHistoryItem(tId int64, kind string, secondary *int64) e
if kind == "tries" {
bid := make([]byte, 5)
binary.LittleEndian.PutUint32(bid, rand.Uint32())
return (&Exercice{Id: e.Id}).NewTry(team, bid)
_, err = (&Exercice{Id: e.Id}).NewTry(team, bid)
return err
} else if kind == "hint" && secondary != nil {
return team.OpenHint(&EHint{Id: *secondary})
} else if kind == "wchoices" && secondary != nil {

View File

@ -14,6 +14,8 @@ type Flag interface {
Check(val interface{}) int
IsOptionnal() bool
FoundBy(t *Team) error
NbTries() (int64, error)
TeamsOnIt() ([]int64, error)
}
// GetFlag returns a list of flags comming with the challenge.

View File

@ -230,6 +230,31 @@ func (k *FlagKey) RecoverId() (Flag, error) {
}
}
// NbTries returns the flag resolution statistics.
func (k *FlagKey) NbTries() (tries int64, err error) {
err = DBQueryRow("SELECT COUNT(*) AS tries FROM exercice_tries_flags WHERE id_flag = ?", k.Id).Scan(&tries)
return
}
func (k *FlagKey) TeamsOnIt() ([]int64, error) {
if rows, err := DBQuery("SELECT DISTINCT M.id_team FROM exercice_tries_flags F INNER JOIN exercice_tries T ON T.id_try = F.id_try INNER JOIN teams M ON M.id_team = T.id_team WHERE id_flag = ?", k.Id); err != nil {
return nil, err
} else {
defer rows.Close()
teams := []int64{}
for rows.Next() {
var idteam int64
if err := rows.Scan(&idteam); err != nil {
return nil, err
}
teams = append(teams, idteam)
}
return teams, nil
}
}
// AddFlagKey creates and fills a new struct Flag, from a hashed flag, and registers it into the database.
func (k *FlagKey) Create(e *Exercice) (Flag, error) {
// Check the regexp compile

View File

@ -71,6 +71,14 @@ func (k *FlagLabel) RecoverId() (Flag, error) {
}
}
func (k *FlagLabel) NbTries() (int64, error) {
return 0, nil
}
func (k *FlagLabel) TeamsOnIt() ([]int64, error) {
return nil, nil
}
// AddFlagLabel creates and fills a new struct Flag and registers it into the database.
func (k *FlagLabel) Create(e *Exercice) (Flag, error) {
if res, err := DBExec("INSERT INTO exercice_flag_labels (id_exercice, ordre, label, variant) VALUES (?, ?, ?, ?)", e.Id, k.Order, k.Label, k.Variant); err != nil {

View File

@ -136,6 +136,31 @@ func (m *MCQ) RecoverId() (Flag, error) {
}
}
// NbTries returns the MCQ resolution statistics.
func (m *MCQ) NbTries() (tries int64, err error) {
err = DBQueryRow("SELECT COUNT(*) AS tries FROM exercice_tries_mcq WHERE id_mcq = ?", m.Id).Scan(&tries)
return
}
func (m *MCQ) TeamsOnIt() ([]int64, error) {
if rows, err := DBQuery("SELECT DISTINCT M.id_team FROM exercice_tries_mcq F INNER JOIN exercice_tries T ON T.id_try = F.id_try INNER JOIN teams M ON M.id_team = T.id_team WHERE id_mcq = ?", m.Id); err != nil {
return nil, err
} else {
defer rows.Close()
teams := []int64{}
for rows.Next() {
var idteam int64
if err := rows.Scan(&idteam); err != nil {
return nil, err
}
teams = append(teams, idteam)
}
return teams, nil
}
}
// Create registers a MCQ into the database and recursively add its entries.
func (m *MCQ) Create(e *Exercice) (Flag, error) {
if res, err := DBExec("INSERT INTO exercice_mcq (id_exercice, ordre, title) VALUES (?, ?, ?)", e.Id, m.Order, m.Title); err != nil {
@ -319,6 +344,24 @@ func (m *MCQ) IsOptionnal() bool {
return false
}
// Check if the given vals contains at least a response for the given MCQ.
func (m *MCQ) HasOneEntry(v interface{}) bool {
var vals map[int]bool
if va, ok := v.(map[int]bool); !ok {
return false
} else {
vals = va
}
for _, n := range m.Entries {
if _, ok := vals[n.Id]; ok {
return true
}
}
return false
}
// Check if the given vals are the expected ones to validate this flag.
func (m *MCQ) Check(v interface{}) int {
var vals map[int]bool