merged master into disable_rfcs
This commit is contained in:
27
app/assets/javascripts/bootstrap-dropdown-submenu.js
vendored
Normal file
27
app/assets/javascripts/bootstrap-dropdown-submenu.js
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
$(document).ready(function () {
|
||||
|
||||
var subMenusSelector = 'ul.dropdown-menu [data-toggle=dropdown]';
|
||||
|
||||
function openSubMenu(event) {
|
||||
if (this.pathname === '/') {
|
||||
event.preventDefault();
|
||||
}
|
||||
event.stopPropagation();
|
||||
|
||||
$(subMenusSelector).parent().removeClass('open');
|
||||
$(this).parent().addClass('open');
|
||||
|
||||
var menu = $(this).parent().find("ul");
|
||||
var menupos = menu.offset();
|
||||
|
||||
var newPos;
|
||||
if ((menupos.left + menu.width()) + 30 > $(window).width()) {
|
||||
newPos = -menu.width();
|
||||
} else {
|
||||
newPos = $(this).parent().width();
|
||||
}
|
||||
menu.css({left: newPos});
|
||||
}
|
||||
|
||||
$(subMenusSelector).on('click', openSubMenu).on('mouseenter', openSubMenu);
|
||||
});
|
@ -23,10 +23,8 @@ $(function() {
|
||||
groups = new vis.DataSet(buildChartGroups());
|
||||
graph = new vis.Graph2d(document.getElementById('graph'), dataset, groups, {
|
||||
dataAxis: {
|
||||
customRange: {
|
||||
left: {
|
||||
min: 0
|
||||
}
|
||||
left: {
|
||||
range: {min: 0}
|
||||
},
|
||||
showMinorLabels: false
|
||||
},
|
||||
|
@ -476,6 +476,7 @@ configureEditors: function () {
|
||||
this.clearOutput();
|
||||
$('#hint').fadeOut();
|
||||
$('#flowrHint').fadeOut();
|
||||
this.clearHints();
|
||||
this.showOutputBar();
|
||||
},
|
||||
|
||||
@ -512,6 +513,30 @@ configureEditors: function () {
|
||||
}
|
||||
},
|
||||
|
||||
clearHints: function() {
|
||||
var container = $('#error-hints');
|
||||
container.find('ul.body > li.hint').remove();
|
||||
container.fadeOut();
|
||||
},
|
||||
|
||||
showHint: function(message) {
|
||||
var template = function(description, hint) {
|
||||
return '\
|
||||
<li class="hint">\
|
||||
<div class="description">\
|
||||
' + description + '\
|
||||
</div>\
|
||||
<div class="hint">\
|
||||
' + hint + '\
|
||||
</div>\
|
||||
</li>\
|
||||
'
|
||||
};
|
||||
var container = $('#error-hints');
|
||||
container.find('ul.body').append(template(message.description, message.hint));
|
||||
container.fadeIn();
|
||||
},
|
||||
|
||||
showContainerDepletedMessage: function() {
|
||||
$.flash.danger({
|
||||
icon: ['fa', 'fa-clock-o'],
|
||||
@ -527,6 +552,10 @@ configureEditors: function () {
|
||||
},
|
||||
|
||||
showWebsocketError: function() {
|
||||
if (window.navigator.userAgent.indexOf('Edge') > -1) {
|
||||
// Mute errors in Microsoft Edge
|
||||
return;
|
||||
}
|
||||
$.flash.danger({
|
||||
text: $('#flash').data('message-failure')
|
||||
});
|
||||
@ -692,4 +721,4 @@ configureEditors: function () {
|
||||
// create autosave when the editor is opened the first time
|
||||
this.autosave();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -4,10 +4,11 @@ CodeOceanEditorWebsocket = {
|
||||
createSocketUrl: function(url) {
|
||||
var sockURL = new URL(window.location);
|
||||
sockURL.pathname = url;
|
||||
sockURL.protocol = '<%= DockerClient.config['ws_client_protocol'] %>';
|
||||
// sanitize socket protocol string, strip trailing slash and other malicious chars if they are there
|
||||
sockURL.protocol = '<%= DockerClient.config['ws_client_protocol']&.match(/(\w+):*\/*/)&.to_a&.at(1) %>:';
|
||||
|
||||
// strip anchor if it is in the url
|
||||
sockURL.hash = ''
|
||||
sockURL.hash = '';
|
||||
|
||||
return sockURL.toString();
|
||||
},
|
||||
@ -30,6 +31,7 @@ CodeOceanEditorWebsocket = {
|
||||
initializeSocketForScoring: function(url) {
|
||||
this.initializeSocket(url);
|
||||
this.websocket.on('default',this.handleScoringResponse.bind(this));
|
||||
this.websocket.on('hint', this.showHint.bind(this));
|
||||
this.websocket.on('exit', this.handleExitCommand.bind(this));
|
||||
},
|
||||
|
||||
@ -43,6 +45,7 @@ CodeOceanEditorWebsocket = {
|
||||
this.websocket.on('exit', this.handleExitCommand.bind(this));
|
||||
this.websocket.on('timeout', this.showTimeoutMessage.bind(this));
|
||||
this.websocket.on('status', this.showStatus.bind(this));
|
||||
this.websocket.on('hint', this.showHint.bind(this));
|
||||
},
|
||||
|
||||
handleExitCommand: function() {
|
||||
@ -53,4 +56,4 @@ CodeOceanEditorWebsocket = {
|
||||
this.cleanUpTurtle();
|
||||
this.cleanUpUI();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -108,5 +108,5 @@ CommandSocket.prototype.flush = function() {
|
||||
*/
|
||||
CommandSocket.prototype.killWebSocket = function() {
|
||||
this.websocket.flush();
|
||||
this.websocket.close();
|
||||
};
|
||||
this.websocket.close(1000);
|
||||
};
|
||||
|
102
app/assets/javascripts/exercise_collections.js.erb
Normal file
102
app/assets/javascripts/exercise_collections.js.erb
Normal file
@ -0,0 +1,102 @@
|
||||
$(function() {
|
||||
if ($.isController('exercise_collections')) {
|
||||
var data = $('#data').data('working-times');
|
||||
var averageWorkingTimeValue = parseFloat($('#data').data('average-working-time'));
|
||||
|
||||
var margin = { top: 30, right: 40, bottom: 30, left: 50 },
|
||||
width = 720 - margin.left - margin.right,
|
||||
height = 500 - margin.top - margin.bottom;
|
||||
|
||||
var x = d3.scaleBand().range([0, width]);
|
||||
var y = d3.scaleLinear().range([height, 0]);
|
||||
|
||||
var xAxis = d3.axisBottom(x);
|
||||
var yAxisLeft = d3.axisLeft(y);
|
||||
|
||||
var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip");
|
||||
|
||||
var averageWorkingTime = d3.line()
|
||||
.x(function (d) { return x(d.index) + x.bandwidth()/2; })
|
||||
.y(function () { return y(averageWorkingTimeValue); });
|
||||
|
||||
var minWorkingTime = d3.line()
|
||||
.x(function (d) { return x(d.index) + x.bandwidth()/2; })
|
||||
.y(function () { return y(0.1*averageWorkingTimeValue); });
|
||||
|
||||
var maxWorkingTime = d3.line()
|
||||
.x(function (d) { return x(d.index) + x.bandwidth()/2; })
|
||||
.y(function () { return y(2*averageWorkingTimeValue); });
|
||||
|
||||
var svg = d3.select('#graph')
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform",
|
||||
"translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
// Get the data
|
||||
data = Object.keys(data).map(function (key, index) {
|
||||
return {
|
||||
index: index,
|
||||
exercise_id: parseInt(key),
|
||||
working_time: parseFloat(data[key])
|
||||
};
|
||||
});
|
||||
|
||||
// Scale the range of the data
|
||||
x.domain(data.map(function (d) { return d.index; }));
|
||||
y.domain([0, d3.max(data, function (d) { return d.working_time; })]);
|
||||
|
||||
// Add the X Axis
|
||||
svg.append("g")
|
||||
.attr("class", "x axis")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(xAxis);
|
||||
|
||||
// Add the Y Axis
|
||||
svg.append("g")
|
||||
.attr("class", "y axis")
|
||||
.style("fill", "steelblue")
|
||||
.call(yAxisLeft);
|
||||
|
||||
// Draw the bars
|
||||
svg.selectAll("bar")
|
||||
.data(data)
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("class", "value-bar")
|
||||
.on("mousemove", function (d){
|
||||
tooltip
|
||||
.style("left", d3.event.pageX - 50 + "px")
|
||||
.style("top", d3.event.pageY + 50 + "px")
|
||||
.style("display", "inline-block")
|
||||
.html("<%= I18n.t('activerecord.models.exercise.one') %> ID: " + d.exercise_id + "<br>" +
|
||||
"<%= I18n.t('exercises.statistics.average_worktime') %>: " + d.working_time + "s");
|
||||
})
|
||||
.on("mouseout", function (){ tooltip.style("display", "none");})
|
||||
.on("click", function (d) {
|
||||
window.location.href = "/exercises/" + d.exercise_id + "/statistics";
|
||||
})
|
||||
.attr("x", function (d) { return x(d.index); })
|
||||
.attr("width", x.bandwidth())
|
||||
.attr("y", function (d) { return y(d.working_time); })
|
||||
.attr("height", function (d) { return height - y(d.working_time); });
|
||||
|
||||
// Add the average working time path
|
||||
svg.append("path")
|
||||
.datum(data)
|
||||
.attr("class", "line average-working-time")
|
||||
.attr("d", averageWorkingTime);
|
||||
|
||||
// Add the anomaly paths (min/max average exercise working time)
|
||||
svg.append("path")
|
||||
.datum(data)
|
||||
.attr("class", "line minimum-working-time")
|
||||
.attr("d", minWorkingTime);
|
||||
svg.append("path")
|
||||
.datum(data)
|
||||
.attr("class", "line maximum-working-time")
|
||||
.attr("d", maxWorkingTime);
|
||||
}
|
||||
});
|
@ -1,9 +1,7 @@
|
||||
$(function() {
|
||||
// http://localhost:3333/exercises/38/statistics good for testing
|
||||
// originally at--> localhost:3333/exercises/69/statistics
|
||||
// /exercises/38/statistics good for testing
|
||||
|
||||
if ($.isController('exercises') && $('.graph-functions-2').isPresent()) {
|
||||
// GET THE DATA
|
||||
var submissions = $('#data').data('submissions');
|
||||
var submissions_length = submissions.length;
|
||||
|
||||
@ -14,10 +12,7 @@ $(function() {
|
||||
submissionsAutosaves = [];
|
||||
var maximumValue = 0;
|
||||
|
||||
var wtimes = $('#wtimes').data('working_times'); //.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until)
|
||||
|
||||
// console.log(submissions);
|
||||
// console.log(wtimes);
|
||||
var wtimes = $('#wtimes').data('working_times');
|
||||
|
||||
for (var i = 0;i<submissions_length;i++){
|
||||
var submission = submissions[i];
|
||||
@ -46,9 +41,6 @@ $(function() {
|
||||
submissionsSaves.push(submissionArray[1]);
|
||||
}
|
||||
}
|
||||
// console.log(submissionsScoreAndTimeAssess.length);
|
||||
// console.log(submissionsScoreAndTimeSubmits);
|
||||
// console.log(submissionsScoreAndTimeRuns);
|
||||
|
||||
function get_minutes (time_stamp) {
|
||||
try {
|
||||
@ -94,33 +86,22 @@ $(function() {
|
||||
height = (width * height_ratio) - margin.top - margin.bottom;
|
||||
|
||||
// Set the ranges
|
||||
var x = d3.scale.linear().range([0, width]);
|
||||
var y = d3.scale.linear().range([height,0]);
|
||||
var x = d3.scaleLinear().range([0, width]);
|
||||
var y = d3.scaleLinear().range([height,0]);
|
||||
|
||||
//var x = d3.scale.linear()
|
||||
//var x = d3.scaleLinear()
|
||||
// .range([0, width]);
|
||||
//var y = d3.scale.linear()
|
||||
//var y = d3.scaleLinear()
|
||||
// .range([0,height]); // - (height/20
|
||||
|
||||
var xAxis = d3.svg.axis()
|
||||
.scale(x)
|
||||
.orient("bottom")
|
||||
.ticks(20);
|
||||
|
||||
|
||||
var yAxis = d3.svg.axis()
|
||||
.scale(d3.scale.linear().domain([0,maximumValue]).range([height,0]))//y
|
||||
// .scale(y)
|
||||
.orient("left")
|
||||
var xAxis = d3.axisBottom(x).ticks(20);
|
||||
var yAxis = d3.axisLeft()
|
||||
.scale(d3.scaleLinear().domain([0,maximumValue]).range([height,0]))
|
||||
.ticks(maximumValue)
|
||||
.innerTickSize(-width)
|
||||
.outerTickSize(0);
|
||||
.tickSizeInner(-width)
|
||||
.tickSizeOuter(0);
|
||||
|
||||
//var line = d3.svg.line()
|
||||
// .x(function(d) { return x(d.date); })
|
||||
// .y(function(d) { return y(d.close); });
|
||||
|
||||
var line = d3.svg.line()
|
||||
var line = d3.line()
|
||||
.x(function (d) {
|
||||
// console.log(d[1]);
|
||||
return x(d[1]);
|
||||
@ -288,23 +269,12 @@ $(function() {
|
||||
.text(color_hash[String(i)][0]);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
// function type(d) {
|
||||
// d.frequency = +d.frequency;
|
||||
// return d;
|
||||
// }
|
||||
|
||||
//.on("mousemove", mMove)//new again
|
||||
//.append("title");
|
||||
|
||||
}
|
||||
|
||||
try{
|
||||
graph_assesses();
|
||||
} catch(err){
|
||||
alert("could not draw the graph");
|
||||
console.error("Could not draw the graph", err);
|
||||
}
|
||||
|
||||
}
|
||||
|
87
app/assets/javascripts/statistics_activity_history.js
Normal file
87
app/assets/javascripts/statistics_activity_history.js
Normal file
@ -0,0 +1,87 @@
|
||||
$(document).ready(function () {
|
||||
|
||||
function manageActivityHistory(prefix) {
|
||||
var containerId = prefix + '-activity-history';
|
||||
|
||||
if ($('.graph#' + containerId).isPresent()) {
|
||||
|
||||
var chartData;
|
||||
var dataset;
|
||||
var graph;
|
||||
var groups;
|
||||
|
||||
var buildChartGroups = function () {
|
||||
return _.map(chartData, function (element) {
|
||||
return {
|
||||
content: element.name,
|
||||
id: element.key,
|
||||
visible: true,
|
||||
options: {
|
||||
interpolation: false,
|
||||
yAxisOrientation: element.axis ? element.axis : 'left'
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var initializeChart = function () {
|
||||
dataset = new vis.DataSet();
|
||||
groups = new vis.DataSet(buildChartGroups());
|
||||
graph = new vis.Graph2d(document.getElementById(containerId), dataset, groups, {
|
||||
dataAxis: {
|
||||
left: {
|
||||
range: {min: 0}
|
||||
},
|
||||
right: {
|
||||
range: {min: 0}
|
||||
},
|
||||
showMinorLabels: true,
|
||||
alignZeros: true
|
||||
},
|
||||
drawPoints: {
|
||||
style: 'circle'
|
||||
},
|
||||
legend: true,
|
||||
start: $('#from-date')[0].value || 0,
|
||||
end: $('#to-date')[0].value || 0
|
||||
});
|
||||
};
|
||||
|
||||
var refreshData = function (callback) {
|
||||
var params = new URLSearchParams(window.location.search.slice(1));
|
||||
var jqxhr = $.ajax(prefix + '-activity-history.json', {
|
||||
dataType: 'json',
|
||||
data: {from: params.get('from'), to: params.get('to'), interval: params.get('interval')},
|
||||
method: 'GET'
|
||||
});
|
||||
jqxhr.done(function (response) {
|
||||
(callback || _.noop)(response);
|
||||
updateChartData(response);
|
||||
});
|
||||
};
|
||||
|
||||
var updateChartData = function (response) {
|
||||
_.each(response, function (group) {
|
||||
_.each(group.data, function (data) {
|
||||
dataset.add({
|
||||
group: group.key,
|
||||
x: data.key,
|
||||
y: data.value
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
refreshData(function (data) {
|
||||
chartData = data;
|
||||
$('#' + containerId).parent().find('.spinner').hide();
|
||||
initializeChart();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ($.isController('statistics')) {
|
||||
manageActivityHistory('rfc');
|
||||
manageActivityHistory('user');
|
||||
}
|
||||
});
|
107
app/assets/javascripts/statistics_graphs.js
Normal file
107
app/assets/javascripts/statistics_graphs.js
Normal file
@ -0,0 +1,107 @@
|
||||
$(document).ready(function () {
|
||||
if ($.isController('statistics') && $('.graph#user-activity').isPresent()) {
|
||||
|
||||
function manageGraph(containerId, url, refreshAfter) {
|
||||
var CHART_START = window.vis ? vis.moment().add(-1, 'minute') : undefined;
|
||||
var DEFAULT_REFRESH_INTERVAL = refreshAfter * 1000 || 10000;
|
||||
|
||||
var refreshInterval;
|
||||
|
||||
var initialData;
|
||||
var dataset;
|
||||
var graph;
|
||||
var groups;
|
||||
|
||||
var buildChartGroups = function() {
|
||||
return _.map(initialData, function(element) {
|
||||
return {
|
||||
content: element.name + (element.unit ? ' [' + element.unit + ']' : ''),
|
||||
id: element.key,
|
||||
visible: false,
|
||||
options: {
|
||||
yAxisOrientation: element.axis ? element.axis : 'left'
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var initializeChart = function() {
|
||||
dataset = new vis.DataSet();
|
||||
groups = new vis.DataSet(buildChartGroups());
|
||||
graph = new vis.Graph2d(document.getElementById(containerId), dataset, groups, {
|
||||
dataAxis: {
|
||||
left: {
|
||||
range: {min: 0}
|
||||
},
|
||||
right: {
|
||||
range: {min: 0}
|
||||
},
|
||||
showMinorLabels: true,
|
||||
alignZeros: true
|
||||
},
|
||||
drawPoints: {
|
||||
style: 'circle'
|
||||
},
|
||||
end: vis.moment(),
|
||||
legend: true,
|
||||
start: CHART_START
|
||||
});
|
||||
};
|
||||
|
||||
var refreshChart = function() {
|
||||
var now = vis.moment();
|
||||
var window = graph.getWindow();
|
||||
var interval = window.end - window.start;
|
||||
graph.setWindow(now - interval, now);
|
||||
};
|
||||
|
||||
var refreshData = function(callback) {
|
||||
if (! ($.isController('statistics') && $('#' + containerId).isPresent())) {
|
||||
clearInterval(refreshInterval);
|
||||
} else {
|
||||
var jqxhr = $.ajax(url, {
|
||||
dataType: 'json',
|
||||
method: 'GET'
|
||||
});
|
||||
jqxhr.done(function(response) {
|
||||
(callback || _.noop)(response);
|
||||
setGroupVisibility(response);
|
||||
updateChartData(response);
|
||||
requestAnimationFrame(refreshChart);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var setGroupVisibility = function(response) {
|
||||
_.each(response, function(data) {
|
||||
groups.update({
|
||||
id: data.key,
|
||||
visible: true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var updateChartData = function(response) {
|
||||
_.each(response, function(data) {
|
||||
dataset.add({
|
||||
group: data.key,
|
||||
x: vis.moment(),
|
||||
y: data.data
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
refreshData(function (data) {
|
||||
initialData = data;
|
||||
$('#' + containerId).parent().find('.spinner').hide();
|
||||
initializeChart();
|
||||
|
||||
var refresh_interval = location.search.match(/interval=(\d+)/) ? parseInt(RegExp.$1) : DEFAULT_REFRESH_INTERVAL;
|
||||
refreshInterval = setInterval(refreshData, refresh_interval);
|
||||
});
|
||||
}
|
||||
|
||||
manageGraph('user-activity', 'graphs/user-activity', 10);
|
||||
manageGraph('rfc-activity', 'graphs/rfc-activity', 30);
|
||||
}
|
||||
});
|
@ -38,18 +38,6 @@ $(function() {
|
||||
}
|
||||
}
|
||||
|
||||
// minutes_count[(maximum_minutes + 1)] = 0;
|
||||
//$('.graph-functions').html("<p></p>")
|
||||
|
||||
// var minutes_count = new Array(10);
|
||||
// var minutes_array_len = minutes_array.length;
|
||||
// for (var i=0; i< minutes_count; i++){
|
||||
//
|
||||
// for (var j = 0; j < minutes_array_len; j++){
|
||||
// if ()
|
||||
// }
|
||||
// }
|
||||
|
||||
function getWidth() {
|
||||
if (self.innerHeight) {
|
||||
return self.innerWidth;
|
||||
@ -81,22 +69,17 @@ $(function() {
|
||||
|
||||
//var formatDate = d3.time.format("%M");
|
||||
|
||||
var x = d3.scale.linear()
|
||||
var x = d3.scaleLinear()
|
||||
.range([0, width]);
|
||||
var y = d3.scale.linear()
|
||||
var y = d3.scaleLinear()
|
||||
.range([height, 0]); // - (height/20
|
||||
var xAxis = d3.svg.axis()
|
||||
.scale(x)
|
||||
.orient("bottom")
|
||||
.ticks(20);
|
||||
var yAxis = d3.svg.axis()
|
||||
.scale(y)
|
||||
.orient("left")
|
||||
var xAxis = d3.axisBottom(x).ticks(20);
|
||||
var yAxis = d3.axisLeft(y)
|
||||
.ticks(20)
|
||||
.innerTickSize(-width)
|
||||
.outerTickSize(0);
|
||||
.tickSizeInner(-width)
|
||||
.tickSizeOuter(0);
|
||||
|
||||
var line = d3.svg.line()
|
||||
var line = d3.line()
|
||||
.x(function (d, i) {
|
||||
return x(i);
|
||||
})
|
||||
@ -225,7 +208,7 @@ $(function() {
|
||||
var x = d3.scale.ordinal()
|
||||
.rangeRoundBands([0, width], .1);
|
||||
|
||||
var y = d3.scale.linear()
|
||||
var y = d3.scaleLinear()
|
||||
.range([0,height-(margin.top + margin.bottom)]);
|
||||
|
||||
|
||||
@ -236,7 +219,7 @@ $(function() {
|
||||
|
||||
|
||||
var yAxis = d3.svg.axis()
|
||||
.scale(d3.scale.linear().domain([0,max_of_array]).range([height,0]))//y
|
||||
.scale(d3.scaleLinear().domain([0,max_of_array]).range([height,0]))//y
|
||||
.orient("left")
|
||||
.ticks(10)
|
||||
.innerTickSize(-width);
|
||||
@ -299,7 +282,7 @@ $(function() {
|
||||
.text("Working Time (Minutes)")
|
||||
.style('font-size', 14);
|
||||
|
||||
y = d3.scale.linear()
|
||||
y = d3.scaleLinear()
|
||||
.domain([(0),max_of_array])
|
||||
.range([0,height]);
|
||||
|
||||
|
38
app/assets/stylesheets/bootstrap-dropdown-submenu.css.scss
vendored
Normal file
38
app/assets/stylesheets/bootstrap-dropdown-submenu.css.scss
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
.dropdown-submenu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-submenu > .dropdown-menu {
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.dropdown-submenu > a:after {
|
||||
display: block;
|
||||
content: " ";
|
||||
float: right;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-left-color: #cccccc;
|
||||
margin-top: 5px;
|
||||
margin-right: -10px;
|
||||
}
|
||||
|
||||
.dropdown-submenu:hover > a:after {
|
||||
border-left-color: #ffffff;
|
||||
}
|
||||
|
||||
.dropdown-submenu.pull-left {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.dropdown-submenu.pull-left > .dropdown-menu {
|
||||
left: -100%;
|
||||
margin-left: 10px;
|
||||
-webkit-border-radius: 6px 0 6px 6px;
|
||||
-moz-border-radius: 6px 0 6px 6px;
|
||||
border-radius: 6px 0 6px 6px;
|
||||
}
|
@ -193,3 +193,24 @@ button i.fa-spin {
|
||||
.enforce-bottom-margin {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
|
||||
#error-hints {
|
||||
display: none;
|
||||
background-color: #FAFAFA;
|
||||
|
||||
.heading {
|
||||
font-weight: bold;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
ul.body {
|
||||
|
||||
li.hint {
|
||||
.description {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hint {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
69
app/assets/stylesheets/exercise_collections.scss
Normal file
69
app/assets/stylesheets/exercise_collections.scss
Normal file
@ -0,0 +1,69 @@
|
||||
$time-color: #008cba;
|
||||
$min-color: #8efa00;
|
||||
$avg-color: #ffca00;
|
||||
$max-color: #ff2600;
|
||||
|
||||
path.line.minimum-working-time {
|
||||
stroke: $min-color;
|
||||
}
|
||||
|
||||
path.line.average-working-time {
|
||||
stroke: $avg-color;
|
||||
}
|
||||
|
||||
path.line.maximum-working-time {
|
||||
stroke: $max-color;
|
||||
}
|
||||
|
||||
rect.value-bar {
|
||||
fill: $time-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#legend {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
|
||||
.legend-entry {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
||||
.box {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: solid 1px #000;
|
||||
}
|
||||
|
||||
.box.time {
|
||||
background-color: $time-color;
|
||||
}
|
||||
|
||||
.box.min {
|
||||
background-color: $min-color;
|
||||
}
|
||||
|
||||
.box.avg {
|
||||
background-color: $avg-color;
|
||||
}
|
||||
|
||||
.box.max {
|
||||
background-color: $max-color;
|
||||
}
|
||||
|
||||
.box-label {
|
||||
margin-left: 5px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.exercise-id-tooltip {
|
||||
position: absolute;
|
||||
display: none;
|
||||
min-width: 80px;
|
||||
height: auto;
|
||||
background: none repeat scroll 0 0 #ffffff;
|
||||
border: 1px solid #008cba;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
}
|
@ -53,64 +53,72 @@
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.testrun-assess-results {
|
||||
|
||||
.testrun-assess-results {
|
||||
.testrun-container {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
||||
display: flex;
|
||||
.testrun-output {
|
||||
overflow-x: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-right: 10px;
|
||||
margin-top: 20px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.passed {
|
||||
border-radius: 50%;
|
||||
background-color: #8efa00;
|
||||
-webkit-box-shadow: 0 0 11px 1px rgba(44,222,0,1);
|
||||
-moz-box-shadow: 0 0 11px 1px rgba(44,222,0,1);
|
||||
box-shadow: 0 0 11px 1px rgba(44,222,0,1);
|
||||
}
|
||||
|
||||
.unknown {
|
||||
border-radius: 50%;
|
||||
background-color: #ffca00;
|
||||
-webkit-box-shadow: 0 0 11px 1px rgb(255, 202, 0);
|
||||
-moz-box-shadow: 0 0 11px 1px rgb(255, 202, 0);
|
||||
box-shadow: 0 0 11px 1px rgb(255, 202, 0);
|
||||
}
|
||||
|
||||
.failed {
|
||||
border-radius: 50%;
|
||||
background-color: #ff2600;
|
||||
-webkit-box-shadow: 0 0 11px 1px rgba(222,0,0,1);
|
||||
-moz-box-shadow: 0 0 11px 1px rgba(222,0,0,1);
|
||||
box-shadow: 0 0 11px 1px rgba(222,0,0,1);
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-right: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.passed {
|
||||
border-radius: 50%;
|
||||
background-color: #8efa00;
|
||||
-webkit-box-shadow: 0 0 11px 1px rgba(44,222,0,1);
|
||||
-moz-box-shadow: 0 0 11px 1px rgba(44,222,0,1);
|
||||
box-shadow: 0 0 11px 1px rgba(44,222,0,1);
|
||||
#mark-as-solved-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.unknown {
|
||||
border-radius: 50%;
|
||||
background-color: #ffca00;
|
||||
-webkit-box-shadow: 0 0 11px 1px rgb(255, 202, 0);
|
||||
-moz-box-shadow: 0 0 11px 1px rgb(255, 202, 0);
|
||||
box-shadow: 0 0 11px 1px rgb(255, 202, 0);
|
||||
#thank-you-container {
|
||||
display: none;
|
||||
margin-top: 20px;
|
||||
padding: 5px;
|
||||
border: solid lightgrey 1px;
|
||||
background-color: rgba(20, 180, 20, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.failed {
|
||||
border-radius: 50%;
|
||||
background-color: #ff2600;
|
||||
-webkit-box-shadow: 0 0 11px 1px rgba(222,0,0,1);
|
||||
-moz-box-shadow: 0 0 11px 1px rgba(222,0,0,1);
|
||||
box-shadow: 0 0 11px 1px rgba(222,0,0,1);
|
||||
#thank-you-note {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#mark-as-solved-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#thank-you-container {
|
||||
display: none;
|
||||
margin-top: 20px;
|
||||
padding: 5px;
|
||||
border: solid lightgrey 1px;
|
||||
background-color: rgba(20, 180, 20, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#thank-you-note {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#commentitor {
|
||||
|
@ -58,3 +58,96 @@ div.negative-result {
|
||||
box-shadow: 0px 0px 11px 1px rgba(222,0,0,1);
|
||||
}
|
||||
|
||||
tr.highlight {
|
||||
border-top: 2px solid rgba(222,0,0,1);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// StatisticsController:
|
||||
|
||||
#statistics-container {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.statistics-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-auto-rows: 150px;
|
||||
grid-gap: 10px;
|
||||
|
||||
> a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
|
||||
> div {
|
||||
border: 2px solid #0055ba;
|
||||
border-radius: 5px;
|
||||
background-color: #008cba;
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-flow: column-reverse;
|
||||
text-align: center;
|
||||
|
||||
> .data {
|
||||
flex-grow: 1;
|
||||
font-size: 40px;
|
||||
vertical-align: middle;
|
||||
line-height: 50px;
|
||||
|
||||
> .unit {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group {
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
h1 {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: #333;
|
||||
|
||||
margin: 100px auto;
|
||||
-webkit-animation: sk-rotateplane 1.2s infinite ease-in-out;
|
||||
animation: sk-rotateplane 1.2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@-webkit-keyframes sk-rotateplane {
|
||||
0% { -webkit-transform: perspective(120px) }
|
||||
50% { -webkit-transform: perspective(120px) rotateY(180deg) }
|
||||
100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) }
|
||||
}
|
||||
|
||||
@keyframes sk-rotateplane {
|
||||
0% {
|
||||
transform: perspective(120px) rotateX(0deg) rotateY(0deg);
|
||||
-webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg)
|
||||
}
|
||||
50% {
|
||||
transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
|
||||
-webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg)
|
||||
}
|
||||
100% {
|
||||
transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
|
||||
-webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ module SubmissionScoring
|
||||
submission.exercise.execution_environment.error_templates.each do |template|
|
||||
pattern = Regexp.new(template.signature).freeze
|
||||
if pattern.match(testrun_output)
|
||||
StructuredError.create_from_template(template, testrun_output)
|
||||
StructuredError.create_from_template(template, testrun_output, submission)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,7 +1,7 @@
|
||||
class ExerciseCollectionsController < ApplicationController
|
||||
include CommonBehavior
|
||||
|
||||
before_action :set_exercise_collection, only: [:show, :edit, :update, :destroy]
|
||||
before_action :set_exercise_collection, only: [:show, :edit, :update, :destroy, :statistics]
|
||||
|
||||
def index
|
||||
@exercise_collections = ExerciseCollection.all.paginate(:page => params[:page])
|
||||
@ -9,6 +9,7 @@ class ExerciseCollectionsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
@exercises = @exercise_collection.exercises.paginate(:page => params[:page])
|
||||
end
|
||||
|
||||
def new
|
||||
@ -34,6 +35,9 @@ class ExerciseCollectionsController < ApplicationController
|
||||
update_and_respond(object: @exercise_collection, params: exercise_collection_params)
|
||||
end
|
||||
|
||||
def statistics
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_exercise_collection
|
||||
@ -46,6 +50,6 @@ class ExerciseCollectionsController < ApplicationController
|
||||
end
|
||||
|
||||
def exercise_collection_params
|
||||
params[:exercise_collection].permit(:name, :exercise_ids => [])
|
||||
params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []).merge(user_type: InternalUser.name)
|
||||
end
|
||||
end
|
||||
|
@ -346,6 +346,16 @@ class ExercisesController < ApplicationController
|
||||
|
||||
def statistics
|
||||
if(@external_user)
|
||||
@submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id).order("created_at")
|
||||
@submissions_and_interventions = (@submissions + UserExerciseIntervention.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id)).sort_by { |a| a.created_at }
|
||||
deltas = @submissions.map.with_index do |item, index|
|
||||
delta = item.created_at - @submissions[index - 1].created_at if index > 0
|
||||
if delta == nil or delta > 10 * 60 then 0 else delta end
|
||||
end
|
||||
@working_times_until = []
|
||||
@submissions_and_interventions.each_with_index do |submission, index|
|
||||
@working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0))
|
||||
end
|
||||
render 'exercises/external_users/statistics'
|
||||
else
|
||||
user_statistics = {}
|
||||
|
59
app/controllers/statistics_controller.rb
Normal file
59
app/controllers/statistics_controller.rb
Normal file
@ -0,0 +1,59 @@
|
||||
class StatisticsController < ApplicationController
|
||||
include StatisticsHelper
|
||||
|
||||
before_action :authorize!, only: [:show, :graphs, :user_activity, :user_activity_history, :rfc_activity,
|
||||
:rfc_activity_history]
|
||||
|
||||
def policy_class
|
||||
StatisticsPolicy
|
||||
end
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json { render(json: statistics_data) }
|
||||
end
|
||||
end
|
||||
|
||||
def graphs
|
||||
end
|
||||
|
||||
def user_activity
|
||||
respond_to do |format|
|
||||
format.json { render(json: user_activity_live_data) }
|
||||
end
|
||||
end
|
||||
|
||||
def user_activity_history
|
||||
respond_to do |format|
|
||||
format.html { render('activity_history', locals: {resource: :user}) }
|
||||
format.json { render_ranged_data :ranged_user_data}
|
||||
end
|
||||
end
|
||||
|
||||
def rfc_activity
|
||||
respond_to do |format|
|
||||
format.json { render(json: rfc_activity_data) }
|
||||
end
|
||||
end
|
||||
|
||||
def rfc_activity_history
|
||||
respond_to do |format|
|
||||
format.html { render('activity_history', locals: {resource: :rfc}) }
|
||||
format.json { render_ranged_data :ranged_rfc_data }
|
||||
end
|
||||
end
|
||||
|
||||
def render_ranged_data(data_source)
|
||||
interval = params[:interval].to_s.empty? ? 'year' : params[:interval]
|
||||
from = DateTime.strptime(params[:from], '%Y-%m-%d') rescue DateTime.new(0)
|
||||
to = DateTime.strptime(params[:to], '%Y-%m-%d') rescue DateTime.now
|
||||
render(json: self.send(data_source, interval, from, to))
|
||||
end
|
||||
|
||||
def authorize!
|
||||
authorize self
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
end
|
@ -197,7 +197,8 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
def kill_socket(tubesock)
|
||||
# search for errors and save them as StructuredError (for scoring runs see submission_scoring.rb)
|
||||
extract_errors
|
||||
errors = extract_errors
|
||||
send_hints(tubesock, errors)
|
||||
|
||||
# save the output of this "run" as a "testrun" (scoring runs are saved in submission_scoring.rb)
|
||||
save_run_output
|
||||
@ -284,14 +285,16 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def extract_errors
|
||||
results = []
|
||||
unless @raw_output.blank?
|
||||
@submission.exercise.execution_environment.error_templates.each do |template|
|
||||
pattern = Regexp.new(template.signature).freeze
|
||||
if pattern.match(@raw_output)
|
||||
StructuredError.create_from_template(template, @raw_output, @submission)
|
||||
results << StructuredError.create_from_template(template, @raw_output, @submission)
|
||||
end
|
||||
end
|
||||
end
|
||||
results
|
||||
end
|
||||
|
||||
def score
|
||||
@ -303,11 +306,22 @@ class SubmissionsController < ApplicationController
|
||||
# to ensure responsiveness, we therefore open a thread here.
|
||||
Thread.new {
|
||||
tubesock.send_data JSON.dump(score_submission(@submission))
|
||||
|
||||
# To enable hints when scoring a submission, uncomment the next line:
|
||||
#send_hints(tubesock, StructuredError.where(submission: @submission))
|
||||
|
||||
tubesock.send_data JSON.dump({'cmd' => 'exit'})
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def send_hints(tubesock, errors)
|
||||
errors = errors.to_a.uniq { |e| e.hint}
|
||||
errors.each do | error |
|
||||
tubesock.send_data JSON.dump({cmd: 'hint', hint: error.hint, description: error.error_template.description})
|
||||
end
|
||||
end
|
||||
|
||||
def set_docker_client
|
||||
@docker_client = DockerClient.new(execution_environment: @submission.execution_environment)
|
||||
end
|
||||
|
@ -69,7 +69,8 @@ class UserExerciseFeedbacksController < ApplicationController
|
||||
@texts = comment_presets.to_a
|
||||
@times = time_presets.to_a
|
||||
@uef = UserExerciseFeedback.new
|
||||
@exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id])
|
||||
exercise_id = if params[:user_exercise_feedback].nil? then params[:exercise_id] else params[:user_exercise_feedback][:exercise_id] end
|
||||
@exercise = Exercise.find(exercise_id)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
211
app/helpers/statistics_helper.rb
Normal file
211
app/helpers/statistics_helper.rb
Normal file
@ -0,0 +1,211 @@
|
||||
module StatisticsHelper
|
||||
|
||||
def statistics_data
|
||||
[
|
||||
{
|
||||
key: 'users',
|
||||
name: t('statistics.sections.users'),
|
||||
entries: user_statistics
|
||||
},
|
||||
{
|
||||
key: 'exercises',
|
||||
name: t('statistics.sections.exercises'),
|
||||
entries: exercise_statistics
|
||||
},
|
||||
{
|
||||
key: 'request_for_comments',
|
||||
name: t('statistics.sections.request_for_comments'),
|
||||
entries: rfc_statistics
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def user_statistics
|
||||
[
|
||||
{
|
||||
key: 'internal_users',
|
||||
name: t('activerecord.models.internal_user.other'),
|
||||
data: InternalUser.count,
|
||||
url: internal_users_path
|
||||
},
|
||||
{
|
||||
key: 'external_users',
|
||||
name: t('activerecord.models.external_user.other'),
|
||||
data: ExternalUser.count,
|
||||
url: external_users_path
|
||||
},
|
||||
{
|
||||
key: 'currently_active',
|
||||
name: t('statistics.entries.users.currently_active'),
|
||||
data: ExternalUser.joins(:submissions)
|
||||
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
|
||||
.distinct('external_users.id').count,
|
||||
url: 'statistics/graphs'
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def exercise_statistics
|
||||
[
|
||||
{
|
||||
key: 'exercises',
|
||||
name: t('activerecord.models.exercise.other'),
|
||||
data: Exercise.count,
|
||||
url: exercises_path
|
||||
},
|
||||
{
|
||||
key: 'average_submissions',
|
||||
name: t('statistics.entries.exercises.average_number_of_submissions'),
|
||||
data: (Submission.count.to_f / Exercise.count).round(2)
|
||||
},
|
||||
{
|
||||
key: 'submissions_per_minute',
|
||||
name: t('statistics.entries.exercises.submissions_per_minute'),
|
||||
data: (Submission.where('created_at >= ?', DateTime.now - 1.hours).count.to_f / 60).round(2),
|
||||
unit: '/min',
|
||||
url: statistics_graphs_path
|
||||
},
|
||||
{
|
||||
key: 'execution_environments',
|
||||
name: t('activerecord.models.execution_environment.other'),
|
||||
data: ExecutionEnvironment.count,
|
||||
url: execution_environments_path
|
||||
},
|
||||
{
|
||||
key: 'exercise_collections',
|
||||
name: t('activerecord.models.exercise_collection.other'),
|
||||
data: ExerciseCollection.count,
|
||||
url: exercise_collections_path
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def rfc_statistics
|
||||
rfc_activity_data + [
|
||||
{
|
||||
key: 'comments',
|
||||
name: t('activerecord.models.comment.other'),
|
||||
data: Comment.count
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def user_activity_live_data
|
||||
[
|
||||
{
|
||||
key: 'active_in_last_hour',
|
||||
name: t('statistics.entries.users.currently_active'),
|
||||
data: ExternalUser.joins(:submissions)
|
||||
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
|
||||
.distinct('external_users.id').count,
|
||||
},
|
||||
{
|
||||
key: 'submissions_per_minute',
|
||||
name: t('statistics.entries.exercises.submissions_per_minute'),
|
||||
data: (Submission.where('created_at >= ?', DateTime.now - 1.hours).count.to_f / 60).round(2),
|
||||
unit: '/min',
|
||||
axis: 'right'
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def rfc_activity_data(from=DateTime.new(0), to=DateTime.now)
|
||||
[
|
||||
{
|
||||
key: 'rfcs',
|
||||
name: t('activerecord.models.request_for_comment.other'),
|
||||
data: RequestForComment.in_range(from, to).count,
|
||||
url: request_for_comments_path
|
||||
},
|
||||
{
|
||||
key: 'percent_solved',
|
||||
name: t('statistics.entries.request_for_comments.percent_solved'),
|
||||
data: (100.0 / RequestForComment.in_range(from, to).count * RequestForComment.in_range(from, to).where(solved: true).count).round(1),
|
||||
unit: '%',
|
||||
axis: 'right',
|
||||
url: statistics_graphs_path
|
||||
},
|
||||
{
|
||||
key: 'percent_soft_solved',
|
||||
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
|
||||
data: (100.0 / RequestForComment.in_range(from, to).count * RequestForComment.in_range(from, to).unsolved.where(full_score_reached: true).count).round(1),
|
||||
unit: '%',
|
||||
axis: 'right',
|
||||
url: statistics_graphs_path
|
||||
},
|
||||
{
|
||||
key: 'percent_unsolved',
|
||||
name: t('statistics.entries.request_for_comments.percent_unsolved'),
|
||||
data: (100.0 / RequestForComment.in_range(from, to).count * RequestForComment.in_range(from, to).unsolved.count).round(1),
|
||||
unit: '%',
|
||||
axis: 'right',
|
||||
url: statistics_graphs_path
|
||||
},
|
||||
{
|
||||
key: 'rfcs_with_comments',
|
||||
name: t('statistics.entries.request_for_comments.with_comments'),
|
||||
data: RequestForComment.in_range(from, to).joins('join "submissions" s on s.id = request_for_comments.submission_id
|
||||
join "files" f on f.context_id = s.id and f.context_type = \'Submission\'
|
||||
join "comments" c on c.file_id = f.id').group('request_for_comments.id').count.size,
|
||||
url: statistics_graphs_path
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def ranged_rfc_data(interval='year', from=DateTime.new(0), to=DateTime.now)
|
||||
[
|
||||
{
|
||||
key: 'rfcs',
|
||||
name: t('activerecord.models.request_for_comment.other'),
|
||||
data: RequestForComment.in_range(from, to)
|
||||
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||
.group('key').order('key')
|
||||
},
|
||||
{
|
||||
key: 'rfcs_solved',
|
||||
name: t('statistics.entries.request_for_comments.percent_solved'),
|
||||
data: RequestForComment.in_range(from, to)
|
||||
.where(solved: true)
|
||||
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||
.group('key').order('key')
|
||||
},
|
||||
{
|
||||
key: 'rfcs_soft_solved',
|
||||
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
|
||||
data: RequestForComment.in_range(from, to).unsolved
|
||||
.where(full_score_reached: true)
|
||||
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||
.group('key').order('key')
|
||||
},
|
||||
{
|
||||
key: 'rfcs_unsolved',
|
||||
name: t('statistics.entries.request_for_comments.percent_unsolved'),
|
||||
data: RequestForComment.in_range(from, to).unsolved
|
||||
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||
.group('key').order('key')
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def ranged_user_data(interval='year', from=DateTime.new(0), to=DateTime.now)
|
||||
[
|
||||
{
|
||||
key: 'active',
|
||||
name: t('statistics.entries.users.active'),
|
||||
data: ExternalUser.joins(:submissions)
|
||||
.where(submissions: {created_at: from..to})
|
||||
.select("date_trunc('#{interval}', submissions.created_at) AS \"key\", count(distinct external_users.id) AS \"value\"")
|
||||
.group('key').order('key')
|
||||
},
|
||||
{
|
||||
key: 'submissions',
|
||||
name: t('statistics.entries.exercises.submissions'),
|
||||
data: Submission.where(created_at: from..to)
|
||||
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||
.group('key').order('key'),
|
||||
axis: 'right'
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
end
|
12
app/helpers/time_helper.rb
Normal file
12
app/helpers/time_helper.rb
Normal file
@ -0,0 +1,12 @@
|
||||
module TimeHelper
|
||||
|
||||
# convert timestamps ('12:34:56.789') to seconds
|
||||
def time_to_f(timestamp)
|
||||
unless timestamp.nil?
|
||||
timestamp = timestamp.split(':')
|
||||
return timestamp[0].to_i * 60 * 60 + timestamp[1].to_i * 60 + timestamp[2].to_f
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
end
|
@ -37,4 +37,18 @@ class UserMailer < ActionMailer::Base
|
||||
@rfc_link = request_for_comment_url(request_for_comments)
|
||||
mail(subject: t('mailers.user_mailer.send_thank_you_note.subject', author: @author), to: receiver.email)
|
||||
end
|
||||
|
||||
def exercise_anomaly_detected(exercise_collection, anomalies)
|
||||
@receiver_displayname = exercise_collection.user.displayname
|
||||
@collection = exercise_collection
|
||||
@anomalies = anomalies
|
||||
mail(subject: t('mailers.user_mailer.exercise_anomaly_detected.subject'), to: exercise_collection.user.email)
|
||||
end
|
||||
|
||||
def exercise_anomaly_needs_feedback(user, exercise, link)
|
||||
@receiver_displayname = user.displayname
|
||||
@exercise_title = exercise.title
|
||||
@link = link
|
||||
mail(subject: t('mailers.user_mailer.exercise_anomaly_needs_feedback.subject'), to: user.email)
|
||||
end
|
||||
end
|
||||
|
5
app/models/anomaly_notification.rb
Normal file
5
app/models/anomaly_notification.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AnomalyNotification < ActiveRecord::Base
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :exercise
|
||||
belongs_to :exercise_collection
|
||||
end
|
@ -36,6 +36,7 @@ class Exercise < ActiveRecord::Base
|
||||
validates :token, presence: true, uniqueness: true
|
||||
|
||||
@working_time_statistics = nil
|
||||
attr_reader :working_time_statistics
|
||||
|
||||
MAX_EXERCISE_FEEDBACKS = 20
|
||||
|
||||
@ -65,21 +66,27 @@ class Exercise < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def user_working_time_query
|
||||
"""
|
||||
"
|
||||
SELECT user_id,
|
||||
sum(working_time_new) AS working_time
|
||||
user_type,
|
||||
SUM(working_time_new) AS working_time,
|
||||
MAX(score) AS score
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
user_type,
|
||||
score,
|
||||
CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
user_type,
|
||||
score,
|
||||
id,
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id=#{id}) AS foo) AS bar
|
||||
GROUP BY user_id
|
||||
"""
|
||||
GROUP BY user_id, user_type
|
||||
"
|
||||
end
|
||||
|
||||
def get_quantiles(quantiles)
|
||||
@ -202,7 +209,7 @@ class Exercise < ActiveRecord::Base
|
||||
def retrieve_working_time_statistics
|
||||
@working_time_statistics = {}
|
||||
self.class.connection.execute(user_working_time_query).each do |tuple|
|
||||
@working_time_statistics[tuple["user_id"].to_i] = tuple
|
||||
@working_time_statistics[tuple['user_id'].to_i] = tuple
|
||||
end
|
||||
end
|
||||
|
||||
@ -345,7 +352,11 @@ class Exercise < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def has_user_solved(user)
|
||||
return maximum_score(user).to_i == maximum_score.to_i
|
||||
maximum_score(user).to_i == maximum_score.to_i
|
||||
end
|
||||
|
||||
def finishers
|
||||
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score, cause: %w(submit assess)}).distinct
|
||||
end
|
||||
|
||||
def set_default_values
|
||||
@ -368,4 +379,15 @@ class Exercise < ActiveRecord::Base
|
||||
user_exercise_feedbacks.size <= MAX_EXERCISE_FEEDBACKS
|
||||
end
|
||||
|
||||
def last_submission_per_user
|
||||
Submission.joins("JOIN (
|
||||
SELECT
|
||||
user_id,
|
||||
user_type,
|
||||
first_value(id) OVER (PARTITION BY user_id ORDER BY created_at DESC) AS fv
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{id}
|
||||
) AS t ON t.fv = submissions.id").distinct
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,6 +1,25 @@
|
||||
class ExerciseCollection < ActiveRecord::Base
|
||||
include TimeHelper
|
||||
|
||||
has_and_belongs_to_many :exercises
|
||||
belongs_to :user, polymorphic: true
|
||||
|
||||
def exercise_working_times
|
||||
working_times = {}
|
||||
exercises.each do |exercise|
|
||||
working_times[exercise.id] = time_to_f exercise.average_working_time
|
||||
end
|
||||
working_times
|
||||
end
|
||||
|
||||
def average_working_time
|
||||
if exercises.empty?
|
||||
0
|
||||
else
|
||||
values = exercise_working_times.values.reject { |v| v.nil?}
|
||||
values.reduce(:+) / exercises.size
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{I18n.t('activerecord.models.exercise_collection.one')}: #{name} (#{id})"
|
||||
|
@ -8,6 +8,7 @@ class RequestForComment < ActiveRecord::Base
|
||||
has_many :subscriptions
|
||||
|
||||
scope :unsolved, -> { where(solved: [false, nil]) }
|
||||
scope :in_range, -> (from, to) { where(created_at: from..to) }
|
||||
|
||||
def self.last_per_user(n = 5)
|
||||
from("(#{row_number_user_sql}) as request_for_comments")
|
||||
|
@ -3,11 +3,21 @@ class StructuredError < ActiveRecord::Base
|
||||
belongs_to :submission
|
||||
belongs_to :file, class_name: 'CodeOcean::File'
|
||||
|
||||
has_many :structured_error_attributes
|
||||
|
||||
def self.create_from_template(template, message_buffer, submission)
|
||||
instance = self.create(error_template: template, submission: submission)
|
||||
template.error_template_attributes.each do |attribute|
|
||||
template.error_template_attributes.each do | attribute |
|
||||
StructuredErrorAttribute.create_from_template(attribute, instance, message_buffer)
|
||||
end
|
||||
instance
|
||||
end
|
||||
|
||||
def hint
|
||||
content = error_template.hint
|
||||
structured_error_attributes.each do | attribute |
|
||||
content.sub! "{{#{attribute.error_template_attribute.key}}}", attribute.value if attribute.match
|
||||
end
|
||||
content
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,7 @@
|
||||
class ExerciseCollectionPolicy < AdminOnlyPolicy
|
||||
|
||||
def statistics?
|
||||
admin?
|
||||
end
|
||||
|
||||
end
|
||||
|
7
app/policies/statistics_policy.rb
Normal file
7
app/policies/statistics_policy.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class StatisticsPolicy < AdminOnlyPolicy
|
||||
|
||||
[:graphs?, :user_activity?, :user_activity_history?, :rfc_activity?, :rfc_activity_history?].each do |action|
|
||||
define_method(action) { admin? }
|
||||
end
|
||||
|
||||
end
|
@ -7,9 +7,11 @@
|
||||
ul.dropdown-menu role='menu'
|
||||
- if current_user.admin?
|
||||
li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path)
|
||||
li = link_to(t('breadcrumbs.statistics.show'), statistics_path)
|
||||
li.divider
|
||||
- models = [ExecutionEnvironment, Exercise, ExerciseCollection, ProxyExercise, Tag, Consumer, CodeHarborLink, UserExerciseFeedback,
|
||||
ErrorTemplate, ErrorTemplateAttribute, ExternalUser, FileType, FileTemplate, InternalUser].sort_by {|model| model.model_name.human(count: 2) }
|
||||
- models.each do |model|
|
||||
- if policy(model).index?
|
||||
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
|
||||
= render('navigation_submenu', title: t('activerecord.models.exercise.other'), models: [Exercise, ExerciseCollection, ProxyExercise, Tag], link: exercises_path)
|
||||
= render('navigation_submenu', title: t('navigation.sections.users'), models: [InternalUser, ExternalUser])
|
||||
= render('navigation_collection_link', model: ExecutionEnvironment)
|
||||
= render('navigation_submenu', title: t('navigation.sections.errors'), models: [ErrorTemplate, ErrorTemplateAttribute])
|
||||
= render('navigation_submenu', title: t('navigation.sections.files'), models: [FileType, FileTemplate])
|
||||
= render('navigation_submenu', title: t('navigation.sections.integrations'), models: [Consumer, CodeHarborLink])
|
||||
|
@ -0,0 +1,2 @@
|
||||
- if policy(model).index?
|
||||
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
|
6
app/views/application/_navigation_submenu.html.slim
Normal file
6
app/views/application/_navigation_submenu.html.slim
Normal file
@ -0,0 +1,6 @@
|
||||
li.dropdown.dropdown-submenu
|
||||
- link = link.nil? ? "#" : link
|
||||
a href=link class="dropdown-toggle" data-toggle="dropdown" = title
|
||||
ul class="dropdown-menu"
|
||||
- models.each do |model|
|
||||
= render('navigation_collection_link', model: model)
|
@ -16,4 +16,5 @@
|
||||
.form-group
|
||||
= f.label(:hint)
|
||||
= f.text_field(:hint, class: 'form-control')
|
||||
.help-block == t('error_templates.hints.hint_templates')
|
||||
.actions = render('shared/submit_button', f: f, object: @error_template)
|
||||
|
@ -1,11 +1,18 @@
|
||||
- exercises = Exercise.order(:title)
|
||||
- users = InternalUser.order(:name)
|
||||
|
||||
= form_for(@exercise_collection, data: {exercises: exercises}, multipart: true) do |f|
|
||||
= form_for(@exercise_collection, data: {exercises: exercises, users: users}, multipart: true) do |f|
|
||||
= render('shared/form_errors', object: @exercise_collection)
|
||||
.form-group
|
||||
= f.label(:name)
|
||||
= f.label(t('activerecord.attributes.exercise_collections.name'))
|
||||
= f.text_field(:name, class: 'form-control', required: true)
|
||||
.form-group
|
||||
= f.label(:exercises)
|
||||
= f.label(t('activerecord.attributes.exercise_collections.use_anomaly_detection'))
|
||||
= f.check_box(:use_anomaly_detection, {class: 'form-control'})
|
||||
.form-group
|
||||
= f.label(t('activerecord.attributes.exercise_collections.user'))
|
||||
= f.collection_select(:user_id, users, :id, :name, {}, {class: 'form-control'})
|
||||
.form-group
|
||||
= f.label(t('activerecord.attributes.exercise_collections.exercises'))
|
||||
= f.collection_select(:exercise_ids, exercises, :id, :title, {}, {class: 'form-control', multiple: true})
|
||||
.actions = render('shared/submit_button', f: f, object: @exercise_collection)
|
||||
|
@ -8,7 +8,7 @@ h1 = ExerciseCollection.model_name.human(count: 2)
|
||||
th = t('activerecord.attributes.exercise_collections.name')
|
||||
th = t('activerecord.attributes.exercise_collections.updated_at')
|
||||
th = t('activerecord.attributes.exercise_collections.exercises')
|
||||
th colspan=3 = t('shared.actions')
|
||||
th colspan=4 = t('shared.actions')
|
||||
tbody
|
||||
- @exercise_collections.each do |collection|
|
||||
tr
|
||||
@ -18,6 +18,7 @@ h1 = ExerciseCollection.model_name.human(count: 2)
|
||||
td = collection.exercises.size
|
||||
td = link_to(t('shared.show'), collection)
|
||||
td = link_to(t('shared.edit'), edit_exercise_collection_path(collection))
|
||||
td = link_to(t('shared.statistics'), statistics_exercise_collection_path(collection))
|
||||
td = link_to(t('shared.destroy'), collection, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
|
||||
|
||||
= render('shared/pagination', collection: @exercise_collections)
|
||||
|
@ -3,9 +3,25 @@ h1
|
||||
= render('shared/edit_button', object: @exercise_collection)
|
||||
|
||||
= row(label: 'exercise_collections.name', value: @exercise_collection.name)
|
||||
= row(label: 'exercise_collections.user', value: link_to(@exercise_collection.user.name, @exercise_collection.user)) unless @exercise_collection.user.nil?
|
||||
= row(label: 'exercise_collections.use_anomaly_detection', value: @exercise_collection.use_anomaly_detection)
|
||||
= row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at)
|
||||
|
||||
h4 = t('activerecord.attributes.exercise_collections.exercises')
|
||||
ul.list-unstyled
|
||||
- @exercise_collection.exercises.sort_by{|c| c.title}.each do |exercise|
|
||||
li = link_to(exercise, exercise)
|
||||
.table-responsive
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th = t('activerecord.attributes.exercise.title')
|
||||
th = t('activerecord.attributes.exercise.execution_environment')
|
||||
th = t('activerecord.attributes.exercise.user')
|
||||
th = t('shared.actions')
|
||||
tbody
|
||||
- @exercises.sort_by{|c| c.title}.each do |exercise|
|
||||
tr
|
||||
td = link_to(exercise.title, exercise)
|
||||
td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
|
||||
td = exercise.user.name
|
||||
td = link_to(t('shared.statistics'), statistics_exercise_path(exercise))
|
||||
|
||||
= render('shared/pagination', collection: @exercises)
|
||||
|
17
app/views/exercise_collections/statistics.html.slim
Normal file
17
app/views/exercise_collections/statistics.html.slim
Normal file
@ -0,0 +1,17 @@
|
||||
h1 = @exercise_collection
|
||||
|
||||
= row(label: 'exercise_collections.name', value: @exercise_collection.name)
|
||||
= row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at)
|
||||
= row(label: 'exercise_collections.exercises', value: @exercise_collection.exercises.count)
|
||||
= row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's')
|
||||
|
||||
#graph
|
||||
#data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.exercise_working_times) data-average-working-time=@exercise_collection.average_working_time)
|
||||
#legend
|
||||
- {time: t('exercises.statistics.average_worktime'),
|
||||
min: 'min. anomaly threshold',
|
||||
avg: 'average time',
|
||||
max: 'max. anomaly threshold'}.each_pair do |klass, label|
|
||||
.legend-entry
|
||||
div(class="box #{klass}")
|
||||
.box-label = label
|
@ -47,9 +47,12 @@ div id='output_sidebar_uncollapsed' class='hidden col-sm-12 enforce-bottom-margi
|
||||
input#prompt-input.form-control type='text'
|
||||
span.input-group-btn
|
||||
button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send')
|
||||
#error-hints
|
||||
.heading = t('exercises.implement.error_hints.heading')
|
||||
ul.body
|
||||
#output
|
||||
pre = t('exercises.implement.no_output_yet')
|
||||
- if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled]
|
||||
#flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab'
|
||||
.panel-heading = 'Gain more insights here'
|
||||
.panel-body
|
||||
.panel-body
|
||||
|
@ -1,19 +1,16 @@
|
||||
h1 = "#{@exercise} (external user #{@external_user})"
|
||||
- submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id).order("created_at")
|
||||
- current_submission = submissions.first
|
||||
- submissions_and_interventions = (submissions + UserExerciseIntervention.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id)).sort_by { |a| a.created_at }
|
||||
|
||||
- current_submission = @submissions.first
|
||||
- if current_submission
|
||||
- initial_files = current_submission.files.to_a
|
||||
|
||||
- all_files = []
|
||||
- file_types = Set.new()
|
||||
- submissions.each do |submission|
|
||||
- @submissions.each do |submission|
|
||||
- submission.files.each do |file|
|
||||
- file_types.add(ActiveSupport::JSON.encode(file.file_type))
|
||||
- all_files.push(submission.files)
|
||||
|
||||
.hidden#data data-submissions=ActiveSupport::JSON.encode(submissions) data-files=ActiveSupport::JSON.encode(all_files) data-file-types=ActiveSupport::JSON.encode(file_types)
|
||||
.hidden#data data-submissions=ActiveSupport::JSON.encode(@submissions) data-files=ActiveSupport::JSON.encode(all_files) data-file-types=ActiveSupport::JSON.encode(file_types)
|
||||
|
||||
#stats-editor.row
|
||||
- index = 0
|
||||
@ -27,14 +24,13 @@ h1 = "#{@exercise} (external user #{@external_user})"
|
||||
button.btn.btn-default id='play-button'
|
||||
span.fa.fa-play
|
||||
#submissions-slider.flex-item
|
||||
input type='range' orient='horizontal' list='datapoints' min=0 max=submissions.length-1 value=0
|
||||
input type='range' orient='horizontal' list='datapoints' min=0 max=@submissions.length-1 value=0
|
||||
datalist#datapoints
|
||||
- index=0
|
||||
- submissions.each do |submission|
|
||||
- @submissions.each do |submission|
|
||||
option data-submission=submission
|
||||
=index
|
||||
- index += 1
|
||||
- working_times_until = Array.new
|
||||
#timeline
|
||||
.table-responsive
|
||||
table.table
|
||||
@ -43,28 +39,27 @@ h1 = "#{@exercise} (external user #{@external_user})"
|
||||
- ['.time', '.cause', '.score', '.tests', '.time_difference'].each do |title|
|
||||
th.header = t(title)
|
||||
tbody
|
||||
- deltas = submissions.map.with_index {|item, index| delta = item.created_at - submissions[index - 1].created_at if index > 0; if delta == nil or delta > 10*60 then 0 else delta end}
|
||||
- submissions_and_interventions.each_with_index do |submission_or_intervention, index|
|
||||
tr data-id=submission_or_intervention.id
|
||||
td.clickable = submission_or_intervention.created_at.strftime("%F %T")
|
||||
- if submission_or_intervention.is_a?(Submission)
|
||||
td = submission_or_intervention.cause
|
||||
td = submission_or_intervention.score
|
||||
- @submissions_and_interventions.each_with_index do |this, index|
|
||||
- highlight = (index > 0 and @working_times_until[index] == @working_times_until[index - 1] and this.created_at > @submissions_and_interventions[index - 1].created_at)
|
||||
tr data-id=this.id class=('highlight' if highlight)
|
||||
td.clickable = this.created_at.strftime("%F %T")
|
||||
- if this.is_a?(Submission)
|
||||
td = this.cause
|
||||
td = this.score
|
||||
td
|
||||
-submission_or_intervention.testruns.each do |run|
|
||||
-this.testruns.each do |run|
|
||||
- if run.passed
|
||||
.unit-test-result.positive-result title=run.output
|
||||
- else
|
||||
.unit-test-result.unknown-result title=run.output
|
||||
td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0
|
||||
-working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0))
|
||||
- elsif submission_or_intervention.is_a? UserExerciseIntervention
|
||||
td = submission_or_intervention.intervention.name
|
||||
td = @working_times_until[index] if index > 0
|
||||
- elsif this.is_a? UserExerciseIntervention
|
||||
td = this.intervention.name
|
||||
td =
|
||||
td =
|
||||
td =
|
||||
p = t('.addendum')
|
||||
.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until);
|
||||
.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until);
|
||||
div#progress_chart.col-lg-12
|
||||
.graph-functions-2
|
||||
|
||||
|
@ -2,9 +2,15 @@ script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"
|
||||
h1 = @exercise
|
||||
|
||||
= row(label: '.participants', value: @exercise.users.distinct.count)
|
||||
|
||||
- [:intermediate, :final].each do |scope|
|
||||
= row(label: ".#{scope}_submissions") do
|
||||
= "#{@exercise.submissions.send(scope).count} (#{t('.users', count: @exercise.submissions.send(scope).distinct.count(:user_id))})"
|
||||
|
||||
= row(label: '.finishing_rate') do
|
||||
p == @exercise.finishers.count ? "#{t('shared.out_of', maximum_value: @exercise.users.distinct.count, value: @exercise.finishers.count)} #{t('exercises.statistics.external_users')}" : empty
|
||||
p = progress_bar((100.0 / @exercise.users.distinct.count * @exercise.finishers.count).round(2))
|
||||
|
||||
= row(label: '.average_score') do
|
||||
p == @exercise.average_score ? t('shared.out_of', maximum_value: @exercise.maximum_score, value: @exercise.average_score.round(2)) : empty
|
||||
p = progress_bar(@exercise.average_percentage)
|
||||
@ -40,4 +46,4 @@ h1 = @exercise
|
||||
td = link_to_if symbol==:external_users, label, {controller: "exercises", action: "statistics", external_user_id: user.id, id: @exercise.id}
|
||||
td = us['maximum_score'] or 0
|
||||
td = us['runs']
|
||||
td = @exercise.average_working_time_for(user.id) or 0
|
||||
td = @exercise.average_working_time_for(user.id) or 0
|
||||
|
@ -43,10 +43,20 @@
|
||||
<% output_runs = testruns.select { |run| run.cause == 'run' } %>
|
||||
<% if output_runs.size > 0 %>
|
||||
<h5><%= t('request_for_comments.runtime_output') %></h5>
|
||||
<div class="testrun-output text">
|
||||
<span class="fa fa-chevron-up collapse-button"></span>
|
||||
<div class="collapsed testrun-output text">
|
||||
<span class="fa fa-chevron-down collapse-button"></span>
|
||||
<% output_runs.each do |testrun| %>
|
||||
<pre><%= testrun.try(:output) or t('request_for_comments.no_output') %></pre>
|
||||
<%
|
||||
output = testrun.try(:output)
|
||||
if output
|
||||
messages = output.scan(/{(?:(?:".+?":".+?")+?,?)+}/)
|
||||
messages.map! {|el| JSON.parse(el)}
|
||||
messages.keep_if {|message| message['cmd'] == 'write'}
|
||||
messages.map! {|message| message['data']}
|
||||
output = messages.join ''
|
||||
end
|
||||
%>
|
||||
<pre><%= output or t('request_for_comments.no_output') %></pre>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@ -56,7 +66,13 @@
|
||||
<h5><%= t('request_for_comments.test_results') %></h5>
|
||||
<div class="testrun-assess-results">
|
||||
<% assess_runs.each do |testrun| %>
|
||||
<div class="result <%= testrun.passed ? 'passed' : 'failed' %>" title="<%= testrun.output %>"></div>
|
||||
<div class="testrun-container">
|
||||
<div class="result <%= testrun.passed ? 'passed' : 'failed' %>"></div>
|
||||
<div class="collapsed testrun-output text">
|
||||
<span class="fa fa-chevron-down collapse-button"></span>
|
||||
<pre><%= testrun.output or t('request_for_comments.no_output')%></pre>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
22
app/views/statistics/activity_history.html.slim
Normal file
22
app/views/statistics/activity_history.html.slim
Normal file
@ -0,0 +1,22 @@
|
||||
- content_for :head do
|
||||
= javascript_include_tag(asset_path('vis.min.js', type: :javascript))
|
||||
= stylesheet_link_tag(asset_path('vis.min.css', type: :stylesheet))
|
||||
|
||||
.group
|
||||
.title
|
||||
h1 = t("statistics.graphs.#{resource}_activity")
|
||||
.spinner
|
||||
.graph id="#{resource}-activity-history"
|
||||
form
|
||||
.form-group
|
||||
label for="from-date" = t('.from')
|
||||
input type="date" class="form-control" id="from-date" name="from" value=(params[:from] || DateTime.new(2016).to_date)
|
||||
.form-group
|
||||
label for="to-date" = t('.to')
|
||||
input type="date" class="form-control" id="to-date" name="to" value=(params[:to] || DateTime.now.to_date)
|
||||
.form-group
|
||||
label for="interval" = t('.interval')
|
||||
select class="form-control" id="interval" name="interval"
|
||||
= [:year, :quarter, :month, :day, :hour, :minute, :second].each do | key |
|
||||
option selected=(key.to_s == params[:interval]) = key
|
||||
button type="submit" class="btn btn-primary" = t('.update')
|
17
app/views/statistics/graphs.html.slim
Normal file
17
app/views/statistics/graphs.html.slim
Normal file
@ -0,0 +1,17 @@
|
||||
- content_for :head do
|
||||
= javascript_include_tag(asset_path('vis.min.js', type: :javascript))
|
||||
= stylesheet_link_tag(asset_path('vis.min.css', type: :stylesheet))
|
||||
|
||||
.group
|
||||
.title
|
||||
h1 = t('.user_activity')
|
||||
a href=statistics_graphs_user_activity_history_path = t('.history')
|
||||
.spinner
|
||||
.graph#user-activity
|
||||
|
||||
.group
|
||||
.title
|
||||
h1 = t('.rfc_activity')
|
||||
a href=statistics_graphs_rfc_activity_history_path = t('.history')
|
||||
.spinner
|
||||
.graph#rfc-activity
|
12
app/views/statistics/show.html.slim
Normal file
12
app/views/statistics/show.html.slim
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
#statistics-container
|
||||
- statistics_data.each do | section |
|
||||
h2 = section[:name]
|
||||
.statistics-wrapper data-key=section[:key]
|
||||
- section[:entries].each do | entry |
|
||||
a href=entry[:url]
|
||||
div data-key=entry[:key]
|
||||
.title = entry[:name]
|
||||
.data
|
||||
span = entry[:data].to_s
|
||||
span.unit = entry[:unit] if entry.key? :unit
|
38
app/views/user_mailer/exercise_anomaly_detected.html.slim
Normal file
38
app/views/user_mailer/exercise_anomaly_detected.html.slim
Normal file
@ -0,0 +1,38 @@
|
||||
== t('mailers.user_mailer.exercise_anomaly_detected.body1',
|
||||
receiver_displayname: @receiver_displayname,
|
||||
collection_name: @collection.name)
|
||||
|
||||
table(border=1)
|
||||
thead
|
||||
tr
|
||||
td = t('activerecord.attributes.exercise.title', locale: :de)
|
||||
td = t('exercises.statistics.average_worktime', locale: :de)
|
||||
td = t('shared.actions', locale: :de)
|
||||
tbody
|
||||
- @anomalies.keys.each do | id |
|
||||
- exercise = Exercise.find(id)
|
||||
tr
|
||||
td = link_to(exercise.title, exercise_path(exercise))
|
||||
td = @anomalies[id]
|
||||
td = link_to(t('shared.statistics', locale: :de), statistics_exercise_path(exercise))
|
||||
|
||||
|
||||
== t('mailers.user_mailer.exercise_anomaly_detected.body2',
|
||||
receiver_displayname: @receiver_displayname,
|
||||
collection_name: @collection.name)
|
||||
|
||||
table(border=1)
|
||||
thead
|
||||
tr
|
||||
td = t('activerecord.attributes.exercise.title', locale: :en)
|
||||
td = t('exercises.statistics.average_worktime', locale: :en)
|
||||
td = t('shared.actions', locale: :en)
|
||||
tbody
|
||||
- @anomalies.keys.each do | id |
|
||||
- exercise = Exercise.find(id)
|
||||
tr
|
||||
td = link_to(exercise.title, exercise_path(exercise))
|
||||
td = @anomalies[id]
|
||||
td = link_to(t('shared.statistics', locale: :en), statistics_exercise_path(exercise))
|
||||
|
||||
== t('mailers.user_mailer.exercise_anomaly_detected.body3')
|
@ -0,0 +1 @@
|
||||
== t('mailers.user_mailer.exercise_anomaly_needs_feedback.body', receiver_displayname: @receiver_displayname, exercise: @exercise_title, link: link_to(@link, @link))
|
Reference in New Issue
Block a user