Merge pull request #161 from openHPI/feature/more-statistics
Feature/more statistics
This commit is contained in:
2
Gemfile
2
Gemfile
@ -40,7 +40,7 @@ gem 'tubesock'
|
||||
gem 'faye-websocket'
|
||||
gem 'eventmachine', '1.0.9.1' # explicitly added, this is used by faye-websocket, version 1.2.5 still has an error in eventmachine.rb:202: [BUG] Segmentation fault, which is not yet fixed and causes the whole ruby process to crash
|
||||
gem 'nokogiri'
|
||||
gem 'd3-rails'
|
||||
gem 'd3-rails', '~>4.0'
|
||||
gem 'rest-client'
|
||||
gem 'rubyzip'
|
||||
gem 'whenever', require: false
|
||||
|
@ -388,7 +388,7 @@ DEPENDENCIES
|
||||
coffee-rails
|
||||
concurrent-ruby
|
||||
concurrent-ruby-ext
|
||||
d3-rails
|
||||
d3-rails (~> 4.0)
|
||||
database_cleaner
|
||||
docker-api
|
||||
eventmachine (= 1.0.9.1)
|
||||
|
102
app/assets/javascripts/exercise_collections.js.erb
Normal file
102
app/assets/javascripts/exercise_collections.js.erb
Normal file
@ -0,0 +1,102 @@
|
||||
$(function() {
|
||||
if ($.isController('exercise_collections')) {
|
||||
var data = $('#data').data('working-times');
|
||||
var averageWorkingTimeValue = parseFloat($('#data').data('average-working-time'));
|
||||
|
||||
var margin = { top: 30, right: 40, bottom: 30, left: 50 },
|
||||
width = 720 - margin.left - margin.right,
|
||||
height = 500 - margin.top - margin.bottom;
|
||||
|
||||
var x = d3.scaleBand().range([0, width]);
|
||||
var y = d3.scaleLinear().range([height, 0]);
|
||||
|
||||
var xAxis = d3.axisBottom(x);
|
||||
var yAxisLeft = d3.axisLeft(y);
|
||||
|
||||
var tooltip = d3.select("#graph").append("div").attr("class", "exercise-id-tooltip");
|
||||
|
||||
var averageWorkingTime = d3.line()
|
||||
.x(function (d) { return x(d.index) + x.bandwidth()/2; })
|
||||
.y(function () { return y(averageWorkingTimeValue); });
|
||||
|
||||
var minWorkingTime = d3.line()
|
||||
.x(function (d) { return x(d.index) + x.bandwidth()/2; })
|
||||
.y(function () { return y(0.1*averageWorkingTimeValue); });
|
||||
|
||||
var maxWorkingTime = d3.line()
|
||||
.x(function (d) { return x(d.index) + x.bandwidth()/2; })
|
||||
.y(function () { return y(2*averageWorkingTimeValue); });
|
||||
|
||||
var svg = d3.select('#graph')
|
||||
.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform",
|
||||
"translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
// Get the data
|
||||
data = Object.keys(data).map(function (key, index) {
|
||||
return {
|
||||
index: index,
|
||||
exercise_id: parseInt(key),
|
||||
working_time: parseFloat(data[key])
|
||||
};
|
||||
});
|
||||
|
||||
// Scale the range of the data
|
||||
x.domain(data.map(function (d) { return d.index; }));
|
||||
y.domain([0, d3.max(data, function (d) { return d.working_time; })]);
|
||||
|
||||
// Add the X Axis
|
||||
svg.append("g")
|
||||
.attr("class", "x axis")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(xAxis);
|
||||
|
||||
// Add the Y Axis
|
||||
svg.append("g")
|
||||
.attr("class", "y axis")
|
||||
.style("fill", "steelblue")
|
||||
.call(yAxisLeft);
|
||||
|
||||
// Draw the bars
|
||||
svg.selectAll("bar")
|
||||
.data(data)
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("class", "value-bar")
|
||||
.on("mousemove", function (d){
|
||||
tooltip
|
||||
.style("left", d3.event.pageX - 50 + "px")
|
||||
.style("top", d3.event.pageY + 50 + "px")
|
||||
.style("display", "inline-block")
|
||||
.html("<%= I18n.t('activerecord.models.exercise.one') %> ID: " + d.exercise_id + "<br>" +
|
||||
"<%= I18n.t('exercises.statistics.average_worktime') %>: " + d.working_time + "s");
|
||||
})
|
||||
.on("mouseout", function (){ tooltip.style("display", "none");})
|
||||
.on("click", function (d) {
|
||||
window.location.href = "/exercises/" + d.exercise_id + "/statistics";
|
||||
})
|
||||
.attr("x", function (d) { return x(d.index); })
|
||||
.attr("width", x.bandwidth())
|
||||
.attr("y", function (d) { return y(d.working_time); })
|
||||
.attr("height", function (d) { return height - y(d.working_time); });
|
||||
|
||||
// Add the average working time path
|
||||
svg.append("path")
|
||||
.datum(data)
|
||||
.attr("class", "line average-working-time")
|
||||
.attr("d", averageWorkingTime);
|
||||
|
||||
// Add the anomaly paths (min/max average exercise working time)
|
||||
svg.append("path")
|
||||
.datum(data)
|
||||
.attr("class", "line minimum-working-time")
|
||||
.attr("d", minWorkingTime);
|
||||
svg.append("path")
|
||||
.datum(data)
|
||||
.attr("class", "line maximum-working-time")
|
||||
.attr("d", maxWorkingTime);
|
||||
}
|
||||
});
|
@ -1,9 +1,7 @@
|
||||
$(function() {
|
||||
// http://localhost:3333/exercises/38/statistics good for testing
|
||||
// originally at--> localhost:3333/exercises/69/statistics
|
||||
// /exercises/38/statistics good for testing
|
||||
|
||||
if ($.isController('exercises') && $('.graph-functions-2').isPresent()) {
|
||||
// GET THE DATA
|
||||
var submissions = $('#data').data('submissions');
|
||||
var submissions_length = submissions.length;
|
||||
|
||||
@ -14,10 +12,7 @@ $(function() {
|
||||
submissionsAutosaves = [];
|
||||
var maximumValue = 0;
|
||||
|
||||
var wtimes = $('#wtimes').data('working_times'); //.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until)
|
||||
|
||||
// console.log(submissions);
|
||||
// console.log(wtimes);
|
||||
var wtimes = $('#wtimes').data('working_times');
|
||||
|
||||
for (var i = 0;i<submissions_length;i++){
|
||||
var submission = submissions[i];
|
||||
@ -46,9 +41,6 @@ $(function() {
|
||||
submissionsSaves.push(submissionArray[1]);
|
||||
}
|
||||
}
|
||||
// console.log(submissionsScoreAndTimeAssess.length);
|
||||
// console.log(submissionsScoreAndTimeSubmits);
|
||||
// console.log(submissionsScoreAndTimeRuns);
|
||||
|
||||
function get_minutes (time_stamp) {
|
||||
try {
|
||||
@ -94,33 +86,22 @@ $(function() {
|
||||
height = (width * height_ratio) - margin.top - margin.bottom;
|
||||
|
||||
// Set the ranges
|
||||
var x = d3.scale.linear().range([0, width]);
|
||||
var y = d3.scale.linear().range([height,0]);
|
||||
var x = d3.scaleLinear().range([0, width]);
|
||||
var y = d3.scaleLinear().range([height,0]);
|
||||
|
||||
//var x = d3.scale.linear()
|
||||
//var x = d3.scaleLinear()
|
||||
// .range([0, width]);
|
||||
//var y = d3.scale.linear()
|
||||
//var y = d3.scaleLinear()
|
||||
// .range([0,height]); // - (height/20
|
||||
|
||||
var xAxis = d3.svg.axis()
|
||||
.scale(x)
|
||||
.orient("bottom")
|
||||
.ticks(20);
|
||||
|
||||
|
||||
var yAxis = d3.svg.axis()
|
||||
.scale(d3.scale.linear().domain([0,maximumValue]).range([height,0]))//y
|
||||
// .scale(y)
|
||||
.orient("left")
|
||||
var xAxis = d3.axisBottom(x).ticks(20);
|
||||
var yAxis = d3.axisLeft()
|
||||
.scale(d3.scaleLinear().domain([0,maximumValue]).range([height,0]))
|
||||
.ticks(maximumValue)
|
||||
.innerTickSize(-width)
|
||||
.outerTickSize(0);
|
||||
.tickSizeInner(-width)
|
||||
.tickSizeOuter(0);
|
||||
|
||||
//var line = d3.svg.line()
|
||||
// .x(function(d) { return x(d.date); })
|
||||
// .y(function(d) { return y(d.close); });
|
||||
|
||||
var line = d3.svg.line()
|
||||
var line = d3.line()
|
||||
.x(function (d) {
|
||||
// console.log(d[1]);
|
||||
return x(d[1]);
|
||||
@ -288,23 +269,12 @@ $(function() {
|
||||
.text(color_hash[String(i)][0]);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
// function type(d) {
|
||||
// d.frequency = +d.frequency;
|
||||
// return d;
|
||||
// }
|
||||
|
||||
//.on("mousemove", mMove)//new again
|
||||
//.append("title");
|
||||
|
||||
}
|
||||
|
||||
try{
|
||||
graph_assesses();
|
||||
} catch(err){
|
||||
alert("could not draw the graph");
|
||||
console.error("Could not draw the graph", err);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -38,18 +38,6 @@ $(function() {
|
||||
}
|
||||
}
|
||||
|
||||
// minutes_count[(maximum_minutes + 1)] = 0;
|
||||
//$('.graph-functions').html("<p></p>")
|
||||
|
||||
// var minutes_count = new Array(10);
|
||||
// var minutes_array_len = minutes_array.length;
|
||||
// for (var i=0; i< minutes_count; i++){
|
||||
//
|
||||
// for (var j = 0; j < minutes_array_len; j++){
|
||||
// if ()
|
||||
// }
|
||||
// }
|
||||
|
||||
function getWidth() {
|
||||
if (self.innerHeight) {
|
||||
return self.innerWidth;
|
||||
@ -81,22 +69,17 @@ $(function() {
|
||||
|
||||
//var formatDate = d3.time.format("%M");
|
||||
|
||||
var x = d3.scale.linear()
|
||||
var x = d3.scaleLinear()
|
||||
.range([0, width]);
|
||||
var y = d3.scale.linear()
|
||||
var y = d3.scaleLinear()
|
||||
.range([height, 0]); // - (height/20
|
||||
var xAxis = d3.svg.axis()
|
||||
.scale(x)
|
||||
.orient("bottom")
|
||||
.ticks(20);
|
||||
var yAxis = d3.svg.axis()
|
||||
.scale(y)
|
||||
.orient("left")
|
||||
var xAxis = d3.axisBottom(x).ticks(20);
|
||||
var yAxis = d3.axisLeft(y)
|
||||
.ticks(20)
|
||||
.innerTickSize(-width)
|
||||
.outerTickSize(0);
|
||||
.tickSizeInner(-width)
|
||||
.tickSizeOuter(0);
|
||||
|
||||
var line = d3.svg.line()
|
||||
var line = d3.line()
|
||||
.x(function (d, i) {
|
||||
return x(i);
|
||||
})
|
||||
@ -225,7 +208,7 @@ $(function() {
|
||||
var x = d3.scale.ordinal()
|
||||
.rangeRoundBands([0, width], .1);
|
||||
|
||||
var y = d3.scale.linear()
|
||||
var y = d3.scaleLinear()
|
||||
.range([0,height-(margin.top + margin.bottom)]);
|
||||
|
||||
|
||||
@ -236,7 +219,7 @@ $(function() {
|
||||
|
||||
|
||||
var yAxis = d3.svg.axis()
|
||||
.scale(d3.scale.linear().domain([0,max_of_array]).range([height,0]))//y
|
||||
.scale(d3.scaleLinear().domain([0,max_of_array]).range([height,0]))//y
|
||||
.orient("left")
|
||||
.ticks(10)
|
||||
.innerTickSize(-width);
|
||||
@ -299,7 +282,7 @@ $(function() {
|
||||
.text("Working Time (Minutes)")
|
||||
.style('font-size', 14);
|
||||
|
||||
y = d3.scale.linear()
|
||||
y = d3.scaleLinear()
|
||||
.domain([(0),max_of_array])
|
||||
.range([0,height]);
|
||||
|
||||
|
69
app/assets/stylesheets/exercise_collections.scss
Normal file
69
app/assets/stylesheets/exercise_collections.scss
Normal file
@ -0,0 +1,69 @@
|
||||
$time-color: #008cba;
|
||||
$min-color: #8efa00;
|
||||
$avg-color: #ffca00;
|
||||
$max-color: #ff2600;
|
||||
|
||||
path.line.minimum-working-time {
|
||||
stroke: $min-color;
|
||||
}
|
||||
|
||||
path.line.average-working-time {
|
||||
stroke: $avg-color;
|
||||
}
|
||||
|
||||
path.line.maximum-working-time {
|
||||
stroke: $max-color;
|
||||
}
|
||||
|
||||
rect.value-bar {
|
||||
fill: $time-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#legend {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
|
||||
.legend-entry {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
|
||||
.box {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: solid 1px #000;
|
||||
}
|
||||
|
||||
.box.time {
|
||||
background-color: $time-color;
|
||||
}
|
||||
|
||||
.box.min {
|
||||
background-color: $min-color;
|
||||
}
|
||||
|
||||
.box.avg {
|
||||
background-color: $avg-color;
|
||||
}
|
||||
|
||||
.box.max {
|
||||
background-color: $max-color;
|
||||
}
|
||||
|
||||
.box-label {
|
||||
margin-left: 5px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.exercise-id-tooltip {
|
||||
position: absolute;
|
||||
display: none;
|
||||
min-width: 80px;
|
||||
height: auto;
|
||||
background: none repeat scroll 0 0 #ffffff;
|
||||
border: 1px solid #008cba;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
}
|
@ -58,3 +58,46 @@ div.negative-result {
|
||||
box-shadow: 0px 0px 11px 1px rgba(222,0,0,1);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// StatisticsController:
|
||||
|
||||
#statistics-container {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.statistics-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-auto-rows: 150px;
|
||||
grid-gap: 10px;
|
||||
|
||||
> a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
|
||||
> div {
|
||||
border: 2px solid #0055ba;
|
||||
border-radius: 5px;
|
||||
background-color: #008cba;
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-flow: column-reverse;
|
||||
text-align: center;
|
||||
|
||||
> .data {
|
||||
flex-grow: 1;
|
||||
font-size: 40px;
|
||||
vertical-align: middle;
|
||||
line-height: 50px;
|
||||
|
||||
> .unit {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
> .title {
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
class ExerciseCollectionsController < ApplicationController
|
||||
include CommonBehavior
|
||||
|
||||
before_action :set_exercise_collection, only: [:show, :edit, :update, :destroy]
|
||||
before_action :set_exercise_collection, only: [:show, :edit, :update, :destroy, :statistics]
|
||||
|
||||
def index
|
||||
@exercise_collections = ExerciseCollection.all.paginate(:page => params[:page])
|
||||
@ -35,6 +35,9 @@ class ExerciseCollectionsController < ApplicationController
|
||||
update_and_respond(object: @exercise_collection, params: exercise_collection_params)
|
||||
end
|
||||
|
||||
def statistics
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_exercise_collection
|
||||
|
16
app/controllers/statistics_controller.rb
Normal file
16
app/controllers/statistics_controller.rb
Normal file
@ -0,0 +1,16 @@
|
||||
class StatisticsController < ApplicationController
|
||||
include StatisticsHelper
|
||||
|
||||
def policy_class
|
||||
StatisticsPolicy
|
||||
end
|
||||
|
||||
def show
|
||||
authorize self
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json { render(json: statistics_data) }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
125
app/helpers/statistics_helper.rb
Normal file
125
app/helpers/statistics_helper.rb
Normal file
@ -0,0 +1,125 @@
|
||||
module StatisticsHelper
|
||||
|
||||
def statistics_data
|
||||
[
|
||||
{
|
||||
key: 'users',
|
||||
name: t('statistics.sections.users'),
|
||||
entries: user_statistics
|
||||
},
|
||||
{
|
||||
key: 'exercises',
|
||||
name: t('statistics.sections.exercises'),
|
||||
entries: exercise_statistics
|
||||
},
|
||||
{
|
||||
key: 'request_for_comments',
|
||||
name: t('statistics.sections.request_for_comments'),
|
||||
entries: rfc_statistics
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def user_statistics
|
||||
[
|
||||
{
|
||||
key: 'internal_users',
|
||||
name: t('activerecord.models.internal_user.other'),
|
||||
data: InternalUser.count,
|
||||
url: internal_users_path
|
||||
},
|
||||
{
|
||||
key: 'external_users',
|
||||
name: t('activerecord.models.external_user.other'),
|
||||
data: ExternalUser.count,
|
||||
url: external_users_path
|
||||
},
|
||||
{
|
||||
key: 'currently_active',
|
||||
name: t('statistics.entries.users.currently_active'),
|
||||
data: ExternalUser.joins(:submissions)
|
||||
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
|
||||
.distinct('external_users.id').count
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def exercise_statistics
|
||||
[
|
||||
{
|
||||
key: 'exercises',
|
||||
name: t('activerecord.models.exercise.other'),
|
||||
data: Exercise.count,
|
||||
url: exercises_path
|
||||
},
|
||||
{
|
||||
key: 'average_submissions',
|
||||
name: t('statistics.entries.exercises.average_number_of_submissions'),
|
||||
data: (Submission.count.to_f / Exercise.count).round(2)
|
||||
},
|
||||
{
|
||||
key: 'submissions_per_minute',
|
||||
name: t('statistics.entries.exercises.submissions_per_minute'),
|
||||
data: (Submission.where('created_at >= ?', DateTime.now - 1.hours).count.to_f / 60).round(2),
|
||||
unit: '/min'
|
||||
},
|
||||
{
|
||||
key: 'execution_environments',
|
||||
name: t('activerecord.models.execution_environment.other'),
|
||||
data: ExecutionEnvironment.count,
|
||||
url: execution_environments_path
|
||||
},
|
||||
{
|
||||
key: 'exercise_collections',
|
||||
name: t('activerecord.models.exercise_collection.other'),
|
||||
data: ExerciseCollection.count,
|
||||
url: exercise_collections_path
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def rfc_statistics
|
||||
[
|
||||
{
|
||||
key: 'rfcs',
|
||||
name: t('activerecord.models.request_for_comment.other'),
|
||||
data: RequestForComment.count,
|
||||
url: request_for_comments_path
|
||||
},
|
||||
{
|
||||
key: 'percent_solved',
|
||||
name: t('statistics.entries.request_for_comments.percent_solved'),
|
||||
data: (100.0 / RequestForComment.count * RequestForComment.where(solved: true).count).round(1),
|
||||
unit: '%',
|
||||
url: request_for_comments_path + '?q%5Bsolved_not_eq%5D=0'
|
||||
},
|
||||
{
|
||||
key: 'percent_soft_solved',
|
||||
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
|
||||
data: (100.0 / RequestForComment.count * RequestForComment.unsolved.where(full_score_reached: true).count).round(1),
|
||||
unit: '%',
|
||||
url: request_for_comments_path
|
||||
},
|
||||
{
|
||||
key: 'percent_unsolved',
|
||||
name: t('statistics.entries.request_for_comments.percent_unsolved'),
|
||||
data: (100.0 / RequestForComment.count * RequestForComment.unsolved.count).round(1),
|
||||
unit: '%',
|
||||
url: request_for_comments_path + '?q%5Bsolved_not_eq%5D=1'
|
||||
},
|
||||
{
|
||||
key: 'comments',
|
||||
name: t('activerecord.models.comment.other'),
|
||||
data: Comment.count
|
||||
},
|
||||
{
|
||||
key: 'rfcs_with_comments',
|
||||
name: t('statistics.entries.request_for_comments.with_comments'),
|
||||
data: RequestForComment.joins('join "submissions" s on s.id = request_for_comments.submission_id
|
||||
join "files" f on f.context_id = s.id and f.context_type = \'Submission\'
|
||||
join "comments" c on c.file_id = f.id').group('request_for_comments.id').count.size
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
end
|
12
app/helpers/time_helper.rb
Normal file
12
app/helpers/time_helper.rb
Normal file
@ -0,0 +1,12 @@
|
||||
module TimeHelper
|
||||
|
||||
# convert timestamps ('12:34:56.789') to seconds
|
||||
def time_to_f(timestamp)
|
||||
unless timestamp.nil?
|
||||
timestamp = timestamp.split(':')
|
||||
return timestamp[0].to_i * 60 * 60 + timestamp[1].to_i * 60 + timestamp[2].to_f
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
end
|
@ -1,8 +1,21 @@
|
||||
class ExerciseCollection < ActiveRecord::Base
|
||||
include TimeHelper
|
||||
|
||||
has_and_belongs_to_many :exercises
|
||||
belongs_to :user, polymorphic: true
|
||||
|
||||
def exercise_working_times
|
||||
working_times = {}
|
||||
exercises.each do |exercise|
|
||||
working_times[exercise.id] = time_to_f exercise.average_working_time
|
||||
end
|
||||
working_times
|
||||
end
|
||||
|
||||
def average_working_time
|
||||
exercise_working_times.values.reduce(:+) / exercises.size
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{I18n.t('activerecord.models.exercise_collection.one')}: #{name} (#{id})"
|
||||
end
|
||||
|
@ -1,3 +1,7 @@
|
||||
class ExerciseCollectionPolicy < AdminOnlyPolicy
|
||||
|
||||
def statistics?
|
||||
admin?
|
||||
end
|
||||
|
||||
end
|
||||
|
2
app/policies/statistics_policy.rb
Normal file
2
app/policies/statistics_policy.rb
Normal file
@ -0,0 +1,2 @@
|
||||
class StatisticsPolicy < AdminOnlyPolicy
|
||||
end
|
@ -7,6 +7,7 @@
|
||||
ul.dropdown-menu role='menu'
|
||||
- if current_user.admin?
|
||||
li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path)
|
||||
li = link_to(t('breadcrumbs.statistics.show'), statistics_path)
|
||||
li.divider
|
||||
- models = [ExecutionEnvironment, Exercise, ExerciseCollection, ProxyExercise, Tag, Consumer, CodeHarborLink, UserExerciseFeedback,
|
||||
ErrorTemplate, ErrorTemplateAttribute, ExternalUser, FileType, FileTemplate, InternalUser].sort_by {|model| model.model_name.human(count: 2) }
|
||||
|
@ -8,7 +8,7 @@ h1 = ExerciseCollection.model_name.human(count: 2)
|
||||
th = t('activerecord.attributes.exercise_collections.name')
|
||||
th = t('activerecord.attributes.exercise_collections.updated_at')
|
||||
th = t('activerecord.attributes.exercise_collections.exercises')
|
||||
th colspan=3 = t('shared.actions')
|
||||
th colspan=4 = t('shared.actions')
|
||||
tbody
|
||||
- @exercise_collections.each do |collection|
|
||||
tr
|
||||
@ -18,6 +18,7 @@ h1 = ExerciseCollection.model_name.human(count: 2)
|
||||
td = collection.exercises.size
|
||||
td = link_to(t('shared.show'), collection)
|
||||
td = link_to(t('shared.edit'), edit_exercise_collection_path(collection))
|
||||
td = link_to(t('shared.statistics'), statistics_exercise_collection_path(collection))
|
||||
td = link_to(t('shared.destroy'), collection, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
|
||||
|
||||
= render('shared/pagination', collection: @exercise_collections)
|
||||
|
17
app/views/exercise_collections/statistics.html.slim
Normal file
17
app/views/exercise_collections/statistics.html.slim
Normal file
@ -0,0 +1,17 @@
|
||||
h1 = @exercise_collection
|
||||
|
||||
= row(label: 'exercise_collections.name', value: @exercise_collection.name)
|
||||
= row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at)
|
||||
= row(label: 'exercise_collections.exercises', value: @exercise_collection.exercises.count)
|
||||
= row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's')
|
||||
|
||||
#graph
|
||||
#data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.exercise_working_times) data-average-working-time=@exercise_collection.average_working_time)
|
||||
#legend
|
||||
- {time: t('exercises.statistics.average_worktime'),
|
||||
min: 'min. anomaly threshold',
|
||||
avg: 'average time',
|
||||
max: 'max. anomaly threshold'}.each_pair do |klass, label|
|
||||
.legend-entry
|
||||
div(class="box #{klass}")
|
||||
.box-label = label
|
12
app/views/statistics/show.html.slim
Normal file
12
app/views/statistics/show.html.slim
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
#statistics-container
|
||||
- statistics_data.each do | section |
|
||||
h2 = section[:name]
|
||||
.statistics-wrapper data-key=section[:key]
|
||||
- section[:entries].each do | entry |
|
||||
a href=entry[:url]
|
||||
div data-key=entry[:key]
|
||||
.title = entry[:name]
|
||||
.data
|
||||
span = entry[:data].to_s
|
||||
span.unit = entry[:unit] if entry.key? :unit
|
@ -219,6 +219,8 @@ de:
|
||||
show: Dashboard
|
||||
sessions:
|
||||
destroy_through_lti: Code-Abgabe
|
||||
statistics:
|
||||
show: "Statistiken"
|
||||
consumers:
|
||||
show:
|
||||
link: Konsument
|
||||
@ -734,3 +736,19 @@ de:
|
||||
subscriptions:
|
||||
successfully_unsubscribed: "Ihr Abonnement für weitere Kommentare auf dieser Kommentaranfrage wurde erfolgreich beendet."
|
||||
subscription_not_existent: "Das Abonnement, von dem Sie sich abmelden wollen, existiert nicht."
|
||||
statistics:
|
||||
sections:
|
||||
users: "Benutzer"
|
||||
exercises: "Aufgaben"
|
||||
request_for_comments: "Kommentaranfragen"
|
||||
entries:
|
||||
exercises:
|
||||
average_number_of_submissions: "Durchschnittliche Zahl von Abgaben"
|
||||
submissions_per_minute: "Aktuelle Abgabenhäufigkeit (1h)"
|
||||
request_for_comments:
|
||||
percent_solved: "Beantwortete Anfragen"
|
||||
percent_unsolved: "Unbeantwortete Anfragen"
|
||||
percent_soft_solved: "Ungelöst mit voller Punktzahl"
|
||||
with_comments: "Anfragen mit Kommentaren"
|
||||
users:
|
||||
currently_active: "Aktiv (5 Minuten)"
|
||||
|
@ -219,6 +219,8 @@ en:
|
||||
show: Dashboard
|
||||
sessions:
|
||||
destroy_through_lti: Code Submission
|
||||
statistics:
|
||||
show: "Statistics"
|
||||
consumers:
|
||||
show:
|
||||
link: Consumer
|
||||
@ -734,3 +736,19 @@ en:
|
||||
subscriptions:
|
||||
successfully_unsubscribed: "You successfully unsubscribed from this Request for Comment"
|
||||
subscription_not_existent: "The subscription you want to unsubscribe from does not exist."
|
||||
statistics:
|
||||
sections:
|
||||
users: "Users"
|
||||
exercises: "Exercises"
|
||||
request_for_comments: "Requests for Comment"
|
||||
entries:
|
||||
exercises:
|
||||
average_number_of_submissions: "Average Number of Submissions"
|
||||
submissions_per_minute: "Current Submission Volume (1h)"
|
||||
request_for_comments:
|
||||
percent_solved: "Solved Requests"
|
||||
percent_unsolved: "Unsolved Requests"
|
||||
percent_soft_solved: "Unsolved with full score"
|
||||
with_comments: "RfCs with Comments"
|
||||
users:
|
||||
currently_active: "Active (5 minutes)"
|
||||
|
@ -42,6 +42,8 @@ Rails.application.routes.draw do
|
||||
|
||||
get '/help', to: 'application#help'
|
||||
|
||||
get 'statistics/', to: 'statistics#show'
|
||||
|
||||
concern :statistics do
|
||||
member do
|
||||
get :statistics
|
||||
@ -82,7 +84,11 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
resources :exercise_collections
|
||||
resources :exercise_collections do
|
||||
member do
|
||||
get :statistics
|
||||
end
|
||||
end
|
||||
|
||||
resources :proxy_exercises do
|
||||
member do
|
||||
|
@ -22,6 +22,8 @@ namespace :detect_exercise_anomalies do
|
||||
AVERAGE_WORKING_TIME_CACHE = {}
|
||||
|
||||
task :with_at_least, [:number_of_exercises, :number_of_solutions] => :environment do |task, args|
|
||||
include TimeHelper
|
||||
|
||||
number_of_exercises = args[:number_of_exercises]
|
||||
number_of_solutions = args[:number_of_solutions]
|
||||
|
||||
@ -71,14 +73,6 @@ namespace :detect_exercise_anomalies do
|
||||
end
|
||||
end
|
||||
|
||||
def time_to_f(timestamp)
|
||||
unless timestamp.nil?
|
||||
timestamp = timestamp.split(':')
|
||||
return timestamp[0].to_i * 60 * 60 + timestamp[1].to_i * 60 + timestamp[2].to_f
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def get_average_working_time(exercise)
|
||||
unless AVERAGE_WORKING_TIME_CACHE.key?(exercise.id)
|
||||
seconds = time_to_f exercise.average_working_time
|
||||
|
Reference in New Issue
Block a user