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 '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'
end

View File

@ -195,6 +195,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)
@ -324,6 +326,7 @@ GEM
json (>= 1.8.0)
unf (0.1.4)
unf_ext
unf (0.1.4-java)
unf_ext (0.0.7.1)
unicode-display_width (0.3.1)
web-console (2.3.0)
@ -383,6 +386,7 @@ DEPENDENCIES
pry
puma (~> 2.15.3)
pundit
rack-mini-profiler
rails (~> 4.1.13)
rails-i18n (~> 4.0.0)
rake
@ -393,6 +397,7 @@ DEPENDENCIES
rubocop
rubocop-rspec
rubytree
rubyzip
sass-rails (~> 4.0.3)
sdoc (~> 0.4.0)
selenium-webdriver

View File

@ -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;
@ -175,7 +179,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;
});
};
@ -404,12 +411,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 +470,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();
};
@ -587,7 +606,7 @@ $(function() {
} else if (output.stdout) {
//if (output_mode_is_streaming){
element.addClass('text-success').append(output.stdout);
flowrOutputBuffer += output.stdout;
// flowrOutputBuffer += output.stdout;
//}else{
// element.addClass('text-success');
// element.data('content_buffer' , element.data('content_buffer') + output.stdout);
@ -704,9 +723,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);
};
@ -870,7 +895,9 @@ $(function() {
}
var showWorkspaceTab = function(event) {
event.preventDefault();
if(event){
event.preventDefault();
}
showTab(0);
};
@ -1030,6 +1057,7 @@ $(function() {
case 'exit':
killWebsocketAndContainer();
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 +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 element = findOrCreateRenderElement(0);
element.append(msg.data);
@ -1154,25 +1217,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 +1264,7 @@ $(function() {
if ($('#editor').isPresent()) {
if (isBrowserSupported()) {
initializeRegexes();
initializeCodePilot();
$('.score, #development-environment').show();
configureEditors();

View File

@ -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<submissions_length;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"){
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");
}
}

View File

@ -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() {

View File

@ -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();

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
[: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

View File

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

View File

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

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?
li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path)
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|
- if policy(model).index?
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}" &#9788
.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)

View File

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

View File

@ -1,46 +1,76 @@
<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">
<%
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.displayname %> | <%= @request_for_comment.requested_at %>
<%= user.displayname %> | <%= @request_for_comment.created_at.localtime %>
</p>
<h5>
<u><%= t('activerecord.attributes.exercise.description') %>:</u> "<%= render_markdown(@request_for_comment.exercise.description) %>"
</h5>
<h5>
<% 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 %>
<%= t('request_for_comments.no_question') %>
<u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> <%= t('request_for_comments.no_question') %>
<% end %>
</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>
<!--
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| %>
<%= (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>
<% end %>
<%= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') %>
<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 userid = commentitor.data('user-id');
commentitor.each(function (index, editor) {
var currentEditor = ace.edit(editor);
currentEditor.setReadOnly(true);
// set editor mode (used for syntax highlighting
currentEditor.getSession().setMode($(editor).data('mode'));
setAnnotations(currentEditor, $(editor).data('file-id'));
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,
row: row,
column: 0,
text: commenttext
text: commenttext,
request_id: $('h4#exercise_caption').data('rfc-id')
}
},
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
maximum_score: Erreichbare Punktzahl
public: Öffentlich
team: Team
team_id: Team
title: Titel
user: Autor
allow_file_creation: "Dateierstellung erlauben"
@ -79,6 +77,7 @@ de:
password_confirmation: Passwort-Bestätigung
role: Rolle
request_for_comments:
comments: Kommentare
exercise: Aufgabe
execution_environment: Sprache
username: Benutzername
@ -91,9 +90,6 @@ de:
files: Dateien
score: Punktzahl
user: Autor
team:
internal_user_ids: Mitglieder
name: Name
file_template:
name: "Name"
file_type: "Dateityp"
@ -135,9 +131,6 @@ de:
submission:
one: Abgabe
other: Abgaben
team:
one: Team
other: Teams
errors:
messages:
together: 'muss zusammen mit %{attribute} definiert werden'
@ -169,6 +162,7 @@ de:
show:
link: Konsument
errors:
connection_refused: Verbindung abgelehnt
index:
count: Anzahl
execution_environments:
@ -217,8 +211,10 @@ de:
submit: Code zur Bewertung abgeben
test: Testen
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:
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."
editor_file_tree:
file_root: Dateien
@ -331,14 +327,38 @@ de:
activation_needed:
body: 'Bitte besuchen Sie %{link} und wählen Sie ein Passwort, um Ihre Registrierung abzuschließen.'
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:
body: 'Bitte besuchen Sie %{link}, sofern Sie Ihr Passwort zurücksetzen wollen.'
subject: Anweisungen zum Zurücksetzen Ihres Passworts
request_for_comments:
comments: Kommentare
index:
get_my_comment_requests: Meine Kommentaranfragen
all: "Alle Kommentaranfragen"
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:
create:
failure: Fehlerhafte E-Mail oder Passwort.

View File

@ -34,8 +34,6 @@ en:
instructions: Instructions
maximum_score: Maximum Score
public: Public
team: Team
team_id: Team
title: Title
user: Author
allow_file_creation: "Allow file creation"
@ -79,6 +77,7 @@ en:
password_confirmation: Passwort Confirmation
role: Role
request_for_comments:
comments: Comments
exercise: Exercise
execution_environment: Language
username: Username
@ -91,9 +90,6 @@ en:
files: Files
score: Score
user: Author
team:
internal_user_ids: Members
name: Name
file_template:
name: "Name"
file_type: "File Type"
@ -135,9 +131,6 @@ en:
submission:
one: Submission
other: Submissions
team:
one: Team
other: Teams
errors:
messages:
together: 'has to be set along with %{attribute}'
@ -169,6 +162,7 @@ en:
show:
link: Consumer
errors:
connection_refused: Connection refused
index:
count: Count
execution_environments:
@ -217,8 +211,10 @@ en:
submit: Submit Code For Assessment
test: Test
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:
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."
editor_file_tree:
file_root: Files
@ -331,14 +327,38 @@ en:
activation_needed:
body: 'Please visit %{link} and set up a password in order to 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:
body: 'Please visit %{link} if you want to reset your password.'
subject: Password reset instructions
request_for_comments:
comments: Comments
index:
all: All Requests for Comments
get_my_comment_requests: My Requests for Comments
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:
create:
failure: Invalid email or password.

View File

@ -7,13 +7,17 @@ Rails.application.routes.draw do
end
end
resources :code_harbor_links
resources :request_for_comments
get '/my_request_for_comments', as: 'my_request_for_comments', to: 'request_for_comments#get_my_comment_requests'
resources :request_for_comments do
member do
get :mark_as_solved
end
end
resources :comments, except: [:destroy] do
collection do
delete :destroy
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'
put '/comments', to: 'comments#update'
@ -90,7 +94,8 @@ Rails.application.routes.draw do
resources :submissions, only: [:create, :index, :show] 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 'run/:filename', as: :run, constraints: {filename: FILENAME_REGEXP}, to: :run
get :score
@ -100,5 +105,4 @@ Rails.application.routes.draw do
end
end
resources :teams
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.
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
enable_extension "plpgsql"
@ -87,7 +87,6 @@ ActiveRecord::Schema.define(version: 20160610111602) do
t.boolean "public"
t.string "user_type"
t.string "token"
t.integer "team_id"
t.boolean "hide_file_tree"
t.boolean "allow_file_creation"
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", ["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|
t.integer "user_id", null: false
t.integer "exercise_id", null: false
t.integer "file_id", null: false
t.datetime "requested_at"
t.integer "user_id", null: false
t.integer "exercise_id", null: false
t.integer "file_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "user_type"
t.text "question"
t.boolean "solved"
t.integer "submission_id"
end
create_table "submissions", force: true do |t|
@ -211,12 +203,6 @@ ActiveRecord::Schema.define(version: 20160610111602) do
t.string "user_type"
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|
t.boolean "passed"
t.text "output"

View File

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

View File

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

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) }
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")
end
end
@ -14,7 +14,7 @@ describe 'Authorization' do
let(:user) { FactoryGirl.create(:external_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")
end
end
@ -27,7 +27,7 @@ describe 'Authorization' do
expect_forbidden_path(:"new_#{model.model_name.singular}_path")
end
[ExecutionEnvironment, Exercise, FileType, Team].each do |model|
[ExecutionEnvironment, Exercise, FileType].each do |model|
expect_permitted_path(:"new_#{model.model_name.singular}_path")
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,7 +3,7 @@ require 'rails_helper'
describe ExercisePolicy do
subject { described_class }
let(:exercise) { FactoryGirl.build(:dummy, team: FactoryGirl.create(:team)) }
let(:exercise) { FactoryGirl.build(:dummy) }
permissions :batch_update? do
it 'grants access to admins only' do
@ -40,10 +40,6 @@ describe ExercisePolicy do
expect(subject).to permit(exercise.author, exercise)
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
[:external_user, :teacher].each do |factory_name|
expect(subject).not_to permit(FactoryGirl.build(factory_name), exercise)
@ -71,9 +67,7 @@ describe ExercisePolicy do
[@admin, @teacher].each do |user|
[true, false].each do |public|
[@team, nil].each do |team|
FactoryGirl.create(:dummy, public: public, team: team, user_id: user.id, user_type: InternalUser.class.name)
end
FactoryGirl.create(:dummy, public: public, user_id: user.id, user_type: InternalUser.class.name)
end
end
end
@ -95,10 +89,6 @@ describe ExercisePolicy do
end
context 'for teachers' do
before(:each) do
@team = FactoryGirl.create(:team)
@team.members << @teacher
end
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))
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
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

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