merge with master

This commit is contained in:
yqbk
2016-08-03 12:09:58 +02:00
77 changed files with 770 additions and 515 deletions

View File

@ -38,6 +38,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
@ -47,6 +48,7 @@ group :development do
gem 'capistrano-rails' gem 'capistrano-rails'
gem 'capistrano-rvm' gem 'capistrano-rvm'
gem 'capistrano-upload-config' gem 'capistrano-upload-config'
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 gem 'web-console', '~> 2.0', platform: :ruby

View File

@ -198,6 +198,8 @@ GEM
pundit (1.1.0) pundit (1.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
rack (1.5.5) rack (1.5.5)
rack-mini-profiler (0.10.1)
rack (>= 1.2.0)
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (4.1.14.1) rails (4.1.14.1)
@ -387,6 +389,7 @@ DEPENDENCIES
pry-byebug pry-byebug
puma (~> 2.15.3) puma (~> 2.15.3)
pundit pundit
rack-mini-profiler
rails (~> 4.1.13) rails (~> 4.1.13)
rails-i18n (~> 4.0.0) rails-i18n (~> 4.0.0)
rake rake
@ -397,6 +400,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

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);
@ -704,9 +737,15 @@ $(function() {
}; };
var renderScore = function() { var renderScore = function() {
var score = $('#score').data('score'); var score = parseFloat($('#score').data('score'));
var maxium_score = $('#score').data('maximum-score'); var maxium_score = parseFloat($('#score').data('maximum-score'));
$('.score').html((score || '?') + ' / ' + maxium_score); if (score >= 0 && score <= maxium_score && maxium_score >0 ) {
var percentage_score = (score / maxium_score * 100 ).toFixed(0);
$('.score').html(percentage_score + '%');
}
else {
$('.score').html( 0 + '%');
}
renderProgressBar(score, maxium_score); renderProgressBar(score, maxium_score);
}; };
@ -737,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); });
}); });
} }
}; };
@ -772,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);
}); });
}; };
@ -870,7 +909,9 @@ $(function() {
} }
var showWorkspaceTab = function(event) { var showWorkspaceTab = function(event) {
event.preventDefault(); if(event){
event.preventDefault();
}
showTab(0); showTab(0);
}; };
@ -949,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);
}); });
} }
}; };
@ -968,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'); }
}; };
@ -1029,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.
@ -1041,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);
@ -1154,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');
@ -1201,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

