diff --git a/ui/src/lib/works.js b/ui/src/lib/works.js
index 1d5976c..c87dd0f 100644
--- a/ui/src/lib/works.js
+++ b/ui/src/lib/works.js
@@ -109,6 +109,18 @@ export class Work {
}
}
+ async addMissingGrades() {
+ const res = await fetch(`api/works/${this.id}/grades`, {
+ method: 'PATCH',
+ headers: {'Accept': 'application/json'},
+ });
+ if (res.status == 200) {
+ return (await res.json()).map((g) => new Grade(g));
+ } else {
+ throw new Error((await res.json()).errmsg);
+ }
+ }
+
async getSubmission(uid) {
const res = await fetch(uid?`api/users/${uid}/works/${this.id}/submission`:`api/works/${this.id}/submission`, {
headers: {'Accept': 'application/json'}
diff --git a/ui/src/routes/works/[wid]/+page.svelte b/ui/src/routes/works/[wid]/+page.svelte
index 9048e5f..df2a76f 100644
--- a/ui/src/routes/works/[wid]/+page.svelte
+++ b/ui/src/routes/works/[wid]/+page.svelte
@@ -40,6 +40,11 @@
stats.mean = sum / grades.length;
});
}
+
+ async function addMissingStudents(w) {
+ await w.addMissingGrades();
+ refresh_grades(w);
+ }
{#if $user && $user.is_admin}
@@ -72,12 +77,22 @@
{#if stats.mean > 0}(moyenne : {Math.round(stats.mean*100)/100}, min : {stats.min}, max : {stats.max}){/if}
-
+
+
+
+
{#await gradesP}
diff --git a/users.go b/users.go
index 5ed899b..26dcfe3 100644
--- a/users.go
+++ b/users.go
@@ -4,6 +4,7 @@ import (
"fmt"
"log"
"net/http"
+ "regexp"
"strconv"
"strings"
"time"
@@ -169,6 +170,33 @@ func getUsers() (users []User, err error) {
}
}
+func getFilteredUsers(promo uint, group string) (users []User, err error) {
+ // Avoid SQL injection: check group name doesn't contains harmful content
+ var validGroup = regexp.MustCompile(`^[a-z0-9-]*$`)
+ if !validGroup.MatchString(group) {
+ return nil, fmt.Errorf("%q is not a valid group name", group)
+ }
+
+ if rows, errr := DBQuery("SELECT id_user, login, email, firstname, lastname, time, promo, groups, is_admin FROM users WHERE promo = ? AND groups LIKE '%,"+group+",%' ORDER BY promo DESC, id_user DESC", promo); errr != nil {
+ return nil, errr
+ } else {
+ defer rows.Close()
+
+ for rows.Next() {
+ var u User
+ if err = rows.Scan(&u.Id, &u.Login, &u.Email, &u.Firstname, &u.Lastname, &u.Time, &u.Promo, &u.Groups, &u.IsAdmin); err != nil {
+ return
+ }
+ users = append(users, u)
+ }
+ if err = rows.Err(); err != nil {
+ return
+ }
+
+ return
+ }
+}
+
func getPromos() (promos []uint, err error) {
if rows, errr := DBQuery("SELECT DISTINCT promo FROM users ORDER BY promo DESC"); errr != nil {
return nil, errr
diff --git a/works.go b/works.go
index e2d857d..6909cec 100644
--- a/works.go
+++ b/works.go
@@ -179,6 +179,49 @@ func declareAPIAdminWorksRoutes(router *gin.RouterGroup) {
c.JSON(http.StatusOK, grades)
})
+ worksRoutes.PATCH("/grades", func(c *gin.Context) {
+ w := c.MustGet("work").(*Work)
+
+ // Fetch existing grades
+ grades, err := w.GetGrades("")
+ if err != nil {
+ log.Printf("Unable to GetGrades(wid=%d): %s", w.Id, err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during grades retrieval."})
+ return
+ }
+
+ // Create an index
+ known_users := map[int64]bool{}
+ for _, g := range grades {
+ known_users[g.IdUser] = true
+ }
+
+ // Fetch students list registered for this course
+ users, err := getFilteredUsers(w.Promo, w.Group)
+ if err != nil {
+ log.Printf("Unable to getFilteredUsers(%d, %s): %s", w.Promo, w.Group, err.Error())
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs during users retrieval."})
+ return
+ }
+
+ var toAdd []WorkGrade
+ for _, user := range users {
+ if _, ok := known_users[user.Id]; !ok {
+ toAdd = append(toAdd, WorkGrade{
+ IdUser: user.Id,
+ Login: user.Login,
+ Grade: 0,
+ Comment: "- Non rendu -",
+ })
+ }
+ }
+
+ if len(toAdd) > 0 {
+ w.AddGrades(toAdd)
+ }
+
+ c.JSON(http.StatusOK, toAdd)
+ })
worksRoutes.PUT("/grades", func(c *gin.Context) {
w := c.MustGet("work").(*Work)