merged master into disable_rfcs

This commit is contained in:
Ralf Teusner
2018-05-16 17:44:28 +02:00
82 changed files with 2007 additions and 376 deletions

View 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);
});

View File

@ -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
},

View File

@ -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();
}
};
};

View File

@ -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();
}
};
};

View File

@ -108,5 +108,5 @@ CommandSocket.prototype.flush = function() {
*/
CommandSocket.prototype.killWebSocket = function() {
this.websocket.flush();
this.websocket.close();
};
this.websocket.close(1000);
};

View 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);
}
});

View File

@ -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);
}
}

View 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');
}
});

View 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);
}
});

View File

@ -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]);

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

View File

@ -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 {}
}
}
}

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

View File

@ -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 {

View File

@ -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);
}
}

View File

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

View File

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

View File

@ -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 = {}

View 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

View File

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

View File

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

View 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

View 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

View File

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

View File

@ -0,0 +1,5 @@
class AnomalyNotification < ActiveRecord::Base
belongs_to :user, polymorphic: true
belongs_to :exercise
belongs_to :exercise_collection
end

View File

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

View File

@ -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})"

View File

@ -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")

View File

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

View File

@ -1,3 +1,7 @@
class ExerciseCollectionPolicy < AdminOnlyPolicy
def statistics?
admin?
end
end

View 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

View File

@ -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])

View File

@ -0,0 +1,2 @@
- if policy(model).index?
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))

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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View 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

View File

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

View File

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

View File

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

View File

@ -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 %>

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

View 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

View 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

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

View File

@ -0,0 +1 @@
== t('mailers.user_mailer.exercise_anomaly_needs_feedback.body', receiver_displayname: @receiver_displayname, exercise: @exercise_title, link: link_to(@link, @link))