[admin] Improve interface

This commit is contained in:
nemunaire 2016-10-13 19:52:54 +02:00
parent 24cbed411e
commit c0c6762313
7 changed files with 420 additions and 13 deletions

BIN
admin/static/img/epita.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
admin/static/img/fic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
admin/static/img/srs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -2,15 +2,40 @@
<html ng-app="FICApp"> <html ng-app="FICApp">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Challenge Forensic FIC 2016 - Administration</title> <title>Challenge Forensic - Administration</title>
<link href="/css/bootstrap.min.css" rel="stylesheet"> <link href="/css/bootstrap.min.css" rel="stylesheet">
<base href="/"> <base href="/">
<script src="//d3js.org/d3.v3.min.js"></script>
</head> </head>
<body> <body>
<div class="container"> <nav class="navbar navbar-inverse navbar-static-top">
<div class="page-header"> <div class="container">
<h1>Challenge FIC Epita!</h1>
<div class="navbar-header">
<a class="navbar-brand" href="/">
<img alt="FIC" src="img/fic.png" style="height: 100%">
</a>
</div>
<ul class="nav navbar-nav">
<li><a href="/teams">&Eacute;quipes</a></li>
<li><a href="/themes">Thèmes</a></li>
<li><a href="/exercices">Exercices</a></li>
<li><a href="/events">&Eacute;vénements</a></li>
</ul>
<p id="clock" class="navbar-text navbar-right" ng-controller="CountdownController">
<span id="hours">{{ time.hours | time }}</span>
<span class="point">:</span>
<span id="min">{{ time.minutes | time }}</span>
<span class="point">:</span>
<span id="sec">{{ time.seconds | time }}</span>
</p>
</div> </div>
</nav>
<div class="container">
<div class="row"> <div class="row">
<div class="col-sm-12" ng-view></div> <div class="col-sm-12" ng-view></div>
</div> </div>

View File

