diff --git a/Gemfile b/Gemfile
index 20b30e9c..acc4c133 100644
--- a/Gemfile
+++ b/Gemfile
@@ -38,6 +38,7 @@ gem 'faye-websocket'
gem 'nokogiri'
gem 'd3-rails'
gem 'rest-client'
+gem 'rubyzip'
group :development do
gem 'better_errors', platform: :ruby
@@ -47,6 +48,7 @@ group :development do
gem 'capistrano-rails'
gem 'capistrano-rvm'
gem 'capistrano-upload-config'
+ gem 'rack-mini-profiler'
gem 'rubocop', require: false
gem 'rubocop-rspec'
gem 'web-console', '~> 2.0', platform: :ruby
diff --git a/Gemfile.lock b/Gemfile.lock
index 8b377658..3a7e03d2 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -198,6 +198,8 @@ GEM
pundit (1.1.0)
activesupport (>= 3.0.0)
rack (1.5.5)
+ rack-mini-profiler (0.10.1)
+ rack (>= 1.2.0)
rack-test (0.6.3)
rack (>= 1.0)
rails (4.1.14.1)
@@ -387,6 +389,7 @@ DEPENDENCIES
pry-byebug
puma (~> 2.15.3)
pundit
+ rack-mini-profiler
rails (~> 4.1.13)
rails-i18n (~> 4.0.0)
rake
@@ -397,6 +400,7 @@ DEPENDENCIES
rubocop
rubocop-rspec
rubytree
+ rubyzip
sass-rails (~> 4.0.3)
sdoc (~> 0.4.0)
selenium-webdriver
diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js.erb
index c69551bb..2d73bbf0 100644
--- a/app/assets/javascripts/editor.js.erb
+++ b/app/assets/javascripts/editor.js.erb
@@ -19,6 +19,10 @@ $(function() {
var SERVER_SEND_EVENT = 2;
var editors = [];
+ var editor_for_file = new Map();
+ var regex_for_language = new Map();
+ var tracepositions_regex;
+
var active_file = undefined;
var active_frame = undefined;
var running = false;
@@ -37,6 +41,7 @@ $(function() {
var ENTER_KEY_CODE = 13;
var flowrOutputBuffer = "";
+ var QaApiOutputBuffer = {'stdout': '', 'stderr': ''};
var flowrResultHtml = '
'
var ajax = function(options) {
@@ -175,7 +180,10 @@ $(function() {
var downloadCode = function(event) {
event.preventDefault();
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;
});
};
@@ -184,8 +192,8 @@ $(function() {
(streamed ? evaluateCodeWithStreamedResponse : evaluateCodeWithoutStreamedResponse)(url, callback);
};
- var evaluateCodeWithStreamedResponse = function(url, callback) {
- initWebsocketConnection(url);
+ var evaluateCodeWithStreamedResponse = function(url, onmessageFunction) {
+ initWebsocketConnection(url, onmessageFunction);
// TODO only init turtle when required
initTurtle();
@@ -306,9 +314,10 @@ $(function() {
}
};
- var handleScoringResponse = function(response) {
- printScoringResults(response);
- var score = _.reduce(response, function(sum, result) {
+ var handleScoringResponse = function(websocket_event) {
+ results = JSON.parse(websocket_event.data);
+ printScoringResults(results);
+ var score = _.reduce(results, function(sum, result) {
return sum + result.score * result.weight;
}, 0).toFixed(2);
$('#score').data('score', score);
@@ -316,6 +325,14 @@ $(function() {
showTab(2);
};
+ var handleQaApiOutput = function() {
+ if (qa_api) {
+ qa_api.executeCommand('syncOutput', [[QaApiOutputBuffer]]);
+ // reset the object
+ }
+ QaApiOutputBuffer = {'stdout': '', 'stderr': ''};
+ }
+
// activate flowr only for half of the audience
var isFlowrEnabled = true;//parseInt($('#editor').data('user-id'))%2 == 0;
var handleStderrOutputForFlowr = function() {
@@ -349,13 +366,14 @@ $(function() {
flowrOutputBuffer = '';
};
- var handleTestResponse = function(response) {
+ var handleTestResponse = function(websocket_event) {
+ result = JSON.parse(websocket_event.data);
clearOutput();
- printOutput(response[0], false, 0);
+ printOutput(result, false, 0);
if (qa_api) {
- qa_api.executeCommand('syncOutput', [response]);
+ qa_api.executeCommand('syncOutput', [result]);
}
- showStatus(response[0]);
+ showStatus(result);
showTab(1);
};
@@ -404,12 +422,18 @@ $(function() {
editor.setTheme(THEME);
editor.commands.bindKey("ctrl+alt+0", null);
editors.push(editor);
+ editor_for_file.set($(element).parent().data('filename'), editor);
var session = editor.getSession();
session.setMode($(element).data('mode'));
session.setTabSize($(element).data('indent-size'));
session.setUseSoftTabs(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');
/*
@@ -457,6 +481,12 @@ $(function() {
$('#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() {
$('[data-tooltip]').tooltip();
};
@@ -527,8 +557,8 @@ $(function() {
};
var isBrowserSupported = function() {
- // eventsource tests for server send events (used for scoring), websockets is used for run
- return Modernizr.eventsource && Modernizr.websockets;
+ // websockets is used for run, score and test
+ return Modernizr.websockets;
};
var populatePanel = function(panel, result, index) {
@@ -574,20 +604,23 @@ $(function() {
// output_mode_is_streaming = false;
//}
if (!colorize) {
- if(output.stdout != ''){
+ if(output.stdout != undefined && output.stdout != ''){
element.append(output.stdout)
}
- if(output.stderr != ''){
+ if(output.stderr != undefined && output.stderr != ''){
element.append('There was an error: StdErr: ' + output.stderr);
}
} else if (output.stderr) {
element.addClass('text-warning').append(output.stderr);
+ flowrOutputBuffer += output.stderr;
+ QaApiOutputBuffer.stderr += output.stderr;
} else if (output.stdout) {
//if (output_mode_is_streaming){
element.addClass('text-success').append(output.stdout);
flowrOutputBuffer += output.stdout;
+ QaApiOutputBuffer.stdout += output.stdout;
//}else{
// element.addClass('text-success');
// element.data('content_buffer' , element.data('content_buffer') + output.stdout);
@@ -704,9 +737,15 @@ $(function() {
};
var renderScore = function() {
- var score = $('#score').data('score');
- var maxium_score = $('#score').data('maximum-score');
- $('.score').html((score || '?') + ' / ' + maxium_score);
+ var score = parseFloat($('#score').data('score'));
+ var maxium_score = parseFloat($('#score').data('maximum-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);
};
@@ -737,7 +776,7 @@ $(function() {
showSpinner($('#run'));
toggleButtonStates();
var url = response.run_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
- evaluateCode(url, true, printChunk);
+ evaluateCode(url, true, function(evt) { parseCanvasMessage(evt.data, true); });
});
}
};
@@ -772,7 +811,7 @@ $(function() {
createSubmission(this, null, function(response) {
showSpinner($('#assess'));
var url = response.score_url;
- evaluateCode(url, false, handleScoringResponse);
+ evaluateCode(url, true, handleScoringResponse);
});
};
@@ -870,7 +909,9 @@ $(function() {
}
var showWorkspaceTab = function(event) {
- event.preventDefault();
+ if(event){
+ event.preventDefault();
+ }
showTab(0);
};
@@ -949,7 +990,7 @@ $(function() {
createSubmission(this, null, function(response) {
showSpinner($('#test'));
var url = response.test_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
- evaluateCode(url, false, handleTestResponse);
+ evaluateCode(url, true, handleTestResponse);
});
}
};
@@ -968,14 +1009,14 @@ $(function() {
$('#test').toggle(isActiveFileTestable());
};
- var initWebsocketConnection = function(url) {
+ var initWebsocketConnection = function(url, onmessageFunction) {
//TODO: get the protocol from config file dependent on environment. (dev: ws, prod: wss)
//causes: Puma::HttpParserError: Invalid HTTP format, parsing fails.
//TODO: make sure that this gets cached.
websocket = new WebSocket('<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + window.location.port + url);
websocket.onopen = function(evt) { resetOutputTab(); }; // todo show some kind of indicator for established connection
websocket.onclose = function(evt) { /* expected at some point */ };
- websocket.onmessage = function(evt) { parseCanvasMessage(evt.data, true); };
+ websocket.onmessage = onmessageFunction;
websocket.onerror = function(evt) { showWebsocketError(); };
websocket.flush = function() { this.send('\n'); }
};
@@ -1029,7 +1070,9 @@ $(function() {
break;
case 'exit':
killWebsocketAndContainer();
+ handleQaApiOutput();
handleStderrOutputForFlowr();
+ augmentStacktraceInOutput();
break;
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.
@@ -1041,6 +1084,41 @@ $(function() {
}
};
+
+ var jumpToSourceLine = function(event){
+ var file = $(event.target).data('file');
+ var line = $(event.target).data('line');
+
+ showWorkspaceTab(null);
+ // set active file ?!?!
+
+ var frame = $('div.frame[data-filename="' + file + '"]');
+ showFrame(frame);
+
+ var editor = editor_for_file.get(file);
+ editor.gotoLine(line, 0);
+
+ };
+
+ var augmentStacktraceInOutput = function() {
+ if(tracepositions_regex){
+ var element = $('#output>pre');
+ var text = element.text();
+ element.on( "click", "a", jumpToSourceLine);
+
+ var matches;
+
+ while(matches = tracepositions_regex.exec(text)){
+ var frame = $('div.frame[data-filename="' + matches[1] + '"]')
+
+ if(frame.length > 0){
+ element.html(text.replace(matches[0], "" + matches[0] + ""));
+ }
+ }
+ }
+
+ };
+
var renderWebsocketOutput = function(msg){
var element = findOrCreateRenderElement(0);
element.append(msg.data);
@@ -1154,25 +1232,25 @@ $(function() {
var file_id = $('.editor').data('id')
var question = $('#question').val();
- $.ajax({
- method: 'POST',
- url: '/request_for_comments',
- data: {
- request_for_comment: {
- exercise_id: exercise_id,
- file_id: file_id,
- question: question,
- "requested_at(1i)": 2015, // these are the timestamp values that the request handler demands
- "requested_at(2i)":3, // they could be random here, because the timestamp is updated on serverside anyway
- "requested_at(3i)":27,
- "requested_at(4i)":17,
- "requested_at(5i)":06
+ var createRequestForComments = function(submission) {
+ $.ajax({
+ method: 'POST',
+ url: '/request_for_comments',
+ data: {
+ request_for_comment: {
+ exercise_id: exercise_id,
+ file_id: file_id,
+ submission_id: submission.id,
+ question: question
+ }
}
- }
- }).done(function() {
- hideSpinner();
- $.flash.success({ text: $('#askForCommentsButton').data('message-success') })
- }).error(ajaxError);
+ }).done(function() {
+ hideSpinner();
+ $.flash.success({ text: $('#askForCommentsButton').data('message-success') });
+ }).error(ajaxError);
+ }
+
+ createSubmission($('.requestCommentsButton'), null, createRequestForComments);
$('#comment-modal').modal('hide');
var button = $('.requestCommentsButton');
@@ -1201,6 +1279,7 @@ $(function() {
if ($('#editor').isPresent()) {
if (isBrowserSupported()) {
+ initializeRegexes();
initializeCodePilot();
$('.score, #development-environment').show();
configureEditors();
diff --git a/app/assets/javascripts/exercise_graphs.js b/app/assets/javascripts/exercise_graphs.js
index 2d3588c6..5f521b39 100644
--- a/app/assets/javascripts/exercise_graphs.js
+++ b/app/assets/javascripts/exercise_graphs.js
@@ -9,6 +9,9 @@ $(function() {
submissionsScoreAndTimeAssess = [[0,0]];
submissionsScoreAndTimeSubmits = [[0,0]];
+ submissionsScoreAndTimeRuns = [];
+ submissionsSaves = [];
+ submissionsAutosaves = [];
var maximumValue = 0;
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 0) {
+ submissionArray[1] = workingTime;
+ }
+ if(submission.score>maximumValue){
+ maximumValue = submission.score;
+ }
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);
} 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);
+ } 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(submissionsScoreAndTimeSubmits);
+ // console.log(submissionsScoreAndTimeRuns);
function get_minutes (time_stamp) {
try {
hours = time_stamp.split(":")[0];
minutes = time_stamp.split(":")[1];
seconds = time_stamp.split(":")[2];
-
- minutes = parseFloat(hours * 60) + parseInt(minutes);
+ seconds /= 60;
+ minutes = parseFloat(hours * 60) + parseInt(minutes) + seconds;
if (minutes > 0){
return minutes;
} else{
@@ -82,6 +84,9 @@ $(function() {
function graph_assesses() {
// MAKE THE GRAPH
var width_ratio = .8;
+ if (getWidth()*width_ratio > 1000){
+ width_ratio = 1000/getWidth();
+ }
var height_ratio = .7; // percent of height
var margin = {top: 100, right: 20, bottom: 70, left: 70},//30,50
@@ -131,15 +136,32 @@ $(function() {
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+ var largestSubmittedTimeStamp = submissions[submissions_length-1];
+ var largestArrayForRange;
- x.domain(d3.extent(submissionsScoreAndTimeAssess, function (d) {
- // console.log(d[1]);
- return (d[1]);
- }));
- y.domain(d3.extent(submissionsScoreAndTimeAssess, function (d) {
+ if(largestSubmittedTimeStamp.cause == "assess"){
+ largestArrayForRange = submissionsScoreAndTimeAssess;
+ x.domain([0,largestArrayForRange[largestArrayForRange.length - 1][1]]).clamp(true);
+ } else if(largestSubmittedTimeStamp.cause == "submit"){
+ 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]);
return (d[0]);
}));
+ // y.domain([0,2]).clamp(true);
svg.append("g") //x axis
.attr("class", "x axis")
@@ -177,15 +199,15 @@ $(function() {
.style('font-size', 20)
.style('text-decoration', 'underline');
- //
- // svg.append("path")
- // //.datum()
- // .attr("class", "line")
- // .attr('id', 'myPath')// new
- // .attr("stroke", "black")
- // .attr("stroke-width", 5)
- // .attr("fill", "none")// end new
- // .attr("d", line(submissionsScoreAndTimeAssess));//---
+
+ svg.append("path")
+ //.datum()
+ .attr("class", "line")
+ .attr('id', 'myPath')// new
+ .attr("stroke", "black")
+ .attr("stroke-width", 5)
+ .attr("fill", "none")// end new
+ .attr("d", line(submissionsScoreAndTimeAssess));//---
svg.append("path")
.datum(submissionsScoreAndTimeAssess)
@@ -204,27 +226,35 @@ $(function() {
.attr("cx", function(d) { return x(d[1]); })
.attr("cy", function(d) { return y(d[0]); });
-
- svg.append("path")
- .datum(submissionsScoreAndTimeSubmits)
- .attr("class", "line2")
- .attr('id', 'myPath')// new
- .attr("stroke", "blue")
- .attr("stroke-width", 5)
- .attr("fill", "none")// end new
- .attr("d", line);//---
+ if (submissionsScoreAndTimeSubmits.length > 0){
+ // get rid of the 0 element at the beginning
+ submissionsScoreAndTimeSubmits.shift();
+ }
svg.selectAll("dot") // Add dots to submits
.data(submissionsScoreAndTimeSubmits)
.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("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"],
- 1 : ["Assesses", "orange"]
- }
+ 1 : ["Assesses", "orange"],
+ 2 : ["Runs", "red"]
+ };
// add legend
var legend = svg.append("g")
@@ -234,7 +264,8 @@ $(function() {
.attr("height", 100)
.attr("width", 100);
- var dataset = [submissionsScoreAndTimeSubmits,submissionsScoreAndTimeAssess];
+ var dataset = [submissionsScoreAndTimeSubmits,submissionsScoreAndTimeAssess, submissionsScoreAndTimeRuns];
+ var yOffset = -70;
legend.selectAll('g').data(dataset)
.enter()
@@ -243,14 +274,14 @@ $(function() {
var g = d3.select(this);
g.append("rect")
.attr("x", 20)
- .attr("y", i*25 + 8)
+ .attr("y", i*25 + yOffset)// + 8
.attr("width", 10)
.attr("height", 10)
.style("fill", color_hash[String(i)][1]);
g.append("text")
.attr("x", 40)
- .attr("y", i * 25 + 18)
+ .attr("y", i * 25 + yOffset + 10)// + 18
.attr("height",30)
.attr("width",100)
.style("fill", color_hash[String(i)][1])
@@ -273,7 +304,7 @@ $(function() {
try{
graph_assesses();
} catch(err){
- // not enough data
+ alert("could not draw the graph");
}
}
diff --git a/app/assets/javascripts/exercises.js b/app/assets/javascripts/exercises.js
index 1f861ef6..81da8cf8 100644
--- a/app/assets/javascripts/exercises.js
+++ b/app/assets/javascripts/exercises.js
@@ -12,6 +12,9 @@ $(function() {
$('#files li:last select[name*="file_type_id"]').val(getSelectedExecutionEnvironment().file_type_id);
$('#files li:last select').chosen(window.CodeOcean.CHOSEN_OPTIONS);
$('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() {
@@ -148,6 +151,22 @@ $(function() {
});
};
+ var updateFileTemplates = function(fileType) {
+ var jqxhr = $.ajax({
+ url: '/file_templates/by_file_type/' + fileType + '.json',
+ dataType: 'json'
+ });
+ jqxhr.done(function(response) {
+ var noTemplateLabel = $('#noTemplateLabel').data('text');
+ var options = "";
+ for (var i = 0; i < response.length; i++) {
+ options += ""
+ }
+ $("#code_ocean_file_file_template_id").find('option').remove().end().append($(options));
+ });
+ jqxhr.fail(ajaxError);
+ }
+
if ($.isController('exercises')) {
if ($('table').isPresent()) {
enableBatchUpdate();
@@ -162,6 +181,10 @@ $(function() {
inferFileAttributes();
observeFileRoleChanges();
overrideTextareaTabBehavior();
+ } else if ($('#files.jstree').isPresent()) {
+ var fileTypeSelect = $('#code_ocean_file_file_type_id');
+ fileTypeSelect.on("change", function() {updateFileTemplates(fileTypeSelect.val())});
+ updateFileTemplates(fileTypeSelect.val());
}
toggleCodeHeight();
if (window.hljs) {
diff --git a/app/assets/javascripts/file_templates.js.coffee b/app/assets/javascripts/file_templates.js.coffee
new file mode 100644
index 00000000..24f83d18
--- /dev/null
+++ b/app/assets/javascripts/file_templates.js.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://coffeescript.org/
diff --git a/app/assets/javascripts/turtle.js b/app/assets/javascripts/turtle.js
index f2fa184b..ddbb27eb 100644
--- a/app/assets/javascripts/turtle.js
+++ b/app/assets/javascripts/turtle.js
@@ -41,8 +41,7 @@ Turtle.prototype.update = function () {
var i, k, canvas, ctx, dx, dy, item, c, length;
canvas = this.canvas[0];
ctx = canvas.getContext('2d');
- ctx.fillStyle = '#fff';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
length = this.items.length;
dx = canvas.width / 2;
dy = canvas.height / 2;
@@ -56,8 +55,8 @@ Turtle.prototype.update = function () {
for (k = 2; k < c.length; k += 2) {
ctx.lineTo(c[k] + dx, c[k + 1] + dy);
}
- if (this.fill) {
- ctx.strokeStyle = this.fill;
+ if (item.fill) {
+ ctx.strokeStyle = item.fill;
}
ctx.stroke();
diff --git a/app/assets/javascripts/working_time_graphs.js b/app/assets/javascripts/working_time_graphs.js
index 315d2c09..181b3799 100644
--- a/app/assets/javascripts/working_time_graphs.js
+++ b/app/assets/javascripts/working_time_graphs.js
@@ -4,7 +4,7 @@ $(function() {
if ($.isController('exercises') && $('.graph-functions').isPresent()) {
var working_times = $('#data').data('working-time');
-
+
function get_minutes (time_stamp){
try{
hours = time_stamp.split(":")[0];
@@ -67,6 +67,9 @@ $(function() {
// DRAW THE LINE GRAPH ------------------------------------------------------------------------------
function draw_line_graph() {
var width_ratio = .8;
+ if (getWidth()*width_ratio > 1000){
+ width_ratio = 1000/getWidth();
+ }
var height_ratio = .7; // percent of height
// currently sets as percentage of window width, however, unfortunately
diff --git a/app/assets/stylesheets/exercises.css.scss b/app/assets/stylesheets/exercises.css.scss
index f348c025..eb1b5300 100644
--- a/app/assets/stylesheets/exercises.css.scss
+++ b/app/assets/stylesheets/exercises.css.scss
@@ -49,7 +49,10 @@ div#chart_2 {
}
-
+a.file-heading {
+ color: black !important;
+ text-decoration: none;
+}
.bar {
fill: orange;
diff --git a/app/assets/stylesheets/file_templates.css.scss b/app/assets/stylesheets/file_templates.css.scss
new file mode 100644
index 00000000..bf8e27e8
--- /dev/null
+++ b/app/assets/stylesheets/file_templates.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the FileTemplates controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/app/controllers/code_ocean/files_controller.rb b/app/controllers/code_ocean/files_controller.rb
index 08ca897d..a0dad3b1 100644
--- a/app/controllers/code_ocean/files_controller.rb
+++ b/app/controllers/code_ocean/files_controller.rb
@@ -10,6 +10,11 @@ module CodeOcean
def create
@file = CodeOcean::File.new(file_params)
+ if @file.file_template_id
+ content = FileTemplate.find(@file.file_template_id).content
+ content.sub! '{{file_name}}', @file.name
+ @file.content = content
+ end
authorize!
create_and_respond(object: @file, path: proc { implement_exercise_path(@file.context.exercise, tab: 2) })
end
diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb
index fd6840ff..fe7f454c 100644
--- a/app/controllers/comments_controller.rb
+++ b/app/controllers/comments_controller.rb
@@ -46,7 +46,11 @@ class CommentsController < ApplicationController
# POST /comments
# POST /comments.json
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|
if @comment.save
@@ -64,7 +68,7 @@ class CommentsController < ApplicationController
# PATCH/PUT /comments/1.json
def update
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.json { render :show, status: :ok, location: @comment }
else
@@ -101,10 +105,14 @@ class CommentsController < ApplicationController
@comment = Comment.find(params[:id])
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.
def comment_params
#params.require(:comment).permit(:user_id, :file_id, :row, :column, :text)
# 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
diff --git a/app/controllers/concerns/file_parameters.rb b/app/controllers/concerns/file_parameters.rb
index e61e719e..295b66c3 100644
--- a/app/controllers/concerns/file_parameters.rb
+++ b/app/controllers/concerns/file_parameters.rb
@@ -1,6 +1,6 @@
module FileParameters
def file_attributes
- %w(content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight)
+ %w(content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight file_template_id)
end
private :file_attributes
end
diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb
index 0304abb4..45fd04d9 100644
--- a/app/controllers/exercises_controller.rb
+++ b/app/controllers/exercises_controller.rb
@@ -9,7 +9,6 @@ class ExercisesController < ApplicationController
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit, :reload]
before_action :set_external_user, only: [:statistics]
before_action :set_file_types, only: [:create, :edit, :new, :update]
- before_action :set_teams, only: [:create, :edit, :new, :update]
skip_before_filter :verify_authenticity_token, 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
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
private :exercise_params
@@ -195,11 +194,6 @@ class ExercisesController < ApplicationController
end
private :set_file_types
- def set_teams
- @teams = Team.all.order(:name)
- end
- private :set_teams
-
def show
end
diff --git a/app/controllers/file_templates_controller.rb b/app/controllers/file_templates_controller.rb
new file mode 100644
index 00000000..a6039500
--- /dev/null
+++ b/app/controllers/file_templates_controller.rb
@@ -0,0 +1,94 @@
+class FileTemplatesController < ApplicationController
+ before_action :set_file_template, only: [:show, :edit, :update, :destroy]
+
+ def authorize!
+ authorize(@file_template || @file_templates)
+ end
+ private :authorize!
+
+ def by_file_type
+ @file_templates = FileTemplate.where(:file_type_id => params[:file_type_id])
+ authorize!
+ respond_to do |format|
+ format.json { render :show, status: :ok, json: @file_templates.to_json }
+ end
+ end
+
+ # GET /file_templates
+ # GET /file_templates.json
+ def index
+ @file_templates = FileTemplate.all.order(:file_type_id).paginate(page: params[:page])
+ authorize!
+ end
+
+ # GET /file_templates/1
+ # GET /file_templates/1.json
+ def show
+ authorize!
+ end
+
+ # GET /file_templates/new
+ def new
+ @file_template = FileTemplate.new
+ authorize!
+ end
+
+ # GET /file_templates/1/edit
+ def edit
+ authorize!
+ end
+
+ # POST /file_templates
+ # POST /file_templates.json
+ def create
+ @file_template = FileTemplate.new(file_template_params)
+ authorize!
+
+ respond_to do |format|
+ if @file_template.save
+ format.html { redirect_to @file_template, notice: 'File template was successfully created.' }
+ format.json { render :show, status: :created, location: @file_template }
+ else
+ format.html { render :new }
+ format.json { render json: @file_template.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # PATCH/PUT /file_templates/1
+ # PATCH/PUT /file_templates/1.json
+ def update
+ authorize!
+ respond_to do |format|
+ if @file_template.update(file_template_params)
+ format.html { redirect_to @file_template, notice: 'File template was successfully updated.' }
+ format.json { render :show, status: :ok, location: @file_template }
+ else
+ format.html { render :edit }
+ format.json { render json: @file_template.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /file_templates/1
+ # DELETE /file_templates/1.json
+ def destroy
+ authorize!
+ @file_template.destroy
+ respond_to do |format|
+ format.html { redirect_to file_templates_url, notice: 'File template was successfully destroyed.' }
+ format.json { head :no_content }
+ end
+ end
+
+ private
+ # Use callbacks to share common setup or constraints between actions.
+ def set_file_template
+ @file_template = FileTemplate.find(params[:id])
+ end
+
+ # Never trust parameters from the scary internet, only allow the white list through.
+ def file_template_params
+ params[:file_template].permit(:name, :file_type_id, :content)
+ end
+end
diff --git a/app/controllers/request_for_comments_controller.rb b/app/controllers/request_for_comments_controller.rb
index 555dad09..37d8bef9 100644
--- a/app/controllers/request_for_comments_controller.rb
+++ b/app/controllers/request_for_comments_controller.rb
@@ -1,5 +1,5 @@
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
@@ -20,6 +20,18 @@ class RequestForCommentsController < ApplicationController
render 'index'
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.json
def show
@@ -70,6 +82,6 @@ class RequestForCommentsController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through.
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
diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb
index 4ec2e7bc..e236ebeb 100644
--- a/app/controllers/submissions_controller.rb
+++ b/app/controllers/submissions_controller.rb
@@ -6,9 +6,9 @@ class SubmissionsController < ApplicationController
include SubmissionScoring
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_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_mime_type, 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
+ 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
if @file.native_file?
send_file(@file.native_file.path)
@@ -174,8 +188,14 @@ class SubmissionsController < ApplicationController
def parse_message(message, output_stream, socket, recursive = true)
begin
parsed = JSON.parse(message)
- socket.send_data message
- Rails.logger.info('parse_message sent: ' + message)
+ if(parsed.class == Hash && parsed.key?('cmd'))
+ socket.send_data message
+ Rails.logger.info('parse_message sent: ' + message)
+ else
+ parsed = {'cmd'=>'write','stream'=>output_stream,'data'=>message}
+ socket.send_data JSON.dump(parsed)
+ Rails.logger.info('parse_message sent: ' + JSON.dump(parsed))
+ end
rescue JSON::ParserError => e
# Check wether the message contains multiple lines, if true try to parse each line
if ((recursive == true) && (message.include? "\n"))
@@ -208,7 +228,11 @@ class SubmissionsController < ApplicationController
end
def score
- render(json: score_submission(@submission))
+ hijack do |tubesock|
+ Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
+ # tubesock is the socket to the client
+ tubesock.send_data JSON.dump(score_submission(@submission))
+ end
end
def set_docker_client
@@ -260,8 +284,14 @@ class SubmissionsController < ApplicationController
private :store_error
def test
- output = @docker_client.execute_test_command(@submission, params[:filename])
- render(json: [output])
+ hijack do |tubesock|
+ Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
+
+ output = @docker_client.execute_test_command(@submission, params[:filename])
+
+ # tubesock is the socket to the client
+ tubesock.send_data JSON.dump(output)
+ end
end
def with_server_sent_events
diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb
deleted file mode 100644
index 1ca8f1fe..00000000
--- a/app/controllers/teams_controller.rb
+++ /dev/null
@@ -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
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 4b3c71f4..e1773d48 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -11,4 +11,13 @@ class UserMailer < ActionMailer::Base
@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)
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
diff --git a/app/models/code_ocean/file.rb b/app/models/code_ocean/file.rb
index 58440f1b..22c6e877 100644
--- a/app/models/code_ocean/file.rb
+++ b/app/models/code_ocean/file.rb
@@ -35,6 +35,7 @@ module CodeOcean
has_many :files
has_many :testruns
+ has_many :comments
alias_method :descendants, :files
mount_uploader :native_file, FileUploader
diff --git a/app/models/exercise.rb b/app/models/exercise.rb
index ec21da0f..29f260c2 100644
--- a/app/models/exercise.rb
+++ b/app/models/exercise.rb
@@ -11,7 +11,6 @@ class Exercise < ActiveRecord::Base
belongs_to :execution_environment
has_many :submissions
- belongs_to :team
has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions
has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
diff --git a/app/models/external_user.rb b/app/models/external_user.rb
index 8038d2dc..538f4997 100644
--- a/app/models/external_user.rb
+++ b/app/models/external_user.rb
@@ -7,7 +7,9 @@ class ExternalUser < ActiveRecord::Base
def displayname
result = name
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
result
end
diff --git a/app/models/file_template.rb b/app/models/file_template.rb
new file mode 100644
index 00000000..ef068e13
--- /dev/null
+++ b/app/models/file_template.rb
@@ -0,0 +1,10 @@
+class FileTemplate < ActiveRecord::Base
+
+ belongs_to :file_type
+
+
+ def to_s
+ name
+ end
+
+end
diff --git a/app/models/file_type.rb b/app/models/file_type.rb
index 53bf18dc..d3b519d5 100644
--- a/app/models/file_type.rb
+++ b/app/models/file_type.rb
@@ -12,6 +12,7 @@ class FileType < ActiveRecord::Base
has_many :execution_environments
has_many :files
+ has_many :file_templates
validates :binary, boolean_presence: true
validates :editor_mode, presence: true, unless: :binary?
diff --git a/app/models/internal_user.rb b/app/models/internal_user.rb
index e5cebde9..8f1bf04b 100644
--- a/app/models/internal_user.rb
+++ b/app/models/internal_user.rb
@@ -3,8 +3,6 @@ class InternalUser < ActiveRecord::Base
authenticates_with_sorcery!
- has_and_belongs_to_many :teams
-
validates :email, presence: true, uniqueness: true
validates :password, confirmation: true, if: :password_void?, on: :update, presence: true
validates :role, inclusion: {in: ROLES}
diff --git a/app/models/request_for_comment.rb b/app/models/request_for_comment.rb
index aca99938..63d932fc 100644
--- a/app/models/request_for_comment.rb
+++ b/app/models/request_for_comment.rb
@@ -1,7 +1,8 @@
class RequestForComment < ActiveRecord::Base
+ include Creation
+ belongs_to :submission
belongs_to :exercise
belongs_to :file, class_name: 'CodeOcean::File'
- belongs_to :user, polymorphic: true
before_create :set_requested_timestamp
@@ -13,10 +14,8 @@ class RequestForComment < ActiveRecord::Base
self.requested_at = Time.now
end
- def submission
- Submission.find(file.context_id)
- end
-
+ # not used right now, finds the last submission for the respective user and exercise.
+ # might be helpful to check whether the exercise has been solved in the meantime.
def last_submission
Submission.find_by_sql(" select * from submissions
where exercise_id = #{exercise_id} AND
@@ -25,12 +24,27 @@ class RequestForComment < ActiveRecord::Base
limit 1").first
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
"RFC-" + self.id.to_s
end
private
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
diff --git a/app/models/submission.rb b/app/models/submission.rb
index 323f1d58..5a95587f 100644
--- a/app/models/submission.rb
+++ b/app/models/submission.rb
@@ -2,7 +2,7 @@ class Submission < ActiveRecord::Base
include Context
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}'
belongs_to :exercise
@@ -28,13 +28,17 @@ class Submission < ActiveRecord::Base
ancestors.merge(descendants).values
end
- [:download, :render, :run, :test].each do |action|
+ [:download_file, :render, :run, :test].each do |action|
filename = FILENAME_URL_PLACEHOLDER.gsub(/\W/, '')
define_method("#{action}_url") do
Rails.application.routes.url_helpers.send(:"#{action}_submission_path", self, filename).sub(filename, FILENAME_URL_PLACEHOLDER)
end
end
+ def download_url
+ Rails.application.routes.url_helpers.send(:download_submission_path, self)
+ end
+
def main_file
collect_files.detect(&:main_file?)
end
diff --git a/app/models/team.rb b/app/models/team.rb
deleted file mode 100644
index a0dcb8d6..00000000
--- a/app/models/team.rb
+++ /dev/null
@@ -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
diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb
index 29a1c570..55f7d16b 100644
--- a/app/policies/exercise_policy.rb
+++ b/app/policies/exercise_policy.rb
@@ -13,24 +13,19 @@ class ExercisePolicy < AdminOrAuthorPolicy
end
[:clone?, :destroy?, :edit?, :statistics?, :update?].each do |action|
- define_method(action) { admin? || author? || team_member? }
+ define_method(action) { admin? || author?}
end
[:implement?, :submit?, :reload?].each do |action|
define_method(action) { everyone }
end
- def team_member?
- @record.team.try(:members, []).include?(@user) if @record.team
- end
- private :team_member?
-
class Scope < Scope
def resolve
if @user.admin?
@scope.all
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
@scope.none
end
diff --git a/app/policies/file_template_policy.rb b/app/policies/file_template_policy.rb
new file mode 100644
index 00000000..92ced442
--- /dev/null
+++ b/app/policies/file_template_policy.rb
@@ -0,0 +1,11 @@
+class FileTemplatePolicy < AdminOnlyPolicy
+
+ def show?
+ everyone
+ end
+
+ def by_file_type?
+ everyone
+ end
+
+end
diff --git a/app/policies/request_for_comment_policy.rb b/app/policies/request_for_comment_policy.rb
index cf252338..f592e3bd 100644
--- a/app/policies/request_for_comment_policy.rb
+++ b/app/policies/request_for_comment_policy.rb
@@ -1,5 +1,8 @@
class RequestForCommentPolicy < ApplicationPolicy
-
+ def author?
+ @user == @record.author
+ end
+ private :author?
def create?
everyone
@@ -13,6 +16,10 @@ class RequestForCommentPolicy < ApplicationPolicy
define_method(action) { admin? }
end
+ def mark_as_solved?
+ admin? || author?
+ end
+
def edit?
admin?
end
diff --git a/app/policies/submission_policy.rb b/app/policies/submission_policy.rb
index 18d39f4c..861f5695 100644
--- a/app/policies/submission_policy.rb
+++ b/app/policies/submission_policy.rb
@@ -8,7 +8,7 @@ class SubmissionPolicy < ApplicationPolicy
everyone
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? }
end
diff --git a/app/policies/team_policy.rb b/app/policies/team_policy.rb
deleted file mode 100644
index 0ab6a300..00000000
--- a/app/policies/team_policy.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class TeamPolicy < ApplicationPolicy
- [:create?, :index?, :new?].each do |action|
- define_method(action) { admin? || teacher? }
- end
-
- [:destroy?, :edit?, :show?, :update?].each do |action|
- define_method(action) { admin? || member? }
- end
-
- def member?
- @record.members.include?(@user)
- end
- private :member?
-end
diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim
index 4ab39e30..8aa289d3 100644
--- a/app/views/application/_navigation.html.slim
+++ b/app/views/application/_navigation.html.slim
@@ -8,7 +8,7 @@
- if current_user.admin?
li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path)
li.divider
- - models = [ExecutionEnvironment, Exercise, Consumer, CodeHarborLink, ExternalUser, FileType, InternalUser, Submission, Team].sort_by { |model| model.model_name.human(count: 2) }
+ - models = [ExecutionEnvironment, Exercise, Consumer, CodeHarborLink, ExternalUser, FileType, FileTemplate, InternalUser, Submission].sort_by { |model| model.model_name.human(count: 2) }
- models.each do |model|
- if policy(model).index?
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
diff --git a/app/views/code_ocean/files/_form.html.slim b/app/views/code_ocean/files/_form.html.slim
index 07dd3355..46c5b2c2 100644
--- a/app/views/code_ocean/files/_form.html.slim
+++ b/app/views/code_ocean/files/_form.html.slim
@@ -8,5 +8,9 @@
.form-group
= f.label(:file_type_id, t('activerecord.attributes.file.file_type_id'))
= f.collection_select(:file_type_id, FileType.where(binary: false).order(:name), :id, :name, {selected: @exercise.execution_environment.file_type.try(:id)}, class: 'form-control')
+ .form-group
+ = f.label(:file_template_id, t('activerecord.attributes.file.file_template_id'))
+ = f.collection_select(:file_template_id, FileTemplate.all.order(:name), :id, :name, {:include_blank => true}, class: 'form-control')
= f.hidden_field(:context_id)
+ .hidden#noTemplateLabel data-text=t('file_template.no_template_label')
.actions = render('shared/submit_button', f: f, object: CodeOcean::File.new)
diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim
index 42b12e42..0ce5b73f 100644
--- a/app/views/exercises/_editor.html.slim
+++ b/app/views/exercises/_editor.html.slim
@@ -38,4 +38,4 @@
= 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('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')
\ No newline at end of file
+= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')
diff --git a/app/views/exercises/_editor_frame.html.slim b/app/views/exercises/_editor_frame.html.slim
index eacc62a9..01640fa8 100644
--- a/app/views/exercises/_editor_frame.html.slim
+++ b/app/views/exercises/_editor_frame.html.slim
@@ -14,6 +14,6 @@
.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
- button.btn.btn-primary.requestCommentsButton type='button'
- i.fa.fa-comment-o
+ button.btn.btn-primary.requestCommentsButton type='button' id="requestComments"
+ i.fa.fa-comment
= t('exercises.editor.requestComments')
\ No newline at end of file
diff --git a/app/views/exercises/_file_form.html.slim b/app/views/exercises/_file_form.html.slim
index c737f068..7bb4cd27 100644
--- a/app/views/exercises/_file_form.html.slim
+++ b/app/views/exercises/_file_form.html.slim
@@ -1,10 +1,10 @@
- id = f.object.id
li.panel.panel-default
.panel-heading role="tab" id="heading"
- div.clearfix role="button"
- span = f.object.name
- a.pull-right data-toggle="collapse" data-parent="#files" href="#collapse#{id}" collapse
- .panel-collapse.collapse.in id="collapse#{id}" role="tabpanel"
+ a.file-heading data-toggle="collapse" data-parent="#files" href="#collapse#{id}"
+ div.clearfix role="button"
+ span = f.object.name
+ .panel-collapse.collapse-in id="collapse#{id}" role="tabpanel"
.panel-body
.clearfix = link_to(t('shared.destroy'), '#', class:'btn btn-warning btn-sm discard-file pull-right')
.form-group
diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim
index f0f69f7a..968540af 100644
--- a/app/views/exercises/_form.html.slim
+++ b/app/views/exercises/_form.html.slim
@@ -17,9 +17,6 @@
= f.label(:instructions)
= f.hidden_field(:instructions)
.form-control.markdown
- /.form-group
- = f.label(:team_id)
- = f.collection_select(:team_id, @teams, :id, :name, {include_blank: true}, class: 'form-control')
.checkbox
label
= f.check_box(:public)
diff --git a/app/views/exercises/external_users/statistics.html.slim b/app/views/exercises/external_users/statistics.html.slim
index 56bd614b..3c755be6 100644
--- a/app/views/exercises/external_users/statistics.html.slim
+++ b/app/views/exercises/external_users/statistics.html.slim
@@ -50,9 +50,9 @@ h1 = "#{@exercise} (external user #{@external_user})"
td
-submission.testruns.each do |run|
- if run.passed
- .unit-test-result.positive-result
+ .unit-test-result.positive-result title=run.output
- 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
-working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0))
p = t('.addendum')
diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim
index 728492b6..0c5e109b 100644
--- a/app/views/exercises/implement.html.slim
+++ b/app/views/exercises/implement.html.slim
@@ -78,6 +78,9 @@
br
- 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'))
+ - 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
#questions-column
#questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}"
diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim
index 97847f1f..ae018e82 100644
--- a/app/views/exercises/index.html.slim
+++ b/app/views/exercises/index.html.slim
@@ -27,7 +27,7 @@ h1 = Exercise.model_name.human(count: 2)
- @exercises.each do |exercise|
tr data-id=exercise.id
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 = exercise.files.teacher_defined_tests.count
td = exercise.maximum_score
diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim
index fc28272a..5c554da8 100644
--- a/app/views/exercises/show.html.slim
+++ b/app/views/exercises/show.html.slim
@@ -12,7 +12,6 @@ h1
= 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.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.public', value: @exercise.public?)
= row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?)
@@ -23,13 +22,15 @@ h1
h2 = t('activerecord.attributes.exercise.files')
ul.list-unstyled.panel-group#files
- - @exercise.files.each do |file|
+ - @exercise.files.order('name').each do |file|
li.panel.panel-default
.panel-heading role="tab" id="heading"
- div.clearfix role="button"
- span.panel-title = file.name_with_extension
- a.pull-right data-toggle="collapse" data-parent="#files" href="#collapse#{file.id}" collapse
- .panel-collapse.collapse.in id="collapse#{file.id}" role="tabpanel"
+ a.file-heading data-toggle="collapse" data-parent="#files" href=".collapse#{file.id}"
+ div.clearfix role="button"
+ span = file.name_with_extension
+ // 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
- 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)
diff --git a/app/views/file_templates/_form.html.slim b/app/views/file_templates/_form.html.slim
new file mode 100644
index 00000000..1a5c34bc
--- /dev/null
+++ b/app/views/file_templates/_form.html.slim
@@ -0,0 +1,12 @@
+= form_for(@file_template) do |f|
+ = render('shared/form_errors', object: @file_template)
+ .form-group
+ = f.label(:name)
+ = f.text_field(:name, class: 'form-control', required: true)
+ .form-group
+ = f.label(:file_type_id)
+ = f.collection_select(:file_type_id, FileType.all.order(:name), :id, :name, {}, class: 'form-control')
+ .form-group
+ = f.label(:content)
+ = f.text_area(:content, class: 'form-control')
+ .actions = render('shared/submit_button', f: f, object: @file_template)
diff --git a/app/views/file_templates/edit.html.slim b/app/views/file_templates/edit.html.slim
new file mode 100644
index 00000000..c198271f
--- /dev/null
+++ b/app/views/file_templates/edit.html.slim
@@ -0,0 +1,3 @@
+h1 = @file_template
+
+= render('form')
diff --git a/app/views/file_templates/index.html.slim b/app/views/file_templates/index.html.slim
new file mode 100644
index 00000000..3022ea53
--- /dev/null
+++ b/app/views/file_templates/index.html.slim
@@ -0,0 +1,20 @@
+h1 = FileTemplate.model_name.human(count: 2)
+
+.table-responsive
+ table.table
+ thead
+ tr
+ th = t('activerecord.attributes.file_template.name')
+ th = t('activerecord.attributes.file_template.file_type')
+ th colspan=3 = t('shared.actions')
+ tbody
+ - @file_templates.each do |file_template|
+ tr
+ td = file_template.name
+ td = link_to(file_template.file_type, file_type_path(file_template.file_type))
+ td = link_to(t('shared.show'), file_template)
+ td = link_to(t('shared.edit'), edit_file_template_path(file_template))
+ td = link_to(t('shared.destroy'), file_template, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
+
+= render('shared/pagination', collection: @file_templates)
+p = render('shared/new_button', model: FileTemplate)
diff --git a/app/views/file_templates/new.html.slim b/app/views/file_templates/new.html.slim
new file mode 100644
index 00000000..bf434860
--- /dev/null
+++ b/app/views/file_templates/new.html.slim
@@ -0,0 +1,3 @@
+h1 = t('shared.new_model', model: FileTemplate.model_name.human)
+
+= render('form')
diff --git a/app/views/file_templates/show.html.slim b/app/views/file_templates/show.html.slim
new file mode 100644
index 00000000..19f0d28f
--- /dev/null
+++ b/app/views/file_templates/show.html.slim
@@ -0,0 +1,7 @@
+h1
+ = @file_template
+ = render('shared/edit_button', object: @file_template)
+
+= row(label: 'file_template.name', value: @file_template.name)
+= row(label: 'file_template.file_type', value: link_to(@file_template.file_type, file_type_path(@file_template.file_type)))
+= row(label: 'file_template.content', value: @file_template.content)
diff --git a/app/views/request_for_comments/index.html.slim b/app/views/request_for_comments/index.html.slim
index 6d4e059c..b7ada0a2 100644
--- a/app/views/request_for_comments/index.html.slim
+++ b/app/views/request_for_comments/index.html.slim
@@ -4,19 +4,29 @@ h1 = RequestForComment.model_name.human(count: 2)
table.table.sortable
thead
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.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.requested_at')
tbody
- @request_for_comments.each do |request_for_comment|
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)
- if request_for_comment.has_attribute?(:question) && request_for_comment.question
td = truncate(request_for_comment.question, length: 200)
- else
td = '-'
+ td = request_for_comment.comments_count
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)
\ No newline at end of file
diff --git a/app/views/request_for_comments/show.html.erb b/app/views/request_for_comments/show.html.erb
index ab53ebd0..4089d878 100644
--- a/app/views/request_for_comments/show.html.erb
+++ b/app/views/request_for_comments/show.html.erb
@@ -1,46 +1,76 @@
-
<%= Exercise.find(@request_for_comment.exercise_id) %>
+
<%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %>
<%
user = @request_for_comment.user
- submission_id = ActiveRecord::Base.connection.execute("select id from submissions
- 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)
+ submission = @request_for_comment.submission
%>
- <%= user %> | <%= @request_for_comment.requested_at %>
+ <%= user.displayname %> | <%= @request_for_comment.created_at.localtime %>
+
+ <%= t('activerecord.attributes.exercise.description') %>: "<%= render_markdown(@request_for_comment.exercise.description) %>"
+
+
<% if @request_for_comment.question and not @request_for_comment.question == '' %>
- <%= t('activerecord.attributes.request_for_comments.question')%>: "<%= @request_for_comment.question %>"
+ <%= t('activerecord.attributes.request_for_comments.question')%>: "<%= @request_for_comment.question %>"
<% else %>
- <%= t('request_for_comments.no_question') %>
+ <%= t('activerecord.attributes.request_for_comments.question')%>: <%= t('request_for_comments.no_question') %>
<% end %>
+ <% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %>
+
+ <% elsif (@request_for_comment.solved?) %>
+
+ <% else %>
+
+ <% end %>
<% submission.files.each do |file| %>
<%= (file.path or "") + "/" + file.name + file.file_type.file_extension %>
-