Merge branch 'master' into fix-additional-line

This commit is contained in:
Ralf Teusner
2015-12-16 16:33:30 +01:00
79 changed files with 1347 additions and 36 deletions

View File

@@ -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); };

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

@@ -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')

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

@@ -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