Merge branch 'master' into fix-additional-line
This commit is contained in:
@@ -421,7 +421,7 @@ $(function() {
|
||||
session.setUseWrapMode(true);
|
||||
|
||||
var file_id = $(element).data('id');
|
||||
setAnnotations(editor, file_id);
|
||||
//setAnnotations(editor, file_id);
|
||||
|
||||
session.on('annotationRemoval', handleAnnotationRemoval);
|
||||
session.on('annotationChange', handleAnnotationChange);
|
||||
@@ -1120,7 +1120,10 @@ $(function() {
|
||||
};
|
||||
|
||||
var initWebsocketConnection = function(url) {
|
||||
websocket = new WebSocket('wss://' + window.location.hostname + ':' + window.location.port + url);
|
||||
//TODO: get the protocol from config file dependent on environment. (dev: ws, prod: wss)
|
||||
//causes: Puma::HttpParserError: Invalid HTTP format, parsing fails.
|
||||
//TODO: make sure that this gets cached.
|
||||
websocket = new WebSocket('<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + window.location.port + url);
|
||||
websocket.onopen = function(evt) { resetOutputTab(); }; // todo show some kind of indicator for established connection
|
||||
websocket.onclose = function(evt) { /* expected at some point */ };
|
||||
websocket.onmessage = function(evt) { parseCanvasMessage(evt.data, true); };
|
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
|
||||
|
@@ -1,6 +1,6 @@
|
||||
- content_for :head do
|
||||
= javascript_include_tag('//cdnjs.cloudflare.com/ajax/libs/vis/3.10.0/vis.min.js')
|
||||
= stylesheet_link_tag('//cdnjs.cloudflare.com/ajax/libs/vis/3.10.0/vis.min.css')
|
||||
= javascript_include_tag(asset_path('vis.min.js', type: :javascript))
|
||||
= stylesheet_link_tag(asset_path('vis.min.css', type: :stylesheet))
|
||||
|
||||
h1 = t('breadcrumbs.dashboard.show')
|
||||
|
||||
|
@@ -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'
|
@@ -5,12 +5,12 @@ html lang='en'
|
||||
meta name='viewport' content='width=device-width, initial-scale=1'
|
||||
title = application_name
|
||||
link href=asset_path('favicon.png') rel='icon' type='image/png'
|
||||
= stylesheet_link_tag('//maxcdn.bootstrapcdn.com/bootswatch/3.3.4/yeti/bootstrap.min.css')
|
||||
= stylesheet_link_tag('//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css')
|
||||
= stylesheet_link_tag(asset_path('bootstrap.min.css', type: :stylesheet))
|
||||
= stylesheet_link_tag(asset_path('font-awesome.min.css', type: :stylesheet))
|
||||
= stylesheet_link_tag('application', media: 'all', 'data-turbolinks-track' => true)
|
||||
= javascript_include_tag('application', 'data-turbolinks-track' => true)
|
||||
= javascript_include_tag('//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js')
|
||||
= javascript_include_tag('//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js')
|
||||
= javascript_include_tag(asset_path('underscore-min.js', type: :javascript))
|
||||
= javascript_include_tag(asset_path('bootstrap.min.js', type: :javascript))
|
||||
= yield(:head)
|
||||
= csrf_meta_tags
|
||||
body
|
||||
|
Reference in New Issue
Block a user