1157 lines
32 KiB
Go
1157 lines
32 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"srs.epita.fr/fic-server/admin/sync"
|
|
"srs.epita.fr/fic-server/libfic"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func declareGlobalExercicesRoutes(router *gin.RouterGroup) {
|
|
router.GET("/resolutions.json", exportResolutionMovies)
|
|
router.GET("/exercices_stats.json", getExercicesStats)
|
|
router.GET("/tags", listTags)
|
|
}
|
|
|
|
func declareExercicesRoutes(router *gin.RouterGroup) {
|
|
router.GET("/exercices", listExercices)
|
|
router.POST("/exercices", createExercice)
|
|
|
|
apiExercicesRoutes := router.Group("/exercices/:eid")
|
|
apiExercicesRoutes.Use(ExerciceHandler)
|
|
apiExercicesRoutes.GET("", showExercice)
|
|
apiExercicesRoutes.PUT("", updateExercice)
|
|
apiExercicesRoutes.PATCH("", partUpdateExercice)
|
|
apiExercicesRoutes.DELETE("", deleteExercice)
|
|
|
|
apiExercicesRoutes.GET("/stats.json", getExerciceStats)
|
|
|
|
apiExercicesRoutes.GET("/history.json", getExerciceHistory)
|
|
|
|
apiHistoryRoutes := apiExercicesRoutes.Group("/history.json")
|
|
apiHistoryRoutes.Use(AssigneeCookieHandler)
|
|
apiHistoryRoutes.PUT("", appendExerciceHistory)
|
|
apiHistoryRoutes.PATCH("", updateExerciceHistory)
|
|
apiHistoryRoutes.DELETE("", delExerciceHistory)
|
|
|
|
apiExercicesRoutes.GET("/hints", listExerciceHints)
|
|
apiExercicesRoutes.POST("/hints", createExerciceHint)
|
|
|
|
apiHintsRoutes := apiExercicesRoutes.Group("/hints/:hid")
|
|
apiHintsRoutes.Use(HintHandler)
|
|
apiHintsRoutes.GET("", showExerciceHint)
|
|
apiHintsRoutes.PUT("", updateExerciceHint)
|
|
apiHintsRoutes.DELETE("", deleteExerciceHint)
|
|
apiHintsRoutes.GET("/dependancies", showExerciceHintDeps)
|
|
|
|
apiExercicesRoutes.GET("/flags", listExerciceFlags)
|
|
apiExercicesRoutes.POST("/flags", createExerciceFlag)
|
|
|
|
apiFlagsRoutes := apiExercicesRoutes.Group("/flags/:kid")
|
|
apiFlagsRoutes.Use(FlagKeyHandler)
|
|
apiFlagsRoutes.GET("", showExerciceFlag)
|
|
apiFlagsRoutes.PUT("", updateExerciceFlag)
|
|
apiFlagsRoutes.POST("/try", tryExerciceFlag)
|
|
apiFlagsRoutes.DELETE("/", deleteExerciceFlag)
|
|
apiFlagsRoutes.GET("/dependancies", showExerciceFlagDeps)
|
|
apiFlagsRoutes.GET("/choices/", listFlagChoices)
|
|
apiFlagsChoicesRoutes := apiExercicesRoutes.Group("/choices/:cid")
|
|
apiFlagsChoicesRoutes.Use(FlagChoiceHandler)
|
|
apiFlagsChoicesRoutes.GET("", showFlagChoice)
|
|
apiFlagsRoutes.POST("/choices/", createFlagChoice)
|
|
apiFlagsChoicesRoutes.PUT("", updateFlagChoice)
|
|
apiFlagsChoicesRoutes.DELETE("", deleteFlagChoice)
|
|
|
|
apiQuizRoutes := apiExercicesRoutes.Group("/quiz/:qid")
|
|
apiQuizRoutes.Use(FlagQuizHandler)
|
|
apiExercicesRoutes.GET("/quiz", listExerciceQuiz)
|
|
apiQuizRoutes.GET("", showExerciceQuiz)
|
|
apiQuizRoutes.PUT("", updateExerciceQuiz)
|
|
apiQuizRoutes.DELETE("", deleteExerciceQuiz)
|
|
apiQuizRoutes.GET("/dependancies", showExerciceQuizDeps)
|
|
|
|
apiExercicesRoutes.GET("/tags", listExerciceTags)
|
|
apiExercicesRoutes.POST("/tags", addExerciceTag)
|
|
apiExercicesRoutes.PUT("/tags", updateExerciceTags)
|
|
|
|
declareFilesRoutes(apiExercicesRoutes)
|
|
declareExerciceClaimsRoutes(apiExercicesRoutes)
|
|
|
|
// Remote
|
|
router.GET("/remote/themes/:thid/exercices/:exid", sync.ApiGetRemoteExercice)
|
|
router.GET("/remote/themes/:thid/exercices/:exid/hints", sync.ApiGetRemoteExerciceHints)
|
|
router.GET("/remote/themes/:thid/exercices/:exid/flags", sync.ApiGetRemoteExerciceFlags)
|
|
}
|
|
|
|
type Exercice struct {
|
|
*fic.Exercice
|
|
ForgeLink string `json:"forge_link,omitempty"`
|
|
}
|
|
|
|
func ExerciceHandler(c *gin.Context) {
|
|
eid, err := strconv.ParseInt(string(c.Params.ByName("eid")), 10, 32)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid exercice identifier"})
|
|
return
|
|
}
|
|
|
|
var exercice *fic.Exercice
|
|
if theme, exists := c.Get("theme"); exists {
|
|
exercice, err = theme.(*fic.Theme).GetExercice(int(eid))
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Exercice not found"})
|
|
return
|
|
}
|
|
} else {
|
|
exercice, err = fic.GetExercice(eid)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Exercice not found"})
|
|
return
|
|
}
|
|
|
|
if exercice.IdTheme != nil {
|
|
theme, err = exercice.GetTheme()
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to find the attached theme."})
|
|
return
|
|
}
|
|
|
|
c.Set("theme", theme)
|
|
} else {
|
|
c.Set("theme", &fic.Theme{Path: sync.StandaloneExercicesDirectory})
|
|
}
|
|
}
|
|
|
|
c.Set("exercice", exercice)
|
|
|
|
c.Next()
|
|
}
|
|
|
|
func HintHandler(c *gin.Context) {
|
|
hid, err := strconv.ParseInt(string(c.Params.ByName("hid")), 10, 32)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid hint identifier"})
|
|
return
|
|
}
|
|
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
hint, err := exercice.GetHint(hid)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Hint not found"})
|
|
return
|
|
}
|
|
|
|
c.Set("hint", hint)
|
|
|
|
c.Next()
|
|
}
|
|
|
|
func FlagKeyHandler(c *gin.Context) {
|
|
kid, err := strconv.ParseInt(string(c.Params.ByName("kid")), 10, 32)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid flag identifier"})
|
|
return
|
|
}
|
|
|
|
var flag *fic.FlagKey
|
|
if exercice, exists := c.Get("exercice"); exists {
|
|
flag, err = exercice.(*fic.Exercice).GetFlagKey(int(kid))
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Flag not found"})
|
|
return
|
|
}
|
|
} else {
|
|
flag, err = fic.GetFlagKey(int(kid))
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Flag not found"})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.Set("flag-key", flag)
|
|
|
|
c.Next()
|
|
}
|
|
|
|
func FlagChoiceHandler(c *gin.Context) {
|
|
cid, err := strconv.ParseInt(string(c.Params.ByName("cid")), 10, 32)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid choice identifier"})
|
|
return
|
|
}
|
|
|
|
flagkey := c.MustGet("flag-key").(*fic.FlagKey)
|
|
choice, err := flagkey.GetChoice(int(cid))
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Choice not found"})
|
|
return
|
|
}
|
|
|
|
c.Set("flag-choice", choice)
|
|
|
|
c.Next()
|
|
}
|
|
|
|
func FlagQuizHandler(c *gin.Context) {
|
|
qid, err := strconv.ParseInt(string(c.Params.ByName("qid")), 10, 64)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Invalid quiz identifier"})
|
|
return
|
|
}
|
|
|
|
var quiz *fic.MCQ
|
|
if exercice, exists := c.Get("exercice"); exists {
|
|
quiz, err = exercice.(*fic.Exercice).GetMCQById(int(qid))
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Quiz not found"})
|
|
return
|
|
}
|
|
} else {
|
|
quiz, err = fic.GetMCQ(int(qid))
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Quiz not found"})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.Set("flag-quiz", quiz)
|
|
|
|
c.Next()
|
|
}
|
|
|
|
func listExercices(c *gin.Context) {
|
|
if theme, exists := c.Get("theme"); exists {
|
|
exercices, err := theme.(*fic.Theme).GetExercices()
|
|
if err != nil {
|
|
log.Println("Unable to listThemedExercices:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercices listing."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, exercices)
|
|
} else {
|
|
exercices, err := fic.GetExercices()
|
|
if err != nil {
|
|
log.Println("Unable to listThemedExercices:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercices listing."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, exercices)
|
|
}
|
|
}
|
|
|
|
func listTags(c *gin.Context) {
|
|
exercices, err := fic.GetExercices()
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
ret := map[string][]*fic.Exercice{}
|
|
for _, exercice := range exercices {
|
|
tags, err := exercice.GetTags()
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
for _, t := range tags {
|
|
if _, ok := ret[t]; !ok {
|
|
ret[t] = []*fic.Exercice{}
|
|
}
|
|
|
|
ret[t] = append(ret[t], exercice)
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, ret)
|
|
}
|
|
|
|
// Generate the csv to export with:
|
|
//
|
|
// curl -s http://127.0.0.1:8081/api/resolutions.json | jq -r ".[] | [ .theme,.level,.title, @uri \"https://fic.srs.epita.fr/$(date +%Y)/\\(.videoURI)\" ] | join(\";\")"
|
|
func exportResolutionMovies(c *gin.Context) {
|
|
exercices, err := fic.GetExercices()
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
export := []map[string]string{}
|
|
for _, exercice := range exercices {
|
|
var tname string
|
|
if exercice.IdTheme != nil {
|
|
theme, err := fic.GetTheme(*exercice.IdTheme)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
tname = theme.Name
|
|
}
|
|
if len(exercice.VideoURI) > 0 {
|
|
level, _ := exercice.GetLevel()
|
|
export = append(export, map[string]string{
|
|
"videoURI": strings.Replace(exercice.VideoURI, "$FILES$/", "files/", 1),
|
|
"theme": tname,
|
|
"title": exercice.Title,
|
|
"level": fmt.Sprintf("%d", level),
|
|
})
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, export)
|
|
}
|
|
|
|
func loadFlags(n func() ([]fic.Flag, error)) (interface{}, error) {
|
|
if flags, err := n(); err != nil {
|
|
return nil, err
|
|
} else {
|
|
var ret []fic.Flag
|
|
|
|
for _, flag := range flags {
|
|
if f, ok := flag.(*fic.FlagKey); ok {
|
|
if k, err := fic.GetFlagKey(f.Id); err != nil {
|
|
return nil, err
|
|
} else {
|
|
ret = append(ret, k)
|
|
}
|
|
} else if f, ok := flag.(*fic.MCQ); ok {
|
|
if m, err := fic.GetMCQ(f.Id); err != nil {
|
|
return nil, err
|
|
} else {
|
|
ret = append(ret, m)
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("Flag type %T not implemented for this flag.", f)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
}
|
|
|
|
func listExerciceHints(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
hints, err := exercice.GetHints()
|
|
if err != nil {
|
|
log.Println("Unable to listExerciceHints:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving hints"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, hints)
|
|
}
|
|
|
|
func listExerciceFlags(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
flags, err := exercice.GetFlagKeys()
|
|
if err != nil {
|
|
log.Println("Unable to listExerciceFlags:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice flags"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, flags)
|
|
}
|
|
|
|
func listFlagChoices(c *gin.Context) {
|
|
flag := c.MustGet("flag-key").(*fic.FlagKey)
|
|
|
|
choices, err := flag.GetChoices()
|
|
if err != nil {
|
|
log.Println("Unable to listFlagChoices:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving flag choices"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, choices)
|
|
}
|
|
|
|
func listExerciceQuiz(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
quiz, err := exercice.GetMCQ()
|
|
if err != nil {
|
|
log.Println("Unable to listExerciceQuiz:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving quiz list"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, quiz)
|
|
}
|
|
|
|
func showExercice(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
var forgelink string
|
|
if fli, ok := sync.GlobalImporter.(sync.ForgeLinkedImporter); ok {
|
|
if u, _ := fli.GetExerciceLink(exercice); u != nil {
|
|
forgelink = u.String()
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, Exercice{exercice, forgelink})
|
|
}
|
|
|
|
func getExerciceHistory(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
history, err := exercice.GetHistory()
|
|
if err != nil {
|
|
log.Println("Unable to getExerciceHistory:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when retrieving exercice history"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, history)
|
|
}
|
|
|
|
type exerciceStats struct {
|
|
IdExercice int64 `json:"id_exercice,omitempty"`
|
|
TeamTries int64 `json:"team_tries"`
|
|
TotalTries int64 `json:"total_tries"`
|
|
SolvedCount int64 `json:"solved_count"`
|
|
FlagSolved []int64 `json:"flag_solved"`
|
|
MCQSolved []int64 `json:"mcq_solved"`
|
|
CurrentGain int64 `json:"current_gain"`
|
|
}
|
|
|
|
func getExerciceStats(c *gin.Context) {
|
|
e := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
current_gain := e.Gain
|
|
if fic.DiscountedFactor > 0 {
|
|
decoted_exercice, err := fic.GetDiscountedExercice(e.Id)
|
|
if err == nil {
|
|
current_gain = decoted_exercice.Gain
|
|
} else {
|
|
log.Println("Unable to fetch decotedExercice:", err.Error())
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, exerciceStats{
|
|
TeamTries: e.TriedTeamCount(),
|
|
TotalTries: e.TriedCount(),
|
|
SolvedCount: e.SolvedCount(),
|
|
FlagSolved: e.FlagSolved(),
|
|
MCQSolved: e.MCQSolved(),
|
|
CurrentGain: current_gain,
|
|
})
|
|
}
|
|
|
|
func getExercicesStats(c *gin.Context) {
|
|
exercices, err := fic.GetDiscountedExercices()
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
ret := []exerciceStats{}
|
|
for _, e := range exercices {
|
|
ret = append(ret, exerciceStats{
|
|
IdExercice: e.Id,
|
|
TeamTries: e.TriedTeamCount(),
|
|
TotalTries: e.TriedCount(),
|
|
SolvedCount: e.SolvedCount(),
|
|
FlagSolved: e.FlagSolved(),
|
|
MCQSolved: e.MCQSolved(),
|
|
CurrentGain: e.Gain,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, ret)
|
|
}
|
|
|
|
func AssigneeCookieHandler(c *gin.Context) {
|
|
myassignee, err := c.Cookie("myassignee")
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "You must be authenticated to perform this action."})
|
|
return
|
|
}
|
|
|
|
aid, err := strconv.ParseInt(myassignee, 10, 32)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "You must be authenticated to perform this action: invalid assignee identifier."})
|
|
return
|
|
}
|
|
|
|
assignee, err := fic.GetAssignee(aid)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "You must be authenticated to perform this action: assignee not found."})
|
|
return
|
|
}
|
|
|
|
c.Set("assignee", assignee)
|
|
|
|
c.Next()
|
|
}
|
|
|
|
type uploadedExerciceHistory struct {
|
|
IdTeam int64 `json:"team_id"`
|
|
Kind string
|
|
Time time.Time
|
|
Secondary *int64
|
|
Coeff float32
|
|
}
|
|
|
|
func appendExerciceHistory(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
myassignee := c.MustGet("assignee").(*fic.ClaimAssignee)
|
|
|
|
var uh uploadedExerciceHistory
|
|
err := c.ShouldBindJSON(&uh)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
err = exercice.AppendHistoryItem(uh.IdTeam, uh.Kind, uh.Secondary)
|
|
if err != nil {
|
|
log.Println("Unable to appendExerciceHistory:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history moditication."})
|
|
return
|
|
}
|
|
log.Printf("AUDIT: %s performs an history append: %s for team %d, exercice %d and optional %v", myassignee.Name, uh.Kind, uh.IdTeam, exercice.Id, uh.Secondary)
|
|
|
|
c.JSON(http.StatusOK, uh)
|
|
}
|
|
|
|
func updateExerciceHistory(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
myassignee := c.MustGet("assignee").(*fic.ClaimAssignee)
|
|
|
|
var uh uploadedExerciceHistory
|
|
err := c.ShouldBindJSON(&uh)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
_, err = exercice.UpdateHistoryItem(uh.Coeff, uh.IdTeam, uh.Kind, uh.Time, uh.Secondary)
|
|
if err != nil {
|
|
log.Println("Unable to updateExerciceHistory:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history update."})
|
|
return
|
|
}
|
|
log.Printf("AUDIT: %s performs an history update: %s for team %d, exercice %d and optional %v, with coeff %f", myassignee.Name, uh.Kind, uh.IdTeam, exercice.Id, uh.Secondary, uh.Coeff)
|
|
|
|
c.JSON(http.StatusOK, uh)
|
|
}
|
|
|
|
func delExerciceHistory(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
myassignee := c.MustGet("assignee").(*fic.ClaimAssignee)
|
|
|
|
var uh uploadedExerciceHistory
|
|
err := c.ShouldBindJSON(&uh)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
_, err = exercice.DelHistoryItem(uh.IdTeam, uh.Kind, uh.Time, uh.Secondary)
|
|
if err != nil {
|
|
log.Println("Unable to delExerciceHistory:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during history deletion."})
|
|
return
|
|
}
|
|
log.Printf("AUDIT: %s performs an history deletion: %s for team %d, exercice %d and optional %v", myassignee.Name, uh.Kind, uh.IdTeam, exercice.Id, uh.Secondary)
|
|
|
|
c.JSON(http.StatusOK, true)
|
|
}
|
|
|
|
func deleteExercice(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
_, err := exercice.DeleteCascade()
|
|
if err != nil {
|
|
log.Println("Unable to deleteExercice:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during exercice deletion"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, true)
|
|
}
|
|
|
|
func updateExercice(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
var ue fic.Exercice
|
|
err := c.ShouldBindJSON(&ue)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
ue.Id = exercice.Id
|
|
|
|
if len(ue.Title) == 0 {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Exercice's title not filled"})
|
|
return
|
|
}
|
|
|
|
if _, err := ue.Update(); err != nil {
|
|
log.Println("Unable to updateExercice:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "An error occurs during exercice update"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, ue)
|
|
}
|
|
|
|
type patchExercice struct {
|
|
Language *string `json:"lang,omitempty"`
|
|
Title *string `json:"title"`
|
|
Disabled *bool `json:"disabled"`
|
|
WIP *bool `json:"wip"`
|
|
URLId *string `json:"urlid"`
|
|
Statement *string `json:"statement"`
|
|
Overview *string `json:"overview"`
|
|
Headline *string `json:"headline"`
|
|
Finished *string `json:"finished"`
|
|
Issue *string `json:"issue"`
|
|
IssueKind *string `json:"issuekind"`
|
|
Gain *int64 `json:"gain"`
|
|
Coefficient *float64 `json:"coefficient"`
|
|
}
|
|
|
|
func partUpdateExercice(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
var ue patchExercice
|
|
err := c.ShouldBindJSON(&ue)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
for _, field := range reflect.VisibleFields(reflect.TypeOf(ue)) {
|
|
if !reflect.ValueOf(ue).FieldByName(field.Name).IsNil() {
|
|
reflect.ValueOf(exercice).Elem().FieldByName(field.Name).Set(reflect.ValueOf(reflect.ValueOf(ue).FieldByName(field.Name).Elem().Interface()))
|
|
}
|
|
}
|
|
|
|
if _, err := exercice.Update(); err != nil {
|
|
log.Println("Unable to partUpdateExercice:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice update."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, exercice)
|
|
}
|
|
|
|
func createExercice(c *gin.Context) {
|
|
theme := c.MustGet("theme").(*fic.Theme)
|
|
|
|
// Create a new exercice
|
|
var ue fic.Exercice
|
|
err := c.ShouldBindJSON(&ue)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
if len(ue.Title) == 0 {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Title not filled"})
|
|
return
|
|
}
|
|
|
|
var depend *fic.Exercice = nil
|
|
if ue.Depend != nil {
|
|
if d, err := fic.GetExercice(*ue.Depend); err != nil {
|
|
log.Println("Unable to createExercice:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice creation."})
|
|
return
|
|
} else {
|
|
depend = d
|
|
}
|
|
}
|
|
|
|
exercice, err := theme.AddExercice(ue.Title, ue.Authors, ue.Image, ue.BackgroundColor, ue.WIP, ue.URLId, ue.Path, ue.Statement, ue.Overview, ue.Headline, depend, ue.Gain, ue.VideoURI, ue.Resolution, ue.SeeAlso, ue.Finished)
|
|
if err != nil {
|
|
log.Println("Unable to createExercice:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during exercice creation."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, exercice)
|
|
}
|
|
|
|
type uploadedHint struct {
|
|
Title string
|
|
Path string
|
|
Content string
|
|
Cost int64
|
|
URI string
|
|
}
|
|
|
|
func createExerciceHint(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
var uh uploadedHint
|
|
err := c.ShouldBindJSON(&uh)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
if len(uh.Content) != 0 {
|
|
hint, err := exercice.AddHint(uh.Title, uh.Content, uh.Cost)
|
|
if err != nil {
|
|
log.Println("Unable to AddHint in createExerciceHint:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to add hint."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, hint)
|
|
} else if len(uh.URI) != 0 {
|
|
hint, err := sync.ImportFile(sync.GlobalImporter, uh.URI,
|
|
func(filePath string, origin string) (interface{}, error) {
|
|
return exercice.AddHint(uh.Title, "$FILES"+strings.TrimPrefix(filePath, fic.FilesDir), uh.Cost)
|
|
})
|
|
|
|
if err != nil {
|
|
log.Println("Unable to AddHint (after ImportFile) in createExerciceHint:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to add hint."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, hint)
|
|
} else {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Hint's content not filled"})
|
|
return
|
|
}
|
|
}
|
|
|
|
func showExerciceHint(c *gin.Context) {
|
|
c.JSON(http.StatusOK, c.MustGet("hint").(*fic.EHint))
|
|
}
|
|
|
|
func showExerciceHintDeps(c *gin.Context) {
|
|
hint := c.MustGet("hint").(*fic.EHint)
|
|
|
|
deps, err := loadFlags(hint.GetDepends)
|
|
if err != nil {
|
|
log.Println("Unable to loaddeps:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve hint dependencies."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, deps)
|
|
}
|
|
|
|
func updateExerciceHint(c *gin.Context) {
|
|
hint := c.MustGet("hint").(*fic.EHint)
|
|
|
|
var uh fic.EHint
|
|
err := c.ShouldBindJSON(&uh)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
uh.Id = hint.Id
|
|
|
|
if len(uh.Title) == 0 {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Hint's title not filled"})
|
|
return
|
|
}
|
|
|
|
if _, err := uh.Update(); err != nil {
|
|
log.Println("Unable to updateExerciceHint:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update hint."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, uh)
|
|
}
|
|
|
|
func deleteExerciceHint(c *gin.Context) {
|
|
hint := c.MustGet("hint").(*fic.EHint)
|
|
|
|
_, err := hint.Delete()
|
|
if err != nil {
|
|
log.Println("Unable to deleteExerciceHint:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete hint."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, true)
|
|
}
|
|
|
|
type uploadedFlag struct {
|
|
Type string
|
|
Label string
|
|
Placeholder string
|
|
Help string
|
|
IgnoreCase bool
|
|
Multiline bool
|
|
NoTrim bool
|
|
CaptureRe *string `json:"capture_regexp"`
|
|
SortReGroups bool `json:"sort_re_grps"`
|
|
Flag string
|
|
Value []byte
|
|
ChoicesCost int32 `json:"choices_cost"`
|
|
BonusGain int32 `json:"bonus_gain"`
|
|
}
|
|
|
|
func createExerciceFlag(c *gin.Context) {
|
|
var uk uploadedFlag
|
|
err := c.ShouldBindJSON(&uk)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
if len(uk.Flag) == 0 {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Flag not filled"})
|
|
return
|
|
}
|
|
|
|
var vre *string = nil
|
|
if uk.CaptureRe != nil && len(*uk.CaptureRe) > 0 {
|
|
vre = uk.CaptureRe
|
|
}
|
|
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
flag, err := exercice.AddRawFlagKey(uk.Label, uk.Type, uk.Placeholder, uk.IgnoreCase, uk.NoTrim, uk.Multiline, vre, uk.SortReGroups, []byte(uk.Flag), uk.ChoicesCost, uk.BonusGain)
|
|
if err != nil {
|
|
log.Println("Unable to createExerciceFlag:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to create flag."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, flag)
|
|
}
|
|
|
|
func showExerciceFlag(c *gin.Context) {
|
|
c.JSON(http.StatusOK, c.MustGet("flag-key").(*fic.FlagKey))
|
|
}
|
|
|
|
func showExerciceFlagDeps(c *gin.Context) {
|
|
flag := c.MustGet("flag-key").(*fic.FlagKey)
|
|
|
|
deps, err := loadFlags(flag.GetDepends)
|
|
if err != nil {
|
|
log.Println("Unable to loaddeps:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve hint dependencies."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, deps)
|
|
}
|
|
|
|
func tryExerciceFlag(c *gin.Context) {
|
|
flag := c.MustGet("flag-key").(*fic.FlagKey)
|
|
|
|
var uk uploadedFlag
|
|
err := c.ShouldBindJSON(&uk)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
if len(uk.Flag) == 0 {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Empty submission"})
|
|
return
|
|
}
|
|
|
|
if flag.Check([]byte(uk.Flag)) == 0 {
|
|
c.AbortWithStatusJSON(http.StatusOK, true)
|
|
return
|
|
}
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad submission"})
|
|
}
|
|
|
|
func updateExerciceFlag(c *gin.Context) {
|
|
flag := c.MustGet("flag-key").(*fic.FlagKey)
|
|
|
|
var uk uploadedFlag
|
|
err := c.ShouldBindJSON(&uk)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
if len(uk.Label) == 0 {
|
|
flag.Label = "Flag"
|
|
} else {
|
|
flag.Label = uk.Label
|
|
}
|
|
|
|
flag.Placeholder = uk.Placeholder
|
|
flag.Help = uk.Help
|
|
flag.IgnoreCase = uk.IgnoreCase
|
|
flag.Multiline = uk.Multiline
|
|
if len(uk.Flag) > 0 {
|
|
var err error
|
|
flag.Checksum, err = flag.ComputeChecksum([]byte(uk.Flag))
|
|
if err != nil {
|
|
log.Println("Unable to ComputeChecksum:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to compute flag checksum"})
|
|
return
|
|
}
|
|
} else {
|
|
flag.Checksum = uk.Value
|
|
}
|
|
flag.ChoicesCost = uk.ChoicesCost
|
|
flag.BonusGain = uk.BonusGain
|
|
|
|
if uk.CaptureRe != nil && len(*uk.CaptureRe) > 0 {
|
|
flag.CaptureRegexp = uk.CaptureRe
|
|
} else {
|
|
flag.CaptureRegexp = nil
|
|
}
|
|
|
|
if _, err := flag.Update(); err != nil {
|
|
log.Println("Unable to updateExerciceFlag:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update flag."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, flag)
|
|
}
|
|
|
|
func deleteExerciceFlag(c *gin.Context) {
|
|
flag := c.MustGet("flag-key").(*fic.FlagKey)
|
|
|
|
_, err := flag.Delete()
|
|
if err != nil {
|
|
log.Println("Unable to deleteExerciceFlag:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete flag."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, true)
|
|
}
|
|
|
|
func createFlagChoice(c *gin.Context) {
|
|
flag := c.MustGet("flag-key").(*fic.FlagKey)
|
|
|
|
var uc fic.FlagChoice
|
|
err := c.ShouldBindJSON(&uc)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
if len(uc.Label) == 0 {
|
|
uc.Label = uc.Value
|
|
}
|
|
|
|
choice, err := flag.AddChoice(&uc)
|
|
if err != nil {
|
|
log.Println("Unable to createFlagChoice:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to create flag choice."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, choice)
|
|
}
|
|
|
|
func showFlagChoice(c *gin.Context) {
|
|
c.JSON(http.StatusOK, c.MustGet("flag-choice").(*fic.FlagChoice))
|
|
}
|
|
|
|
func updateFlagChoice(c *gin.Context) {
|
|
choice := c.MustGet("flag-choice").(*fic.FlagChoice)
|
|
|
|
var uc fic.FlagChoice
|
|
err := c.ShouldBindJSON(&uc)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
if len(uc.Label) == 0 {
|
|
choice.Label = uc.Value
|
|
} else {
|
|
choice.Label = uc.Label
|
|
}
|
|
|
|
choice.Value = uc.Value
|
|
|
|
if _, err := choice.Update(); err != nil {
|
|
log.Println("Unable to updateFlagChoice:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to update flag choice."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, choice)
|
|
}
|
|
|
|
func deleteFlagChoice(c *gin.Context) {
|
|
choice := c.MustGet("flag-choice").(*fic.FlagChoice)
|
|
|
|
_, err := choice.Delete()
|
|
if err != nil {
|
|
log.Println("Unable to deleteExerciceChoice:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete choice."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, true)
|
|
}
|
|
|
|
func showExerciceQuiz(c *gin.Context) {
|
|
c.JSON(http.StatusOK, c.MustGet("flag-quiz").(*fic.MCQ))
|
|
}
|
|
|
|
func showExerciceQuizDeps(c *gin.Context) {
|
|
quiz := c.MustGet("flag-quiz").(*fic.MCQ)
|
|
|
|
deps, err := loadFlags(quiz.GetDepends)
|
|
if err != nil {
|
|
log.Println("Unable to loaddeps:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to retrieve quiz dependencies."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, deps)
|
|
}
|
|
|
|
func updateExerciceQuiz(c *gin.Context) {
|
|
quiz := c.MustGet("flag-quiz").(*fic.MCQ)
|
|
|
|
var uq fic.MCQ
|
|
err := c.ShouldBindJSON(&uq)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
quiz.Title = uq.Title
|
|
|
|
if _, err := quiz.Update(); err != nil {
|
|
log.Println("Unable to updateExerciceQuiz:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to update quiz."})
|
|
return
|
|
}
|
|
|
|
// Update and remove old entries
|
|
var delete []int
|
|
for i, cur := range quiz.Entries {
|
|
seen := false
|
|
for _, next := range uq.Entries {
|
|
if cur.Id == next.Id {
|
|
seen = true
|
|
|
|
if cur.Label != next.Label || cur.Response != next.Response {
|
|
cur.Label = next.Label
|
|
cur.Response = next.Response
|
|
if _, err := cur.Update(); err != nil {
|
|
log.Println("Unable to update MCQ entry:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to update some MCQ entry"})
|
|
return
|
|
}
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if seen == false {
|
|
if _, err := cur.Delete(); err != nil {
|
|
log.Println("Unable to delete MCQ entry:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to delete some MCQ entry"})
|
|
return
|
|
} else {
|
|
delete = append(delete, i)
|
|
}
|
|
}
|
|
}
|
|
for n, i := range delete {
|
|
quiz.Entries = append(quiz.Entries[:i-n-1], quiz.Entries[:i-n+1]...)
|
|
}
|
|
|
|
// Add new choices
|
|
for _, choice := range uq.Entries {
|
|
if choice.Id == 0 {
|
|
if ch, err := quiz.AddEntry(choice); err != nil {
|
|
log.Println("Unable to add MCQ entry:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to add some MCQ entry"})
|
|
return
|
|
} else {
|
|
quiz.Entries = append(quiz.Entries, ch)
|
|
}
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, quiz)
|
|
}
|
|
|
|
func deleteExerciceQuiz(c *gin.Context) {
|
|
quiz := c.MustGet("flag-quiz").(*fic.MCQ)
|
|
|
|
for _, choice := range quiz.Entries {
|
|
if _, err := choice.Delete(); err != nil {
|
|
log.Println("Unable to deleteExerciceQuiz (entry):", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete quiz entry."})
|
|
return
|
|
}
|
|
}
|
|
|
|
_, err := quiz.Delete()
|
|
if err != nil {
|
|
log.Println("Unable to deleteExerciceQuiz:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to delete quiz."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, true)
|
|
}
|
|
|
|
func listExerciceTags(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
tags, err := exercice.GetTags()
|
|
if err != nil {
|
|
log.Println("Unable to listExerciceTags:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to get tags."})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, tags)
|
|
}
|
|
|
|
func addExerciceTag(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
var ut []string
|
|
err := c.ShouldBindJSON(&ut)
|
|
if err != nil {
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
|
|
return
|
|
}
|
|
|
|
// TODO: a DB transaction should be done here: on error we should rollback
|
|
for _, t := range ut {
|
|
if _, err := exercice.AddTag(t); err != nil {
|
|
log.Println("Unable to addExerciceTag:", err.Error())
|
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when trying to add some tag."})
|
|
return
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, ut)
|
|
}
|
|
|
|
func updateExerciceTags(c *gin.Context) {
|
|
exercice := c.MustGet("exercice").(*fic.Exercice)
|
|
|
|
exercice.WipeTags()
|
|
addExerciceTag(c)
|
|
}
|