frontend: add players possibility to report problems with exercices

This commit is contained in:
nemunaire 2020-01-20 15:56:02 +01:00
parent 32dc9c1a8c
commit 9186bbc229
13 changed files with 206 additions and 0 deletions

View File

@ -179,6 +179,7 @@ func main() {
AllowRegistration: false,
CanJoinTeam: false,
DenyNameChange: false,
AcceptNewIssue: true,
EnableResolutionRoute: false,
PartialValidation: true,
UnlockedChallengeDepth: 0,

View File

@ -128,6 +128,13 @@
</label>
</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">
<label class="custom-control custom-checkbox">
<input class="custom-control-input" type="checkbox" ng-model="config.enableResolutionRoute">

57
backend/issue.go Normal file
View 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)
}
}
}
}
}

View File

@ -186,6 +186,8 @@ func treat(raw_path string) {
switch spath[2] {
case "name":
treatRename(raw_path, team)
case "issue":
treatIssue(raw_path, team)
case "hint":
treatOpeningHint(raw_path, team)
case "choices":

View File

@ -86,6 +86,11 @@ server {
rewrite ^/.*$ /index.html;
}
location /issue {
include fic-auth.conf;
rewrite ^/.*$ /index.html;
}
location /rank {
include fic-auth.conf;
@ -167,6 +172,15 @@ server {
proxy_set_header X-Forwarded-For $remote_addr;
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 {
include fic-auth.conf;

View File

@ -78,6 +78,11 @@ server {
rewrite ^/.*$ /index.html;
}
location /issue {
include fic-auth.conf;
rewrite ^/.*$ /index.html;
}
location /rank {
include fic-auth.conf;
@ -159,6 +164,15 @@ server {
proxy_set_header X-Forwarded-For $remote_addr;
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 {
include fic-auth.conf;

21
frontend/issue.go Normal file
View 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)
}
}

View File

@ -47,6 +47,7 @@ func main() {
// 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/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/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}))

View File

@ -57,6 +57,14 @@ angular.module("FICApp", ["ngRoute", "ngSanitize"])
controller: "MyTeamController",
templateUrl: "views/team-edit.html"
})
.when("/issue", {
controller: "IssueController",
templateUrl: "views/issue.html"
})
.when("/issue/:eid", {
controller: "IssueController",
templateUrl: "views/issue.html"
})
.when("/rank", {
controller: "RankController",
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) {
$rootScope.current_theme = 0;
$rootScope.current_exercice = 0;

View File

@ -24,6 +24,7 @@
<p class="text-justify" ng-bind-html="themes[current_theme].intro"></p>
</div>
<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>
<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>

View File

@ -29,6 +29,7 @@
<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>
<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>
<li><strong>Gain&nbsp;:</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&nbsp;:</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>

View 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>

View File

@ -55,6 +55,8 @@ type FICSettings struct {
CanJoinTeam bool `json:"canJoinTeam"`
// DenyNameChange disallow Team to change their name.
DenyNameChange bool `json:"denyNameChange"`
// AcceptNewIssue enables the reporting system.
AcceptNewIssue bool `json:"acceptNewIssue"`
// EnableResolutionRoute activates the route displaying resolution movies.
EnableResolutionRoute bool `json:"enableResolutionRoute"`
// PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try.