Distribute and handle challenge.json
This commit is contained in:
parent
e8f6a03cd9
commit
dff4f4eb63
|
@ -6,6 +6,7 @@ WORKDIR /go/src/srs.epita.fr/fic-server/
|
||||||
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
COPY libfic ./libfic/
|
COPY libfic ./libfic/
|
||||||
|
COPY settings ./settings/
|
||||||
COPY remote/scores-sync-zqds ./remote/scores-sync-zqds/
|
COPY remote/scores-sync-zqds ./remote/scores-sync-zqds/
|
||||||
|
|
||||||
RUN go get -d -v ./remote/scores-sync-zqds && \
|
RUN go get -d -v ./remote/scores-sync-zqds && \
|
||||||
|
|
|
@ -16,6 +16,9 @@ import (
|
||||||
var IsProductionEnv = false
|
var IsProductionEnv = false
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
router.GET("/api/challenge.json", apiHandler(getChallengeInfo))
|
||||||
|
router.PUT("/api/challenge.json", apiHandler(saveChallengeInfo))
|
||||||
|
|
||||||
router.GET("/api/settings-ro.json", apiHandler(getROSettings))
|
router.GET("/api/settings-ro.json", apiHandler(getROSettings))
|
||||||
router.GET("/api/settings.json", apiHandler(getSettings))
|
router.GET("/api/settings.json", apiHandler(getSettings))
|
||||||
router.PUT("/api/settings.json", apiHandler(saveSettings))
|
router.PUT("/api/settings.json", apiHandler(saveSettings))
|
||||||
|
@ -44,6 +47,23 @@ func getROSettings(_ httprouter.Params, body []byte) (interface{}, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getChallengeInfo(_ httprouter.Params, body []byte) (interface{}, error) {
|
||||||
|
return settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveChallengeInfo(_ httprouter.Params, body []byte) (interface{}, error) {
|
||||||
|
var info *settings.ChallengeInfo
|
||||||
|
if err := json.Unmarshal(body, &info); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := settings.SaveChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile), info); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return info, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getSettings(_ httprouter.Params, body []byte) (interface{}, error) {
|
func getSettings(_ httprouter.Params, body []byte) (interface{}, error) {
|
||||||
if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
|
if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -83,9 +103,7 @@ func ApplySettings(config *settings.Settings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResetSettings() error {
|
func ResetSettings() error {
|
||||||
return settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), &settings.FICSettings{
|
return settings.SaveSettings(path.Join(settings.SettingsDir, settings.SettingsFile), &settings.Settings{
|
||||||
Title: "Challenge FIC",
|
|
||||||
Authors: "Laboratoire SRS, ÉPITA",
|
|
||||||
WorkInProgress: IsProductionEnv,
|
WorkInProgress: IsProductionEnv,
|
||||||
FirstBlood: fic.FirstBlood,
|
FirstBlood: fic.FirstBlood,
|
||||||
SubmissionCostBase: fic.SubmissionCostBase,
|
SubmissionCostBase: fic.SubmissionCostBase,
|
||||||
|
|
|
@ -22,8 +22,10 @@ func init() {
|
||||||
router.GET("/api/session-forensic.yaml", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) {
|
router.GET("/api/session-forensic.yaml", apiHandler(func(_ httprouter.Params, _ []byte) (interface{}, error) {
|
||||||
if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
|
if s, err := settings.ReadSettings(path.Join(settings.SettingsDir, settings.SettingsFile)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
} else if c, err := settings.ReadChallengeInfo(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
|
||||||
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
return fic.GenZQDSSessionFile(s)
|
return fic.GenZQDSSessionFile(c, s)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
router.GET("/api/files-bindings", apiHandler(bindingFiles))
|
router.GET("/api/files-bindings", apiHandler(bindingFiles))
|
||||||
|
|
|
@ -6,27 +6,6 @@
|
||||||
|
|
||||||
<input type="hidden" class="form-control form-control-sm" id="lastRegeneration" ng-model="config.generation">
|
<input type="hidden" class="form-control form-control-sm" id="lastRegeneration" ng-model="config.generation">
|
||||||
|
|
||||||
<div class="form-group row">
|
|
||||||
<label for="challengeName" class="col-sm-3 col-form-label col-form-label-sm">Nom du challenge</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<input type="text" class="form-control form-control-sm" id="challengeName" ng-model="config.title">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
|
||||||
<label for="challengeAuthors" class="col-sm-3 col-form-label col-form-label-sm">Auteurs du challenge</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<input type="text" class="form-control form-control-sm" id="challengeAuthors" ng-model="config.authors">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
|
||||||
<label for="challengeVideo" class="col-sm-3 col-form-label col-form-label-sm">Lien vidéos de résolution</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<input type="text" class="form-control form-control-sm" id="challengeVideo" ng-model="config.videoslink">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label for="startTime" class="col-sm-3 col-form-label col-form-label-sm">Début du challenge</label>
|
<label for="startTime" class="col-sm-3 col-form-label col-form-label-sm">Début du challenge</label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
|
|
|
@ -165,6 +165,12 @@ server {
|
||||||
expires epoch;
|
expires epoch;
|
||||||
add_header Cache-Control no-cache;
|
add_header Cache-Control no-cache;
|
||||||
}
|
}
|
||||||
|
location = /challenge.json {
|
||||||
|
root /srv/SETTINGSDIST/;
|
||||||
|
expires epoch;
|
||||||
|
add_header X-FIC-time $msec;
|
||||||
|
add_header Cache-Control no-cache;
|
||||||
|
}
|
||||||
location = /settings.json {
|
location = /settings.json {
|
||||||
root /srv/SETTINGSDIST/;
|
root /srv/SETTINGSDIST/;
|
||||||
expires epoch;
|
expires epoch;
|
||||||
|
|
|
@ -146,6 +146,12 @@ server {
|
||||||
expires epoch;
|
expires epoch;
|
||||||
add_header Cache-Control no-cache;
|
add_header Cache-Control no-cache;
|
||||||
}
|
}
|
||||||
|
location /challenge.json {
|
||||||
|
root ${PATH_SETTINGS}/;
|
||||||
|
expires epoch;
|
||||||
|
add_header X-FIC-time $msec;
|
||||||
|
add_header Cache-Control no-cache;
|
||||||
|
}
|
||||||
location /settings.json {
|
location /settings.json {
|
||||||
root ${PATH_SETTINGS}/;
|
root ${PATH_SETTINGS}/;
|
||||||
expires epoch;
|
expires epoch;
|
||||||
|
|
|
@ -155,6 +155,12 @@ server {
|
||||||
expires epoch;
|
expires epoch;
|
||||||
add_header Cache-Control no-cache;
|
add_header Cache-Control no-cache;
|
||||||
}
|
}
|
||||||
|
location = /challenge.json {
|
||||||
|
root /srv/SETTINGSDIST/;
|
||||||
|
expires epoch;
|
||||||
|
add_header X-FIC-time $msec;
|
||||||
|
add_header Cache-Control no-cache;
|
||||||
|
}
|
||||||
location = /settings.json {
|
location = /settings.json {
|
||||||
root /srv/SETTINGSDIST/;
|
root /srv/SETTINGSDIST/;
|
||||||
expires epoch;
|
expires epoch;
|
||||||
|
|
|
@ -107,6 +107,14 @@ func init() {
|
||||||
fwd_request(w, r, "http://127.0.0.1:8081/")
|
fwd_request(w, r, "http://127.0.0.1:8081/")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
api.Router().GET("/challenge.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
|
if forwarder != nil {
|
||||||
|
fwd_request(w, r, *forwarder)
|
||||||
|
} else {
|
||||||
|
http.ServeFile(w, r, path.Join(settings.SettingsDir, settings.ChallengeFile))
|
||||||
|
}
|
||||||
|
})
|
||||||
api.Router().GET("/settings.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
api.Router().GET("/settings.json", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
if forwarder != nil {
|
if forwarder != nil {
|
||||||
|
|
|
@ -37,6 +37,15 @@ func reloadSettings(config *settings.Settings) {
|
||||||
log.Println("Unable to move new settings file:", err)
|
log.Println("Unable to move new settings file:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy the challenge info file for distribution
|
||||||
|
if data, err := ioutil.ReadFile(path.Join(settings.SettingsDir, settings.ChallengeFile)); err != nil {
|
||||||
|
log.Println("Unable to read challenge info file:", err)
|
||||||
|
} else if err = ioutil.WriteFile(path.Join(SettingsDistDir, settings.ChallengeFile+".tmp"), data, 0644); err != nil {
|
||||||
|
log.Println("Unable to write tmp challenge info file:", err)
|
||||||
|
} else if err := os.Rename(path.Join(SettingsDistDir, settings.ChallengeFile+".tmp"), path.Join(SettingsDistDir, settings.ChallengeFile)); err != nil {
|
||||||
|
log.Println("Unable to move new challenge info file:", err)
|
||||||
|
}
|
||||||
|
|
||||||
if challengeStart != config.Start || challengeEnd != config.End {
|
if challengeStart != config.Start || challengeEnd != config.End {
|
||||||
if touchTimer != nil {
|
if touchTimer != nil {
|
||||||
touchTimer.Stop()
|
touchTimer.Stop()
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
|
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
|
||||||
<meta name="author" content="EPITA Laboratoire SRS">
|
|
||||||
<meta name="robots" content="all">
|
<meta name="robots" content="all">
|
||||||
<base href="/">
|
<base href="/">
|
||||||
%svelte.head%
|
%svelte.head%
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
Icon,
|
Icon,
|
||||||
} from 'sveltestrap';
|
} from 'sveltestrap';
|
||||||
|
|
||||||
|
import { challengeInfo } from '../stores/challengeinfo.js';
|
||||||
import { settings, time } from '../stores/settings.js';
|
import { settings, time } from '../stores/settings.js';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -53,9 +54,9 @@
|
||||||
Classement
|
Classement
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="{$settings.videoslink}"
|
href="{$challengeInfo.videoslink}"
|
||||||
class="btn btn-light"
|
class="btn btn-light"
|
||||||
class:disabled={$settings.videoslink === ''}
|
class:disabled={$challengeInfo.videoslink === ''}
|
||||||
>
|
>
|
||||||
<Icon name="laptop-fill" />
|
<Icon name="laptop-fill" />
|
||||||
Vidéos
|
Vidéos
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
import ExerciceVideo from '../../components/ExerciceVideo.svelte';
|
import ExerciceVideo from '../../components/ExerciceVideo.svelte';
|
||||||
import ThemeNav from '../../components/ThemeNav.svelte';
|
import ThemeNav from '../../components/ThemeNav.svelte';
|
||||||
|
|
||||||
|
import { challengeInfo } from '../../stores/challengeinfo.js';
|
||||||
import { my } from '../../stores/my.js';
|
import { my } from '../../stores/my.js';
|
||||||
import { settings } from '../../stores/settings.js';
|
import { settings } from '../../stores/settings.js';
|
||||||
|
|
||||||
|
@ -54,7 +55,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{exercice?exercice.title+" - ":""}{$settings.title}</title>
|
<title>{exercice?exercice.title+" - ":""}{$challengeInfo.title}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if exercice}
|
{#if exercice}
|
||||||
|
|
|
@ -30,13 +30,13 @@
|
||||||
Container,
|
Container,
|
||||||
} from 'sveltestrap';
|
} from 'sveltestrap';
|
||||||
|
|
||||||
import { settings } from '../../stores/settings.js';
|
import { challengeInfo } from '../../stores/challengeinfo.js';
|
||||||
|
|
||||||
export let theme;
|
export let theme;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{theme?theme.name:""} - {$settings.title}</title>
|
<title>{theme?theme.name:""} - {$challengeInfo.title}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if theme}
|
{#if theme}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script context="module">
|
<script context="module">
|
||||||
|
import { challengeInfo } from '../stores/challengeinfo.js';
|
||||||
import { issuesStore } from '../stores/issues.js';
|
import { issuesStore } from '../stores/issues.js';
|
||||||
import { my } from '../stores/my.js';
|
import { my } from '../stores/my.js';
|
||||||
import { teamsStore } from '../stores/teams.js';
|
import { teamsStore } from '../stores/teams.js';
|
||||||
|
@ -41,6 +42,10 @@
|
||||||
settings.update(await fetch('settings.json', {headers: {'Accept': 'application/json'}}), cb);
|
settings.update(await fetch('settings.json', {headers: {'Accept': 'application/json'}}), cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refresh_challengeInfo(cb=null) {
|
||||||
|
challengeInfo.update(await fetch('challenge.json', {headers: {'Accept': 'application/json'}}), cb);
|
||||||
|
}
|
||||||
|
|
||||||
let refresh_interval_teams = null;
|
let refresh_interval_teams = null;
|
||||||
async function refresh_teams(cb=null, interval=null) {
|
async function refresh_teams(cb=null, interval=null) {
|
||||||
if (refresh_interval_teams)
|
if (refresh_interval_teams)
|
||||||
|
@ -102,6 +107,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function load({ stuff }) {
|
export async function load({ stuff }) {
|
||||||
|
await refresh_challengeInfo();
|
||||||
await refresh_settings();
|
await refresh_settings();
|
||||||
await refresh_themes();
|
await refresh_themes();
|
||||||
refresh_teams();
|
refresh_teams();
|
||||||
|
@ -115,6 +121,7 @@
|
||||||
return {
|
return {
|
||||||
stuff: {
|
stuff: {
|
||||||
...stuff,
|
...stuff,
|
||||||
|
refresh_challengeInfo,
|
||||||
refresh_settings,
|
refresh_settings,
|
||||||
refresh_teams,
|
refresh_teams,
|
||||||
refresh_themes,
|
refresh_themes,
|
||||||
|
@ -138,7 +145,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{$settings.title}</title>
|
<title>{$challengeInfo.title}</title>
|
||||||
|
<meta name="author" content="{$challengeInfo.authors}">
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<!--Styles /-->
|
<!--Styles /-->
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
import { my } from '../stores/my.js';
|
import { my } from '../stores/my.js';
|
||||||
import { rank } from '../stores/teams.js';
|
import { rank } from '../stores/teams.js';
|
||||||
import { settings } from '../stores/settings.js';
|
import { challengeInfo } from '../stores/challengeinfo.js';
|
||||||
|
|
||||||
import CardTheme from '../components/CardTheme.svelte';
|
import CardTheme from '../components/CardTheme.svelte';
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<Container fluid class="my-3">
|
<Container fluid class="my-3">
|
||||||
<h1 class="text-dark">
|
<h1 class="text-dark">
|
||||||
{$settings.title}
|
{$challengeInfo.title}
|
||||||
<small class="text-muted">Classement</small>
|
<small class="text-muted">Classement</small>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="card niceborder text-light">
|
<div class="card niceborder text-light">
|
||||||
|
|
|
@ -6,12 +6,13 @@
|
||||||
Icon,
|
Icon,
|
||||||
} from 'sveltestrap';
|
} from 'sveltestrap';
|
||||||
|
|
||||||
|
import { challengeInfo } from '../stores/challengeinfo.js';
|
||||||
import { settings } from '../stores/settings.js';
|
import { settings } from '../stores/settings.js';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Container class="my-3">
|
<Container class="my-3">
|
||||||
<h1 class="text-dark">
|
<h1 class="text-dark">
|
||||||
{$settings.title}
|
{$challengeInfo.title}
|
||||||
<small class="text-muted">Règles générales</small>
|
<small class="text-muted">Règles générales</small>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { readable, writable } from 'svelte/store';
|
||||||
|
|
||||||
|
function createChallengeStore() {
|
||||||
|
const { subscribe, set, update } = writable({});
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
update: (res_challenge, cb) => {
|
||||||
|
if (res_challenge.status === 200) {
|
||||||
|
res_challenge.json().then((challenge) => {
|
||||||
|
update((s) => (Object.assign({}, challenge)));
|
||||||
|
|
||||||
|
if (cb) {
|
||||||
|
cb(challenge);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const challengeInfo = createChallengeStore();
|
|
@ -20,7 +20,7 @@ type ChallengeZQDS struct {
|
||||||
type SessionZQDS struct {
|
type SessionZQDS struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Scenario string `json:"scenario"`
|
Scenario string `json:"scenario,omitempty"`
|
||||||
YourMission string `json:"your_mission"`
|
YourMission string `json:"your_mission"`
|
||||||
Rules string `json:"rules"`
|
Rules string `json:"rules"`
|
||||||
Start time.Time `json:"start"`
|
Start time.Time `json:"start"`
|
||||||
|
@ -28,7 +28,7 @@ type SessionZQDS struct {
|
||||||
Challenges []*ChallengeZQDS `json:"challenges"`
|
Challenges []*ChallengeZQDS `json:"challenges"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenZQDSSessionFile(s *settings.Settings) (*SessionZQDS, error) {
|
func GenZQDSSessionFile(c *settings.ChallengeInfo, s *settings.Settings) (*SessionZQDS, error) {
|
||||||
themes, err := GetThemes()
|
themes, err := GetThemes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -58,11 +58,10 @@ func GenZQDSSessionFile(s *settings.Settings) (*SessionZQDS, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SessionZQDS{
|
return &SessionZQDS{
|
||||||
Name: "Challenge Forensic",
|
Name: c.Title,
|
||||||
Description: "",
|
Description: c.Description,
|
||||||
Scenario: "",
|
Rules: c.Rules,
|
||||||
YourMission: "",
|
YourMission: c.YourMission,
|
||||||
Rules: "",
|
|
||||||
Start: s.Start,
|
Start: s.Start,
|
||||||
End: s.End,
|
End: s.End,
|
||||||
Challenges: challenges,
|
Challenges: challenges,
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChallengeFile is the expected name of the file containing the challenge infos.
|
||||||
|
const ChallengeFile = "challenge.json"
|
||||||
|
|
||||||
|
// ChallengeInfo stores common descriptions and informations about the challenge.
|
||||||
|
type ChallengeInfo struct {
|
||||||
|
// Title is the displayed name of the challenge.
|
||||||
|
Title string `json:"title"`
|
||||||
|
// Authors is the group name of people making the challenge.
|
||||||
|
Authors string `json:"authors"`
|
||||||
|
// VideoLink is the link to explaination videos when the challenge is over.
|
||||||
|
VideosLink string `json:"videoslink"`
|
||||||
|
|
||||||
|
// Description gives an overview of the challenge.
|
||||||
|
Description string `json:"description"`
|
||||||
|
// Rules tell the player some help.
|
||||||
|
Rules string `json:"rules"`
|
||||||
|
// YourMission is a small introduction to understand the goals.
|
||||||
|
YourMission string `json:"your_mission"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadChallenge parses the file at the given location.
|
||||||
|
func ReadChallengeInfo(path string) (*ChallengeInfo, error) {
|
||||||
|
var s ChallengeInfo
|
||||||
|
if fd, err := os.Open(path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
defer fd.Close()
|
||||||
|
jdec := json.NewDecoder(fd)
|
||||||
|
|
||||||
|
if err := jdec.Decode(&s); err != nil {
|
||||||
|
return &s, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveChallenge saves challenge at the given location.
|
||||||
|
func SaveChallengeInfo(path string, s *ChallengeInfo) error {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,12 +22,6 @@ var SettingsDir string = "./SETTINGS"
|
||||||
|
|
||||||
// Settings represents the settings panel.
|
// Settings represents the settings panel.
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
// Title is the displayed name of the challenge.
|
|
||||||
Title string `json:"title"`
|
|
||||||
// Authors is the group name of people making the challenge.
|
|
||||||
Authors string `json:"authors"`
|
|
||||||
// VideoLink is the link to explaination videos when the challenge is over.
|
|
||||||
VideosLink string `json:"videoslink"`
|
|
||||||
// WorkInProgress indicates if the current challenge is under development or if it is in production.
|
// WorkInProgress indicates if the current challenge is under development or if it is in production.
|
||||||
WorkInProgress bool `json:"wip,omitempty"`
|
WorkInProgress bool `json:"wip,omitempty"`
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue