qa: New service to handle QA testing by students

This commit is contained in:
nemunaire 2020-09-08 12:50:41 +02:00
commit a237936feb
37 changed files with 1476 additions and 0 deletions

36
qa/api/exercice.go Normal file
View file

@ -0,0 +1,36 @@
package api
import (
"strconv"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/exercices/", apiHandler(listExercices))
router.GET("/api/exercices/:eid", apiHandler(exerciceHandler(showExercice)))
}
func exerciceHandler(f func(QAUser, fic.Exercice, []byte) (interface{}, error)) func(QAUser, httprouter.Params, []byte) (interface{}, error) {
return func(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) {
if eid, err := strconv.ParseInt(string(ps.ByName("eid")), 10, 64); err != nil {
return nil, err
} else if exercice, err := fic.GetExercice(eid); err != nil {
return nil, err
} else {
return f(u, exercice, body)
}
}
}
func listExercices(_ QAUser, _ httprouter.Params, body []byte) (interface{}, error) {
// List all exercices
return fic.GetExercices()
}
func showExercice(_ QAUser, exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice, nil
}

111
qa/api/handler.go Normal file
View file

@ -0,0 +1,111 @@
package api
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"strconv"
"time"
"github.com/julienschmidt/httprouter"
)
var Simulator string
var TeamsDir string
type QAUser struct {
User string `json:"name"`
TeamId int64 `json:"id_team"`
}
type DispatchFunction func(QAUser, httprouter.Params, []byte) (interface{}, error)
func apiHandler(f DispatchFunction) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
ficteam := Simulator
if t := r.Header.Get("X-FIC-Team"); t != "" {
ficteam = t
}
var teamid int64
var err error
if ficteam == "" {
log.Printf("%s 401 \"%s %s\" [%s]\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent())
w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf("{errmsg:\"Need to authenticate.\"}"), http.StatusUnauthorized)
return
} else if teamid, err = strconv.ParseInt(ficteam, 10, 64); err != nil {
if lnk, err := os.Readlink(path.Join(TeamsDir, ficteam)); err != nil {
log.Printf("[ERR] Unable to readlink %q: %s\n", path.Join(TeamsDir, ficteam), err)
return
} else if teamid, err = strconv.ParseInt(lnk, 10, 64); err != nil {
log.Printf("[ERR] Error during ParseInt team %q: %s\n", lnk, err)
return
}
}
log.Printf("%s \"%s %s\" [%s]\n", r.RemoteAddr, r.Method, r.URL.Path, r.UserAgent())
// Read the body
if r.ContentLength < 0 || r.ContentLength > 6553600 {
http.Error(w, fmt.Sprintf("{errmsg:\"Request too large or request size unknown\"}"), http.StatusRequestEntityTooLarge)
return
}
var body []byte
if r.ContentLength > 0 {
tmp := make([]byte, 1024)
for {
n, err := r.Body.Read(tmp)
for j := 0; j < n; j++ {
body = append(body, tmp[j])
}
if err != nil || n <= 0 {
break
}
}
}
var ret interface{}
ret, err = f(QAUser{ficteam, teamid}, ps, body)
// Format response
resStatus := http.StatusOK
if err != nil {
ret = map[string]string{"errmsg": err.Error()}
resStatus = http.StatusBadRequest
log.Println(r.RemoteAddr, resStatus, err.Error())
}
if ret == nil {
ret = map[string]string{"errmsg": "Page not found"}
resStatus = http.StatusNotFound
}
w.Header().Set("X-FIC-Time", fmt.Sprintf("%f", float64(time.Now().UnixNano()/1000)/1000000))
if str, found := ret.(string); found {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resStatus)
io.WriteString(w, str)
} else if bts, found := ret.([]byte); found {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment")
w.Header().Set("Content-Transfer-Encoding", "binary")
w.WriteHeader(resStatus)
w.Write(bts)
} else if j, err := json.Marshal(ret); err != nil {
w.Header().Set("Content-Type", "application/json")
http.Error(w, fmt.Sprintf("{\"errmsg\":%q}", err), http.StatusInternalServerError)
} else {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resStatus)
w.Write(j)
}
}
}

