Compare commits

...

3 commits

Author SHA1 Message Date
f6713c768b admin: Better identify tries on exercice page
All checks were successful
continuous-integration/drone/push Build is passing
2025-03-30 15:20:11 +02:00
38e3a4efdf admin: Obtain full current gains from a solved exercice 2025-03-30 13:46:10 +02:00
9f25bc54d3 Round score instead of floor + display score100 to player 2025-03-30 13:31:52 +02:00
9 changed files with 184 additions and 9 deletions

View file

@ -36,9 +36,16 @@ func declareExercicesRoutes(router *gin.RouterGroup) {
apiExercicesRoutes.POST("/diff-sync", APIDiffExerciceWithRemote)
apiExercicesRoutes.GET("/history.json", getExerciceHistory)
apiExercicesRoutes.GET("/stats.json", getExerciceStats)
apiExercicesRoutes.GET("/history.json", getExerciceHistory)
apiExercicesRoutes.GET("/tries", listTries)
apiTriesRoutes := apiExercicesRoutes.Group("/tries/:trid")
apiTriesRoutes.Use(ExerciceTryHandler)
apiTriesRoutes.GET("", getExerciceTry)
apiTriesRoutes.DELETE("", deleteExerciceTry)
apiHistoryRoutes := apiExercicesRoutes.Group("/history.json")
apiHistoryRoutes.Use(AssigneeCookieHandler)
@ -1619,3 +1626,58 @@ func APIDiffExerciceWithRemote(c *gin.Context) {
c.JSON(http.StatusOK, diffs)
}
func listTries(c *gin.Context) {
exercice := c.MustGet("exercice").(*fic.Exercice)
tries, err := exercice.TriesList()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, tries)
}
func ExerciceTryHandler(c *gin.Context) {
trid, err := strconv.ParseInt(string(c.Params.ByName("trid")), 10, 32)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid try identifier"})
return
}
exercice := c.MustGet("exercice").(*fic.Exercice)
try, err := exercice.GetTry(trid)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Try not found"})
return
}
c.Set("try", try)
c.Next()
}
func getExerciceTry(c *gin.Context) {
try := c.MustGet("try").(*fic.ExerciceTry)
err := try.FillDetails()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.JSON(http.StatusOK, try)
}
func deleteExerciceTry(c *gin.Context) {
try := c.MustGet("try").(*fic.ExerciceTry)
_, err := try.Delete()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
return
}
c.Status(http.StatusNoContent)
}

View file

