2018-03-09 18:07:08 +00:00
|
|
|
// Package settings is shared across multiple services for easy parsing and
|
|
|
|
// retrieval of the challenge settings.
|
2016-12-30 11:45:14 +00:00
|
|
|
package settings
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"log"
|
|
|
|
"os"
|
2018-12-09 18:55:54 +00:00
|
|
|
"os/signal"
|
2016-12-30 11:45:14 +00:00
|
|
|
"path"
|
2018-12-09 18:55:54 +00:00
|
|
|
"syscall"
|
2020-05-16 01:51:36 +00:00
|
|
|
"time"
|
2016-12-30 11:45:14 +00:00
|
|
|
|
|
|
|
"gopkg.in/fsnotify.v1"
|
|
|
|
)
|
|
|
|
|
2018-03-09 18:07:08 +00:00
|
|
|
// SettingsFile is the expected name of the file containing the settings.
|
2016-12-30 11:45:14 +00:00
|
|
|
const SettingsFile = "settings.json"
|
|
|
|
|
2018-03-09 18:07:08 +00:00
|
|
|
// SettingsDir is the relative location where the SettingsFile lies.
|
2017-11-25 15:05:03 +00:00
|
|
|
var SettingsDir string = "./SETTINGS"
|
|
|
|
|
2022-05-01 20:15:16 +00:00
|
|
|
// Settings represents the settings panel.
|
|
|
|
type Settings struct {
|
2022-01-21 08:07:27 +00:00
|
|
|
// WorkInProgress indicates if the current challenge is under development or if it is in production.
|
|
|
|
WorkInProgress bool `json:"wip,omitempty"`
|
2017-04-02 09:40:23 +00:00
|
|
|
|
2018-03-09 18:07:08 +00:00
|
|
|
// Start is the departure time (expected or effective).
|
2020-05-16 01:51:36 +00:00
|
|
|
Start time.Time `json:"start"`
|
2023-05-12 12:53:15 +00:00
|
|
|
// End is the expected end time (if empty their is no end-date).
|
|
|
|
End *time.Time `json:"end,omitempty"`
|
2024-03-17 15:49:15 +00:00
|
|
|
// NextChangeTime is the time of the next expected reload.
|
|
|
|
NextChangeTime *time.Time `json:"nextchangetime,omitempty"`
|
2016-12-30 11:45:14 +00:00
|
|
|
|
2018-03-09 18:07:08 +00:00
|
|
|
// FirstBlood is the coefficient applied to each first team who solve a challenge.
|
2020-05-16 01:51:36 +00:00
|
|
|
FirstBlood float64 `json:"firstBlood"`
|
2018-03-09 18:07:08 +00:00
|
|
|
// SubmissionCostBase is a complex number representing the cost of each attempts.
|
2020-05-16 01:51:36 +00:00
|
|
|
SubmissionCostBase float64 `json:"submissionCostBase"`
|
2019-01-17 12:26:49 +00:00
|
|
|
// ExerciceCurrentCoefficient is the current coefficient applied globaly to exercices.
|
2020-05-16 01:51:36 +00:00
|
|
|
ExerciceCurCoefficient float64 `json:"exerciceCurrentCoefficient"`
|
2019-01-17 11:03:18 +00:00
|
|
|
// HintCurrentCoefficient is the current coefficient applied to hint discovery.
|
2020-05-16 01:51:36 +00:00
|
|
|
HintCurCoefficient float64 `json:"hintCurrentCoefficient"`
|
2019-01-17 11:03:18 +00:00
|
|
|
// WChoiceCurCoefficient is the current coefficient applied to wanted choices.
|
2020-05-16 01:51:36 +00:00
|
|
|
WChoiceCurCoefficient float64 `json:"wchoiceCurrentCoefficient"`
|
2021-09-06 09:58:03 +00:00
|
|
|
// GlobalScoreCoefficient is a coefficient to apply on display scores, not considered bonus.
|
|
|
|
GlobalScoreCoefficient float64 `json:"globalScoreCoefficient"`
|
2023-04-01 15:11:40 +00:00
|
|
|
// DiscountedFactor stores the percentage of the exercice's gain lost on each validation.
|
|
|
|
DiscountedFactor float64 `json:"discountedFactor,omitempty"`
|
2016-12-30 11:45:14 +00:00
|
|
|
|
2018-03-09 18:07:08 +00:00
|
|
|
// AllowRegistration permits unregistered Team to register themselves.
|
2020-11-13 13:29:23 +00:00
|
|
|
AllowRegistration bool `json:"allowRegistration,omitempty"`
|
2019-02-04 17:14:46 +00:00
|
|
|
// CanJoinTeam permits unregistered account to join an already existing team.
|
2020-11-13 13:29:23 +00:00
|
|
|
CanJoinTeam bool `json:"canJoinTeam,omitempty"`
|
2020-05-16 01:51:36 +00:00
|
|
|
// DenyTeamCreation forces unregistered account to join a team, it's not possible to create a new team.
|
2020-11-13 13:29:23 +00:00
|
|
|
DenyTeamCreation bool `json:"denyTeamCreation,omitempty"`
|
2018-03-09 18:07:08 +00:00
|
|
|
// DenyNameChange disallow Team to change their name.
|
2020-11-13 13:29:23 +00:00
|
|
|
DenyNameChange bool `json:"denyNameChange,omitempty"`
|
2021-09-06 10:02:50 +00:00
|
|
|
// IgnoreTeamMembers don't ask team to have known members.
|
|
|
|
IgnoreTeamMembers bool `json:"ignoreTeamMembers,omitempty"`
|
2020-01-20 14:56:02 +00:00
|
|
|
// AcceptNewIssue enables the reporting system.
|
2020-11-13 13:29:23 +00:00
|
|
|
AcceptNewIssue bool `json:"acceptNewIssue,omitempty"`
|
|
|
|
// QAenabled enables links to QA interface.
|
|
|
|
QAenabled bool `json:"QAenabled,omitempty"`
|
2023-11-04 20:14:16 +00:00
|
|
|
// CanResetProgression allows a team to reset the progress it made on a given exercice.
|
|
|
|
CanResetProgression bool `json:"canResetProgress,omitempty"`
|
2018-03-09 18:07:08 +00:00
|
|
|
// EnableResolutionRoute activates the route displaying resolution movies.
|
2020-11-13 13:29:23 +00:00
|
|
|
EnableResolutionRoute bool `json:"enableResolutionRoute,omitempty"`
|
2018-03-09 18:07:08 +00:00
|
|
|
// PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try.
|
2020-11-13 13:29:23 +00:00
|
|
|
PartialValidation bool `json:"partialValidation,omitempty"`
|
2019-01-18 19:30:47 +00:00
|
|
|
// UnlockedChallengeDepth don't show (or permit to solve) to team challenges they are not unlocked through dependancies.
|
2020-05-16 01:51:36 +00:00
|
|
|
UnlockedChallengeDepth int `json:"unlockedChallengeDepth"`
|
2022-06-08 14:37:25 +00:00
|
|
|
// UnlockedChallengeUpTo unlock challenge up to a given level of deps.
|
|
|
|
UnlockedChallengeUpTo int `json:"unlockedChallengeUpTo"`
|
2024-03-16 09:45:26 +00:00
|
|
|
// UnlockedStandaloneExercices unlock this number of standalone exercice.
|
|
|
|
UnlockedStandaloneExercices int `json:"unlockedStandaloneExercices,omitempty"`
|
2024-03-17 09:17:39 +00:00
|
|
|
// UnlockedStandaloneExercicesByThemeStepValidation unlock this number of standalone exercice for each theme step validated.
|
|
|
|
UnlockedStandaloneExercicesByThemeStepValidation float64 `json:"unlockedStandaloneExercicesByThemeStepValidation,omitempty"`
|
|
|
|
// UnlockedStandaloneExercicesByStandaloneExerciceValidation unlock this number of standalone exercice for each standalone exercice validated.
|
|
|
|
UnlockedStandaloneExercicesByStandaloneExerciceValidation float64 `json:"unlockedStandaloneExercicesByStandaloneExerciceValidation,omitempty"`
|
2018-12-06 21:18:08 +00:00
|
|
|
// SubmissionUniqueness don't count multiple times identical tries.
|
2020-11-13 13:29:23 +00:00
|
|
|
SubmissionUniqueness bool `json:"submissionUniqueness,omitempty"`
|
2021-09-08 01:34:48 +00:00
|
|
|
// CountOnlyNotGoodTries don't count as a try when one good response is given at least.
|
|
|
|
CountOnlyNotGoodTries bool `json:"countOnlyNotGoodTries,omitempty"`
|
2019-01-19 23:14:20 +00:00
|
|
|
// DisplayAllFlags doesn't respect the predefined constraint existing between flags.
|
2020-11-13 13:29:23 +00:00
|
|
|
DisplayAllFlags bool `json:"displayAllFlags,omitempty"`
|
2023-11-05 09:59:10 +00:00
|
|
|
// HideCaseSensitivity never tells the user if the flag is case sensitive or not.
|
|
|
|
HideCaseSensitivity bool `json:"hideCaseSensitivity,omitempty"`
|
2022-01-20 15:06:27 +00:00
|
|
|
// DisplayMCQBadCount activates the report of MCQ bad responses counter.
|
|
|
|
DisplayMCQBadCount bool `json:"displayMCQBadCount,omitempty"`
|
2019-01-19 07:04:10 +00:00
|
|
|
// EventKindness will ask browsers to delay notification interval.
|
2020-11-13 13:29:23 +00:00
|
|
|
EventKindness bool `json:"eventKindness,omitempty"`
|
2022-06-08 02:39:20 +00:00
|
|
|
// DisableSubmitButton replace button by this text (eg. scheduled updates, ...).
|
|
|
|
DisableSubmitButton string `json:"disablesubmitbutton,omitempty"`
|
2022-06-08 02:55:19 +00:00
|
|
|
// GlobalTopMessage display a message on top of each pages.
|
|
|
|
GlobalTopMessage string `json:"globaltopmessage,omitempty"`
|
|
|
|
// GlobalTopMessageVariant control the variant/color of the previous message.
|
|
|
|
GlobalTopMessageVariant string `json:"globaltopmessagevariant,omitempty"`
|
2023-05-12 13:29:38 +00:00
|
|
|
// HideHeader will hide the countdown and partners block on front pages.
|
|
|
|
HideHeader bool `json:"hide_header,omitempty"`
|
2023-07-25 07:04:31 +00:00
|
|
|
|
|
|
|
// DelegatedQA contains the users allowed to perform administrative tasks on the QA platform.
|
|
|
|
DelegatedQA []string `json:"delegated_qa,omitempty"`
|
2016-12-30 11:45:14 +00:00
|
|
|
}
|
|
|
|
|
2018-03-09 18:07:08 +00:00
|
|
|
// ExistsSettings checks if the settings file can by found at the given path.
|
2017-01-16 12:10:15 +00:00
|
|
|
func ExistsSettings(settingsPath string) bool {
|
|
|
|
_, err := os.Stat(settingsPath)
|
|
|
|
return !os.IsNotExist(err)
|
|
|
|
}
|
|
|
|
|
2018-03-09 18:07:08 +00:00
|
|
|
// ReadSettings parses the file at the given location.
|
2022-05-01 20:15:16 +00:00
|
|
|
func ReadSettings(path string) (*Settings, error) {
|
|
|
|
var s Settings
|
2016-12-30 11:45:14 +00:00
|
|
|
if fd, err := os.Open(path); err != nil {
|
2022-05-01 19:32:19 +00:00
|
|
|
return nil, err
|
2016-12-30 11:45:14 +00:00
|
|
|
} else {
|
|
|
|
defer fd.Close()
|
|
|
|
jdec := json.NewDecoder(fd)
|
|
|
|
|
|
|
|
if err := jdec.Decode(&s); err != nil {
|
2022-05-01 19:32:19 +00:00
|
|
|
return &s, err
|
2016-12-30 11:45:14 +00:00
|
|
|
}
|
|
|
|
|
2022-05-01 19:32:19 +00:00
|
|
|
return &s, nil
|
2016-12-30 11:45:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-09 18:07:08 +00:00
|
|
|
// SaveSettings saves settings at the given location.
|
2022-05-26 20:54:46 +00:00
|
|
|
func SaveSettings(path string, s interface{}) error {
|
2016-12-30 11:45:14 +00:00
|
|
|
if fd, err := os.Create(path); err != nil {
|
|
|
|
return err
|
|
|
|
} else {
|
|
|
|
defer fd.Close()
|
|
|
|
jenc := json.NewEncoder(fd)
|
|
|
|
|
|
|
|
if err := jenc.Encode(s); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-09 18:07:08 +00:00
|
|
|
// LoadAndWatchSettings is the function you are looking for!
|
|
|
|
// Giving the location and a callback, this function will first call your reload function
|
|
|
|
// before returning (if the file can be parsed); then it starts watching modifications made to
|
|
|
|
// this file. Your callback is then run each time the file is modified.
|
2022-05-01 20:15:16 +00:00
|
|
|
func LoadAndWatchSettings(settingsPath string, reload func(*Settings)) {
|
2016-12-30 11:45:14 +00:00
|
|
|
// First load of configuration if it exists
|
|
|
|
if _, err := os.Stat(settingsPath); !os.IsNotExist(err) {
|
|
|
|
if config, err := ReadSettings(settingsPath); err != nil {
|
|
|
|
log.Println("ERROR: Unable to read challenge settings:", err)
|
|
|
|
} else {
|
|
|
|
reload(config)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-09 18:55:54 +00:00
|
|
|
// Register SIGHUP
|
|
|
|
c := make(chan os.Signal, 1)
|
|
|
|
signal.Notify(c, syscall.SIGHUP)
|
2020-05-16 01:51:36 +00:00
|
|
|
go func() {
|
2018-12-09 18:55:54 +00:00
|
|
|
for range c {
|
|
|
|
log.Println("SIGHUP received, reloading settings...")
|
2019-01-17 12:03:15 +00:00
|
|
|
go tryReload(settingsPath, reload)
|
2018-12-09 18:55:54 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2016-12-30 11:45:14 +00:00
|
|
|
// Watch the configuration file
|
|
|
|
if watcher, err := fsnotify.NewWatcher(); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
} else {
|
|
|
|
if err := watcher.Add(path.Dir(settingsPath)); err != nil {
|
|
|
|
log.Fatal("Unable to watch: ", path.Dir(settingsPath), ": ", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
defer watcher.Close()
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case ev := <-watcher.Events:
|
2020-05-16 01:51:36 +00:00
|
|
|
if path.Base(ev.Name) == SettingsFile && ev.Op&(fsnotify.Write|fsnotify.Create) != 0 {
|
2016-12-30 11:45:14 +00:00
|
|
|
log.Println("Settings file changes, reloading it!")
|
2019-01-17 12:03:15 +00:00
|
|
|
go tryReload(settingsPath, reload)
|
2016-12-30 11:45:14 +00:00
|
|
|
}
|
|
|
|
case err := <-watcher.Errors:
|
|
|
|
log.Println("watcher error:", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
2019-01-17 12:03:15 +00:00
|
|
|
|
2022-05-01 20:15:16 +00:00
|
|
|
func tryReload(settingsPath string, reload func(*Settings)) {
|
2019-01-17 12:03:15 +00:00
|
|
|
if config, err := ReadSettings(settingsPath); err != nil {
|
|
|
|
log.Println("ERROR: Unable to read challenge settings:", err)
|
|
|
|
} else {
|
|
|
|
reload(config)
|
|
|
|
}
|
|
|
|
}
|