diff --git a/admin/api/exercice.go b/admin/api/exercice.go index 442e4660..fb557ded 100644 --- a/admin/api/exercice.go +++ b/admin/api/exercice.go @@ -407,22 +407,34 @@ type exerciceStats struct { SolvedCount int64 `json:"solved_count"` FlagSolved []int64 `json:"flag_solved"` MCQSolved []int64 `json:"mcq_solved"` + CurrentGain int64 `json:"current_gain"` } func getExerciceStats(c *gin.Context) { e := c.MustGet("exercice").(*fic.Exercice) + current_gain := e.Gain + if fic.DiscountedFactor > 0 { + decoted_exercice, err := fic.GetDiscountedExercice(e.Id) + if err == nil { + current_gain = decoted_exercice.Gain + } else { + log.Println("Unable to fetch decotedExercice:", err.Error()) + } + } + c.JSON(http.StatusOK, exerciceStats{ TeamTries: e.TriedTeamCount(), TotalTries: e.TriedCount(), SolvedCount: e.SolvedCount(), FlagSolved: e.FlagSolved(), MCQSolved: e.MCQSolved(), + CurrentGain: current_gain, }) } func getExercicesStats(c *gin.Context) { - exercices, err := fic.GetExercices() + exercices, err := fic.GetDiscountedExercices() if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) } @@ -436,6 +448,7 @@ func getExercicesStats(c *gin.Context) { SolvedCount: e.SolvedCount(), FlagSolved: e.FlagSolved(), MCQSolved: e.MCQSolved(), + CurrentGain: e.Gain, }) } diff --git a/admin/api/settings.go b/admin/api/settings.go index cf0df11c..e5dd9bca 100644 --- a/admin/api/settings.go +++ b/admin/api/settings.go @@ -307,6 +307,13 @@ func ApplySettings(config *settings.Settings) { fic.SubmissionCostBase = config.SubmissionCostBase fic.SubmissionUniqueness = config.SubmissionUniqueness fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries + + if config.DiscountedFactor != fic.DiscountedFactor { + fic.DiscountedFactor = config.DiscountedFactor + if err := fic.DBRecreateDiscountedView(); err != nil { + log.Println("Unable to recreate exercices_discounted view:", err.Error()) + } + } } func ResetSettings() error { @@ -318,6 +325,7 @@ func ResetSettings() error { HintCurCoefficient: 1, WChoiceCurCoefficient: 1, GlobalScoreCoefficient: 1, + DiscountedFactor: 0, AllowRegistration: false, CanJoinTeam: false, DenyTeamCreation: false, diff --git a/admin/static/views/exercice.html b/admin/static/views/exercice.html index e7b1ea4e..0c745afd 100644 --- a/admin/static/views/exercice.html +++ b/admin/static/views/exercice.html @@ -62,6 +62,9 @@
+
Points actuels
+
+
Défi tenté par
diff --git a/admin/static/views/settings.html b/admin/static/views/settings.html index d69a8db3..0a4fb787 100644 --- a/admin/static/views/settings.html +++ b/admin/static/views/settings.html @@ -88,13 +88,23 @@
- -
- +
+ +
+ +
- -
- +
+ +
+ +
+
+
+ +
+ +
diff --git a/backend/main.go b/backend/main.go index 50cb87cd..b1294da6 100644 --- a/backend/main.go +++ b/backend/main.go @@ -75,7 +75,7 @@ func reloadSettings(config *settings.Settings) { fic.WChoiceCoefficient = config.WChoiceCurCoefficient fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient ChStarted = config.Start.Unix() > 0 && time.Since(config.Start) >= 0 - if lastRegeneration != config.Generation || fic.PartialValidation != config.PartialValidation || fic.UnlockedChallengeDepth != config.UnlockedChallengeDepth || fic.UnlockedChallengeUpTo != config.UnlockedChallengeUpTo || fic.DisplayAllFlags != config.DisplayAllFlags || fic.FirstBlood != config.FirstBlood || fic.SubmissionCostBase != config.SubmissionCostBase || fic.SubmissionUniqueness != config.SubmissionUniqueness { + if lastRegeneration != config.Generation || fic.PartialValidation != config.PartialValidation || fic.UnlockedChallengeDepth != config.UnlockedChallengeDepth || 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.PartialValidation = config.PartialValidation fic.UnlockedChallengeDepth = config.UnlockedChallengeDepth fic.UnlockedChallengeUpTo = config.UnlockedChallengeUpTo @@ -86,6 +86,7 @@ func reloadSettings(config *settings.Settings) { fic.SubmissionUniqueness = config.SubmissionUniqueness fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries + fic.DiscountedFactor = config.DiscountedFactor if !skipInitialGeneration { log.Println("Generating files...") diff --git a/frontend/ui/src/routes/[theme]/[exercice]/+page.svelte b/frontend/ui/src/routes/[theme]/[exercice]/+page.svelte index 045eb453..02e0d772 100644 --- a/frontend/ui/src/routes/[theme]/[exercice]/+page.svelte +++ b/frontend/ui/src/routes/[theme]/[exercice]/+page.svelte @@ -61,16 +61,31 @@
-
- Gain -
+ {#if $settings.discountedFactor > 0 && $my && $my.exercices[$current_exercice.id]} +
+ Cote +
+ {:else} +
+ Gain +
+ {/if}
- {$current_exercice.gain} {$current_exercice.gain==1?"point":"points"} + {#if $settings.discountedFactor && $current_exercice.solved} + + {Math.trunc($current_exercice.gain * (1-$settings.discountedFactor*$current_exercice.solved)*10)/10} {$current_exercice.gain==1?"point":"points"} + {:else} + {$current_exercice.gain} {$current_exercice.gain==1?"point":"points"} + {/if}
{#if $settings.firstBlood && $current_exercice.solved < 1}
+{$settings.firstBlood * 100}% (prem's)
+ {:else if $settings.discountedFactor > 0 && $my && $my.exercices[$current_exercice.id]} +
+ initialement {$current_exercice.gain} {$current_exercice.gain==1?"point":"points"} +
{/if} {#if $current_exercice.curcoeff != 1.0 || $settings.exerciceCurrentCoefficient != 1.0}
diff --git a/libfic/db.go b/libfic/db.go index 2df0ddb0..9fe40337 100644 --- a/libfic/db.go +++ b/libfic/db.go @@ -3,6 +3,7 @@ package fic import ( "database/sql" "errors" + "fmt" "log" "os" "strings" @@ -548,10 +549,22 @@ CREATE TABLE IF NOT EXISTS teams_qa_view( if _, err := db.Exec("CREATE OR REPLACE VIEW exercice_distinct_tries_notgood AS SELECT id_exercice, id_team, MAX(time) AS time, cksum, nbdiff, MAX(onegood) AS onegood FROM exercice_tries_notgood GROUP BY id_team, id_exercice, cksum;"); err != nil { return err } + if err := DBRecreateDiscountedView(); err != nil { + return err + } return nil } +func DBRecreateDiscountedView() (err error) { + if db == nil { + return + } + + _, err = db.Exec("CREATE OR REPLACE VIEW exercices_discounted AS SELECT E.id_exercice, E.id_theme, E.title, E.disabled, E.headline, E.url_id, E.path, E.statement, E.overview, E.issue, E.issue_kind, E.depend, E.gain - " + fmt.Sprintf("%f", DiscountedFactor) + " * E.gain * (COUNT(*) - 1) AS gain, E.coefficient_cur, E.finished, E.video_uri, E.resolution, E.seealso FROM exercices E LEFT OUTER JOIN exercice_solved S ON S.id_exercice = E.id_exercice GROUP BY E.id_exercice;") + return +} + // DBClose closes the connection to the database func DBClose() error { return db.Close() diff --git a/libfic/exercice.go b/libfic/exercice.go index 96d94bc5..b61dd317 100644 --- a/libfic/exercice.go +++ b/libfic/exercice.go @@ -3,6 +3,7 @@ package fic import ( "errors" "fmt" + "math" "time" ) @@ -69,11 +70,13 @@ func (e *Exercice) AnalyzeTitle() { } } -func getExercice(condition string, args ...interface{}) (*Exercice, error) { +func getExercice(table, condition string, args ...interface{}) (*Exercice, error) { var e Exercice - if err := DBQueryRow("SELECT id_exercice, id_theme, title, disabled, url_id, path, statement, overview, headline, issue, issue_kind, depend, gain, coefficient_cur, video_uri, resolution, seealso, finished FROM exercices "+condition, args...).Scan(&e.Id, &e.IdTheme, &e.Title, &e.Disabled, &e.URLId, &e.Path, &e.Statement, &e.Overview, &e.Headline, &e.Issue, &e.IssueKind, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI, &e.Resolution, &e.SeeAlso, &e.Finished); err != nil { + var tmpgain float64 + if err := DBQueryRow("SELECT id_exercice, id_theme, title, disabled, url_id, path, statement, overview, headline, issue, issue_kind, depend, gain, coefficient_cur, video_uri, resolution, seealso, finished FROM "+table+" "+condition, args...).Scan(&e.Id, &e.IdTheme, &e.Title, &e.Disabled, &e.URLId, &e.Path, &e.Statement, &e.Overview, &e.Headline, &e.Issue, &e.IssueKind, &e.Depend, &tmpgain, &e.Coefficient, &e.VideoURI, &e.Resolution, &e.SeeAlso, &e.Finished); err != nil { return nil, err } + e.Gain = int64(math.Trunc(tmpgain)) e.AnalyzeTitle() return &e, nil @@ -81,27 +84,36 @@ func getExercice(condition string, args ...interface{}) (*Exercice, error) { // GetExercice retrieves the challenge with the given id. func GetExercice(id int64) (*Exercice, error) { - return getExercice("WHERE id_exercice = ?", id) + return getExercice("exercices", "WHERE id_exercice = ?", id) } // GetExercice retrieves the challenge with the given id. func (t *Theme) GetExercice(id int) (*Exercice, error) { - return getExercice("WHERE id_theme = ? AND id_exercice = ?", t.Id, id) + return getExercice("exercices", "WHERE id_theme = ? AND id_exercice = ?", t.Id, id) } // GetExerciceByTitle retrieves the challenge with the given title. func (t *Theme) GetExerciceByTitle(title string) (*Exercice, error) { - return getExercice("WHERE id_theme = ? AND title = ?", t.Id, title) + return getExercice("exercices", "WHERE id_theme = ? AND title = ?", t.Id, title) } // GetExerciceByPath retrieves the challenge with the given path. func (t *Theme) GetExerciceByPath(epath string) (*Exercice, error) { - return getExercice("WHERE id_theme = ? AND path = ?", t.Id, epath) + return getExercice("exercices", "WHERE id_theme = ? AND path = ?", t.Id, epath) } -// GetExercices returns the list of all challenges present in the database. -func GetExercices() ([]*Exercice, error) { - if rows, err := DBQuery("SELECT id_exercice, id_theme, title, disabled, url_id, path, statement, overview, headline, issue, issue_kind, depend, gain, coefficient_cur, video_uri, resolution, seealso, finished FROM exercices ORDER BY path ASC"); err != nil { +// GetDiscountedExercice retrieves the challenge with the given id. +func GetDiscountedExercice(id int64) (*Exercice, error) { + table := "exercices" + if DiscountedFactor > 0 { + table = "exercices_discounted" + } + return getExercice(table, "WHERE id_exercice = ?", id) +} + +// getExercices returns the list of all challenges present in the database. +func getExercices(table string) ([]*Exercice, error) { + if rows, err := DBQuery("SELECT id_exercice, id_theme, title, disabled, url_id, path, statement, overview, headline, issue, issue_kind, depend, gain, coefficient_cur, video_uri, resolution, seealso, finished FROM " + table + " ORDER BY path ASC"); err != nil { return nil, err } else { defer rows.Close() @@ -109,9 +121,11 @@ func GetExercices() ([]*Exercice, error) { exos := []*Exercice{} for rows.Next() { e := &Exercice{} - if err := rows.Scan(&e.Id, &e.IdTheme, &e.Title, &e.Disabled, &e.URLId, &e.Path, &e.Statement, &e.Overview, &e.Headline, &e.Issue, &e.IssueKind, &e.Depend, &e.Gain, &e.Coefficient, &e.VideoURI, &e.Resolution, &e.SeeAlso, &e.Finished); err != nil { + var tmpgain float64 + if err := rows.Scan(&e.Id, &e.IdTheme, &e.Title, &e.Disabled, &e.URLId, &e.Path, &e.Statement, &e.Overview, &e.Headline, &e.Issue, &e.IssueKind, &e.Depend, &tmpgain, &e.Coefficient, &e.VideoURI, &e.Resolution, &e.SeeAlso, &e.Finished); err != nil { return nil, err } + e.Gain = int64(math.Trunc(tmpgain)) e.AnalyzeTitle() exos = append(exos, e) } @@ -123,6 +137,18 @@ func GetExercices() ([]*Exercice, error) { } } +func GetExercices() ([]*Exercice, error) { + return getExercices("exercices") +} + +func GetDiscountedExercices() ([]*Exercice, error) { + table := "exercices" + if DiscountedFactor > 0 { + table = "exercices_discounted" + } + return getExercices(table) +} + // GetExercices returns the list of all challenges in the Theme. func (t *Theme) GetExercices() ([]*Exercice, error) { if rows, err := DBQuery("SELECT id_exercice, id_theme, title, disabled, url_id, path, statement, overview, headline, issue, issue_kind, depend, gain, coefficient_cur, video_uri, resolution, seealso, finished FROM exercices WHERE id_theme = ? ORDER BY path ASC", t.Id); err != nil { diff --git a/libfic/stats.go b/libfic/stats.go index 5cab0822..b1ae2314 100644 --- a/libfic/stats.go +++ b/libfic/stats.go @@ -20,6 +20,9 @@ var SubmissionUniqueness = false // CountOnlyNotGoodTries don't count as a try when one good response is given at least. var CountOnlyNotGoodTries = false +// DiscountedFactor stores the percentage of the exercice's gain lost on each validation. +var DiscountedFactor = 0.0 + func exoptsQuery(whereExo string) string { tries_table := "exercice_tries" if SubmissionUniqueness { @@ -30,10 +33,15 @@ func exoptsQuery(whereExo string) string { tries_table += "_notgood" } + exercices_table := "exercices" + if DiscountedFactor > 0 { + exercices_table = "exercices_discounted" + } + return `SELECT S.id_team, S.time, E.gain AS points, coeff, S.reason, S.id_exercice FROM ( SELECT id_team, id_exercice, MIN(time) AS time, ` + fmt.Sprintf("%f", FirstBlood) + ` AS coeff, "First blood" AS reason FROM exercice_solved GROUP BY id_exercice UNION SELECT id_team, id_exercice, time, coefficient AS coeff, "Validation" AS reason FROM exercice_solved - ) S INNER JOIN exercices E ON S.id_exercice = E.id_exercice ` + whereExo + ` UNION ALL + ) S INNER JOIN ` + exercices_table + ` E ON S.id_exercice = E.id_exercice ` + whereExo + ` 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 ` + whereExo + ` GROUP BY id_exercice, id_team` } diff --git a/libfic/team_my.go b/libfic/team_my.go index 6387ce82..197d5743 100644 --- a/libfic/team_my.go +++ b/libfic/team_my.go @@ -116,7 +116,7 @@ func MyJSONTeam(t *Team, started bool) (interface{}, error) { // Fill exercices, only if the challenge is started ret.Exercices = map[string]myTeamExercice{} - if exos, err := GetExercices(); err != nil { + if exos, err := GetDiscountedExercices(); err != nil { return ret, err } else if started { for _, e := range exos { diff --git a/settings/settings.go b/settings/settings.go index 122968e8..9c586105 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -46,6 +46,8 @@ type Settings struct { WChoiceCurCoefficient float64 `json:"wchoiceCurrentCoefficient"` // GlobalScoreCoefficient is a coefficient to apply on display scores, not considered bonus. GlobalScoreCoefficient float64 `json:"globalScoreCoefficient"` + // DiscountedFactor stores the percentage of the exercice's gain lost on each validation. + DiscountedFactor float64 `json:"discountedFactor,omitempty"` // AllowRegistration permits unregistered Team to register themselves. AllowRegistration bool `json:"allowRegistration,omitempty"`