frontend: display issues related to the team

This commit is contained in:
nemunaire 2020-01-23 16:03:31 +01:00
parent 7bec409ab8
commit a3ffdeae17
12 changed files with 238 additions and 12 deletions

View File

@ -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
}
}

View File

@ -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 {

View File

@ -62,6 +62,11 @@
</div>
<div ng-repeat="description in claim.descriptions | orderBy:'id':true" class="alert" ng-class="{'alert-info': '' + description.id_assignee != whoami, 'alert-dark': '' + description.id_assignee == whoami}">
<div class="float-right btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm" ng-class="{'btn-outline-secondary':!description.publish, 'btn-success':description.publish}">
<input type="checkbox" ng-model="description.publish" ng-change="updateDescription(description)"><i class="glyphicon" ng-class="{'glyphicon-eye-open':description.publish,'glyphicon-eye-close':!description.publish}"></i>
</label>
</div>
<strong>Par <span ng-repeat="assignee in assignees" ng-if="assignee.id == description.id_assignee">{{ assignee.name }}</span> le {{ description.date | date:"mediumTime" }} :</strong>
<span style="white-space: pre-line">{{ description.content }}</span>
</div>

View File

@ -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))

View File

@ -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)
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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"

View File

@ -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),

View File

@ -1,3 +1,31 @@
<div class="card niceborder border-warning bg-primary text-light" ng-if="issues.length > 0">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>Objet</th>
<th>État / Priorité</th>
<th>Géré par</th>
<th>Messages</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="issue in issues">
<td>{{ issue.subject }} <span ng-if="issue.exercice">(challenge {{ issue.exercice }})</span></td>
<td>{{ issue.state }} / {{ issue.priority }}</td>
<td>{{ issue.assignee }}</td>
<td>
<div class="row" ng-repeat="text in issue.texts | orderBy:'date':'reverse'">
<span ng-if="text.assignee == null || text.assignee == '$team'">Vous</span>
<span ng-if="text.assignee != null && text.assignee != '$team'" ng-bind="text.assignee"></span>&nbsp;à {{ text.date | date:"mediumTime" }}&nbsp;:&nbsp;
<span style="white-space: pre-line">{{ text.cnt }}</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="card border-warning mt-3" ng-if="!settings.acceptNewIssue">
<div class="card-header bg-warning text-light">Rapporter une anomalie sur un exercice</div>
<div class="card-body">

View File

@ -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;

View File

@ -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
}