Merge remote-tracking branch 'origin/master' into ace-editor-exercise-edit

This commit is contained in:
yqbk
2016-08-12 13:23:02 +02:00
67 changed files with 520 additions and 451 deletions

View File

@ -17,5 +17,4 @@ language: ruby
rvm: rvm:
- 2.1.5 - 2.1.5
- 2.2.1 - 2.2.1
- jruby-19mode
script: bundle exec rspec --tag ~docker script: bundle exec rspec --tag ~docker

View File

@ -28,6 +28,8 @@ gem 'rubytree'
gem 'sass-rails', '~> 4.0.3' gem 'sass-rails', '~> 4.0.3'
gem 'sdoc', '~> 0.4.0', group: :doc gem 'sdoc', '~> 0.4.0', group: :doc
gem 'slim' gem 'slim'
gem 'bootstrap_pagedown'
gem 'pagedown-rails', '~> 1.1.4'
gem 'sorcery' gem 'sorcery'
gem 'thread_safe' gem 'thread_safe'
gem 'turbolinks' gem 'turbolinks'
@ -38,6 +40,7 @@ gem 'faye-websocket'
gem 'nokogiri' gem 'nokogiri'
gem 'd3-rails' gem 'd3-rails'
gem 'rest-client' gem 'rest-client'
gem 'rubyzip'
group :development do group :development do
gem 'better_errors', platform: :ruby gem 'better_errors', platform: :ruby
@ -50,12 +53,13 @@ group :development do
gem 'rack-mini-profiler' gem 'rack-mini-profiler'
gem 'rubocop', require: false gem 'rubocop', require: false
gem 'rubocop-rspec' gem 'rubocop-rspec'
gem 'web-console', '~> 2.0', platform: :ruby
end end
group :development, :test do group :development, :test do
gem 'byebug', platform: :ruby gem 'byebug', platform: :ruby
gem 'spring' gem 'spring'
gem 'web-console', '~> 2.0', platform: :ruby
end end
group :test do group :test do

View File

@ -48,6 +48,8 @@ GEM
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootstrap-will_paginate (0.0.10) bootstrap-will_paginate (0.0.10)
will_paginate will_paginate
bootstrap_pagedown (1.1.0)
rails (>= 3.2)
builder (3.2.2) builder (3.2.2)
byebug (8.2.2) byebug (8.2.2)
capistrano (3.3.5) capistrano (3.3.5)
@ -175,6 +177,8 @@ GEM
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
pagedown-rails (1.1.4)
railties (> 3.1)
parser (2.3.0.6) parser (2.3.0.6)
ast (~> 2.2) ast (~> 2.2)
pg (0.18.4) pg (0.18.4)
@ -355,6 +359,7 @@ DEPENDENCIES
better_errors better_errors
binding_of_caller binding_of_caller
bootstrap-will_paginate bootstrap-will_paginate
bootstrap_pagedown
byebug byebug
capistrano (~> 3.3.0) capistrano (~> 3.3.0)
capistrano-rails capistrano-rails
@ -382,6 +387,7 @@ DEPENDENCIES
newrelic_rpm newrelic_rpm
nokogiri nokogiri
nyan-cat-formatter nyan-cat-formatter
pagedown-rails (~> 1.1.4)
pg pg
pry pry
puma (~> 2.15.3) puma (~> 2.15.3)
@ -397,6 +403,7 @@ DEPENDENCIES
rubocop rubocop
rubocop-rspec rubocop-rspec
rubytree rubytree
rubyzip
sass-rails (~> 4.0.3) sass-rails (~> 4.0.3)
sdoc (~> 0.4.0) sdoc (~> 0.4.0)
selenium-webdriver selenium-webdriver
@ -410,3 +417,6 @@ DEPENDENCIES
uglifier (>= 1.3.0) uglifier (>= 1.3.0)
web-console (~> 2.0) web-console (~> 2.0)
will_paginate (~> 3.0) will_paginate (~> 3.0)
BUNDLED WITH
1.12.4

View File

@ -21,3 +21,7 @@
//= require turbolinks //= require turbolinks
//= require_tree ../../../lib //= require_tree ../../../lib
//= require_tree . //= require_tree .
//= require bootstrap_pagedown
//= require markdown.converter
//= require markdown.sanitizer
//= require markdown.editor

View File

