Merge remote-tracking branch 'origin/master' into fix/exercise-anomaly-detection-nil-values
This commit is contained in:
6
Gemfile
6
Gemfile
@ -25,10 +25,10 @@ gem 'rails', '4.2.10'
|
|||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'ransack'
|
gem 'ransack'
|
||||||
gem 'rubytree'
|
gem 'rubytree'
|
||||||
gem 'sass-rails'
|
gem 'sass-rails', '>= 5.0.7'
|
||||||
gem 'sdoc', group: :doc
|
gem 'sdoc', group: :doc
|
||||||
gem 'slim-rails'
|
gem 'slim-rails'
|
||||||
gem 'bootstrap_pagedown'
|
gem 'bootstrap_pagedown', '>= 1.1.0'
|
||||||
gem 'pagedown-rails'
|
gem 'pagedown-rails'
|
||||||
gem 'sorcery'
|
gem 'sorcery'
|
||||||
gem 'thread_safe'
|
gem 'thread_safe'
|
||||||
@ -66,7 +66,7 @@ end
|
|||||||
group :test do
|
group :test do
|
||||||
gem 'autotest-rails'
|
gem 'autotest-rails'
|
||||||
gem 'capybara'
|
gem 'capybara'
|
||||||
gem 'capybara-selenium'
|
gem 'capybara-selenium', '>= 0.0.6'
|
||||||
gem 'headless'
|
gem 'headless'
|
||||||
gem 'codeclimate-test-reporter', require: false
|
gem 'codeclimate-test-reporter', require: false
|
||||||
gem 'database_cleaner'
|
gem 'database_cleaner'
|
||||||
|
42
Gemfile.lock
42
Gemfile.lock
@ -78,13 +78,13 @@ GEM
|
|||||||
capistrano (~> 3.7)
|
capistrano (~> 3.7)
|
||||||
capistrano-bundler
|
capistrano-bundler
|
||||||
puma (~> 3.4)
|
puma (~> 3.4)
|
||||||
capybara (2.18.0)
|
capybara (3.3.1)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (>= 1.3.3)
|
nokogiri (~> 1.8)
|
||||||
rack (>= 1.0.0)
|
rack (>= 1.6.0)
|
||||||
rack-test (>= 0.5.4)
|
rack-test (>= 0.6.3)
|
||||||
xpath (>= 2.0, < 4.0)
|
xpath (~> 3.1)
|
||||||
capybara-selenium (0.0.6)
|
capybara-selenium (0.0.6)
|
||||||
capybara
|
capybara
|
||||||
selenium-webdriver
|
selenium-webdriver
|
||||||
@ -92,7 +92,7 @@ GEM
|
|||||||
activemodel (>= 4.0.0)
|
activemodel (>= 4.0.0)
|
||||||
activesupport (>= 4.0.0)
|
activesupport (>= 4.0.0)
|
||||||
mime-types (>= 1.16)
|
mime-types (>= 1.16)
|
||||||
childprocess (0.8.0)
|
childprocess (0.9.0)
|
||||||
ffi (~> 1.0, >= 1.0.11)
|
ffi (~> 1.0, >= 1.0.11)
|
||||||
chronic (0.10.2)
|
chronic (0.10.2)
|
||||||
codeclimate-test-reporter (1.0.7)
|
codeclimate-test-reporter (1.0.7)
|
||||||
@ -108,7 +108,7 @@ GEM
|
|||||||
concurrent-ruby (1.0.5)
|
concurrent-ruby (1.0.5)
|
||||||
concurrent-ruby-ext (1.0.5)
|
concurrent-ruby-ext (1.0.5)
|
||||||
concurrent-ruby (= 1.0.5)
|
concurrent-ruby (= 1.0.5)
|
||||||
crass (1.0.3)
|
crass (1.0.4)
|
||||||
d3-rails (4.13.0)
|
d3-rails (4.13.0)
|
||||||
railties (>= 3.1)
|
railties (>= 3.1)
|
||||||
database_cleaner (1.6.2)
|
database_cleaner (1.6.2)
|
||||||
@ -135,7 +135,7 @@ GEM
|
|||||||
faye-websocket (0.10.7)
|
faye-websocket (0.10.7)
|
||||||
eventmachine (>= 0.12.0)
|
eventmachine (>= 0.12.0)
|
||||||
websocket-driver (>= 0.5.1)
|
websocket-driver (>= 0.5.1)
|
||||||
ffi (1.9.23)
|
ffi (1.9.25)
|
||||||
forgery (0.7.0)
|
forgery (0.7.0)
|
||||||
globalid (0.4.1)
|
globalid (0.4.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
@ -161,7 +161,7 @@ GEM
|
|||||||
json (2.1.0)
|
json (2.1.0)
|
||||||
jwt (1.5.6)
|
jwt (1.5.6)
|
||||||
kramdown (1.16.2)
|
kramdown (1.16.2)
|
||||||
loofah (2.2.0)
|
loofah (2.2.2)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.0)
|
mail (2.7.0)
|
||||||
@ -181,7 +181,7 @@ GEM
|
|||||||
net-ssh (4.2.0)
|
net-ssh (4.2.0)
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
newrelic_rpm (4.8.0.341)
|
newrelic_rpm (4.8.0.341)
|
||||||
nokogiri (1.8.2)
|
nokogiri (1.8.3)
|
||||||
mini_portile2 (~> 2.3.0)
|
mini_portile2 (~> 2.3.0)
|
||||||
nyan-cat-formatter (0.12.0)
|
nyan-cat-formatter (0.12.0)
|
||||||
rspec (>= 2.99, >= 2.14.2, < 4)
|
rspec (>= 2.99, >= 2.14.2, < 4)
|
||||||
@ -211,7 +211,7 @@ GEM
|
|||||||
puma (3.11.3)
|
puma (3.11.3)
|
||||||
pundit (1.1.0)
|
pundit (1.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
rack (1.6.9)
|
rack (1.6.10)
|
||||||
rack-mini-profiler (0.10.7)
|
rack-mini-profiler (0.10.7)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-test (0.6.3)
|
rack-test (0.6.3)
|
||||||
@ -233,8 +233,8 @@ GEM
|
|||||||
activesupport (>= 4.2.0, < 5.0)
|
activesupport (>= 4.2.0, < 5.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
rails-deprecated_sanitizer (>= 1.0.1)
|
rails-deprecated_sanitizer (>= 1.0.1)
|
||||||
rails-html-sanitizer (1.0.3)
|
rails-html-sanitizer (1.0.4)
|
||||||
loofah (~> 2.0)
|
loofah (~> 2.2, >= 2.2.2)
|
||||||
rails-i18n (4.0.9)
|
rails-i18n (4.0.9)
|
||||||
i18n (~> 0.7)
|
i18n (~> 0.7)
|
||||||
railties (~> 4.0)
|
railties (~> 4.0)
|
||||||
@ -244,7 +244,7 @@ GEM
|
|||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.18.1, < 2.0)
|
thor (>= 0.18.1, < 2.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.0.0)
|
||||||
rake (12.3.0)
|
rake (12.3.1)
|
||||||
ransack (1.8.7)
|
ransack (1.8.7)
|
||||||
actionpack (>= 3.0)
|
actionpack (>= 3.0)
|
||||||
activerecord (>= 3.0)
|
activerecord (>= 3.0)
|
||||||
@ -296,7 +296,7 @@ GEM
|
|||||||
json (~> 2.1)
|
json (~> 2.1)
|
||||||
structured_warnings (~> 0.3)
|
structured_warnings (~> 0.3)
|
||||||
rubyzip (1.2.1)
|
rubyzip (1.2.1)
|
||||||
sass (3.5.5)
|
sass (3.5.6)
|
||||||
sass-listen (~> 4.0.0)
|
sass-listen (~> 4.0.0)
|
||||||
sass-listen (4.0.0)
|
sass-listen (4.0.0)
|
||||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||||
@ -309,7 +309,7 @@ GEM
|
|||||||
tilt (>= 1.1, < 3)
|
tilt (>= 1.1, < 3)
|
||||||
sdoc (1.0.0)
|
sdoc (1.0.0)
|
||||||
rdoc (>= 5.0)
|
rdoc (>= 5.0)
|
||||||
selenium-webdriver (3.10.0)
|
selenium-webdriver (3.13.0)
|
||||||
childprocess (~> 0.5)
|
childprocess (~> 0.5)
|
||||||
rubyzip (~> 1.2)
|
rubyzip (~> 1.2)
|
||||||
simplecov (0.15.1)
|
simplecov (0.15.1)
|
||||||
@ -330,7 +330,7 @@ GEM
|
|||||||
oauth2 (~> 1.0, >= 0.8.0)
|
oauth2 (~> 1.0, >= 0.8.0)
|
||||||
spring (2.0.2)
|
spring (2.0.2)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
sprockets (3.7.1)
|
sprockets (3.7.2)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
sprockets-rails (3.2.1)
|
sprockets-rails (3.2.1)
|
||||||
@ -369,7 +369,7 @@ GEM
|
|||||||
whenever (0.10.0)
|
whenever (0.10.0)
|
||||||
chronic (>= 0.6.3)
|
chronic (>= 0.6.3)
|
||||||
will_paginate (3.1.6)
|
will_paginate (3.1.6)
|
||||||
xpath (3.0.0)
|
xpath (3.1.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
@ -383,7 +383,7 @@ DEPENDENCIES
|
|||||||
better_errors
|
better_errors
|
||||||
binding_of_caller
|
binding_of_caller
|
||||||
bootstrap-will_paginate
|
bootstrap-will_paginate
|
||||||
bootstrap_pagedown
|
bootstrap_pagedown (>= 1.1.0)
|
||||||
byebug
|
byebug
|
||||||
capistrano
|
capistrano
|
||||||
capistrano-rails
|
capistrano-rails
|
||||||
@ -391,7 +391,7 @@ DEPENDENCIES
|
|||||||
capistrano-upload-config
|
capistrano-upload-config
|
||||||
capistrano3-puma
|
capistrano3-puma
|
||||||
capybara
|
capybara
|
||||||
capybara-selenium
|
capybara-selenium (>= 0.0.6)
|
||||||
carrierwave
|
carrierwave
|
||||||
codeclimate-test-reporter
|
codeclimate-test-reporter
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
@ -430,7 +430,7 @@ DEPENDENCIES
|
|||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
rubytree
|
rubytree
|
||||||
rubyzip
|
rubyzip
|
||||||
sass-rails
|
sass-rails (>= 5.0.7)
|
||||||
sdoc
|
sdoc
|
||||||
simplecov
|
simplecov
|
||||||
slim-rails
|
slim-rails
|
||||||
|
@ -153,18 +153,22 @@ CodeOceanEditorEvaluation = {
|
|||||||
if (!colorize) {
|
if (!colorize) {
|
||||||
if (output.stdout != undefined && output.stdout != '') {
|
if (output.stdout != undefined && output.stdout != '') {
|
||||||
element.append(output.stdout)
|
element.append(output.stdout)
|
||||||
|
//element.text(element.text() + output.stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output.stderr != undefined && output.stderr != '') {
|
if (output.stderr != undefined && output.stderr != '') {
|
||||||
element.append('StdErr: ' + output.stderr);
|
element.append('StdErr: ' + output.stderr);
|
||||||
|
//element.text('StdErr: ' + element.text() + output.stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (output.stderr) {
|
} else if (output.stderr) {
|
||||||
element.addClass('text-warning').append(output.stderr);
|
element.addClass('text-warning').append(output.stderr);
|
||||||
|
//element.addClass('text-warning').text(element.text() + output.stderr);
|
||||||
this.flowrOutputBuffer += output.stderr;
|
this.flowrOutputBuffer += output.stderr;
|
||||||
this.QaApiOutputBuffer.stderr += output.stderr;
|
this.QaApiOutputBuffer.stderr += output.stderr;
|
||||||
} else if (output.stdout) {
|
} else if (output.stdout) {
|
||||||
element.addClass('text-success').append(output.stdout);
|
element.addClass('text-success').append(output.stdout);
|
||||||
|
//element.addClass('text-success').text(element.text() + output.stdout);
|
||||||
this.flowrOutputBuffer += output.stdout;
|
this.flowrOutputBuffer += output.stdout;
|
||||||
this.QaApiOutputBuffer.stdout += output.stdout;
|
this.QaApiOutputBuffer.stdout += output.stdout;
|
||||||
} else {
|
} else {
|
||||||
|
@ -89,7 +89,8 @@ CodeOceanEditorRequestForComments = {
|
|||||||
this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this));
|
this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this));
|
||||||
|
|
||||||
$('#comment-modal').modal('hide');
|
$('#comment-modal').modal('hide');
|
||||||
var button = $('#requestComments');
|
// we disabled the button to prevent that the user spams RFCs, but decided against this now.
|
||||||
button.prop('disabled', true);
|
//var button = $('#requestComments');
|
||||||
|
//button.prop('disabled', true);
|
||||||
},
|
},
|
||||||
};
|
};
|
@ -1,9 +1,13 @@
|
|||||||
$(function() {
|
$(function() {
|
||||||
if ($.isController('exercise_collections')) {
|
if ($.isController('exercise_collections')) {
|
||||||
var data = $('#data').data('working-times');
|
var dataElement = $('#data');
|
||||||
var averageWorkingTimeValue = parseFloat($('#data').data('average-working-time'));
|
var exerciseList = $('#exercise-list');
|
||||||
|
|
||||||
var margin = { top: 30, right: 40, bottom: 30, left: 50 },
|
if (dataElement.isPresent()) {
|
||||||
|
var data = dataElement.data('working-times');
|
||||||
|
var averageWorkingTimeValue = parseFloat(dataElement.data('average-working-time'));
|
||||||
|
|
||||||
|
var margin = {top: 30, right: 40, bottom: 30, left: 50},
|
||||||
width = 720 - margin.left - margin.right,
|
width = 720 - margin.left - margin.right,
|
||||||
height = 500 - margin.top - margin.bottom;
|
height = 500 - margin.top - margin.bottom;
|
||||||
|
|
||||||
@ -16,16 +20,28 @@ $(function() {
|
|||||||
var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip");
|
var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip");
|
||||||
|
|
||||||
var averageWorkingTime = d3.line()
|
var averageWorkingTime = d3.line()
|
||||||
.x(function (d) { return x(d.index) + x.bandwidth()/2; })
|
.x(function (d) {
|
||||||
.y(function () { return y(averageWorkingTimeValue); });
|
return x(d.index) + x.bandwidth() / 2;
|
||||||
|
})
|
||||||
|
.y(function () {
|
||||||
|
return y(averageWorkingTimeValue);
|
||||||
|
});
|
||||||
|
|
||||||
var minWorkingTime = d3.line()
|
var minWorkingTime = d3.line()
|
||||||
.x(function (d) { return x(d.index) + x.bandwidth()/2; })
|
.x(function (d) {
|
||||||
.y(function () { return y(0.1*averageWorkingTimeValue); });
|
return x(d.index) + x.bandwidth() / 2;
|
||||||
|
})
|
||||||
|
.y(function () {
|
||||||
|
return y(0.1 * averageWorkingTimeValue);
|
||||||
|
});
|
||||||
|
|
||||||
var maxWorkingTime = d3.line()
|
var maxWorkingTime = d3.line()
|
||||||
.x(function (d) { return x(d.index) + x.bandwidth()/2; })
|
.x(function (d) {
|
||||||
.y(function () { return y(2*averageWorkingTimeValue); });
|
return x(d.index) + x.bandwidth() / 2;
|
||||||
|
})
|
||||||
|
.y(function () {
|
||||||
|
return y(2 * averageWorkingTimeValue);
|
||||||
|
});
|
||||||
|
|
||||||
var svg = d3.select('#graph')
|
var svg = d3.select('#graph')
|
||||||
.append("svg")
|
.append("svg")
|
||||||
@ -36,17 +52,22 @@ $(function() {
|
|||||||
"translate(" + margin.left + "," + margin.top + ")");
|
"translate(" + margin.left + "," + margin.top + ")");
|
||||||
|
|
||||||
// Get the data
|
// Get the data
|
||||||
data = Object.keys(data).map(function (key, index) {
|
data = Object.keys(data).map(function (key) {
|
||||||
return {
|
return {
|
||||||
index: index,
|
index: parseInt(key),
|
||||||
exercise_id: parseInt(key),
|
exercise_id: parseInt(data[key]['exercise_id']),
|
||||||
working_time: parseFloat(data[key])
|
exercise_title: data[key]['exercise_title'],
|
||||||
|
working_time: parseFloat(data[key]['working_time'])
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Scale the range of the data
|
// Scale the range of the data
|
||||||
x.domain(data.map(function (d) { return d.index; }));
|
x.domain(data.map(function (d) {
|
||||||
y.domain([0, d3.max(data, function (d) { return d.working_time; })]);
|
return d.index;
|
||||||
|
}));
|
||||||
|
y.domain([0, d3.max(data, function (d) {
|
||||||
|
return d.working_time;
|
||||||
|
})]);
|
||||||
|
|
||||||
// Add the X Axis
|
// Add the X Axis
|
||||||
svg.append("g")
|
svg.append("g")
|
||||||
@ -66,22 +87,31 @@ $(function() {
|
|||||||
.enter()
|
.enter()
|
||||||
.append("rect")
|
.append("rect")
|
||||||
.attr("class", "value-bar")
|
.attr("class", "value-bar")
|
||||||
.on("mousemove", function (d){
|
.on("mousemove", function (d) {
|
||||||
tooltip
|
tooltip
|
||||||
.style("left", d3.event.pageX - 50 + "px")
|
.style("left", d3.event.pageX - 50 + "px")
|
||||||
.style("top", d3.event.pageY + 50 + "px")
|
.style("top", d3.event.pageY + 50 + "px")
|
||||||
.style("display", "inline-block")
|
.style("display", "inline-block")
|
||||||
.html("<%= I18n.t('activerecord.models.exercise.one') %> ID: " + d.exercise_id + "<br>" +
|
.html("<%= I18n.t('activerecord.models.exercise.one') %> ID: " + d.exercise_id + "<br>" +
|
||||||
|
"<%= I18n.t('activerecord.attributes.exercise.title') %>: " + d.exercise_title + "<br>" +
|
||||||
"<%= I18n.t('exercises.statistics.average_worktime') %>: " + d.working_time + "s");
|
"<%= I18n.t('exercises.statistics.average_worktime') %>: " + d.working_time + "s");
|
||||||
})
|
})
|
||||||
.on("mouseout", function (){ tooltip.style("display", "none");})
|
.on("mouseout", function () {
|
||||||
|
tooltip.style("display", "none");
|
||||||
|
})
|
||||||
.on("click", function (d) {
|
.on("click", function (d) {
|
||||||
window.location.href = "/exercises/" + d.exercise_id + "/statistics";
|
window.location.href = "/exercises/" + d.exercise_id + "/statistics";
|
||||||
})
|
})
|
||||||
.attr("x", function (d) { return x(d.index); })
|
.attr("x", function (d) {
|
||||||
|
return x(d.index);
|
||||||
|
})
|
||||||
.attr("width", x.bandwidth())
|
.attr("width", x.bandwidth())
|
||||||
.attr("y", function (d) { return y(d.working_time); })
|
.attr("y", function (d) {
|
||||||
.attr("height", function (d) { return height - y(d.working_time); });
|
return y(d.working_time);
|
||||||
|
})
|
||||||
|
.attr("height", function (d) {
|
||||||
|
return height - y(d.working_time);
|
||||||
|
});
|
||||||
|
|
||||||
// Add the average working time path
|
// Add the average working time path
|
||||||
svg.append("path")
|
svg.append("path")
|
||||||
@ -98,5 +128,80 @@ $(function() {
|
|||||||
.datum(data)
|
.datum(data)
|
||||||
.attr("class", "line maximum-working-time")
|
.attr("class", "line maximum-working-time")
|
||||||
.attr("d", maxWorkingTime);
|
.attr("d", maxWorkingTime);
|
||||||
|
} else if (exerciseList.isPresent()) {
|
||||||
|
var exerciseSelect = $('#exercise-select');
|
||||||
|
var list = $("#sortable");
|
||||||
|
|
||||||
|
var updateExerciseList = function () {
|
||||||
|
// remove all options from the hidden select and add all selected exercises in the new order
|
||||||
|
exerciseSelect.find('option').remove();
|
||||||
|
var exerciseIdsInSortedOrder = list.sortable('toArray', {attribute: 'data-id'});
|
||||||
|
for (var i = 0; i < exerciseIdsInSortedOrder.length; i += 1) {
|
||||||
|
exerciseSelect.append('<option value="' + exerciseIdsInSortedOrder[i] + '" selected></option>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.sortable({
|
||||||
|
items: 'tr',
|
||||||
|
update: updateExerciseList
|
||||||
|
});
|
||||||
|
list.disableSelection();
|
||||||
|
|
||||||
|
var addExercisesForm = $('#exercise-selection');
|
||||||
|
var addExercisesButton = $('#add-exercises');
|
||||||
|
var removeExerciseButtons = $('.remove-exercise');
|
||||||
|
var sortButton = $('#sort-button');
|
||||||
|
|
||||||
|
var collectContainedExercises = function () {
|
||||||
|
return exerciseList.find('tbody > tr').toArray().map(function (item) {return $(item).data('id')});
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortExercises = function() {
|
||||||
|
var listitems = $('tr', list);
|
||||||
|
listitems.sort(function (a, b) {
|
||||||
|
return ($(a).find('td:nth-child(2)').text().toUpperCase() > $(b).find('td:nth-child(2)').text().toUpperCase()) ? 1 : -1;
|
||||||
|
});
|
||||||
|
list.append(listitems);
|
||||||
|
list.sortable('refresh');
|
||||||
|
updateExerciseList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var addExercise = function (id, title) {
|
||||||
|
var exercise = {id: id, title: title}
|
||||||
|
var collectionExercises = collectContainedExercises();
|
||||||
|
if (collectionExercises.indexOf(exercise.id) === -1) {
|
||||||
|
// only add exercises that are not already contained in the collection
|
||||||
|
var template = '<tr data-id="' + exercise.id + '">' +
|
||||||
|
'<td><span class="fa fa-bars"></span></td>' +
|
||||||
|
'<td>' + exercise.title + '</td>' +
|
||||||
|
'<td><a href="/exercises/' + exercise.id + '"><%= I18n.t('shared.show') %></td>' +
|
||||||
|
'<td><a class="remove-exercise" href="#"><%= I18n.t('shared.destroy') %></td></tr>';
|
||||||
|
exerciseList.find('tbody').append(template);
|
||||||
|
$('#exercise-list').find('option[value="' + exercise.id + '"]').prop('selected', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addExercisesButton.on('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var selectedExercises = addExercisesForm.find('select')[0].selectedOptions;
|
||||||
|
for (var i = 0; i < selectedExercises.length; i++) {
|
||||||
|
addExercise(selectedExercises[i].value, selectedExercises[i].label);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
removeExerciseButtons.on('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var row = $(this).parent().parent();
|
||||||
|
var exerciseId = row.data('id');
|
||||||
|
$('#exercise-list').find('option[value="' + exerciseId + '"]').prop('selected', false);
|
||||||
|
row.remove()
|
||||||
|
});
|
||||||
|
|
||||||
|
sortButton.on('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
sortExercises();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -57,6 +57,10 @@ rect.value-bar {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-responsive#exercise-list {
|
||||||
|
max-height: 512px;
|
||||||
|
}
|
||||||
|
|
||||||
.exercise-id-tooltip {
|
.exercise-id-tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: none;
|
display: none;
|
||||||
@ -67,3 +71,19 @@ rect.value-bar {
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#exercise-list {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-exercise-list {
|
||||||
|
min-height: 450px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exercise-actions {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,7 +9,6 @@ class ExerciseCollectionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@exercises = @exercise_collection.exercises.paginate(:page => params[:page])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@ -50,6 +49,8 @@ class ExerciseCollectionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def exercise_collection_params
|
def exercise_collection_params
|
||||||
params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []).merge(user_type: InternalUser.name)
|
sanitized_params = params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []).merge(user_type: InternalUser.name)
|
||||||
|
sanitized_params[:exercise_ids] = sanitized_params[:exercise_ids].reject {|v| v.nil? or v == ''}
|
||||||
|
sanitized_params.tap {|p| p[:exercise_collection_items] = p[:exercise_ids].map.with_index {|_id, index| ExerciseCollectionItem.find_or_create_by(exercise_id: _id, exercise_collection_id: @exercise_collection.id, position: index)}; p.delete(:exercise_ids)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -14,7 +14,8 @@ class Exercise < ActiveRecord::Base
|
|||||||
|
|
||||||
has_and_belongs_to_many :proxy_exercises
|
has_and_belongs_to_many :proxy_exercises
|
||||||
has_many :user_proxy_exercise_exercises
|
has_many :user_proxy_exercise_exercises
|
||||||
has_and_belongs_to_many :exercise_collections
|
has_many :exercise_collection_items
|
||||||
|
has_many :exercise_collections, through: :exercise_collection_items
|
||||||
has_many :user_exercise_interventions
|
has_many :user_exercise_interventions
|
||||||
has_many :interventions, through: :user_exercise_interventions
|
has_many :interventions, through: :user_exercise_interventions
|
||||||
has_many :exercise_tags
|
has_many :exercise_tags
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
class ExerciseCollection < ActiveRecord::Base
|
class ExerciseCollection < ActiveRecord::Base
|
||||||
include TimeHelper
|
include TimeHelper
|
||||||
|
|
||||||
has_and_belongs_to_many :exercises
|
has_many :exercise_collection_items
|
||||||
|
alias_method :items, :exercise_collection_items
|
||||||
|
has_many :exercises, through: :exercise_collection_items
|
||||||
belongs_to :user, polymorphic: true
|
belongs_to :user, polymorphic: true
|
||||||
|
|
||||||
def exercise_working_times
|
def collection_statistics
|
||||||
working_times = {}
|
statistics = {}
|
||||||
exercises.each do |exercise|
|
exercise_collection_items.each do |item|
|
||||||
working_times[exercise.id] = time_to_f exercise.average_working_time
|
statistics[item.position] = {exercise_id: item.exercise.id, exercise_title: item.exercise.title, working_time: time_to_f(item.exercise.average_working_time)}
|
||||||
end
|
end
|
||||||
working_times
|
statistics
|
||||||
end
|
end
|
||||||
|
|
||||||
def average_working_time
|
def average_working_time
|
||||||
if exercises.empty?
|
if exercises.empty?
|
||||||
0
|
0
|
||||||
else
|
else
|
||||||
values = exercise_working_times.values.reject { |v| v.nil?}
|
values = collection_statistics.values.reject { |o| o[:working_time].nil?}
|
||||||
values.reduce(:+) / exercises.size
|
sum = values.reduce(0) {|sum, item| sum + item[:working_time]}
|
||||||
|
sum / values.size
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
4
app/models/exercise_collection_item.rb
Normal file
4
app/models/exercise_collection_item.rb
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
class ExerciseCollectionItem < ActiveRecord::Base
|
||||||
|
belongs_to :exercise_collection
|
||||||
|
belongs_to :exercise
|
||||||
|
end
|
@ -36,8 +36,23 @@ class ProxyExercise < ActiveRecord::Base
|
|||||||
Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" )
|
Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" )
|
||||||
assigned_user_proxy_exercise.exercise
|
assigned_user_proxy_exercise.exercise
|
||||||
else
|
else
|
||||||
Rails.logger.debug("find new matching exercise for user #{user.id}" )
|
|
||||||
matching_exercise =
|
matching_exercise =
|
||||||
|
if (token.eql? "e85689d5")
|
||||||
|
Rails.logger.debug("Proxy exercise with token e85689d5, split user in groups..")
|
||||||
|
group = UserGroupSeparator.getGroupExerciseDescriptionTesting(user)
|
||||||
|
Rails.logger.debug("user assigned to group #{group}")
|
||||||
|
case group
|
||||||
|
when :group_a
|
||||||
|
exercises.where(id: 557).first
|
||||||
|
when :group_b
|
||||||
|
exercises.where(id: 558).first
|
||||||
|
when :group_c
|
||||||
|
exercises.where(id: 559).first
|
||||||
|
when :group_d
|
||||||
|
exercises.where(id: 560).first
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Rails.logger.debug("find new matching exercise for user #{user.id}" )
|
||||||
begin
|
begin
|
||||||
find_matching_exercise(user)
|
find_matching_exercise(user)
|
||||||
rescue => e #fallback
|
rescue => e #fallback
|
||||||
@ -46,6 +61,7 @@ class ProxyExercise < ActiveRecord::Base
|
|||||||
@reason[:error] = "#{$!}:\n\t#{e.backtrace.join("\n\t")}"
|
@reason[:error] = "#{$!}:\n\t#{e.backtrace.join("\n\t")}"
|
||||||
exercises.where("expected_difficulty > 1").shuffle.first # difficulty should be > 1 to prevent dummy exercise from being chosen.
|
exercises.where("expected_difficulty > 1").shuffle.first # difficulty should be > 1 to prevent dummy exercise from being chosen.
|
||||||
end
|
end
|
||||||
|
end
|
||||||
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
|
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
|
||||||
matching_exercise
|
matching_exercise
|
||||||
end
|
end
|
||||||
|
8
app/views/exercise_collections/_add_exercise_modal.slim
Normal file
8
app/views/exercise_collections/_add_exercise_modal.slim
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
- exercises = Exercise.order(:title)
|
||||||
|
|
||||||
|
form#exercise-selection
|
||||||
|
.form-group
|
||||||
|
span.label = t('activerecord.attributes.exercise_collections.exercises')
|
||||||
|
= collection_select({}, :exercise_ids, exercises, :id, :title, {}, {id: 'add-exercise-list', class: 'form-control', multiple: true})
|
||||||
|
|
||||||
|
button.btn.btn-primary#add-exercises = t('exercise_collections.form.add_exercises')
|
@ -1,7 +1,4 @@
|
|||||||
- exercises = Exercise.order(:title)
|
= form_for(@exercise_collection, multipart: true) do |f|
|
||||||
- users = InternalUser.order(:name)
|
|
||||||
|
|
||||||
= form_for(@exercise_collection, data: {exercises: exercises, users: users}, multipart: true) do |f|
|
|
||||||
= render('shared/form_errors', object: @exercise_collection)
|
= render('shared/form_errors', object: @exercise_collection)
|
||||||
.form-group
|
.form-group
|
||||||
= f.label(t('activerecord.attributes.exercise_collections.name'))
|
= f.label(t('activerecord.attributes.exercise_collections.name'))
|
||||||
@ -11,8 +8,30 @@
|
|||||||
= f.check_box(:use_anomaly_detection, {class: 'form-control'})
|
= f.check_box(:use_anomaly_detection, {class: 'form-control'})
|
||||||
.form-group
|
.form-group
|
||||||
= f.label(t('activerecord.attributes.exercise_collections.user'))
|
= f.label(t('activerecord.attributes.exercise_collections.user'))
|
||||||
= f.collection_select(:user_id, users, :id, :name, {}, {class: 'form-control'})
|
= f.collection_select(:user_id, InternalUser.order(:name), :id, :name, {}, {class: 'form-control'})
|
||||||
.form-group
|
|
||||||
= f.label(t('activerecord.attributes.exercise_collections.exercises'))
|
.table-responsive#exercise-list
|
||||||
= f.collection_select(:exercise_ids, exercises, :id, :title, {}, {class: 'form-control', multiple: true})
|
table.table
|
||||||
|
thead
|
||||||
|
tr
|
||||||
|
th
|
||||||
|
th = t('activerecord.attributes.exercise_collection_item.exercise')
|
||||||
|
th colspan=2 = t('shared.actions')
|
||||||
|
tbody#sortable
|
||||||
|
- @exercise_collection.items.order(:position).each do |item|
|
||||||
|
tr data-id=item.exercise.id
|
||||||
|
td
|
||||||
|
span.fa.fa-bars
|
||||||
|
td = item.exercise.title
|
||||||
|
td = link_to(t('shared.show'), item.exercise)
|
||||||
|
td
|
||||||
|
a.remove-exercise href='#' = t('shared.destroy')
|
||||||
|
.hidden
|
||||||
|
= f.collection_select(:exercise_ids, Exercise.all, :id, :title, {}, {id: 'exercise-select', class: 'form-control', multiple: true})
|
||||||
|
.exercise-actions
|
||||||
|
button.btn.btn-primary type='button' data-toggle='modal' data-target='#add-exercise-modal' = t('exercise_collections.form.add_exercises')
|
||||||
|
button.btn.btn-secondary#sort-button type='button' = t('exercise_collections.form.sort_by_title')
|
||||||
|
|
||||||
.actions = render('shared/submit_button', f: f, object: @exercise_collection)
|
.actions = render('shared/submit_button', f: f, object: @exercise_collection)
|
||||||
|
|
||||||
|
= render('shared/modal', id: 'add-exercise-modal', title: t('.add_exercises'), template: 'exercise_collections/_add_exercise_modal')
|
||||||
|
@ -8,20 +8,21 @@ h1
|
|||||||
= row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at)
|
= row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at)
|
||||||
|
|
||||||
h4 = t('activerecord.attributes.exercise_collections.exercises')
|
h4 = t('activerecord.attributes.exercise_collections.exercises')
|
||||||
.table-responsive
|
.table-responsive#exercise-list
|
||||||
table.table
|
table.table
|
||||||
thead
|
thead
|
||||||
tr
|
tr
|
||||||
|
th = '#'
|
||||||
th = t('activerecord.attributes.exercise.title')
|
th = t('activerecord.attributes.exercise.title')
|
||||||
th = t('activerecord.attributes.exercise.execution_environment')
|
th = t('activerecord.attributes.exercise.execution_environment')
|
||||||
th = t('activerecord.attributes.exercise.user')
|
th = t('activerecord.attributes.exercise.user')
|
||||||
th = t('shared.actions')
|
th = t('shared.actions')
|
||||||
tbody
|
tbody
|
||||||
- @exercises.sort_by{|c| c.title}.each do |exercise|
|
- @exercise_collection.items.sort_by{|item| item.position}.each do |exercise_collection_item|
|
||||||
|
- exercise = exercise_collection_item.exercise
|
||||||
tr
|
tr
|
||||||
|
td = exercise_collection_item.position
|
||||||
td = link_to(exercise.title, exercise)
|
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 = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
|
||||||
td = exercise.user.name
|
td = exercise.user.name
|
||||||
td = link_to(t('shared.statistics'), statistics_exercise_path(exercise))
|
td = link_to(t('shared.statistics'), statistics_exercise_path(exercise))
|
||||||
|
|
||||||
= render('shared/pagination', collection: @exercises)
|
|
||||||
|
@ -6,7 +6,7 @@ h1 = @exercise_collection
|
|||||||
= row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's')
|
= row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's')
|
||||||
|
|
||||||
#graph
|
#graph
|
||||||
#data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.exercise_working_times) data-average-working-time=@exercise_collection.average_working_time)
|
#data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.collection_statistics) data-average-working-time=@exercise_collection.average_working_time)
|
||||||
#legend
|
#legend
|
||||||
- {time: t('exercises.statistics.average_worktime'),
|
- {time: t('exercises.statistics.average_worktime'),
|
||||||
min: 'min. anomaly threshold',
|
min: 'min. anomaly threshold',
|
||||||
|
@ -131,6 +131,8 @@ de:
|
|||||||
user: "Autor"
|
user: "Autor"
|
||||||
exercise: "Aufgabe"
|
exercise: "Aufgabe"
|
||||||
feedback_text: "Feedback Text"
|
feedback_text: "Feedback Text"
|
||||||
|
exercise_collection_item:
|
||||||
|
exercise: "Aufgabe"
|
||||||
models:
|
models:
|
||||||
code_harbor_link:
|
code_harbor_link:
|
||||||
one: CodeHarbor-Link
|
one: CodeHarbor-Link
|
||||||
@ -782,3 +784,7 @@ de:
|
|||||||
files: "Dateien"
|
files: "Dateien"
|
||||||
users: "Benutzer"
|
users: "Benutzer"
|
||||||
integrations: "Integrationen"
|
integrations: "Integrationen"
|
||||||
|
exercise_collections:
|
||||||
|
form:
|
||||||
|
add_exercises: "Aufgaben hinzufügen"
|
||||||
|
sort_by_title: "Nach Titel sortieren"
|
||||||
|
@ -131,6 +131,8 @@ en:
|
|||||||
user: "Author"
|
user: "Author"
|
||||||
exercise: "Exercise"
|
exercise: "Exercise"
|
||||||
feedback_text: "Feedback Text"
|
feedback_text: "Feedback Text"
|
||||||
|
exercise_collection_item:
|
||||||
|
exercise: "Exercise"
|
||||||
models:
|
models:
|
||||||
code_harbor_link:
|
code_harbor_link:
|
||||||
one: CodeHarbor Link
|
one: CodeHarbor Link
|
||||||
@ -782,3 +784,7 @@ en:
|
|||||||
files: "Files"
|
files: "Files"
|
||||||
users: "Users"
|
users: "Users"
|
||||||
integrations: "Integrations"
|
integrations: "Integrations"
|
||||||
|
exercise_collections:
|
||||||
|
form:
|
||||||
|
add_exercises: "Add exercises"
|
||||||
|
sort_by_title: "Sort by title"
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
class CreateExerciseCollectionItems < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
rename_table :exercise_collections_exercises, :exercise_collection_items
|
||||||
|
add_column :exercise_collection_items, :position, :integer, default: 0, null: false
|
||||||
|
add_column :exercise_collection_items, :id, :primary_key
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :exercise_collection_items, :position
|
||||||
|
remove_column :exercise_collection_items, :id
|
||||||
|
rename_table :exercise_collection_items, :exercise_collections_exercises
|
||||||
|
end
|
||||||
|
end
|
19
db/schema.rb
19
db/schema.rb
@ -11,7 +11,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20180515110030) do
|
ActiveRecord::Schema.define(version: 20180703125302) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@ -114,6 +114,15 @@ ActiveRecord::Schema.define(version: 20180515110030) do
|
|||||||
t.boolean "network_enabled"
|
t.boolean "network_enabled"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "exercise_collection_items", force: :cascade do |t|
|
||||||
|
t.integer "exercise_collection_id"
|
||||||
|
t.integer "exercise_id"
|
||||||
|
t.integer "position", default: 0, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "exercise_collection_items", ["exercise_collection_id"], name: "index_exercise_collection_items_on_exercise_collection_id", using: :btree
|
||||||
|
add_index "exercise_collection_items", ["exercise_id"], name: "index_exercise_collection_items_on_exercise_id", using: :btree
|
||||||
|
|
||||||
create_table "exercise_collections", force: :cascade do |t|
|
create_table "exercise_collections", force: :cascade do |t|
|
||||||
t.string "name"
|
t.string "name"
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
@ -125,14 +134,6 @@ ActiveRecord::Schema.define(version: 20180515110030) do
|
|||||||
|
|
||||||
add_index "exercise_collections", ["user_type", "user_id"], name: "index_exercise_collections_on_user_type_and_user_id", using: :btree
|
add_index "exercise_collections", ["user_type", "user_id"], name: "index_exercise_collections_on_user_type_and_user_id", using: :btree
|
||||||
|
|
||||||
create_table "exercise_collections_exercises", id: false, force: :cascade do |t|
|
|
||||||
t.integer "exercise_collection_id"
|
|
||||||
t.integer "exercise_id"
|
|
||||||
end
|
|
||||||
|
|
||||||
add_index "exercise_collections_exercises", ["exercise_collection_id"], name: "index_exercise_collections_exercises_on_exercise_collection_id", using: :btree
|
|
||||||
add_index "exercise_collections_exercises", ["exercise_id"], name: "index_exercise_collections_exercises_on_exercise_id", using: :btree
|
|
||||||
|
|
||||||
create_table "exercise_tags", force: :cascade do |t|
|
create_table "exercise_tags", force: :cascade do |t|
|
||||||
t.integer "exercise_id"
|
t.integer "exercise_id"
|
||||||
t.integer "tag_id"
|
t.integer "tag_id"
|
||||||
|
@ -2,7 +2,7 @@ class JunitAdapter < TestingFrameworkAdapter
|
|||||||
COUNT_REGEXP = /Tests run: (\d+)/
|
COUNT_REGEXP = /Tests run: (\d+)/
|
||||||
FAILURES_REGEXP = /Failures: (\d+)/
|
FAILURES_REGEXP = /Failures: (\d+)/
|
||||||
SUCCESS_REGEXP = /OK \((\d+) test[s]?\)/
|
SUCCESS_REGEXP = /OK \((\d+) test[s]?\)/
|
||||||
ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:\s(.*)|org\.junit\.ComparisonFailure:\s(.*)/
|
ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:?\s(.*?)\tat org.junit|org\.junit\.ComparisonFailure:\s(.*?)\tat org.junit/m
|
||||||
|
|
||||||
def self.framework_name
|
def self.framework_name
|
||||||
'JUnit'
|
'JUnit'
|
||||||
|
@ -21,51 +21,57 @@ namespace :detect_exercise_anomalies do
|
|||||||
WORKING_TIME_CACHE = {}
|
WORKING_TIME_CACHE = {}
|
||||||
AVERAGE_WORKING_TIME_CACHE = {}
|
AVERAGE_WORKING_TIME_CACHE = {}
|
||||||
|
|
||||||
task :with_at_least, [:number_of_exercises, :number_of_solutions] => :environment do |task, args|
|
task :with_at_least, [:number_of_exercises, :number_of_users] => :environment do |task, args|
|
||||||
include TimeHelper
|
include TimeHelper
|
||||||
|
|
||||||
number_of_exercises = args[:number_of_exercises]
|
number_of_exercises = args[:number_of_exercises]
|
||||||
number_of_solutions = args[:number_of_solutions]
|
number_of_users = args[:number_of_users]
|
||||||
|
|
||||||
puts "Searching for exercise collections with at least #{number_of_exercises} exercises and #{number_of_solutions} users."
|
log "Searching for exercise collections with at least #{number_of_exercises} exercises and #{number_of_users} users."
|
||||||
# Get all exercise collections that have at least the specified amount of exercises and at least the specified
|
# Get all exercise collections that have at least the specified amount of exercises and at least the specified
|
||||||
# number of submissions AND are flagged for anomaly detection
|
# number of users AND are flagged for anomaly detection
|
||||||
collections = get_collections(number_of_exercises, number_of_solutions)
|
collections = get_collections(number_of_exercises, number_of_users)
|
||||||
puts "Found #{collections.length}."
|
log "Found #{collections.length}."
|
||||||
|
|
||||||
collections.each do |collection|
|
collections.each do |collection|
|
||||||
puts "\t- #{collection}"
|
log(collection, 1, '- ')
|
||||||
anomalies = find_anomalies(collection)
|
anomalies = find_anomalies(collection)
|
||||||
|
|
||||||
if anomalies.length > 0 and not collection.user.nil?
|
if anomalies.length > 0
|
||||||
|
unless collection.user.nil?
|
||||||
notify_collection_author(collection, anomalies)
|
notify_collection_author(collection, anomalies)
|
||||||
|
end
|
||||||
notify_users(collection, anomalies)
|
notify_users(collection, anomalies)
|
||||||
reset_anomaly_detection_flag(collection)
|
reset_anomaly_detection_flag(collection)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
puts 'Done.'
|
log 'Done.'
|
||||||
|
end
|
||||||
|
|
||||||
|
def log(message='', indent_level=0, prefix='')
|
||||||
|
puts("\t" * indent_level + "#{prefix}#{message}")
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_collections(number_of_exercises, number_of_solutions)
|
def get_collections(number_of_exercises, number_of_solutions)
|
||||||
ExerciseCollection
|
ExerciseCollection
|
||||||
.where(:use_anomaly_detection => true)
|
.where(:use_anomaly_detection => true)
|
||||||
.joins("join exercise_collections_exercises ece on exercise_collections.id = ece.exercise_collection_id
|
.joins("join exercise_collection_items eci on exercise_collections.id = eci.exercise_collection_id
|
||||||
join
|
join
|
||||||
(select e.id
|
(select e.id
|
||||||
from exercises e
|
from exercises e
|
||||||
join submissions s on s.exercise_id = e.id
|
join submissions s on s.exercise_id = e.id
|
||||||
group by e.id
|
group by e.id
|
||||||
having count(s.user_id) > #{ExerciseCollection.sanitize(number_of_solutions)}
|
having count(s.user_id) > #{ExerciseCollection.sanitize(number_of_solutions)}
|
||||||
) as exercises_with_submissions on exercises_with_submissions.id = ece.exercise_id")
|
) as exercises_with_submissions on exercises_with_submissions.id = eci.exercise_id")
|
||||||
.group('exercise_collections.id')
|
.group('exercise_collections.id')
|
||||||
.having('count(exercises_with_submissions.id) > ?', number_of_exercises)
|
.having('count(exercises_with_submissions.id) > ?', number_of_exercises)
|
||||||
end
|
end
|
||||||
|
|
||||||
def collect_working_times(collection)
|
def collect_working_times(collection)
|
||||||
working_times = {}
|
working_times = {}
|
||||||
collection.exercises.each do |exercise|
|
collection.exercise_collection_items.order(:position).each do |eci|
|
||||||
puts "\t\t> #{exercise.title}"
|
log(eci.exercise.title, 2, '> ')
|
||||||
working_times[exercise.id] = get_average_working_time(exercise)
|
working_times[eci.exercise.id] = get_average_working_time(eci.exercise)
|
||||||
end
|
end
|
||||||
working_times
|
working_times
|
||||||
end
|
end
|
||||||
@ -98,16 +104,16 @@ namespace :detect_exercise_anomalies do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def notify_collection_author(collection, anomalies)
|
def notify_collection_author(collection, anomalies)
|
||||||
puts "\t\tSending E-Mail to author (#{collection.user.displayname} <#{collection.user.email}>)..."
|
log("Sending E-Mail to author (#{collection.user.displayname} <#{collection.user.email}>)...", 2)
|
||||||
UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now
|
UserMailer.exercise_anomaly_detected(collection, anomalies).deliver_now
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_users(collection, anomalies)
|
def notify_users(collection, anomalies)
|
||||||
by_id_and_type = proc { |u| {user_id: u[:user_id], user_type: u[:user_type]} }
|
by_id_and_type = proc { |u| {user_id: u[:user_id], user_type: u[:user_type]} }
|
||||||
|
|
||||||
puts "\t\tSending E-Mails to best and worst performing users of each anomaly..."
|
log("Sending E-Mails to best and worst performing users of each anomaly...", 2)
|
||||||
anomalies.each do |exercise_id, average_working_time|
|
anomalies.each do |exercise_id, average_working_time|
|
||||||
puts "\t\tAnomaly in exercise #{exercise_id} (avg: #{average_working_time} seconds):"
|
log("Anomaly in exercise #{exercise_id} (avg: #{average_working_time} seconds):", 2)
|
||||||
exercise = Exercise.find(exercise_id)
|
exercise = Exercise.find(exercise_id)
|
||||||
users_to_notify = []
|
users_to_notify = []
|
||||||
|
|
||||||
@ -135,7 +141,7 @@ namespace :detect_exercise_anomalies do
|
|||||||
feedback_link = url_for(action: :new, controller: :user_exercise_feedbacks, exercise_id: exercise.id, host: host)
|
feedback_link = url_for(action: :new, controller: :user_exercise_feedbacks, exercise_id: exercise.id, host: host)
|
||||||
UserMailer.exercise_anomaly_needs_feedback(user, exercise, feedback_link).deliver
|
UserMailer.exercise_anomaly_needs_feedback(user, exercise, feedback_link).deliver
|
||||||
end
|
end
|
||||||
puts "\t\tAsked #{users_to_notify.size} users for feedback."
|
log("Asked #{users_to_notify.size} users for feedback.", 2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -159,7 +165,7 @@ namespace :detect_exercise_anomalies do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reset_anomaly_detection_flag(collection)
|
def reset_anomaly_detection_flag(collection)
|
||||||
puts "\t\tResetting flag..."
|
log("Resetting flag...", 2)
|
||||||
collection.use_anomaly_detection = false
|
collection.use_anomaly_detection = false
|
||||||
collection.save!
|
collection.save!
|
||||||
end
|
end
|
||||||
|
@ -31,4 +31,18 @@ class UserGroupSeparator
|
|||||||
:recommended_assignment
|
:recommended_assignment
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.getGroupExerciseDescriptionTesting(user)
|
||||||
|
groupById = user.id % 4
|
||||||
|
if groupById == 0
|
||||||
|
:group_a
|
||||||
|
elsif groupById == 1
|
||||||
|
:group_b
|
||||||
|
elsif groupById == 2
|
||||||
|
:group_c
|
||||||
|
else # 3
|
||||||
|
:group_d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
Reference in New Issue
Block a user