@ -320,6 +320,9 @@ angular.module("FICApp")
.factory("ExerciceHistory", function ($resource) {
return $resource("api/exercices/:exerciceId/history.json", { exerciceId: '@id' })
})
.factory("ExerciceTries", function ($resource) {
return $resource("api/exercices/:exerciceId/tries/:tryId", { exerciceId: '@idExercice', tryId: '@id' })
})
.factory("ExercicesStats", function ($resource) {
return $resource("api/exercices_stats.json", { themeId: '@id' })
})
@ -2117,6 +2120,10 @@ angular.module("FICApp")
}
})
.controller("SearchTryController", function ($scope, ExerciceTries) {
$scope.tr = ExerciceTries.get({ exerciceId: $scope.exercice.id, tryId: $scope.row.secondary });
})
.controller("SubmissionsStatsController", function ($scope, $http, $interval) {
var refresh = function () {
$http({

View file

@ -83,6 +83,7 @@
<div ng-controller="ExerciceFlagStatsController" ng-init="init(flag)">
<strong>Statistiques</strong>
<ul>
<li>ID: {{ flag.id }}</li>
<li>Validés: {{ stats["completed"] }}</li>
<li>
Tentés: {{ stats["tries"] }}
@ -188,6 +189,7 @@
<div ng-controller="ExerciceMCQStatsController" ng-init="init(q)">
<strong>Statistiques</strong>
<ul>
<li>ID: {{ q.id }}</li>
<li>Validés: {{ stats["completed"] }}</li>
<li>
Tentés: {{ stats["tries"] }}

View file

@ -347,7 +347,8 @@
<a href="exercices/{{ row.primary }}#quizz-{{ row.secondary }}" ng-if="row.kind == 'mcq_found'">{{ row.secondary_title }}</a>
<a href="exercices/{{ row.primary }}#hint-{{ row.secondary }}" ng-if="row.kind == 'hint'">{{ row.secondary_title }}</a>
</span>
<span ng-if="!row.secondary_title && row.secondary && row.kind != 'solved'">: {{ row.secondary }}</span>
<span ng-if="!row.secondary_title && row.secondary && row.kind != 'solved' && row.kind != 'tries'">: {{ row.secondary }}</span>
<span ng-if="!row.secondary_title && row.secondary && row.kind == 'tries'" ng-controller="SearchTryController"><br><span ng-repeat="line in tr.details"><span ng-if="!$first">, </span>{{ line.kind }}<span ng-if="line.related">#{{ line.related }}</span></span></span>
</td>
<td style="vertical-align: middle; padding: 0; background-color: {{ row.team_color }}" ng-show="logged">
<button type="button" data-toggle="modal" data-target="#updHistory" ng-if="row.kind != 'flag_found' && row.kind != 'tries' && row.kind != 'mcq_found'" data-idteam="{{ row.team_id }}" data-kind="{{ row.kind }}" data-time="{{ row.time }}" data-secondary="{{ row.secondary }}" data-coeff="{{ row.coefficient }}" class="float-right btn btn-sm btn-info"><span class="glyphicon glyphicon-edit" aria-hidden="true"></span></button>

View file

@ -36,7 +36,7 @@
<tfoot>
<th></th>
<th></th>
<th>{{ my.score }}</th>
<th>{{ my.score100 / 100 }}</th>
</thead>
</tbody>
</table>

View file

@ -95,7 +95,7 @@
<Nav class="ms-auto text-light" navbar>
{#if $my && $my.team_id}
<NavItem>
{Math.round($my.score*100)/100} {$my.score === 1 ? 'point' : 'points'}
{$my.score100/100} {Math.abs($my.score) < 2 ? 'point' : 'points'}
{#if $teams && $teams[$my.team_id] && $teams[$my.team_id].rank}
&ndash; {$teams[$my.team_id].rank}<sup>e</sup> sur {Object.keys($teams).length}
{/if}

View file

@ -11,7 +11,7 @@ import (
func (e *Exercice) GetHistory() ([]map[string]interface{}, error) {
hist := make([]map[string]interface{}, 0)
if rows, err := DBQuery(`SELECT id_team, U.name, U.color, "tries" AS kind, time, 0, id_exercice, NULL, NULL FROM exercice_tries NATURAL JOIN teams U WHERE id_exercice = ? UNION
if rows, err := DBQuery(`SELECT id_team, U.name, U.color, "tries" AS kind, time, 0, id_exercice, id_try, NULL FROM exercice_tries NATURAL JOIN teams U WHERE id_exercice = ? UNION
SELECT id_team, U.name, U.color, "solved" AS kind, time, coefficient, id_exercice, NULL, NULL FROM exercice_solved S NATURAL JOIN teams U WHERE id_exercice = ? UNION
SELECT id_team, U.name, U.color, "hint" AS kind, time, coefficient, id_exercice, H.id_hint, H.title FROM team_hints T INNER JOIN exercice_hints H ON H.id_hint = T.id_hint NATURAL JOIN teams U WHERE id_exercice = ? UNION
SELECT id_team, U.name, U.color, "wchoices" AS kind, time, coefficient, id_exercice, F.id_flag, F.type FROM team_wchoices W INNER JOIN exercice_flags F ON F.id_flag = W.id_flag NATURAL JOIN teams U WHERE id_exercice = ? UNION
@ -68,7 +68,7 @@ func (e *Exercice) AppendHistoryItem(tId int64, kind string, secondary *int64) e
if kind == "tries" {
bid := make([]byte, 5)
binary.LittleEndian.PutUint32(bid, rand.Uint32())
_, err = (&Exercice{Id: e.Id}).NewTry(team, bid)
_, err = e.NewTry(team, bid)
return err
} else if kind == "hint" && secondary != nil {
return team.OpenHint(&EHint{Id: *secondary})
@ -79,7 +79,7 @@ func (e *Exercice) AppendHistoryItem(tId int64, kind string, secondary *int64) e
} else if kind == "mcq_found" && secondary != nil {
return (&MCQ{Id: int(*secondary)}).FoundBy(team)
} else if kind == "solved" {
return (&Exercice{Id: e.Id}).Solved(team)
return e.Solved(team)
} else {
return nil
}

102
libfic/exercice_tries.go Normal file
View file

@ -0,0 +1,102 @@
package fic
import (
"time"
)
type ExerciceTryDetail struct {
Kind string `json:"kind"`
RelatedId int64 `json:"related"`
}
type ExerciceTry struct {
Id int64 `json:"id"`
IdTeam int64 `json:"id_team"`
Time time.Time `json:"time"`
Checksum []byte `json:"checksum"`
NbDiff int64 `json:"nb_diff"`
OneGood bool `json:"one_good"`
Details []ExerciceTryDetail `json:"details,omitempty"`
}
// TriesList returns a list of tries related to the exercice.
func (e *Exercice) TriesList() ([]*ExerciceTry, error) {
tries_table := "exercice_tries"
if SubmissionUniqueness {
tries_table = "exercice_distinct_tries"
}
if rows, err := DBQuery("SELECT id_try, id_team, time, cksum, nbdiff, onegood FROM "+tries_table+" WHERE id_exercice = ?", e.Id); err != nil {
return nil, err
} else {
defer rows.Close()
var tries []*ExerciceTry
for rows.Next() {
var try ExerciceTry
if err := rows.Scan(&try.Id, &try.IdTeam, &try.Time, &try.Checksum, &try.NbDiff, &try.OneGood); err != nil {
return nil, err
}
tries = append(tries, &try)
}
return tries, nil
}
}
func (e *Exercice) GetTry(id int64) (try *ExerciceTry, err error) {
try = &ExerciceTry{}
err = DBQueryRow("SELECT id_try, id_team, time, cksum, nbdiff, onegood FROM exercice_tries WHERE id_exercice = ? AND id_try = ?", e.Id, id).Scan(&try.Id, &try.IdTeam, &try.Time, &try.Checksum, &try.NbDiff, &try.OneGood)
return
}
func (try *ExerciceTry) FillDetails() error {
if rows, err := DBQuery("SELECT id_flag FROM exercice_tries_flags WHERE id_try = ?", try.Id); err != nil {
return err
} else {
defer rows.Close()
for rows.Next() {
var relatedid int64
if err := rows.Scan(&relatedid); err != nil {
return err
}
try.Details = append(try.Details, ExerciceTryDetail{
Kind: "flag",
RelatedId: relatedid,
})
}
}
if rows, err := DBQuery("SELECT id_mcq FROM exercice_tries_mcq WHERE id_try = ?", try.Id); err != nil {
return err
} else {
defer rows.Close()
for rows.Next() {
var relatedid int64
if err := rows.Scan(&relatedid); err != nil {
return err
}
try.Details = append(try.Details, ExerciceTryDetail{
Kind: "mcq",
RelatedId: relatedid,
})
}
}
return nil
}
func (try *ExerciceTry) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM exercice_tries WHERE id_try = ?", try.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log"
"math"
"os"
"path"
"sort"
@ -124,8 +125,8 @@ func MyJSONTeam(t *Team, started bool) (interface{}, error) {
ret.Name = t.Name
ret.Id = t.Id
points, _ := t.GetPoints()
ret.Points = int64(float64(points) * GlobalScoreCoefficient)
ret.Points100 = int64(float64(points) * GlobalScoreCoefficient * 100)
ret.Points = int64(math.Round(float64(points) * GlobalScoreCoefficient))
ret.Points100 = int64(math.Round(float64(points) * GlobalScoreCoefficient * 100))
if members, err := t.GetMembers(); err == nil {
ret.Members = members
}