Compare commits

...
This repository has been archived on 2025-06-10. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

17 commits

Author SHA1 Message Date
ee2f65aae7 admin: new route to get exercice stats 2019-02-04 09:11:31 +01:00
c2887a1812 admin: add a new route to update team history coefficient 2019-02-04 09:11:13 +01:00
4125c7b161 admin: include coefficient in history.json 2019-02-04 09:11:12 +01:00
b73eaa8b3f admin: add exercice history.json 2019-02-04 09:11:12 +01:00
1dd55a0f73 libfic: fix checks in handling of team history deletiion 2019-02-04 09:11:12 +01:00
8afc7b9488 sync: avoid useless line break at the end of markdown processing 2019-02-04 09:11:12 +01:00
2fcbc44c60 frontend: display hint cost on public interface 2019-02-04 09:11:12 +01:00
8803852d47 frontend: public interface: keep number of tries between refresh 2019-02-04 09:11:12 +01:00
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
6c03d04f36 admin: show only active team in export 2019-02-04 09:11:12 +01:00
0b0a978964 admin: thanks to ng-base, don't need other modifications 2019-02-04 09:11:12 +01:00
caa0511023 admin: add a new route to generate a file for movie links 2019-01-25 16:31:48 +01:00
81502ce238 frontend: prefer default border color in home public screen 2019-01-25 13:32:27 +01:00
7587cd9140 Remove old unused files 2019-01-25 12:39:27 +01:00
2b106df669 frontend: avoid fetching events.json on public interface 2019-01-25 08:29:39 +01:00
04b42de061 libfic: add igncorecase flag to regexp related to ignorecase flag 2019-01-25 07:54:27 +01:00
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)