From c9d64640e209eee6f0f9dcd27f5f458bef5b7fa9 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 20 Nov 2020 15:46:52 +0100 Subject: [PATCH] Add users and grades display --- db.go | 5 ++ grades.go | 128 ++++++++++++++++++++++++++++++++++++ htdocs/index.html | 1 + htdocs/js/atsebayt.js | 96 +++++++++++++++++++++++++++ htdocs/views/grades.html | 21 ++++++ htdocs/views/responses.html | 9 ++- htdocs/views/survey.html | 4 +- htdocs/views/user.html | 62 +++++++++++++++++ htdocs/views/users.html | 29 ++++++++ static-dev.go | 9 +++ static.go | 21 ++++++ surveys.go | 52 +++++++++++++-- users.go | 12 ++-- 13 files changed, 432 insertions(+), 17 deletions(-) create mode 100644 grades.go create mode 100644 htdocs/views/grades.html create mode 100644 htdocs/views/user.html create mode 100644 htdocs/views/users.html diff --git a/db.go b/db.go index e6f6722..66dde25 100644 --- a/db.go +++ b/db.go @@ -148,6 +148,11 @@ CREATE TABLE IF NOT EXISTS student_corrected( FOREIGN KEY(id_user) REFERENCES users(id_user), FOREIGN KEY(id_template) REFERENCES correction_templates(id_template) ) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; +`); err != nil { + return err + } + if _, err := db.Exec(` +CREATE VIEW IF NOT EXISTS student_scores AS SELECT U.id_user, id_survey, Q.id_question, MAX(R.score) as score FROM survey_quests Q CROSS JOIN users U LEFT JOIN survey_responses R ON Q.id_question = R.id_question AND R.id_user = U.id_user GROUP BY Q.id_question, U.id_user; `); err != nil { return err } diff --git a/grades.go b/grades.go new file mode 100644 index 0000000..b6d762c --- /dev/null +++ b/grades.go @@ -0,0 +1,128 @@ +package main + +import ( + "errors" + "net/http" + + "github.com/julienschmidt/httprouter" +) + +func init() { + router.GET("/api/users/:uid/surveys/:sid/grades", apiAuthHandler(func(uauth *User, ps httprouter.Params, body []byte) HTTPResponse { + return surveyAuthHandler(func(s Survey, uauth *User, _ []byte) HTTPResponse { + return userHandler(func(u User, _ []byte) HTTPResponse { + if uauth != nil && ((s.Shown && u.Id == uauth.Id) || uauth.IsAdmin) { + if score, err := s.GetUserGrades(&u); err != nil { + return APIErrorResponse{err: err} + } else if score == nil { + return APIResponse{"N/A"} + } else { + return APIResponse{score} + } + } else { + return APIErrorResponse{ + status: http.StatusForbidden, + err: errors.New("Not accessible"), + } + } + })(ps, body) + })(uauth, ps, body) + }, loggedUser)) + router.GET("/api/grades", apiAuthHandler(func(uauth *User, ps httprouter.Params, body []byte) HTTPResponse { + if uauth != nil && uauth.IsAdmin { + if score, err := GetAllGrades(); err != nil { + return APIErrorResponse{err: err} + } else if score == nil { + return APIResponse{"N/A"} + } else { + return APIResponse{score} + } + } else { + return APIErrorResponse{ + status: http.StatusForbidden, + err: errors.New("Not accessible"), + } + } + }, adminRestricted)) +} + +func GetAllGrades() (scores map[int64]map[int64]*float64, err error) { + if rows, errr := DBQuery("SELECT id_user, id_survey, SUM(score)/COUNT(*) FROM student_scores GROUP BY id_user, id_survey"); err != nil { + return nil, errr + } else { + defer rows.Close() + + scores = map[int64]map[int64]*float64{} + + for rows.Next() { + var id_user int64 + var id_survey int64 + var score *float64 + + if err = rows.Scan(&id_user, &id_survey, &score); err != nil { + return + } + + if scores[id_user] == nil { + scores[id_user] = map[int64]*float64{} + } + + scores[id_user][id_survey] = score + } + if err = rows.Err(); err != nil { + return + } + } + + return +} + +func (s Survey) GetGrades() (scores map[int64]*float64, err error) { + if rows, errr := DBQuery("SELECT id_question, SUM(score)/COUNT(*) FROM student_scores WHERE id_survey=? GROUP BY id_question", s.Id); err != nil { + return nil, errr + } else { + defer rows.Close() + + scores = map[int64]*float64{} + + for rows.Next() { + var id_question int64 + var score *float64 + + if err = rows.Scan(&id_question, &score); err != nil { + return + } + scores[id_question] = score + } + if err = rows.Err(); err != nil { + return + } + } + + return +} + +func (s Survey) GetUserGrades(u *User) (scores map[int64]*float64, err error) { + if rows, errr := DBQuery("SELECT id_question, MAX(score) FROM student_scores WHERE id_survey=? AND id_user = ? GROUP BY id_question", s.Id, u.Id); err != nil { + return nil, errr + } else { + defer rows.Close() + + scores = map[int64]*float64{} + + for rows.Next() { + var id_question int64 + var score *float64 + + if err = rows.Scan(&id_question, &score); err != nil { + return + } + scores[id_question] = score + } + if err = rows.Err(); err != nil { + return + } + } + + return +} diff --git a/htdocs/index.html b/htdocs/index.html index 99dd878..8afb33a 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -28,6 +28,7 @@ + diff --git a/htdocs/js/atsebayt.js b/htdocs/js/atsebayt.js index bb14663..53b2554 100644 --- a/htdocs/js/atsebayt.js +++ b/htdocs/js/atsebayt.js @@ -5,6 +5,10 @@ angular.module("AtsebaytApp", ["ngRoute", "ngResource", "ngSanitize"]) controller: "AuthController", templateUrl: "views/auth.html" }) + .when("/grades", { + controller: "GradesController", + templateUrl: "views/grades.html" + }) .when("/surveys", { controller: "SurveysController", templateUrl: "views/surveys.html" @@ -21,6 +25,14 @@ angular.module("AtsebaytApp", ["ngRoute", "ngResource", "ngSanitize"]) controller: "QuestionController", templateUrl: "views/correction.html" }) + .when("/users", { + controller: "UsersController", + templateUrl: "views/users.html" + }) + .when("/users/:userId", { + controller: "UserController", + templateUrl: "views/user.html" + }) .when("/", { controller: "SurveysController", templateUrl: "views/home.html" @@ -42,14 +54,26 @@ angular.module("AtsebaytApp") .factory("MyResponse", function($resource) { return $resource("/api/surveys/:surveyId/responses/:respId", { surveyId: '@id', respId: '@id' }) }) + .factory("Grades", function($resource) { + return $resource("/api/grades") + }) .factory("Survey", function($resource) { return $resource("/api/surveys/:surveyId", { surveyId: '@id' }, { 'update': {method: 'PUT'}, }) }) + .factory("SurveyGrades", function($resource) { + return $resource("/api/surveys/:surveyId/grades", { surveyId: '@id' }) + }) .factory("SurveyScore", function($resource) { return $resource("/api/surveys/:surveyId/score", { surveyId: '@id' }) }) + .factory("SurveyUserScore", function($resource) { + return $resource("/api/users/:userId/surveys/:surveyId/score", { userId: '@id', surveyId: '@id' }) + }) + .factory("SurveyUserGrades", function($resource) { + return $resource("/api/users/:userId/surveys/:surveyId/grades", { userId: '@id', surveyId: '@id' }) + }) .factory("SurveyQuest", function($resource) { return $resource("/api/surveys/:surveyId/questions/:questId", { surveyId: '@id', questId: '@id' }, { 'update': {method: 'PUT'}, @@ -236,6 +260,7 @@ angular.module("AtsebaytApp") .controller("SurveysController", function($scope, $rootScope, Survey, $location) { $rootScope.qactive = $location.$$path == "/surveys"; + $rootScope.uactive = $location.$$path.indexOf("/users") != -1; $scope.surveys = Survey.query(); $scope.surveys.$promise.then(function(data) { data.forEach(function(d,k) { @@ -247,6 +272,7 @@ angular.module("AtsebaytApp") .controller("SurveyController", function($scope, $rootScope, Survey, $routeParams, $location) { $rootScope.qactive = true; + $rootScope.uactive = false; $scope.survey = Survey.get({ surveyId: $routeParams.surveyId }); $scope.survey.$promise.then(function(survey) { survey.readonly = Date.now() > Date.parse(survey.end_availability) @@ -277,6 +303,26 @@ angular.module("AtsebaytApp") } }) + .controller("SurveyGradesController", function($scope, SurveyGrades) { + $scope.grades = SurveyGrades.get({ surveyId: $scope.survey.id }) + $scope.mean = 0; + $scope.grades.$promise.then(function (grades) { + var sum = 0 + var total = 0 + for (var gid in grades) { + if (parseInt(gid, 10) > 0) { + total++ + if (grades[gid]) { + sum += grades[gid] + } + } + } + if (total > 0) { + $scope.mean = sum/total + } + }) + }) + .controller("ResponsesController", function($scope, AllResponses) { $scope.responses = AllResponses.query({ surveyId: $scope.survey.id, questId: $scope.question.id }); }) @@ -285,6 +331,56 @@ angular.module("AtsebaytApp") $scope.score = SurveyScore.get({ surveyId: $scope.survey.id }) }) + .controller("UserScoreController", function($scope, SurveyUserScore) { + $scope.score = SurveyUserScore.get({ userId: $scope.user.id, surveyId: $scope.survey.id }) + }) + + .controller("UserGradesController", function($scope, SurveyUserGrades) { + $scope.grades = SurveyUserGrades.get({ userId: $scope.user.id, surveyId: $scope.survey.id }) + $scope.avancement = 0; + $scope.grades.$promise.then(function (grades) { + var answered = 0 + var total = 0 + for (var gid in grades) { + if (parseInt(gid, 10) > 0) { + total++ + if (grades[gid]) { + answered++ + } + } + } + if (total > 0) { + $scope.avancement = answered/total + } + }) + }) + + .controller("GradesController", function($scope, $rootScope, Grades, Survey, User, $location) { + $scope.users = User.query(); + $scope.surveys = Survey.query(); + $scope.grades = Grades.get(); + + $scope.showUser = function() { + $location.url("users/" + this.user.id); + } + }) + + .controller("UsersController", function($scope, $rootScope, User, $location) { + $rootScope.qactive = false; + $rootScope.uactive = true; + $scope.users = User.query() + + $scope.showUser = function() { + $location.url("users/" + this.user.id); + } + }) + + .controller("UserController", function($scope, $routeParams, $rootScope, User) { + $rootScope.qactive = false; + $rootScope.uactive = true; + $scope.user = User.get({ userId: $routeParams.userId}) + }) + .controller("QuestionController", function($scope, Survey, SurveyQuest, SurveyQuest, AllResponses, CorrectionTemplate, $http, $routeParams) { $scope.notCorrected = true $scope.highlight = '' diff --git a/htdocs/views/grades.html b/htdocs/views/grades.html new file mode 100644 index 0000000..08b05c4 --- /dev/null +++ b/htdocs/views/grades.html @@ -0,0 +1,21 @@ +

+ Étudiants + Notes +

+ + + + + + + + + + + + + + + + +
IDLogin{{ survey.title }}
{{ user.id }}{{ user.login }}{{ grades[user.id][survey.id] | number:2 }}
diff --git a/htdocs/views/responses.html b/htdocs/views/responses.html index 2005544..d03cfbe 100644 --- a/htdocs/views/responses.html +++ b/htdocs/views/responses.html @@ -12,14 +12,19 @@ Question Réponses - + Moyenne - + {{ responses.length }} + {{ grades[question.id] }} % + + + Moyenne + {{ mean }} % diff --git a/htdocs/views/survey.html b/htdocs/views/survey.html index 7ec4d9c..dfb62bd 100644 --- a/htdocs/views/survey.html +++ b/htdocs/views/survey.html @@ -1,5 +1,5 @@ - - + +

{{ survey.title }} diff --git a/htdocs/views/user.html b/htdocs/views/user.html new file mode 100644 index 0000000..08fd4f8 --- /dev/null +++ b/htdocs/views/user.html @@ -0,0 +1,62 @@ +

+ Étudiant + + {{ user.login }} + +

+ +
+ +
+
+
ID
+
{{ user.id }}
+ +
Login
+
{{ user.login }}
+ +
E-mail
+
{{ user.email }}
+ +
Nom
+
{{ user.lastname }}
+ +
Prénom
+
{{ user.firstname }}
+ +
Date d'inscription
+
{{ user.time }}
+ +
Admin
+
{{ user.id_admin?"Oui":"Non" }}
+
+
+ +
+ avatar +
+
+ +
+
+ Questionnaires +
+ + + + + + + + + + + + + + + + + +
IDTitreAvancementNote
{{ survey.id }}{{ survey.title }}{{ avancement * 100 | number:2 }} %{{ score.score | number:2 }}/20
+
diff --git a/htdocs/views/users.html b/htdocs/views/users.html new file mode 100644 index 0000000..350fc70 --- /dev/null +++ b/htdocs/views/users.html @@ -0,0 +1,29 @@ + +

+ Étudiants +

+ + + + + + + + + + + + + + + + + + + + + + + + +
IDLoginE-mailNomPrénomDate d'inscriptionadmin
{{ user.id }}{{ user.login }}{{ user.email }}{{ user.lastname }}{{ user.firstname }}{{ user.time }}{{ user.is_admin?"Oui":"Non" }}
diff --git a/static-dev.go b/static-dev.go index 5e7d63f..94fe79f 100644 --- a/static-dev.go +++ b/static-dev.go @@ -21,12 +21,21 @@ func init() { Router().GET("/auth", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { http.ServeFile(w, r, path.Join(StaticDir, "index.html")) }) + Router().GET("/grades", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, "index.html")) + }) Router().GET("/surveys", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { http.ServeFile(w, r, path.Join(StaticDir, "index.html")) }) Router().GET("/surveys/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { http.ServeFile(w, r, path.Join(StaticDir, "index.html")) }) + Router().GET("/users", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, "index.html")) + }) + Router().GET("/users/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + http.ServeFile(w, r, path.Join(StaticDir, "index.html")) + }) Router().GET("/css/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { serveStaticAsset(w, r) }) diff --git a/static.go b/static.go index 944089e..c1910c4 100644 --- a/static.go +++ b/static.go @@ -28,6 +28,13 @@ func init() { w.Write(data) } }) + Router().GET("/grades", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + if data, err := Asset("htdocs/index.html"); err != nil { + fmt.Fprintf(w, "{\"errmsg\":%q}", err) + } else { + w.Write(data) + } + }) Router().GET("/surveys", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { if data, err := Asset("htdocs/index.html"); err != nil { fmt.Fprintf(w, "{\"errmsg\":%q}", err) @@ -42,6 +49,20 @@ func init() { w.Write(data) } }) + Router().GET("/users", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + if data, err := Asset("htdocs/index.html"); err != nil { + fmt.Fprintf(w, "{\"errmsg\":%q}", err) + } else { + w.Write(data) + } + }) + Router().GET("/users/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + if data, err := Asset("htdocs/index.html"); err != nil { + fmt.Fprintf(w, "{\"errmsg\":%q}", err) + } else { + w.Write(data) + } + }) Router().GET("/css/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { w.Header().Set("Content-Type", "text/css") if data, err := Asset(path.Join("htdocs", r.URL.Path)); err != nil { diff --git a/surveys.go b/surveys.go index 1114325..cd531d3 100644 --- a/surveys.go +++ b/surveys.go @@ -70,6 +70,26 @@ func init() { } } }), loggedUser)) + router.GET("/api/users/:uid/surveys/:sid/score", apiAuthHandler(func(uauth *User, ps httprouter.Params, body []byte) HTTPResponse { + return surveyAuthHandler(func(s Survey, uauth *User, _ []byte) HTTPResponse { + return userHandler(func(u User, _ []byte) HTTPResponse { + if uauth != nil && ((s.Shown && u.Id == uauth.Id) || uauth.IsAdmin) { + if score, err := s.GetScore(&u); err != nil { + return APIErrorResponse{err: err} + } else if score == nil { + return APIResponse{map[string]string{"score": "N/A"}} + } else { + return APIResponse{map[string]float64{"score": *score}} + } + } else { + return APIErrorResponse{ + status: http.StatusForbidden, + err: errors.New("Not accessible"), + } + } + })(ps, body) + })(uauth, ps, body) + }, loggedUser)) } func surveyHandler(f func(Survey, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse { @@ -142,17 +162,35 @@ func NewSurvey(title string, shown bool, startAvailability time.Time, endAvailab } func (s Survey) GetScore(u *User) (score *float64, err error) { - if err = DBQueryRow("SELECT SUM(M.score) FROM (SELECT MAX(score) AS score FROM survey_responses R INNER JOIN survey_quests Q ON Q.id_question = R.id_question WHERE Q.id_survey=? AND R.id_user=? GROUP BY Q.id_question) M", s.Id, u.Id).Scan(&score); err != nil || score == nil { - return + err = DBQueryRow("SELECT SUM(score)/COUNT(*) FROM student_scores WHERE id_survey=? AND id_user=?", s.Id, u.Id).Scan(&score) + if score != nil { + *score = *score / 5.0 } + return +} - var questions []Question - if questions, err = s.GetQuestions(); err != nil { - return +func (s Survey) GetScores() (scores map[int64]*float64, err error) { + if rows, errr := DBQuery("SELECT id_user, SUM(score)/COUNT(*) FROM student_scores WHERE id_survey=? GROUP BY id_user", s.Id); err != nil { + return nil, errr + } else { + defer rows.Close() + + scores = map[int64]*float64{} + + for rows.Next() { + var id_user int64 + var score *float64 + + if err = rows.Scan(&id_user, &score); err != nil { + return + } + scores[id_user] = score + } + if err = rows.Err(); err != nil { + return + } } - *score = *score / (float64(len(questions)) * 5.0) - return } diff --git a/users.go b/users.go index 9b4519d..5fc9171 100644 --- a/users.go +++ b/users.go @@ -2,8 +2,8 @@ package main import ( "encoding/json" - "time" "strconv" + "time" "github.com/julienschmidt/httprouter" ) @@ -17,23 +17,23 @@ func init() { func(u User, _ []byte) HTTPResponse { return APIResponse{u} }))) - router.PUT("/api/users/:uid", apiHandler(userHandler(updateUser))) + router.PUT("/api/users/:uid", apiHandler(userHandler(updateUser), adminRestricted)) router.DELETE("/api/users/:uid", apiHandler(userHandler( func(u User, _ []byte) HTTPResponse { return formatApiResponse(u.Delete()) - }))) + }), adminRestricted)) } func userHandler(f func(User, []byte) HTTPResponse) func(httprouter.Params, []byte) HTTPResponse { return func(ps httprouter.Params, body []byte) HTTPResponse { if uid, err := strconv.Atoi(string(ps.ByName("uid"))); err != nil { if user, err := getUserByLogin(ps.ByName("uid")); err != nil { - return APIErrorResponse{err:err} + return APIErrorResponse{err: err} } else { return f(user, body) } } else if user, err := getUser(uid); err != nil { - return APIErrorResponse{err:err} + return APIErrorResponse{err: err} } else { return f(user, body) } @@ -140,7 +140,7 @@ func ClearUsers() (int64, error) { func updateUser(current User, body []byte) HTTPResponse { var new User if err := json.Unmarshal(body, &new); err != nil { - return APIErrorResponse{err:err} + return APIErrorResponse{err: err} } current.Login = new.Login