Compare commits

...

17 Commits

Author SHA1 Message Date
nemunaire ee2f65aae7 admin: new route to get exercice stats 2019-02-04 09:11:31 +01:00
nemunaire c2887a1812 admin: add a new route to update team history coefficient 2019-02-04 09:11:13 +01:00
nemunaire 4125c7b161 admin: include coefficient in history.json 2019-02-04 09:11:12 +01:00
nemunaire b73eaa8b3f admin: add exercice history.json 2019-02-04 09:11:12 +01:00
nemunaire 1dd55a0f73 libfic: fix checks in handling of team history deletiion 2019-02-04 09:11:12 +01:00
nemunaire 8afc7b9488 sync: avoid useless line break at the end of markdown processing 2019-02-04 09:11:12 +01:00
nemunaire 2fcbc44c60 frontend: display hint cost on public interface 2019-02-04 09:11:12 +01:00
nemunaire 8803852d47 frontend: public interface: keep number of tries between refresh 2019-02-04 09:11:12 +01:00
nemunaire 8790d6d678 frontend: don't reuse tries in public interface; use a separate field to store total tries count for an exercice; and display it in interface 2019-02-04 09:11:12 +01:00
nemunaire 6c03d04f36 admin: show only active team in export 2019-02-04 09:11:12 +01:00
nemunaire 0b0a978964 admin: thanks to ng-base, don't need other modifications 2019-02-04 09:11:12 +01:00
nemunaire caa0511023 admin: add a new route to generate a file for movie links 2019-01-25 16:31:48 +01:00
nemunaire 81502ce238 frontend: prefer default border color in home public screen 2019-01-25 13:32:27 +01:00
nemunaire 7587cd9140 Remove old unused files 2019-01-25 12:39:27 +01:00
nemunaire 2b106df669 frontend: avoid fetching events.json on public interface 2019-01-25 08:29:39 +01:00
nemunaire 04b42de061 libfic: add igncorecase flag to regexp related to ignorecase flag 2019-01-25 07:54:27 +01:00
nemunaire c129b2e477 frontend: polish public version checks 2019-01-25 07:53:46 +01:00
20 changed files with 418 additions and 348 deletions

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"strings"
"time"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
@ -14,12 +15,17 @@ import (
func init() {
router.GET("/api/exercices/", apiHandler(listExercices))
router.GET("/api/resolutions.json", apiHandler(exportResolutionMovies))
router.GET("/api/exercices/:eid", apiHandler(exerciceHandler(showExercice)))
router.PUT("/api/exercices/:eid", apiHandler(exerciceHandler(updateExercice)))
router.PATCH("/api/exercices/:eid", apiHandler(exerciceHandler(partUpdateExercice)))
router.DELETE("/api/exercices/:eid", apiHandler(exerciceHandler(deleteExercice)))
router.GET("/api/exercices/:eid/stats", apiHandler(exerciceHandler(getExerciceStats)))
router.GET("/api/exercices/:eid/history.json", apiHandler(exerciceHandler(getExerciceHistory)))
router.DELETE("/api/exercices/:eid/history.json", apiHandler(exerciceHandler(delExerciceHistory)))
router.GET("/api/exercices/:eid/files", apiHandler(exerciceHandler(listExerciceFiles)))
router.POST("/api/exercices/:eid/files", apiHandler(exerciceHandler(createExerciceFile)))
router.GET("/api/exercices/:eid/files/:fid", apiHandler(exerciceFileHandler(showExerciceFile)))
@ -85,6 +91,28 @@ func listExercices(_ httprouter.Params, body []byte) (interface{}, error) {
return fic.GetExercices()
}
// Generate the csv to export with:
// curl -s http://127.0.0.1:8081/api/resolutions.json | jq -r ".[] | [ .theme,.title, @uri \"https://fic.srs.epita.fr/resolution/\\(.videoURI)\" ] | join(\";\")"
func exportResolutionMovies(_ httprouter.Params, body []byte) (interface{}, error) {
if exercices, err := fic.GetExercices(); err != nil {
return nil, err
} else {
export := []map[string]string{}
for _, exercice := range exercices {
if theme, err := fic.GetTheme(exercice.IdTheme); err != nil {
return nil, err
} else {
export = append(export, map[string]string{
"videoURI": exercice.VideoURI,
"theme": theme.Name,
"title": exercice.Title,
})
}
}
return export, nil
}
}
func listExerciceFiles(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice.GetFiles()
}
@ -109,6 +137,35 @@ func showExercice(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice, nil
}
func getExerciceStats(exercice fic.Exercice, body []byte) (interface{}, error) {
team_tries, tries := exercice.TriesByTeam()
return map[string]interface{}{
"solved": exercice.SolvedTeams(),
"total_tried": tries,
"team_tries": team_tries,
}, nil
}
func getExerciceHistory(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice.GetHistory()
}
type uploadedExerciceHistory struct {
IdTeam int64 `json:"team_id"`
Kind string
Time time.Time
Secondary *int64
}
func delExerciceHistory(exercice fic.Exercice, body []byte) (interface{}, error) {
var uh uploadedExerciceHistory
if err := json.Unmarshal(body, &uh); err != nil {
return nil, err
}
return exercice.DelHistoryItem(uh.IdTeam, uh.Kind, uh.Time, uh.Secondary)
}
func deleteExercice(exercice fic.Exercice, _ []byte) (interface{}, error) {
return exercice.Delete()
}

View File

@ -238,10 +238,27 @@ func setTeamMember(team fic.Team, body []byte) (interface{}, error) {
}
type uploadedHistory struct {
Kind string
Time time.Time
Primary *int64
Secondary *int64
Kind string
Time time.Time
Primary *int64
Secondary *int64
Coefficient float32
}
func updateHistory(team *fic.Team, body []byte) (interface{}, error) {
var uh uploadedHistory
if err := json.Unmarshal(body, &uh); err != nil {
return nil, err
}
var givenId int64
if uh.Secondary != nil {
givenId = *uh.Secondary
} else if uh.Primary != nil {
givenId = *uh.Primary
}
return team.UpdateHistoryCoeff(uh.Kind, uh.Time, givenId, uh.Coefficient)
}
func delHistory(team *fic.Team, body []byte) (interface{}, error) {

View File

@ -1,262 +0,0 @@
#!/bin/bash
BASEURL="http://localhost:8081"
BASEURI="https://owncloud.srs.epita.fr/remote.php/webdav/FIC 2018"
BASEFILE="/mnt/fic/"
CLOUDPASS="$CLOUD_USER:$CLOUD_PASS"
new_theme() {
NAME=`echo $1 | sed 's/"/\\\\"/g'`
AUTHORS=`echo $2 | sed 's/"/\\\\"/g'`
curl -f -s -d "{\"name\": \"$NAME\", \"authors\": \"$AUTHORS\"}" "${BASEURL}/api/themes" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
new_exercice() {
THEME="$1"
TITLE=`echo "$2" | sed 's/"/\\\\"/g'`
STATEMENT=`echo "$3" | sed 's/"/\\\\"/g' | sed ':a;N;$!ba;s/\n/<br>/g'`
DEPEND="$4"
GAIN="$5"
VIDEO="$6"
curl -f -s -d "{\"title\": \"$TITLE\", \"statement\": \"$STATEMENT\", \"depend\": $DEPEND, \"gain\": $GAIN, \"videoURI\": \"$VIDEO\"}" "${BASEURL}/api/themes/$THEME/exercices" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
new_file() (
THEME="$1"
EXERCICE="$2"
URI="$3"
DIGEST="$4"
ARGS="$5"
FIRST=
PARTS=$(echo "$ARGS" | while read arg
do
[ -n "$arg" ] && {
[ -z "${FIRST}" ] || echo -n ","
echo "\"$arg\""
}
FIRST=1
done)
[ -n "${DIGEST}" ] && DIGEST=", \"digest\": \"${DIGEST}\""
cat <<EOF >&2
{"path": "${BASEFILE}${URI}"${DIGEST}, "parts": [${PARTS}]}
EOF
# curl -f -s -d "{\"URI\": \"${BASEFILE}${URI}\"}" "${BASEURL}/api/themes/$THEME/$EXERCICE/files" |
curl -f -s -d @- "${BASEURL}/api/themes/$THEME/exercices/$EXERCICE/files" <<EOF | grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
{"path": "${BASEFILE}${URI}"${DIGEST}, "parts": [${PARTS}]}
EOF
)
new_hint() {
THEME="$1"
EXERCICE="$2"
TITLE=`echo "$3" | sed 's/"/\\\\"/g'`
CONTENT=`echo "$4" | sed 's/"/\\\\"/g' | sed ':a;N;$!ba;s/\n/<br>/g'`
COST="$5"
URI="$6"
[ -n "${CONTENT}" ] && CONTENT=", \"content\": \"${CONTENT}\""
[ -n "${URI}" ] && URI=", \"path\": \"${BASEFILE}${URI}\""
curl -f -s -d "{\"title\": \"$TITLE\"$CONTENT$URI, \"cost\": $COST}" "${BASEURL}/api/themes/$THEME/exercices/$EXERCICE/hints" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
new_key() {
THEME="$1"
EXERCICE="$2"
TYPE="$3"
KEY=`echo $4 | sed 's#\\\\#\\\\\\\\#g' | sed 's/"/\\\\"/g'`
curl -f -s -d "{\"type\": \"$TYPE\", \"key\": \"$KEY\"}" "${BASEURL}/api/themes/$THEME/exercices/$EXERCICE/keys" |
grep -Eo '"id":[0-9]+,' | grep -Eo "[0-9]+"
}
get_dir_from_cloud() {
curl -f -s -X PROPFIND -u "${CLOUDPASS}" "${BASEURI}$1" | xmllint --format - | grep 'd:href' | sed -E 's/^.*>(.*)<.*$/\1/'
}
get_dir() {
ls "${BASEFILE}$1" 2> /dev/null
}
#alias get_dir=get_dir_from_cloud
get_file_from_cloud() {
curl -f -s -u "${CLOUDPASS}" "${BASEURI}$1" | tr -d '\r'
}
get_file() {
cat "${BASEFILE}$1" 2> /dev/null | tr -d '\r'
echo
}
#alias get_file=get_file_from_cloud
unhtmlentities() {
cat | sed -E 's/%20/ /g' | sed -E "s/%27/'/g" | sed -E 's/%c3%a9/é/g' | sed -E 's/%c3%a8/è/g'
}
# Theme
{
if [ $# -ge 1 ]; then
echo $1
else
get_dir ""
fi
} | while read f; do basename "$f"; done | while read THEME_URI
do
THM_BASEURI="/${THEME_URI}/"
THEME_NAME=$(echo "${THEME_URI#*-}" | unhtmlentities)
THEME_AUTHORS=$(get_file "${THM_BASEURI}/AUTHORS.txt" | sed '/^$/d;s/$/, /' | tr -d '\n' | sed 's/, $//')
THEME_ID=`new_theme "$THEME_NAME" "$THEME_AUTHORS"`
if [ -z "$THEME_ID" ]; then
echo -e "\e[31;01m!!! An error occured during theme add\e[00m"
continue
else
echo -e "\e[33m>>> New theme created:\e[00m $THEME_ID - $THEME_NAME"
fi
LAST=null
EXO_NUM=0
{
if [ $# -ge 2 ]; then
echo "$2"
else
get_dir "${THM_BASEURI}"
fi
} | while read f; do basename "$f"; done | while read EXO_URI
do
case ${EXO_URI} in
[0-9]-*)
;;
*)
continue;;
esac
#EXO_NUM=$((EXO_NUM + 1))
EXO_NUM=${EXO_URI%-*}
EXO_NAME=$(echo "${EXO_URI#*-}" | unhtmlentities)
echo
echo -e "\e[36m--- Filling exercice ${EXO_NUM} in theme ${THEME_NAME}\e[00m"
EXO_BASEURI="${EXO_URI}/"
EXO_VIDEO=$(get_dir "${THM_BASEURI}${EXO_BASEURI}/resolution/" | grep -E "\.(mov|mkv|mp4|avi|flv|ogv|webm)$" | while read f; do basename "$f"; done | tail -1)
[ -n "$EXO_VIDEO" ] && EXO_VIDEO="/resolution${THM_BASEURI}${EXO_BASEURI}resolution/${EXO_VIDEO}"
if [ "${LAST}" = "null" ]; then
echo ">>> Assuming this exercice has no dependency"
else
echo ">>> Assuming this exercice depends on the last entry (id=${LAST})"
fi
EXO_GAIN=$((3 * (2 ** $EXO_NUM) - 1))
HINT_COST=$(($EXO_GAIN / 4))
echo ">>> Using default gain: ${EXO_GAIN} points"
EXO_SCENARIO=$(get_file "${THM_BASEURI}${EXO_BASEURI}/scenario.txt")
EXO_ID=`new_exercice "${THEME_ID}" "${EXO_NAME}" "${EXO_SCENARIO}" "${LAST}" "${EXO_GAIN}" "${EXO_VIDEO}"`
if [ -z "$EXO_ID" ]; then
echo -e "\e[31;01m!!! An error occured during exercice add.\e[00m"
continue
else
echo -e "\e[32m>>> New exercice created:\e[00m $EXO_ID - $EXO_NAME"
fi
# Keys
get_file "${THM_BASEURI}${EXO_BASEURI}/flags.txt" | while read KEYLINE
do
[ -z "${KEYLINE}" ] && continue
KEY_NAME=$(echo "$KEYLINE" | cut -d$'\t' -f 1)
KEY_RAW=$(echo "$KEYLINE" | cut -d$'\t' -f 2-)
if [ -z "${KEY_RAW}" ] || [ "${KEY_NAME}" = "${KEY_RAW}" ]; then
KEY_NAME=$(echo "$KEYLINE" | cut -d : -f 1)
KEY_RAW=$(echo "$KEYLINE" | cut -d : -f 2-)
fi
if [ -z "${KEY_NAME}" ]; then
KEY_NAME="Flag"
fi
KEY_ID=`new_key "${THEME_ID}" "${EXO_ID}" "${KEY_NAME}" "${KEY_RAW}"`
if [ -z "$KEY_ID" ]; then
echo -e "\e[31;01m!!! An error occured during key import!\e[00m (name=${KEYNAME};raw=${KEY_RAW})"
else
echo -e "\e[32m>>> New key added:\e[00m $KEY_ID - $KEY_NAME"
fi
done
# Hints
HINTS=$(get_dir "${THM_BASEURI}${EXO_BASEURI}/hints/" | sed -E 's#(.*)#hints/\1#')
[ -z "${HINTS}" ] && HINTS=$(get_dir "${THM_BASEURI}${EXO_BASEURI}/" | grep ^hint.)
[ -z "${HINTS}" ] && HINTS="hint.txt"
HINT_COUNT=1
echo "${HINTS}" | while read HINT
do
EXO_HINT=$(get_file "${THM_BASEURI}${EXO_BASEURI}/${HINT}")
if [ -n "$EXO_HINT" ]; then
EXO_HINT_TYPE=$(echo "${EXO_HINT}" | file --mime-type -b -)
if echo "${EXO_HINT_TYPE}" | grep text/ && [ $(echo "${EXO_HINT}" | wc -l) -lt 25 ]; then
HINT_ID=`new_hint "${THEME_ID}" "${EXO_ID}" "Astuce #${HINT_COUNT}" "${EXO_HINT}" "${HINT_COST}"`
else
HINT_ID=`new_hint "${THEME_ID}" "${EXO_ID}" "Astuce #${HINT_COUNT}" "" "${HINT_COST}" "${THM_BASEURI}${EXO_BASEURI}/${HINT}"`
fi
if [ -z "$HINT_ID" ]; then
echo -e "\e[31;01m!!! An error occured during hint import!\e[00m (title=Astuce #${HINT_COUNT};content::${EXO_HINT_TYPE};cost=${HINT_COST})"
else
echo -e "\e[32m>>> New hint added:\e[00m $HINT_ID - Astuce #${HINT_COUNT}"
fi
fi
HINT_COUNT=$(($HINT_COUNT + 1))
done
# Files: splited
get_dir "${THM_BASEURI}${EXO_BASEURI}files/" | grep -v DIGESTS.txt | grep '[0-9][0-9]$' | sed -E 's/\.?([0-9][0-9])$//' | sort | uniq | while read f; do basename "$f"; done | while read FILE_URI
do
DIGEST=$(get_file "${THM_BASEURI}${EXO_BASEURI}files/DIGESTS.txt" | grep "${FILE_URI}\$" | awk '{ print $1; }')
PARTS=
for part in $(get_dir "${THM_BASEURI}${EXO_BASEURI}files/" | grep "${FILE_URI}" | sort)
do
PARTS="${PARTS}${BASEFILE}${THM_BASEURI}${EXO_BASEURI}files/${part}
"
done
echo -e "\e[35mImport splited file ${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI} from\e[00m `echo ${PARTS} | tr '\n' ' '`"
FILE_ID=`new_file "${THEME_ID}" "${EXO_ID}" "${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}" "${DIGEST}" "${PARTS}"`
if [ -z "$FILE_ID" ]; then
echo -e "\e[31;01m!!! An error occured during file import! Please check path.\e[00m"
else
echo -e "\e[32m>>> New file added:\e[00m $FILE_ID - $FILE_URI"
fi
done
# Files: entire
get_dir "${THM_BASEURI}${EXO_BASEURI}files/" | grep -v DIGESTS.txt | grep -v '[0-9][0-9]$' | while read f; do basename "$f"; done | while read FILE_URI
do
DIGEST=$(get_file "${THM_BASEURI}${EXO_BASEURI}files/DIGESTS.txt" | grep "${FILE_URI}\$" | awk '{ print $1; }')
echo "Import file ${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}"
FILE_ID=`new_file "${THEME_ID}" "${EXO_ID}" "${THM_BASEURI}${EXO_BASEURI}files/${FILE_URI}" "${DIGEST}"`
if [ -z "$FILE_ID" ]; then
echo -e "\e[31;01m!!! An error occured during file import! Please check path.\e[00m"
else
echo -e "\e[32m>>> New file added:\e[00m $FILE_ID - $FILE_URI"
fi
done
LAST=$EXO_ID
done
echo
done

View File

@ -1,23 +0,0 @@
#!/bin/sh
BASEURL="http://localhost:8081"
BASEURI="https://srs.epita.fr/owncloud/remote.php/webdav/FIC 2016"
CLOUDUSER='fic'
CLOUDPASS='f>t\nV33R|(+?$i*'
if [ $# -gt 0 ]
then
WHERE=$1
else
WHERE="files"
fi
curl -q -f ${BASEURL}/api/themes/files-bindings | while read l
do
FROM=$(echo "$l" | cut -d ";" -f 1)
DEST=$(echo "$l" | cut -d ";" -f 2)
mkdir -p $(dirname "${WHERE}${DEST}")
wget -O "${WHERE}${DEST}" --user "${CLOUDUSER}" --password "${CLOUDPASS}" "${BASEURI}${FROM}"
done

View File

@ -32,8 +32,8 @@ const indextpl = `<!DOCTYPE html>
</head>
<body class="bg-light text-dark">
<nav class="navbar sticky-top navbar-expand-lg navbar-dark bg-dark text-light">
<a class="navbar-brand" href="{{.urlbase}}">
<img alt="FIC" src="{{.urlbase}}img/fic.png" style="height: 30px">
<a class="navbar-brand" href=".">
<img alt="FIC" src="img/fic.png" style="height: 30px">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#adminMenu" aria-controls="adminMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
@ -41,15 +41,15 @@ const indextpl = `<!DOCTYPE html>
<div class="collapse navbar-collapse" id="adminMenu">
<ul class="navbar-nav mr-auto">
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}teams">&Eacute;quipes</a></li>
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}pki">PKI</a></li>
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}themes">Thèmes</a></li>
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}exercices">Exercices</a></li>
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}files">Fichiers</a></li>
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}public/0">Public</a></li>
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}events">&Eacute;vénements</a></li>
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}claims">Tâches</a></li>
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}settings">Paramètres</a></li>
<li class="nav-item"><a class="nav-link" href="teams">&Eacute;quipes</a></li>
<li class="nav-item"><a class="nav-link" href="pki">PKI</a></li>
<li class="nav-item"><a class="nav-link" href="themes">Thèmes</a></li>
<li class="nav-item"><a class="nav-link" href="exercices">Exercices</a></li>
<li class="nav-item"><a class="nav-link" href="files">Fichiers</a></li>
<li class="nav-item"><a class="nav-link" href="public/0">Public</a></li>
<li class="nav-item"><a class="nav-link" href="events">&Eacute;vénements</a></li>
<li class="nav-item"><a class="nav-link" href="claims">Tâches</a></li>
<li class="nav-item"><a class="nav-link" href="settings">Paramètres</a></li>
</ul>
</div>
@ -87,14 +87,14 @@ const indextpl = `<!DOCTYPE html>
<div class="container mt-1" ng-view></div>
<script src="/js/jquery.min.js"></script>
<script src="{{.urlbase}}js/popper.min.js"></script>
<script src="js/popper.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/angular.min.js"></script>
<script src="{{.urlbase}}js/angular-resource.min.js"></script>
<script src="js/angular-resource.min.js"></script>
<script src="/js/angular-route.min.js"></script>
<script src="/js/angular-sanitize.min.js"></script>
<script src="{{.urlbase}}js/app.js"></script>
<script src="{{.urlbase}}js/common.js"></script>
<script src="js/app.js"></script>
<script src="js/common.js"></script>
</body>
</html>
`

