From 8ddc41b8529330160143cfbd3319028ac5ae10cb Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 17 Dec 2015 11:56:06 +0100 Subject: [PATCH 01/22] Fix slider --- app/assets/javascripts/submission_statistics.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/submission_statistics.js b/app/assets/javascripts/submission_statistics.js index 9cb820a0..8ffdfcf2 100644 --- a/app/assets/javascripts/submission_statistics.js +++ b/app/assets/javascripts/submission_statistics.js @@ -90,6 +90,12 @@ $(function() { showActiveFile(); }); + stopReplay = function() { + clearInterval(playInterval); + playInterval = undefined; + playButton.find('span.fa').removeClass('fa-pause').addClass('fa-play') + } + playButton.on('click', function(event) { if (playInterval == undefined) { playInterval = setInterval(function() { @@ -97,13 +103,12 @@ $(function() { slider.val(parseInt(slider.val()) + 1); slider.change() } else { - clearInterval(playInterval); + stopReplay(); } }, 5000); playButton.find('span.fa').removeClass('fa-play').addClass('fa-pause') } else { - clearInterval(playInterval); - playButton.find('span.fa').removeClass('fa-pause').addClass('fa-play') + stopReplay(); } }); From a5dc19ad86f8304037e92d135be8735bab244084 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 17 Dec 2015 16:17:31 +0100 Subject: [PATCH 02/22] Fix submission order --- app/views/exercises/external_users/statistics.html.slim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/exercises/external_users/statistics.html.slim b/app/views/exercises/external_users/statistics.html.slim index fd66ca26..79f6458d 100644 --- a/app/views/exercises/external_users/statistics.html.slim +++ b/app/views/exercises/external_users/statistics.html.slim @@ -1,5 +1,5 @@ h1 = "#{@exercise} (external user #{@external_user})" -- submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id) +- submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id).order("created_at") - current_submission = submissions.first - if current_submission - initial_files = current_submission.files.to_a From 76e91ec2cf5a7aa3fb0cca10c83b7de866eb56b0 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Wed, 6 Jan 2016 16:59:51 +0100 Subject: [PATCH 03/22] Add number of users and relative scores to execution environment statistics --- app/views/execution_environments/statistics.html.slim | 8 ++++++-- config/locales/de.yml | 3 +++ config/locales/en.yml | 3 +++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/views/execution_environments/statistics.html.slim b/app/views/execution_environments/statistics.html.slim index 57579476..1b804e95 100644 --- a/app/views/execution_environments/statistics.html.slim +++ b/app/views/execution_environments/statistics.html.slim @@ -4,12 +4,16 @@ h1 = @execution_environment table.table thead tr - - ['.exercise', '.score', '.runs', '.worktime'].each do |title| + - ['.exercise', '.users', '.score', '.maximum_score', '.percentage_correct', '.runs', '.worktime'].each do |title| th.header = t(title) tbody - @execution_environment.exercises.each do |exercise| + - average_score = exercise.average_score tr td = link_to exercise.title, controller: "exercises", action: "statistics", id: exercise.id - td = exercise.average_score + td = exercise.users.distinct.count + td = average_score + td = exercise.maximum_score + td = 100 / exercise.maximum_score * average_score td = exercise.average_number_of_submissions td = exercise.average_working_time diff --git a/config/locales/de.yml b/config/locales/de.yml index 40b745ad..a09f13bf 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -169,7 +169,10 @@ de: headline: Shell statistics: exercise: Übung + users: Anzahl (externer) Nutzer score: Durchschnittliche Punktzahl + maximum_score: Maximale Punktzahl + percentage_correct: Prozent Korrekt runs: Durchschnittliche Anzahl von Versuchen worktime: Durchschnittliche Arbeitszeit exercises: diff --git a/config/locales/en.yml b/config/locales/en.yml index 501fe745..7abde2fa 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -169,7 +169,10 @@ en: headline: Shell statistics: exercise: Exercise + users: (External) Users Count score: Average Score + maximum_score: Maximum Score + percentage_correct: Percentage Correct runs: Average Number of Runs worktime: Average Working Time exercises: From 943e3c6c3a6c5d06ef95ecd5de3cb0e0ee1bd82f Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 7 Jan 2016 13:19:02 +0100 Subject: [PATCH 04/22] reworked execution_environment statistics --- .../execution_environments_controller.rb | 35 +++++++++++++++++++ app/policies/external_user_policy.rb | 2 +- .../statistics.html.slim | 3 +- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/app/controllers/execution_environments_controller.rb b/app/controllers/execution_environments_controller.rb index 574aaed7..eeae03f9 100644 --- a/app/controllers/execution_environments_controller.rb +++ b/app/controllers/execution_environments_controller.rb @@ -28,7 +28,42 @@ class ExecutionEnvironmentsController < ApplicationController render(json: @docker_client.execute_arbitrary_command(params[:command])) end + + def working_time_query + """ + SELECT exercise_id, avg(working_time) as average_time + FROM + ( + SELECT user_id, + exercise_id, + sum(working_time_new) AS working_time + FROM + (SELECT user_id, + exercise_id, + CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new + FROM + (SELECT user_id, + exercise_id, + id, + (created_at - lag(created_at) over (PARTITION BY user_id, exercise_id + ORDER BY created_at)) AS working_time + FROM submissions + WHERE exercise_id IN (SELECT ID FROM exercises WHERE execution_environment_id = #{@execution_environment.id}) + GROUP BY exercise_id, user_id, id) AS foo) AS bar + GROUP BY user_id, exercise_id + ) AS baz GROUP BY exercise_id; + """ + end + def statistics + working_time_statistics = {} + ActiveRecord::Base.connection.execute(working_time_query).each do |tuple| + working_time_statistics[tuple["exercise_id"].to_i] = tuple + end + + render locals: { + working_time_statistics: working_time_statistics + } end def execution_environment_params diff --git a/app/policies/external_user_policy.rb b/app/policies/external_user_policy.rb index 2e11060b..5932e3f0 100644 --- a/app/policies/external_user_policy.rb +++ b/app/policies/external_user_policy.rb @@ -1,5 +1,5 @@ class ExternalUserPolicy < AdminOnlyPolicy def statistics? - admin? + admin? || author? || team_member? end end diff --git a/app/views/execution_environments/statistics.html.slim b/app/views/execution_environments/statistics.html.slim index 1b804e95..a0c99fe1 100644 --- a/app/views/execution_environments/statistics.html.slim +++ b/app/views/execution_environments/statistics.html.slim @@ -8,7 +8,6 @@ h1 = @execution_environment th.header = t(title) tbody - @execution_environment.exercises.each do |exercise| - - average_score = exercise.average_score tr td = link_to exercise.title, controller: "exercises", action: "statistics", id: exercise.id td = exercise.users.distinct.count @@ -16,4 +15,4 @@ h1 = @execution_environment td = exercise.maximum_score td = 100 / exercise.maximum_score * average_score td = exercise.average_number_of_submissions - td = exercise.average_working_time + td = working_time_statistics[exercise.id]["average_time"] From f195607ba1cb436e284e937497b960b8f14429dd Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 7 Jan 2016 13:55:28 +0100 Subject: [PATCH 05/22] Fix execution environment statistics view --- app/views/execution_environments/statistics.html.slim | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/execution_environments/statistics.html.slim b/app/views/execution_environments/statistics.html.slim index a0c99fe1..e0141e7e 100644 --- a/app/views/execution_environments/statistics.html.slim +++ b/app/views/execution_environments/statistics.html.slim @@ -8,6 +8,9 @@ h1 = @execution_environment th.header = t(title) tbody - @execution_environment.exercises.each do |exercise| + - average_score = exercise.average_score + - wts = working_time_statistics[exercise.id] + - if wts then average_time = wts["average_time"] else 0 tr td = link_to exercise.title, controller: "exercises", action: "statistics", id: exercise.id td = exercise.users.distinct.count @@ -15,4 +18,4 @@ h1 = @execution_environment td = exercise.maximum_score td = 100 / exercise.maximum_score * average_score td = exercise.average_number_of_submissions - td = working_time_statistics[exercise.id]["average_time"] + td = average_time From a508d47e3e2fb98d6697bde261d684f5908f31e0 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 7 Jan 2016 15:42:53 +0100 Subject: [PATCH 06/22] Retrieve user statistics in an execution environment for all exercises at once --- .../execution_environments_controller.rb | 35 ++++++++++++++++++- app/models/exercise.rb | 2 +- .../statistics.html.slim | 13 +++---- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/app/controllers/execution_environments_controller.rb b/app/controllers/execution_environments_controller.rb index eeae03f9..af4323b3 100644 --- a/app/controllers/execution_environments_controller.rb +++ b/app/controllers/execution_environments_controller.rb @@ -55,14 +55,47 @@ class ExecutionEnvironmentsController < ApplicationController """ end + def user_query + """ + SELECT + id AS exercise_id, + COUNT(DISTINCT user_id) AS users, + AVG(score) AS average_score, + MAX(score) AS maximum_score, + CASE + WHEN MAX(score)=0 THEN 0 + ELSE 100 / MAX(score) * AVG(score) + END AS percent_correct, + SUM(submission_count) / COUNT(DISTINCT user_id) AS average_submission_count + FROM + (SELECT e.id, + s.user_id, + MAX(s.score) AS score, + COUNT(s.id) AS submission_count + FROM submissions s + JOIN exercises e ON e.id = s.exercise_id + WHERE e.execution_environment_id = #{@execution_environment.id} + GROUP BY e.id, + s.user_id) AS inner_query + GROUP BY id; + """ + end + def statistics working_time_statistics = {} + user_statistics = {} + ActiveRecord::Base.connection.execute(working_time_query).each do |tuple| working_time_statistics[tuple["exercise_id"].to_i] = tuple end + ActiveRecord::Base.connection.execute(user_query).each do |tuple| + user_statistics[tuple["exercise_id"].to_i] = tuple + end + render locals: { - working_time_statistics: working_time_statistics + working_time_statistics: working_time_statistics, + user_statistics: user_statistics } end diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 6e7acdea..6fe198ac 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -34,7 +34,7 @@ class Exercise < ActiveRecord::Base def average_score if submissions.exists?(cause: 'submit') - maximum_scores_query = submissions.select('MAX(score) AS maximum_score').where(cause: 'submit').group(:user_id).to_sql.sub('$1', id.to_s) + maximum_scores_query = submissions.select('MAX(score) AS maximum_score').group(:user_id).to_sql.sub('$1', id.to_s) self.class.connection.execute("SELECT AVG(maximum_score) AS average_score FROM (#{maximum_scores_query}) AS maximum_scores").first['average_score'].to_f else 0 end end diff --git a/app/views/execution_environments/statistics.html.slim b/app/views/execution_environments/statistics.html.slim index e0141e7e..97c58ee4 100644 --- a/app/views/execution_environments/statistics.html.slim +++ b/app/views/execution_environments/statistics.html.slim @@ -8,14 +8,15 @@ h1 = @execution_environment th.header = t(title) tbody - @execution_environment.exercises.each do |exercise| - - average_score = exercise.average_score + - us = user_statistics[exercise.id] + - if not us then us = {"users" => 0, "average_score" => 0.0, "maximum_score" => 0, "percent_correct" => nil, "average_submission_count" => 0} - wts = working_time_statistics[exercise.id] - if wts then average_time = wts["average_time"] else 0 tr td = link_to exercise.title, controller: "exercises", action: "statistics", id: exercise.id - td = exercise.users.distinct.count - td = average_score - td = exercise.maximum_score - td = 100 / exercise.maximum_score * average_score - td = exercise.average_number_of_submissions + td = us["users"] + td = us["average_score"] + td = us["maximum_score"] + td = us["percent_correct"] + td = us["average_submission_count"] td = average_time From e1e6eb04f4ce703df6f485e36547f33a2df86ef6 Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 7 Jan 2016 17:01:47 +0100 Subject: [PATCH 07/22] Make user stats in exercise statistics faster --- app/controllers/exercises_controller.rb | 10 +++++++++- app/views/exercises/statistics.html.slim | 5 ++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 4f5e5b81..5b8d825a 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -155,7 +155,15 @@ class ExercisesController < ApplicationController if(@external_user) render 'exercises/external_users/statistics' else - render 'exercises/statistics' + user_statistics = {} + query = "SELECT user_id, MAX(score) AS maximum_score, COUNT(id) AS runs + FROM submissions WHERE exercise_id = 101 GROUP BY user_id;" + ActiveRecord::Base.connection.execute(query).each do |tuple| + user_statistics[tuple["user_id"].to_i] = tuple + end + render locals: { + user_statistics: user_statistics + } end end diff --git a/app/views/exercises/statistics.html.slim b/app/views/exercises/statistics.html.slim index 0cb26e9a..4b7c5f61 100644 --- a/app/views/exercises/statistics.html.slim +++ b/app/views/exercises/statistics.html.slim @@ -22,8 +22,7 @@ h1 = @exercise tbody - @exercise.send(symbol).distinct().each do |user| tr - - submissions = @exercise.submissions.where('user_id=?', user.id) td = link_to_if symbol==:external_users, "#{user.name} (#{user.email})", {controller: "exercises", action: "statistics", external_user_id: user.id, id: @exercise.id} - td = submissions.maximum('score') or 0 - td = submissions.count('id') + td = user_statistics[user.id]['maximum_score'] or 0 + td = user_statistics[user.id]['runs'] td = @exercise.average_working_time_for(user.id) or 0 From cb98f6d0fa64cee3f587acc41771cf2350bd416a Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 7 Jan 2016 17:44:43 +0100 Subject: [PATCH 08/22] Fix controller --- app/controllers/exercises_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 5b8d825a..cfe4d155 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -157,7 +157,8 @@ class ExercisesController < ApplicationController else user_statistics = {} query = "SELECT user_id, MAX(score) AS maximum_score, COUNT(id) AS runs - FROM submissions WHERE exercise_id = 101 GROUP BY user_id;" + FROM submissions WHERE exercise_id = #{@exercise.id} GROUP BY + user_id;" ActiveRecord::Base.connection.execute(query).each do |tuple| user_statistics[tuple["user_id"].to_i] = tuple end From e8cb23849ad7a9a880d194eb68b6f0771286378e Mon Sep 17 00:00:00 2001 From: Maximilian Grundke Date: Thu, 7 Jan 2016 17:45:00 +0100 Subject: [PATCH 09/22] Make tables sortable --- app/assets/javascripts/sortable.js | 498 ++++++++++++++++++ .../statistics.html.slim | 2 +- app/views/exercises/statistics.html.slim | 11 +- 3 files changed, 505 insertions(+), 6 deletions(-) create mode 100644 app/assets/javascripts/sortable.js diff --git a/app/assets/javascripts/sortable.js b/app/assets/javascripts/sortable.js new file mode 100644 index 00000000..c9a767c8 --- /dev/null +++ b/app/assets/javascripts/sortable.js @@ -0,0 +1,498 @@ +$(document).ready(function(){ + (function vendorTableSorter(){ + /* + SortTable + version 2 + 7th April 2007 + Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ + + Instructions: + Download this file + Add to your HTML + Add class="sortable" to any table you'd like to make sortable + Click on the headers to sort + + Thanks to many, many people for contributions and suggestions. + Licenced as X11: http://www.kryogenix.org/code/browser/licence.html + This basically means: do what you want with it. + */ + + + var stIsIE = /*@cc_on!@*/false; + + sorttable = { + init: function() { + // quit if this function has already been called + if (arguments.callee.done) return; + // flag this function so we don't do the same thing twice + arguments.callee.done = true; + // kill the timer + if (_timer) clearInterval(_timer); + + if (!document.createElement || !document.getElementsByTagName) return; + + sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; + + forEach(document.getElementsByTagName('table'), function(table) { + if (table.className.search(/\bsortable\b/) != -1) { + sorttable.makeSortable(table); + } + }); + + }, + + makeSortable: function(table) { + if (table.getElementsByTagName('thead').length == 0) { + // table doesn't have a tHead. Since it should have, create one and + // put the first table row in it. + the = document.createElement('thead'); + the.appendChild(table.rows[0]); + table.insertBefore(the,table.firstChild); + } + // Safari doesn't support table.tHead, sigh + if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; + + if (table.tHead.rows.length != 1) return; // can't cope with two header rows + + // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as + // "total" rows, for example). This is B&R, since what you're supposed + // to do is put them in a tfoot. So, if there are sortbottom rows, + // for backwards compatibility, move them to tfoot (creating it if needed). + sortbottomrows = []; + for (var i=0; i5' : ' ▴'; + this.appendChild(sortrevind); + return; + } + if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { + // if we're already sorted by this column in reverse, just + // re-reverse the table, which is quicker + sorttable.reverse(this.sorttable_tbody); + this.className = this.className.replace('sorttable_sorted_reverse', + 'sorttable_sorted'); + this.removeChild(document.getElementById('sorttable_sortrevind')); + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; + this.appendChild(sortfwdind); + return; + } + + // remove sorttable_sorted classes + theadrow = this.parentNode; + forEach(theadrow.childNodes, function(cell) { + if (cell.nodeType == 1) { // an element + cell.className = cell.className.replace('sorttable_sorted_reverse',''); + cell.className = cell.className.replace('sorttable_sorted',''); + } + }); + sortfwdind = document.getElementById('sorttable_sortfwdind'); + if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } + sortrevind = document.getElementById('sorttable_sortrevind'); + if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } + + this.className += ' sorttable_sorted'; + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; + this.appendChild(sortfwdind); + + // build an array to sort. This is a Schwartzian transform thing, + // i.e., we "decorate" each row with the actual sort key, + // sort based on the sort keys, and then put the rows back in order + // which is a lot faster because you only do getInnerText once per row + row_array = []; + col = this.sorttable_columnindex; + rows = this.sorttable_tbody.rows; + for (var j=0; j 12) { + // definitely dd/mm + return sorttable.sort_ddmm; + } else if (second > 12) { + return sorttable.sort_mmdd; + } else { + // looks like a date, but we can't tell which, so assume + // that it's dd/mm (English imperialism!) and keep looking + sortfn = sorttable.sort_ddmm; + } + } + } + } + return sortfn; + }, + + getInnerText: function(node) { + // gets the text we want to use for sorting for a cell. + // strips leading and trailing whitespace. + // this is *not* a generic getInnerText function; it's special to sorttable. + // for example, you can override the cell text with a customkey attribute. + // it also gets .value for fields. + + if (!node) return ""; + + hasInputs = (typeof node.getElementsByTagName == 'function') && + node.getElementsByTagName('input').length; + + if (node.getAttribute("sorttable_customkey") != null) { + return node.getAttribute("sorttable_customkey"); + } + else if (typeof node.textContent != 'undefined' && !hasInputs) { + return node.textContent.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.innerText != 'undefined' && !hasInputs) { + return node.innerText.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.text != 'undefined' && !hasInputs) { + return node.text.replace(/^\s+|\s+$/g, ''); + } + else { + switch (node.nodeType) { + case 3: + if (node.nodeName.toLowerCase() == 'input') { + return node.value.replace(/^\s+|\s+$/g, ''); + } + case 4: + return node.nodeValue.replace(/^\s+|\s+$/g, ''); + break; + case 1: + case 11: + var innerText = ''; + for (var i = 0; i < node.childNodes.length; i++) { + innerText += sorttable.getInnerText(node.childNodes[i]); + } + return innerText.replace(/^\s+|\s+$/g, ''); + break; + default: + return ''; + } + } + }, + + reverse: function(tbody) { + // reverse the rows in a tbody + newrows = []; + for (var i=0; i=0; i--) { + tbody.appendChild(newrows[i]); + } + delete newrows; + }, + + /* sort functions + each sort function takes two parameters, a and b + you are comparing a[0] and b[0] */ + sort_numeric: function(a,b) { + aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); + if (isNaN(aa)) aa = 0; + bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); + if (isNaN(bb)) bb = 0; + return aa-bb; + }, + sort_alpha: function(a,b) { + if (a[0]==b[0]) return 0; + if (a[0] 0 ) { + var q = list[i]; list[i] = list[i+1]; list[i+1] = q; + swap = true; + } + } // for + t--; + + if (!swap) break; + + for(var i = t; i > b; --i) { + if ( comp_func(list[i], list[i-1]) < 0 ) { + var q = list[i]; list[i] = list[i-1]; list[i-1] = q; + swap = true; + } + } // for + b++; + + } // while(swap) + } + } + + /* ****************************************************************** + Supporting functions: bundled here to avoid depending on a library + ****************************************************************** */ + + // Dean Edwards/Matthias Miller/John Resig + + /* for Mozilla/Opera9 */ + if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", sorttable.init, false); + } + + /* for Internet Explorer */ + /*@cc_on @*/ + /*@if (@_win32) + document.write("