From a3ffdeae177b0deb90e723c5e3e32d65213a8989 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 23 Jan 2020 16:03:31 +0100 Subject: [PATCH] frontend: display issues related to the team --- admin/api/claim.go | 52 ++++++++++++++++++- admin/static/js/app.js | 17 ++++++- admin/static/views/claim.html | 5 ++ backend/generation.go | 21 ++++++++ backend/issue.go | 3 +- configs/nginx-demo.conf | 7 +++ configs/nginx-prod.conf | 7 +++ fickit-frontend.yml | 3 ++ frontend/static/js/challenge.js | 20 ++++++++ frontend/static/views/issue.html | 28 +++++++++++ libfic/db.go | 1 + libfic/todo.go | 86 +++++++++++++++++++++++++++++--- 12 files changed, 238 insertions(+), 12 deletions(-) diff --git a/admin/api/claim.go b/admin/api/claim.go index d3438254..c15a6474 100644 --- a/admin/api/claim.go +++ b/admin/api/claim.go @@ -3,6 +3,9 @@ package api import ( "encoding/json" "errors" + "fmt" + "io/ioutil" + "path" "time" "srs.epita.fr/fic-server/libfic" @@ -11,6 +14,11 @@ import ( ) func init() { + router.GET("/api/teams/:tid/issue.json", apiHandler(teamHandler( + func(team fic.Team, _ []byte) (interface{}, error) { + return team.MyIssueFile() + }))) + // Tasks router.GET("/api/claims", apiHandler(getClaims)) router.POST("/api/claims", apiHandler(newClaim)) @@ -24,6 +32,8 @@ func init() { router.POST("/api/claims/:cid", apiHandler(claimHandler(addClaimDescription))) router.DELETE("/api/claims/:cid", apiHandler(claimHandler(deleteClaim))) + router.PUT("/api/claims/:cid/descriptions", apiHandler(claimHandler(updateClaimDescription))) + // Assignees router.GET("/api/claims-assignees", apiHandler(getAssignees)) router.POST("/api/claims-assignees", apiHandler(newAssignee)) @@ -157,6 +167,17 @@ func clearClaims(_ httprouter.Params, _ []byte) (interface{}, error) { return fic.ClearClaims() } +func generateTeamIssuesFile(team fic.Team) error { + if my, err := team.MyIssueFile(); err != nil { + return err + } else if j, err := json.Marshal(my); err != nil { + return err + } else if err = ioutil.WriteFile(path.Join(TeamsDir, fmt.Sprintf("%d", team.Id), "issues.json"), j, 0644); err != nil { + return err + } + return nil +} + func addClaimDescription(claim fic.Claim, body []byte) (interface{}, error) { var ud fic.ClaimDescription if err := json.Unmarshal(body, &ud); err != nil { @@ -165,8 +186,31 @@ func addClaimDescription(claim fic.Claim, body []byte) (interface{}, error) { if assignee, err := fic.GetAssignee(ud.IdAssignee); err != nil { return nil, err + } else if description, err := claim.AddDescription(ud.Content, assignee, ud.Publish); err != nil { + return nil, err } else { - return claim.AddDescription(ud.Content, assignee) + if team, _ := claim.GetTeam(); team != nil { + err = generateTeamIssuesFile(*team) + } + + return description, err + } +} + +func updateClaimDescription(claim fic.Claim, body []byte) (interface{}, error) { + var ud fic.ClaimDescription + if err := json.Unmarshal(body, &ud); err != nil { + return nil, err + } + + if _, err := ud.Update(); err != nil { + return nil, err + } else { + if team, _ := claim.GetTeam(); team != nil { + err = generateTeamIssuesFile(*team) + } + + return ud, err } } @@ -181,7 +225,11 @@ func updateClaim(claim fic.Claim, body []byte) (interface{}, error) { if _, err := uc.Update(); err != nil { return nil, err } else { - return uc, nil + if team, _ := claim.GetTeam(); team != nil { + err = generateTeamIssuesFile(*team) + } + + return uc, err } } diff --git a/admin/static/js/app.js b/admin/static/js/app.js index 8b8d5e7d..767c17d3 100644 --- a/admin/static/js/app.js +++ b/admin/static/js/app.js @@ -1133,13 +1133,26 @@ angular.module("FICApp") $scope.changeState = function(state) { this.claim.state = state; if (this.claim.id) - this.saveClaim(false); + this.saveClaim(state == "invalid" || state == "closed"); } $scope.assignToMe = function() { this.claim.id_assignee = $scope.whoami; if (this.claim.id) this.saveClaim(false); } + $scope.updateDescription = function(description) { + $http({ + url: "/api/claims/" + $scope.claim.id + "/descriptions", + method: "PUT", + data: description + }).then(function(response) { + $scope.claim = Claim.get({ claimId: $routeParams.claimId }, function(v) { + v.id_team = "" + v.id_team; + if (!v.priority) + v.priority = "medium"; + }); + }); + } $scope.saveDescription = function() { $http({ url: "/api/claims/" + $scope.claim.id, @@ -1152,7 +1165,7 @@ angular.module("FICApp") $location.url("/claims/" + $scope.claim.id + "/"); }); } - $scope.saveClaim = function(backToList) { + $scope.saveClaim = function(backToList) { if (this.claim.id_team) { this.claim.id_team = parseInt(this.claim.id_team, 10); } else { diff --git a/admin/static/views/claim.html b/admin/static/views/claim.html index 320283c1..016d7eaa 100644 --- a/admin/static/views/claim.html +++ b/admin/static/views/claim.html @@ -62,6 +62,11 @@
+
+ +
Par {{ assignee.name }} le {{ description.date | date:"mediumTime" }} : {{ description.content }}
diff --git a/backend/generation.go b/backend/generation.go index 5ea259bb..dcc5f7d4 100644 --- a/backend/generation.go +++ b/backend/generation.go @@ -96,6 +96,27 @@ func consumer() { } } +// Generate issues.json for a given team +func genTeamIssuesFile(team fic.Team) error { + dirPath := path.Join(TeamsDir, fmt.Sprintf("%d", team.Id)) + + if s, err := os.Stat(dirPath); os.IsNotExist(err) { + os.MkdirAll(dirPath, 0777) + } else if !s.IsDir() { + return errors.New(fmt.Sprintf("%s is not a directory", dirPath)) + } + + if my, err := team.MyIssueFile(); err != nil { + return err + } else if j, err := json.Marshal(my); err != nil { + return err + } else if err = ioutil.WriteFile(path.Join(dirPath, "issues.json"), j, 0644); err != nil { + return err + } + + return nil +} + // Generate my.json and wait.json for a given team func genTeamMyFile(team *fic.Team) error { dirPath := path.Join(TeamsDir, fmt.Sprintf("%d", team.Id)) diff --git a/backend/issue.go b/backend/issue.go index b25cb468..c04cb564 100644 --- a/backend/issue.go +++ b/backend/issue.go @@ -44,7 +44,7 @@ func treatIssue(pathname string, team fic.Team) { if claim, err := fic.NewClaim(issue.Subject, &team, exercice, nil, "medium"); err != nil { log.Printf("%s [ERR] Unable to create new issue: %s\n", id, err) } else if len(issue.Description) > 0 { - if _, err := claim.AddDescription(issue.Description, fic.ClaimAssignee{Id: 0}); err != nil { + if _, err := claim.AddDescription(issue.Description, fic.ClaimAssignee{Id: 0}, true); err != nil { log.Printf("%s [WRN] Unable to add description to issue: %s\n", id, err) } else { log.Printf("%s [OOK] New issue created: id=%d\n", id, claim.Id) @@ -53,5 +53,6 @@ func treatIssue(pathname string, team fic.Team) { } } } + genTeamIssuesFile(team) } } diff --git a/configs/nginx-demo.conf b/configs/nginx-demo.conf index b9d8d214..a66995d9 100644 --- a/configs/nginx-demo.conf +++ b/configs/nginx-demo.conf @@ -141,6 +141,13 @@ server { rewrite ^/.* /wait.json; } } + location /issues.json { + include fic-auth.conf; + + root /srv/TEAMS/$team/; + expires epoch; + add_header Cache-Control no-cache; + } location = /events.json { root /srv/TEAMS/; expires epoch; diff --git a/configs/nginx-prod.conf b/configs/nginx-prod.conf index 9d737cc6..bc1591d0 100644 --- a/configs/nginx-prod.conf +++ b/configs/nginx-prod.conf @@ -133,6 +133,13 @@ server { rewrite ^/.* /wait.json; } } + location /issues.json { + include fic-auth.conf; + + root /srv/TEAMS/$team/; + expires epoch; + add_header Cache-Control no-cache; + } location = /events.json { root /srv/TEAMS/; expires epoch; diff --git a/fickit-frontend.yml b/fickit-frontend.yml index b6a730e4..a99489d3 100644 --- a/fickit-frontend.yml +++ b/fickit-frontend.yml @@ -390,6 +390,9 @@ files: - path: www/htdocs-frontend/views/home.html source: frontend/static/views/home.html mode: "0644" + - path: www/htdocs-frontend/views/issue.html + source: frontend/static/views/issue.html + mode: "0644" - path: www/htdocs-frontend/views/rank.html source: frontend/static/views/rank.html mode: "0644" diff --git a/frontend/static/js/challenge.js b/frontend/static/js/challenge.js index eff89960..0b80d6d3 100644 --- a/frontend/static/js/challenge.js +++ b/frontend/static/js/challenge.js @@ -108,6 +108,7 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"]) $rootScope.current_exercice = 0; $rootScope.current_tag = undefined; $rootScope.notify_field = 0; + $rootScope.issues_known_responses = 0; if ('Notification' in window) Notification.requestPermission(function(result) { @@ -219,6 +220,23 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"]) }); } + var refreshIssuesInterval + var refreshIssues = function() { + if (refreshIssuesInterval) + $interval.cancel(refreshIssuesInterval); + refreshIssuesInterval = $interval(refreshIssues, Math.floor(Math.random() * 24000) + 32000); + + $http.get("issues.json").then(function(response) { + $rootScope.issues_nb_responses = 0; + $rootScope.issues_need_info = 0; + $rootScope.issues = response.data; + $rootScope.issues.forEach(function(issue) { + $rootScope.issues_nb_responses += issue.texts.length; + if (issue.state == 'need-info') $rootScope.issues_need_info++; + }) + }); + } + var refreshThemesInterval var refreshThemes = function() { if (refreshThemesInterval) @@ -443,6 +461,7 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"]) refreshThemes(); $rootScope.refreshTeams(); refreshEvents(); + refreshIssues(); } else if (justSettings) { refreshSettings(); @@ -647,6 +666,7 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"]) .controller("IssueController", function($scope, $http, $rootScope, $routeParams) { $rootScope.current_tag = undefined; $rootScope.current_exercice = $routeParams.eid; + $rootScope.issues_known_responses = $rootScope.issues_nb_responses; $scope.issue = { id_exercice: parseInt($routeParams.eid, 10), diff --git a/frontend/static/views/issue.html b/frontend/static/views/issue.html index ba7e161a..7a379093 100644 --- a/frontend/static/views/issue.html +++ b/frontend/static/views/issue.html @@ -1,3 +1,31 @@ + +
+ + + + + + + + + + + + + + + + + +
ObjetÉtat / PrioritéGéré parMessages
{{ issue.subject }} (challenge {{ issue.exercice }}){{ issue.state }} / {{ issue.priority }}{{ issue.assignee }} +
+ Vous +  à {{ text.date | date:"mediumTime" }} :  + {{ text.cnt }} +
+
+
+
Rapporter une anomalie sur un exercice
diff --git a/libfic/db.go b/libfic/db.go index 1d13f85b..407dd5b6 100644 --- a/libfic/db.go +++ b/libfic/db.go @@ -412,6 +412,7 @@ CREATE TABLE IF NOT EXISTS claim_descriptions( id_assignee INTEGER NOT NULL, date TIMESTAMP NOT NULL, content TEXT NOT NULL, + publish BOOLEAN NOT NULL DEFAULT 0, FOREIGN KEY(id_assignee) REFERENCES claim_assignees(id_assignee), FOREIGN KEY(id_claim) REFERENCES claims(id_claim) ) DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; diff --git a/libfic/todo.go b/libfic/todo.go index 623cea14..8ed27205 100644 --- a/libfic/todo.go +++ b/libfic/todo.go @@ -189,19 +189,21 @@ type ClaimDescription struct { Content string `json:"content"` // Date is the timestamp when the description was written. Date time.Time `json:"date"` + // Publish indicates wether it is shown back to the team. + Publish bool `json:"publish"` } // GetDescriptions returns a list of all descriptions stored in the database for the Claim. -func (c Claim) GetDescriptions() (res []ClaimDescription, err error) { +func (c Claim) GetDescriptions() (res []ClaimDescription, err error) { var rows *sql.Rows - if rows, err = DBQuery("SELECT id_description, id_assignee, content, date FROM claim_descriptions WHERE id_claim = ?", c.Id); err != nil { + if rows, err = DBQuery("SELECT id_description, id_assignee, content, date, publish FROM claim_descriptions WHERE id_claim = ?", c.Id); err != nil { return nil, err } defer rows.Close() for rows.Next() { var d ClaimDescription - if err = rows.Scan(&d.Id, &d.IdAssignee, &d.Content, &d.Date); err != nil { + if err = rows.Scan(&d.Id, &d.IdAssignee, &d.Content, &d.Date, &d.Publish); err != nil { return } res = append(res, d) @@ -212,19 +214,25 @@ func (c Claim) GetDescriptions() (res []ClaimDescription, err error) { } // AddDescription append in the database a new description; then returns the corresponding structure. -func (c Claim) AddDescription(content string, assignee ClaimAssignee) (ClaimDescription, error) { - if res, err := DBExec("INSERT INTO claim_descriptions (id_claim, id_assignee, content, date) VALUES (?, ?, ?, ?)", c.Id, assignee.Id, content, time.Now()); err != nil { +func (c Claim) AddDescription(content string, assignee ClaimAssignee, publish bool) (ClaimDescription, error) { + if res, err := DBExec("INSERT INTO claim_descriptions (id_claim, id_assignee, content, date, publish) VALUES (?, ?, ?, ?, ?)", c.Id, assignee.Id, content, time.Now(), publish); err != nil { return ClaimDescription{}, err } else if did, err := res.LastInsertId(); err != nil { return ClaimDescription{}, err } else { - return ClaimDescription{did, assignee.Id, content, time.Now()}, nil + return ClaimDescription{did, assignee.Id, content, time.Now(), publish}, nil } } +// GetAssignee retrieves an assignee from its identifier. +func (d ClaimDescription) GetAssignee() (a ClaimAssignee, err error) { + err = DBQueryRow("SELECT id_assignee, name FROM claim_assignees WHERE id_assignee = ?", d.IdAssignee).Scan(&a.Id, &a.Name) + return +} + // Update applies modifications back to the database func (d ClaimDescription) Update() (int64, error) { - if res, err := DBExec("UPDATE claim_descriptions SET id_assignee = ?, content = ?, date = ? WHERE id_description = ?", d.IdAssignee, d.Content, d.Date, d.Id); err != nil { + if res, err := DBExec("UPDATE claim_descriptions SET id_assignee = ?, content = ?, date = ?, publish = ? WHERE id_description = ?", d.IdAssignee, d.Content, d.Date, d.Publish, d.Id); err != nil { return 0, err } else if nb, err := res.RowsAffected(); err != nil { return 0, err @@ -335,3 +343,67 @@ func (c Claim) GetAssignee() (*ClaimAssignee, error) { func (c Claim) SetAssignee(a ClaimAssignee) { c.IdAssignee = &a.Id } + +type teamIssueText struct { + Content string `json:"cnt"` + Assignee string `json:"assignee"` + Date time.Time `json:"date"` +} + +type teamIssueFile struct { + Subject string `json:"subject"` + Exercice *string `json:"exercice,omitempty"` + Assignee *string `json:"assignee,omitempty"` + State string `json:"state"` + Priority string `json:"priority"` + Texts []teamIssueText `json:"texts"` +} + +func (t Team) MyIssueFile() (ret []teamIssueFile, err error) { + var claims []Claim + if claims, err = t.GetClaims(); err == nil { + for _, claim := range claims { + var exercice *string = nil + + if exo, err := claim.GetExercice(); err == nil && exo != nil { + exercice = &exo.Title + } + + var assignee *string = nil + if a, err := claim.GetAssignee(); err == nil && a != nil { + assignee = &a.Name + } + + if descriptions, err := claim.GetDescriptions(); err != nil { + return nil, err + } else { + tif := teamIssueFile{ + Subject: claim.Subject, + Exercice: exercice, + Assignee: assignee, + State: claim.State, + Priority: claim.Priority, + Texts: []teamIssueText{}, + } + + for _, description := range descriptions { + if description.Publish { + if people, err := description.GetAssignee(); err != nil { + return nil, err + } else { + tif.Texts = append(tif.Texts, teamIssueText{ + Content: description.Content, + Assignee: people.Name, + Date: description.Date, + }) + } + } + } + + ret = append(ret, tif) + } + } + } + + return +}