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:
Ralf Teusner
2016-07-28 15:16:11 +02:00
60 changed files with 471 additions and 491 deletions

View File

@ -38,6 +38,7 @@ gem 'faye-websocket'
gem 'nokogiri' gem 'nokogiri'
gem 'd3-rails' gem 'd3-rails'
gem 'rest-client' gem 'rest-client'
gem 'rubyzip'
group :development do group :development do
gem 'better_errors', platform: :ruby gem 'better_errors', platform: :ruby
@ -47,6 +48,7 @@ group :development do
gem 'capistrano-rails' gem 'capistrano-rails'
gem 'capistrano-rvm' gem 'capistrano-rvm'
gem 'capistrano-upload-config' gem 'capistrano-upload-config'
gem 'rack-mini-profiler'
gem 'rubocop', require: false gem 'rubocop', require: false
gem 'rubocop-rspec' gem 'rubocop-rspec'
end end

View File

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

View File

@ -19,6 +19,10 @@ $(function() {
var SERVER_SEND_EVENT = 2; var SERVER_SEND_EVENT = 2;
var editors = []; var editors = [];
var editor_for_file = new Map();
var regex_for_language = new Map();
var tracepositions_regex;
var active_file = undefined; var active_file = undefined;
var active_frame = undefined; var active_frame = undefined;
var running = false; var running = false;
@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -46,7 +46,11 @@ class CommentsController < ApplicationController
# POST /comments # POST /comments
# POST /comments.json # POST /comments.json
def create def create
@comment = Comment.new(comment_params) @comment = Comment.new(comment_params_without_request_id)
if comment_params[:request_id]
UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user)
end
respond_to do |format| respond_to do |format|
if @comment.save if @comment.save
@ -64,7 +68,7 @@ class CommentsController < ApplicationController
# PATCH/PUT /comments/1.json # PATCH/PUT /comments/1.json
def update def update
respond_to do |format| respond_to do |format|
if @comment.update(comment_params) if @comment.update(comment_params_without_request_id)
format.html { head :no_content, notice: 'Comment was successfully updated.' } format.html { head :no_content, notice: 'Comment was successfully updated.' }
format.json { render :show, status: :ok, location: @comment } format.json { render :show, status: :ok, location: @comment }
else else
@ -101,10 +105,14 @@ class CommentsController < ApplicationController
@comment = Comment.find(params[:id]) @comment = Comment.find(params[:id])
end end
def comment_params_without_request_id
comment_params.except :request_id
end
# Never trust parameters from the scary internet, only allow the white list through. # Never trust parameters from the scary internet, only allow the white list through.
def comment_params def comment_params
#params.require(:comment).permit(:user_id, :file_id, :row, :column, :text) #params.require(:comment).permit(:user_id, :file_id, :row, :column, :text)
# fuer production mode, damit böse menschen keine falsche user_id uebergeben: # fuer production mode, damit böse menschen keine falsche user_id uebergeben:
params.require(:comment).permit(:file_id, :row, :column, :text).merge(user_id: current_user.id, user_type: current_user.class.name) params.require(:comment).permit(:file_id, :row, :column, :text, :request_id).merge(user_id: current_user.id, user_type: current_user.class.name)
end end
end end

View File

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

View File

@ -1,5 +1,5 @@
class RequestForCommentsController < ApplicationController class RequestForCommentsController < ApplicationController
before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy] before_action :set_request_for_comment, only: [:show, :edit, :update, :destroy, :mark_as_solved]
skip_after_action :verify_authorized skip_after_action :verify_authorized
@ -20,6 +20,18 @@ class RequestForCommentsController < ApplicationController
render 'index' render 'index'
end end
def mark_as_solved
authorize!
@request_for_comment.solved = true
respond_to do |format|
if @request_for_comment.save
format.json { render :show, status: :ok, location: @request_for_comment }
else
format.json { render json: @request_for_comment.errors, status: :unprocessable_entity }
end
end
end
# GET /request_for_comments/1 # GET /request_for_comments/1
# GET /request_for_comments/1.json # GET /request_for_comments/1.json
def show def show
@ -70,6 +82,6 @@ class RequestForCommentsController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through. # Never trust parameters from the scary internet, only allow the white list through.
def request_for_comment_params def request_for_comment_params
params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at).merge(user_id: current_user.id, user_type: current_user.class.name) params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name)
end end
end end

View File

@ -6,9 +6,9 @@ class SubmissionsController < ApplicationController
include SubmissionScoring include SubmissionScoring
include Tubesock::Hijack include Tubesock::Hijack
before_action :set_submission, only: [:download_file, :render_file, :run, :score, :show, :statistics, :stop, :test] before_action :set_submission, only: [:download, :download_file, :render_file, :run, :score, :show, :statistics, :stop, :test]
before_action :set_docker_client, only: [:run, :test] before_action :set_docker_client, only: [:run, :test]
before_action :set_files, only: [:download_file, :render_file, :show] before_action :set_files, only: [:download, :download_file, :render_file, :show]
before_action :set_file, only: [:download_file, :render_file] before_action :set_file, only: [:download_file, :render_file]
before_action :set_mime_type, only: [:download_file, :render_file] before_action :set_mime_type, only: [:download_file, :render_file]
skip_before_action :verify_authenticity_token, only: [:download_file, :render_file] skip_before_action :verify_authenticity_token, only: [:download_file, :render_file]
@ -53,6 +53,20 @@ class SubmissionsController < ApplicationController
end end
end end
def download
# files = @submission.files.map{ }
# zipline( files, 'submission.zip')
# send_data(@file.content, filename: @file.name_with_extension)
require 'zip'
stringio = Zip::OutputStream.write_buffer do |zio|
@files.each do |file|
zio.put_next_entry(file.name_with_extension)
zio.write(file.content)
end
end
send_data(stringio.string, filename: @submission.exercise.title.tr(" ", "_") + ".zip")
end
def download_file def download_file
if @file.native_file? if @file.native_file?
send_file(@file.native_file.path) send_file(@file.native_file.path)

