Merge pull request #32 from openHPI/statistics

Statistics
This commit is contained in:
jprberlin
2015-12-15 17:20:53 +01:00
19 changed files with 324 additions and 10 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@
/rubocop.html
/tmp
/vagrant/
/.vagrant
*.sublime-*
/.idea
/.vagrant

View File

@ -388,6 +388,3 @@ DEPENDENCIES
uglifier (>= 1.3.0)
web-console (~> 2.0)
will_paginate (~> 3.0)
BUNDLED WITH
1.10.6

View File

@ -0,0 +1,96 @@
$(function() {
var ACE_FILES_PATH = '/assets/ace/';
var THEME = 'ace/theme/textmate';
var currentSubmission = 0;
var active_file = undefined;
var fileTrees = []
var editor = undefined;
var fileTypeById = {}
var showActiveFile = function() {
var session = editor.getSession();
var fileType = fileTypeById[active_file.file_type_id]
session.setMode(fileType.editor_mode);
session.setTabSize(fileType.indent_size);
session.setValue(active_file.content);
session.setUseSoftTabs(true);
session.setUseWrapMode(true);
showFileTree(currentSubmission);
filetree = $(fileTrees[currentSubmission])
filetree.jstree("deselect_all");
filetree.jstree().select_node(active_file.file_id);
};
var initializeFileTree = function() {
$('.files').each(function(index, element) {
fileTree = $(element).jstree($(element).data('entries'));
fileTree.on('click', 'li.jstree-leaf', function() {
var id = parseInt($(this).attr('id'))
_.each(files[currentSubmission], function(file) {
if (file.file_id === id) {
active_file = file;
}
});
showActiveFile();
});
fileTrees.push(fileTree);
});
};
var showFileTree = function(index) {
$('.files').hide();
$(fileTrees[index].context).show();
}
if ($.isController('exercises') && $('#timeline').isPresent()) {
_.each(['modePath', 'themePath', 'workerPath'], function(attribute) {
ace.config.set(attribute, ACE_FILES_PATH);
});
var slider = $('#submissions-slider>input');
var submissions = $('#data').data('submissions');
var files = $('#data').data('files');
var filetypes = $('#data').data('file-types');
editor = ace.edit('current-file');
editor.setShowPrintMargin(false);
editor.setTheme(THEME);
editor.$blockScrolling = Infinity;
editor.setReadOnly(true);
_.each(filetypes, function (filetype) {
filetype = JSON.parse(filetype);
fileTypeById[filetype.id] = filetype;
});
$('tr[data-id]>.clickable').each(function(index, element) {
element = $(element);
element.click(function() {
slider.val(index);
slider.change()
});
});
slider.on('change', function(event) {
currentSubmission = slider.val();
var currentFiles = files[currentSubmission];
var fileIndex = 0;
_.each(currentFiles, function(file, index) {
if (file.name === active_file.name) {
fileIndex = index;
}
})
active_file = currentFiles[fileIndex];
showActiveFile();
});
active_file = files[0][0]
initializeFileTree();
showActiveFile();
}
});

View File

@ -0,0 +1,12 @@
#submissions-slider {
margin-top: 25px;
margin-bottom: 25px;
}
#current-file.editor {
height: 400px;
}
.clickable {
cursor: pointer;
}

View File

@ -2,7 +2,7 @@ class ExecutionEnvironmentsController < ApplicationController
include CommonBehavior
before_action :set_docker_images, only: [:create, :edit, :new, :update]
before_action :set_execution_environment, only: MEMBER_ACTIONS + [:execute_command, :shell]
before_action :set_execution_environment, only: MEMBER_ACTIONS + [:execute_command, :shell, :statistics]
before_action :set_testing_framework_adapters, only: [:create, :edit, :new, :update]
def authorize!
@ -28,6 +28,9 @@ class ExecutionEnvironmentsController < ApplicationController
render(json: @docker_client.execute_arbitrary_command(params[:command]))
end
def statistics
end
def execution_environment_params
params[:execution_environment].permit(:docker_image, :exposed_ports, :editor_mode, :file_extension, :file_type_id, :help, :indent_size, :memory_limit, :name, :network_enabled, :permitted_execution_time, :pool_size, :run_command, :test_command, :testing_framework).merge(user_id: current_user.id, user_type: current_user.class.name)
end

View File

@ -7,6 +7,7 @@ class ExercisesController < ApplicationController
before_action :handle_file_uploads, only: [:create, :update]
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit, :reload]
before_action :set_external_user, only: [:statistics]
before_action :set_file_types, only: [:create, :edit, :new, :update]
before_action :set_teams, only: [:create, :edit, :new, :update]
@ -125,6 +126,14 @@ class ExercisesController < ApplicationController
end
private :set_exercise
def set_external_user
if params[:external_user_id]
@external_user = ExternalUser.find(params[:external_user_id])
authorize!
end
end
private :set_exercise
def set_file_types
@file_types = FileType.all.order(:name)
end
@ -143,6 +152,11 @@ class ExercisesController < ApplicationController
end
def statistics
if(@external_user)
render 'exercises/external_users/statistics'
else
render 'exercises/statistics'
end
end
def submit