View File

@ -30,8 +30,8 @@
</head>
<body class="bg-light text-dark">
<nav class="navbar sticky-top navbar-expand-lg navbar-dark bg-dark text-light">
<a class="navbar-brand" href="/admin/">
<img alt="FIC" src="/admin/img/fic.png" style="height: 30px">
<a class="navbar-brand" href=".">
<img alt="FIC" src="img/fic.png" style="height: 30px">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#adminMenu" aria-controls="adminMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
@ -39,15 +39,15 @@
<div class="collapse navbar-collapse" id="adminMenu">
<ul class="navbar-nav mr-auto">
<li class="nav-item"><a class="nav-link" href="/admin/teams">&Eacute;quipes</a></li>
<li class="nav-item"><a class="nav-link" href="/admin/pki">PKI</a></li>
<li class="nav-item"><a class="nav-link" href="/admin/themes">Thèmes</a></li>
<li class="nav-item"><a class="nav-link" href="/admin/exercices">Exercices</a></li>
<li class="nav-item"><a class="nav-link" href="/admin/files">Fichiers</a></li>
<li class="nav-item"><a class="nav-link" href="/admin/public/0">Public</a></li>
<li class="nav-item"><a class="nav-link" href="/admin/events">&Eacute;vénements</a></li>
<li class="nav-item"><a class="nav-link" href="/admin/claims">Tâches</a></li>
<li class="nav-item"><a class="nav-link" href="/admin/settings">Paramètres</a></li>
<li class="nav-item"><a class="nav-link" href="teams">&Eacute;quipes</a></li>
<li class="nav-item"><a class="nav-link" href="pki">PKI</a></li>
<li class="nav-item"><a class="nav-link" href="themes">Thèmes</a></li>
<li class="nav-item"><a class="nav-link" href="exercices">Exercices</a></li>
<li class="nav-item"><a class="nav-link" href="files">Fichiers</a></li>
<li class="nav-item"><a class="nav-link" href="public/0">Public</a></li>
<li class="nav-item"><a class="nav-link" href="events">&Eacute;vénements</a></li>
<li class="nav-item"><a class="nav-link" href="claims">Tâches</a></li>
<li class="nav-item"><a class="nav-link" href="settings">Paramètres</a></li>
</ul>
</div>
@ -85,13 +85,13 @@
<div class="container mt-1" ng-view></div>
<script src="/js/jquery.min.js"></script>
<script src="/admin/js/popper.min.js"></script>
<script src="js/popper.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/angular.min.js"></script>
<script src="/admin/js/angular-resource.min.js"></script>
<script src="js/angular-resource.min.js"></script>
<script src="/js/angular-route.min.js"></script>
<script src="/js/angular-sanitize.min.js"></script>
<script src="/admin/js/app.js"></script>
<script src="/admin/js/common.js"></script>
<script src="js/app.js"></script>
<script src="js/common.js"></script>
</body>
</html>