140
qa/api/qa.go Normal file
View file

@ -0,0 +1,140 @@
package api
import (
"encoding/json"
"errors"
"strconv"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/qa/:eid", apiHandler(exerciceHandler(getExerciceQA)))
router.POST("/api/qa/:eid", apiHandler(exerciceHandler(createExerciceQA)))
router.PUT("/api/qa/:eid/:qid", apiHandler(qaHandler(updateExerciceQA)))
router.DELETE("/api/qa/:eid/:qid", apiHandler(qaHandler(deleteExerciceQA)))
router.GET("/api/qa/:eid/:qid/comments", apiHandler(qaHandler(getQAComments)))
router.POST("/api/qa/:eid/:qid/comments", apiHandler(qaHandler(createQAComment)))
router.DELETE("/api/qa/:eid/:qid/comments/:cid", apiHandler(qaCommentHandler(deleteQAComment)))
}
func qaHandler(f func(QAUser, fic.QAQuery, fic.Exercice, []byte) (interface{}, error)) func(QAUser, httprouter.Params, []byte) (interface{}, error) {
return func(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) {
return exerciceHandler(func(u QAUser, exercice fic.Exercice, _ []byte) (interface{}, error) {
if qid, err := strconv.ParseInt(string(ps.ByName("qid")), 10, 64); err != nil {
return nil, err
} else if query, err := exercice.GetQAQuery(qid); err != nil {
return nil, err
} else {
return f(u, query, exercice, body)
}
})(u, ps, body)
}
}
func qaCommentHandler(f func(QAUser, fic.QAComment, fic.QAQuery, fic.Exercice, []byte) (interface{}, error)) func(QAUser, httprouter.Params, []byte) (interface{}, error) {
return func(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) {
return qaHandler(func(u QAUser, query fic.QAQuery, exercice fic.Exercice, _ []byte) (interface{}, error) {
if cid, err := strconv.ParseInt(string(ps.ByName("cid")), 10, 64); err != nil {
return nil, err
} else if comment, err := query.GetComment(cid); err != nil {
return nil, err
} else {
return f(u, comment, query, exercice, body)
}
})(u, ps, body)
}
}
func getExerciceQA(_ QAUser, exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice.GetQAQueries()
}
func createExerciceQA(u QAUser, exercice fic.Exercice, body []byte) (interface{}, error) {
// Create a new query
var uq fic.QAQuery
if err := json.Unmarshal(body, &uq); err != nil {
return nil, err
}
if len(uq.State) == 0 {
return nil, errors.New("State not filled")
}
if len(uq.Subject) == 0 {
return nil, errors.New("Subject not filled")
}
if qa, err := exercice.NewQAQuery(uq.Subject, u.TeamId, u.User, uq.State); err != nil {
return nil, err
} else {
var uc fic.QAComment
if err := json.Unmarshal(body, &uc); err != nil {
return nil, err
}
if uc.Content != "" {
_, err = qa.AddComment(uc.Content, u.TeamId, u.User)
}
return qa, err
}
}
func updateExerciceQA(u QAUser, query fic.QAQuery, exercice fic.Exercice, body []byte) (interface{}, error) {
var uq fic.QAQuery
if err := json.Unmarshal(body, &uq); err != nil {
return nil, err
}
uq.Id = query.Id
if uq.User != query.User && (uq.IdExercice != query.IdExercice || uq.IdTeam != query.IdTeam || uq.User != query.User || uq.Creation != query.Creation || uq.State != query.State || uq.Subject != query.Subject || uq.Closed != query.Closed) {
return nil, errors.New("You can only update your own entry.")
}
if _, err := uq.Update(); err != nil {
return nil, err
} else {
return uq, err
}
}
func deleteExerciceQA(u QAUser, query fic.QAQuery, exercice fic.Exercice, body []byte) (interface{}, error) {
if u.User != query.User {
return nil, errors.New("You can only delete your own entry.")
}
return query.Delete()
}
func getQAComments(_ QAUser, query fic.QAQuery, exercice fic.Exercice, body []byte) (interface{}, error) {
return query.GetComments()
}
func createQAComment(u QAUser, query fic.QAQuery, exercice fic.Exercice, body []byte) (interface{}, error) {
// Create a new query
var uc fic.QAComment
if err := json.Unmarshal(body, &uc); err != nil {
return nil, err
}
if len(uc.Content) == 0 {
return nil, errors.New("Empty comment")
}
return query.AddComment(uc.Content, u.TeamId, u.User)
}
func deleteQAComment(u QAUser, comment fic.QAComment, query fic.QAQuery, exercice fic.Exercice, body []byte) (interface{}, error) {
if u.User != comment.User {
return nil, errors.New("You can only delete your own comment.")
}
return comment.Delete()
}