View File

@ -13,4 +13,10 @@ class ExternalUsersController < ApplicationController
@user = ExternalUser.find(params[:id])
authorize!
end
def statistics
@user = ExternalUser.find(params[:id])
authorize!
end
end

View File

@ -11,7 +11,10 @@ class Exercise < ActiveRecord::Base
belongs_to :execution_environment
has_many :submissions
belongs_to :team
has_many :users, source_type: ExternalUser, through: :submissions
has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions
has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
alias_method :users, :external_users
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
@ -22,15 +25,54 @@ class Exercise < ActiveRecord::Base
validates :title, presence: true
validates :token, presence: true, uniqueness: true
def average_percentage
(average_score/ maximum_score * 100).round if average_score
(average_score / maximum_score * 100).round if average_score
end
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)
self.class.connection.execute("SELECT AVG(maximum_score) AS average_score FROM (#{maximum_scores_query}) AS maximum_scores").first['average_score'].to_f
end
else 0 end
end
def average_number_of_submissions
user_count = internal_users.distinct.count + external_users.distinct.count
return user_count == 0 ? 0 : submissions.count() / user_count.to_f()
end
def user_working_time_query
"""
SELECT user_id,
sum(working_time_new) AS working_time
FROM
(SELECT user_id,
CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new
FROM
(SELECT user_id,
id,
(created_at - lag(created_at) over (PARTITION BY user_id
ORDER BY id)) AS working_time
FROM submissions
WHERE exercise_id=#{id}) AS foo) AS bar
GROUP BY user_id
"""
end
def average_working_time
self.class.connection.execute("""
SELECT avg(working_time) as average_time
FROM
(#{user_working_time_query}) AS baz;
""").first['average_time']
end
def average_working_time_for(user_id)
self.class.connection.execute("""
#{user_working_time_query}
HAVING user_id = #{user_id}
""").first['working_time']
end
def duplicate(attributes = {})

View File

@ -4,7 +4,7 @@ class ExecutionEnvironmentPolicy < AdminOrAuthorPolicy
end
private :author?
[:execute_command?, :shell?].each do |action|
[:execute_command?, :shell?, :statistics?].each do |action|
define_method(action) { admin? || author? }
end
end

View File

@ -1,2 +1,5 @@
class ExternalUserPolicy < AdminOnlyPolicy
def statistics?
admin?
end
end

View File

@ -10,7 +10,7 @@ h1 = ExecutionEnvironment.model_name.human(count: 2)
th = t('activerecord.attributes.execution_environment.memory_limit')
th = t('activerecord.attributes.execution_environment.network_enabled')
th = t('activerecord.attributes.execution_environment.permitted_execution_time')
th colspan=4 = t('shared.actions')
th colspan=5 = t('shared.actions')
th colspan=2 = t('shared.resources')
tbody
- @execution_environments.each do |execution_environment|
@ -25,6 +25,7 @@ h1 = ExecutionEnvironment.model_name.human(count: 2)
td = link_to(t('shared.edit'), edit_execution_environment_path(execution_environment))
td = link_to(t('shared.destroy'), execution_environment, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
td = link_to(t('.shell'), shell_execution_environment_path(execution_environment))
td = link_to(t('shared.statistics'), statistics_execution_environment_path(execution_environment))
td = link_to(t('activerecord.models.error.other'), execution_environment_errors_path(execution_environment.id))
td = link_to(t('activerecord.models.hint.other'), execution_environment_hints_path(execution_environment.id))

View File

@ -0,0 +1,15 @@
h1 = @execution_environment
.table-responsive
table.table
thead
tr
- ['.exercise', '.score', '.runs', '.worktime'].each do |title|
th.header = t(title)
tbody
- @execution_environment.exercises.each do |exercise|
tr
td = link_to exercise.title, controller: "exercises", action: "statistics", id: exercise.id
td = exercise.average_score
td = exercise.average_number_of_submissions
td = exercise.average_working_time

View File

@ -0,0 +1,48 @@
h1 = "#{@exercise} (external user #{@external_user})"
- submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id)
- current_submission = submissions.first
- if current_submission
- initial_files = current_submission.files.to_a
- all_files = []
- file_types = Set.new()
- submissions.each do |submission|
- submission.files.each do |file|
- file_types.add(ActiveSupport::JSON.encode(file.file_type))
- all_files.push(submission.files)
.hidden#data data-submissions=ActiveSupport::JSON.encode(submissions) data-files=ActiveSupport::JSON.encode(all_files) data-file-types=ActiveSupport::JSON.encode(file_types)
#stats-editor.row
- index = 0
- all_files.each do |files|
.files class=(@exercise.hide_file_tree ? 'hidden col-sm-3' : 'col-sm-3') data-index=index data-entries=FileTree.new(files).to_js_tree
- index += 1
div class=(@exercise.hide_file_tree ? 'col-sm-12' : 'col-sm-9')
#current-file.editor
#submissions-slider
input type='range' orient='horizontal' list='datapoints' min=0 max=submissions.length-1 value=0
datalist#datapoints
- index=0
- submissions.each do |submission|
option data-submission=submission
=index
- index += 1
#timeline
.table-responsive
table.table
thead
tr
- ['.time', '.cause', '.score'].each do |title|
th.header = t(title)
tbody
- submissions.each do |submission|
tr data-id=submission.id
td.clickable = submission.created_at.strftime("%F %T")
td = submission.cause
td = submission.score
- else
p = t('.no_data_available')

