diff --git a/Gemfile b/Gemfile
index 08f5461e..29c2ab7d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -25,10 +25,10 @@ gem 'rails', '4.2.10'
gem 'rails-i18n'
gem 'ransack'
gem 'rubytree'
-gem 'sass-rails'
+gem 'sass-rails', '>= 5.0.7'
gem 'sdoc', group: :doc
gem 'slim-rails'
-gem 'bootstrap_pagedown'
+gem 'bootstrap_pagedown', '>= 1.1.0'
gem 'pagedown-rails'
gem 'sorcery'
gem 'thread_safe'
@@ -66,7 +66,7 @@ end
group :test do
gem 'autotest-rails'
gem 'capybara'
- gem 'capybara-selenium'
+ gem 'capybara-selenium', '>= 0.0.6'
gem 'headless'
gem 'codeclimate-test-reporter', require: false
gem 'database_cleaner'
diff --git a/Gemfile.lock b/Gemfile.lock
index d167b88c..262c7687 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -78,13 +78,13 @@ GEM
capistrano (~> 3.7)
capistrano-bundler
puma (~> 3.4)
- capybara (2.18.0)
+ capybara (3.3.1)
addressable
mini_mime (>= 0.1.3)
- nokogiri (>= 1.3.3)
- rack (>= 1.0.0)
- rack-test (>= 0.5.4)
- xpath (>= 2.0, < 4.0)
+ nokogiri (~> 1.8)
+ rack (>= 1.6.0)
+ rack-test (>= 0.6.3)
+ xpath (~> 3.1)
capybara-selenium (0.0.6)
capybara
selenium-webdriver
@@ -92,7 +92,7 @@ GEM
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
- childprocess (0.8.0)
+ childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
codeclimate-test-reporter (1.0.7)
@@ -108,7 +108,7 @@ GEM
concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5)
concurrent-ruby (= 1.0.5)
- crass (1.0.3)
+ crass (1.0.4)
d3-rails (4.13.0)
railties (>= 3.1)
database_cleaner (1.6.2)
@@ -135,7 +135,7 @@ GEM
faye-websocket (0.10.7)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
- ffi (1.9.23)
+ ffi (1.9.25)
forgery (0.7.0)
globalid (0.4.1)
activesupport (>= 4.2.0)
@@ -161,7 +161,7 @@ GEM
json (2.1.0)
jwt (1.5.6)
kramdown (1.16.2)
- loofah (2.2.0)
+ loofah (2.2.2)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.0)
@@ -181,7 +181,7 @@ GEM
net-ssh (4.2.0)
netrc (0.11.0)
newrelic_rpm (4.8.0.341)
- nokogiri (1.8.2)
+ nokogiri (1.8.3)
mini_portile2 (~> 2.3.0)
nyan-cat-formatter (0.12.0)
rspec (>= 2.99, >= 2.14.2, < 4)
@@ -211,7 +211,7 @@ GEM
puma (3.11.3)
pundit (1.1.0)
activesupport (>= 3.0.0)
- rack (1.6.9)
+ rack (1.6.10)
rack-mini-profiler (0.10.7)
rack (>= 1.2.0)
rack-test (0.6.3)
@@ -233,8 +233,8 @@ GEM
activesupport (>= 4.2.0, < 5.0)
nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1)
- rails-html-sanitizer (1.0.3)
- loofah (~> 2.0)
+ rails-html-sanitizer (1.0.4)
+ loofah (~> 2.2, >= 2.2.2)
rails-i18n (4.0.9)
i18n (~> 0.7)
railties (~> 4.0)
@@ -244,7 +244,7 @@ GEM
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (3.0.0)
- rake (12.3.0)
+ rake (12.3.1)
ransack (1.8.7)
actionpack (>= 3.0)
activerecord (>= 3.0)
@@ -296,7 +296,7 @@ GEM
json (~> 2.1)
structured_warnings (~> 0.3)
rubyzip (1.2.1)
- sass (3.5.5)
+ sass (3.5.6)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
@@ -309,7 +309,7 @@ GEM
tilt (>= 1.1, < 3)
sdoc (1.0.0)
rdoc (>= 5.0)
- selenium-webdriver (3.10.0)
+ selenium-webdriver (3.13.0)
childprocess (~> 0.5)
rubyzip (~> 1.2)
simplecov (0.15.1)
@@ -330,7 +330,7 @@ GEM
oauth2 (~> 1.0, >= 0.8.0)
spring (2.0.2)
activesupport (>= 4.2)
- sprockets (3.7.1)
+ sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
@@ -369,7 +369,7 @@ GEM
whenever (0.10.0)
chronic (>= 0.6.3)
will_paginate (3.1.6)
- xpath (3.0.0)
+ xpath (3.1.0)
nokogiri (~> 1.8)
PLATFORMS
@@ -383,7 +383,7 @@ DEPENDENCIES
better_errors
binding_of_caller
bootstrap-will_paginate
- bootstrap_pagedown
+ bootstrap_pagedown (>= 1.1.0)
byebug
capistrano
capistrano-rails
@@ -391,7 +391,7 @@ DEPENDENCIES
capistrano-upload-config
capistrano3-puma
capybara
- capybara-selenium
+ capybara-selenium (>= 0.0.6)
carrierwave
codeclimate-test-reporter
concurrent-ruby
@@ -430,7 +430,7 @@ DEPENDENCIES
rubocop-rspec
rubytree
rubyzip
- sass-rails
+ sass-rails (>= 5.0.7)
sdoc
simplecov
slim-rails
diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js.erb
index 3aa4e25d..19f57ce3 100644
--- a/app/assets/javascripts/editor/evaluation.js.erb
+++ b/app/assets/javascripts/editor/evaluation.js.erb
@@ -153,18 +153,22 @@ CodeOceanEditorEvaluation = {
if (!colorize) {
if (output.stdout != undefined && output.stdout != '') {
element.append(output.stdout)
+ //element.text(element.text() + output.stdout)
}
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) {
- 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.QaApiOutputBuffer.stderr += output.stderr;
} 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.QaApiOutputBuffer.stdout += output.stdout;
} else {
diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb
index 63787f4a..a826cbf0 100644
--- a/app/assets/javascripts/editor/participantsupport.js.erb
+++ b/app/assets/javascripts/editor/participantsupport.js.erb
@@ -89,7 +89,8 @@ CodeOceanEditorRequestForComments = {
this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this));
$('#comment-modal').modal('hide');
- var button = $('#requestComments');
- button.prop('disabled', true);
+ // we disabled the button to prevent that the user spams RFCs, but decided against this now.
+ //var button = $('#requestComments');
+ //button.prop('disabled', true);
},
};
\ No newline at end of file
diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb
index 3530b636..8ac76c1a 100644
--- a/app/assets/javascripts/exercise_collections.js.erb
+++ b/app/assets/javascripts/exercise_collections.js.erb
@@ -1,102 +1,207 @@
$(function() {
if ($.isController('exercise_collections')) {
- var data = $('#data').data('working-times');
- var averageWorkingTimeValue = parseFloat($('#data').data('average-working-time'));
+ var dataElement = $('#data');
+ var exerciseList = $('#exercise-list');
- var margin = { top: 30, right: 40, bottom: 30, left: 50 },
- width = 720 - margin.left - margin.right,
- height = 500 - margin.top - margin.bottom;
+ if (dataElement.isPresent()) {
+ var data = dataElement.data('working-times');
+ var averageWorkingTimeValue = parseFloat(dataElement.data('average-working-time'));
- var x = d3.scaleBand().range([0, width]);
- var y = d3.scaleLinear().range([height, 0]);
+ var margin = {top: 30, right: 40, bottom: 30, left: 50},
+ width = 720 - margin.left - margin.right,
+ height = 500 - margin.top - margin.bottom;
- var xAxis = d3.axisBottom(x);
- var yAxisLeft = d3.axisLeft(y);
+ var x = d3.scaleBand().range([0, width]);
+ 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()
- .x(function (d) { return x(d.index) + x.bandwidth()/2; })
- .y(function () { return y(averageWorkingTimeValue); });
+ var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip");
- var minWorkingTime = d3.line()
- .x(function (d) { return x(d.index) + x.bandwidth()/2; })
- .y(function () { return y(0.1*averageWorkingTimeValue); });
+ var averageWorkingTime = d3.line()
+ .x(function (d) {
+ return x(d.index) + x.bandwidth() / 2;
+ })
+ .y(function () {
+ return y(averageWorkingTimeValue);
+ });
- var maxWorkingTime = d3.line()
- .x(function (d) { return x(d.index) + x.bandwidth()/2; })
- .y(function () { return y(2*averageWorkingTimeValue); });
+ var minWorkingTime = d3.line()
+ .x(function (d) {
+ return x(d.index) + x.bandwidth() / 2;
+ })
+ .y(function () {
+ return y(0.1 * 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 + ")");
+ var maxWorkingTime = d3.line()
+ .x(function (d) {
+ return x(d.index) + x.bandwidth() / 2;
+ })
+ .y(function () {
+ return y(2 * averageWorkingTimeValue);
+ });
- // Get the data
- data = Object.keys(data).map(function (key, index) {
- return {
- index: index,
- exercise_id: parseInt(key),
- working_time: parseFloat(data[key])
- };
- });
+ 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 + ")");
- // 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; })]);
+ // Get the data
+ data = Object.keys(data).map(function (key) {
+ 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
- svg.append("g")
- .attr("class", "x axis")
- .attr("transform", "translate(0," + height + ")")
- .call(xAxis);
+ // 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 Y Axis
- svg.append("g")
- .attr("class", "y axis")
- .style("fill", "steelblue")
- .call(yAxisLeft);
+ // Add the X Axis
+ svg.append("g")
+ .attr("class", "x axis")
+ .attr("transform", "translate(0," + height + ")")
+ .call(xAxis);
- // 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 + "
" +
- "<%= 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 Y Axis
+ svg.append("g")
+ .attr("class", "y axis")
+ .style("fill", "steelblue")
+ .call(yAxisLeft);
- // Add the average working time path
- svg.append("path")
- .datum(data)
- .attr("class", "line average-working-time")
- .attr("d", averageWorkingTime);
+ // 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 + "
" +
+ "<%= I18n.t('activerecord.attributes.exercise.title') %>: " + d.exercise_title + "
" +
+ "<%= 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)
- 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);
+ // 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);
+ } 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('')
+ }
+ }
+
+ 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 = '
' +
+ ' | ' +
+ '' + exercise.title + ' | ' +
+ '<%= I18n.t('shared.show') %> | ' +
+ '<%= I18n.t('shared.destroy') %> |
';
+ 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();
+ });
+ }
}
});
diff --git a/app/assets/stylesheets/exercise_collections.scss b/app/assets/stylesheets/exercise_collections.scss
index 11b6b3a1..79141988 100644
--- a/app/assets/stylesheets/exercise_collections.scss
+++ b/app/assets/stylesheets/exercise_collections.scss
@@ -57,6 +57,10 @@ rect.value-bar {
}
}
+.table-responsive#exercise-list {
+ max-height: 512px;
+}
+
.exercise-id-tooltip {
position: absolute;
display: none;
@@ -67,3 +71,19 @@ rect.value-bar {
padding: 14px;
text-align: center;
}
+
+#exercise-list {
+ margin-bottom: 20px;
+}
+
+#add-exercise-list {
+ min-height: 450px;
+}
+
+.exercise-actions {
+ margin-bottom: 20px;
+
+ button {
+ margin-right: 10px;
+ }
+}
diff --git a/app/controllers/exercise_collections_controller.rb b/app/controllers/exercise_collections_controller.rb
index de425dcd..1d7c1782 100644
--- a/app/controllers/exercise_collections_controller.rb
+++ b/app/controllers/exercise_collections_controller.rb
@@ -9,7 +9,6 @@ class ExerciseCollectionsController < ApplicationController
end
def show
- @exercises = @exercise_collection.exercises.paginate(:page => params[:page])
end
def new
@@ -50,6 +49,8 @@ class ExerciseCollectionsController < ApplicationController
end
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
diff --git a/app/models/exercise.rb b/app/models/exercise.rb
index 3ff49ad8..b2b27af4 100644
--- a/app/models/exercise.rb
+++ b/app/models/exercise.rb
@@ -14,7 +14,8 @@ class Exercise < ActiveRecord::Base
has_and_belongs_to_many :proxy_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 :interventions, through: :user_exercise_interventions
has_many :exercise_tags
diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb
index 5c159dda..a4a19f94 100644
--- a/app/models/exercise_collection.rb
+++ b/app/models/exercise_collection.rb
@@ -1,23 +1,26 @@
class ExerciseCollection < ActiveRecord::Base
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
- def exercise_working_times
- working_times = {}
- exercises.each do |exercise|
- working_times[exercise.id] = time_to_f exercise.average_working_time
+ def collection_statistics
+ statistics = {}
+ exercise_collection_items.each do |item|
+ statistics[item.position] = {exercise_id: item.exercise.id, exercise_title: item.exercise.title, working_time: time_to_f(item.exercise.average_working_time)}
end
- working_times
+ statistics
end
def average_working_time
if exercises.empty?
0
else
- values = exercise_working_times.values.reject { |v| v.nil?}
- values.reduce(:+) / exercises.size
+ values = collection_statistics.values.reject { |o| o[:working_time].nil?}
+ sum = values.reduce(0) {|sum, item| sum + item[:working_time]}
+ sum / values.size
end
end
diff --git a/app/models/exercise_collection_item.rb b/app/models/exercise_collection_item.rb
new file mode 100644
index 00000000..c7b01f20
--- /dev/null
+++ b/app/models/exercise_collection_item.rb
@@ -0,0 +1,4 @@
+class ExerciseCollectionItem < ActiveRecord::Base
+ belongs_to :exercise_collection
+ belongs_to :exercise
+end
diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb
index 3d9197fb..c6da3870 100644
--- a/app/models/proxy_exercise.rb
+++ b/app/models/proxy_exercise.rb
@@ -36,15 +36,31 @@ class ProxyExercise < ActiveRecord::Base
Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" )
assigned_user_proxy_exercise.exercise
else
- Rails.logger.debug("find new matching exercise for user #{user.id}" )
matching_exercise =
- begin
- find_matching_exercise(user)
- rescue => e #fallback
- Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" )
- @reason[:reason] = "fallback because of error"
- @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.
+ 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
+ find_matching_exercise(user)
+ rescue => e #fallback
+ Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" )
+ @reason[:reason] = "fallback because of error"
+ @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.
+ end
end
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
matching_exercise
diff --git a/app/views/exercise_collections/_add_exercise_modal.slim b/app/views/exercise_collections/_add_exercise_modal.slim
new file mode 100644
index 00000000..62080f4f
--- /dev/null
+++ b/app/views/exercise_collections/_add_exercise_modal.slim
@@ -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')
diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim
index c204db47..9c765071 100644
--- a/app/views/exercise_collections/_form.html.slim
+++ b/app/views/exercise_collections/_form.html.slim
@@ -1,7 +1,4 @@
-- exercises = Exercise.order(:title)
-- users = InternalUser.order(:name)
-
-= form_for(@exercise_collection, data: {exercises: exercises, users: users}, multipart: true) do |f|
+= form_for(@exercise_collection, multipart: true) do |f|
= render('shared/form_errors', object: @exercise_collection)
.form-group
= f.label(t('activerecord.attributes.exercise_collections.name'))
@@ -11,8 +8,30 @@
= 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})
+ = f.collection_select(:user_id, InternalUser.order(:name), :id, :name, {}, {class: 'form-control'})
+
+ .table-responsive#exercise-list
+ 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)
+
+= render('shared/modal', id: 'add-exercise-modal', title: t('.add_exercises'), template: 'exercise_collections/_add_exercise_modal')
diff --git a/app/views/exercise_collections/show.html.slim b/app/views/exercise_collections/show.html.slim
index ab85a3ba..c55d1d6b 100644
--- a/app/views/exercise_collections/show.html.slim
+++ b/app/views/exercise_collections/show.html.slim
@@ -8,20 +8,21 @@ h1
= row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at)
h4 = t('activerecord.attributes.exercise_collections.exercises')
-.table-responsive
+.table-responsive#exercise-list
table.table
thead
tr
+ th = '#'
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|
+ - @exercise_collection.items.sort_by{|item| item.position}.each do |exercise_collection_item|
+ - exercise = exercise_collection_item.exercise
tr
+ td = exercise_collection_item.position
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)
diff --git a/app/views/exercise_collections/statistics.html.slim b/app/views/exercise_collections/statistics.html.slim
index 486a0dbd..60c49a01 100644
--- a/app/views/exercise_collections/statistics.html.slim
+++ b/app/views/exercise_collections/statistics.html.slim
@@ -6,7 +6,7 @@ h1 = @exercise_collection
= 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)
+ #data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.collection_statistics) data-average-working-time=@exercise_collection.average_working_time)
#legend
- {time: t('exercises.statistics.average_worktime'),
min: 'min. anomaly threshold',
diff --git a/config/locales/de.yml b/config/locales/de.yml
index eed4d500..be078620 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -131,6 +131,8 @@ de:
user: "Autor"
exercise: "Aufgabe"
feedback_text: "Feedback Text"
+ exercise_collection_item:
+ exercise: "Aufgabe"
models:
code_harbor_link:
one: CodeHarbor-Link
@@ -782,3 +784,7 @@ de:
files: "Dateien"
users: "Benutzer"
integrations: "Integrationen"
+ exercise_collections:
+ form:
+ add_exercises: "Aufgaben hinzufügen"
+ sort_by_title: "Nach Titel sortieren"
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9fed1190..863b0a64 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -131,6 +131,8 @@ en:
user: "Author"
exercise: "Exercise"
feedback_text: "Feedback Text"
+ exercise_collection_item:
+ exercise: "Exercise"
models:
code_harbor_link:
one: CodeHarbor Link
@@ -782,3 +784,7 @@ en:
files: "Files"
users: "Users"
integrations: "Integrations"
+ exercise_collections:
+ form:
+ add_exercises: "Add exercises"
+ sort_by_title: "Sort by title"
diff --git a/db/migrate/20180703125302_create_exercise_collection_items.rb b/db/migrate/20180703125302_create_exercise_collection_items.rb
new file mode 100644
index 00000000..c881c8a5
--- /dev/null
+++ b/db/migrate/20180703125302_create_exercise_collection_items.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index 73a2c228..ae1398a6 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# 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
enable_extension "plpgsql"
@@ -114,6 +114,15 @@ ActiveRecord::Schema.define(version: 20180515110030) do
t.boolean "network_enabled"
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|
t.string "name"
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
- 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|
t.integer "exercise_id"
t.integer "tag_id"
diff --git a/lib/junit_adapter.rb b/lib/junit_adapter.rb
index 6230a785..df0eff5d 100644
--- a/lib/junit_adapter.rb
+++ b/lib/junit_adapter.rb
@@ -2,7 +2,7 @@ class JunitAdapter < TestingFrameworkAdapter
COUNT_REGEXP = /Tests run: (\d+)/
FAILURES_REGEXP = /Failures: (\d+)/
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
'JUnit'
diff --git a/lib/tasks/detect_exercise_anomalies.rake b/lib/tasks/detect_exercise_anomalies.rake
index fd8210da..43fe421a 100644
--- a/lib/tasks/detect_exercise_anomalies.rake
+++ b/lib/tasks/detect_exercise_anomalies.rake
@@ -21,51 +21,57 @@ namespace :detect_exercise_anomalies do
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
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
- # number of submissions AND are flagged for anomaly detection
- collections = get_collections(number_of_exercises, number_of_solutions)
- puts "Found #{collections.length}."
+ # number of users AND are flagged for anomaly detection
+ collections = get_collections(number_of_exercises, number_of_users)
+ log "Found #{collections.length}."
collections.each do |collection|
- puts "\t- #{collection}"
+ log(collection, 1, '- ')
anomalies = find_anomalies(collection)
- if anomalies.length > 0 and not collection.user.nil?
- notify_collection_author(collection, anomalies)
+ if anomalies.length > 0
+ unless collection.user.nil?
+ notify_collection_author(collection, anomalies)
+ end
notify_users(collection, anomalies)
reset_anomaly_detection_flag(collection)
end
end
- puts 'Done.'
+ log 'Done.'
+ end
+
+ def log(message='', indent_level=0, prefix='')
+ puts("\t" * indent_level + "#{prefix}#{message}")
end
def get_collections(number_of_exercises, number_of_solutions)
ExerciseCollection
.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
(select e.id
from exercises e
join submissions s on s.exercise_id = e.id
group by e.id
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')
.having('count(exercises_with_submissions.id) > ?', number_of_exercises)
end
def collect_working_times(collection)
working_times = {}
- collection.exercises.each do |exercise|
- puts "\t\t> #{exercise.title}"
- working_times[exercise.id] = get_average_working_time(exercise)
+ collection.exercise_collection_items.order(:position).each do |eci|
+ log(eci.exercise.title, 2, '> ')
+ working_times[eci.exercise.id] = get_average_working_time(eci.exercise)
end
working_times
end
@@ -98,16 +104,16 @@ namespace :detect_exercise_anomalies do
end
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
end
def notify_users(collection, anomalies)
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|
- 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)
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)
UserMailer.exercise_anomaly_needs_feedback(user, exercise, feedback_link).deliver
end
- puts "\t\tAsked #{users_to_notify.size} users for feedback."
+ log("Asked #{users_to_notify.size} users for feedback.", 2)
end
end
@@ -159,7 +165,7 @@ namespace :detect_exercise_anomalies do
end
def reset_anomaly_detection_flag(collection)
- puts "\t\tResetting flag..."
+ log("Resetting flag...", 2)
collection.use_anomaly_detection = false
collection.save!
end
diff --git a/lib/user_group_separator.rb b/lib/user_group_separator.rb
index 4dcd00e1..2333b686 100644
--- a/lib/user_group_separator.rb
+++ b/lib/user_group_separator.rb
@@ -31,4 +31,18 @@ class UserGroupSeparator
:recommended_assignment
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
\ No newline at end of file