frontend: add players possibility to report problems with exercices
This commit is contained in:
parent
32dc9c1a8c
commit
9186bbc229
13 changed files with 206 additions and 0 deletions
|
@ -179,6 +179,7 @@ func main() {
|
||||||
AllowRegistration: false,
|
AllowRegistration: false,
|
||||||
CanJoinTeam: false,
|
CanJoinTeam: false,
|
||||||
DenyNameChange: false,
|
DenyNameChange: false,
|
||||||
|
AcceptNewIssue: true,
|
||||||
EnableResolutionRoute: false,
|
EnableResolutionRoute: false,
|
||||||
PartialValidation: true,
|
PartialValidation: true,
|
||||||
UnlockedChallengeDepth: 0,
|
UnlockedChallengeDepth: 0,
|
||||||
|
|
|
@ -128,6 +128,13 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="custom-control custom-checkbox">
|
||||||
|
<input class="custom-control-input" type="checkbox" ng-model="config.acceptNewIssue">
|
||||||
|
<span class="custom-control-label">Activer le rapport d'anomalies par les équipes</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<label class="custom-control custom-checkbox">
|
<label class="custom-control custom-checkbox">
|
||||||
<input class="custom-control-input" type="checkbox" ng-model="config.enableResolutionRoute">
|
<input class="custom-control-input" type="checkbox" ng-model="config.enableResolutionRoute">
|
||||||
|
|
57
backend/issue.go
Normal file
57
backend/issue.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"srs.epita.fr/fic-server/libfic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IssueUpload struct {
|
||||||
|
IdExercice int64 `json:"id_exercice"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func treatIssue(pathname string, team fic.Team) {
|
||||||
|
// Generate a unique identifier to follow the request in logs
|
||||||
|
bid := make([]byte, 5)
|
||||||
|
binary.LittleEndian.PutUint32(bid, rand.Uint32())
|
||||||
|
id := "[" + base64.StdEncoding.EncodeToString(bid) + "]"
|
||||||
|
log.Println(id, "New issue receive", pathname)
|
||||||
|
|
||||||
|
var issue IssueUpload
|
||||||
|
|
||||||
|
if cnt_raw, err := ioutil.ReadFile(pathname); err != nil {
|
||||||
|
log.Printf("%s [ERR] %s\n", id, err)
|
||||||
|
} else if err := json.Unmarshal(cnt_raw, &issue); err != nil {
|
||||||
|
log.Printf("%s [ERR] %s\n", id, err)
|
||||||
|
} else if len(issue.Subject) == 0 && len(issue.Description) == 0 {
|
||||||
|
log.Printf("%s Empty issue: not treated.\n", id)
|
||||||
|
} else if len(issue.Subject) == 0 {
|
||||||
|
log.Printf("%s Issue with no subject: not treated.\n", id)
|
||||||
|
} else {
|
||||||
|
var exercice *fic.Exercice = nil
|
||||||
|
if e, err := fic.GetExercice(issue.IdExercice); err == nil {
|
||||||
|
exercice = &e
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
if err = os.Remove(pathname); err != nil {
|
||||||
|
log.Printf("%s [ERR] %s\n", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -186,6 +186,8 @@ func treat(raw_path string) {
|
||||||
switch spath[2] {
|
switch spath[2] {
|
||||||
case "name":
|
case "name":
|
||||||
treatRename(raw_path, team)
|
treatRename(raw_path, team)
|
||||||
|
case "issue":
|
||||||
|
treatIssue(raw_path, team)
|
||||||
case "hint":
|
case "hint":
|
||||||
treatOpeningHint(raw_path, team)
|
treatOpeningHint(raw_path, team)
|
||||||
case "choices":
|
case "choices":
|
||||||
|
|
|
@ -86,6 +86,11 @@ server {
|
||||||
|
|
||||||
rewrite ^/.*$ /index.html;
|
rewrite ^/.*$ /index.html;
|
||||||
}
|
}
|
||||||
|
location /issue {
|
||||||
|
include fic-auth.conf;
|
||||||
|
|
||||||
|
rewrite ^/.*$ /index.html;
|
||||||
|
}
|
||||||
location /rank {
|
location /rank {
|
||||||
include fic-auth.conf;
|
include fic-auth.conf;
|
||||||
|
|
||||||
|
@ -167,6 +172,15 @@ server {
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
}
|
}
|
||||||
|
location /submit/issue {
|
||||||
|
include fic-auth.conf;
|
||||||
|
|
||||||
|
rewrite ^/submit/.*$ /issue/$team break;
|
||||||
|
|
||||||
|
proxy_pass http://frontend:8080/;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
location /submit/name {
|
location /submit/name {
|
||||||
include fic-auth.conf;
|
include fic-auth.conf;
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,11 @@ server {
|
||||||
|
|
||||||
rewrite ^/.*$ /index.html;
|
rewrite ^/.*$ /index.html;
|
||||||
}
|
}
|
||||||
|
location /issue {
|
||||||
|
include fic-auth.conf;
|
||||||
|
|
||||||
|
rewrite ^/.*$ /index.html;
|
||||||
|
}
|
||||||
location /rank {
|
location /rank {
|
||||||
include fic-auth.conf;
|
include fic-auth.conf;
|
||||||
|
|
||||||
|
@ -159,6 +164,15 @@ server {
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
}
|
}
|
||||||
|
location /submit/issue {
|
||||||
|
include fic-auth.conf;
|
||||||
|
|
||||||
|
rewrite ^/submit/.*$ /issue/$team break;
|
||||||
|
|
||||||
|
proxy_pass http://frontend:8080/;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
location /submit/name {
|
location /submit/name {
|
||||||
include fic-auth.conf;
|
include fic-auth.conf;
|
||||||
|
|
||||||
|
|
21
frontend/issue.go
Normal file
21
frontend/issue.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
var acceptNewIssues bool = true
|
||||||
|
|
||||||
|
func IssueHandler(w http.ResponseWriter, r *http.Request, team string, sURL []string) {
|
||||||
|
if !acceptNewIssues {
|
||||||
|
log.Printf("UNHANDELED %s issue request from %s: %s [%s]\n", r.Method, r.RemoteAddr, r.URL.Path, r.UserAgent())
|
||||||
|
http.Error(w, "{\"errmsg\":\"Il n'est pas possible de rapporter d'anomalie.\"}", http.StatusForbidden)
|
||||||
|
} else if len(sURL) != 0 {
|
||||||
|
http.Error(w, "{\"errmsg\":\"Arguments manquants.\"}", http.StatusBadRequest)
|
||||||
|
} else if saveTeamFile(path.Join(team, "issue"), w, r) {
|
||||||
|
// File enqueued for backend treatment
|
||||||
|
http.Error(w, "{\"errmsg\":\"Anomalie signalée avec succès. Merci de votre patience...\"}", http.StatusAccepted)
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,6 +47,7 @@ func main() {
|
||||||
|
|
||||||
// Register handlers
|
// Register handlers
|
||||||
http.Handle(fmt.Sprintf("%s/chname/", *prefix), http.StripPrefix(fmt.Sprintf("%s/chname/", *prefix), submissionTeamChecker{"name change", ChNameHandler, *teamsDir}))
|
http.Handle(fmt.Sprintf("%s/chname/", *prefix), http.StripPrefix(fmt.Sprintf("%s/chname/", *prefix), submissionTeamChecker{"name change", ChNameHandler, *teamsDir}))
|
||||||
|
http.Handle(fmt.Sprintf("%s/issue/", *prefix), http.StripPrefix(fmt.Sprintf("%s/issue/", *prefix), submissionTeamChecker{"issue", IssueHandler, *teamsDir}))
|
||||||
http.Handle(fmt.Sprintf("%s/openhint/", *prefix), http.StripPrefix(fmt.Sprintf("%s/openhint/", *prefix), submissionTeamChecker{"opening hint", HintHandler, *teamsDir}))
|
http.Handle(fmt.Sprintf("%s/openhint/", *prefix), http.StripPrefix(fmt.Sprintf("%s/openhint/", *prefix), submissionTeamChecker{"opening hint", HintHandler, *teamsDir}))
|
||||||
http.Handle(fmt.Sprintf("%s/wantchoices/", *prefix), http.StripPrefix(fmt.Sprintf("%s/wantchoices/", *prefix), submissionTeamChecker{"wantint choices", WantChoicesHandler, *teamsDir}))
|
http.Handle(fmt.Sprintf("%s/wantchoices/", *prefix), http.StripPrefix(fmt.Sprintf("%s/wantchoices/", *prefix), submissionTeamChecker{"wantint choices", WantChoicesHandler, *teamsDir}))
|
||||||
http.Handle(fmt.Sprintf("%s/registration/", *prefix), http.StripPrefix(fmt.Sprintf("%s/registration/", *prefix), submissionChecker{"registration", RegistrationHandler}))
|
http.Handle(fmt.Sprintf("%s/registration/", *prefix), http.StripPrefix(fmt.Sprintf("%s/registration/", *prefix), submissionChecker{"registration", RegistrationHandler}))
|
||||||
|
|
|
@ -57,6 +57,14 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
|
||||||
controller: "MyTeamController",
|
controller: "MyTeamController",
|
||||||
templateUrl: "views/team-edit.html"
|
templateUrl: "views/team-edit.html"
|
||||||
})
|
})
|
||||||
|
.when("/issue", {
|
||||||
|
controller: "IssueController",
|
||||||
|
templateUrl: "views/issue.html"
|
||||||
|
})
|
||||||
|
.when("/issue/:eid", {
|
||||||
|
controller: "IssueController",
|
||||||
|
templateUrl: "views/issue.html"
|
||||||
|
})
|
||||||
.when("/rank", {
|
.when("/rank", {
|
||||||
controller: "RankController",
|
controller: "RankController",
|
||||||
templateUrl: "views/rank.html"
|
templateUrl: "views/rank.html"
|
||||||
|
@ -636,6 +644,42 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.controller("IssueController", function($scope, $http, $rootScope, $routeParams) {
|
||||||
|
$rootScope.current_tag = undefined;
|
||||||
|
$rootScope.current_exercice = $routeParams.eid;
|
||||||
|
|
||||||
|
$scope.issue = {
|
||||||
|
id_exercice: parseInt($routeParams.eid, 10),
|
||||||
|
subject: "",
|
||||||
|
description: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.isubmit = function() {
|
||||||
|
$rootScope.sberr = "";
|
||||||
|
if ($scope.issue.subject.length < 3) {
|
||||||
|
$rootScope.messageClass = {"text-danger": true};
|
||||||
|
$rootScope.sberr = "L'object de votre rapport d'anomalie est trop court !";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$http({
|
||||||
|
url: "submit/issue",
|
||||||
|
method: "POST",
|
||||||
|
data: $scope.issue
|
||||||
|
}).then(function(response) {
|
||||||
|
$rootScope.messageClass = {"text-success": true};
|
||||||
|
$rootScope.message = response.data.errmsg;
|
||||||
|
$scope.issue.subject = "";
|
||||||
|
$scope.issue.description = "";
|
||||||
|
}, function(response) {
|
||||||
|
$rootScope.messageClass = {"text-danger": true};
|
||||||
|
$rootScope.message = response.data.errmsg;
|
||||||
|
if (response.status != 402) {
|
||||||
|
$rootScope.sberr = "Une erreur est survenue lors de l'envoi. Veuillez réessayer dans quelques instants.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})
|
||||||
.controller("MyTeamController", function($scope, $http, $rootScope, $timeout, $location) {
|
.controller("MyTeamController", function($scope, $http, $rootScope, $timeout, $location) {
|
||||||
$rootScope.current_theme = 0;
|
$rootScope.current_theme = 0;
|
||||||
$rootScope.current_exercice = 0;
|
$rootScope.current_exercice = 0;
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
<p class="text-justify" ng-bind-html="themes[current_theme].intro"></p>
|
<p class="text-justify" ng-bind-html="themes[current_theme].intro"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="jumbotron niceborder text-indent mt-3" ng-if="(my.exercices[current_exercice])">
|
<div class="jumbotron niceborder text-indent mt-3" ng-if="(my.exercices[current_exercice])">
|
||||||
|
<a ng-if="settings.acceptNewIssue" class="float-right btn btn-sm btn-warning" ng-href="issue/{{ current_exercice }}"><span class="glyphicon glyphicon-warning-sign" aria-hidden="true"></span> Rapporter une anomalie sur cet exercice</a>
|
||||||
<h3 class="display-4">{{ themes[current_theme].exercices[current_exercice].title }}</h3>
|
<h3 class="display-4">{{ themes[current_theme].exercices[current_exercice].title }}</h3>
|
||||||
<a ng-href="tags/{{tag}}" class="badge badge-pill badge-secondary mr-2 mb-2" ng-repeat="tag in themes[current_theme].exercices[current_exercice].tags">#{{ tag }}</a>
|
<a ng-href="tags/{{tag}}" class="badge badge-pill badge-secondary mr-2 mb-2" ng-repeat="tag in themes[current_theme].exercices[current_exercice].tags">#{{ tag }}</a>
|
||||||
<p class="lead text-justify" ng-bind-html="my.exercices[current_exercice].statement"></p>
|
<p class="lead text-justify" ng-bind-html="my.exercices[current_exercice].statement"></p>
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
<p class="lead text-justify" ng-bind-html="my.exercices[current_exercice].statement"></p>
|
<p class="lead text-justify" ng-bind-html="my.exercices[current_exercice].statement"></p>
|
||||||
<div class="alert alert-{{my.exercices[current_exercice].issuekind}}" ng-if="my.exercices[current_exercice].issue" ng-bind-html="my.exercices[current_exercice].issue"></div>
|
<div class="alert alert-{{my.exercices[current_exercice].issuekind}}" ng-if="my.exercices[current_exercice].issue" ng-bind-html="my.exercices[current_exercice].issue"></div>
|
||||||
<hr class="my-3">
|
<hr class="my-3">
|
||||||
|
<a ng-if="settings.acceptNewIssue" class="float-right btn btn-sm btn-warning" ng-href="issue/{{ current_exercice }}"><span class="glyphicon glyphicon-warning-sign" aria-hidden="true"></span> Rapporter une anomalie sur cet exercice</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Gain :</strong> <ng-pluralize count="themes[current_theme].exercices[current_exercice].gain" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize> <em ng-if="settings.firstBlood && themes[current_theme].exercices[current_exercice].solved < 1">{{ 1 + settings.firstBlood | coeff }} prem's</em> <em ng-if="themes[current_theme].exercices[current_exercice].curcoeff != 1.0 || settings.exerciceCurrentCoefficient != 1.0">{{ themes[current_theme].exercices[current_exercice].curcoeff * settings.exerciceCurrentCoefficient | coeff }} bonus</em></li>
|
<li><strong>Gain :</strong> <ng-pluralize count="themes[current_theme].exercices[current_exercice].gain" when="{'one': '{} point', 'other': '{} points'}"></ng-pluralize> <em ng-if="settings.firstBlood && themes[current_theme].exercices[current_exercice].solved < 1">{{ 1 + settings.firstBlood | coeff }} prem's</em> <em ng-if="themes[current_theme].exercices[current_exercice].curcoeff != 1.0 || settings.exerciceCurrentCoefficient != 1.0">{{ themes[current_theme].exercices[current_exercice].curcoeff * settings.exerciceCurrentCoefficient | coeff }} bonus</em></li>
|
||||||
<li ng-if="themes[current_theme].exercices[current_exercice].tried"><strong>Tenté par :</strong> <ng-pluralize count="themes[current_theme].exercices[current_exercice].tried" when="{'0': 'aucune équipe', 'one': '{} équipe', 'other': '{} équipes'}"></ng-pluralize> <span ng-if="my.exercices[current_exercice].total_tries">(cumulant <ng-pluralize count="my.exercices[current_exercice].total_tries" when="{'one': '{} tentative', 'other': '{} tentatives'}"></ng-pluralize>)</span></li>
|
<li ng-if="themes[current_theme].exercices[current_exercice].tried"><strong>Tenté par :</strong> <ng-pluralize count="themes[current_theme].exercices[current_exercice].tried" when="{'0': 'aucune équipe', 'one': '{} équipe', 'other': '{} équipes'}"></ng-pluralize> <span ng-if="my.exercices[current_exercice].total_tries">(cumulant <ng-pluralize count="my.exercices[current_exercice].total_tries" when="{'one': '{} tentative', 'other': '{} tentatives'}"></ng-pluralize>)</span></li>
|
||||||
|
|
41
frontend/static/views/issue.html
Normal file
41
frontend/static/views/issue.html
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<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">
|
||||||
|
<p class="card-text">Rapprochez-vous d'un membre de l'équipe serveur afin d'obtenir de l'aide.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card border-warning mt-3 mb-4" ng-if="settings.acceptNewIssue">
|
||||||
|
<div class="card-header bg-warning text-light">Rapporter une anomalie <span ng-if="issue.id_exercice">sur un exercice</span></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p ng-class="messageClass" ng-if="message || sberr"><strong ng-if="!sberr">Votre rapport a bien été envoyé !</strong><strong ng-if="sberr">{{ sberr }}</strong> {{ message }}</p>
|
||||||
|
<form ng-submit="isubmit()">
|
||||||
|
|
||||||
|
<div class="form-group row" ng-if="issue.id_exercice">
|
||||||
|
<label for="idExercice" class="col-sm-2 col-form-label">Exercice</label>
|
||||||
|
<div class="col-sm-10" ng-if="current_theme">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="idExercice" value="{{ themes[current_theme].exercices[issue.id_exercice].title }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-10" ng-if="!current_theme">
|
||||||
|
<input type="text" readonly class="form-control-plaintext" id="idExercice" value="{{ issue.id_exercice }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="subject" class="col col-form-label">Objet</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" id="subject" ng-model="issue.subject" placeholder="Intitulé succinct">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="description" class="col col-form-label">Description</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<textarea class="form-control" id="description" ng-model="issue.description" placeholder="Décrivez en détail votre problème ici. Si nécessaire, incluez un lien vers une capture d'écran montrant votre problème."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="float-right btn btn-warning">Envoyer le rapport</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -55,6 +55,8 @@ type FICSettings struct {
|
||||||
CanJoinTeam bool `json:"canJoinTeam"`
|
CanJoinTeam bool `json:"canJoinTeam"`
|
||||||
// DenyNameChange disallow Team to change their name.
|
// DenyNameChange disallow Team to change their name.
|
||||||
DenyNameChange bool `json:"denyNameChange"`
|
DenyNameChange bool `json:"denyNameChange"`
|
||||||
|
// AcceptNewIssue enables the reporting system.
|
||||||
|
AcceptNewIssue bool `json:"acceptNewIssue"`
|
||||||
// EnableResolutionRoute activates the route displaying resolution movies.
|
// EnableResolutionRoute activates the route displaying resolution movies.
|
||||||
EnableResolutionRoute bool `json:"enableResolutionRoute"`
|
EnableResolutionRoute bool `json:"enableResolutionRoute"`
|
||||||
// PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try.
|
// PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try.
|
||||||
|
|
Reference in a new issue