Merge branch 'master' into feature-file-templates
Conflicts: app/views/application/_navigation.html.slim config/locales/de.yml config/locales/en.yml db/schema.rb
This commit is contained in:
2
Gemfile
2
Gemfile
@ -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'
|
||||||
end
|
end
|
||||||
|
@ -195,6 +195,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)
|
||||||
@ -324,6 +326,7 @@ GEM
|
|||||||
json (>= 1.8.0)
|
json (>= 1.8.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
|
unf (0.1.4-java)
|
||||||
unf_ext (0.0.7.1)
|
unf_ext (0.0.7.1)
|
||||||
unicode-display_width (0.3.1)
|
unicode-display_width (0.3.1)
|
||||||
web-console (2.3.0)
|
web-console (2.3.0)
|
||||||
@ -383,6 +386,7 @@ DEPENDENCIES
|
|||||||
pry
|
pry
|
||||||
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
|
||||||
@ -393,6 +397,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
|
||||||
|
@ -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;
|
||||||
@ -175,7 +179,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;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -404,12 +411,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 +470,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();
|
||||||
};
|
};
|
||||||
@ -587,7 +606,7 @@ $(function() {
|
|||||||
} 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;
|
||||||
//}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 +723,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);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -870,7 +895,9 @@ $(function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var showWorkspaceTab = function(event) {
|
var showWorkspaceTab = function(event) {
|
||||||
event.preventDefault();
|
if(event){
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
showTab(0);
|
showTab(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1030,6 +1057,7 @@ $(function() {
|
|||||||
case 'exit':
|
case 'exit':
|
||||||
killWebsocketAndContainer();
|
killWebsocketAndContainer();
|
||||||
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 +1069,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 +1217,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 +1264,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();
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
|
@ -49,7 +49,10 @@ div#chart_2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a.file-heading {
|
||||||
|
color: black !important;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.bar {
|
.bar {
|
||||||
fill: orange;
|
fill: orange;
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
class TeamPolicy < ApplicationPolicy
|
|
||||||
[:create?, :index?, :new?].each do |action|
|
|
||||||
define_method(action) { admin? }
|
|
||||||
end
|
|
||||||
|
|
||||||
[:destroy?, :edit?, :show?, :update?].each do |action|
|
|
||||||
define_method(action) { admin? || member? }
|
|
||||||
end
|
|
||||||
|
|
||||||
def member?
|
|
||||||
@record.members.include?(@user)
|
|
||||||
end
|
|
||||||
private :member?
|
|
||||||
end
|
|
@ -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, FileTemplate, 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"))
|
||||||
|
@ -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')
|
||||||
|
@ -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')
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
@ -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}"
|
||||||
|
@ -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
|
||||||
|
@ -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}" ☼
|
||||||
|
.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)
|
||||||
|
@ -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)
|
@ -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.displayname %> | <%= @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',
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
|
@ -1,3 +0,0 @@
|
|||||||
h1 = @hint
|
|
||||||
|
|
||||||
= render('form')
|
|
@ -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)
|
|
@ -1,3 +0,0 @@
|
|||||||
h1 = t('shared.new_model', model: Team.model_name.human)
|
|
||||||
|
|
||||||
= render('form')
|
|
@ -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)
|
|
1
app/views/user_mailer/got_new_comment.slim
Normal file
1
app/views/user_mailer/got_new_comment.slim
Normal 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)
|
@ -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"
|
||||||
@ -79,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
|
||||||
@ -91,9 +90,6 @@ de:
|
|||||||
files: Dateien
|
files: Dateien
|
||||||
score: Punktzahl
|
score: Punktzahl
|
||||||
user: Autor
|
user: Autor
|
||||||
team:
|
|
||||||
internal_user_ids: Mitglieder
|
|
||||||
name: Name
|
|
||||||
file_template:
|
file_template:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
file_type: "Dateityp"
|
file_type: "Dateityp"
|
||||||
@ -135,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'
|
||||||
@ -169,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:
|
||||||
@ -217,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
|
||||||
@ -331,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.
|
||||||
|
@ -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"
|
||||||
@ -79,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
|
||||||
@ -91,9 +90,6 @@ en:
|
|||||||
files: Files
|
files: Files
|
||||||
score: Score
|
score: Score
|
||||||
user: Author
|
user: Author
|
||||||
team:
|
|
||||||
internal_user_ids: Members
|
|
||||||
name: Name
|
|
||||||
file_template:
|
file_template:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
file_type: "File Type"
|
file_type: "File Type"
|
||||||
@ -135,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}'
|
||||||
@ -169,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:
|
||||||
@ -217,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
|
||||||
@ -331,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.
|
||||||
|
@ -7,13 +7,17 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
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'
|
||||||
@ -90,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
|
||||||
@ -100,5 +105,4 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :teams
|
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
class AddSolvedToRequestForComments < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :request_for_comments, :solved, :boolean
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
class AddSubmissionToRequestForComments < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_reference :request_for_comments, :submission
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
class RemoveRequestedAtFromRequestForComments < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
remove_column :request_for_comments, :requested_at
|
||||||
|
end
|
||||||
|
end
|
7
db/migrate/20160704143402_remove_teams.rb
Normal file
7
db/migrate/20160704143402_remove_teams.rb
Normal 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
|
26
db/schema.rb
26
db/schema.rb
@ -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: 20160610111602) 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: 20160610111602) 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
|
||||||
@ -182,23 +181,16 @@ ActiveRecord::Schema.define(version: 20160610111602) 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|
|
||||||
@ -211,12 +203,6 @@ ActiveRecord::Schema.define(version: 20160610111602) 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"
|
||||||
|
@ -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))
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||||||
FactoryGirl.define do
|
|
||||||
factory :team do
|
|
||||||
internal_users { build_pair :teacher }
|
|
||||||
name 'The A-Team'
|
|
||||||
end
|
|
||||||
end
|
|
@ -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
|
||||||
|
@ -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
|
|
@ -3,8 +3,8 @@ require 'rails_helper'
|
|||||||
describe ExercisePolicy do
|
describe ExercisePolicy do
|
||||||
subject { described_class }
|
subject { described_class }
|
||||||
|
|
||||||
let(:exercise) { FactoryGirl.build(:dummy, team: FactoryGirl.create(:team)) }
|
let(:exercise) { FactoryGirl.build(:dummy) }
|
||||||
|
|
||||||
permissions :batch_update? do
|
permissions :batch_update? do
|
||||||
it 'grants access to admins only' do
|
it 'grants access to admins only' do
|
||||||
expect(subject).to permit(FactoryGirl.build(:admin), exercise)
|
expect(subject).to permit(FactoryGirl.build(:admin), exercise)
|
||||||
@ -40,10 +40,6 @@ describe ExercisePolicy do
|
|||||||
expect(subject).to permit(exercise.author, exercise)
|
expect(subject).to permit(exercise.author, exercise)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'grants access to team members' do
|
|
||||||
expect(subject).to permit(exercise.team.members.first, exercise)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not grant access to all other users' do
|
it 'does not grant access to all other users' do
|
||||||
[:external_user, :teacher].each do |factory_name|
|
[:external_user, :teacher].each do |factory_name|
|
||||||
expect(subject).not_to permit(FactoryGirl.build(factory_name), exercise)
|
expect(subject).not_to permit(FactoryGirl.build(factory_name), exercise)
|
||||||
@ -71,9 +67,7 @@ describe ExercisePolicy do
|
|||||||
|
|
||||||
[@admin, @teacher].each do |user|
|
[@admin, @teacher].each do |user|
|
||||||
[true, false].each do |public|
|
[true, false].each do |public|
|
||||||
[@team, nil].each do |team|
|
FactoryGirl.create(:dummy, public: public, user_id: user.id, user_type: InternalUser.class.name)
|
||||||
FactoryGirl.create(:dummy, public: public, team: team, user_id: user.id, user_type: InternalUser.class.name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -95,10 +89,6 @@ describe ExercisePolicy do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'for teachers' do
|
context 'for teachers' do
|
||||||
before(:each) do
|
|
||||||
@team = FactoryGirl.create(:team)
|
|
||||||
@team.members << @teacher
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:scope) { Pundit.policy_scope!(@teacher, Exercise) }
|
let(:scope) { Pundit.policy_scope!(@teacher, Exercise) }
|
||||||
|
|
||||||
@ -110,12 +100,8 @@ describe ExercisePolicy do
|
|||||||
expect(scope.map(&:id)).to include(*Exercise.where(public: false, user_id: @teacher.id).map(&:id))
|
expect(scope.map(&:id)).to include(*Exercise.where(public: false, user_id: @teacher.id).map(&:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
it "includes all of team members' non-public exercises" do
|
|
||||||
expect(scope.map(&:id)).to include(*Exercise.where(public: false, team_id: @teacher.teams.first.id).map(&:id))
|
|
||||||
end
|
|
||||||
|
|
||||||
it "does not include other authors' non-public exercises" do
|
it "does not include other authors' non-public exercises" do
|
||||||
expect(scope.map(&:id)).not_to include(*Exercise.where(public: false).where("team_id <> #{@team.id} AND user_id <> #{@teacher.id}").map(&:id))
|
expect(scope.map(&:id)).not_to include(*Exercise.where(public: false).where(user_id <> #{@teacher.id}").map(&:id))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -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
|
|
Reference in New Issue
Block a user