1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@
|
||||
/rubocop.html
|
||||
/tmp
|
||||
/vagrant/
|
||||
/.vagrant
|
||||
*.sublime-*
|
||||
/.idea
|
||||
/.vagrant
|
||||
|
@ -388,6 +388,3 @@ DEPENDENCIES
|
||||
uglifier (>= 1.3.0)
|
||||
web-console (~> 2.0)
|
||||
will_paginate (~> 3.0)
|
||||
|
||||
BUNDLED WITH
|
||||
1.10.6
|
||||
|
96
app/assets/javascripts/submission_statistics.js
Normal file
96
app/assets/javascripts/submission_statistics.js
Normal 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();
|
||||
}
|
||||
|
||||
});
|
12
app/assets/stylesheets/statistics.css.scss
Normal file
12
app/assets/stylesheets/statistics.css.scss
Normal file
@ -0,0 +1,12 @@
|
||||
#submissions-slider {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
#current-file.editor {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -13,4 +13,10 @@ class ExternalUsersController < ApplicationController
|
||||
@user = ExternalUser.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def statistics
|
||||
@user = ExternalUser.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -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 = {})
|
||||
|
@ -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
|
||||
|
@ -1,2 +1,5 @@
|
||||
class ExternalUserPolicy < AdminOnlyPolicy
|
||||
def statistics?
|
||||
admin?
|
||||
end
|
||||
end
|
||||
|
@ -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))
|
||||
|
||||
|
15
app/views/execution_environments/statistics.html.slim
Normal file
15
app/views/execution_environments/statistics.html.slim
Normal 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
|
48
app/views/exercises/external_users/statistics.html.slim
Normal file
48
app/views/exercises/external_users/statistics.html.slim
Normal 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')
|
@ -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
|
||||
|
2
app/views/external_users/statistics.html.slim
Normal file
2
app/views/external_users/statistics.html.slim
Normal file
@ -0,0 +1,2 @@
|
||||
h1 = @user
|
||||
H2 = 'Hallo'
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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 }
|
||||
|
||||
|
Reference in New Issue
Block a user