admin: new interface to manage claims
This commit is contained in:
parent
3932bba83d
commit
1eef71923a
189
admin/api/claim.go
Normal file
189
admin/api/claim.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"srs.epita.fr/fic-server/libfic"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Tasks
|
||||||
|
router.GET("/api/claims/", apiHandler(getClaims))
|
||||||
|
router.POST("/api/claims/", apiHandler(newClaim))
|
||||||
|
router.DELETE("/api/claims/", apiHandler(clearClaims))
|
||||||
|
|
||||||
|
router.GET("/api/claims/:cid", apiHandler(claimHandler(showClaim)))
|
||||||
|
router.PUT("/api/claims/:cid", apiHandler(claimHandler(updateClaim)))
|
||||||
|
router.POST("/api/claims/:cid", apiHandler(claimHandler(addClaimDescription)))
|
||||||
|
router.DELETE("/api/claims/:cid", apiHandler(claimHandler(deleteClaim)))
|
||||||
|
|
||||||
|
// Assignees
|
||||||
|
router.GET("/api/claims-assignees/", apiHandler(getAssignees))
|
||||||
|
router.POST("/api/claims-assignees/", apiHandler(newAssignee))
|
||||||
|
|
||||||
|
router.GET("/api/claims-assignees/:aid", apiHandler(claimAssigneeHandler(showClaimAssignee)))
|
||||||
|
router.PUT("/api/claims-assignees/:aid", apiHandler(claimAssigneeHandler(updateClaimAssignee)))
|
||||||
|
router.DELETE("/api/claims-assignees/:aid", apiHandler(claimAssigneeHandler(deleteClaimAssignee)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClaims(_ httprouter.Params, _ []byte) (interface{}, error) {
|
||||||
|
return fic.GetClaims()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaimExported struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
IdTeam *int64 `json:"id_team"`
|
||||||
|
Team *fic.Team `json:"team"`
|
||||||
|
IdAssignee *int64 `json:"id_assignee"`
|
||||||
|
Assignee *fic.ClaimAssignee `json:"assignee"`
|
||||||
|
Creation time.Time `json:"creation"`
|
||||||
|
LastUpdate time.Time `json:"last_update"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Priority string `json:"priority"`
|
||||||
|
Descriptions []fic.ClaimDescription `json:"descriptions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func showClaim(claim fic.Claim, _ []byte) (interface{}, error) {
|
||||||
|
var e ClaimExported
|
||||||
|
var err error
|
||||||
|
if e.Team, err = claim.GetTeam(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if e.Assignee, err = claim.GetAssignee(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if e.Descriptions, err = claim.GetDescriptions(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.LastUpdate = e.Creation
|
||||||
|
for _, d := range e.Descriptions {
|
||||||
|
if d.Date.After(e.LastUpdate) {
|
||||||
|
e.LastUpdate = d.Date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Id = claim.Id
|
||||||
|
e.IdAssignee = claim.IdAssignee
|
||||||
|
e.IdTeam = claim.IdTeam
|
||||||
|
e.Subject = claim.Subject
|
||||||
|
e.Creation = claim.Creation
|
||||||
|
e.State = claim.State
|
||||||
|
e.Priority = claim.Priority
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClaimUploaded struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Team *int `json:"id_team"`
|
||||||
|
Assignee *int64 `json:"id_assignee"`
|
||||||
|
Priority string `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClaim(_ httprouter.Params, body []byte) (interface{}, error) {
|
||||||
|
var uc ClaimUploaded
|
||||||
|
if err := json.Unmarshal(body, &uc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var t *fic.Team
|
||||||
|
if uc.Team != nil {
|
||||||
|
if team, err := fic.GetTeam(*uc.Team); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
t = &team
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var a *fic.ClaimAssignee
|
||||||
|
if uc.Assignee != nil {
|
||||||
|
if assignee, err := fic.GetAssignee(*uc.Assignee); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
a = &assignee
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
a = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fic.NewClaim(uc.Subject, t, a, uc.Priority)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearClaims(_ httprouter.Params, _ []byte) (interface{}, error) {
|
||||||
|
return fic.ClearClaims()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func addClaimDescription(claim fic.Claim, body []byte) (interface{}, error) {
|
||||||
|
var ud fic.ClaimDescription
|
||||||
|
if err := json.Unmarshal(body, &ud); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if assignee, err := fic.GetAssignee(ud.IdAssignee); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return claim.AddDescription(ud.Content, assignee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateClaim(claim fic.Claim, body []byte) (interface{}, error) {
|
||||||
|
var uc fic.Claim
|
||||||
|
if err := json.Unmarshal(body, &uc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uc.Id = claim.Id
|
||||||
|
|
||||||
|
if _, err := uc.Update(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return uc, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteClaim(claim fic.Claim, _ []byte) (interface{}, error) {
|
||||||
|
return claim.Delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func getAssignees(_ httprouter.Params, _ []byte) (interface{}, error) {
|
||||||
|
return fic.GetAssignees()
|
||||||
|
}
|
||||||
|
|
||||||
|
func showClaimAssignee(assignee fic.ClaimAssignee, _ []byte) (interface{}, error) {
|
||||||
|
return assignee, nil
|
||||||
|
}
|
||||||
|
func newAssignee(_ httprouter.Params, body []byte) (interface{}, error) {
|
||||||
|
var ua fic.ClaimAssignee
|
||||||
|
if err := json.Unmarshal(body, &ua); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fic.NewClaimAssignee(ua.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateClaimAssignee(assignee fic.ClaimAssignee, body []byte) (interface{}, error) {
|
||||||
|
var ua fic.ClaimAssignee
|
||||||
|
if err := json.Unmarshal(body, &ua); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ua.Id = assignee.Id
|
||||||
|
|
||||||
|
if _, err := ua.Update(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return ua, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteClaimAssignee(assignee fic.ClaimAssignee, _ []byte) (interface{}, error) {
|
||||||
|
return assignee.Delete()
|
||||||
|
}
|
@ -248,6 +248,30 @@ func eventHandler(f func(fic.Event,[]byte) (interface{}, error)) func (httproute
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func claimHandler(f func(fic.Claim,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
|
||||||
|
return func (ps httprouter.Params, body []byte) (interface{}, error) {
|
||||||
|
if cid, err := strconv.Atoi(string(ps.ByName("cid"))); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if claim, err := fic.GetClaim(cid); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return f(claim, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func claimAssigneeHandler(f func(fic.ClaimAssignee,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
|
||||||
|
return func (ps httprouter.Params, body []byte) (interface{}, error) {
|
||||||
|
if aid, err := strconv.Atoi(string(ps.ByName("aid"))); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if assignee, err := fic.GetAssignee(int64(aid)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return f(assignee, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func fileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
|
func fileHandler(f func(fic.EFile,[]byte) (interface{}, error)) func (httprouter.Params,[]byte) (interface{}, error) {
|
||||||
return func (ps httprouter.Params, body []byte) (interface{}, error) {
|
return func (ps httprouter.Params, body []byte) (interface{}, error) {
|
||||||
if fileid, err := strconv.Atoi(string(ps.ByName("fileid"))); err != nil {
|
if fileid, err := strconv.Atoi(string(ps.ByName("fileid"))); err != nil {
|
||||||
|
@ -36,6 +36,7 @@ const indextpl = `<!DOCTYPE html>
|
|||||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}files">Fichiers</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}files">Fichiers</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}public/0">Public</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}public/0">Public</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}events">Événements</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}events">Événements</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}claims">Tâches</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}settings">Paramètres</a></li>
|
<li class="nav-item"><a class="nav-link" href="{{.urlbase}}settings">Paramètres</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,9 @@ func init() {
|
|||||||
api.Router().GET("/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
api.Router().GET("/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
||||||
})
|
})
|
||||||
|
api.Router().GET("/claims/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
||||||
|
})
|
||||||
api.Router().GET("/exercices/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
api.Router().GET("/exercices/*_", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
http.ServeFile(w, r, path.Join(StaticDir, "index.html"))
|
||||||
})
|
})
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
<li class="nav-item"><a class="nav-link" href="/files">Fichiers</a></li>
|
<li class="nav-item"><a class="nav-link" href="/files">Fichiers</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/public/0">Public</a></li>
|
<li class="nav-item"><a class="nav-link" href="/public/0">Public</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/events">Événements</a></li>
|
<li class="nav-item"><a class="nav-link" href="/events">Événements</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/claims">Tâches</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/settings">Paramètres</a></li>
|
<li class="nav-item"><a class="nav-link" href="/settings">Paramètres</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,12 +61,46 @@ angular.module("FICApp", ["ngRoute", "ngResource", "ngSanitize"])
|
|||||||
controller: "EventController",
|
controller: "EventController",
|
||||||
templateUrl: "views/event.html"
|
templateUrl: "views/event.html"
|
||||||
})
|
})
|
||||||
|
.when("/claims", {
|
||||||
|
controller: "ClaimsListController",
|
||||||
|
templateUrl: "views/claim-list.html"
|
||||||
|
})
|
||||||
|
.when("/claims/:claimId", {
|
||||||
|
controller: "ClaimController",
|
||||||
|
templateUrl: "views/claim.html"
|
||||||
|
})
|
||||||
.when("/", {
|
.when("/", {
|
||||||
templateUrl: "views/home.html"
|
templateUrl: "views/home.html"
|
||||||
});
|
});
|
||||||
$locationProvider.html5Mode(true);
|
$locationProvider.html5Mode(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setCookie(name, value, days) {
|
||||||
|
var expires;
|
||||||
|
|
||||||
|
if (days) {
|
||||||
|
var date = new Date();
|
||||||
|
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
|
expires = "; expires=" + date.toGMTString();
|
||||||
|
} else {
|
||||||
|
expires = "";
|
||||||
|
}
|
||||||
|
document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
var nameEQ = encodeURIComponent(name) + "=";
|
||||||
|
var ca = document.cookie.split(';');
|
||||||
|
for (var i = 0; i < ca.length; i++) {
|
||||||
|
var c = ca[i];
|
||||||
|
while (c.charAt(0) === ' ')
|
||||||
|
c = c.substring(1, c.length);
|
||||||
|
if (c.indexOf(nameEQ) === 0)
|
||||||
|
return decodeURIComponent(c.substring(nameEQ.length, c.length));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
angular.module("FICApp")
|
angular.module("FICApp")
|
||||||
.directive('autofocus', ['$timeout', function($timeout) {
|
.directive('autofocus', ['$timeout', function($timeout) {
|
||||||
return {
|
return {
|
||||||
@ -88,6 +122,16 @@ angular.module("FICApp")
|
|||||||
'update': {method: 'PUT'},
|
'update': {method: 'PUT'},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
.factory("Claim", function($resource) {
|
||||||
|
return $resource("/api/claims/:claimId", { claimId: '@id' }, {
|
||||||
|
'update': {method: 'PUT'},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.factory("ClaimAssignee", function($resource) {
|
||||||
|
return $resource("/api/claims-assignees/:assigneeId", { assigneeId: '@id' }, {
|
||||||
|
'update': {method: 'PUT'},
|
||||||
|
})
|
||||||
|
})
|
||||||
.factory("File", function($resource) {
|
.factory("File", function($resource) {
|
||||||
return $resource("/api/files/:fileId", { fileId: '@id' })
|
return $resource("/api/files/:fileId", { fileId: '@id' })
|
||||||
})
|
})
|
||||||
@ -616,6 +660,134 @@ angular.module("FICApp")
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
.controller("AssigneesListController", function($scope, ClaimAssignee, $location) {
|
||||||
|
$scope.assignees = ClaimAssignee.query();
|
||||||
|
|
||||||
|
$scope.setMyAId = function(aid) {
|
||||||
|
setCookie("myassignee", aid, 5);
|
||||||
|
$location.url("/claims/");
|
||||||
|
}
|
||||||
|
$scope.whoami = getCookie("myassignee");
|
||||||
|
$scope.newAssignee = function() {
|
||||||
|
$scope.assignees.push(new ClaimAssignee());
|
||||||
|
}
|
||||||
|
$scope.edit = function(a) {
|
||||||
|
a.edit = true;
|
||||||
|
}
|
||||||
|
$scope.updateAssignee = function(a) {
|
||||||
|
if (a.id) {
|
||||||
|
a.$update(function() { $location.url("/claims/");});
|
||||||
|
} else {
|
||||||
|
a.$save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scope.removeAssignee = function(a) {
|
||||||
|
a.$remove(function() { $location.url("/claims/");});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.controller("ClaimsListController", function($scope, Claim, ClaimAssignee, Teams, $location) {
|
||||||
|
$scope.claims = Claim.query();
|
||||||
|
$scope.assignees = ClaimAssignee.query();
|
||||||
|
$scope.whoami = getCookie("myassignee");
|
||||||
|
$scope.teams = Teams.get();
|
||||||
|
$scope.fields = ["id", "state", "subject", "creation", "id_team", "id_assignee"];
|
||||||
|
|
||||||
|
$scope.order = "priority";
|
||||||
|
$scope.chOrder = function(no) {
|
||||||
|
$scope.order = no;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.clearClaims = function(id) {
|
||||||
|
Claim.delete(function() {
|
||||||
|
$scope.claims = [];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
$scope.show = function(id) {
|
||||||
|
$location.url("/claims/" + id);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.controller("ClaimController", function($scope, Claim, ClaimAssignee, Teams, $routeParams, $location, $http) {
|
||||||
|
$scope.claim = Claim.get({ claimId: $routeParams.claimId }, function(v) {
|
||||||
|
v.id_team = "" + v.id_team;
|
||||||
|
if (!v.priority)
|
||||||
|
v.priority = "medium";
|
||||||
|
});
|
||||||
|
if ($routeParams.claimId == "new")
|
||||||
|
$scope.fields = ["id_team", "subject", "priority", "id_assignee"];
|
||||||
|
else
|
||||||
|
$scope.fields = ["state", "subject", "priority", "id_assignee", "id_team", "creation"];
|
||||||
|
$scope.assignees = ClaimAssignee.query();
|
||||||
|
$scope.whoami = Math.floor(getCookie("myassignee"));
|
||||||
|
$scope.teams = Teams.get();
|
||||||
|
$scope.namedFields = {
|
||||||
|
"subject": "Objet",
|
||||||
|
"id_assignee": "Assigné a",
|
||||||
|
"state": "État",
|
||||||
|
"id_team": "Équipe",
|
||||||
|
"creation": "Création",
|
||||||
|
"priority": "Priorité",
|
||||||
|
"description": "Description",
|
||||||
|
};
|
||||||
|
$scope.states = {
|
||||||
|
"new": "Nouveau",
|
||||||
|
"need-info": "Besoin d'infos",
|
||||||
|
"confirmed": "Confirmé",
|
||||||
|
"in-progress": "En cours",
|
||||||
|
"need-review": "Fait",
|
||||||
|
"closed": "Clos",
|
||||||
|
"invalid": "Invalide",
|
||||||
|
};
|
||||||
|
$scope.priorities = {
|
||||||
|
"low": "Basse",
|
||||||
|
"medium": "Moyenne",
|
||||||
|
"high": "Haute",
|
||||||
|
"critical": "Critique",
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.assignToMe = function() {
|
||||||
|
this.claim.id_assignee = $scope.whoami;
|
||||||
|
if (this.claim.id)
|
||||||
|
this.saveClaim();
|
||||||
|
}
|
||||||
|
$scope.saveDescription = function() {
|
||||||
|
$http({
|
||||||
|
url: "/api/claims/" + $scope.claim.id,
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
"id_assignee": $scope.whoami,
|
||||||
|
"content": $scope.ndescription
|
||||||
|
}
|
||||||
|
}).then(function() {
|
||||||
|
$location.url("/claims/" + $scope.claim.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$scope.saveClaim = function() {
|
||||||
|
if (this.claim.id_team) {
|
||||||
|
this.claim.id_team = Math.floor(this.claim.id_team);
|
||||||
|
} else {
|
||||||
|
this.claim.id_team = null;
|
||||||
|
}
|
||||||
|
if (this.claim.id) {
|
||||||
|
this.claim.$update(function(v) {
|
||||||
|
v.id_team = "" + v.id_team;
|
||||||
|
if ($scope.ndescription)
|
||||||
|
$scope.saveDescription();
|
||||||
|
else
|
||||||
|
$location.url("/claims/");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.claim.$save(function() {
|
||||||
|
if (!this.ndescription)
|
||||||
|
this.ndescription = "Création de la tâche";
|
||||||
|
$scope.saveDescription();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scope.deleteClaim = function() {
|
||||||
|
this.claim.$remove(function() { $location.url("/claims/");});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
.controller("ThemesListController", function($scope, Theme, $location, $rootScope, $http) {
|
.controller("ThemesListController", function($scope, Theme, $location, $rootScope, $http) {
|
||||||
$scope.themes = Theme.query();
|
$scope.themes = Theme.query();
|
||||||
$scope.fields = ["id", "name"];
|
$scope.fields = ["id", "name"];
|
||||||
|
61
admin/static/views/claim-list.html
Normal file
61
admin/static/views/claim-list.html
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<h2>
|
||||||
|
Tâches et réclammations ({{ claims.length }})
|
||||||
|
<button ng-click="show('new')" class="float-right btn btn-sm btn-primary" style="margin-right: 10px"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter une tâche</button>
|
||||||
|
<small style="height: 0px;">
|
||||||
|
<div class="checkbox float-right"><label><input type="checkbox" ng-model="showOnlyUnassigned"> Non assignée</label></div>
|
||||||
|
<div class="checkbox float-right"><label><input type="checkbox" ng-model="showOnlyMines" ng-show="whoami"> Que mes tâches</label></div>
|
||||||
|
<div class="checkbox float-right"><label><input type="checkbox" ng-model="showClosed"> Tâches closes</label></div>
|
||||||
|
</small>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p><input type="search" class="form-control" placeholder="Search" ng-model="query" autofocus></p>
|
||||||
|
<table class="table table-hover table-bordered table-striped table-sm">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th ng-repeat="field in fields" ng-click="chOrder(field)">
|
||||||
|
{{ field }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="claim in claims | filter: query | orderBy:order" ng-click="show(claim.id)" ng-class="{'bg-info': claim.priority == 'medium', 'bg-warning': claim.priority == 'high', 'bg-danger': claim.priority == 'critical'}" ng-if="(showClosed || (claim.state != 'closed' && claim.state != 'invalid')) && (!showOnlyMines || claim.id_assignee == whoami) && (!showOnlyUnassigned || !claim.id_assignee)">
|
||||||
|
<td ng-repeat="field in fields">
|
||||||
|
{{ claim[field] }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div ng-controller="AssigneesListController">
|
||||||
|
<h2>
|
||||||
|
Assignables à
|
||||||
|
<button ng-click="newAssignee()" class="float-right btn btn-sm btn-primary" style="margin-right: 10px"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter une personne</button>
|
||||||
|
</h2>
|
||||||
|
<table class="table table-hover table-bordered table-striped table-sm">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Nom</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="a in assignees" ng-click="edit(a)">
|
||||||
|
<td>
|
||||||
|
{{ a.id }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span ng-if="a.id && !a.edit">{{ a.name }}</span>
|
||||||
|
<input type="text" class="form-control form-control-sm" ng-model="a.name" ng-if="!a.id || a.edit">
|
||||||
|
</td>
|
||||||
|
<td style="width: 10%;">
|
||||||
|
<button class="btn btn-sm btn-info" ng-if="a.id" ng-click="setMyAId(a.id)" ng-class="{'disabled': whoami == a.id}"><span class="glyphicon glyphicon-user"></span></button>
|
||||||
|
<button class="btn btn-sm btn-danger" ng-if="a.id && !a.edit" ng-click="removeAssignee(a)"><span class="glyphicon glyphicon-remove"></span></button>
|
||||||
|
<button class="btn btn-sm btn-success" ng-if="!a.id || a.edit" ng-click="updateAssignee(a)"><span class="glyphicon glyphicon-ok"></span></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
36
admin/static/views/claim.html
Normal file
36
admin/static/views/claim.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<h2>Tâche</h2>
|
||||||
|
|
||||||
|
<form ng-submit="saveClaim()">
|
||||||
|
<div class="form-group row" ng-repeat="field in fields">
|
||||||
|
<label for="{{ field }}" class="col-sm-2 col-form-label text-right">{{ namedFields[field] }}</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="{{ field }}" ng-model="claim[field]" ng-if="field != 'state' && field != 'priority' && field != 'creation' && field != 'id_team' && field != 'id_assignee' && field != 'description'">
|
||||||
|
<input type="datetime" class="form-control" id="{{ field }}" ng-model="claim[field]" ng-if="field == 'creation' && claim.id">
|
||||||
|
<select class="custom-select" id="{{ field }}" ng-model="claim[field]" ng-options="k as v for (k, v) in states" ng-if="field == 'state'"></select>
|
||||||
|
<select class="custom-select" id="{{ field }}" ng-model="claim[field]" ng-options="k as v for (k, v) in priorities" ng-if="field == 'priority'"><option value="medium">Par défaut</option></select>
|
||||||
|
<select class="custom-select" id="{{ field }}" ng-model="claim[field]" ng-options="k as t.name for (k, t) in teams" ng-if="field == 'id_team' && !claim.id" autofocus><option></option></select>
|
||||||
|
<select class="custom-select" id="{{ field }}" ng-model="claim[field]" ng-options="k as t.name for (k, t) in teams" ng-if="field == 'id_team' && claim.id"><option></option></select>
|
||||||
|
<select class="custom-select" id="{{ field }}" ng-model="claim[field]" ng-options="a.id as a.name for a in assignees" ng-if="field == 'id_assignee'"><option></option></select> <button type="button" class="btn" ng-if="field == 'id_assignee' && whoami" ng-class="{'disabled': whoami == claim[field]}" ng-click="assignToMe()">Me l'assigner</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row" ng-show="whoami">
|
||||||
|
<label for="ndescription" class="col-sm-2 col-form-label text-right">Ajouter commentaire</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<textarea class="form-control" id="ndescription" ng-model="ndescription" autofocus></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right" ng-show="claim.id">
|
||||||
|
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-save" aria-hidden="true"></span> Save</button>
|
||||||
|
<button class="btn btn-danger" ng-click="deleteClaim()"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Delete</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-right" ng-show="!claim.id">
|
||||||
|
<button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Ajouter la tâche</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div ng-repeat="description in claim.descriptions" class="alert text-light" ng-class="{'alert-dark': '' + description.id_assignee != whoami, 'alert-info': '' + description.id_assignee == whoami}">
|
||||||
|
<strong>Par <em ng-if="!description.id_assignee">anonymous</em> <span ng-repeat="assignee in assignees" ng-if="assignee.id == description.id_assignee">{{ assignee.name }}</span> le {{ description.date | date:"mediumTime" }} :</strong>
|
||||||
|
{{ description.content }}
|
||||||
|
</div>
|
36
libfic/db.go
36
libfic/db.go
@ -229,6 +229,42 @@ CREATE TABLE IF NOT EXISTS team_hints(
|
|||||||
FOREIGN KEY(id_hint) REFERENCES exercice_hints(id_hint),
|
FOREIGN KEY(id_hint) REFERENCES exercice_hints(id_hint),
|
||||||
FOREIGN KEY(id_team) REFERENCES teams(id_team)
|
FOREIGN KEY(id_team) REFERENCES teams(id_team)
|
||||||
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS claim_assignees(
|
||||||
|
id_assignee INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(255) NOT NULL UNIQUE
|
||||||
|
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS claims(
|
||||||
|
id_claim INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
subject VARCHAR(255) NOT NULL,
|
||||||
|
id_team INTEGER,
|
||||||
|
id_assignee INTEGER,
|
||||||
|
creation TIMESTAMP NOT NULL,
|
||||||
|
state ENUM('new', 'need-info', 'confirmed', 'in-progress', 'need-review', 'closed', 'invalid') NOT NULL,
|
||||||
|
priority ENUM('low', 'medium', 'high', 'critical') NOT NULL,
|
||||||
|
FOREIGN KEY(id_assignee) REFERENCES claim_assignees(id_assignee),
|
||||||
|
FOREIGN KEY(id_team) REFERENCES teams(id_team)
|
||||||
|
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||||
|
`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.Exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS claim_descriptions(
|
||||||
|
id_description INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
id_claim INTEGER NOT NULL,
|
||||||
|
id_assignee INTEGER NOT NULL,
|
||||||
|
date TIMESTAMP NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(id_assignee) REFERENCES claim_assignees(id_assignee),
|
||||||
|
FOREIGN KEY(id_claim) REFERENCES claims(id_claim)
|
||||||
|
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
|
||||||
`); err != nil {
|
`); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
268
libfic/todo.go
Normal file
268
libfic/todo.go
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
package fic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Claim struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
IdTeam *int64 `json:"id_team"`
|
||||||
|
IdAssignee *int64 `json:"id_assignee"`
|
||||||
|
Creation time.Time `json:"creation"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Priority string `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClaim(id int) (c Claim, err error) {
|
||||||
|
err = DBQueryRow("SELECT id_claim, subject, id_team, id_assignee, creation, state, priority FROM claims WHERE id_claim = ?", id).Scan(&c.Id, &c.Subject, &c.IdTeam, &c.IdAssignee, &c.Creation, &c.State, &c.Priority)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClaims() (res []Claim, err error) {
|
||||||
|
var rows *sql.Rows
|
||||||
|
if rows, err = DBQuery("SELECT id_claim, subject, id_team, id_assignee, creation, state, priority FROM claims"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var c Claim
|
||||||
|
if err = rows.Scan(&c.Id, &c.Subject, &c.IdTeam, &c.IdAssignee, &c.Creation, &c.State, &c.Priority); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res = append(res, c)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Team) GetClaims() (res []Claim, err error) {
|
||||||
|
var rows *sql.Rows
|
||||||
|
if rows, err = DBQuery("SELECT id_claim, subject, IdTeam, id_assignee, creation, state, priority FROM claims WHERE IdTeam = ?", t.Id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var c Claim
|
||||||
|
if err = rows.Scan(&c.Id, &c.Subject, &c.IdTeam, &c.IdAssignee, &c.Creation, &c.State, &c.Priority); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res = append(res, c)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClaim(subject string, team *Team, assignee *ClaimAssignee, priority string) (Claim, error) {
|
||||||
|
var tid *int64
|
||||||
|
if team == nil {
|
||||||
|
tid = nil
|
||||||
|
} else {
|
||||||
|
tid = &team.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
var aid *int64
|
||||||
|
if assignee == nil {
|
||||||
|
aid = nil
|
||||||
|
} else {
|
||||||
|
aid = &assignee.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := DBExec("INSERT INTO claims (subject, id_team, id_assignee, creation, state, priority) VALUES (?, ?, ?, ?, ?, ?)", subject, tid, aid, time.Now(), "new", priority); err != nil {
|
||||||
|
return Claim{}, err
|
||||||
|
} else if cid, err := res.LastInsertId(); err != nil {
|
||||||
|
return Claim{}, err
|
||||||
|
} else {
|
||||||
|
return Claim{cid, subject, tid, aid, time.Now(), "new", priority}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Claim) GetTeam() (*Team, error) {
|
||||||
|
if c.IdTeam == nil {
|
||||||
|
return nil, nil
|
||||||
|
} else if t, err := GetTeam(int(*c.IdTeam)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return &t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Claim) SetTeam(t Team) {
|
||||||
|
c.IdTeam = &t.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Claim) Update() (int64, error) {
|
||||||
|
if res, err := DBExec("UPDATE claims SET subject = ?, id_team = ?, id_assignee = ?, creation = ?, state = ?, priority = ? WHERE id_claim = ?", c.Subject, c.IdTeam, c.IdAssignee, c.Creation, c.State, c.Priority, c.Id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Claim) Delete() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM claims WHERE id_claim = ?", c.Id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearClaims() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM claims"); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type ClaimDescription struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
IdAssignee int64 `json:"id_assignee"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res = append(res, d)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return ClaimDescription{}, err
|
||||||
|
} else if did, err := res.LastInsertId(); err != nil {
|
||||||
|
return ClaimDescription{}, err
|
||||||
|
} else {
|
||||||
|
return ClaimDescription{did, assignee.Id, content, time.Now()}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d ClaimDescription) Delete() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM claim_descriptions WHERE id_description = ?", d.Id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type ClaimAssignee struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAssignee(id int64) (a ClaimAssignee, err error) {
|
||||||
|
err = DBQueryRow("SELECT id_assignee, name FROM claim_assignees WHERE id_assignee = ?", id).Scan(&a.Id, &a.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAssignees() (res []ClaimAssignee, err error) {
|
||||||
|
var rows *sql.Rows
|
||||||
|
if rows, err = DBQuery("SELECT id_assignee, name FROM claim_assignees"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var a ClaimAssignee
|
||||||
|
if err = rows.Scan(&a.Id, &a.Name); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res = append(res, a)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClaimAssignee(name string) (ClaimAssignee, error) {
|
||||||
|
if res, err := DBExec("INSERT INTO claim_assignees (name) VALUES (?)", name); err != nil {
|
||||||
|
return ClaimAssignee{}, err
|
||||||
|
} else if aid, err := res.LastInsertId(); err != nil {
|
||||||
|
return ClaimAssignee{}, err
|
||||||
|
} else {
|
||||||
|
return ClaimAssignee{aid, name}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ClaimAssignee) Update() (int64, error) {
|
||||||
|
if res, err := DBExec("UPDATE claim_assignees SET name = ? WHERE id_assignee = ?", a.Name, a.Id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ClaimAssignee) Delete() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM claim_assignees WHERE id_assignee = ?", a.Id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearAssignees() (int64, error) {
|
||||||
|
if res, err := DBExec("DELETE FROM claim_assignees"); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else if nb, err := res.RowsAffected(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return nb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Claim) GetAssignee() (*ClaimAssignee, error) {
|
||||||
|
if c.IdAssignee == nil {
|
||||||
|
return nil, nil
|
||||||
|
} else if a, err := GetAssignee(*c.IdAssignee); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Claim) SetAssignee(a ClaimAssignee) {
|
||||||
|
c.IdAssignee = &a.Id
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user