View File

@ -7,3 +7,23 @@ h1 = @exercise
= row(label: '.average_score') do
p == @exercise.average_score ? t('shared.out_of', maximum_value: @exercise.maximum_score, value: @exercise.average_score.round(2)) : empty
p = progress_bar(@exercise.average_percentage)
= row(label: '.average_worktime') do
p = @exercise.average_working_time
- Hash[:internal_users => t('.internal_users'), :external_users => t('.external_users')].each_pair do |symbol, label|
strong = label
.table-responsive
table.table
thead
tr
- ['.user', '.score', '.runs', '.worktime'].each do |title|
th.header = t(title)
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 = @exercise.average_working_time_for(user.id) or 0

View File

@ -0,0 +1,2 @@
h1 = @user
H2 = 'Hallo'

View File

@ -167,6 +167,11 @@ de:
shell:
command: Befehl
headline: Shell
statistics:
exercise: Übung
score: Durchschnittliche Punktzahl
runs: Durchschnittliche Anzahl von Versuchen
worktime: Durchschnittliche Arbeitszeit
exercises:
editor:
confirm_start_over: Wollen Sie wirklich von vorne anfangen?
@ -239,8 +244,21 @@ de:
intermediate_submissions: Intermediäre Abgaben
participants: Bearbeitende Nutzer
users: '%{count} verschiedene Nutzer'
user: Nutzer
score: Punktzahl
runs: Versuche
worktime: Arbeitszeit
average_worktime: Durchschnittliche Arbeitszeit
internal_users: Interne Nutzer
external_user: Externe Nutzer
submit:
failure: Beim Übermitteln Ihrer Punktzahl ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.
external_users:
statistics:
no_data_available: Keine Daten verfügbar.
time: Zeit
cause: Grund
score: Punktzahl
files:
roles:
main_file: Hauptdatei

View File

@ -167,6 +167,11 @@ en:
shell:
command: Command
headline: Shell
statistics:
exercise: Exercise
score: Average Score
runs: Average Number of Runs
worktime: Average Working Time
exercises:
editor:
confirm_start_over: Do you really want to start over?
@ -239,8 +244,21 @@ en:
intermediate_submissions: Intermediate Submissions
participants: Participating Users
users: '%{count} distinct users'
user: User
score: Score
runs: Runs
worktime: Working Time
average_worktime: Average Working Time
internal_users: Internal Users
external_users: External Users
submit:
failure: An error occured while transmitting your score. Please try again later.
external_users:
statistics:
no_data_available: No data available.
time: Time
cause: Cause
score: Score
files:
roles:
main_file: Main File

View File

@ -20,12 +20,20 @@ Rails.application.routes.draw do
get '/help', to: 'application#help'
concern :statistics do
member do
get :statistics
end
end
resources :consumers
resources :execution_environments do
member do
get :shell
post 'shell', as: :execute_command, to: :execute_command
get :statistics
end
resources :errors, only: [:create, :index, :show]
@ -46,7 +54,9 @@ Rails.application.routes.draw do
end
end
resources :external_users, only: [:index, :show]
resources :external_users, only: [:index, :show], concerns: :statistics do
resources :exercises, concerns: :statistics
end
namespace :code_ocean do
resources :files, only: [:create, :destroy]

View File

@ -130,6 +130,14 @@ describe ExecutionEnvironmentsController do
expect_template(:shell)
end
describe 'GET #statistics' do
before(:each) { get :statistics, id: execution_environment.id }
expect_assigns(execution_environment: :execution_environment)
expect_status(200)
expect_template(:statistics)
end
describe 'GET #show' do
before(:each) { get :show, id: execution_environment.id }