@ -9,6 +9,9 @@ $(function() {
submissionsScoreAndTimeAssess = [[0,0]]; submissionsScoreAndTimeAssess = [[0,0]];
submissionsScoreAndTimeSubmits = [[0,0]]; submissionsScoreAndTimeSubmits = [[0,0]];
submissionsScoreAndTimeRuns = [];
submissionsSaves = [];
submissionsAutosaves = [];
var maximumValue = 0; var maximumValue = 0;
var wtimes = $('#wtimes').data('working_times'); //.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until) var wtimes = $('#wtimes').data('working_times'); //.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until)
@ -18,43 +21,42 @@ $(function() {
for (var i = 0;i<submissions_length;i++){ for (var i = 0;i<submissions_length;i++){
var submission = submissions[i]; var submission = submissions[i];
var workingTime;
var submissionArray;
workingTime = get_minutes(wtimes[i]);
submissionArray = [submission.score, 0];
if (workingTime > 0) {
submissionArray[1] = workingTime;
}
if(submission.score>maximumValue){
maximumValue = submission.score;
}
if(submission.cause == "assess"){ if(submission.cause == "assess"){
var workingTime = get_minutes(wtimes[i]);
var submissionArray = [submission.score, 0];
if (workingTime > 0) {
submissionArray[1] = workingTime;
}
if(submission.score>maximumValue){
maximumValue = submission.score;
}
submissionsScoreAndTimeAssess.push(submissionArray); submissionsScoreAndTimeAssess.push(submissionArray);
} else if(submission.cause == "submit"){ } else if(submission.cause == "submit"){
var workingTime = get_minutes(wtimes[i]);
var submissionArray = [submission.score, 0];
if (workingTime > 0) {
submissionArray[1] = workingTime;
}
if(submission.score>maximumValue){
maximumValue = submission.score;
}
submissionsScoreAndTimeSubmits.push(submissionArray); submissionsScoreAndTimeSubmits.push(submissionArray);
} else if(submission.cause == "run"){
submissionsScoreAndTimeRuns.push(submissionArray[1]);
} else if(submission.cause == "autosave"){
submissionsAutosaves.push(submissionArray[1]);
} else if(submission.cause == "save"){
submissionsSaves.push(submissionArray[1]);
} }
} }
// console.log(submissionsScoreAndTimeAssess.length); // console.log(submissionsScoreAndTimeAssess.length);
// console.log(submissionsScoreAndTimeSubmits); // console.log(submissionsScoreAndTimeSubmits);
// console.log(submissionsScoreAndTimeRuns);
function get_minutes (time_stamp) { function get_minutes (time_stamp) {
try { try {
hours = time_stamp.split(":")[0]; hours = time_stamp.split(":")[0];
minutes = time_stamp.split(":")[1]; minutes = time_stamp.split(":")[1];
seconds = time_stamp.split(":")[2]; seconds = time_stamp.split(":")[2];
seconds /= 60;
minutes = parseFloat(hours * 60) + parseInt(minutes); minutes = parseFloat(hours * 60) + parseInt(minutes) + seconds;
if (minutes > 0){ if (minutes > 0){
return minutes; return minutes;
} else{ } else{
@ -82,6 +84,9 @@ $(function() {
function graph_assesses() { function graph_assesses() {
// MAKE THE GRAPH // MAKE THE GRAPH
var width_ratio = .8; var width_ratio = .8;
if (getWidth()*width_ratio > 1000){
width_ratio = 1000/getWidth();
}
var height_ratio = .7; // percent of height var height_ratio = .7; // percent of height
var margin = {top: 100, right: 20, bottom: 70, left: 70},//30,50 var margin = {top: 100, right: 20, bottom: 70, left: 70},//30,50
@ -131,15 +136,32 @@ $(function() {
.append("g") .append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var largestSubmittedTimeStamp = submissions[submissions_length-1];
var largestArrayForRange;
x.domain(d3.extent(submissionsScoreAndTimeAssess, function (d) { if(largestSubmittedTimeStamp.cause == "assess"){
// console.log(d[1]); largestArrayForRange = submissionsScoreAndTimeAssess;
return (d[1]); x.domain([0,largestArrayForRange[largestArrayForRange.length - 1][1]]).clamp(true);
})); } else if(largestSubmittedTimeStamp.cause == "submit"){
y.domain(d3.extent(submissionsScoreAndTimeAssess, function (d) { largestArrayForRange = submissionsScoreAndTimeSubmits;
x.domain([0,largestArrayForRange[largestArrayForRange.length - 1][1]]).clamp(true);
} else if(largestSubmittedTimeStamp.cause == "run"){
largestArrayForRange = submissionsScoreAndTimeRuns;
x.domain([0,largestArrayForRange[largestArrayForRange.length - 1]]).clamp(true);
} else if(largestSubmittedTimeStamp.cause == "autosave"){
largestArrayForRange = submissionsAutosaves;
x.domain([0,largestArrayForRange[largestArrayForRange.length - 1]]).clamp(true);
} else if(largestSubmittedTimeStamp.cause == "save"){
largestArrayForRange = submissionsSaves;
x.domain([0,largestArrayForRange[largestArrayForRange.length - 1]]).clamp(true);
}
// take maximum value between assesses and submits
var yDomain = submissionsScoreAndTimeAssess.concat(submissionsScoreAndTimeSubmits);
y.domain(d3.extent(yDomain, function (d) {
// console.log(d[0]); // console.log(d[0]);
return (d[0]); return (d[0]);
})); }));
// y.domain([0,2]).clamp(true);
svg.append("g") //x axis svg.append("g") //x axis
.attr("class", "x axis") .attr("class", "x axis")
@ -177,15 +199,15 @@ $(function() {
.style('font-size', 20) .style('font-size', 20)
.style('text-decoration', 'underline'); .style('text-decoration', 'underline');
//
// svg.append("path") svg.append("path")
// //.datum() //.datum()
// .attr("class", "line") .attr("class", "line")
// .attr('id', 'myPath')// new .attr('id', 'myPath')// new
// .attr("stroke", "black") .attr("stroke", "black")
// .attr("stroke-width", 5) .attr("stroke-width", 5)
// .attr("fill", "none")// end new .attr("fill", "none")// end new
// .attr("d", line(submissionsScoreAndTimeAssess));//--- .attr("d", line(submissionsScoreAndTimeAssess));//---
svg.append("path") svg.append("path")
.datum(submissionsScoreAndTimeAssess) .datum(submissionsScoreAndTimeAssess)
@ -204,27 +226,35 @@ $(function() {
.attr("cx", function(d) { return x(d[1]); }) .attr("cx", function(d) { return x(d[1]); })
.attr("cy", function(d) { return y(d[0]); }); .attr("cy", function(d) { return y(d[0]); });
if (submissionsScoreAndTimeSubmits.length > 0){
svg.append("path") // get rid of the 0 element at the beginning
.datum(submissionsScoreAndTimeSubmits) submissionsScoreAndTimeSubmits.shift();
.attr("class", "line2") }
.attr('id', 'myPath')// new
.attr("stroke", "blue")
.attr("stroke-width", 5)
.attr("fill", "none")// end new
.attr("d", line);//---
svg.selectAll("dot") // Add dots to submits svg.selectAll("dot") // Add dots to submits
.data(submissionsScoreAndTimeSubmits) .data(submissionsScoreAndTimeSubmits)
.enter().append("circle") .enter().append("circle")
.attr("r", 3.5) .attr("r", 6)
.attr("stroke", "black")
.attr("fill", "blue")
.attr("cx", function(d) { return x(d[1]); }) .attr("cx", function(d) { return x(d[1]); })
.attr("cy", function(d) { return y(d[0]); }); .attr("cy", function(d) { return y(d[0]); });
for (var i = 0; i < submissionsScoreAndTimeRuns.length; i++) {
svg.append("line")
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("fill", "none")// end new
.attr("y1", y(0))
.attr("y2", 0)
.attr("x1", x(submissionsScoreAndTimeRuns[i]))
.attr("x2", x(submissionsScoreAndTimeRuns[i]));
}
var color_hash = { 0 : ["Submissions", "blue"], var color_hash = { 0 : ["Submissions", "blue"],
1 : ["Assesses", "orange"] 1 : ["Assesses", "orange"],
} 2 : ["Runs", "red"]
};
// add legend // add legend
var legend = svg.append("g") var legend = svg.append("g")
@ -234,7 +264,8 @@ $(function() {
.attr("height", 100) .attr("height", 100)
.attr("width", 100); .attr("width", 100);
var dataset = [submissionsScoreAndTimeSubmits,submissionsScoreAndTimeAssess]; var dataset = [submissionsScoreAndTimeSubmits,submissionsScoreAndTimeAssess, submissionsScoreAndTimeRuns];
var yOffset = -70;
legend.selectAll('g').data(dataset) legend.selectAll('g').data(dataset)
.enter() .enter()
@ -243,14 +274,14 @@ $(function() {
var g = d3.select(this); var g = d3.select(this);
g.append("rect") g.append("rect")
.attr("x", 20) .attr("x", 20)
.attr("y", i*25 + 8) .attr("y", i*25 + yOffset)// + 8
.attr("width", 10) .attr("width", 10)
.attr("height", 10) .attr("height", 10)
.style("fill", color_hash[String(i)][1]); .style("fill", color_hash[String(i)][1]);
g.append("text") g.append("text")
.attr("x", 40) .attr("x", 40)
.attr("y", i * 25 + 18) .attr("y", i * 25 + yOffset + 10)// + 18
.attr("height",30) .attr("height",30)
.attr("width",100) .attr("width",100)
.style("fill", color_hash[String(i)][1]) .style("fill", color_hash[String(i)][1])
@ -273,7 +304,7 @@ $(function() {
try{ try{
graph_assesses(); graph_assesses();
} catch(err){ } catch(err){
// not enough data alert("could not draw the graph");
} }
} }

View File

@ -12,6 +12,9 @@ $(function() {
$('#files li:last select[name*="file_type_id"]').val(getSelectedExecutionEnvironment().file_type_id); $('#files li:last select[name*="file_type_id"]').val(getSelectedExecutionEnvironment().file_type_id);
$('#files li:last select').chosen(window.CodeOcean.CHOSEN_OPTIONS); $('#files li:last select').chosen(window.CodeOcean.CHOSEN_OPTIONS);
$('body, html').scrollTo('#add-file'); $('body, html').scrollTo('#add-file');
// if we collapse the file forms by default, we need to click on the new element in order to open it.
// however, this crashes for more files (if we add several ones by clicking the add button more often), since the elements are probably not correctly added to the files list.
//$('#files li:last>div:first>a>div').click();
}; };
var ajaxError = function() { var ajaxError = function() {
@ -148,6 +151,22 @@ $(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();
@ -162,6 +181,10 @@ $(function() {
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

@ -41,8 +41,7 @@ Turtle.prototype.update = function () {
var i, k, canvas, ctx, dx, dy, item, c, length; var i, k, canvas, ctx, dx, dy, item, c, length;
canvas = this.canvas[0]; canvas = this.canvas[0];
ctx = canvas.getContext('2d'); ctx = canvas.getContext('2d');
ctx.fillStyle = '#fff'; ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
length = this.items.length; length = this.items.length;
dx = canvas.width / 2; dx = canvas.width / 2;
dy = canvas.height / 2; dy = canvas.height / 2;
@ -56,8 +55,8 @@ Turtle.prototype.update = function () {
for (k = 2; k < c.length; k += 2) { for (k = 2; k < c.length; k += 2) {
ctx.lineTo(c[k] + dx, c[k + 1] + dy); ctx.lineTo(c[k] + dx, c[k + 1] + dy);
} }
if (this.fill) { if (item.fill) {
ctx.strokeStyle = this.fill; ctx.strokeStyle = item.fill;
} }
ctx.stroke(); ctx.stroke();

View File

@ -4,7 +4,7 @@ $(function() {
if ($.isController('exercises') && $('.graph-functions').isPresent()) { if ($.isController('exercises') && $('.graph-functions').isPresent()) {
var working_times = $('#data').data('working-time'); var working_times = $('#data').data('working-time');
function get_minutes (time_stamp){ function get_minutes (time_stamp){
try{ try{
hours = time_stamp.split(":")[0]; hours = time_stamp.split(":")[0];
@ -67,6 +67,9 @@ $(function() {
// DRAW THE LINE GRAPH ------------------------------------------------------------------------------ // DRAW THE LINE GRAPH ------------------------------------------------------------------------------
function draw_line_graph() { function draw_line_graph() {
var width_ratio = .8; var width_ratio = .8;
if (getWidth()*width_ratio > 1000){
width_ratio = 1000/getWidth();
}
var height_ratio = .7; // percent of height var height_ratio = .7; // percent of height
// currently sets as percentage of window width, however, unfortunately // currently sets as percentage of window width, however, unfortunately

View File

@ -49,7 +49,10 @@ div#chart_2 {
} }
a.file-heading {
color: black !important;
text-decoration: none;
}
.bar { .bar {
fill: orange; fill: orange;

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

@ -46,7 +46,11 @@ class CommentsController < ApplicationController
# POST /comments # POST /comments
# POST /comments.json # POST /comments.json
def create def create
@comment = Comment.new(comment_params) @comment = Comment.new(comment_params_without_request_id)
if comment_params[:request_id]
UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user)
end
respond_to do |format| respond_to do |format|
if @comment.save if @comment.save
@ -64,7 +68,7 @@ class CommentsController < ApplicationController
# PATCH/PUT /comments/1.json # PATCH/PUT /comments/1.json
def update def update
respond_to do |format| respond_to do |format|
if @comment.update(comment_params) if @comment.update(comment_params_without_request_id)
format.html { head :no_content, notice: 'Comment was successfully updated.' } format.html { head :no_content, notice: 'Comment was successfully updated.' }
format.json { render :show, status: :ok, location: @comment } format.json { render :show, status: :ok, location: @comment }
else else
@ -101,10 +105,14 @@ class CommentsController < ApplicationController
@comment = Comment.find(params[:id]) @comment = Comment.find(params[:id])
end end
def comment_params_without_request_id
comment_params.except :request_id
end
# 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 comment_params def comment_params
#params.require(:comment).permit(:user_id, :file_id, :row, :column, :text) #params.require(:comment).permit(:user_id, :file_id, :row, :column, :text)
# fuer production mode, damit böse menschen keine falsche user_id uebergeben: # fuer production mode, damit böse menschen keine falsche user_id uebergeben:
params.require(:comment).permit(:file_id, :row, :column, :text).merge(user_id: current_user.id, user_type: current_user.class.name) params.require(:comment).permit(:file_id, :row, :column, :text, :request_id).merge(user_id: current_user.id, user_type: current_user.class.name)
end end
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, :run, :statistics, :submit, :reload] before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :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]
@ -119,7 +118,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
@ -195,11 +194,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

@ -1,5 +1,5 @@
class RequestForCommentsController < ApplicationController class RequestForCommentsController < ApplicationController
before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy] before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy, :mark_as_solved]
skip_after_action :verify_authorized skip_after_action :verify_authorized
@ -20,6 +20,18 @@ class RequestForCommentsController < ApplicationController
render 'index' render 'index'
end end
def mark_as_solved
authorize!
@request_for_comment.solved = true
respond_to do |format|
if @request_for_comment.save
format.json { render :show, status: :ok, location: @request_for_comment }
else
format.json { render json: @request_for_comment.errors, status: :unprocessable_entity }
end
end
end
# GET /request_for_comments/1 # GET /request_for_comments/1
# GET /request_for_comments/1.json # GET /request_for_comments/1.json
def show def show
@ -70,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).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)
@ -174,8 +188,14 @@ class SubmissionsController < ApplicationController
def parse_message(message, output_stream, socket, recursive = true) def parse_message(message, output_stream, socket, recursive = true)
begin begin
parsed = JSON.parse(message) parsed = JSON.parse(message)
socket.send_data message if(parsed.class == Hash && parsed.key?('cmd'))
Rails.logger.info('parse_message sent: ' + message) socket.send_data message
Rails.logger.info('parse_message sent: ' + message)
else
parsed = {'cmd'=>'write','stream'=>output_stream,'data'=>message}
socket.send_data JSON.dump(parsed)
Rails.logger.info('parse_message sent: ' + JSON.dump(parsed))
end
rescue JSON::ParserError => e rescue JSON::ParserError => e
# Check wether the message contains multiple lines, if true try to parse each line # Check wether the message contains multiple lines, if true try to parse each line
if ((recursive == true) && (message.include? "\n")) if ((recursive == true) && (message.include? "\n"))
@ -208,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
@ -260,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,4 +11,13 @@ class UserMailer < ActionMailer::Base
@reset_password_url = reset_password_internal_user_url(user, token: user.reset_password_token) @reset_password_url = reset_password_internal_user_url(user, token: user.reset_password_token)
mail(subject: t('mailers.user_mailer.reset_password.subject'), to: user.email) mail(subject: t('mailers.user_mailer.reset_password.subject'), to: user.email)
end end
def got_new_comment(comment, request_for_comment, commenting_user)
# todo: check whether we can take the last known locale of the receiver?
@receiver_displayname = request_for_comment.user.displayname
@commenting_user_displayname = commenting_user.displayname
@comment_text = comment.text
@rfc_link = request_for_comment_url(request_for_comment)
mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email).deliver
end
end end

View File

@ -35,6 +35,7 @@ module CodeOcean
has_many :files has_many :files
has_many :testruns has_many :testruns
has_many :comments
alias_method :descendants, :files alias_method :descendants, :files
mount_uploader :native_file, FileUploader mount_uploader :native_file, FileUploader

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

@ -7,7 +7,9 @@ class ExternalUser < ActiveRecord::Base
def displayname def displayname
result = name result = name
if(consumer.name == 'openHPI') if(consumer.name == 'openHPI')
result = Xikolo::UserClient.get(external_id.to_s)[:display_name] result = Rails.cache.fetch("#{cache_key}/displayname", expires_in: 12.hours) do
Xikolo::UserClient.get(external_id.to_s)[:display_name]
end
end end
result result
end end

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,7 +1,8 @@
class RequestForComment < ActiveRecord::Base class RequestForComment < ActiveRecord::Base
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'
belongs_to :user, polymorphic: true
before_create :set_requested_timestamp before_create :set_requested_timestamp
@ -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,12 +24,27 @@ 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
submission.files.map { |file| file.comments.size}.sum
end
def to_s def to_s
"RFC-" + self.id.to_s "RFC-" + self.id.to_s
end end
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, 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

@ -1,5 +1,8 @@
class RequestForCommentPolicy < ApplicationPolicy class RequestForCommentPolicy < ApplicationPolicy
def author?
@user == @record.author
end
private :author?
def create? def create?
everyone everyone
@ -13,6 +16,10 @@ class RequestForCommentPolicy < ApplicationPolicy
define_method(action) { admin? } define_method(action) { admin? }
end end
def mark_as_solved?
admin? || author?
end
def edit? def edit?
admin? admin?
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? || teacher? }
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, Submission].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

@ -38,4 +38,4 @@
= t('exercises.editor.test') = t('exercises.editor.test')
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s')) = render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s'))
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')

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

@ -1,10 +1,10 @@
- id = f.object.id - id = f.object.id
li.panel.panel-default li.panel.panel-default
.panel-heading role="tab" id="heading" .panel-heading role="tab" id="heading"
div.clearfix role="button" a.file-heading data-toggle="collapse" data-parent="#files" href="#collapse#{id}"
span = f.object.name div.clearfix role="button"
a.pull-right data-toggle="collapse" data-parent="#files" href="#collapse#{id}" collapse span = f.object.name
.panel-collapse.collapse.in id="collapse#{id}" role="tabpanel" .panel-collapse.collapse-in id="collapse#{id}" role="tabpanel"
.panel-body .panel-body
.clearfix = link_to(t('shared.destroy'), '#', class:'btn btn-warning btn-sm discard-file pull-right') .clearfix = link_to(t('shared.destroy'), '#', class:'btn btn-warning btn-sm discard-file pull-right')
.form-group .form-group

View File

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

@ -78,6 +78,9 @@
br br
- if session[:lti_parameters].try(:has_key?, 'lis_outcome_service_url') - if session[:lti_parameters].try(:has_key?, 'lis_outcome_service_url')
p.text-center = render('editor_button', classes: 'btn-lg btn-success', data: {:'data-url' => submit_exercise_path(@exercise)}, icon: 'fa fa-send', id: 'submit', label: t('exercises.editor.submit')) p.text-center = render('editor_button', classes: 'btn-lg btn-success', data: {:'data-url' => submit_exercise_path(@exercise)}, icon: 'fa fa-send', id: 'submit', label: t('exercises.editor.submit'))
- else
p.text-center = render('editor_button', classes: 'btn-lg btn-warning-outline', data: {:'data-placement' => 'bottom', :'data-tooltip' => true} , icon: 'fa fa-clock-o', id: 'submit_outdated', label: t('exercises.editor.exercise_deadline_passed'), title: t('exercises.editor.tooltips.exercise_deadline_passed'))
- if qa_url - if qa_url
#questions-column #questions-column
#questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}" #questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}"

View File

@ -27,7 +27,7 @@ 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(policy(exercise.author).show?, exercise.author, exercise.author) 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

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?)
@ -23,13 +22,15 @@ h1
h2 = t('activerecord.attributes.exercise.files') h2 = t('activerecord.attributes.exercise.files')
ul.list-unstyled.panel-group#files ul.list-unstyled.panel-group#files
- @exercise.files.each do |file| - @exercise.files.order('name').each do |file|
li.panel.panel-default li.panel.panel-default
.panel-heading role="tab" id="heading" .panel-heading role="tab" id="heading"
div.clearfix role="button" a.file-heading data-toggle="collapse" data-parent="#files" href=".collapse#{file.id}"
span.panel-title = file.name_with_extension div.clearfix role="button"
a.pull-right data-toggle="collapse" data-parent="#files" href="#collapse#{file.id}" collapse span = file.name_with_extension
.panel-collapse.collapse.in id="collapse#{file.id}" role="tabpanel" // probably set an icon here that shows that the rows can be collapsed
//span.pull-right.collapse.in class="collapse#{file.id}" &#9788
.panel-collapse.collapse class="collapse#{file.id}" role="tabpanel"
.panel-body .panel-body
- if policy(file).destroy? - if policy(file).destroy?
.clearfix = link_to(t('shared.destroy'), file, class:'btn btn-warning btn-sm pull-right', data: {confirm: t('shared.confirm_destroy')}, method: :delete) .clearfix = link_to(t('shared.destroy'), file, class:'btn btn-warning btn-sm pull-right', data: {confirm: t('shared.confirm_destroy')}, method: :delete)

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

@ -4,19 +4,29 @@ h1 = RequestForComment.model_name.human(count: 2)
table.table.sortable table.table.sortable
thead thead
tr tr
th
i class="fa fa-lightbulb-o" aria-hidden="true" title = t('request_for_comments.solved') align="right"
th = t('activerecord.attributes.request_for_comments.exercise') th = t('activerecord.attributes.request_for_comments.exercise')
th = t('activerecord.attributes.request_for_comments.question') th = t('activerecord.attributes.request_for_comments.question')
th
i class="fa fa-comment" aria-hidden="true" title = t('request_for_comments.comments') align="center"
th = t('activerecord.attributes.request_for_comments.username') th = t('activerecord.attributes.request_for_comments.username')
th = t('activerecord.attributes.request_for_comments.requested_at') th = t('activerecord.attributes.request_for_comments.requested_at')
tbody tbody
- @request_for_comments.each do |request_for_comment| - @request_for_comments.each do |request_for_comment|
tr data-id=request_for_comment.id tr data-id=request_for_comment.id
- if request_for_comment.solved?
td
span class="fa fa-check" aria-hidden="true"
- else
td = ''
td = link_to(request_for_comment.exercise.title, request_for_comment) td = link_to(request_for_comment.exercise.title, request_for_comment)
- if request_for_comment.has_attribute?(:question) && request_for_comment.question - if request_for_comment.has_attribute?(:question) && request_for_comment.question
td = truncate(request_for_comment.question, length: 200) td = truncate(request_for_comment.question, length: 200)
- else - else
td = '-' td = '-'
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,46 +1,76 @@
<div class="list-group"> <div class="list-group">
<h4 class="list-group-item-heading"><%= 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 %> | <%= @request_for_comment.requested_at %> <%= user.displayname %> | <%= @request_for_comment.created_at.localtime %>
</p> </p>
<h5>
<u><%= t('activerecord.attributes.exercise.description') %>:</u> "<%= render_markdown(@request_for_comment.exercise.description) %>"
</h5>
<h5> <h5>
<% if @request_for_comment.question and not @request_for_comment.question == '' %> <% if @request_for_comment.question and not @request_for_comment.question == '' %>
<%= t('activerecord.attributes.request_for_comments.question')%>: "<%= @request_for_comment.question %>" <u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> "<%= @request_for_comment.question %>"
<% else %> <% else %>
<%= t('request_for_comments.no_question') %> <u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> <%= t('request_for_comments.no_question') %>
<% end %> <% end %>
</h5> </h5>
<% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %>
<button class="btn btn-default" id="mark-as-solved-button"><%= t('request_for_comments.mark_as_solved') %></button>
<% elsif (@request_for_comment.solved?) %>
<button type="button" class="btn btn-success"><%= t('request_for_comments.solved') %></button>
<% else %>
<% end %>
</div> </div>
<!-- <!--
do not put a carriage return in the line below. it will be present in the presentation of the source code, otherwise. do not put a carriage return in the line below. it will be present in the presentation of the source code, otherwise.
also, all settings from the rails model needed for the editor configuration in the JavaScript are attached to the editor as data attributes here.
--> -->
<% submission.files.each do |file| %> <% submission.files.each do |file| %>
<%= (file.path or "") + "/" + file.name + file.file_type.file_extension %> <%= (file.path or "") + "/" + file.name + file.file_type.file_extension %>
<div id='commentitor' class='editor' data-read-only='true' data-file-id='<%=file.id%>'><%= file.content %> <div id='commentitor' class='editor' data-read-only='true' data-file-id='<%=file.id%>' data-mode='<%=file.file_type.editor_mode%>'><%= file.content %>
</div> </div>
<% end %> <% end %>
<%= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') %> <%= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') %>
<script type="text/javascript"> <script type="text/javascript">
var solvedButton = $('#mark-as-solved-button');
solvedButton.on('click', function(event){
var jqrequest = $.ajax({
dataType: 'json',
method: 'GET',
url: location + '/mark_as_solved'
});
jqrequest.done(function(response){
if(response.solved){
solvedButton.hide();
}
});
});
// set file paths for ace
var ACE_FILES_PATH = '/assets/ace/';
_.each(['modePath', 'themePath', 'workerPath'], function(attribute) {
ace.config.set(attribute, ACE_FILES_PATH);
});
var commentitor = $('.editor'); var commentitor = $('.editor');
var userid = commentitor.data('user-id'); var userid = commentitor.data('user-id');
commentitor.each(function (index, editor) { commentitor.each(function (index, editor) {
var currentEditor = ace.edit(editor); var currentEditor = ace.edit(editor);
currentEditor.setReadOnly(true); currentEditor.setReadOnly(true);
// set editor mode (used for syntax highlighting
currentEditor.getSession().setMode($(editor).data('mode'));
setAnnotations(currentEditor, $(editor).data('file-id')); setAnnotations(currentEditor, $(editor).data('file-id'));
currentEditor.on("guttermousedown", handleSidebarClick); currentEditor.on("guttermousedown", handleSidebarClick);
@ -101,7 +131,8 @@ do not put a carriage return in the line below. it will be present in the presen
file_id: file_id, file_id: file_id,
row: row, row: row,
column: 0, column: 0,
text: commenttext text: commenttext,
request_id: $('h4#exercise_caption').data('rfc-id')
} }
}, },
dataType: 'json', dataType: 'json',

View File

@ -1 +1 @@
json.extract! @request_for_comment, :id, :user_id, :exercise_id, :file_id, :requested_at, :created_at, :updated_at, :user_type json.extract! @request_for_comment, :id, :user_id, :exercise_id, :file_id, :requested_at, :created_at, :updated_at, :user_type, :solved

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

@ -0,0 +1 @@
== t('mailers.user_mailer.got_new_comment.body', receiver_displayname: @receiver_displayname, link: link_to(@rfc_link, @rfc_link), commenting_user_displayname: @commenting_user_displayname, comment_text: @comment_text)

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
@ -78,6 +77,7 @@ de:
password_confirmation: Passwort-Bestätigung password_confirmation: Passwort-Bestätigung
role: Rolle role: Rolle
request_for_comments: request_for_comments:
comments: Kommentare
exercise: Aufgabe exercise: Aufgabe
execution_environment: Sprache execution_environment: Sprache
username: Benutzername username: Benutzername
@ -90,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
@ -115,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
@ -127,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'
@ -161,6 +162,7 @@ de:
show: show:
link: Konsument link: Konsument
errors: errors:
connection_refused: Verbindung abgelehnt
index: index:
count: Anzahl count: Anzahl
execution_environments: execution_environments:
@ -209,8 +211,10 @@ de:
submit: Code zur Bewertung abgeben submit: Code zur Bewertung abgeben
test: Testen test: Testen
timeout: 'Ausführung gestoppt. Ihr Code hat die erlaubte Ausführungszeit von %{permitted_execution_time} Sekunden überschritten.' timeout: 'Ausführung gestoppt. Ihr Code hat die erlaubte Ausführungszeit von %{permitted_execution_time} Sekunden überschritten.'
exercise_deadline_passed: 'Die Abgabefrist für diese Aufgabe ist bereits abgelaufen.'
tooltips: tooltips:
save: Ihr Code wird automatisch gespeichert, wann immer Sie eine Datei herunterladen, ausführen oder testen. Explizites Speichern ist also selten notwendig. save: Ihr Code wird automatisch gespeichert, wann immer Sie eine Datei herunterladen, ausführen oder testen. Explizites Speichern ist also selten notwendig.
exercise_deadline_passed: 'Die hier erzielten Punkte können nur bis zum Ablauf der Abgabefrist an die E-Learning-Plattform übertragen werden.'
request_for_comments_sent: "Kommentaranfrage gesendet." request_for_comments_sent: "Kommentaranfrage gesendet."
editor_file_tree: editor_file_tree:
file_root: Dateien file_root: Dateien
@ -323,14 +327,38 @@ de:
activation_needed: activation_needed:
body: 'Bitte besuchen Sie %{link} und wählen Sie ein Passwort, um Ihre Registrierung abzuschließen.' body: 'Bitte besuchen Sie %{link} und wählen Sie ein Passwort, um Ihre Registrierung abzuschließen.'
subject: Bitte schließen Sie Ihre Registrierung ab. subject: Bitte schließen Sie Ihre Registrierung ab.
got_new_comment:
body: |
English version below <br>
_________________________<br>
<br>
Hallo %{receiver_displayname}, <br>
<br>
es gibt einen neuen Kommentar von %{commenting_user_displayname} zu Ihrer Kommentaranfrage auf CodeOcean. <br>
Sie finden ihn hier: %{link} <br>
<br>
Diese Mail wurde automatisch von CodeOcean verschickt.<br>
<br>
_________________________<br>
<br>
Dear %{receiver_displayname}, <br>
<br>
you received a new comment from %{commenting_user_displayname} to your request for comments on CodeOcean. <br>
You can find it here: %{link} <br>
<br>
This mail was automatically sent by CodeOcean. <br>
subject: Sie haben einen neuen Kommentar von %{commenting_user_displayname} auf CodeOcean erhalten.
reset_password: reset_password:
body: 'Bitte besuchen Sie %{link}, sofern Sie Ihr Passwort zurücksetzen wollen.' body: 'Bitte besuchen Sie %{link}, sofern Sie Ihr Passwort zurücksetzen wollen.'
subject: Anweisungen zum Zurücksetzen Ihres Passworts subject: Anweisungen zum Zurücksetzen Ihres Passworts
request_for_comments: request_for_comments:
comments: Kommentare
index: index:
get_my_comment_requests: Meine Kommentaranfragen get_my_comment_requests: Meine Kommentaranfragen
all: "Alle Kommentaranfragen" all: "Alle Kommentaranfragen"
no_question: "Der Autor hat keine Frage zu dieser Anfrage gestellt." no_question: "Der Autor hat keine Frage zu dieser Anfrage gestellt."
mark_as_solved: "Diese Frage als beantwortet markieren"
solved: "Diese Frage wurde erfolgreich beantwortet"
sessions: sessions:
create: create:
failure: Fehlerhafte E-Mail oder Passwort. failure: Fehlerhafte E-Mail oder Passwort.
@ -427,3 +455,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
@ -78,6 +77,7 @@ en:
password_confirmation: Passwort Confirmation password_confirmation: Passwort Confirmation
role: Role role: Role
request_for_comments: request_for_comments:
comments: Comments
exercise: Exercise exercise: Exercise
execution_environment: Language execution_environment: Language
username: Username username: Username
@ -90,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
@ -115,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
@ -127,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}'
@ -161,6 +162,7 @@ en:
show: show:
link: Consumer link: Consumer
errors: errors:
connection_refused: Connection refused
index: index:
count: Count count: Count
execution_environments: execution_environments:
@ -209,8 +211,10 @@ en:
submit: Submit Code For Assessment submit: Submit Code For Assessment
test: Test test: Test
timeout: 'Execution stopped. Your code exceeded the permitted execution time of %{permitted_execution_time} seconds.' timeout: 'Execution stopped. Your code exceeded the permitted execution time of %{permitted_execution_time} seconds.'
exercise_deadline_passed: 'The deadline for this exercise has already passed'
tooltips: tooltips:
save: Your code is automatically saved whenever you download, run, or test it. Therefore, explicitly saving is rarely necessary. save: Your code is automatically saved whenever you download, run, or test it. Therefore, explicitly saving is rarely necessary.
exercise_deadline_passed: 'The results for this exercise can only be submitted to the e-learning platform before the deadline has passed.'
request_for_comments_sent: "Request for comments sent." request_for_comments_sent: "Request for comments sent."
editor_file_tree: editor_file_tree:
file_root: Files file_root: Files
@ -323,14 +327,38 @@ en:
activation_needed: activation_needed:
body: 'Please visit %{link} and set up a password in order to complete your registration.' body: 'Please visit %{link} and set up a password in order to complete your registration.'
subject: Please complete your registration. subject: Please complete your registration.
got_new_comment:
body: |
English version below <br>
_________________________<br>
<br>
Hallo %{receiver_displayname}, <br>
<br>
es gibt einen neuen Kommentar von %{commenting_user_displayname} zu Ihrer Kommentaranfrage auf CodeOcean. <br>
Sie finden ihn hier: %{link} <br>
<br>
Diese Mail wurde automatisch von CodeOcean verschickt.<br>
<br>
_________________________<br>
<br>
Dear %{receiver_displayname}, <br>
<br>
you received a new comment from %{commenting_user_displayname} to your request for comments on CodeOcean. <br>
You can find it here: %{link} <br>
<br>
This mail was automatically sent by CodeOcean. <br>
subject: 'You received a new comment on CodeOcean from %{commenting_user_displayname}.'
reset_password: reset_password:
body: 'Please visit %{link} if you want to reset your password.' body: 'Please visit %{link} if you want to reset your password.'
subject: Password reset instructions subject: Password reset instructions
request_for_comments: request_for_comments:
comments: Comments
index: index:
all: All Requests for Comments all: All Requests for Comments
get_my_comment_requests: My Requests for Comments get_my_comment_requests: My Requests for Comments
no_question: "The author did not enter a question for this request." no_question: "The author did not enter a question for this request."
mark_as_solved: "Mark this question as answered"
solved: "This question has been answered"
sessions: sessions:
create: create:
failure: Invalid email or password. failure: Invalid email or password.
@ -427,3 +455,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,14 +1,23 @@
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 resources :request_for_comments do
get '/my_request_for_comments', as: 'my_request_for_comments', to: 'request_for_comments#get_my_comment_requests' member do
get :mark_as_solved
end
end
resources :comments, except: [:destroy] do resources :comments, except: [:destroy] do
collection do collection do
delete :destroy delete :destroy
end end
end end
get '/my_request_for_comments', as: 'my_request_for_comments', to: 'request_for_comments#get_my_comment_requests'
delete '/comment_by_id', to: 'comments#destroy_by_id' delete '/comment_by_id', to: 'comments#destroy_by_id'
put '/comments', to: 'comments#update' put '/comments', to: 'comments#update'
@ -85,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
@ -95,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 AddSolvedToRequestForComments < ActiveRecord::Migration
def change
add_column :request_for_comments, :solved, :boolean
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: 20160512131539) 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: 20160512131539) 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: 20160512131539) 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: 20160512131539) 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,23 +181,16 @@ ActiveRecord::Schema.define(version: 20160512131539) 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.integer "submission_id"
end end
create_table "submissions", force: true do |t| create_table "submissions", force: true do |t|
@ -202,12 +203,6 @@ ActiveRecord::Schema.define(version: 20160512131539) 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

@ -203,7 +203,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,6 +1,7 @@
class PyUnitAdapter < TestingFrameworkAdapter class PyUnitAdapter < TestingFrameworkAdapter
COUNT_REGEXP = /Ran (\d+) test/ COUNT_REGEXP = /Ran (\d+) test/
FAILURES_REGEXP = /FAILED \(failures=(\d+)\)/ FAILURES_REGEXP = /FAILED \(.*failures=(\d+).*\)/
ERRORS_REGEXP = /FAILED \(.*errors=(\d+).*\)/
ASSERTION_ERROR_REGEXP = /AssertionError:\s(.*)/ ASSERTION_ERROR_REGEXP = /AssertionError:\s(.*)/
def self.framework_name def self.framework_name
@ -9,9 +10,11 @@ class PyUnitAdapter < TestingFrameworkAdapter
def parse_output(output) def parse_output(output)
count = COUNT_REGEXP.match(output[:stderr]).captures.first.to_i count = COUNT_REGEXP.match(output[:stderr]).captures.first.to_i
matches = FAILURES_REGEXP.match(output[:stderr]) failures_matches = FAILURES_REGEXP.match(output[:stderr])
failed = matches ? matches.captures.try(:first).to_i : 0 failed = failures_matches ? failures_matches.captures.try(:first).to_i : 0
error_matches = ASSERTION_ERROR_REGEXP.match(output[:stderr]).try(:captures) || [] error_matches = ERRORS_REGEXP.match(output[:stderr])
{count: count, failed: failed, error_messages: error_matches} errors = error_matches ? error_matches.captures.try(:first).to_i : 0
assertion_error_matches = ASSERTION_ERROR_REGEXP.match(output[:stderr]).try(:captures) || []
{count: count, failed: failed + errors, error_messages: assertion_error_matches}
end end
end end

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)
@ -79,9 +75,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
@ -103,10 +97,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) }
@ -118,12 +108,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