New option to allow teams to self reset their progression
This commit is contained in:
parent
a0c34018cf
commit
d6ff46ca7f
@ -182,6 +182,13 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="custom-control custom-checkbox">
|
||||||
|
<input class="custom-control-input" type="checkbox" ng-model="config.canResetProgress" ng-disabled="!config.wip">
|
||||||
|
<span class="custom-control-label" ng-class="{'text-primary font-weight-bold': !config.canResetProgress != !dist_config.canResetProgress}">Autoriser les équipes à auto-effacer leur progression</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<label class="custom-control custom-checkbox">
|
<label class="custom-control custom-checkbox">
|
||||||
<input class="custom-control-input" type="checkbox" ng-model="config.enableExerciceDepend" ng-change="exerciceDependChange()">
|
<input class="custom-control-input" type="checkbox" ng-model="config.enableExerciceDepend" ng-change="exerciceDependChange()">
|
||||||
|
@ -71,6 +71,7 @@ func reloadSettings(config *settings.Settings) {
|
|||||||
allowRegistration = config.AllowRegistration
|
allowRegistration = config.AllowRegistration
|
||||||
canJoinTeam = config.CanJoinTeam
|
canJoinTeam = config.CanJoinTeam
|
||||||
denyTeamCreation = config.DenyTeamCreation
|
denyTeamCreation = config.DenyTeamCreation
|
||||||
|
canResetProgression = config.WorkInProgress && config.CanResetProgression
|
||||||
fic.HintCoefficient = config.HintCurCoefficient
|
fic.HintCoefficient = config.HintCurCoefficient
|
||||||
fic.WChoiceCoefficient = config.WChoiceCurCoefficient
|
fic.WChoiceCoefficient = config.WChoiceCurCoefficient
|
||||||
fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient
|
fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient
|
||||||
@ -211,6 +212,8 @@ func treat(raw_path string) {
|
|||||||
treatOpeningHint(raw_path, team)
|
treatOpeningHint(raw_path, team)
|
||||||
case "choices":
|
case "choices":
|
||||||
treatWantChoices(raw_path, team)
|
treatWantChoices(raw_path, team)
|
||||||
|
case "reset_progress":
|
||||||
|
treatResetProgress(raw_path, team)
|
||||||
case ".locked":
|
case ".locked":
|
||||||
treatLocked(raw_path, team)
|
treatLocked(raw_path, team)
|
||||||
default:
|
default:
|
||||||
|
54
checker/reset_progress.go
Normal file
54
checker/reset_progress.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"srs.epita.fr/fic-server/libfic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var canResetProgression = false
|
||||||
|
|
||||||
|
type askResetProgress struct {
|
||||||
|
ExerciceId int64 `json:"eid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func treatResetProgress(pathname string, team *fic.Team) {
|
||||||
|
if !canResetProgression {
|
||||||
|
log.Printf("[!!!] Receive reset_progress, whereas desactivated in settings.\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique identifier to follow the request in logs
|
||||||
|
bid := make([]byte, 5)
|
||||||
|
binary.LittleEndian.PutUint32(bid, rand.Uint32())
|
||||||
|
id := "[" + base64.StdEncoding.EncodeToString(bid) + "]"
|
||||||
|
log.Println(id, "New ResetProgress receive", pathname)
|
||||||
|
|
||||||
|
var ask askResetProgress
|
||||||
|
|
||||||
|
if cnt_raw, err := ioutil.ReadFile(pathname); err != nil {
|
||||||
|
log.Printf("%s [ERR] %s\n", id, err)
|
||||||
|
} else if err = json.Unmarshal(cnt_raw, &ask); err != nil {
|
||||||
|
log.Printf("%s [ERR] %s\n", id, err)
|
||||||
|
} else if ask.ExerciceId == 0 {
|
||||||
|
log.Printf("%s [WRN] Invalid content in reset_progress file: %s\n", id, pathname)
|
||||||
|
os.Remove(pathname)
|
||||||
|
} else if exercice, err := fic.GetExercice(ask.ExerciceId); err != nil {
|
||||||
|
log.Printf("%s [ERR] Unable to retrieve the given exercice: %s\n", id, err)
|
||||||
|
} else if exercice.Disabled {
|
||||||
|
log.Println("[!!!] The team submits something for a disabled exercice")
|
||||||
|
} else if err := team.ResetProgressionOnExercice(exercice); err != nil {
|
||||||
|
log.Printf("%s [ERR] Unable to reset progression: %s\n", id, err)
|
||||||
|
} else {
|
||||||
|
appendGenQueue(fic.GenStruct{Id: id, Type: fic.GenTeam, TeamId: team.Id})
|
||||||
|
if err = os.Remove(pathname); err != nil {
|
||||||
|
log.Printf("%s [ERR] %s\n", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@
|
|||||||
Col,
|
Col,
|
||||||
Icon,
|
Icon,
|
||||||
Row,
|
Row,
|
||||||
|
Spinner,
|
||||||
} from 'sveltestrap';
|
} from 'sveltestrap';
|
||||||
|
|
||||||
import ExerciceDownloads from '$lib/components/ExerciceDownloads.svelte';
|
import ExerciceDownloads from '$lib/components/ExerciceDownloads.svelte';
|
||||||
@ -24,8 +25,29 @@
|
|||||||
import { current_theme } from '$lib/stores/themes';
|
import { current_theme } from '$lib/stores/themes';
|
||||||
import { settings } from '$lib/stores/settings';
|
import { settings } from '$lib/stores/settings';
|
||||||
|
|
||||||
|
import { waitDiff, waitInProgress } from '$lib/wait.js';
|
||||||
|
|
||||||
let solved = {};
|
let solved = {};
|
||||||
let openResolution = false;
|
let openResolution = false;
|
||||||
|
|
||||||
|
async function askProgressReset() {
|
||||||
|
waitInProgress.set(true);
|
||||||
|
const response = await fetch(
|
||||||
|
"reset_progress",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {'Accept': 'application/json'},
|
||||||
|
body: JSON.stringify({"eid": $current_exercice.id}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.status < 300) {
|
||||||
|
waitDiff(13, $my.exercices[$current_exercice.id]);
|
||||||
|
} else {
|
||||||
|
data = await response.json();
|
||||||
|
waitInProgress.set(false);
|
||||||
|
alert("Quelque chose s'est mal passé : " + data.errmsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $current_exercice}
|
{#if $current_exercice}
|
||||||
@ -162,19 +184,29 @@
|
|||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
{#if $my && $my.team_id}
|
{#if $my && $my.team_id}
|
||||||
<Col xs="auto">
|
<Col xs="auto" class="d-flex flex-column justify-content-between">
|
||||||
{#if $settings.acceptNewIssue}
|
{#if $settings.acceptNewIssue}
|
||||||
<a href="issues/?eid={$current_exercice.id}" class="float-end btn btn-sm btn-warning">
|
<a href="issues/?eid={$current_exercice.id}" class="btn btn-sm btn-warning">
|
||||||
<Icon name="bug" />
|
<Icon name="bug" />
|
||||||
Rapporter une anomalie sur ce défi
|
Rapporter une anomalie sur ce défi
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $settings.QAenabled}
|
{#if $settings.QAenabled}
|
||||||
<a href="qa/exercices/{$current_exercice.id}" class="float-end btn btn-sm btn-info" target="_self">
|
<a href="qa/exercices/{$current_exercice.id}" class="btn btn-sm btn-info" target="_self">
|
||||||
<Icon name="bug" />
|
<Icon name="bug" />
|
||||||
Voir les éléments QA sur ce défi
|
Voir les éléments QA sur ce défi
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $settings.wip && $settings.canResetProgress}
|
||||||
|
<Button size="sm" color="dark" on:click={askProgressReset} disabled={!$my.exercices[$current_exercice.id].solved_time || $waitInProgress}>
|
||||||
|
{#if $waitInProgress}
|
||||||
|
<Spinner size="sm" />
|
||||||
|
{:else}
|
||||||
|
<Icon name="skip-start-fill" />
|
||||||
|
{/if}
|
||||||
|
Effacer ma progression sur ce défi
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</Col>
|
</Col>
|
||||||
{/if}
|
{/if}
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -139,3 +139,47 @@ func (t *Team) DelHistoryItem(kind string, h time.Time, primary *int64, secondar
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResetProgressionOnExercice removes all tries and validations for a given Exercice.
|
||||||
|
func (t *Team) ResetProgressionOnExercice(exercice *Exercice) error {
|
||||||
|
hints, err := exercice.GetHints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
flags, err := exercice.GetFlags()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := DBExec("DELETE FROM exercice_tries WHERE id_team = ? AND id_exercice = ?", t.Id, exercice.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := DBExec("DELETE FROM exercice_solved WHERE id_team = ? AND id_exercice = ?", t.Id, exercice.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hint := range hints {
|
||||||
|
if _, err := DBExec("DELETE FROM team_hints WHERE id_team = ? AND id_hint = ?", t.Id, hint.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flag := range flags {
|
||||||
|
if k, ok := flag.(*FlagKey); ok {
|
||||||
|
if _, err := DBExec("DELETE FROM team_wchoices WHERE id_team = ? AND id_flag = ?", t.Id, k.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := DBExec("DELETE FROM flag_found WHERE id_team = ? AND id_flag = ?", t.Id, k.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if mcq, ok := flag.(*MCQ); ok {
|
||||||
|
if _, err := DBExec("DELETE FROM mcq_found WHERE id_team = ? AND id_mcq = ?", t.Id, mcq.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -52,6 +52,7 @@ func main() {
|
|||||||
http.Handle(fmt.Sprintf("%s/wantchoices/", *prefix), http.StripPrefix(fmt.Sprintf("%s/wantchoices/", *prefix), submissionTeamChecker{"wantint choices", WantChoicesHandler, *teamsDir, *simulator}))
|
http.Handle(fmt.Sprintf("%s/wantchoices/", *prefix), http.StripPrefix(fmt.Sprintf("%s/wantchoices/", *prefix), submissionTeamChecker{"wantint choices", WantChoicesHandler, *teamsDir, *simulator}))
|
||||||
http.Handle(fmt.Sprintf("%s/registration", *prefix), http.StripPrefix(fmt.Sprintf("%s/registration", *prefix), submissionChecker{"registration", RegistrationHandler}))
|
http.Handle(fmt.Sprintf("%s/registration", *prefix), http.StripPrefix(fmt.Sprintf("%s/registration", *prefix), submissionChecker{"registration", RegistrationHandler}))
|
||||||
http.Handle(fmt.Sprintf("%s/resolution/", *prefix), http.StripPrefix(fmt.Sprintf("%s/resolution/", *prefix), ResolutionHandler{}))
|
http.Handle(fmt.Sprintf("%s/resolution/", *prefix), http.StripPrefix(fmt.Sprintf("%s/resolution/", *prefix), ResolutionHandler{}))
|
||||||
|
http.Handle(fmt.Sprintf("%s/reset_progress", *prefix), http.StripPrefix(fmt.Sprintf("%s/reset_progress", *prefix), submissionTeamChecker{"reset_progress", ResetProgressHandler, *teamsDir, *simulator}))
|
||||||
http.Handle(fmt.Sprintf("%s/submission/", *prefix), http.StripPrefix(fmt.Sprintf("%s/submission/", *prefix), submissionTeamChecker{"submission", SubmissionHandler, *teamsDir, *simulator}))
|
http.Handle(fmt.Sprintf("%s/submission/", *prefix), http.StripPrefix(fmt.Sprintf("%s/submission/", *prefix), submissionTeamChecker{"submission", SubmissionHandler, *teamsDir, *simulator}))
|
||||||
|
|
||||||
if *simulator != "" {
|
if *simulator != "" {
|
||||||
|
26
receiver/reset.go
Normal file
26
receiver/reset.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var enableResetProgression = false
|
||||||
|
|
||||||
|
func ResetProgressHandler(w http.ResponseWriter, r *http.Request, team string, sURL []string) {
|
||||||
|
if !enableResetProgression {
|
||||||
|
http.Error(w, "{\"errmsg\":\"Le challenge est terminé, trop tard !\"}", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if challengeEnd != nil && time.Now().After(*challengeEnd) {
|
||||||
|
http.Error(w, "{\"errmsg\":\"Le challenge est terminé, trop tard !\"}", http.StatusGone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue file for backend treatment
|
||||||
|
if saveTeamFile(path.Join(team, "reset_progress"), w, r) {
|
||||||
|
http.Error(w, "{\"errmsg\":\"Demande acceptée...\"}", http.StatusAccepted)
|
||||||
|
}
|
||||||
|
}
|
@ -64,4 +64,5 @@ func reloadSettings(config *settings.Settings) {
|
|||||||
denyNameChange = config.DenyNameChange
|
denyNameChange = config.DenyNameChange
|
||||||
acceptNewIssues = config.AcceptNewIssue
|
acceptNewIssues = config.AcceptNewIssue
|
||||||
allowRegistration = config.AllowRegistration
|
allowRegistration = config.AllowRegistration
|
||||||
|
enableResetProgression = config.WorkInProgress && config.CanResetProgression
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,8 @@ type Settings struct {
|
|||||||
AcceptNewIssue bool `json:"acceptNewIssue,omitempty"`
|
AcceptNewIssue bool `json:"acceptNewIssue,omitempty"`
|
||||||
// QAenabled enables links to QA interface.
|
// QAenabled enables links to QA interface.
|
||||||
QAenabled bool `json:"QAenabled,omitempty"`
|
QAenabled bool `json:"QAenabled,omitempty"`
|
||||||
|
// CanResetProgression allows a team to reset the progress it made on a given exercice.
|
||||||
|
CanResetProgression bool `json:"canResetProgress,omitempty"`
|
||||||
// EnableResolutionRoute activates the route displaying resolution movies.
|
// EnableResolutionRoute activates the route displaying resolution movies.
|
||||||
EnableResolutionRoute bool `json:"enableResolutionRoute,omitempty"`
|
EnableResolutionRoute bool `json:"enableResolutionRoute,omitempty"`
|
||||||
// PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try.
|
// PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user