admin: Can gain points for each question answered // partial exercice solved

This commit is contained in:
nemunaire 2025-03-28 13:33:09 +01:00
parent 4ca2bc106a
commit 8e196136c3
8 changed files with 30 additions and 5 deletions

View file

@ -310,6 +310,7 @@ func ApplySettings(config *settings.Settings) {
fic.SubmissionCostBase = config.SubmissionCostBase fic.SubmissionCostBase = config.SubmissionCostBase
fic.SubmissionUniqueness = config.SubmissionUniqueness fic.SubmissionUniqueness = config.SubmissionUniqueness
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
fic.QuestionGainRatio = config.QuestionGainRatio
if config.DiscountedFactor != fic.DiscountedFactor { if config.DiscountedFactor != fic.DiscountedFactor {
fic.DiscountedFactor = config.DiscountedFactor fic.DiscountedFactor = config.DiscountedFactor
@ -329,6 +330,7 @@ func ResetSettings() error {
WChoiceCurCoefficient: 1, WChoiceCurCoefficient: 1,
GlobalScoreCoefficient: 1, GlobalScoreCoefficient: 1,
DiscountedFactor: 0, DiscountedFactor: 0,
QuestionGainRatio: 0,
UnlockedStandaloneExercices: 10, UnlockedStandaloneExercices: 10,
UnlockedStandaloneExercicesByThemeStepValidation: 1, UnlockedStandaloneExercicesByThemeStepValidation: 1,
UnlockedStandaloneExercicesByStandaloneExerciceValidation: 0, UnlockedStandaloneExercicesByStandaloneExerciceValidation: 0,

View file

@ -104,6 +104,12 @@
<input type="text" class="form-control form-control-sm" id="submissionCostBase" ng-model="config.submissionCostBase" float ng-class="{'border-primary': config.submissionCostBase != dist_config.submissionCostBase}"> <input type="text" class="form-control form-control-sm" id="submissionCostBase" ng-model="config.submissionCostBase" float ng-class="{'border-primary': config.submissionCostBase != dist_config.submissionCostBase}">
</div> </div>
</div> </div>
<div class="col-sm row" title="Accorder des points aux exercices partiellement résolu, par questions validée. Ce champ est le pourcentage de points que peut rapporter la complétion de toutes les questions d'un exercice. Par exemple avec 25%, un exercice avec 10 questions, chaque question validée rapportera GAIN * 25% / 10">
<label for="questionGainRatio" class="col-sm-8 col-form-label col-form-label-sm text-right text-truncate" ng-class="{'text-primary font-weight-bold': config.questionGainRatio != dist_config.questionGainRatio}">Gain par questions</label>
<div class="col-sm-4">
<input type="text" class="form-control form-control-sm" id="questionGainRatio" ng-model="config.questionGainRatio" float ng-class="{'border-primary': config.questionGainRatio != dist_config.questionGainRatio}">
</div>
</div>
</div> </div>
<hr> <hr>

View file

@ -13,7 +13,7 @@
<th>Date</th> <th>Date</th>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="row in scores | filter: query | orderBy:'time'" ng-class="{'bg-danger': row.reason == 'Bonus flag', 'bg-ffound': row.reason == 'First blood', 'bg-wchoices': row.reason == 'Display choices', 'bg-success': row.reason == 'Validation', 'bg-info': row.reason == 'Hint', 'bg-warning': row.reason == 'Tries'}"> <tr ng-repeat="row in scores | filter: query | orderBy:'time'" ng-class="{'bg-danger': row.reason == 'Bonus flag', 'bg-ffound': row.reason == 'First blood', 'bg-wchoices': row.reason == 'Display choices', 'bg-success': row.reason == 'Validation', 'bg-info': row.reason == 'Hint', 'bg-secondary': row.reason.startsWith('Response '), 'bg-warning': row.reason == 'Tries'}">
<td> <td>
<a ng-repeat="exercice in exercices" ng-if="exercice.id == row.id_exercice" href="exercices/{{ row.id_exercice }}">{{ exercice.title }}</a> <a ng-repeat="exercice in exercices" ng-if="exercice.id == row.id_exercice" href="exercices/{{ row.id_exercice }}">{{ exercice.title }}</a>
</td> </td>
@ -23,9 +23,12 @@
<td> <td>
{{ row.points * row.coeff }} {{ row.points * row.coeff }}
</td> </td>
<td> <td ng-if="!row.reason.startsWith('Response ')">
{{ row.points }} * {{ row.coeff }} {{ row.points }} * {{ row.coeff }}
</td> </td>
<td ng-if="row.reason.startsWith('Response ')">
{{ row.points }} * {{ settings.questionGainRatio }} / {{ settings.questionGainRatio / row.coeff }}
</td>
<td> <td>
<nobr title="{{ row.time }}">{{ row.time | date:"mediumTime" }}</nobr> <nobr title="{{ row.time }}">{{ row.time | date:"mediumTime" }}</nobr>
</td> </td>

View file

@ -90,6 +90,7 @@ func reloadSettings(config *settings.Settings) {
fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
fic.DiscountedFactor = config.DiscountedFactor fic.DiscountedFactor = config.DiscountedFactor
fic.QuestionGainRatio = config.QuestionGainRatio
} }
func main() { func main() {

View file

@ -10,6 +10,7 @@
import DateFormat from '$lib/components/DateFormat.svelte'; import DateFormat from '$lib/components/DateFormat.svelte';
import { my } from '$lib/stores/my.js'; import { my } from '$lib/stores/my.js';
import { settings } from '$lib/stores/settings.js';
import { themes, exercices_idx } from '$lib/stores/themes.js'; import { themes, exercices_idx } from '$lib/stores/themes.js';
let req = null; let req = null;
@ -55,6 +56,10 @@
{:else if row.reason == "Display choices"} {:else if row.reason == "Display choices"}
<Badge color="secondary"><Icon name="info-square" /></Badge> <Badge color="secondary"><Icon name="info-square" /></Badge>
Échange champ de texte contre liste de choix Échange champ de texte contre liste de choix
{:else if row.reason.startsWith("Response ")}
{@const fields = row.reason.split(" ")}
<Badge color="secondary"><Icon name="clipboard2-check" /></Badge>
Validation {fields[1]}
{:else} {:else}
<Badge color="primary"><Icon name="question" /></Badge> <Badge color="primary"><Icon name="question" /></Badge>
{row.reason} {row.reason}
@ -66,7 +71,7 @@
{/if} {/if}
</Column> </Column>
<Column header="Détail"> <Column header="Détail">
<span title="Valeur initiale (cette valeur est fixe)">{Math.trunc(10*row.points)/10}</span> &times; <span title="Coefficient multiplicateur (il varie selon les événements en cours sur la plateforme)">{row.coeff}</span> <span title="Valeur initiale (cette valeur est fixe)">{Math.trunc(10*row.points)/10}</span> &times; {#if row.reason.startsWith("Response ")}<span title="Pourcentage des points accordés pour avoir répondu aux questions d'un défi, sans avoir validé entièrement le défi">{$settings.questionGainRatio}</span> &divide; <span title="Nombre de questions du défi">{$settings.questionGainRatio / row.coeff}</span>{:else}<span title="Coefficient multiplicateur (il varie selon les événements en cours sur la plateforme)">{row.coeff}</span>{/if}
</Column> </Column>
<Column header="Points"> <Column header="Points">
{Math.trunc(10*row.points * row.coeff)/10} {Math.trunc(10*row.points * row.coeff)/10}

View file

@ -30,7 +30,7 @@ func reloadSettings(config *settings.Settings) {
fic.WChoiceCoefficient = config.WChoiceCurCoefficient fic.WChoiceCoefficient = config.WChoiceCurCoefficient
fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient
ChStarted = config.Start.Unix() > 0 && time.Since(config.Start) >= 0 ChStarted = config.Start.Unix() > 0 && time.Since(config.Start) >= 0
if allowRegistration != config.AllowRegistration || fic.PartialValidation != config.PartialValidation || fic.UnlockedChallengeDepth != config.UnlockedChallengeDepth || fic.UnlockedStandaloneExercices != config.UnlockedStandaloneExercices || fic.UnlockedStandaloneExercicesByThemeStepValidation != config.UnlockedStandaloneExercicesByThemeStepValidation || fic.UnlockedStandaloneExercicesByStandaloneExerciceValidation != config.UnlockedStandaloneExercicesByStandaloneExerciceValidation || fic.UnlockedChallengeUpTo != config.UnlockedChallengeUpTo || fic.DisplayAllFlags != config.DisplayAllFlags || fic.FirstBlood != config.FirstBlood || fic.SubmissionCostBase != config.SubmissionCostBase || fic.SubmissionUniqueness != config.SubmissionUniqueness || fic.DiscountedFactor != config.DiscountedFactor || fic.HideCaseSensitivity != config.HideCaseSensitivity { if allowRegistration != config.AllowRegistration || fic.PartialValidation != config.PartialValidation || fic.UnlockedChallengeDepth != config.UnlockedChallengeDepth || fic.UnlockedStandaloneExercices != config.UnlockedStandaloneExercices || fic.UnlockedStandaloneExercicesByThemeStepValidation != config.UnlockedStandaloneExercicesByThemeStepValidation || fic.UnlockedStandaloneExercicesByStandaloneExerciceValidation != config.UnlockedStandaloneExercicesByStandaloneExerciceValidation || fic.UnlockedChallengeUpTo != config.UnlockedChallengeUpTo || fic.DisplayAllFlags != config.DisplayAllFlags || fic.FirstBlood != config.FirstBlood || fic.SubmissionCostBase != config.SubmissionCostBase || fic.SubmissionUniqueness != config.SubmissionUniqueness || fic.DiscountedFactor != config.DiscountedFactor || fic.QuestionGainRatio != config.QuestionGainRatio || fic.HideCaseSensitivity != config.HideCaseSensitivity {
allowRegistration = config.AllowRegistration allowRegistration = config.AllowRegistration
fic.PartialValidation = config.PartialValidation fic.PartialValidation = config.PartialValidation
@ -48,6 +48,7 @@ func reloadSettings(config *settings.Settings) {
fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
fic.HideCaseSensitivity = config.HideCaseSensitivity fic.HideCaseSensitivity = config.HideCaseSensitivity
fic.DiscountedFactor = config.DiscountedFactor fic.DiscountedFactor = config.DiscountedFactor
fic.QuestionGainRatio = config.QuestionGainRatio
if !skipInitialGeneration { if !skipInitialGeneration {
log.Println("Generating files...") log.Println("Generating files...")

View file

@ -23,6 +23,9 @@ var CountOnlyNotGoodTries = false
// DiscountedFactor stores the percentage of the exercice's gain lost on each validation. // DiscountedFactor stores the percentage of the exercice's gain lost on each validation.
var DiscountedFactor = 0.0 var DiscountedFactor = 0.0
// QuestionGainRatio is the ratio given to a partially solved exercice in the final scoreboard.
var QuestionGainRatio = 0.0
func exoptsQuery(whereExo string) string { func exoptsQuery(whereExo string) string {
tries_table := "exercice_tries" tries_table := "exercice_tries"
if SubmissionUniqueness { if SubmissionUniqueness {
@ -39,8 +42,10 @@ func exoptsQuery(whereExo string) string {
} }
query := `SELECT S.id_team, S.time, E.gain AS points, coeff, S.reason, S.id_exercice FROM ( query := `SELECT S.id_team, S.time, E.gain AS points, coeff, S.reason, S.id_exercice FROM (
SELECT id_team, F.id_exercice AS id_exercice, time, ` + fmt.Sprintf("%f", QuestionGainRatio) + ` / (COALESCE(T.total_flags, 0) + COALESCE(TMCQ.total_mcqs, 0)) AS coeff, CONCAT("Response flag ", F.id_flag) AS reason FROM flag_found B INNER JOIN exercice_flags F ON F.id_flag = B.id_flag LEFT JOIN (SELECT id_exercice, COUNT(*) AS total_flags FROM exercice_flags GROUP BY id_exercice) T ON F.id_exercice = T.id_exercice LEFT JOIN (SELECT id_exercice, COUNT(*) AS total_mcqs FROM exercice_mcq GROUP BY id_exercice) TMCQ ON F.id_exercice = TMCQ.id_exercice WHERE F.bonus_gain = 0 UNION
SELECT id_team, F.id_exercice AS id_exercice, time, ` + fmt.Sprintf("%f", QuestionGainRatio) + ` / (COALESCE(T.total_flags, 0) + COALESCE(TMCQ.total_mcqs, 0)) AS coeff, CONCAT("Response MCQ ", F.id_mcq) AS reason FROM mcq_found B INNER JOIN exercice_mcq F ON F.id_mcq = B.id_mcq LEFT JOIN (SELECT id_exercice, COUNT(*) AS total_flags FROM exercice_flags GROUP BY id_exercice) T ON F.id_exercice = T.id_exercice LEFT JOIN (SELECT id_exercice, COUNT(*) AS total_mcqs FROM exercice_mcq GROUP BY id_exercice) TMCQ ON F.id_exercice = TMCQ.id_exercice UNION
SELECT id_team, id_exercice, time, ` + fmt.Sprintf("%f", FirstBlood) + ` AS coeff, "First blood" AS reason FROM exercice_solved JOIN (SELECT id_exercice, MIN(time) time FROM exercice_solved GROUP BY id_exercice) d1 USING (id_exercice, time) UNION SELECT id_team, id_exercice, time, ` + fmt.Sprintf("%f", FirstBlood) + ` AS coeff, "First blood" AS reason FROM exercice_solved JOIN (SELECT id_exercice, MIN(time) time FROM exercice_solved GROUP BY id_exercice) d1 USING (id_exercice, time) UNION
SELECT id_team, id_exercice, time, coefficient AS coeff, "Validation" AS reason FROM exercice_solved SELECT id_team, id_exercice, time, coefficient - ` + fmt.Sprintf("%f", QuestionGainRatio) + ` AS coeff, "Validation" AS reason FROM exercice_solved
) S INNER JOIN ` + exercices_table + ` E ON S.id_exercice = E.id_exercice UNION ALL ) S INNER JOIN ` + exercices_table + ` E ON S.id_exercice = E.id_exercice UNION ALL
SELECT B.id_team, B.time, F.bonus_gain AS points, 1 AS coeff, "Bonus flag" AS reason, F.id_exercice FROM flag_found B INNER JOIN exercice_flags F ON F.id_flag = B.id_flag WHERE F.bonus_gain != 0 HAVING points != 0 UNION ALL SELECT B.id_team, B.time, F.bonus_gain AS points, 1 AS coeff, "Bonus flag" AS reason, F.id_exercice FROM flag_found B INNER JOIN exercice_flags F ON F.id_flag = B.id_flag WHERE F.bonus_gain != 0 HAVING points != 0 UNION ALL
SELECT id_team, MAX(time) AS time, (FLOOR(COUNT(*)/10 - 1) * (FLOOR(COUNT(*)/10)))/0.2 + (FLOOR(COUNT(*)/10) * (COUNT(*)%10)) AS points, ` + fmt.Sprintf("%f", SubmissionCostBase*-1) + ` AS coeff, "Tries" AS reason, id_exercice FROM ` + tries_table + ` S GROUP BY id_exercice, id_team` SELECT id_team, MAX(time) AS time, (FLOOR(COUNT(*)/10 - 1) * (FLOOR(COUNT(*)/10)))/0.2 + (FLOOR(COUNT(*)/10) * (COUNT(*)%10)) AS points, ` + fmt.Sprintf("%f", SubmissionCostBase*-1) + ` AS coeff, "Tries" AS reason, id_exercice FROM ` + tries_table + ` S GROUP BY id_exercice, id_team`

View file

@ -46,6 +46,8 @@ type Settings struct {
GlobalScoreCoefficient float64 `json:"globalScoreCoefficient"` GlobalScoreCoefficient float64 `json:"globalScoreCoefficient"`
// DiscountedFactor stores the percentage of the exercice's gain lost on each validation. // DiscountedFactor stores the percentage of the exercice's gain lost on each validation.
DiscountedFactor float64 `json:"discountedFactor,omitempty"` DiscountedFactor float64 `json:"discountedFactor,omitempty"`
// QuestionGainRatio is the ratio given to a partially solved exercice in the final scoreboard.
QuestionGainRatio float64 `json:"questionGainRatio,omitempty"`
// AllowRegistration permits unregistered Team to register themselves. // AllowRegistration permits unregistered Team to register themselves.
AllowRegistration bool `json:"allowRegistration,omitempty"` AllowRegistration bool `json:"allowRegistration,omitempty"`