@ -17,34 +17,89 @@ angular.module("FICApp", ["ngRoute", "ngResource"])
controller: "TeamsListController", controller: "TeamsListController",
templateUrl: "views/team-list.html" templateUrl: "views/team-list.html"
}) })
.when("/teams/:teamId", {
controller: "TeamController",
templateUrl: "views/team.html"
})
.when("/teams/new", { .when("/teams/new", {
controller: "TeamNewController", controller: "TeamNewController",
templateUrl: "views/team-new.html" templateUrl: "views/team-new.html"
})
.when("/", {
templateUrl: "views/home.html"
}); });
$locationProvider.html5Mode(true); $locationProvider.html5Mode(true);
}); });
angular.module("FICApp") angular.module("FICApp")
.factory("Version", function($resource) {
return $resource("/api/version")
})
.factory("Team", function($resource) { .factory("Team", function($resource) {
return $resource("/api/teams/:teamId", { teamId: '@id' }, { return $resource("/api/teams/:teamId", { teamId: '@id' }, {
'save': {method: 'PATCH'}, 'save': {method: 'PATCH'},
}) })
}); })
angular.module("FICApp") .factory("TeamMember", function($resource) {
return $resource("/api/teams/:teamId/members", { teamId: '@id' })
})
.factory("TeamMy", function($resource) {
return $resource("/api/teams/:teamId/my.json", { teamId: '@id' })
})
.factory("Teams", function($resource) {
return $resource("/api/teams/teams.json")
})
.factory("TeamStats", function($resource) {
return $resource("/api/teams/:teamId/stats.json", { teamId: '@id' })
})
.factory("TeamPresence", function($resource) {
return $resource("/api/teams/:teamId/tries", { teamId: '@id' })
})
.factory("Theme", function($resource) { .factory("Theme", function($resource) {
return $resource("/api/themes/:themeId", null, { return $resource("/api/themes/:themeId", null, {
'save': {method: 'PATCH'}, 'save': {method: 'PATCH'},
}) })
}); })
angular.module("FICApp") .factory("Themes", function($resource) {
.factory("Exercice", function($resource) { return $resource("/api/themes/themes.json", null, {
return $resource("/api/themes/:themeId/:exerciceId", null, { 'get': {method: 'GET'},
'query': {method: "GET", url: "/api/themes/:themeId/exercices", isArray: true},
'save': {method: 'PATCH'},
}) })
})
.factory("Exercice", function($resource) {
return $resource("/api/exercices/:exerciceId")
}); });
String.prototype.capitalize = function() {
return this
.toLowerCase()
.replace(
/(^|\s)([a-z])/g,
function(m,p1,p2) { return p1+p2.toUpperCase(); }
);
}
angular.module("FICApp") angular.module("FICApp")
.filter("capitalize", function() {
return function(input) {
return input.capitalize();
}
})
.filter("time", function() {
return function(input) {
if (input == undefined) {
return "--";
} else if (input >= 10) {
return input;
} else {
return "0" + input;
}
}
})
.controller("VersionController", function($scope, Version) {
$scope.v = Version.get();
})
.controller("ThemesListController", function($scope, Theme, $location) { .controller("ThemesListController", function($scope, Theme, $location) {
$scope.themes = Theme.query(); $scope.themes = Theme.query();
$scope.fields = ["id", "name"]; $scope.fields = ["id", "name"];
@ -81,14 +136,278 @@ angular.module("FICApp")
.controller("TeamsListController", function($scope, Team, $location) { .controller("TeamsListController", function($scope, Team, $location) {
$scope.teams = Team.query(); $scope.teams = Team.query();
$scope.fields = ["id", "name"]; $scope.fields = ["id", "name", "initialName"];
$scope.show = function(id) { $scope.show = function(id) {
$location.url("/teams/" + id); $location.url("/teams/" + id);
}; };
}) })
.controller("TeamController", function($scope, Team, TeamMember, $routeParams) {
$scope.team = Team.get({ teamId: $routeParams.teamId });
$scope.members = TeamMember.query({ teamId: $routeParams.teamId });
})
.controller("TeamStatsController", function($scope, TeamStats, $routeParams) {
$scope.teamstats = TeamStats.get({ teamId: $routeParams.teamId });
$scope.teamstats.$promise.then(function(res) {
solvedByLevelPie("#pieLevels", res.levels);
solvedByThemesPie("#pieThemes", res.themes);
});
})
.controller("TeamExercicesController", function($scope, Teams, Themes, TeamMy, Exercice, $routeParams) {
$scope.teams = Teams.get();
$scope.themes = Themes.get();
$scope.exercices = Exercice.query();
$scope.my = TeamMy.get({ teamId: $routeParams.teamId });
$scope.teams.$promise.then(function(res){
$scope.nb_teams = 0;
$scope.nb_reg_teams = Object.keys(res).length;
angular.forEach(res, function(team, tid) {
if (team.rank)
$scope.nb_teams += 1;
}, 0);
});
$scope.my.$promise.then(function(res){
$scope.solved_exercices = 0;
angular.forEach(res.exercices, function(exercice, eid) {
if (exercice.solved) {
$scope.solved_exercices += 1;
}
}, 0);
});
})
.controller("TeamNewController", function($scope, Team, $location) { .controller("TeamNewController", function($scope, Team, $location) {
$scope.contact = new Team({ $scope.contact = new Team({
}) })
})
.controller("PresenceController", function($scope, TeamPresence, $routeParams) {
$scope.presence = TeamPresence.query({ teamId: $routeParams.teamId });
$scope.presence.$promise.then(function(res) {
presenceCal("#presenceCal", res);
});
})
.controller("CountdownController", function($scope, $http, $timeout) {
$scope.time = {};
function updTime() {
$timeout.cancel($scope.cbm);
$scope.cbm = $timeout(updTime, 1000);
if (sessionStorage.userService) {
var time = angular.fromJson(sessionStorage.userService);
var srv_cur = (Date.now() + (time.cu * 1000 - time.he)) / 1000;
var remain = time.du;
if (time.st == Math.floor(srv_cur)) {
$scope.refresh(true);
}
if (time.st > 0 && time.st <= srv_cur) {
remain = time.st + time.du - srv_cur;
}
if (remain < 0) {
remain = 0;
$scope.time.end = true;
$scope.time.expired = true;
} else if (remain < 60) {
$scope.time.end = false;
$scope.time.expired = true;
} else {
$scope.time.end = false;
$scope.time.expired = false;
}
$scope.time.start = time.st * 1000;
$scope.time.duration = time.du;
$scope.time.remaining = remain;
$scope.time.hours = Math.floor(remain / 3600);
$scope.time.minutes = Math.floor((remain % 3600) / 60);
$scope.time.seconds = Math.floor(remain % 60);
}
}
$http.get("/time.json").success(function(time) {
time.he = (new Date()).getTime();
sessionStorage.userService = angular.toJson(time);
updTime();
});
}); });
function solvedByLevelPie(location, data) {
var width = d3.select(location).node().getBoundingClientRect().width - 10,
height = d3.select(location).node().getBoundingClientRect().width - 10,
radius = Math.min(width, height) / 2,
innerRadius = 0.1 * radius;
var color = d3.scale.ordinal()
.range(["#9E0041", "#C32F4B", "#E1514B", "#F47245", "#FB9F59", "#FEC574", "#FAE38C", "#EAD195", "#C7E89E", "#9CD6A4", "#6CC4A4", "#4D9DB4", "#4776B4", "#5E4EA1"]);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.width; });
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(function (d) {
return (radius - innerRadius) * (d.data.score / 100.0) + innerRadius;
});
var outlineArc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(radius);
var svg = d3.select(location).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
data.forEach(function(d) {
d.score = d.solved * 100 / d.total;
d.width = d.tries + 1;
});
var path = svg.selectAll(".solidArc")
.data(pie(data))
.enter().append("path")
.attr("fill", function(d) { return color(d.data.tip); })
.attr("class", "solidArc")
.attr("stroke", "gray")
.attr("d", arc);
var outerPath = svg.selectAll(".outlineArc")
.data(pie(data))
.enter().append("path")
.attr("fill", "none")
.attr("stroke", "gray")
.attr("class", "outlineArc")
.attr("d", outlineArc);
var labelArc = d3.svg.arc()
.outerRadius(0.8 * radius)
.innerRadius(0.8 * radius);
svg.selectAll(".labelArc")
.data(pie(data))
.enter().append("text")
.attr("transform", function(d) { return "translate(" + labelArc.centroid(d) + ")"; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.data.tip + ": " + d.data.solved + "/" + d.data.total; });
svg.selectAll(".label2Arc")
.data(pie(data))
.enter().append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.data.tries; });
}
function solvedByThemesPie(location, data) {
var width = d3.select(location).node().getBoundingClientRect().width,
height = d3.select(location).node().getBoundingClientRect().width,
radius = Math.min(width, height) / 2,
innerRadius = 0.1 * radius;
var color = d3.scale.ordinal()
.range(["#9E0041", "#C32F4B", "#E1514B", "#F47245", "#FB9F59", "#FEC574", "#FAE38C", "#EAD195", "#C7E89E", "#9CD6A4", "#6CC4A4", "#4D9DB4", "#4776B4", "#5E4EA1"]);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.width; });
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(function (d) {
return (radius - innerRadius) * (d.data.score / 100.0) + innerRadius;
});
var outlineArc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(radius);
var svg = d3.select(location).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
data.forEach(function(d) {
d.score = d.solved * 100 / d.total;
d.width = d.tries + 0.5;
});
var path = svg.selectAll(".solidArc")
.data(pie(data))
.enter().append("path")
.attr("fill", function(d) { return color(d.data.tip); })
.attr("class", "solidArc")
.attr("stroke", "gray")
.attr("d", arc);
var outerPath = svg.selectAll(".outlineArc")
.data(pie(data))
.enter().append("path")
.attr("fill", "none")
.attr("stroke", "gray")
.attr("class", "outlineArc")
.attr("d", outlineArc);
svg.selectAll(".label2Arc")
.data(pie(data))
.enter().append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.data.solved; });
var labelArc = d3.svg.arc()
.outerRadius(0.8 * radius)
.innerRadius(0.8 * radius);
svg.selectAll(".labelArc")
.data(pie(data))
.enter().append("text")
.attr("transform", function(d) { return "translate(" + labelArc.centroid(d) + ")"; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.data.tip + ": " + d.data.tries; });
}
function presenceCal(location, data) {
var width = d3.select(location).node().getBoundingClientRect().width,
height = 80,
cellSize = 17; // cell size
var percent = d3.format(".1%"),
format = d3.time.format("%H:%M");
var color = d3.scale.quantize()
.domain([0, 16])
.range(d3.range(8).map(function(d) { return "q" + d + "-8"; }));
var svg = d3.select(location).selectAll("svg")
.data(d3.range(26, 29))
.enter().append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "RdYlGn")
.append("g")
.attr("transform", "translate(" + ((width - cellSize * 24) / 2) + "," + (height - cellSize * 4 - 1) + ")");
svg.append("text")
.attr("transform", "translate(-6," + cellSize * 2.6 + ")rotate(-90)")
.style("text-anchor", "middle")
.text(function(d) { return d + "-02"; });
var rect = svg.selectAll(".quarter")
.data(function(d) { return d3.time.minutes(new Date(2016, 1, d, 0), new Date(2016, 1, d, 24), 15); })
.enter().append("rect")
.attr("width", cellSize)
.attr("height", cellSize)
.attr("class", function(d) { return color(data.reduce(function(prev, cur){
cur = new Date(cur).getTime();
dv = d.getTime();
return prev + ((dv <= cur && cur < dv+15*60000)?1:0);
}, 0)); });
}

