after merge

This commit is contained in:
yqbk
2016-08-03 11:22:39 +02:00
60 changed files with 457 additions and 423 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

View File

@ -397,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;
@ -37,6 +41,7 @@ $(function() {
var ENTER_KEY_CODE = 13;
var flowrOutputBuffer = "";
var QaApiOutputBuffer = {'stdout': '', 'stderr': ''};
var flowrResultHtml = '<div class="panel panel-default"><div id="{{headingId}}" role="tab" class="panel-heading"><h4 class="panel-title"><a data-toggle="collapse" data-parent="#flowrHint" href="#{{collapseId}}" aria-expanded="true" aria-controls="{{collapseId}}"></a></h4></div><div id="{{collapseId}}" role="tabpanel" aria-labelledby="{{headingId}}" class="panel-collapse collapse"><div class="panel-body"></div></div></div>'
var ajax = function(options) {
@ -175,7 +180,10 @@ $(function() {
var downloadCode = function(event) {
event.preventDefault();
createSubmission(this, null,function(response) {
var url = response.download_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
var url = response.download_url;
// to download just a single file, use the following url
//var url = response.download_file_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
window.location = url;
});
};
@ -184,8 +192,8 @@ $(function() {
(streamed ? evaluateCodeWithStreamedResponse : evaluateCodeWithoutStreamedResponse)(url, callback);
};
var evaluateCodeWithStreamedResponse = function(url, callback) {
initWebsocketConnection(url);
var evaluateCodeWithStreamedResponse = function(url, onmessageFunction) {
initWebsocketConnection(url, onmessageFunction);
// TODO only init turtle when required
initTurtle();
@ -306,9 +314,10 @@ $(function() {
}
};
var handleScoringResponse = function(response) {
printScoringResults(response);
var score = _.reduce(response, function(sum, result) {
var handleScoringResponse = function(websocket_event) {
results = JSON.parse(websocket_event.data);
printScoringResults(results);
var score = _.reduce(results, function(sum, result) {
return sum + result.score * result.weight;
}, 0).toFixed(2);
$('#score').data('score', score);
@ -316,6 +325,14 @@ $(function() {
showTab(2);
};
var handleQaApiOutput = function() {
if (qa_api) {
qa_api.executeCommand('syncOutput', [[QaApiOutputBuffer]]);
// reset the object
}
QaApiOutputBuffer = {'stdout': '', 'stderr': ''};
}
// activate flowr only for half of the audience
var isFlowrEnabled = true;//parseInt($('#editor').data('user-id'))%2 == 0;
var handleStderrOutputForFlowr = function() {
@ -349,13 +366,14 @@ $(function() {
flowrOutputBuffer = '';
};
var handleTestResponse = function(response) {
var handleTestResponse = function(websocket_event) {
result = JSON.parse(websocket_event.data);
clearOutput();
printOutput(response[0], false, 0);
printOutput(result, false, 0);
if (qa_api) {
qa_api.executeCommand('syncOutput', [response]);
qa_api.executeCommand('syncOutput', [result]);
}
showStatus(response[0]);
showStatus(result);
showTab(1);
};
@ -404,12 +422,18 @@ $(function() {
editor.setTheme(THEME);
editor.commands.bindKey("ctrl+alt+0", null);
editors.push(editor);
editor_for_file.set($(element).parent().data('filename'), editor);
var session = editor.getSession();
session.setMode($(element).data('mode'));
session.setTabSize($(element).data('indent-size'));
session.setUseSoftTabs(true);
session.setUseWrapMode(true);
// set regex for parsing error traces based on the mode of the main file.
if( $(element).parent().data('role') == "main_file"){
tracepositions_regex = regex_for_language.get($(element).data('mode'));
}
var file_id = $(element).data('id');
/*
@ -457,6 +481,12 @@ $(function() {
$('#request-for-comments').on('click', requestComments);
};
var initializeRegexes = function(){
regex_for_language.set("ace/mode/python", /File "(.+?)", line (\d+)/g);
regex_for_language.set("ace/mode/java", /(.*\.java):(\d+):/g);
}
var initializeTooltips = function() {
$('[data-tooltip]').tooltip();
};
@ -527,8 +557,8 @@ $(function() {
};
var isBrowserSupported = function() {
// eventsource tests for server send events (used for scoring), websockets is used for run
return Modernizr.eventsource && Modernizr.websockets;
// websockets is used for run, score and test
return Modernizr.websockets;
};
var populatePanel = function(panel, result, index) {
@ -574,20 +604,23 @@ $(function() {
// output_mode_is_streaming = false;
//}
if (!colorize) {
if(output.stdout != ''){
if(output.stdout != undefined && output.stdout != ''){
element.append(output.stdout)
}
if(output.stderr != ''){
if(output.stderr != undefined && output.stderr != ''){
element.append('There was an error: StdErr: ' + output.stderr);
}
} else if (output.stderr) {
element.addClass('text-warning').append(output.stderr);
flowrOutputBuffer += output.stderr;
QaApiOutputBuffer.stderr += output.stderr;
} else if (output.stdout) {
//if (output_mode_is_streaming){
element.addClass('text-success').append(output.stdout);
flowrOutputBuffer += output.stdout;
QaApiOutputBuffer.stdout += output.stdout;
//}else{
// element.addClass('text-success');
// element.data('content_buffer' , element.data('content_buffer') + output.stdout);
@ -743,7 +776,7 @@ $(function() {
showSpinner($('#run'));
toggleButtonStates();
var url = response.run_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
evaluateCode(url, true, printChunk);
evaluateCode(url, true, function(evt) { parseCanvasMessage(evt.data, true); });
});
}
};
@ -778,7 +811,7 @@ $(function() {
createSubmission(this, null, function(response) {
showSpinner($('#assess'));
var url = response.score_url;
evaluateCode(url, false, handleScoringResponse);
evaluateCode(url, true, handleScoringResponse);
});
};
@ -876,7 +909,9 @@ $(function() {
}
var showWorkspaceTab = function(event) {
event.preventDefault();
if(event){
event.preventDefault();
}
showTab(0);
};
@ -955,7 +990,7 @@ $(function() {
createSubmission(this, null, function(response) {
showSpinner($('#test'));
var url = response.test_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
evaluateCode(url, false, handleTestResponse);
evaluateCode(url, true, handleTestResponse);
});
}
};
@ -974,14 +1009,14 @@ $(function() {
$('#test').toggle(isActiveFileTestable());
};
var initWebsocketConnection = function(url) {
var initWebsocketConnection = function(url, onmessageFunction) {
//TODO: get the protocol from config file dependent on environment. (dev: ws, prod: wss)
//causes: Puma::HttpParserError: Invalid HTTP format, parsing fails.
//TODO: make sure that this gets cached.
websocket = new WebSocket('<%= DockerClient.config['ws_client_protocol'] %>' + window.location.hostname + ':' + window.location.port + url);
websocket.onopen = function(evt) { resetOutputTab(); }; // todo show some kind of indicator for established connection
websocket.onclose = function(evt) { /* expected at some point */ };
websocket.onmessage = function(evt) { parseCanvasMessage(evt.data, true); };
websocket.onmessage = onmessageFunction;
websocket.onerror = function(evt) { showWebsocketError(); };
websocket.flush = function() { this.send('\n'); }
};
@ -1035,7 +1070,9 @@ $(function() {
break;
case 'exit':
killWebsocketAndContainer();
handleQaApiOutput();
handleStderrOutputForFlowr();
augmentStacktraceInOutput();
break;
case 'timeout':
// just show the timeout message here. Another exit command is sent by the rails backend when the socket to the docker container closes.
@ -1047,6 +1084,41 @@ $(function() {
}
};
var jumpToSourceLine = function(event){
var file = $(event.target).data('file');
var line = $(event.target).data('line');
showWorkspaceTab(null);
// set active file ?!?!
var frame = $('div.frame[data-filename="' + file + '"]');
showFrame(frame);
var editor = editor_for_file.get(file);
editor.gotoLine(line, 0);
};
var augmentStacktraceInOutput = function() {
if(tracepositions_regex){
var element = $('#output>pre');
var text = element.text();
element.on( "click", "a", jumpToSourceLine);
var matches;
while(matches = tracepositions_regex.exec(text)){
var frame = $('div.frame[data-filename="' + matches[1] + '"]')
if(frame.length > 0){
element.html(text.replace(matches[0], "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>"));
}
}
}
};
var renderWebsocketOutput = function(msg){
var element = findOrCreateRenderElement(0);
element.append(msg.data);
@ -1160,25 +1232,25 @@ $(function() {
var file_id = $('.editor').data('id')
var question = $('#question').val();
$.ajax({
method: 'POST',
url: '/request_for_comments',
data: {
request_for_comment: {
exercise_id: exercise_id,
file_id: file_id,
question: question,
"requested_at(1i)": 2015, // these are the timestamp values that the request handler demands
"requested_at(2i)":3, // they could be random here, because the timestamp is updated on serverside anyway
"requested_at(3i)":27,
"requested_at(4i)":17,
"requested_at(5i)":06
var createRequestForComments = function(submission) {
$.ajax({
method: 'POST',
url: '/request_for_comments',
data: {
request_for_comment: {
exercise_id: exercise_id,
file_id: file_id,
submission_id: submission.id,
question: question
}
}
}
}).done(function() {
hideSpinner();
$.flash.success({ text: $('#askForCommentsButton').data('message-success') })
}).error(ajaxError);
}).done(function() {
hideSpinner();
$.flash.success({ text: $('#askForCommentsButton').data('message-success') });
}).error(ajaxError);
}
createSubmission($('.requestCommentsButton'), null, createRequestForComments);
$('#comment-modal').modal('hide');
var button = $('.requestCommentsButton');
@ -1207,6 +1279,7 @@ $(function() {
if ($('#editor').isPresent()) {
if (isBrowserSupported()) {
initializeRegexes();
initializeCodePilot();
$('.score, #development-environment').show();
configureEditors();

View File

@ -151,6 +151,22 @@ $(function() {
});
};
var updateFileTemplates = function(fileType) {
var jqxhr = $.ajax({
url: '/file_templates/by_file_type/' + fileType + '.json',
dataType: 'json'
});
jqxhr.done(function(response) {
var noTemplateLabel = $('#noTemplateLabel').data('text');
var options = "<option value>" + noTemplateLabel + "</option>";
for (var i = 0; i < response.length; i++) {
options += "<option value='" + response[i].id + "'>" + response[i].name + "</option>"
}
$("#code_ocean_file_file_template_id").find('option').remove().end().append($(options));
});
jqxhr.fail(ajaxError);
}
if ($.isController('exercises')) {
if ($('table').isPresent()) {
enableBatchUpdate();
@ -165,6 +181,10 @@ $(function() {
inferFileAttributes();
observeFileRoleChanges();
overrideTextareaTabBehavior();
} else if ($('#files.jstree').isPresent()) {
var fileTypeSelect = $('#code_ocean_file_file_type_id');
fileTypeSelect.on("change", function() {updateFileTemplates(fileTypeSelect.val())});
updateFileTemplates(fileTypeSelect.val());
}
toggleCodeHeight();
if (window.hljs) {

View File

@ -0,0 +1,3 @@
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

View File

@ -0,0 +1,3 @@
// Place all the styles related to the FileTemplates controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View File

@ -10,6 +10,11 @@ module CodeOcean
def create
@file = CodeOcean::File.new(file_params)
if @file.file_template_id
content = FileTemplate.find(@file.file_template_id).content
content.sub! '{{file_name}}', @file.name
@file.content = content
end
authorize!
create_and_respond(object: @file, path: proc { implement_exercise_path(@file.context.exercise, tab: 2) })
end

View File

@ -1,6 +1,6 @@
module FileParameters
def file_attributes
%w(content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight)
%w(content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight file_template_id)
end
private :file_attributes
end

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

@ -0,0 +1,94 @@
class FileTemplatesController < ApplicationController
before_action :set_file_template, only: [:show, :edit, :update, :destroy]
def authorize!
authorize(@file_template || @file_templates)
end
private :authorize!
def by_file_type
@file_templates = FileTemplate.where(:file_type_id => params[:file_type_id])
authorize!
respond_to do |format|
format.json { render :show, status: :ok, json: @file_templates.to_json }
end
end
# GET /file_templates
# GET /file_templates.json
def index
@file_templates = FileTemplate.all.order(:file_type_id).paginate(page: params[:page])
authorize!
end
# GET /file_templates/1
# GET /file_templates/1.json
def show
authorize!
end
# GET /file_templates/new
def new
@file_template = FileTemplate.new
authorize!
end
# GET /file_templates/1/edit
def edit
authorize!
end
# POST /file_templates
# POST /file_templates.json
def create
@file_template = FileTemplate.new(file_template_params)
authorize!
respond_to do |format|
if @file_template.save
format.html { redirect_to @file_template, notice: 'File template was successfully created.' }
format.json { render :show, status: :created, location: @file_template }
else
format.html { render :new }
format.json { render json: @file_template.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /file_templates/1
# PATCH/PUT /file_templates/1.json
def update
authorize!
respond_to do |format|
if @file_template.update(file_template_params)
format.html { redirect_to @file_template, notice: 'File template was successfully updated.' }
format.json { render :show, status: :ok, location: @file_template }
else
format.html { render :edit }
format.json { render json: @file_template.errors, status: :unprocessable_entity }
end
end
end
# DELETE /file_templates/1
# DELETE /file_templates/1.json
def destroy
authorize!
@file_template.destroy
respond_to do |format|
format.html { redirect_to file_templates_url, notice: 'File template was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_file_template
@file_template = FileTemplate.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def file_template_params
params[:file_template].permit(:name, :file_type_id, :content)
end
end

View File

@ -82,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, :solved).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)
@ -214,7 +228,11 @@ class SubmissionsController < ApplicationController
end
def score
render(json: score_submission(@submission))
hijack do |tubesock|
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
# tubesock is the socket to the client
tubesock.send_data JSON.dump(score_submission(@submission))
end
end
def set_docker_client
@ -266,8 +284,14 @@ class SubmissionsController < ApplicationController
private :store_error
def test
output = @docker_client.execute_test_command(@submission, params[:filename])
render(json: [output])
hijack do |tubesock|
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
output = @docker_client.execute_test_command(@submission, params[:filename])
# tubesock is the socket to the client
tubesock.send_data JSON.dump(output)
end
end
def with_server_sent_events

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

@ -0,0 +1,10 @@
class FileTemplate < ActiveRecord::Base
belongs_to :file_type
def to_s
name
end
end

View File

@ -12,6 +12,7 @@ class FileType < ActiveRecord::Base
has_many :execution_environments
has_many :files
has_many :file_templates
validates :binary, boolean_presence: true
validates :editor_mode, presence: true, unless: :binary?

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,5 +1,6 @@
class RequestForComment < ActiveRecord::Base
include Creation
belongs_to :submission
belongs_to :exercise
belongs_to :file, class_name: 'CodeOcean::File'
@ -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,6 +24,17 @@ 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
@ -35,6 +45,6 @@ class RequestForComment < ActiveRecord::Base
private
def self.row_number_user_sql
select("id, user_id, exercise_id, file_id, question, requested_at, created_at, updated_at, user_type, solved, 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

@ -0,0 +1,11 @@
class FileTemplatePolicy < AdminOnlyPolicy
def show?
everyone
end
def by_file_type?
everyone
end
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, InternalUser, Team].sort_by { |model| model.model_name.human(count: 2) }
- models = [ExecutionEnvironment, Exercise, Consumer, CodeHarborLink, ExternalUser, FileType, FileTemplate, InternalUser].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

@ -8,5 +8,9 @@
.form-group
= f.label(:file_type_id, t('activerecord.attributes.file.file_type_id'))
= f.collection_select(:file_type_id, FileType.where(binary: false).order(:name), :id, :name, {selected: @exercise.execution_environment.file_type.try(:id)}, class: 'form-control')
.form-group
= f.label(:file_template_id, t('activerecord.attributes.file.file_template_id'))
= f.collection_select(:file_template_id, FileTemplate.all.order(:name), :id, :name, {:include_blank => true}, class: 'form-control')
= f.hidden_field(:context_id)
.hidden#noTemplateLabel data-text=t('file_template.no_template_label')
.actions = render('shared/submit_button', f: f, object: CodeOcean::File.new)

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

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

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

View File

@ -0,0 +1,12 @@
= form_for(@file_template) do |f|
= render('shared/form_errors', object: @file_template)
.form-group
= f.label(:name)
= f.text_field(:name, class: 'form-control', required: true)
.form-group
= f.label(:file_type_id)
= f.collection_select(:file_type_id, FileType.all.order(:name), :id, :name, {}, class: 'form-control')
.form-group
= f.label(:content)
= f.text_area(:content, class: 'form-control')
.actions = render('shared/submit_button', f: f, object: @file_template)

View File

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

View File

@ -0,0 +1,20 @@
h1 = FileTemplate.model_name.human(count: 2)
.table-responsive
table.table
thead
tr
th = t('activerecord.attributes.file_template.name')
th = t('activerecord.attributes.file_template.file_type')
th colspan=3 = t('shared.actions')
tbody
- @file_templates.each do |file_template|
tr
td = file_template.name
td = link_to(file_template.file_type, file_type_path(file_template.file_type))
td = link_to(t('shared.show'), file_template)
td = link_to(t('shared.edit'), edit_file_template_path(file_template))
td = link_to(t('shared.destroy'), file_template, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
= render('shared/pagination', collection: @file_templates)
p = render('shared/new_button', model: FileTemplate)

View File

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

View File

@ -0,0 +1,7 @@
h1
= @file_template
= render('shared/edit_button', object: @file_template)
= row(label: 'file_template.name', value: @file_template.name)
= row(label: 'file_template.file_type', value: link_to(@file_template.file_type, file_type_path(@file_template.file_type)))
= row(label: 'file_template.content', value: @file_template.content)

View File

@ -27,6 +27,6 @@ h1 = RequestForComment.model_name.human(count: 2)
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,21 +1,14 @@
<div class="list-group">
<h4 id ="exercise_caption" class="list-group-item-heading" data-rfc-id = "<%= @request_for_comment.id %>" ><%= 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.instructions') %>:</u> "<%= @request_for_comment.exercise.description %>"
<u><%= t('activerecord.attributes.exercise.description') %>:</u> "<%= render_markdown(@request_for_comment.exercise.description) %>"
</h5>
<h5>

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

@ -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"
@ -54,6 +52,7 @@ de:
read_only: Schreibgeschützt
role: Rolle
weight: Punktzahl
file_template_id: "Dateivorlage"
file_type:
binary: Binär
editor_mode: Editor-Modus
@ -91,9 +90,10 @@ de:
files: Dateien
score: Punktzahl
user: Autor
team:
internal_user_ids: Mitglieder
name: Name
file_template:
name: "Name"
file_type: "Dateityp"
content: "Code"
models:
code_harbor_link:
one: CodeHarbor-Link
@ -116,6 +116,9 @@ de:
file:
one: Datei
other: Dateien
file_template:
one: Dateivorlage
other: Dateivorlagen
file_type:
one: Dateityp
other: Dateitypen
@ -128,9 +131,6 @@ de:
submission:
one: Abgabe
other: Abgaben
team:
one: Team
other: Teams
errors:
messages:
together: 'muss zusammen mit %{attribute} definiert werden'
@ -455,3 +455,5 @@ de:
next_label: 'Nächste Seite &#8594;'
page_gap: '&hellip;'
previous_label: '&#8592; Vorherige Seite'
file_template:
no_template_label: "Leere Datei"

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"
@ -54,6 +52,7 @@ en:
read_only: Read-only
role: Role
weight: Score
file_template_id: "File Template"
file_type:
binary: Binary
editor_mode: Editor Mode
@ -91,9 +90,10 @@ en:
files: Files
score: Score
user: Author
team:
internal_user_ids: Members
name: Name
file_template:
name: "Name"
file_type: "File Type"
content: "Content"
models:
code_harbor_link:
one: CodeHarbor Link
@ -116,6 +116,9 @@ en:
file:
one: File
other: Files
file_template:
one: File Template
other: File Templates
file_type:
one: File Type
other: File Types
@ -128,9 +131,6 @@ en:
submission:
one: Submission
other: Submissions
team:
one: Team
other: Teams
errors:
messages:
together: 'has to be set along with %{attribute}'
@ -455,3 +455,5 @@ en:
next_label: 'Next Page &#8594;'
page_gap: '&hellip;'
previous_label: '&#8592; Previous Page'
file_template:
no_template_label: "Empty File"

View File

@ -1,6 +1,11 @@
FILENAME_REGEXP = /[\w\.]+/ unless Kernel.const_defined?(:FILENAME_REGEXP)
Rails.application.routes.draw do
resources :file_templates do
collection do
get 'by_file_type/:file_type_id', as: :by_file_type, to: :by_file_type
end
end
resources :code_harbor_links
resources :request_for_comments do
member do
@ -89,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
@ -99,5 +105,4 @@ Rails.application.routes.draw do
end
end
resources :teams
end

View File

@ -0,0 +1,10 @@
class CreateFileTemplates < ActiveRecord::Migration
def change
create_table :file_templates do |t|
t.string :name
t.text :content
t.belongs_to :file_type
t.timestamps
end
end
end

View File

@ -0,0 +1,5 @@
class AddFileTemplateToFile < ActiveRecord::Migration
def change
add_reference :files, :file_template
end
end

View File

@ -0,0 +1,5 @@
class 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: 20160624130951) 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: 20160624130951) 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
@ -101,6 +100,14 @@ ActiveRecord::Schema.define(version: 20160624130951) do
t.datetime "updated_at"
end
create_table "file_templates", force: true do |t|
t.string "name"
t.text "content"
t.integer "file_type_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "file_types", force: true do |t|
t.string "editor_mode"
t.string "file_extension"
@ -132,6 +139,7 @@ ActiveRecord::Schema.define(version: 20160624130951) do
t.string "feedback_message"
t.float "weight"
t.string "path"
t.integer "file_template_id"
end
add_index "files", ["context_id", "context_type"], name: "index_files_on_context_id_and_context_type", using: :btree
@ -173,24 +181,16 @@ ActiveRecord::Schema.define(version: 20160624130951) 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|
@ -203,12 +203,6 @@ ActiveRecord::Schema.define(version: 20160624130951) 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

@ -200,7 +200,7 @@ class DockerClient
execute_command(command, nil, block)
end
#only used by server sent events (deprecated?)
#only used by score
def execute_command(command, before_execution_block, output_consuming_block)
#tries ||= 0
@container = DockerContainerPool.get_container(@execution_environment)

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,8 +3,8 @@ 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
expect(subject).to permit(FactoryGirl.build(:admin), exercise)
@ -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