Merge remote-tracking branch 'origin/master' into feature/improved-tag-stats
This commit is contained in:
@@ -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.
|
||||
* Because of this, sometimes multiple commands might be executed in one message.
|
||||
* @param message
|
||||
|
@@ -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 + "<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 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 + "<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)
|
||||
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('<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 "`r`n", '\n'
|
||||
$content = $content -replace "`n", '\n'
|
||||
$content = $content -replace "`t", '\t'
|
||||
$content = $content.replace('"', '\"')
|
||||
return $content
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ button i.fa-spin {
|
||||
|
||||
.frame {
|
||||
display: none;
|
||||
min-height: 300px;
|
||||
|
||||
audio, img, video {
|
||||
max-width: 100%;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
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) }
|
||||
|
||||
# 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
|
||||
|
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)
|
||||
- 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')
|
||||
|
@@ -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)
|
||||
|
@@ -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',
|
||||
|
Reference in New Issue
Block a user