Merge remote-tracking branch 'origin/master' into feature/improved-tag-stats
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
|
||||||
|
@ -34,7 +34,7 @@ CommandSocket.prototype.onMessage = function(event) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a message, checks wether it contains multiple commands (seperated by linebreaks)
|
* Parses a message, checks whether it contains multiple commands (seperated by linebreaks)
|
||||||
* This needs to be done because of the behavior of the docker-socket connection.
|
* This needs to be done because of the behavior of the docker-socket connection.
|
||||||
* Because of this, sometimes multiple commands might be executed in one message.
|
* Because of this, sometimes multiple commands might be executed in one message.
|
||||||
* @param message
|
* @param message
|
||||||
|
@ -1,102 +1,207 @@
|
|||||||
$(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()) {
|
||||||
width = 720 - margin.left - margin.right,
|
var data = dataElement.data('working-times');
|
||||||
height = 500 - margin.top - margin.bottom;
|
var averageWorkingTimeValue = parseFloat(dataElement.data('average-working-time'));
|
||||||
|
|
||||||
var x = d3.scaleBand().range([0, width]);
|
var margin = {top: 30, right: 40, bottom: 30, left: 50},
|
||||||
var y = d3.scaleLinear().range([height, 0]);
|
width = 720 - margin.left - margin.right,
|
||||||
|
height = 500 - margin.top - margin.bottom;
|
||||||
|
|
||||||
var xAxis = d3.axisBottom(x);
|
var x = d3.scaleBand().range([0, width]);
|
||||||
var yAxisLeft = d3.axisLeft(y);
|
var y = d3.scaleLinear().range([height, 0]);
|
||||||
|
|
||||||
var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip");
|
var xAxis = d3.axisBottom(x);
|
||||||
|
var yAxisLeft = d3.axisLeft(y);
|
||||||
|
|
||||||
var averageWorkingTime = d3.line()
|
var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip");
|
||||||
.x(function (d) { return x(d.index) + x.bandwidth()/2; })
|
|
||||||
.y(function () { return y(averageWorkingTimeValue); });
|
|
||||||
|
|
||||||
var minWorkingTime = d3.line()
|
var averageWorkingTime = 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(averageWorkingTimeValue);
|
||||||
|
});
|
||||||
|
|
||||||
var maxWorkingTime = d3.line()
|
var minWorkingTime = 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(0.1 * averageWorkingTimeValue);
|
||||||
|
});
|
||||||
|
|
||||||
var svg = d3.select('#graph')
|
var maxWorkingTime = d3.line()
|
||||||
.append("svg")
|
.x(function (d) {
|
||||||
.attr("width", width + margin.left + margin.right)
|
return x(d.index) + x.bandwidth() / 2;
|
||||||
.attr("height", height + margin.top + margin.bottom)
|
})
|
||||||
.append("g")
|
.y(function () {
|
||||||
.attr("transform",
|
return y(2 * averageWorkingTimeValue);
|
||||||
"translate(" + margin.left + "," + margin.top + ")");
|
});
|
||||||
|
|
||||||
// Get the data
|
var svg = d3.select('#graph')
|
||||||
data = Object.keys(data).map(function (key, index) {
|
.append("svg")
|
||||||
return {
|
.attr("width", width + margin.left + margin.right)
|
||||||
index: index,
|
.attr("height", height + margin.top + margin.bottom)
|
||||||
exercise_id: parseInt(key),
|
.append("g")
|
||||||
working_time: parseFloat(data[key])
|
.attr("transform",
|
||||||
};
|
"translate(" + margin.left + "," + margin.top + ")");
|
||||||
});
|
|
||||||
|
|
||||||
// Scale the range of the data
|
// Get the data
|
||||||
x.domain(data.map(function (d) { return d.index; }));
|
data = Object.keys(data).map(function (key) {
|
||||||
y.domain([0, d3.max(data, function (d) { return d.working_time; })]);
|
return {
|
||||||
|
index: parseInt(key),
|
||||||
|
exercise_id: parseInt(data[key]['exercise_id']),
|
||||||
|
exercise_title: data[key]['exercise_title'],
|
||||||
|
working_time: parseFloat(data[key]['working_time'])
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Add the X Axis
|
// Scale the range of the data
|
||||||
svg.append("g")
|
x.domain(data.map(function (d) {
|
||||||
.attr("class", "x axis")
|
return d.index;
|
||||||
.attr("transform", "translate(0," + height + ")")
|
}));
|
||||||
.call(xAxis);
|
y.domain([0, d3.max(data, function (d) {
|
||||||
|
return d.working_time;
|
||||||
|
})]);
|
||||||
|
|
||||||
// Add the Y Axis
|
// Add the X Axis
|
||||||
svg.append("g")
|
svg.append("g")
|
||||||
.attr("class", "y axis")
|
.attr("class", "x axis")
|
||||||
.style("fill", "steelblue")
|
.attr("transform", "translate(0," + height + ")")
|
||||||
.call(yAxisLeft);
|
.call(xAxis);
|
||||||
|
|
||||||
// Draw the bars
|
// Add the Y Axis
|
||||||
svg.selectAll("bar")
|
svg.append("g")
|
||||||
.data(data)
|
.attr("class", "y axis")
|
||||||
.enter()
|
.style("fill", "steelblue")
|
||||||
.append("rect")
|
.call(yAxisLeft);
|
||||||
.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
|
// Draw the bars
|
||||||
svg.append("path")
|
svg.selectAll("bar")
|
||||||
.datum(data)
|
.data(data)
|
||||||
.attr("class", "line average-working-time")
|
.enter()
|
||||||
.attr("d", averageWorkingTime);
|
.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('activerecord.attributes.exercise.title') %>: " + d.exercise_title + "<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 anomaly paths (min/max average exercise working time)
|
// Add the average working time path
|
||||||
svg.append("path")
|
svg.append("path")
|
||||||
.datum(data)
|
.datum(data)
|
||||||
.attr("class", "line minimum-working-time")
|
.attr("class", "line average-working-time")
|
||||||
.attr("d", minWorkingTime);
|
.attr("d", averageWorkingTime);
|
||||||
svg.append("path")
|
|
||||||
.datum(data)
|
// Add the anomaly paths (min/max average exercise working time)
|
||||||
.attr("class", "line maximum-working-time")
|
svg.append("path")
|
||||||
.attr("d", maxWorkingTime);
|
.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);
|
||||||
|
} 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -62,6 +62,7 @@ function get_escaped_file_content ($file){
|
|||||||
$content = $content.replace('\', '\\')
|
$content = $content.replace('\', '\\')
|
||||||
$content = $content -replace "`r`n", '\n'
|
$content = $content -replace "`r`n", '\n'
|
||||||
$content = $content -replace "`n", '\n'
|
$content = $content -replace "`n", '\n'
|
||||||
|
$content = $content -replace "`t", '\t'
|
||||||
$content = $content.replace('"', '\"')
|
$content = $content.replace('"', '\"')
|
||||||
return $content
|
return $content
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ button i.fa-spin {
|
|||||||
|
|
||||||
.frame {
|
.frame {
|
||||||
display: none;
|
display: none;
|
||||||
|
min-height: 300px;
|
||||||
|
|
||||||
audio, img, video {
|
audio, img, video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
@ -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
|
@ -70,6 +70,6 @@ class Submission < ActiveRecord::Base
|
|||||||
# RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) }
|
# RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) }
|
||||||
|
|
||||||
# experimental query:
|
# experimental query:
|
||||||
RequestForComment.unsolved.joins('JOIN exercise_collections_exercises ece ON ece.exercise_id = request_for_comments.exercise_id').where('ece.exercise_collection_id != 3 OR user_id%10 > 3').where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) }
|
RequestForComment.unsolved.joins('JOIN exercise_collection_items eci ON eci.exercise_id = request_for_comments.exercise_id').where('eci.exercise_collection_id != 3 OR user_id%10 > 3').where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) }
|
||||||
end
|
end
|
||||||
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
|
||||||
@ -356,7 +358,7 @@ en:
|
|||||||
external_users: External Users
|
external_users: External Users
|
||||||
finishing_rate: Finishing Rate
|
finishing_rate: Finishing Rate
|
||||||
submit:
|
submit:
|
||||||
failure: An error occured while transmitting your score. Please try again later.
|
failure: An error occurred while transmitting your score. Please try again later.
|
||||||
full_score_redirect_to_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Another participant has a question concerning the exercise you just solved. Your help and comments will be greatly appreciated!
|
full_score_redirect_to_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Another participant has a question concerning the exercise you just solved. Your help and comments will be greatly appreciated!
|
||||||
full_score_redirect_to_own_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Your question concerning the exercise is solved? If so, please share the essential insight with your fellows and mark the question as solved, before you close this window!
|
full_score_redirect_to_own_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Your question concerning the exercise is solved? If so, please share the essential insight with your fellows and mark the question as solved, before you close this window!
|
||||||
external_users:
|
external_users:
|
||||||
@ -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"
|
||||||
|
@ -21,59 +21,65 @@ 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
|
||||||
notify_collection_author(collection, anomalies)
|
unless collection.user.nil?
|
||||||
|
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
|
||||||
|
|
||||||
def find_anomalies(collection)
|
def find_anomalies(collection)
|
||||||
working_times = collect_working_times(collection).reject {|_, value| value.nil?}
|
working_times = collect_working_times(collection).reject {|_, value| value.nil?}
|
||||||
if working_times.size > 0
|
if working_times.values.size > 0
|
||||||
average = working_times.reduce(:+) / working_times.size
|
average = working_times.values.reduce(:+) / working_times.values.size
|
||||||
return working_times.select do |_, working_time|
|
return working_times.select do |_, working_time|
|
||||||
working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR
|
working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR
|
||||||
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
|
||||||
|
Reference in New Issue
Block a user