server/admin/api/exercice.go

640 lines
17 KiB
Go

package api
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
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.json", apiHandler(exerciceHandler(getExerciceStats)))
router.GET("/api/exercices_stats.json", apiHandler(getExercicesStats))
router.GET("/api/exercices/:eid/history.json", apiHandler(exerciceHandler(getExerciceHistory)))
router.PATCH("/api/exercices/:eid/history.json", apiHandler(exerciceHandler(updateExerciceHistory)))
router.DELETE("/api/exercices/:eid/history.json", apiHandler(exerciceHandler(delExerciceHistory)))
router.GET("/api/exercices/:eid/hints", apiHandler(exerciceHandler(listExerciceHints)))
router.POST("/api/exercices/:eid/hints", apiHandler(exerciceHandler(createExerciceHint)))
router.GET("/api/exercices/:eid/hints/:hid", apiHandler(hintHandler(showExerciceHint)))
router.PUT("/api/exercices/:eid/hints/:hid", apiHandler(hintHandler(updateExerciceHint)))
router.DELETE("/api/exercices/:eid/hints/:hid", apiHandler(hintHandler(deleteExerciceHint)))
router.GET("/api/exercices/:eid/hints/:hid/dependancies", apiHandler(hintHandler(showExerciceHintDeps)))
router.GET("/api/exercices/:eid/flags", apiHandler(exerciceHandler(listExerciceFlags)))
router.POST("/api/exercices/:eid/flags", apiHandler(exerciceHandler(createExerciceFlag)))
router.GET("/api/exercices/:eid/flags/:kid", apiHandler(flagKeyHandler(showExerciceFlag)))
router.PUT("/api/exercices/:eid/flags/:kid", apiHandler(flagKeyHandler(updateExerciceFlag)))
router.POST("/api/exercices/:eid/flags/:kid/try", apiHandler(flagKeyHandler(tryExerciceFlag)))
router.DELETE("/api/exercices/:eid/flags/:kid", apiHandler(flagKeyHandler(deleteExerciceFlag)))
router.GET("/api/exercices/:eid/flags/:kid/dependancies", apiHandler(flagKeyHandler(showExerciceFlagDeps)))
router.GET("/api/exercices/:eid/flags/:kid/choices/", apiHandler(flagKeyHandler(listFlagChoices)))
router.GET("/api/exercices/:eid/flags/:kid/choices/:cid", apiHandler(choiceHandler(showFlagChoice)))
router.POST("/api/exercices/:eid/flags/:kid/choices/", apiHandler(flagKeyHandler(createFlagChoice)))
router.PUT("/api/exercices/:eid/flags/:kid/choices/:cid", apiHandler(choiceHandler(updateFlagChoice)))
router.DELETE("/api/exercices/:eid/flags/:kid/choices/:cid", apiHandler(choiceHandler(deleteFlagChoice)))
router.GET("/api/exercices/:eid/quiz", apiHandler(exerciceHandler(listExerciceQuiz)))
router.GET("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(showExerciceQuiz)))
router.PUT("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(updateExerciceQuiz)))
router.DELETE("/api/exercices/:eid/quiz/:qid", apiHandler(quizHandler(deleteExerciceQuiz)))
router.GET("/api/exercices/:eid/quiz/:qid/dependancies", apiHandler(quizHandler(showExerciceQuizDeps)))
router.GET("/api/exercices/:eid/tags", apiHandler(exerciceHandler(listExerciceTags)))
router.POST("/api/exercices/:eid/tags", apiHandler(exerciceHandler(addExerciceTag)))
router.PUT("/api/exercices/:eid/tags", apiHandler(exerciceHandler(updateExerciceTags)))
// Remote
router.GET("/api/remote/themes/:thid/exercices/:exid", apiHandler(sync.ApiGetRemoteExercice))
router.GET("/api/remote/themes/:thid/exercices/:exid/hints", apiHandler(sync.ApiGetRemoteExerciceHints))
router.GET("/api/remote/themes/:thid/exercices/:exid/flags", apiHandler(sync.ApiGetRemoteExerciceFlags))
// Synchronize
router.POST("/api/sync/themes/:thid/exercices/:eid", apiHandler(themedExerciceHandler(
func(theme fic.Theme, exercice fic.Exercice, _ []byte) (interface{}, error) {
_, _, errs := sync.SyncExercice(sync.GlobalImporter, theme, exercice.Path, nil)
return errs, nil
})))
router.POST("/api/sync/exercices/:eid/hints", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) {
_, errs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice))
return errs, nil
})))
router.POST("/api/sync/exercices/:eid/flags", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) {
_, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice)
_, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice))
return append(errs, herrs...), nil
})))
router.POST("/api/sync/exercices/:eid/fixurlid", apiHandler(exerciceHandler(
func(exercice fic.Exercice, _ []byte) (interface{}, error) {
if exercice.FixURLId() {
return exercice.Update()
}
return 0, nil
})))
}
func listExercices(_ httprouter.Params, body []byte) (interface{}, error) {
// List all exercices
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 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(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice.GetHints()
}
func listExerciceFlags(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice.GetFlagKeys()
}
func listFlagChoices(flag fic.FlagKey, _ fic.Exercice, body []byte) (interface{}, error) {
return flag.GetChoices()
}
func listExerciceQuiz(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice.GetMCQ()
}
func showExercice(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice, nil
}
func getExerciceHistory(exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice.GetHistory()
}
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"`
}
func getExerciceStats(e fic.Exercice, body []byte) (interface{}, error) {
return exerciceStats{
TeamTries: e.TriedTeamCount(),
TotalTries: e.TriedCount(),
SolvedCount: e.SolvedCount(),
FlagSolved: e.FlagSolved(),
MCQSolved: e.MCQSolved(),
}, nil
}
func getExercicesStats(_ httprouter.Params, body []byte) (interface{}, error) {
if exercices, err := fic.GetExercices(); err != nil {
return nil, err
} else {
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(),
})
}
return ret, nil
}
}
type uploadedExerciceHistory struct {
IdTeam int64 `json:"team_id"`
Kind string
Time time.Time
Secondary *int64
Coeff float32
}
func updateExerciceHistory(exercice fic.Exercice, body []byte) (interface{}, error) {
var uh uploadedExerciceHistory
if err := json.Unmarshal(body, &uh); err != nil {
return nil, err
}
return exercice.UpdateHistoryItem(uh.Coeff, uh.IdTeam, uh.Kind, uh.Time, uh.Secondary)
}
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.DeleteCascade()
}
func updateExercice(exercice fic.Exercice, body []byte) (interface{}, error) {
var ue fic.Exercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
ue.Id = exercice.Id
if len(ue.Title) == 0 {
return nil, errors.New("Exercice's title not filled")
}
if _, err := ue.Update(); err != nil {
return nil, err
}
return ue, nil
}
func partUpdateExercice(exercice fic.Exercice, body []byte) (interface{}, error) {
var ue fic.Exercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
if len(ue.Title) > 0 {
exercice.Title = ue.Title
}
if len(ue.URLId) > 0 {
exercice.URLId = ue.URLId
}
if len(ue.Statement) > 0 {
exercice.Statement = ue.Statement
}
if len(ue.Headline) > 0 {
exercice.Headline = ue.Headline
}
if len(ue.Finished) > 0 {
exercice.Finished = ue.Finished
}
if len(ue.Overview) > 0 {
exercice.Overview = ue.Overview
}
if len(ue.Issue) > 0 {
exercice.Issue = ue.Issue
}
if len(ue.IssueKind) > 0 {
exercice.IssueKind = ue.IssueKind
}
if ue.Depend != nil {
exercice.Depend = ue.Depend
}
if ue.Gain != 0 {
exercice.Gain = ue.Gain
}
if ue.Coefficient != 0 {
exercice.Coefficient = ue.Coefficient
}
if len(ue.VideoURI) > 0 {
exercice.VideoURI = ue.VideoURI
}
if _, err := exercice.Update(); err != nil {
return nil, err
}
return exercice, nil
}
func createExercice(theme fic.Theme, body []byte) (interface{}, error) {
// Create a new exercice
var ue fic.Exercice
if err := json.Unmarshal(body, &ue); err != nil {
return nil, err
}
if len(ue.Title) == 0 {
return nil, errors.New("Title not filled")
}
var depend *fic.Exercice = nil
if ue.Depend != nil {
if d, err := fic.GetExercice(*ue.Depend); err != nil {
return nil, err
} else {
depend = &d
}
}
return theme.AddExercice(ue.Title, ue.URLId, ue.Path, ue.Statement, ue.Overview, ue.Headline, depend, ue.Gain, ue.VideoURI, ue.Finished)
}
type uploadedHint struct {
Title string
Path string
Content string
Cost int64
URI string
}
func createExerciceHint(exercice fic.Exercice, body []byte) (interface{}, error) {
var uh uploadedHint
if err := json.Unmarshal(body, &uh); err != nil {
return nil, err
}
if len(uh.Content) != 0 {
return exercice.AddHint(uh.Title, uh.Content, uh.Cost)
} else if len(uh.URI) != 0 {
return 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)
})
} else {
return nil, errors.New("Hint's content not filled")
}
}
func showExerciceHint(hint fic.EHint, body []byte) (interface{}, error) {
return hint, nil
}
func showExerciceHintDeps(hint fic.EHint, body []byte) (interface{}, error) {
return loadFlags(hint.GetDepends)
}
func updateExerciceHint(hint fic.EHint, body []byte) (interface{}, error) {
var uh fic.EHint
if err := json.Unmarshal(body, &uh); err != nil {
return nil, err
}
uh.Id = hint.Id
if len(uh.Title) == 0 {
return nil, errors.New("Hint's title not filled")
}
if _, err := uh.Update(); err != nil {
return nil, err
}
return uh, nil
}
func deleteExerciceHint(hint fic.EHint, _ []byte) (interface{}, error) {
return hint.Delete()
}
type uploadedFlag struct {
Label string
Placeholder string
IgnoreCase bool
Multiline bool
ValidatorRe *string `json:"validator_regexp"`
Flag string
Value []byte
ChoicesCost int64 `json:"choices_cost"`
}
func createExerciceFlag(exercice fic.Exercice, body []byte) (interface{}, error) {
var uk uploadedFlag
if err := json.Unmarshal(body, &uk); err != nil {
return nil, err
}
if len(uk.Flag) == 0 {
return nil, errors.New("Flag not filled")
}
var vre *string = nil
if uk.ValidatorRe != nil && len(*uk.ValidatorRe) > 0 {
vre = uk.ValidatorRe
}
return exercice.AddRawFlagKey(uk.Label, uk.Placeholder, uk.IgnoreCase, uk.Multiline, vre, []byte(uk.Flag), uk.ChoicesCost)
}
func showExerciceFlag(flag fic.FlagKey, _ fic.Exercice, body []byte) (interface{}, error) {
return flag, nil
}
func showExerciceFlagDeps(flag fic.FlagKey, _ fic.Exercice, body []byte) (interface{}, error) {
return loadFlags(flag.GetDepends)
}
func tryExerciceFlag(flag fic.FlagKey, _ fic.Exercice, body []byte) (interface{}, error) {
var uk uploadedFlag
if err := json.Unmarshal(body, &uk); err != nil {
return nil, err
}
if len(uk.Flag) == 0 {
return nil, errors.New("Empty submission")
}
if flag.Check([]byte(uk.Flag)) == 0 {
return true, nil
} else {
return nil, errors.New("Bad submission")
}
}
func updateExerciceFlag(flag fic.FlagKey, exercice fic.Exercice, body []byte) (interface{}, error) {
var uk uploadedFlag
if err := json.Unmarshal(body, &uk); err != nil {
return nil, err
}
if len(uk.Label) == 0 {
flag.Label = "Flag"
} else {
flag.Label = uk.Label
}
flag.Placeholder = uk.Placeholder
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 {
return nil, err
}
} else {
flag.Checksum = uk.Value
}
flag.ChoicesCost = uk.ChoicesCost
if uk.ValidatorRe != nil && len(*uk.ValidatorRe) > 0 {
flag.ValidatorRegexp = uk.ValidatorRe
} else {
flag.ValidatorRegexp = nil
}
if _, err := flag.Update(); err != nil {
return nil, err
}
return flag, nil
}
func deleteExerciceFlag(flag fic.FlagKey, _ fic.Exercice, _ []byte) (interface{}, error) {
return flag.Delete()
}
func createFlagChoice(flag fic.FlagKey, exercice fic.Exercice, body []byte) (interface{}, error) {
var uc fic.FlagChoice
if err := json.Unmarshal(body, &uc); err != nil {
return nil, err
}
if len(uc.Label) == 0 {
uc.Label = uc.Value
}
return flag.AddChoice(uc)
}
func showFlagChoice(choice fic.FlagChoice, _ fic.Exercice, body []byte) (interface{}, error) {
return choice, nil
}
func updateFlagChoice(choice fic.FlagChoice, _ fic.Exercice, body []byte) (interface{}, error) {
var uc fic.FlagChoice
if err := json.Unmarshal(body, &uc); err != nil {
return nil, err
}
if len(uc.Label) == 0 {
choice.Label = uc.Value
} else {
choice.Label = uc.Label
}
choice.Value = uc.Value
if _, err := choice.Update(); err != nil {
return nil, err
}
return choice, nil
}
func deleteFlagChoice(choice fic.FlagChoice, _ fic.Exercice, _ []byte) (interface{}, error) {
return choice.Delete()
}
func showExerciceQuiz(quiz fic.MCQ, _ fic.Exercice, body []byte) (interface{}, error) {
return quiz, nil
}
func showExerciceQuizDeps(quiz fic.MCQ, _ fic.Exercice, body []byte) (interface{}, error) {
return loadFlags(quiz.GetDepends)
}
func updateExerciceQuiz(quiz fic.MCQ, exercice fic.Exercice, body []byte) (interface{}, error) {
var uq fic.MCQ
if err := json.Unmarshal(body, &uq); err != nil {
return nil, err
}
quiz.Title = uq.Title
if _, err := quiz.Update(); err != nil {
return nil, err
}
// 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 {
return nil, err
}
}
break
}
}
if seen == false {
if _, err := cur.Delete(); err != nil {
return nil, err
} 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 c, err := quiz.AddEntry(choice); err != nil {
return nil, err
} else {
quiz.Entries = append(quiz.Entries, c)
}
}
}
return quiz, nil
}
func deleteExerciceQuiz(quiz fic.MCQ, _ fic.Exercice, _ []byte) (interface{}, error) {
for _, choice := range quiz.Entries {
if _, err := choice.Delete(); err != nil {
return nil, err
}
}
return quiz.Delete()
}
func listExerciceTags(exercice fic.Exercice, _ []byte) (interface{}, error) {
return exercice.GetTags()
}
func addExerciceTag(exercice fic.Exercice, body []byte) (interface{}, error) {
var ut []string
if err := json.Unmarshal(body, &ut); err != nil {
return nil, err
}
// TODO: a DB transaction should be done here: on error we should rollback
for _, t := range ut {
if _, err := exercice.AddTag(t); err != nil {
return nil, err
}
}
return ut, nil
}
func updateExerciceTags(exercice fic.Exercice, body []byte) (interface{}, error) {
exercice.WipeTags()
return addExerciceTag(exercice, body)
}