View File

@ -228,6 +228,9 @@ angular.module("FICApp")
update: {method: 'PUT'}
})
})
.factory("ExerciceHistory", function($resource) {
return $resource("/api/exercices/:exerciceId/history.json", { exerciceId: '@id' })
})
.factory("ExerciceFile", function($resource) {
return $resource("/api/exercices/:exerciceId/files/:fileId", { exerciceId: '@idExercice', fileId: '@id' }, {
update: {method: 'PUT'}
@ -1194,6 +1197,12 @@ angular.module("FICApp")
} else {
$scope.exercice = Exercice.get({ exerciceId: $routeParams.exerciceId });
}
$http({
url: "/api/exercices/" + $routeParams.exerciceId + "/stats",
method: "GET"
}).then(function(response) {
$scope.stats = response.data;
});
$http({
url: "/api/themes.json",
method: "GET"
@ -1272,6 +1281,22 @@ angular.module("FICApp")
}
})
.controller("ExerciceHistoryController", function($scope, ExerciceHistory, $routeParams, $http, $rootScope) {
$scope.history = ExerciceHistory.query({ exerciceId: $routeParams.exerciceId });
$scope.delHistory = function(row) {
$http({
url: "/api/exercices/" + $routeParams.exerciceId + "/history.json",
method: "DELETE",
data: row
}).then(function(response) {
$rootScope.staticFilesNeedUpdate++;
$scope.history = ExerciceHistory.query({ exerciceId: $routeParams.exerciceId });
}, function(response) {
$rootScope.newBox('danger', 'An error occurs when removing history item: ', response.data.errmsg);
});
}
})
.controller("ExerciceFilesController", function($scope, ExerciceFile, $routeParams, $rootScope, $http) {
$scope.files = ExerciceFile.query({ exerciceId: $routeParams.exerciceId });

View File

@ -221,6 +221,34 @@
</div>
</div>
<div class="mt-2" style="overflow-y: scroll; height: 450px">
<h3>Historique</h3>
<table ng-controller="ExerciceHistoryController" class="table table-hover table-striped table-bordered bg-primary text-light">
<tbody>
<tr ng-repeat="row in history" ng-class="{'bg-ffound': row.kind == 'flag_found', 'bg-mfound': row.kind == 'mcq_found', 'bg-wchoices': row.kind == 'wchoices', 'bg-success': row.kind == 'solved', 'bg-info': row.kind == 'hint', 'bg-warning': row.kind == 'tries'}">
<td>
<nobr title="{{ row.time }}">{{ row.time | date:"mediumTime" }}</nobr><br>{{ row.kind }} <span ng-if="row.kind != 'flag_found' && row.kind != 'tries' && row.kind != 'mcq_found'">x{{ row.coefficient }}</span>
</td>
<td>
<span ng-if="row.team_id">
<a href="teams/{{ row.team_id }}">{{ row.team_name }}</a>
</span>
<span ng-if="row.secondary_title">
:
<a href="exercices/{{ row.primary }}#key-{{ row.secondary }}" ng-if="row.kind == 'flag_found' || row.kind == 'wchoices'">{{ row.secondary_title }}</a>
<a href="exercices/{{ row.primary }}#quizz-{{ row.secondary }}" ng-if="row.kind == 'mcq_found'">{{ row.secondary_title }}</a>
<a href="exercices/{{ row.primary }}#hint-{{ row.secondary }}" ng-if="row.kind == 'hint'">{{ row.secondary_title }}</a>
</span>
<span ng-if="!row.secondary_title && row.secondary && row.kind != 'solved'">: {{ row.secondary }}</span>
</td>
<td style="vertical-align: middle; padding: 0; background-color: {{ row.team_color }}">
<button type="button" ng-click="delHistory(row)" class="float-right btn btn-sm btn-danger"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -104,12 +104,12 @@
</div>
<div class="col-lg-4">
<div class="col-lg-4" style="overflow-y: scroll; height: 95vh">
<table ng-controller="TeamHistoryController" class="table table-hover table-striped table-bordered bg-primary text-light">
<tbody>
<tr ng-repeat="row in history" ng-class="{'bg-ffound': row.kind == 'flag_found', 'bg-mfound': row.kind == 'mcq_found', 'bg-wchoices': row.kind == 'wchoices', 'bg-success': row.kind == 'solved', 'bg-info': row.kind == 'hint', 'bg-warning': row.kind == 'tries'}">
<td>
<nobr title="{{ row.time }}">{{ row.time | date:"mediumTime" }}</nobr><br>{{ row.kind }}
<nobr title="{{ row.time }}">{{ row.time | date:"mediumTime" }}</nobr><br>{{ row.kind }} <span ng-if="row.kind != 'flag_found' && row.kind != 'tries' && row.kind != 'mcq_found'">x{{ row.coefficient }}</span>
</td>
<td>
<span ng-if="row.primary_title">
@ -118,7 +118,7 @@
<span ng-if="!row.primary_title">{{ row.primary }}</span>
<span ng-if="row.secondary_title">
:
<a href="exercices/{{ row.primary }}#key-{{ row.secondary }}" ng-if="row.kind == 'key_found'">{{ row.secondary_title }}</a>
<a href="exercices/{{ row.primary }}#key-{{ row.secondary }}" ng-if="row.kind == 'flag_found' || row.kind == 'wchoices'">{{ row.secondary_title }}</a>
<a href="exercices/{{ row.primary }}#quizz-{{ row.secondary }}" ng-if="row.kind == 'mcq_found'">{{ row.secondary_title }}</a>
<a href="exercices/{{ row.primary }}#hint-{{ row.secondary }}" ng-if="row.kind == 'hint'">{{ row.secondary_title }}</a>
</span>

View File

@ -1,4 +1,4 @@
<div ng-repeat="team in teams" ng-click="show(team.id)" ng-controller="TeamController">
<div ng-repeat="team in teams" ng-click="show(team.id)" ng-controller="TeamController" ng-if="team.active">
<div ng-controller="TeamExercicesController">
<div ng-if="teams[my.team_id].rank">
<h2>{{ team.name }} <small>{{ teams[my.team_id].rank }}/{{ nb_teams }} &ndash; <ng-pluralize count="my.score" when="{'one': '{}&nbsp;point', 'other': '{}&nbsp;points'}"></ng-pluralize></small></h2>

View File

@ -59,5 +59,8 @@ func ProcessMarkdown(i Importer, input string, rootDir string) (output string, e
}
}
// Trim output
output = strings.TrimSpace(output)
return
}

View File

@ -19,12 +19,21 @@ function treatFlagKey(flag) {
}
if (flag.found == null && flag.soluce !== undefined) {
if (check === undefined) check = true;
if (flag.value && flag.soluce) {
if (flag.ignore_case)
flag.value = flag.value.toLowerCase();
if (flag.validator_regexp) {
var re = new RegExp(flag.validator_regexp, flag.ignore_case?'ui':'u');
var match = re.exec(flag.value);
match.shift();
flag.value = match.join("+");
}
if (flag.value && flag.soluce == b2sum(flag.value))
flag.found = new Date();
check &= flag.found;
if (flag.soluce == b2sum(flag.value))
flag.found = new Date();
}
}
return flag.found !== undefined && flag.found !== false;
}
angular.module("FICApp", ["ngRoute", "ngSanitize"])
@ -241,6 +250,8 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
var refreshRate = 1200;
if ($rootScope.notify_field == 0 && eventsLastTreated)
refreshRate = 30000;
if ($scope.my && !$scope.my.team_id)
return;
refreshEventsInterval = $interval(refreshEvents, Math.floor(Math.random() * refreshRate * 2) + refreshRate);
if (!eventsLastTreated) {
@ -329,7 +340,15 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
angular.forEach(data.exercices, function(exercice, eid) {
if ($scope.my && $scope.my.exercices[eid] && $scope.my.exercices[eid].submitted)
data.exercices[eid].timeouted = true;
if ($scope.my && $scope.my.exercices[eid] && $scope.my.exercices[eid].solved !== undefined)
data.exercices[eid].solved = $scope.my.exercices[eid].solved;
if ($scope.my && $scope.my.exercices[eid] && $scope.my.exercices[eid].solved_time !== undefined && data.exercices[eid].solved_time === undefined)
data.exercices[eid].solved_time = $scope.my.exercices[eid].solved_time;
if ($scope.my && $scope.my.exercices[eid] && $scope.my.exercices[eid].tries !== undefined && data.exercices[eid].tries === undefined)
data.exercices[eid].tries = $scope.my.exercices[eid].tries;
angular.forEach(exercice.flags, function(flag, fid) {
if ($scope.my && $scope.my.exercices[eid] && $scope.my.exercices[eid].flags && $scope.my.exercices[eid].flags[fid] && $scope.my.exercices[eid].flags[fid].found !== undefined)
data.exercices[eid].flags[fid].found = $scope.my.exercices[eid].flags[fid].found;
if ($scope.my && $scope.my.exercices[eid] && $scope.my.exercices[eid].flags && $scope.my.exercices[eid].flags[fid] && $scope.my.exercices[eid].flags[fid].value !== undefined)
data.exercices[eid].flags[fid].value = $scope.my.exercices[eid].flags[fid].value;
if ($scope.my && $scope.my.exercices[eid] && $scope.my.exercices[eid].flags && $scope.my.exercices[eid].flags[fid] && $scope.my.exercices[eid].flags[fid].values !== undefined)
@ -337,6 +356,10 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
else
data.exercices[eid].flags[fid].values = [""];
});
angular.forEach(exercice.mcqs, function(mcq, mid) {
if ($scope.my && $scope.my.exercices[eid] && $scope.my.exercices[eid].mcqs && $scope.my.exercices[eid].mcqs[mid] && $scope.my.exercices[eid].mcqs[mid].solved !== undefined)
data.exercices[eid].mcqs[mid].solved = $scope.my.exercices[eid].mcqs[mid].solved;
});
});
angular.forEach(data.exercices, function(exercice, eid) {
angular.forEach(exercice.mcqs, function(mcq, mid) {
@ -474,7 +497,9 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
{
resp["flags"] = {};
angular.forEach($scope.my.exercices[$rootScope.current_exercice].flags, function(flag,kid) {
treatFlagKey(flag);
if (check === undefined) check = true;
check &= treatFlagKey(flag) || flag.found;
if (flag.found == null && flag.soluce === undefined) {
resp["flags"][kid] = flag.value;
}
@ -483,9 +508,9 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
if ($scope.my.exercices[$rootScope.current_exercice].mcqs && Object.keys($scope.my.exercices[$rootScope.current_exercice].mcqs).length)
{
var soluce = "";
resp["mcqs"] = {};
angular.forEach($scope.my.exercices[$rootScope.current_exercice].mcqs, function(mcq) {
var soluce = "";
if (mcq.solved == null) {
angular.forEach(mcq.choices, function(choice, cid) {
if (mcq.soluce !== undefined) {
@ -515,6 +540,8 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
if (check !== undefined)
{
if (!$scope.my.exercices[$rootScope.current_exercice].tries)
$scope.my.exercices[$rootScope.current_exercice].tries = 0;
$scope.my.exercices[$rootScope.current_exercice].tries += 1;
$scope.my.exercices[$rootScope.current_exercice].solved_time = new Date();

View File

@ -31,6 +31,7 @@
<hr class="my-3">
<ul>
<li><strong>Gain&nbsp;:</strong> <ng-pluralize count="themes[current_theme].exercices[current_exercice].gain" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize> <em ng-if="settings.firstBlood && themes[current_theme].exercices[current_exercice].solved < 1">{{ 1 + settings.firstBlood | coeff }} prem's</em> <em ng-if="themes[current_theme].exercices[current_exercice].curcoeff != 1.0 || settings.exerciceCurrentCoefficient">{{ themes[current_theme].exercices[current_exercice].curcoeff * settings.exerciceCurrentCoefficient | coeff }} bonus</em></li>
<li ng-if="themes[current_theme].exercices[current_exercice].tried"><strong>Tenté par&nbsp;:</strong> <ng-pluralize count="themes[current_theme].exercices[current_exercice].tried" when="{'0': 'aucune équipe', 'one': '{} équipe', 'other': '{} équipes'}"></ng-pluralize> <span ng-if="my.exercices[current_exercice].total_tries">(cumulant <ng-pluralize count="my.exercices[current_exercice].total_tries" when="{'one': '{} tentative', 'other': '{} tentatives'}"></ng-pluralize>)</span>.</li>
<li><strong>Résolu par&nbsp;:</strong> <ng-pluralize count="themes[current_theme].exercices[current_exercice].solved" when="{'0': 'aucune équipe', 'one': '{} équipe', 'other': '{} équipes'}"></ng-pluralize>.</li>
</ul>
</div>
@ -73,7 +74,7 @@
<h4 class="list-group-item-heading">{{ hint.title }}</h4>
<p class="list-group-item-text" ng-if="!hint.file && hint.content && !hint.hidden" ng-bind-html="hint.content"></p>
<p class="list-group-item-text" ng-if="hint.file">Cliquez ici pour télécharger l'indice.<br>b2sum&nbsp;: <samp class="cksum" title="Somme de contrôle BLAKE2b : {{ hint.content }}">{{ hint.content }}</samp></p>
<p class="list-group-item-text" ng-if="!hint.file && !hint.content && (hint.hidden === true || hint.hidden === undefined)">Débloquer cet indice vous fera perdre <ng-pluralize count="hint.cost * settings.hintCurrentCoefficient" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize>.</p>
<p class="list-group-item-text" ng-if="!hint.file && ((!hint.content && hint.hidden === undefined) || hint.hidden === true)">Débloquer cet indice vous fera perdre <ng-pluralize count="hint.cost * settings.hintCurrentCoefficient" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize>.</p>
</a>
</div>
</div>
@ -119,9 +120,12 @@
<span class="glyphicon glyphicon-flag" aria-hidden="true"></span> Défi réussi !
</div>
<div class="card-body">
<p class="card-text">
<p class="card-text" ng-if="my.exercices[current_exercice].solved_rank">
Vous êtes la {{ my.exercices[current_exercice].solved_rank }}<sup><ng-pluralize count="my.exercices[current_exercice].solved_rank" when="{'one': 're', 'other': 'e'}"></ng-pluralize></sup> équipe à avoir résolu ce défi à {{ my.exercices[current_exercice].solved_time | date:"mediumTime" }}. Vous avez marqué <ng-pluralize count="my.exercices[current_exercice].gain" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize> !
</p>
<p class="card-text" ng-if="!my.exercices[current_exercice].solved_rank">
Bravo, vous avez résolu ce défi à {{ my.exercices[current_exercice].solved_time | date:"mediumTime" }}. Vous marquez <ng-pluralize count="my.exercices[current_exercice].gain" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize> !
</p>
<hr ng-if="my.exercices[current_exercice].finished">
<p class="card-text" ng-if="my.exercices[current_exercice].finished" ng-bind-html="my.exercices[current_exercice].finished"></p>
<hr ng-if="my.exercices[current_exercice].finished && themes[current_theme].exercices[current_exercice].next">

View File

@ -15,7 +15,7 @@
</div>
<div class="card-columns">
<div class="card niceborder excard" ng-repeat="(k,theme) in themes" ng-click="goTheme()" ng-class="{'border-success': theme.solved > 0, 'border-warning': theme.exercice_coeff_max > 1}">
<div class="card niceborder excard" ng-repeat="(k,theme) in themes" ng-click="goTheme()" ng-class="{'border-success': my.team_id && theme.solved > 0, 'border-warning': theme.exercice_coeff_max > 1}">
<div class="card-img-top theme-card" ng-show="theme.image" style="background-image: url({{ theme.image }})"></div>
<div class="card-body text-indent">
<h5 class="card-title">

View File

@ -1,5 +1,5 @@
<div class="card-columns">
<div class="card niceborder excard" ng-repeat="ex in exercices" ng-click="goDefi()" ng-class="{'border-success': ex.exercice.solved, 'border-secondary': !my.exercices[ex.eid], 'border-warning': ex.exercice.curcoeff > 1.0}">
<div class="card niceborder excard" ng-repeat="ex in exercices" ng-click="goDefi()" ng-class="{'border-success': my.team_id && ex.exercice.solved, 'border-secondary': !my.exercices[ex.eid], 'border-warning': ex.exercice.curcoeff > 1.0}">
<div class="card-img-top theme-card" ng-show="ex.theme.image" style="background-image: url({{ ex.theme.image }})"></div>
<div class="card-body">
<h6 class="card-title">

View File

@ -330,6 +330,21 @@ func (e Exercice) SolvedCount() int64 {
}
}
// SolvedTeams returns the list of Team that already have solved the challenge.
func (e Exercice) SolvedTeams() (teams []int64) {
if rows, err := DBQuery("SELECT id_team FROM exercice_solved WHERE id_exercice = ?", e.Id); err == nil {
defer rows.Close()
for rows.Next() {
var tid int64
if err := rows.Scan(&tid); err == nil {
teams = append(teams, tid)
}
}
}
return
}
// TriedTeamCount returns the number of Team that attempted to solve the exercice.
func (e Exercice) TriedTeamCount() int64 {
tries_table := "exercice_tries"
@ -345,6 +360,32 @@ func (e Exercice) TriedTeamCount() int64 {
}
}
type TbT struct {
IdTeam int64 `json:"id_team"`
Tries int64 `json:"tries"`
}
// TriesByTeam returns the number of tries by Team.
func (e Exercice) TriesByTeam() (ts []TbT, sum int64) {
tries_table := "exercice_tries"
if SubmissionUniqueness {
tries_table = "exercice_distinct_tries"
}
if rows, err := DBQuery("SELECT id_team, COUNT(id_team) FROM " + tries_table + " WHERE id_exercice = ? GROUP BY id_team", e.Id); err == nil {
defer rows.Close()
for rows.Next() {
var tbt TbT
if err := rows.Scan(&tbt.IdTeam, &tbt.Tries); err == nil {
sum += tbt.Tries
ts = append(ts, tbt)
}
}
}
return
}
// TriedCount returns the number of cumulative attempts, all Team combined, for the exercice.
func (e Exercice) TriedCount() int64 {
tries_table := "exercice_tries"

112
libfic/exercice_history.go Normal file
View File

@ -0,0 +1,112 @@
package fic
import (
"fmt"
"time"
)
// GetHistory aggregates all sources of events or actions for an Exercice
func (e Exercice) GetHistory() ([]map[string]interface{}, error) {
hist := make([]map[string]interface{}, 0)
if rows, err := DBQuery(`SELECT id_team, U.name, U.color, "tries" AS kind, time, 0, id_exercice, NULL, NULL FROM exercice_tries NATURAL JOIN teams U WHERE id_exercice = ? UNION
SELECT id_team, U.name, U.color, "solved" AS kind, time, coefficient, id_exercice, NULL, NULL FROM exercice_solved S NATURAL JOIN teams U WHERE id_exercice = ? UNION
SELECT id_team, U.name, U.color, "hint" AS kind, time, coefficient, id_exercice, H.id_hint, H.title FROM team_hints T INNER JOIN exercice_hints H ON H.id_hint = T.id_hint NATURAL JOIN teams U WHERE id_exercice = ? UNION
SELECT id_team, U.name, U.color, "wchoices" AS kind, time, coefficient, id_exercice, F.id_flag, F.type FROM team_wchoices W INNER JOIN exercice_flags F ON F.id_flag = W.id_flag NATURAL JOIN teams U WHERE id_exercice = ? UNION
SELECT id_team, U.name, U.color, "flag_found" AS kind, time, 0, id_exercice, K.id_flag, K.type FROM flag_found F INNER JOIN exercice_flags K ON K.id_flag = F.id_flag NATURAL JOIN teams U WHERE id_exercice = ? UNION
SELECT id_team, U.name, U.color, "mcq_found" AS kind, time, 0, id_exercice, Q.id_mcq, Q.title FROM mcq_found F INNER JOIN exercice_mcq Q ON Q.id_mcq = F.id_mcq NATURAL JOIN teams U WHERE id_exercice = ?
ORDER BY time DESC`, e.Id, e.Id, e.Id, e.Id, e.Id, e.Id); err != nil {
return nil, err
} else {
defer rows.Close()
for rows.Next() {
var id_team int64
var team_name string
var team_color uint32
var kind string
var time time.Time
var coeff float32
var exercice int64
var secondary *int64
var secondary_title *string
if err := rows.Scan(&id_team, &team_name, &team_color, &kind, &time, &coeff, &exercice, &secondary, &secondary_title); err != nil {
return nil, err
}
h := map[string]interface{}{}
h["team_id"] = id_team
h["team_name"] = team_name
h["team_color"] = fmt.Sprintf("#%x", team_color)
h["kind"] = kind
h["time"] = time
h["coefficient"] = coeff
h["primary"] = e.Id
if secondary != nil {
h["secondary"] = secondary
h["secondary_title"] = secondary_title
}
hist = append(hist, h)
}
}
return hist, nil
}
// DelHistoryItem removes from the database an entry from the history.
func (e Exercice) DelHistoryItem(tId int64, kind string, h time.Time, secondary *int64) (interface{}, error) {
if kind == "tries" {
if res, err := DBExec("DELETE FROM exercice_tries WHERE id_team = ? AND time = ? AND id_exercice = ?", tId, h, e.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
} else if kind == "hint" && secondary != nil {
if res, err := DBExec("DELETE FROM team_hints WHERE id_team = ? AND time = ? AND id_hint = ?", tId, h, *secondary); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
} else if kind == "wchoices" && secondary != nil {
if res, err := DBExec("DELETE FROM team_wchoices WHERE id_team = ? AND time = ? AND id_flag = ?", tId, h, *secondary); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
} else if kind == "flag_found" && secondary != nil {
if res, err := DBExec("DELETE FROM flag_found WHERE id_team = ? AND time = ? AND id_flag = ?", tId, h, *secondary); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
} else if kind == "mcq_found" && secondary != nil {
if res, err := DBExec("DELETE FROM mcq_found WHERE id_team = ? AND time = ? AND id_mcq = ?", tId, h, *secondary); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
} else if kind == "solved" {
if res, err := DBExec("DELETE FROM exercice_solved WHERE id_team = ? AND time = ? AND id_exercice = ?", tId, h, e.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
} else {
return nil, nil
}
}

View File

@ -73,7 +73,10 @@ func getHashedFlag(raw_value []byte) [blake2b.Size]byte {
return hash
}
func ExecValidatorRegexp(vre string, val []byte) ([]byte, error) {
func ExecValidatorRegexp(vre string, val []byte, ignorecase bool) ([]byte, error) {
if (ignorecase) {
vre = "(?i)" + vre
}
if re, err := regexp.Compile(vre); err != nil {
return val, err
} else if res := re.FindSubmatch(val); res == nil {
@ -92,7 +95,7 @@ func (e Exercice) AddRawFlagKey(name string, help string, ignorecase bool, valid
// Check that raw value passes through the regexp
if validator_regexp != nil {
var err error
if raw_value, err = ExecValidatorRegexp(*validator_regexp, raw_value); err != nil {
if raw_value, err = ExecValidatorRegexp(*validator_regexp, raw_value, ignorecase); err != nil {
return FlagKey{}, err
}
}
@ -127,7 +130,7 @@ func (k FlagKey) ComputeChecksum(val []byte) (cksum []byte, err error) {
// Check that raw value passes through the regexp
if k.ValidatorRegexp != nil {
if val, err = ExecValidatorRegexp(*k.ValidatorRegexp, val); err != nil {
if val, err = ExecValidatorRegexp(*k.ValidatorRegexp, val, k.IgnoreCase); err != nil {
return
}
}

View File

@ -8,12 +8,12 @@ import (
func (t Team) GetHistory() ([]map[string]interface{}, error) {
hist := make([]map[string]interface{}, 0)
if rows, err := DBQuery(`SELECT id_team, "tries" AS kind, time, E.id_exercice, E.title, NULL, NULL FROM exercice_tries T INNER JOIN exercices E ON E.id_exercice = T.id_exercice WHERE id_team = ? UNION
SELECT id_team, "solved" AS kind, time, E.id_exercice, E.title, coefficient, NULL FROM exercice_solved S INNER JOIN exercices E ON E.id_exercice = S.id_exercice WHERE id_team = ? UNION
SELECT id_team, "hint" AS kind, time, E.id_exercice, E.title, H.id_hint, H.title FROM team_hints T INNER JOIN exercice_hints H ON H.id_hint = T.id_hint INNER JOIN exercices E ON E.id_exercice = H.id_exercice WHERE id_team = ? UNION
SELECT id_team, "wchoices" AS kind, time, E.id_exercice, E.title, F.id_flag, F.type FROM team_wchoices W INNER JOIN exercice_flags F ON F.id_flag = W.id_flag INNER JOIN exercices E ON E.id_exercice = F.id_exercice WHERE id_team = ? UNION
SELECT id_team, "flag_found" AS kind, time, E.id_exercice, E.title, K.id_flag, K.type FROM flag_found F INNER JOIN exercice_flags K ON K.id_flag = F.id_flag INNER JOIN exercices E ON K.id_exercice = E.id_exercice WHERE id_team = ? UNION
SELECT id_team, "mcq_found" AS kind, time, E.id_exercice, E.title, Q.id_mcq, Q.title FROM mcq_found F INNER JOIN exercice_mcq Q ON Q.id_mcq = F.id_mcq INNER JOIN exercices E ON Q.id_exercice = E.id_exercice WHERE id_team = ?
if rows, err := DBQuery(`SELECT id_team, "tries" AS kind, time, 0, E.id_exercice, E.title, NULL, NULL FROM exercice_tries T INNER JOIN exercices E ON E.id_exercice = T.id_exercice WHERE id_team = ? UNION
SELECT id_team, "solved" AS kind, time, coefficient, E.id_exercice, E.title, NULL, NULL FROM exercice_solved S INNER JOIN exercices E ON E.id_exercice = S.id_exercice WHERE id_team = ? UNION
SELECT id_team, "hint" AS kind, time, coefficient, E.id_exercice, E.title, H.id_hint, H.title FROM team_hints T INNER JOIN exercice_hints H ON H.id_hint = T.id_hint INNER JOIN exercices E ON E.id_exercice = H.id_exercice WHERE id_team = ? UNION
SELECT id_team, "wchoices" AS kind, time, coefficient, E.id_exercice, E.title, F.id_flag, F.type FROM team_wchoices W INNER JOIN exercice_flags F ON F.id_flag = W.id_flag INNER JOIN exercices E ON E.id_exercice = F.id_exercice WHERE id_team = ? UNION
SELECT id_team, "flag_found" AS kind, time, 0, E.id_exercice, E.title, K.id_flag, K.type FROM flag_found F INNER JOIN exercice_flags K ON K.id_flag = F.id_flag INNER JOIN exercices E ON K.id_exercice = E.id_exercice WHERE id_team = ? UNION
SELECT id_team, "mcq_found" AS kind, time, 0, E.id_exercice, E.title, Q.id_mcq, Q.title FROM mcq_found F INNER JOIN exercice_mcq Q ON Q.id_mcq = F.id_mcq INNER JOIN exercices E ON Q.id_exercice = E.id_exercice WHERE id_team = ?
ORDER BY time DESC`, t.Id, t.Id, t.Id, t.Id, t.Id, t.Id); err != nil {
return nil, err
} else {
@ -23,12 +23,13 @@ func (t Team) GetHistory() ([]map[string]interface{}, error) {
var id_team int64
var kind string
var time time.Time
var coefficient float32
var primary *int64
var primary_title *string
var secondary *int64
var secondary_title *string
if err := rows.Scan(&id_team, &kind, &time, &primary, &primary_title, &secondary, &secondary_title); err != nil {
if err := rows.Scan(&id_team, &kind, &time, &coefficient, &primary, &primary_title, &secondary, &secondary_title); err != nil {
return nil, err
}
@ -36,6 +37,7 @@ func (t Team) GetHistory() ([]map[string]interface{}, error) {
h["kind"] = kind
h["time"] = time
h["coefficient"] = coefficient
if primary != nil {
h["primary"] = primary
h["primary_title"] = primary_title
@ -52,6 +54,37 @@ func (t Team) GetHistory() ([]map[string]interface{}, error) {
return hist, nil
}
// UpdateHistoryCoeff updates the coefficient for a given entry.
func (t Team) UpdateHistoryCoeff(kind string, h time.Time, givenId int64, newCoeff float32) (interface{}, error) {
if kind == "hint" {
if res, err := DBExec("UPDATE team_hints SET coefficient = ? WHERE id_team = ? AND time = ? AND id_hint = ?", newCoeff, t.Id, h, givenId); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
} else if kind == "wchoices" {
if res, err := DBExec("UPDATE team_wchoices SET coefficient = ? WHERE id_team = ? AND time = ? AND id_flag = ?", newCoeff, t.Id, h, givenId); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
} else if kind == "solved" {
if res, err := DBExec("UPDATE exercice_solved SET coefficient = ? WHERE id_team = ? AND time = ? AND id_exercice = ?", newCoeff, t.Id, h, givenId); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
} else {
return nil, nil
}
}
// DelHistoryItem removes from the database an entry from the history.
func (t Team) DelHistoryItem(kind string, h time.Time, primary *int64, secondary *int64) (interface{}, error) {
if kind == "tries" && primary != nil {
@ -62,7 +95,7 @@ func (t Team) DelHistoryItem(kind string, h time.Time, primary *int64, secondary
} else {
return nb, err
}
} else if kind == "hint" && primary != nil {
} else if kind == "hint" && primary != nil && secondary != nil {
if res, err := DBExec("DELETE FROM team_hints WHERE id_team = ? AND time = ? AND id_hint = ?", t.Id, h, *secondary); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
@ -70,7 +103,7 @@ func (t Team) DelHistoryItem(kind string, h time.Time, primary *int64, secondary
} else {
return nb, err
}
} else if kind == "wchoices" && primary != nil {
} else if kind == "wchoices" && primary != nil && secondary != nil {
if res, err := DBExec("DELETE FROM team_wchoices WHERE id_team = ? AND time = ? AND id_flag = ?", t.Id, h, *secondary); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
@ -78,7 +111,7 @@ func (t Team) DelHistoryItem(kind string, h time.Time, primary *int64, secondary
} else {
return nb, err
}
} else if kind == "flag_found" && secondary != nil {
} else if kind == "flag_found" && primary != nil && secondary != nil {
if res, err := DBExec("DELETE FROM flag_found WHERE id_team = ? AND time = ? AND id_flag = ?", t.Id, h, *secondary); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
@ -86,7 +119,7 @@ func (t Team) DelHistoryItem(kind string, h time.Time, primary *int64, secondary
} else {
return nb, err
}
} else if kind == "mcq_found" && secondary != nil {
} else if kind == "mcq_found" && primary != nil && secondary != nil {
if res, err := DBExec("DELETE FROM mcq_found WHERE id_team = ? AND time = ? AND id_mcq = ?", t.Id, h, *secondary); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {

View File

@ -30,6 +30,8 @@ type myTeamFlag struct {
Help string `json:"help,omitempty"`
Separator string `json:"separator,omitempty"`
IgnoreOrder bool `json:"ignore_order,omitempty"`
IgnoreCase bool `json:"ignore_case,omitempty"`
ValidatorRe *string `json:"validator_regexp,omitempty"`
Solved *time.Time `json:"found,omitempty"`
Soluce string `json:"soluce,omitempty"`
Choices map[string]string `json:"choices,omitempty"`
@ -62,6 +64,7 @@ type myTeamExercice struct {
SolvedTime *time.Time `json:"solved_time,omitempty"`
SolvedRank int64 `json:"solved_rank,omitempty"`
Tries int64 `json:"tries,omitempty"`
TotalTries int64 `json:"total_tries,omitempty"`
VideoURI string `json:"video_uri,omitempty"`
Issue string `json:"issue,omitempty"`
IssueKind string `json:"issuekind,omitempty"`
@ -112,7 +115,7 @@ func MyJSONTeam(t *Team, started bool) (interface{}, error) {
exercice.Overview = strings.Replace(e.Overview, "$FILES$", FilesDir, -1)
exercice.Finished = strings.Replace(e.Finished, "$FILES$", FilesDir, -1)
exercice.VideoURI = e.VideoURI
exercice.Tries = e.TriedCount()
exercice.TotalTries = e.TriedCount()
exercice.Gain = int(float64(e.Gain) * e.Coefficient)
} else {
solved, stime := t.HasSolved(e)
@ -189,6 +192,8 @@ func MyJSONTeam(t *Team, started bool) (interface{}, error) {
// Retrieve solved state or solution for public iface
if t == nil {
flag.IgnoreCase = k.IgnoreCase
flag.ValidatorRe = k.ValidatorRegexp
flag.Soluce = hex.EncodeToString(k.Checksum)
} else if PartialValidation {
flag.Solved = t.HasPartiallySolved(k)