View File

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

View File

@ -11,4 +11,13 @@ class UserMailer < ActionMailer::Base
@reset_password_url = reset_password_internal_user_url(user, token: user.reset_password_token) @reset_password_url = reset_password_internal_user_url(user, token: user.reset_password_token)
mail(subject: t('mailers.user_mailer.reset_password.subject'), to: user.email) mail(subject: t('mailers.user_mailer.reset_password.subject'), to: user.email)
end end
def got_new_comment(comment, request_for_comment, commenting_user)
# todo: check whether we can take the last known locale of the receiver?
@receiver_displayname = request_for_comment.user.displayname
@commenting_user_displayname = commenting_user.displayname
@comment_text = comment.text
@rfc_link = request_for_comment_url(request_for_comment)
mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email).deliver
end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
- if current_user.admin? - if current_user.admin?
li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path) li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path)
li.divider li.divider
- models = [ExecutionEnvironment, Exercise, Consumer, CodeHarborLink, ExternalUser, FileType, 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"))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,7 @@ h1 = Exercise.model_name.human(count: 2)
- @exercises.each do |exercise| - @exercises.each do |exercise|
tr data-id=exercise.id tr data-id=exercise.id
td = exercise.title td = exercise.title
td = link_to_if(policy(exercise.author).show?, exercise.author, exercise.author) td = link_to_if(exercise.author && policy(exercise.author).show?, exercise.author, exercise.author)
td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
td = exercise.files.teacher_defined_tests.count td = exercise.files.teacher_defined_tests.count
td = exercise.maximum_score td = exercise.maximum_score

View File

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

View File

@ -4,19 +4,29 @@ h1 = RequestForComment.model_name.human(count: 2)
table.table.sortable table.table.sortable
thead thead
tr tr
th
i class="fa fa-lightbulb-o" aria-hidden="true" title = t('request_for_comments.solved') align="right"
th = t('activerecord.attributes.request_for_comments.exercise') th = t('activerecord.attributes.request_for_comments.exercise')
th = t('activerecord.attributes.request_for_comments.question') th = t('activerecord.attributes.request_for_comments.question')
th
i class="fa fa-comment" aria-hidden="true" title = t('request_for_comments.comments') align="center"
th = t('activerecord.attributes.request_for_comments.username') th = t('activerecord.attributes.request_for_comments.username')
th = t('activerecord.attributes.request_for_comments.requested_at') th = t('activerecord.attributes.request_for_comments.requested_at')
tbody tbody
- @request_for_comments.each do |request_for_comment| - @request_for_comments.each do |request_for_comment|
tr data-id=request_for_comment.id tr data-id=request_for_comment.id
- if request_for_comment.solved?
td
span class="fa fa-check" aria-hidden="true"
- else
td = ''
td = link_to(request_for_comment.exercise.title, request_for_comment) td = link_to(request_for_comment.exercise.title, request_for_comment)
- if request_for_comment.has_attribute?(:question) && request_for_comment.question - if request_for_comment.has_attribute?(:question) && request_for_comment.question
td = truncate(request_for_comment.question, length: 200) td = truncate(request_for_comment.question, length: 200)
- else - else
td = '-' td = '-'
td = request_for_comment.comments_count
td = request_for_comment.user.displayname td = request_for_comment.user.displayname
td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.requested_at)) td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.created_at))
= render('shared/pagination', collection: @request_for_comments) = render('shared/pagination', collection: @request_for_comments)

View File

@ -1,46 +1,76 @@
<div class="list-group"> <div class="list-group">
<h4 class="list-group-item-heading"><%= Exercise.find(@request_for_comment.exercise_id) %></h4> <h4 id ="exercise_caption" class="list-group-item-heading" data-rfc-id = "<%= @request_for_comment.id %>" ><%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %></h4>
<p class="list-group-item-text"> <p class="list-group-item-text">
<% <%
user = @request_for_comment.user user = @request_for_comment.user
submission_id = ActiveRecord::Base.connection.execute("select id from submissions submission = @request_for_comment.submission
where exercise_id =
#{@request_for_comment.exercise_id} AND
user_id = #{@request_for_comment.user_id} AND
'#{@request_for_comment.created_at}' > created_at
order by created_at desc
limit 1").first['id'].to_i
submission = Submission.find(submission_id)
%> %>
<%= user.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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,8 +34,6 @@ de:
instructions: Anweisungen instructions: Anweisungen
maximum_score: Erreichbare Punktzahl maximum_score: Erreichbare Punktzahl
public: Öffentlich public: Öffentlich
team: Team
team_id: Team
title: Titel title: Titel
user: Autor user: Autor
allow_file_creation: "Dateierstellung erlauben" allow_file_creation: "Dateierstellung erlauben"
@ -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.

View File

@ -34,8 +34,6 @@ en:
instructions: Instructions instructions: Instructions
maximum_score: Maximum Score maximum_score: Maximum Score
public: Public public: Public
team: Team
team_id: Team
title: Title title: Title
user: Author user: Author
allow_file_creation: "Allow file creation" allow_file_creation: "Allow file creation"
@ -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.

View File

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

View File

@ -0,0 +1,5 @@
class AddSolvedToRequestForComments < ActiveRecord::Migration
def change
add_column :request_for_comments, :solved, :boolean
end
end

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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