@ -19,6 +19,10 @@ $(function() {
var SERVER_SEND_EVENT = 2; var SERVER_SEND_EVENT = 2;
var editors = []; var editors = [];
var editor_for_file = new Map();
var regex_for_language = new Map();
var tracepositions_regex;
var active_file = undefined; var active_file = undefined;
var active_frame = undefined; var active_frame = undefined;
var running = false; var running = false;
@ -37,6 +41,7 @@ $(function() {
var ENTER_KEY_CODE = 13; var ENTER_KEY_CODE = 13;
var flowrOutputBuffer = ""; var flowrOutputBuffer = "";
var QaApiOutputBuffer = {'stdout': '', 'stderr': ''};
var flowrResultHtml = '<div class="panel panel-default"><div id="{{headingId}}" role="tab" class="panel-heading"><h4 class="panel-title"><a data-toggle="collapse" data-parent="#flowrHint" href="#{{collapseId}}" aria-expanded="true" aria-controls="{{collapseId}}"></a></h4></div><div id="{{collapseId}}" role="tabpanel" aria-labelledby="{{headingId}}" class="panel-collapse collapse"><div class="panel-body"></div></div></div>' var flowrResultHtml = '<div class="panel panel-default"><div id="{{headingId}}" role="tab" class="panel-heading"><h4 class="panel-title"><a data-toggle="collapse" data-parent="#flowrHint" href="#{{collapseId}}" aria-expanded="true" aria-controls="{{collapseId}}"></a></h4></div><div id="{{collapseId}}" role="tabpanel" aria-labelledby="{{headingId}}" class="panel-collapse collapse"><div class="panel-body"></div></div></div>'
var ajax = function(options) { var ajax = function(options) {
@ -175,7 +180,10 @@ $(function() {
var downloadCode = function(event) { var downloadCode = function(event) {
event.preventDefault(); event.preventDefault();
createSubmission(this, null,function(response) { createSubmission(this, null,function(response) {
var url = response.download_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); var url = response.download_url;
// to download just a single file, use the following url
//var url = response.download_file_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
window.location = url; window.location = url;
}); });
}; };
@ -184,8 +192,8 @@ $(function() {
(streamed ? evaluateCodeWithStreamedResponse : evaluateCodeWithoutStreamedResponse)(url, callback); (streamed ? evaluateCodeWithStreamedResponse : evaluateCodeWithoutStreamedResponse)(url, callback);
}; };
var evaluateCodeWithStreamedResponse = function(url, callback) { var evaluateCodeWithStreamedResponse = function(url, onmessageFunction) {
initWebsocketConnection(url); initWebsocketConnection(url, onmessageFunction);
// TODO only init turtle when required // TODO only init turtle when required
initTurtle(); initTurtle();
@ -306,9 +314,10 @@ $(function() {
} }
}; };
var handleScoringResponse = function(response) { var handleScoringResponse = function(websocket_event) {
printScoringResults(response); results = JSON.parse(websocket_event.data);
var score = _.reduce(response, function(sum, result) { printScoringResults(results);
var score = _.reduce(results, function(sum, result) {
return sum + result.score * result.weight; return sum + result.score * result.weight;
}, 0).toFixed(2); }, 0).toFixed(2);
$('#score').data('score', score); $('#score').data('score', score);
@ -316,6 +325,14 @@ $(function() {
showTab(2); showTab(2);
}; };
var handleQaApiOutput = function() {
if (qa_api) {
qa_api.executeCommand('syncOutput', [[QaApiOutputBuffer]]);
// reset the object
}
QaApiOutputBuffer = {'stdout': '', 'stderr': ''};
}
// activate flowr only for half of the audience // activate flowr only for half of the audience
var isFlowrEnabled = true;//parseInt($('#editor').data('user-id'))%2 == 0; var isFlowrEnabled = true;//parseInt($('#editor').data('user-id'))%2 == 0;
var handleStderrOutputForFlowr = function() { var handleStderrOutputForFlowr = function() {
@ -349,13 +366,14 @@ $(function() {
flowrOutputBuffer = ''; flowrOutputBuffer = '';
}; };
var handleTestResponse = function(response) { var handleTestResponse = function(websocket_event) {
result = JSON.parse(websocket_event.data);
clearOutput(); clearOutput();
printOutput(response[0], false, 0); printOutput(result, false, 0);
if (qa_api) { if (qa_api) {
qa_api.executeCommand('syncOutput', [response]); qa_api.executeCommand('syncOutput', [result]);
} }
showStatus(response[0]); showStatus(result);
showTab(1); showTab(1);
}; };
@ -404,12 +422,18 @@ $(function() {
editor.setTheme(THEME); editor.setTheme(THEME);
editor.commands.bindKey("ctrl+alt+0", null); editor.commands.bindKey("ctrl+alt+0", null);
editors.push(editor); editors.push(editor);
editor_for_file.set($(element).parent().data('filename'), editor);
var session = editor.getSession(); var session = editor.getSession();
session.setMode($(element).data('mode')); session.setMode($(element).data('mode'));
session.setTabSize($(element).data('indent-size')); session.setTabSize($(element).data('indent-size'));
session.setUseSoftTabs(true); session.setUseSoftTabs(true);
session.setUseWrapMode(true); session.setUseWrapMode(true);
// set regex for parsing error traces based on the mode of the main file.
if( $(element).parent().data('role') == "main_file"){
tracepositions_regex = regex_for_language.get($(element).data('mode'));
}
var file_id = $(element).data('id'); var file_id = $(element).data('id');
/* /*
@ -457,6 +481,12 @@ $(function() {
$('#request-for-comments').on('click', requestComments); $('#request-for-comments').on('click', requestComments);
}; };
var initializeRegexes = function(){
regex_for_language.set("ace/mode/python", /File "(.+?)", line (\d+)/g);
regex_for_language.set("ace/mode/java", /(.*\.java):(\d+):/g);
}
var initializeTooltips = function() { var initializeTooltips = function() {
$('[data-tooltip]').tooltip(); $('[data-tooltip]').tooltip();
}; };
@ -527,8 +557,8 @@ $(function() {
}; };
var isBrowserSupported = function() { var isBrowserSupported = function() {
// eventsource tests for server send events (used for scoring), websockets is used for run // websockets is used for run, score and test
return Modernizr.eventsource && Modernizr.websockets; return Modernizr.websockets;
}; };
var populatePanel = function(panel, result, index) { var populatePanel = function(panel, result, index) {
@ -574,20 +604,23 @@ $(function() {
// output_mode_is_streaming = false; // output_mode_is_streaming = false;
//} //}
if (!colorize) { if (!colorize) {
if(output.stdout != ''){ if(output.stdout != undefined && output.stdout != ''){
element.append(output.stdout) element.append(output.stdout)
} }
if(output.stderr != ''){ if(output.stderr != undefined && output.stderr != ''){
element.append('There was an error: StdErr: ' + output.stderr); element.append('There was an error: StdErr: ' + output.stderr);
} }
} else if (output.stderr) { } else if (output.stderr) {
element.addClass('text-warning').append(output.stderr); element.addClass('text-warning').append(output.stderr);
flowrOutputBuffer += output.stderr;
QaApiOutputBuffer.stderr += output.stderr;
} else if (output.stdout) { } else if (output.stdout) {
//if (output_mode_is_streaming){ //if (output_mode_is_streaming){
element.addClass('text-success').append(output.stdout); element.addClass('text-success').append(output.stdout);
flowrOutputBuffer += output.stdout; flowrOutputBuffer += output.stdout;
QaApiOutputBuffer.stdout += output.stdout;
//}else{ //}else{
// element.addClass('text-success'); // element.addClass('text-success');
// element.data('content_buffer' , element.data('content_buffer') + output.stdout); // element.data('content_buffer' , element.data('content_buffer') + output.stdout);
@ -743,7 +776,7 @@ $(function() {
showSpinner($('#run')); showSpinner($('#run'));
toggleButtonStates(); toggleButtonStates();
var url = response.run_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); var url = response.run_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
evaluateCode(url, true, printChunk); evaluateCode(url, true, function(evt) { parseCanvasMessage(evt.data, true); });
}); });
} }
}; };
@ -778,7 +811,7 @@ $(function() {
createSubmission(this, null, function(response) { createSubmission(this, null, function(response) {
showSpinner($('#assess')); showSpinner($('#assess'));
var url = response.score_url; var url = response.score_url;
evaluateCode(url, false, handleScoringResponse); evaluateCode(url, true, handleScoringResponse);
}); });
}; };
@ -876,7 +909,9 @@ $(function() {
} }
var showWorkspaceTab = function(event) { var showWorkspaceTab = function(event) {
event.preventDefault(); if(event){
event.preventDefault();
}
showTab(0); showTab(0);
}; };
@ -955,7 +990,7 @@ $(function() {
createSubmission(this, null, function(response) { createSubmission(this, null, function(response) {
showSpinner($('#test')); showSpinner($('#test'));
var url = response.test_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename); var url = response.test_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
evaluateCode(url, false, handleTestResponse); evaluateCode(url, true, handleTestResponse);
}); });
} }
}; };
@ -974,14 +1009,14 @@ $(function() {
$('#test').toggle(isActiveFileTestable()); $('#test').toggle(isActiveFileTestable());
}; };
var initWebsocketConnection = function(url) { var initWebsocketConnection = function(url, onmessageFunction) {
//TODO: get the protocol from config file dependent on environment. (dev: ws, prod: wss) //TODO: get the protocol from config file dependent on environment. (dev: ws, prod: wss)
//causes: Puma::HttpParserError: Invalid HTTP format, parsing fails. //causes: Puma::HttpParserError: Invalid HTTP format, parsing fails.
//TODO: make sure that this gets cached. //TODO: make sure that this gets cached.
websocket = new WebSocket('<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + window.location.port + url); 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.onopen = function(evt) { resetOutputTab(); }; // todo show some kind of indicator for established connection
websocket.onclose = function(evt) { /* expected at some point */ }; websocket.onclose = function(evt) { /* expected at some point */ };
websocket.onmessage = function(evt) { parseCanvasMessage(evt.data, true); }; websocket.onmessage = onmessageFunction;
websocket.onerror = function(evt) { showWebsocketError(); }; websocket.onerror = function(evt) { showWebsocketError(); };
websocket.flush = function() { this.send('\n'); } websocket.flush = function() { this.send('\n'); }
}; };
@ -1035,7 +1070,9 @@ $(function() {
break; break;
case 'exit': case 'exit':
killWebsocketAndContainer(); killWebsocketAndContainer();
handleQaApiOutput();
handleStderrOutputForFlowr(); handleStderrOutputForFlowr();
augmentStacktraceInOutput();
break; break;
case 'timeout': case 'timeout':
// just show the timeout message here. Another exit command is sent by the rails backend when the socket to the docker container closes. // just show the timeout message here. Another exit command is sent by the rails backend when the socket to the docker container closes.
@ -1047,6 +1084,41 @@ $(function() {
} }
}; };
var jumpToSourceLine = function(event){
var file = $(event.target).data('file');
var line = $(event.target).data('line');
showWorkspaceTab(null);
// set active file ?!?!
var frame = $('div.frame[data-filename="' + file + '"]');
showFrame(frame);
var editor = editor_for_file.get(file);
editor.gotoLine(line, 0);
};
var augmentStacktraceInOutput = function() {
if(tracepositions_regex){
var element = $('#output>pre');
var text = element.text();
element.on( "click", "a", jumpToSourceLine);
var matches;
while(matches = tracepositions_regex.exec(text)){
var frame = $('div.frame[data-filename="' + matches[1] + '"]')
if(frame.length > 0){
element.html(text.replace(matches[0], "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>"));
}
}
}
};
var renderWebsocketOutput = function(msg){ var renderWebsocketOutput = function(msg){
var element = findOrCreateRenderElement(0); var element = findOrCreateRenderElement(0);
element.append(msg.data); element.append(msg.data);
@ -1160,25 +1232,25 @@ $(function() {
var file_id = $('.editor').data('id') var file_id = $('.editor').data('id')
var question = $('#question').val(); var question = $('#question').val();
$.ajax({ var createRequestForComments = function(submission) {
method: 'POST', $.ajax({
url: '/request_for_comments', method: 'POST',
data: { url: '/request_for_comments',
request_for_comment: { data: {
exercise_id: exercise_id, request_for_comment: {
file_id: file_id, exercise_id: exercise_id,
question: question, file_id: file_id,
"requested_at(1i)": 2015, // these are the timestamp values that the request handler demands submission_id: submission.id,
"requested_at(2i)":3, // they could be random here, because the timestamp is updated on serverside anyway question: question
"requested_at(3i)":27, }
"requested_at(4i)":17,
"requested_at(5i)":06
} }
} }).done(function() {
}).done(function() { hideSpinner();
hideSpinner(); $.flash.success({ text: $('#askForCommentsButton').data('message-success') });
$.flash.success({ text: $('#askForCommentsButton').data('message-success') }) }).error(ajaxError);
}).error(ajaxError); }
createSubmission($('.requestCommentsButton'), null, createRequestForComments);
$('#comment-modal').modal('hide'); $('#comment-modal').modal('hide');
var button = $('.requestCommentsButton'); var button = $('.requestCommentsButton');
@ -1207,6 +1279,7 @@ $(function() {
if ($('#editor').isPresent()) { if ($('#editor').isPresent()) {
if (isBrowserSupported()) { if (isBrowserSupported()) {
initializeRegexes();
initializeCodePilot(); initializeCodePilot();
$('.score, #development-environment').show(); $('.score, #development-environment').show();
configureEditors(); configureEditors();

View File

@ -151,20 +151,41 @@ $(function() {
}); });
}; };
var updateFileTemplates = function(fileType) {
var jqxhr = $.ajax({
url: '/file_templates/by_file_type/' + fileType + '.json',
dataType: 'json'
});
jqxhr.done(function(response) {
var noTemplateLabel = $('#noTemplateLabel').data('text');
var options = "<option value>" + noTemplateLabel + "</option>";
for (var i = 0; i < response.length; i++) {
options += "<option value='" + response[i].id + "'>" + response[i].name + "</option>"
}
$("#code_ocean_file_file_template_id").find('option').remove().end().append($(options));
});
jqxhr.fail(ajaxError);
}
if ($.isController('exercises')) { if ($.isController('exercises')) {
if ($('table').isPresent()) { if ($('table').isPresent()) {
enableBatchUpdate(); enableBatchUpdate();
} else if ($('.edit_exercise, .new_exercise').isPresent()) { } else if ($('.edit_exercise, .new_exercise').isPresent()) {
execution_environments = $('form').data('execution-environments'); execution_environments = $('form').data('execution-environments');
file_types = $('form').data('file-types'); file_types = $('form').data('file-types');
// new MarkdownEditor('#exercise_instructions'); // new MarkdownEditor('#exercise_instructions');
new MarkdownEditor('#exercise_description'); // new MarkdownEditor('#exercise_description')
// todo: add an ace editor for each file // todo: add an ace editor for each file
new PagedownEditor('#exercise_description');
enableInlineFileCreation(); enableInlineFileCreation();
inferFileAttributes(); inferFileAttributes();
observeFileRoleChanges(); observeFileRoleChanges();
overrideTextareaTabBehavior(); overrideTextareaTabBehavior();
} else if ($('#files.jstree').isPresent()) {
var fileTypeSelect = $('#code_ocean_file_file_type_id');
fileTypeSelect.on("change", function() {updateFileTemplates(fileTypeSelect.val())});
updateFileTemplates(fileTypeSelect.val());
} }
toggleCodeHeight(); toggleCodeHeight();
if (window.hljs) { if (window.hljs) {

View File

@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@ -0,0 +1,16 @@
(function() {
var ACE_FILES_PATH = '/assets/ace/';
window.MarkdownEditor = function(selector) {
ace.config.set('modePath', ACE_FILES_PATH);
var editor = ace.edit($(selector).next()[0]);
editor.on('change', function() {
$(selector).val(editor.getValue());
});
editor.setShowPrintMargin(false);
var session = editor.getSession();
session.setMode('markdown');
session.setUseWrapMode(true);
session.setValue($(selector).val());
};
})();

View File

@ -1,16 +0,0 @@
(function() {
var ACE_FILES_PATH = '/assets/ace/';
window.MarkdownEditor = function(selector) {
ace.config.set('modePath', ACE_FILES_PATH);
var editor = ace.edit($(selector).next()[0]);
editor.on('change', function() {
$(selector).val(editor.getValue());
});
editor.setShowPrintMargin(false);
var session = editor.getSession();
session.setMode('markdown');
session.setUseWrapMode(true);
session.setValue($(selector).val());
};
})();

View File

@ -0,0 +1,10 @@
(function() {
var ACE_FILES_PATH = '/assets/ace/';
window.PagedownEditor = function(selector) {
var converter = Markdown.getSanitizingConverter();
var editor = new Markdown.Editor( converter );
editor.run();
};
})();

View File

@ -14,4 +14,6 @@
*= require_tree ../../../lib *= require_tree ../../../lib
*= require_tree ../../../vendor/assets/stylesheets/ *= require_tree ../../../vendor/assets/stylesheets/
*= require_self *= require_self
*/ *= require bootstrap_pagedown
*= require markdown
*/

View File

@ -0,0 +1,3 @@
// Place all the styles related to the FileTemplates controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -10,6 +10,11 @@ module CodeOcean
def create def create
@file = CodeOcean::File.new(file_params) @file = CodeOcean::File.new(file_params)
if @file.file_template_id
content = FileTemplate.find(@file.file_template_id).content
content.sub! '{{file_name}}', @file.name
@file.content = content
end
authorize! authorize!
create_and_respond(object: @file, path: proc { implement_exercise_path(@file.context.exercise, tab: 2) }) create_and_respond(object: @file, path: proc { implement_exercise_path(@file.context.exercise, tab: 2) })
end end

View File

@ -1,6 +1,6 @@
module FileParameters module FileParameters
def file_attributes def file_attributes
%w(content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight) %w(content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight file_template_id)
end end
private :file_attributes private :file_attributes
end end

View File

@ -9,7 +9,6 @@ class ExercisesController < ApplicationController
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :edit, :run, :statistics, :submit, :reload] before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :edit, :run, :statistics, :submit, :reload]
before_action :set_external_user, only: [:statistics] before_action :set_external_user, only: [:statistics]
before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_file_types, only: [:create, :edit, :new, :update]
before_action :set_teams, only: [:create, :edit, :new, :update]
skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml] skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml]
skip_after_action :verify_authorized, only: [:import_proforma_xml] skip_after_action :verify_authorized, only: [:import_proforma_xml]
@ -121,7 +120,7 @@ class ExercisesController < ApplicationController
private :user_by_code_harbor_token private :user_by_code_harbor_token
def exercise_params def exercise_params
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :team_id, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name) params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
end end
private :exercise_params private :exercise_params
@ -197,11 +196,6 @@ class ExercisesController < ApplicationController
end end
private :set_file_types private :set_file_types
def set_teams
@teams = Team.all.order(:name)
end
private :set_teams
def show def show
end end

View File

@ -0,0 +1,94 @@
class FileTemplatesController < ApplicationController
before_action :set_file_template, only: [:show, :edit, :update, :destroy]
def authorize!
authorize(@file_template || @file_templates)
end
private :authorize!
def by_file_type
@file_templates = FileTemplate.where(:file_type_id => params[:file_type_id])
authorize!
respond_to do |format|
format.json { render :show, status: :ok, json: @file_templates.to_json }
end
end
# GET /file_templates
# GET /file_templates.json
def index
@file_templates = FileTemplate.all.order(:file_type_id).paginate(page: params[:page])
authorize!
end
# GET /file_templates/1
# GET /file_templates/1.json
def show
authorize!
end
# GET /file_templates/new
def new
@file_template = FileTemplate.new
authorize!
end
# GET /file_templates/1/edit
def edit
authorize!
end
# POST /file_templates
# POST /file_templates.json
def create
@file_template = FileTemplate.new(file_template_params)
authorize!
respond_to do |format|
if @file_template.save
format.html { redirect_to @file_template, notice: 'File template was successfully created.' }
format.json { render :show, status: :created, location: @file_template }
else
format.html { render :new }
format.json { render json: @file_template.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /file_templates/1
# PATCH/PUT /file_templates/1.json
def update
authorize!
respond_to do |format|
if @file_template.update(file_template_params)
format.html { redirect_to @file_template, notice: 'File template was successfully updated.' }
format.json { render :show, status: :ok, location: @file_template }
else
format.html { render :edit }
format.json { render json: @file_template.errors, status: :unprocessable_entity }
end
end
end
# DELETE /file_templates/1
# DELETE /file_templates/1.json
def destroy
authorize!
@file_template.destroy
respond_to do |format|
format.html { redirect_to file_templates_url, notice: 'File template was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_file_template
@file_template = FileTemplate.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def file_template_params
params[:file_template].permit(:name, :file_type_id, :content)
end
end

View File

@ -82,6 +82,6 @@ class RequestForCommentsController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through. # Never trust parameters from the scary internet, only allow the white list through.
def request_for_comment_params def request_for_comment_params
params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved).merge(user_id: current_user.id, user_type: current_user.class.name) params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name)
end end
end end

View File

@ -6,9 +6,9 @@ class SubmissionsController < ApplicationController
include SubmissionScoring include SubmissionScoring
include Tubesock::Hijack include Tubesock::Hijack
before_action :set_submission, only: [:download_file, :render_file, :run, :score, :show, :statistics, :stop, :test] before_action :set_submission, only: [:download, :download_file, :render_file, :run, :score, :show, :statistics, :stop, :test]
before_action :set_docker_client, only: [:run, :test] before_action :set_docker_client, only: [:run, :test]
before_action :set_files, only: [:download_file, :render_file, :show] before_action :set_files, only: [:download, :download_file, :render_file, :show]
before_action :set_file, only: [:download_file, :render_file] before_action :set_file, only: [:download_file, :render_file]
before_action :set_mime_type, only: [:download_file, :render_file] before_action :set_mime_type, only: [:download_file, :render_file]
skip_before_action :verify_authenticity_token, only: [:download_file, :render_file] skip_before_action :verify_authenticity_token, only: [:download_file, :render_file]
@ -53,6 +53,20 @@ class SubmissionsController < ApplicationController
end end
end end
def download
# files = @submission.files.map{ }
# zipline( files, 'submission.zip')
# send_data(@file.content, filename: @file.name_with_extension)
require 'zip'
stringio = Zip::OutputStream.write_buffer do |zio|
@files.each do |file|
zio.put_next_entry(file.name_with_extension)
zio.write(file.content)
end
end
send_data(stringio.string, filename: @submission.exercise.title.tr(" ", "_") + ".zip")
end
def download_file def download_file
if @file.native_file? if @file.native_file?
send_file(@file.native_file.path) send_file(@file.native_file.path)
@ -214,7 +228,11 @@ class SubmissionsController < ApplicationController
end end
def score def score
render(json: score_submission(@submission)) hijack do |tubesock|
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
# tubesock is the socket to the client
tubesock.send_data JSON.dump(score_submission(@submission))
end
end end
def set_docker_client def set_docker_client
@ -266,8 +284,14 @@ class SubmissionsController < ApplicationController
private :store_error private :store_error
def test def test
output = @docker_client.execute_test_command(@submission, params[:filename]) hijack do |tubesock|
render(json: [output]) Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
output = @docker_client.execute_test_command(@submission, params[:filename])
# tubesock is the socket to the client
tubesock.send_data JSON.dump(output)
end
end end
def with_server_sent_events def with_server_sent_events

View File

@ -1,51 +0,0 @@
class TeamsController < ApplicationController
include CommonBehavior
before_action :set_team, only: MEMBER_ACTIONS
def authorize!
authorize(@team || @teams)
end
private :authorize!
def create
@team = Team.new(team_params)
authorize!
create_and_respond(object: @team)
end
def destroy
destroy_and_respond(object: @team)
end
def edit
end
def index
@teams = Team.all.includes(:internal_users).order(:name).paginate(page: params[:page])
authorize!
end
def new
@team = Team.new
authorize!
end
def set_team
@team = Team.find(params[:id])
authorize!
end
private :set_team
def show
end
def team_params
params[:team].permit(:name, internal_user_ids: [])
end
private :team_params
def update
update_and_respond(object: @team, params: team_params)
end
end

View File

@ -11,7 +11,6 @@ class Exercise < ActiveRecord::Base
belongs_to :execution_environment belongs_to :execution_environment
has_many :submissions has_many :submissions
belongs_to :team
has_many :external_users, source: :user, 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 has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions

View File

@ -0,0 +1,10 @@
class FileTemplate < ActiveRecord::Base
belongs_to :file_type
def to_s
name
end
end

View File

@ -12,6 +12,7 @@ class FileType < ActiveRecord::Base
has_many :execution_environments has_many :execution_environments
has_many :files has_many :files
has_many :file_templates
validates :binary, boolean_presence: true validates :binary, boolean_presence: true
validates :editor_mode, presence: true, unless: :binary? validates :editor_mode, presence: true, unless: :binary?

View File

@ -3,8 +3,6 @@ class InternalUser < ActiveRecord::Base
authenticates_with_sorcery! authenticates_with_sorcery!
has_and_belongs_to_many :teams
validates :email, presence: true, uniqueness: true validates :email, presence: true, uniqueness: true
validates :password, confirmation: true, if: :password_void?, on: :update, presence: true validates :password, confirmation: true, if: :password_void?, on: :update, presence: true
validates :role, inclusion: {in: ROLES} validates :role, inclusion: {in: ROLES}

View File

@ -1,5 +1,6 @@
class RequestForComment < ActiveRecord::Base class RequestForComment < ActiveRecord::Base
include Creation include Creation
belongs_to :submission
belongs_to :exercise belongs_to :exercise
belongs_to :file, class_name: 'CodeOcean::File' belongs_to :file, class_name: 'CodeOcean::File'
@ -13,10 +14,8 @@ class RequestForComment < ActiveRecord::Base
self.requested_at = Time.now self.requested_at = Time.now
end end
def submission # not used right now, finds the last submission for the respective user and exercise.
Submission.find(file.context_id) # might be helpful to check whether the exercise has been solved in the meantime.
end
def last_submission def last_submission
Submission.find_by_sql(" select * from submissions Submission.find_by_sql(" select * from submissions
where exercise_id = #{exercise_id} AND where exercise_id = #{exercise_id} AND
@ -25,6 +24,17 @@ class RequestForComment < ActiveRecord::Base
limit 1").first limit 1").first
end end
# not used any longer, since we directly saved the submission_id now.
# Was used before that to determine the submission belonging to the request_for_comment.
def last_submission_before_creation
Submission.find_by_sql(" select * from submissions
where exercise_id = #{exercise_id} AND
user_id = #{user_id} AND
'#{created_at.localtime}' > created_at
order by created_at desc
limit 1").first
end
def comments_count def comments_count
submission.files.map { |file| file.comments.size}.sum submission.files.map { |file| file.comments.size}.sum
end end
@ -35,6 +45,6 @@ class RequestForComment < ActiveRecord::Base
private private
def self.row_number_user_sql def self.row_number_user_sql
select("id, user_id, exercise_id, file_id, question, requested_at, created_at, updated_at, user_type, solved, row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) as row_number").to_sql select("id, user_id, exercise_id, file_id, question, created_at, updated_at, user_type, solved, submission_id, row_number() OVER (PARTITION BY user_id ORDER BY created_at DESC) as row_number").to_sql
end end
end end

View File

@ -2,7 +2,7 @@ class Submission < ActiveRecord::Base
include Context include Context
include Creation include Creation
CAUSES = %w(assess download file render run save submit test autosave) CAUSES = %w(assess download file render run save submit test autosave requestComments)
FILENAME_URL_PLACEHOLDER = '{filename}' FILENAME_URL_PLACEHOLDER = '{filename}'
belongs_to :exercise belongs_to :exercise
@ -28,13 +28,17 @@ class Submission < ActiveRecord::Base
ancestors.merge(descendants).values ancestors.merge(descendants).values
end end
[:download, :render, :run, :test].each do |action| [:download_file, :render, :run, :test].each do |action|
filename = FILENAME_URL_PLACEHOLDER.gsub(/\W/, '') filename = FILENAME_URL_PLACEHOLDER.gsub(/\W/, '')
define_method("#{action}_url") do define_method("#{action}_url") do
Rails.application.routes.url_helpers.send(:"#{action}_submission_path", self, filename).sub(filename, FILENAME_URL_PLACEHOLDER) Rails.application.routes.url_helpers.send(:"#{action}_submission_path", self, filename).sub(filename, FILENAME_URL_PLACEHOLDER)
end end
end end
def download_url
Rails.application.routes.url_helpers.send(:download_submission_path, self)
end
def main_file def main_file
collect_files.detect(&:main_file?) collect_files.detect(&:main_file?)
end end

View File

@ -1,10 +0,0 @@
class Team < ActiveRecord::Base
has_and_belongs_to_many :internal_users
alias_method :members, :internal_users
validates :name, presence: true
def to_s
name
end
end

View File

@ -13,24 +13,19 @@ class ExercisePolicy < AdminOrAuthorPolicy
end end
[:clone?, :destroy?, :edit?, :statistics?, :update?].each do |action| [:clone?, :destroy?, :edit?, :statistics?, :update?].each do |action|
define_method(action) { admin? || author? || team_member? } define_method(action) { admin? || author?}
end end
[:implement?, :submit?, :reload?].each do |action| [:implement?, :submit?, :reload?].each do |action|
define_method(action) { everyone } define_method(action) { everyone }
end end
def team_member?
@record.team.try(:members, []).include?(@user) if @record.team
end
private :team_member?
class Scope < Scope class Scope < Scope
def resolve def resolve
if @user.admin? if @user.admin?
@scope.all @scope.all
elsif @user.internal_user? elsif @user.internal_user?
@scope.where('user_id = ? OR public = TRUE OR (team_id IS NOT NULL AND team_id IN (SELECT t.id FROM teams t JOIN internal_users_teams iut ON t.id = iut.team_id WHERE iut.internal_user_id = ?))', @user.id, @user.id) @scope.where('user_id = ? OR public = TRUE', @user.id)
else else
@scope.none @scope.none
end end

View File

@ -0,0 +1,11 @@
class FileTemplatePolicy < AdminOnlyPolicy
def show?
everyone
end
def by_file_type?
everyone
end
end

View File

@ -8,7 +8,7 @@ class SubmissionPolicy < ApplicationPolicy
everyone everyone
end end
[:download_file?, :render_file?, :run?, :score?, :show?, :statistics?, :stop?, :test?].each do |action| [:download?, :download_file?, :render_file?, :run?, :score?, :show?, :statistics?, :stop?, :test?].each do |action|
define_method(action) { admin? || author? } define_method(action) { admin? || author? }
end end

View File

@ -1,14 +0,0 @@
class TeamPolicy < ApplicationPolicy
[:create?, :index?, :new?].each do |action|
define_method(action) { admin? }
end
[:destroy?, :edit?, :show?, :update?].each do |action|
define_method(action) { admin? || member? }
end
def member?
@record.members.include?(@user)
end
private :member?
end

View File

@ -8,7 +8,7 @@
- if current_user.admin? - if current_user.admin?
li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path) li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path)
li.divider li.divider
- models = [ExecutionEnvironment, Exercise, Consumer, CodeHarborLink, ExternalUser, FileType, InternalUser, Submission, Team].sort_by { |model| model.model_name.human(count: 2) } - models = [ExecutionEnvironment, Exercise, Consumer, CodeHarborLink, ExternalUser, FileType, FileTemplate, InternalUser].sort_by { |model| model.model_name.human(count: 2) }
- models.each do |model| - models.each do |model|
- if policy(model).index? - if policy(model).index?
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path")) li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))

View File

@ -8,5 +8,9 @@
.form-group .form-group
= f.label(:file_type_id, t('activerecord.attributes.file.file_type_id')) = f.label(:file_type_id, t('activerecord.attributes.file.file_type_id'))
= f.collection_select(:file_type_id, FileType.where(binary: false).order(:name), :id, :name, {selected: @exercise.execution_environment.file_type.try(:id)}, class: 'form-control') = f.collection_select(:file_type_id, FileType.where(binary: false).order(:name), :id, :name, {selected: @exercise.execution_environment.file_type.try(:id)}, class: 'form-control')
.form-group
= f.label(:file_template_id, t('activerecord.attributes.file.file_template_id'))
= f.collection_select(:file_template_id, FileTemplate.all.order(:name), :id, :name, {:include_blank => true}, class: 'form-control')
= f.hidden_field(:context_id) = f.hidden_field(:context_id)
.hidden#noTemplateLabel data-text=t('file_template.no_template_label')
.actions = render('shared/submit_button', f: f, object: CodeOcean::File.new) .actions = render('shared/submit_button', f: f, object: CodeOcean::File.new)

View File

@ -14,6 +14,6 @@
.editor-content.hidden data-file-id=file.ancestor_id = file.content .editor-content.hidden data-file-id=file.ancestor_id = file.content
.editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-read-only=file.read_only data-id=file.id .editor data-file-id=file.ancestor_id data-indent-size=file.file_type.indent_size data-mode=file.file_type.editor_mode data-read-only=file.read_only data-id=file.id
button.btn.btn-primary.requestCommentsButton type='button' button.btn.btn-primary.requestCommentsButton type='button' id="requestComments"
i.fa.fa-comment-o i.fa.fa-comment
= t('exercises.editor.requestComments') = t('exercises.editor.requestComments')

View File

@ -8,8 +8,8 @@
= f.text_field(:title, class: 'form-control', required: true) = f.text_field(:title, class: 'form-control', required: true)
.form-group .form-group
= f.label(:description) = f.label(:description)
= f.hidden_field(:description) = f.pagedown_editor :description
.form-control.markdown
.form-group .form-group
= f.label(:execution_environment_id) = f.label(:execution_environment_id)
= f.collection_select(:execution_environment_id, @execution_environments, :id, :name, {}, class: 'form-control') = f.collection_select(:execution_environment_id, @execution_environments, :id, :name, {}, class: 'form-control')
@ -17,9 +17,6 @@
= f.label(:instructions) = f.label(:instructions)
= f.hidden_field(:instructions) = f.hidden_field(:instructions)
.form-control.markdown .form-control.markdown
/.form-group
= f.label(:team_id)
= f.collection_select(:team_id, @teams, :id, :name, {include_blank: true}, class: 'form-control')
.checkbox .checkbox
label label
= f.check_box(:public) = f.check_box(:public)

View File

@ -50,9 +50,9 @@ h1 = "#{@exercise} (external user #{@external_user})"
td td
-submission.testruns.each do |run| -submission.testruns.each do |run|
- if run.passed - if run.passed
.unit-test-result.positive-result .unit-test-result.positive-result title=run.output
- else - else
.unit-test-result.negative-result .unit-test-result.negative-result title=run.output
td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0 td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0
-working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0)) -working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0))
p = t('.addendum') p = t('.addendum')

View File

@ -13,7 +13,6 @@ h1 = Exercise.model_name.human(count: 2)
thead thead
tr tr
th = sort_link(@search, :title, t('activerecord.attributes.exercise.title')) th = sort_link(@search, :title, t('activerecord.attributes.exercise.title'))
th = sort_link(@search, :user_id, t('activerecord.attributes.exercise.user'))
th = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment')) th = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment'))
th = t('.test_files') th = t('.test_files')
th = t('activerecord.attributes.exercise.maximum_score') th = t('activerecord.attributes.exercise.maximum_score')
@ -27,17 +26,23 @@ h1 = Exercise.model_name.human(count: 2)
- @exercises.each do |exercise| - @exercises.each do |exercise|
tr data-id=exercise.id tr data-id=exercise.id
td = exercise.title td = exercise.title
td = link_to_if(exercise.author && policy(exercise.author).show?, exercise.author, exercise.author)
td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
td = exercise.files.teacher_defined_tests.count td = exercise.files.teacher_defined_tests.count
td = exercise.maximum_score td = exercise.maximum_score
td.public data-value=exercise.public? = symbol_for(exercise.public?) td.public data-value=exercise.public? = symbol_for(exercise.public?)
td = link_to(t('shared.show'), exercise) if policy(exercise).show?
td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit? td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit?
td = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(exercise).destroy?
td = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(exercise).clone?
td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement? td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement?
td = link_to(t('shared.statistics'), statistics_exercise_path(exercise)) if policy(exercise).statistics? td = link_to(t('shared.statistics'), statistics_exercise_path(exercise)) if policy(exercise).statistics?
td
.btn-group
button.btn.btn-primary-outline.btn-xs.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button')
span.caret
span.sr-only Toggle Dropdown
ul.dropdown-menu.pull-right role="menu"
li = link_to(t('shared.show'), exercise) if policy(exercise).show?
li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(exercise).destroy?
li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(exercise).clone?
= render('shared/pagination', collection: @exercises) = render('shared/pagination', collection: @exercises)
p = render('shared/new_button', model: Exercise) p = render('shared/new_button', model: Exercise)

View File

@ -12,7 +12,6 @@ h1
= row(label: 'exercise.description', value: render_markdown(@exercise.description)) = row(label: 'exercise.description', value: render_markdown(@exercise.description))
= row(label: 'exercise.execution_environment', value: link_to_if(policy(@exercise.execution_environment).show?, @exercise.execution_environment, @exercise.execution_environment)) = row(label: 'exercise.execution_environment', value: link_to_if(policy(@exercise.execution_environment).show?, @exercise.execution_environment, @exercise.execution_environment))
/= row(label: 'exercise.instructions', value: render_markdown(@exercise.instructions)) /= row(label: 'exercise.instructions', value: render_markdown(@exercise.instructions))
= row(label: 'exercise.team', value: @exercise.team ? link_to(@exercise.team, @exercise.team) : nil)
= row(label: 'exercise.maximum_score', value: @exercise.maximum_score) = row(label: 'exercise.maximum_score', value: @exercise.maximum_score)
= row(label: 'exercise.public', value: @exercise.public?) = row(label: 'exercise.public', value: @exercise.public?)
= row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?) = row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?)

View File

@ -0,0 +1,12 @@
= form_for(@file_template) do |f|
= render('shared/form_errors', object: @file_template)
.form-group
= f.label(:name)
= f.text_field(:name, class: 'form-control', required: true)
.form-group
= f.label(:file_type_id)
= f.collection_select(:file_type_id, FileType.all.order(:name), :id, :name, {}, class: 'form-control')
.form-group
= f.label(:content)
= f.text_area(:content, class: 'form-control')
.actions = render('shared/submit_button', f: f, object: @file_template)

View File

@ -0,0 +1,3 @@
h1 = @file_template
= render('form')

View File

@ -0,0 +1,20 @@
h1 = FileTemplate.model_name.human(count: 2)
.table-responsive
table.table
thead
tr
th = t('activerecord.attributes.file_template.name')
th = t('activerecord.attributes.file_template.file_type')
th colspan=3 = t('shared.actions')
tbody
- @file_templates.each do |file_template|
tr
td = file_template.name
td = link_to(file_template.file_type, file_type_path(file_template.file_type))
td = link_to(t('shared.show'), file_template)
td = link_to(t('shared.edit'), edit_file_template_path(file_template))
td = link_to(t('shared.destroy'), file_template, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
= render('shared/pagination', collection: @file_templates)
p = render('shared/new_button', model: FileTemplate)

View File

@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: FileTemplate.model_name.human)
= render('form')

View File

@ -0,0 +1,7 @@
h1
= @file_template
= render('shared/edit_button', object: @file_template)
= row(label: 'file_template.name', value: @file_template.name)
= row(label: 'file_template.file_type', value: link_to(@file_template.file_type, file_type_path(@file_template.file_type)))
= row(label: 'file_template.content', value: @file_template.content)

View File

@ -27,6 +27,6 @@ h1 = RequestForComment.model_name.human(count: 2)
td = '-' td = '-'
td = request_for_comment.comments_count td = request_for_comment.comments_count
td = request_for_comment.user.displayname td = request_for_comment.user.displayname
td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.requested_at)) td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.created_at))
= render('shared/pagination', collection: @request_for_comments) = render('shared/pagination', collection: @request_for_comments)

View File

@ -1,21 +1,14 @@
<div class="list-group"> <div class="list-group">
<h4 id ="exercise_caption" class="list-group-item-heading" data-rfc-id = "<%= @request_for_comment.id %>" ><%= Exercise.find(@request_for_comment.exercise_id) %></h4> <h4 id ="exercise_caption" class="list-group-item-heading" data-rfc-id = "<%= @request_for_comment.id %>" ><%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %></h4>
<p class="list-group-item-text"> <p class="list-group-item-text">
<% <%
user = @request_for_comment.user user = @request_for_comment.user
submission_id = ActiveRecord::Base.connection.execute("select id from submissions submission = @request_for_comment.submission
where exercise_id =
#{@request_for_comment.exercise_id} AND
user_id = #{@request_for_comment.user_id} AND
'#{@request_for_comment.created_at}' > created_at
order by created_at desc
limit 1").first['id'].to_i
submission = Submission.find(submission_id)
%> %>
<%= user.displayname %> | <%= @request_for_comment.requested_at %> <%= user.displayname %> | <%= @request_for_comment.created_at.localtime %>
</p> </p>
<h5> <h5>
<u><%= t('activerecord.attributes.exercise.instructions') %>:</u> "<%= @request_for_comment.exercise.description %>" <u><%= t('activerecord.attributes.exercise.description') %>:</u> "<%= render_markdown(@request_for_comment.exercise.description) %>"
</h5> </h5>
<h5> <h5>

View File

@ -1 +1 @@
json.extract! @submission, :download_url, :id, :score_url, :render_url, :run_url, :stop_url, :test_url, :files json.extract! @submission, :download_url, :download_file_url, :id, :score_url, :render_url, :run_url, :stop_url, :test_url, :files

View File

@ -1,9 +0,0 @@
= form_for(@team) do |f|
= render('shared/form_errors', object: @team)
.form-group
= f.label(:name)
= f.text_field(:name, class: 'form-control', required: true)
.form-group
= f.label(:internal_user_ids)
= f.collection_select(:internal_user_ids, InternalUser.all.order(:name), :id, :name, {}, {class: 'form-control', multiple: true})
.actions = render('shared/submit_button', f: f, object: @team)

View File

@ -1,3 +0,0 @@
h1 = @hint
= render('form')

View File

@ -1,20 +0,0 @@
h1 = Team.model_name.human(count: 2)
.table-responsive
table.table
thead
tr
th = t('activerecord.attributes.team.name')
th = t('activerecord.attributes.team.internal_user_ids')
th colspan=3 = t('shared.actions')
tbody
- @teams.each do |team|
tr
td = team.name
td = team.members.count
td = link_to(t('shared.show'), team_path(team.id))
td = link_to(t('shared.edit'), edit_team_path(team.id))
td = link_to(t('shared.destroy'), team_path(team.id), data: {confirm: t('shared.confirm_destroy')}, method: :delete)
= render('shared/pagination', collection: @teams)
p = render('shared/new_button', model: Team, path: new_team_path)

View File

@ -1,3 +0,0 @@
h1 = t('shared.new_model', model: Team.model_name.human)
= render('form')

View File

@ -1,9 +0,0 @@
h1
= @team
= render('shared/edit_button', object: @team, path: edit_team_path(@team.id))
= row(label: 'team.name', value: @team.name)
= row(label: 'team.internal_user_ids') do
ul.list-unstyled
- @team.members.order(:name).each do |internal_user|
li = link_to(internal_user, internal_user)

View File

@ -34,8 +34,6 @@ de:
instructions: Anweisungen instructions: Anweisungen
maximum_score: Erreichbare Punktzahl maximum_score: Erreichbare Punktzahl
public: Öffentlich public: Öffentlich
team: Team
team_id: Team
title: Titel title: Titel
user: Autor user: Autor
allow_file_creation: "Dateierstellung erlauben" allow_file_creation: "Dateierstellung erlauben"
@ -54,6 +52,7 @@ de:
read_only: Schreibgeschützt read_only: Schreibgeschützt
role: Rolle role: Rolle
weight: Punktzahl weight: Punktzahl
file_template_id: "Dateivorlage"
file_type: file_type:
binary: Binär binary: Binär
editor_mode: Editor-Modus editor_mode: Editor-Modus
@ -91,9 +90,10 @@ de:
files: Dateien files: Dateien
score: Punktzahl score: Punktzahl
user: Autor user: Autor
team: file_template:
internal_user_ids: Mitglieder name: "Name"
name: Name file_type: "Dateityp"
content: "Code"
models: models:
code_harbor_link: code_harbor_link:
one: CodeHarbor-Link one: CodeHarbor-Link
@ -116,6 +116,9 @@ de:
file: file:
one: Datei one: Datei
other: Dateien other: Dateien
file_template:
one: Dateivorlage
other: Dateivorlagen
file_type: file_type:
one: Dateityp one: Dateityp
other: Dateitypen other: Dateitypen
@ -128,9 +131,6 @@ de:
submission: submission:
one: Abgabe one: Abgabe
other: Abgaben other: Abgaben
team:
one: Team
other: Teams
errors: errors:
messages: messages:
together: 'muss zusammen mit %{attribute} definiert werden' together: 'muss zusammen mit %{attribute} definiert werden'
@ -402,6 +402,7 @@ de:
created_at: Erstellt created_at: Erstellt
destroy: Löschen destroy: Löschen
edit: Bearbeiten edit: Bearbeiten
actions_button: 'Andere Aktionen'
errors_one: 'Beim Speichern ist ein Fehler aufgetreten' errors_one: 'Beim Speichern ist ein Fehler aufgetreten'
errors_other: 'Beim Speichern sind %{count} Fehler aufgetreten' errors_other: 'Beim Speichern sind %{count} Fehler aufgetreten'
help: help:
@ -455,3 +456,5 @@ de:
next_label: 'Nächste Seite &#8594;' next_label: 'Nächste Seite &#8594;'
page_gap: '&hellip;' page_gap: '&hellip;'
previous_label: '&#8592; Vorherige Seite' previous_label: '&#8592; Vorherige Seite'
file_template:
no_template_label: "Leere Datei"

View File

@ -34,8 +34,6 @@ en:
instructions: Instructions instructions: Instructions
maximum_score: Maximum Score maximum_score: Maximum Score
public: Public public: Public
team: Team
team_id: Team
title: Title title: Title
user: Author user: Author
allow_file_creation: "Allow file creation" allow_file_creation: "Allow file creation"
@ -54,6 +52,7 @@ en:
read_only: Read-only read_only: Read-only
role: Role role: Role
weight: Score weight: Score
file_template_id: "File Template"
file_type: file_type:
binary: Binary binary: Binary
editor_mode: Editor Mode editor_mode: Editor Mode
@ -91,9 +90,10 @@ en:
files: Files files: Files
score: Score score: Score
user: Author user: Author
team: file_template:
internal_user_ids: Members name: "Name"
name: Name file_type: "File Type"
content: "Content"
models: models:
code_harbor_link: code_harbor_link:
one: CodeHarbor Link one: CodeHarbor Link
@ -116,6 +116,9 @@ en:
file: file:
one: File one: File
other: Files other: Files
file_template:
one: File Template
other: File Templates
file_type: file_type:
one: File Type one: File Type
other: File Types other: File Types
@ -128,9 +131,6 @@ en:
submission: submission:
one: Submission one: Submission
other: Submissions other: Submissions
team:
one: Team
other: Teams
errors: errors:
messages: messages:
together: 'has to be set along with %{attribute}' together: 'has to be set along with %{attribute}'
@ -402,6 +402,7 @@ en:
created_at: Created At created_at: Created At
destroy: Delete destroy: Delete
edit: Edit edit: Edit
actions_button: 'Other actions'
errors_one: 'An error prohibited this %{model} from being saved' errors_one: 'An error prohibited this %{model} from being saved'
errors_other: '%{count} errors prohibited this %{model} from being saved' errors_other: '%{count} errors prohibited this %{model} from being saved'
help: help:
@ -455,3 +456,5 @@ en:
next_label: 'Next Page &#8594;' next_label: 'Next Page &#8594;'
page_gap: '&hellip;' page_gap: '&hellip;'
previous_label: '&#8592; Previous Page' previous_label: '&#8592; Previous Page'
file_template:
no_template_label: "Empty File"

View File

@ -1,6 +1,11 @@
FILENAME_REGEXP = /[\w\.]+/ unless Kernel.const_defined?(:FILENAME_REGEXP) FILENAME_REGEXP = /[\w\.]+/ unless Kernel.const_defined?(:FILENAME_REGEXP)
Rails.application.routes.draw do Rails.application.routes.draw do
resources :file_templates do
collection do
get 'by_file_type/:file_type_id', as: :by_file_type, to: :by_file_type
end
end
resources :code_harbor_links resources :code_harbor_links
resources :request_for_comments do resources :request_for_comments do
member do member do
@ -89,7 +94,8 @@ Rails.application.routes.draw do
resources :submissions, only: [:create, :index, :show] do resources :submissions, only: [:create, :index, :show] do
member do member do
get 'download/:filename', as: :download, constraints: {filename: FILENAME_REGEXP}, to: :download_file get 'download', as: :download, to: :download
get 'download/:filename', as: :download_file, constraints: {filename: FILENAME_REGEXP}, to: :download_file
get 'render/:filename', as: :render, constraints: {filename: FILENAME_REGEXP}, to: :render_file get 'render/:filename', as: :render, constraints: {filename: FILENAME_REGEXP}, to: :render_file
get 'run/:filename', as: :run, constraints: {filename: FILENAME_REGEXP}, to: :run get 'run/:filename', as: :run, constraints: {filename: FILENAME_REGEXP}, to: :run
get :score get :score
@ -99,5 +105,4 @@ Rails.application.routes.draw do
end end
end end
resources :teams
end end

View File

@ -0,0 +1,10 @@
class CreateFileTemplates < ActiveRecord::Migration
def change
create_table :file_templates do |t|
t.string :name
t.text :content
t.belongs_to :file_type
t.timestamps
end
end
end

View File

@ -0,0 +1,5 @@
class AddFileTemplateToFile < ActiveRecord::Migration
def change
add_reference :files, :file_template
end
end

View File

@ -0,0 +1,5 @@
class AddSubmissionToRequestForComments < ActiveRecord::Migration
def change
add_reference :request_for_comments, :submission
end
end

View File

@ -0,0 +1,5 @@
class RemoveRequestedAtFromRequestForComments < ActiveRecord::Migration
def change
remove_column :request_for_comments, :requested_at
end
end

View File

@ -0,0 +1,7 @@
class RemoveTeams < ActiveRecord::Migration
def change
remove_reference :exercises, :team
drop_table :teams
drop_table :internal_users_teams
end
end

View File

@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160624130951) do ActiveRecord::Schema.define(version: 20160704143402) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -87,7 +87,6 @@ ActiveRecord::Schema.define(version: 20160624130951) do
t.boolean "public" t.boolean "public"
t.string "user_type" t.string "user_type"
t.string "token" t.string "token"
t.integer "team_id"
t.boolean "hide_file_tree" t.boolean "hide_file_tree"
t.boolean "allow_file_creation" t.boolean "allow_file_creation"
end end
@ -101,6 +100,14 @@ ActiveRecord::Schema.define(version: 20160624130951) do
t.datetime "updated_at" t.datetime "updated_at"
end end
create_table "file_templates", force: true do |t|
t.string "name"
t.text "content"
t.integer "file_type_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "file_types", force: true do |t| create_table "file_types", force: true do |t|
t.string "editor_mode" t.string "editor_mode"
t.string "file_extension" t.string "file_extension"
@ -132,6 +139,7 @@ ActiveRecord::Schema.define(version: 20160624130951) do
t.string "feedback_message" t.string "feedback_message"
t.float "weight" t.float "weight"
t.string "path" t.string "path"
t.integer "file_template_id"
end end
add_index "files", ["context_id", "context_type"], name: "index_files_on_context_id_and_context_type", using: :btree add_index "files", ["context_id", "context_type"], name: "index_files_on_context_id_and_context_type", using: :btree
@ -173,24 +181,16 @@ ActiveRecord::Schema.define(version: 20160624130951) do
add_index "internal_users", ["remember_me_token"], name: "index_internal_users_on_remember_me_token", using: :btree add_index "internal_users", ["remember_me_token"], name: "index_internal_users_on_remember_me_token", using: :btree
add_index "internal_users", ["reset_password_token"], name: "index_internal_users_on_reset_password_token", using: :btree add_index "internal_users", ["reset_password_token"], name: "index_internal_users_on_reset_password_token", using: :btree
create_table "internal_users_teams", force: true do |t|
t.integer "internal_user_id"
t.integer "team_id"
end
add_index "internal_users_teams", ["internal_user_id"], name: "index_internal_users_teams_on_internal_user_id", using: :btree
add_index "internal_users_teams", ["team_id"], name: "index_internal_users_teams_on_team_id", using: :btree
create_table "request_for_comments", force: true do |t| create_table "request_for_comments", force: true do |t|
t.integer "user_id", null: false t.integer "user_id", null: false
t.integer "exercise_id", null: false t.integer "exercise_id", null: false
t.integer "file_id", null: false t.integer "file_id", null: false
t.datetime "requested_at"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "user_type" t.string "user_type"
t.text "question" t.text "question"
t.boolean "solved" t.boolean "solved"
t.integer "submission_id"
end end
create_table "submissions", force: true do |t| create_table "submissions", force: true do |t|
@ -203,12 +203,6 @@ ActiveRecord::Schema.define(version: 20160624130951) do
t.string "user_type" t.string "user_type"
end end
create_table "teams", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "testruns", force: true do |t| create_table "testruns", force: true do |t|
t.boolean "passed" t.boolean "passed"
t.text "output" t.text "output"

View File

@ -22,6 +22,3 @@ Hint.create_factories
# submissions # submissions
FactoryGirl.create(:submission, exercise: @exercises[:fibonacci]) FactoryGirl.create(:submission, exercise: @exercises[:fibonacci])
# teams
FactoryGirl.create(:team, internal_users: InternalUser.limit(10))

View File

@ -200,7 +200,7 @@ class DockerClient
execute_command(command, nil, block) execute_command(command, nil, block)
end end
#only used by server sent events (deprecated?) #only used by score
def execute_command(command, before_execution_block, output_consuming_block) def execute_command(command, before_execution_block, output_consuming_block)
#tries ||= 0 #tries ||= 0
@container = DockerContainerPool.get_container(@execution_environment) @container = DockerContainerPool.get_container(@execution_environment)

View File

@ -1,93 +0,0 @@
require 'rails_helper'
describe TeamsController do
let(:team) { FactoryGirl.create(:team) }
let(:user) { FactoryGirl.create(:admin) }
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
describe 'POST #create' do
context 'with a valid team' do
let(:request) { proc { post :create, team: FactoryGirl.attributes_for(:team) } }
before(:each) { request.call }
expect_assigns(team: Team)
it 'creates the team' do
expect { request.call }.to change(Team, :count).by(1)
end
expect_redirect(Team.last)
end
context 'with an invalid team' do
before(:each) { post :create, team: {} }
expect_assigns(team: Team)
expect_status(200)
expect_template(:new)
end
end
describe 'DELETE #destroy' do
before(:each) { delete :destroy, id: team.id }
expect_assigns(team: Team)
it 'destroys the team' do
team = FactoryGirl.create(:team)
expect { delete :destroy, id: team.id }.to change(Team, :count).by(-1)
end
expect_redirect(:teams)
end
describe 'GET #edit' do
before(:each) { get :edit, id: team.id }
expect_assigns(team: Team)
expect_status(200)
expect_template(:edit)
end
describe 'GET #index' do
before(:all) { FactoryGirl.create_pair(:team) }
before(:each) { get :index }
expect_assigns(teams: Team.all)
expect_status(200)
expect_template(:index)
end
describe 'GET #new' do
before(:each) { get :new }
expect_assigns(team: Team)
expect_status(200)
expect_template(:new)
end
describe 'GET #show' do
before(:each) { get :show, id: team.id }
expect_assigns(team: :team)
expect_status(200)
expect_template(:show)
end
describe 'PUT #update' do
context 'with a valid team' do
before(:each) { put :update, team: FactoryGirl.attributes_for(:team), id: team.id }
expect_assigns(team: Team)
expect_redirect(:team)
end
context 'with an invalid team' do
before(:each) { put :update, team: {name: ''}, id: team.id }
expect_assigns(team: Team)
expect_status(200)
expect_template(:edit)
end
end
end

View File

@ -1,6 +0,0 @@
FactoryGirl.define do
factory :team do
internal_users { build_pair :teacher }
name 'The A-Team'
end
end

View File

@ -5,7 +5,7 @@ describe 'Authorization' do
let(:user) { FactoryGirl.create(:admin) } let(:user) { FactoryGirl.create(:admin) }
before(:each) { allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) } before(:each) { allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) }
[Consumer, ExecutionEnvironment, Exercise, FileType, InternalUser, Team].each do |model| [Consumer, ExecutionEnvironment, Exercise, FileType, InternalUser].each do |model|
expect_permitted_path(:"new_#{model.model_name.singular}_path") expect_permitted_path(:"new_#{model.model_name.singular}_path")
end end
end end
@ -14,7 +14,7 @@ describe 'Authorization' do
let(:user) { FactoryGirl.create(:external_user) } let(:user) { FactoryGirl.create(:external_user) }
before(:each) { allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) } before(:each) { allow_any_instance_of(ApplicationController).to receive(:current_user).and_return(user) }
[Consumer, ExecutionEnvironment, Exercise, FileType, InternalUser, Team].each do |model| [Consumer, ExecutionEnvironment, Exercise, FileType, InternalUser].each do |model|
expect_forbidden_path(:"new_#{model.model_name.singular}_path") expect_forbidden_path(:"new_#{model.model_name.singular}_path")
end end
end end
@ -27,7 +27,7 @@ describe 'Authorization' do
expect_forbidden_path(:"new_#{model.model_name.singular}_path") expect_forbidden_path(:"new_#{model.model_name.singular}_path")
end end
[ExecutionEnvironment, Exercise, FileType, Team].each do |model| [ExecutionEnvironment, Exercise, FileType].each do |model|
expect_permitted_path(:"new_#{model.model_name.singular}_path") expect_permitted_path(:"new_#{model.model_name.singular}_path")
end end
end end