11
qa/api/router.go Normal file
View file

@ -0,0 +1,11 @@
package api
import (
"github.com/julienschmidt/httprouter"
)
var router = httprouter.New()
func Router() *httprouter.Router {
return router
}

64
qa/api/theme.go Normal file
View file

@ -0,0 +1,64 @@
package api
import (
"strconv"
"srs.epita.fr/fic-server/libfic"
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/themes", apiHandler(listThemes))
router.GET("/api/themes.json", apiHandler(exportThemes))
router.GET("/api/themes/:thid", apiHandler(themeHandler(showTheme)))
router.GET("/api/themes/:thid/exercices", apiHandler(themeHandler(listThemedExercices)))
router.GET("/api/themes/:thid/exercices/:eid", apiHandler(exerciceHandler(showExercice)))
}
func themeHandler(f func(QAUser, fic.Theme, []byte) (interface{}, error)) func(QAUser, httprouter.Params, []byte) (interface{}, error) {
return func(u QAUser, ps httprouter.Params, body []byte) (interface{}, error) {
if thid, err := strconv.ParseInt(string(ps.ByName("thid")), 10, 64); err != nil {
return nil, err
} else if theme, err := fic.GetTheme(thid); err != nil {
return nil, err
} else {
return f(u, theme, body)
}
}
}
func getExercice(args []string) (fic.Exercice, error) {
if tid, err := strconv.ParseInt(string(args[0]), 10, 64); err != nil {
return fic.Exercice{}, err
} else if theme, err := fic.GetTheme(tid); err != nil {
return fic.Exercice{}, err
} else if eid, err := strconv.Atoi(string(args[1])); err != nil {
return fic.Exercice{}, err
} else {
return theme.GetExercice(eid)
}
}
func listThemes(_ QAUser, _ httprouter.Params, _ []byte) (interface{}, error) {
return fic.GetThemes()
}
func exportThemes(_ QAUser, _ httprouter.Params, _ []byte) (interface{}, error) {
return fic.ExportThemes()
}
func showTheme(_ QAUser, theme fic.Theme, _ []byte) (interface{}, error) {
return theme, nil
}
func listThemedExercices(_ QAUser, theme fic.Theme, _ []byte) (interface{}, error) {
return theme.GetExercices()
}
func showThemedExercice(_ QAUser, theme fic.Theme, exercice fic.Exercice, body []byte) (interface{}, error) {
return exercice, nil
}

13
qa/api/version.go Normal file
View file

@ -0,0 +1,13 @@
package api
import (
"github.com/julienschmidt/httprouter"
)
func init() {
router.GET("/api/version", apiHandler(showVersion))
}
func showVersion(u QAUser, _ httprouter.Params, body []byte) (interface{}, error) {
return map[string]interface{}{"version": 0.1, "auth": u}, nil
}