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 @@
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é par |
+ Messages |
+
+
+
+
+ {{ issue.subject }} (challenge {{ issue.exercice }}) |
+ {{ issue.state }} / {{ issue.priority }} |
+ {{ issue.assignee }} |
+
+
+ Vous
+ à {{ text.date | date:"mediumTime" }} :
+ {{ text.cnt }}
+
+ |
+
+
+
+
+
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
+}