View File

@ -1,9 +0,0 @@
require 'rails_helper'
describe Team do
let(:team) { described_class.create }
it 'validates the presence of a name' do
expect(team.errors[:name]).to be_present
end
end

View File

@ -3,8 +3,8 @@ require 'rails_helper'
describe ExercisePolicy do describe ExercisePolicy do
subject { described_class } subject { described_class }
let(:exercise) { FactoryGirl.build(:dummy, team: FactoryGirl.create(:team)) } let(:exercise) { FactoryGirl.build(:dummy) }
permissions :batch_update? do permissions :batch_update? do
it 'grants access to admins only' do it 'grants access to admins only' do
expect(subject).to permit(FactoryGirl.build(:admin), exercise) expect(subject).to permit(FactoryGirl.build(:admin), exercise)
@ -40,10 +40,6 @@ describe ExercisePolicy do
expect(subject).to permit(exercise.author, exercise) expect(subject).to permit(exercise.author, exercise)
end end
it 'grants access to team members' do
expect(subject).to permit(exercise.team.members.first, exercise)
end
it 'does not grant access to all other users' do it 'does not grant access to all other users' do
[:external_user, :teacher].each do |factory_name| [:external_user, :teacher].each do |factory_name|
expect(subject).not_to permit(FactoryGirl.build(factory_name), exercise) expect(subject).not_to permit(FactoryGirl.build(factory_name), exercise)
@ -71,9 +67,7 @@ describe ExercisePolicy do
[@admin, @teacher].each do |user| [@admin, @teacher].each do |user|
[true, false].each do |public| [true, false].each do |public|
[@team, nil].each do |team| FactoryGirl.create(:dummy, public: public, user_id: user.id, user_type: InternalUser.class.name)
FactoryGirl.create(:dummy, public: public, team: team, user_id: user.id, user_type: InternalUser.class.name)
end
end end
end end
end end
@ -95,10 +89,6 @@ describe ExercisePolicy do
end end
context 'for teachers' do context 'for teachers' do
before(:each) do
@team = FactoryGirl.create(:team)
@team.members << @teacher
end
let(:scope) { Pundit.policy_scope!(@teacher, Exercise) } let(:scope) { Pundit.policy_scope!(@teacher, Exercise) }
@ -110,12 +100,8 @@ describe ExercisePolicy do
expect(scope.map(&:id)).to include(*Exercise.where(public: false, user_id: @teacher.id).map(&:id)) expect(scope.map(&:id)).to include(*Exercise.where(public: false, user_id: @teacher.id).map(&:id))
end end
it "includes all of team members' non-public exercises" do
expect(scope.map(&:id)).to include(*Exercise.where(public: false, team_id: @teacher.teams.first.id).map(&:id))
end
it "does not include other authors' non-public exercises" do it "does not include other authors' non-public exercises" do
expect(scope.map(&:id)).not_to include(*Exercise.where(public: false).where("team_id <> #{@team.id} AND user_id <> #{@teacher.id}").map(&:id)) expect(scope.map(&:id)).not_to include(*Exercise.where(public: false).where(user_id <> #{@teacher.id}").map(&:id))
end end
end end
end end

View File

@ -1,41 +0,0 @@
require 'rails_helper'
describe TeamPolicy do
subject { described_class }
let(:team) { FactoryGirl.build(:team) }
[:create?, :index?, :new?].each do |action|
permissions(action) do
it 'grants access to admins' do
expect(subject).to permit(FactoryGirl.build(:admin), team)
end
it 'grants access to teachers' do
expect(subject).to permit(FactoryGirl.build(:teacher), team)
end
it 'does not grant access to external users' do
expect(subject).not_to permit(FactoryGirl.build(:external_user), team)
end
end
end
[:destroy?, :edit?, :show?, :update?].each do |action|
permissions(action) do
it 'grants access to admins' do
expect(subject).to permit(FactoryGirl.build(:admin), team)
end
it 'grants access to members' do
expect(subject).to permit(team.members.last, team)
end
it 'does not grant access to all other users' do
[:external_user, :teacher].each do |factory_name|
expect(subject).not_to permit(FactoryGirl.build(factory_name), team)
end
end
end
end
end