View File

@ -0,0 +1,9 @@
<div class="well well-lg">
<h3>Interface d'administration du challenge</h3>
<p>
Sélectionnez une action dans le menu ci-dessus.
</p>
<p ng-controller="VersionController">
Version de l'API : {{ v.version }}
</p>
</div>

View File

@ -0,0 +1,54 @@
<style>
.RdYlGn .q0-8{fill:rgb(245,250,250)}
.RdYlGn .q1-8{fill:rgb(190,200,200)}
.RdYlGn .q2-8{fill:rgb(170,180,180)}
.RdYlGn .q3-8{fill:rgb(150,160,160)}
.RdYlGn .q4-8{fill:rgb(130,140,140)}
.RdYlGn .q5-8{fill:rgb(110,120,120)}
.RdYlGn .q6-8{fill:rgb(90,100,100)}
.RdYlGn .q7-8{fill:rgb(70,80,80)}
</style>
<h1>{{ team.name }}<span ng-show="team.name != team.initialName"> ({{ team.initialName}})</span> <small><span ng-repeat="member in members"><span ng-show="$last && !$first"> et </span><span ng-show="$middle">, </span>{{ member.firstname | capitalize }} <em ng-show="member.nickname">{{ member.nickname }}</em> {{ member.lastname | capitalize }}</span></small></h1>
<div ng-controller="TeamExercicesController">
<dl class="dl-horizontal">
<dt>Points</dt>
<dd>{{ my.score }}</dd>
<dt>Classement</dt>
<dd>{{ teams[my.team_id].rank }}/{{ nb_teams }} ({{ nb_reg_teams }} registered teams)</dd>
</dl>
<h2>Présence</h2>
<div id="presenceCal" ng-controller="PresenceController">
</div>
<h2>Exercices résolus : {{ solved_exercices }}/{{ exercices.length }} {{ solved_exercices * 100 / exercices.length | number:0 }}%</h2>
<dl>
<div style="float: left;padding: 0 5px; margin: 5px; border: 1px solid #ccc; border-radius: 3px; min-width: 5vw" ng-repeat="(tid,theme) in themes" class="text-center">
<dt>{{ theme.name }}</dt>
<dd>
<ul class="list-unstyled">
<li ng-repeat="(eid,exercice) in theme.exercices" ng-show="my.exercices[eid] && my.exercices[eid].solved"><a href="https://fic.srs.epita.fr/{{ my.exercices[eid].theme_id }}/{{ eid }}" target="_blank"><abbr title="{{ my.exercices[eid].statement }}">{{ exercice.title }}</abbr></a> (<abbr title="{{ my.exercices[eid].solved_time | date:'mediumDate' }} à {{ my.exercices[eid].solved_time | date:'mediumTime' }}">{{ my.exercices[eid].solved_number }}<sup>e</sup></abbr>)</li>
</ul>
</dd>
</div>
</dl>
<div class="clearfix"></div>
<div class="container" ng-controller="TeamStatsController">
<div class="row">
<div class="col-sm-6" id="pieLevels">
<h4 class="text-center">Tentatives par niveaux</h4>
</div>
<div class="col-sm-6" id="pieThemes">
<h4 class="text-center">Tentatives par thèmes</h4>
</div>
</div>
</div>
</div>