Merge branch 'master' into rt/comments
Conflicts: app/assets/javascripts/editor.js
This commit is contained in:
@ -274,6 +274,29 @@ $(function() {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
var lastCopyText;
|
||||
var handleCopyEvent = function(text){
|
||||
lastCopyText = text;
|
||||
};
|
||||
|
||||
var handlePasteEvent = function (pasteObject) {
|
||||
//console.log("Handling paste event. this is ", this );
|
||||
//console.log("Text: " + pasteObject.text);
|
||||
|
||||
var same = (lastCopyText === pasteObject.text)
|
||||
//console.log("Text is the same: " + same);
|
||||
|
||||
// if the text is not copied from within the editor (from any file), send an event to lanalytics
|
||||
//if(!same){
|
||||
// publishCodeOceanEvent("codeocean_editor_paste", {
|
||||
// text: pasteObject.text,
|
||||
// exercise: $('#editor').data('exercise-id'),
|
||||
// file_id: "1"
|
||||
//
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
var handleScoringResponse = function(response) {
|
||||
printScoringResults(response);
|
||||
var score = _.reduce(response, function(sum, result) {
|
||||
@ -362,18 +385,12 @@ $(function() {
|
||||
var initializeEditors = function() {
|
||||
$('.editor').each(function(index, element) {
|
||||
var editor = ace.edit(element);
|
||||
|
||||
if (qa_api) {
|
||||
editor.getSession().on("change", function (deltaObject) {
|
||||
qa_api.executeCommand('syncEditor', [active_file, deltaObject]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// listener for autosave
|
||||
editor.getSession().on("change", function (deltaObject) {
|
||||
resetSaveTimer();
|
||||
});
|
||||
|
||||
var document = editor.getSession().getDocument();
|
||||
// insert pre-existing code into editor. we have to use insertLines, otherwise the deltas are not properly added
|
||||
var file_id = $(element).data('file-id');
|
||||
@ -412,37 +429,28 @@ $(function() {
|
||||
var row = e.getDocumentPosition().row;
|
||||
e.stop();
|
||||
|
||||
var commentModal = $('#comment-modal');
|
||||
/*
|
||||
* Register event handlers
|
||||
*/
|
||||
|
||||
if (hasCommentsInRow(editor, row)) {
|
||||
var rowComments = getCommentsForRow(editor, row);
|
||||
var comments = _.pluck(rowComments, 'text').join('\n');
|
||||
commentModal.find('#other-comments').text(comments);
|
||||
} else {
|
||||
commentModal.find('#other-comments').text('none');
|
||||
}
|
||||
// editor itself
|
||||
editor.on("paste", handlePasteEvent);
|
||||
editor.on("copy", handleCopyEvent);
|
||||
editor.on("guttermousedown", handleSidebarClick);
|
||||
|
||||
commentModal.find('#addCommentButton').off('click');
|
||||
commentModal.find('#removeAllButton').off('click');
|
||||
/* // alternative:
|
||||
editor.on("guttermousedown", function(e) {
|
||||
handleSidebarClick(e);
|
||||
});
|
||||
*/
|
||||
|
||||
commentModal.find('#addCommentButton').on('click', function(e){
|
||||
var user_id = $(element).data('user-id');
|
||||
var commenttext = commentModal.find('textarea').val();
|
||||
var file_id = $(element).data('id');
|
||||
//session
|
||||
session.on('annotationRemoval', handleAnnotationRemoval);
|
||||
session.on('annotationChange', handleAnnotationChange);
|
||||
|
||||
if (commenttext !== "") {
|
||||
createComment(user_id, file_id, row, editor, commenttext);
|
||||
commentModal.modal('hide');
|
||||
}
|
||||
})
|
||||
|
||||
commentModal.find('#removeAllButton').on('click', function(e){
|
||||
var user_id = $(element).data('user-id');
|
||||
deleteComment(user_id,file_id,row,editor);
|
||||
commentModal.modal('hide');
|
||||
})
|
||||
|
||||
commentModal.modal('show');
|
||||
// listener for autosave
|
||||
session.on("change", function (deltaObject) {
|
||||
resetSaveTimer();
|
||||
});
|
||||
});
|
||||
};
|
||||
@ -451,13 +459,13 @@ $(function() {
|
||||
return editor.getSession().getAnnotations().some(function(element) {
|
||||
return element.row === row;
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
var getCommentsForRow = function (editor, row){
|
||||
return editor.getSession().getAnnotations().filter(function(element) {
|
||||
return element.row === row;
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
var setAnnotations = function (editor, file_id){
|
||||
var session = editor.getSession();
|
||||
@ -476,29 +484,27 @@ $(function() {
|
||||
setAnnotationsCallback(response, session);
|
||||
});
|
||||
jqrequest.fail(ajaxError);
|
||||
}
|
||||
};
|
||||
|
||||
var setAnnotationsCallback = function (response, session) {
|
||||
var annotations = response;
|
||||
|
||||
// add classname and the username in front of each comment
|
||||
$.each(annotations, function(index, comment){
|
||||
comment.className = "code-ocean_comment";
|
||||
comment.text = comment.username + ": " + comment.text;
|
||||
// comment.text = comment.user_id + ": " + comment.text;
|
||||
});
|
||||
|
||||
session.setAnnotations(annotations);
|
||||
}
|
||||
|
||||
var deleteComment = function (user_id, file_id, row, editor) {
|
||||
var deleteComment = function (file_id, row, editor) {
|
||||
var jqxhr = $.ajax({
|
||||
type: 'DELETE',
|
||||
url: "/comments",
|
||||
data: {
|
||||
row: row,
|
||||
file_id: file_id,
|
||||
user_id: user_id
|
||||
}
|
||||
file_id: file_id }
|
||||
});
|
||||
jqxhr.done(function (response) {
|
||||
setAnnotations(editor, file_id);
|
||||
@ -506,7 +512,7 @@ $(function() {
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
var createComment = function (user_id, file_id, row, editor, commenttext){
|
||||
var createComment = function (file_id, row, editor, commenttext){
|
||||
var jqxhr = $.ajax({
|
||||
data: {
|
||||
comment: {
|
||||
@ -524,7 +530,7 @@ $(function() {
|
||||
setAnnotations(editor, file_id);
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
};
|
||||
|
||||
var handleAnnotationRemoval = function(removedAnnotations) {
|
||||
removedAnnotations.forEach(function(annotation) {
|
||||
@ -536,7 +542,7 @@ $(function() {
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
var handleAnnotationChange = function(changedAnnotations) {
|
||||
changedAnnotations.forEach(function(annotation) {
|
||||
@ -553,8 +559,54 @@ $(function() {
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
// Code for clicks on gutter / sidepanel
|
||||
var handleSidebarClick = function(e) {
|
||||
var target = e.domEvent.target;
|
||||
var editor = e.editor;
|
||||
|
||||
if (target.className.indexOf("ace_gutter-cell") == -1) return;
|
||||
if (!editor.isFocused()) return;
|
||||
if (e.clientX > 25 + target.getBoundingClientRect().left) return;
|
||||
|
||||
var row = e.getDocumentPosition().row;
|
||||
e.stop();
|
||||
|
||||
var commentModal = $('#comment-modal');
|
||||
|
||||
if (hasCommentsInRow(editor, row)) {
|
||||
var rowComments = getCommentsForRow(editor, row);
|
||||
var comments = _.pluck(rowComments, 'text').join('\n');
|
||||
commentModal.find('#other-comments').text(comments);
|
||||
} else {
|
||||
commentModal.find('#other-comments').text('none');
|
||||
}
|
||||
|
||||
commentModal.find('#addCommentButton').off('click');
|
||||
commentModal.find('#removeAllButton').off('click');
|
||||
|
||||
commentModal.find('#addCommentButton').on('click', function(e){
|
||||
var commenttext = commentModal.find('textarea').val();
|
||||
// attention: use id of data attribute here, not file-id (file-id is the original file)
|
||||
var file_id = $(editor.container).data('id');
|
||||
|
||||
if (commenttext !== "") {
|
||||
createComment(file_id, row, editor, commenttext);
|
||||
commentModal.modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
commentModal.find('#removeAllButton').on('click', function(e){
|
||||
// attention: use id of data attribute here, not file-id (file-id is the original file)
|
||||
var file_id = $(editor.container).data('id');
|
||||
deleteComment(file_id,row, editor);
|
||||
commentModal.modal('hide');
|
||||
});
|
||||
|
||||
commentModal.modal('show');
|
||||
};
|
||||
|
||||
var initializeEventHandlers = function() {
|
||||
$(document).on('click', '#results a', showOutput);
|
||||
$(document).on('keypress', handleKeyPress);
|
||||
@ -733,6 +785,31 @@ $(function() {
|
||||
}
|
||||
};
|
||||
|
||||
// Publishing events for other (JS) components to react to codeocean events
|
||||
var publishCodeOceanEvent = function (eventName, contextData) {
|
||||
|
||||
var payload = {
|
||||
user: {
|
||||
resource_uuid: $('#editor').data('user-id')
|
||||
},
|
||||
verb: eventName,
|
||||
resource: {},
|
||||
timestamp: new Date().toISOString(),
|
||||
with_result: {},
|
||||
in_context: contextData
|
||||
};
|
||||
|
||||
$.ajax("https://open.hpi.de/lanalytics/log", {
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
dataType: 'JSON',
|
||||
data: payload,
|
||||
success: {},
|
||||
error: {}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
var renderCode = function(event) {
|
||||
event.preventDefault();
|
||||
if ($('#render').is(':visible')) {
|
||||
@ -1044,12 +1121,13 @@ $(function() {
|
||||
}
|
||||
}
|
||||
|
||||
// save on quit
|
||||
$(window).on("beforeunload", function() {
|
||||
if(autosaveTimer){
|
||||
autosave();
|
||||
}
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
if ($('#editor').isPresent()) {
|
||||
if (isBrowserSupported()) {
|
||||
|
@ -26,8 +26,14 @@ module SubmissionScoring
|
||||
|
||||
def score_submission(submission)
|
||||
outputs = collect_test_results(submission)
|
||||
score = outputs.map { |output|
|
||||
output[:score] * output[:weight] }.reduce(:+)
|
||||
score = 0.0
|
||||
if not (outputs.nil? || outputs.empty?)
|
||||
outputs.each do |output|
|
||||
if not output.nil?
|
||||
score += output[:score] * output[:weight]
|
||||
end
|
||||
end
|
||||
end
|
||||
submission.update(score: score)
|
||||
outputs
|
||||
end
|
||||
|
@ -56,7 +56,7 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def index
|
||||
@search = Submission.last(100).search(params[:q])
|
||||
@search = Submission.search(params[:q])
|
||||
@submissions = @search.result.includes(:exercise, :user).paginate(page: params[:page])
|
||||
authorize!
|
||||
end
|
||||
|
@ -4,6 +4,7 @@ require 'pathname'
|
||||
class DockerClient
|
||||
CONTAINER_WORKSPACE_PATH = '/workspace'
|
||||
DEFAULT_MEMORY_LIMIT = 256
|
||||
# Ralf: I suggest to replace this with the environment variable. Ask Hauke why this is not the case!
|
||||
LOCAL_WORKSPACE_ROOT = Rails.root.join('tmp', 'files', Rails.env)
|
||||
MINIMUM_MEMORY_LIMIT = 4
|
||||
RECYCLE_CONTAINERS = true
|
||||
@ -17,6 +18,14 @@ class DockerClient
|
||||
raise(Error, "The Docker host at #{Docker.url} is not reachable!")
|
||||
end
|
||||
|
||||
def self.clean_container_workspace(container)
|
||||
local_workspace_path = local_workspace_path(container)
|
||||
if local_workspace_path && Pathname.new(local_workspace_path).exist?
|
||||
Pathname.new(local_workspace_path).children.each{ |p| p.rmtree}
|
||||
#FileUtils.rmdir(Pathname.new(local_workspace_path))
|
||||
end
|
||||
end
|
||||
|
||||
def command_substitutions(filename)
|
||||
{class_name: File.basename(filename, File.extname(filename)).camelize, filename: filename, module_name: File.basename(filename, File.extname(filename)).underscore}
|
||||
end
|
||||
@ -51,6 +60,7 @@ class DockerClient
|
||||
tries ||= 0
|
||||
container = Docker::Container.create(container_creation_options(execution_environment))
|
||||
local_workspace_path = generate_local_workspace_path
|
||||
# container.start always creates the passed local_workspace_path on disk. Seems like we have to live with that, therefore we can also just create the empty folder ourselves.
|
||||
FileUtils.mkdir(local_workspace_path)
|
||||
container.start(container_start_options(execution_environment, local_workspace_path))
|
||||
container.start_time = Time.now
|
||||
@ -61,8 +71,8 @@ class DockerClient
|
||||
end
|
||||
|
||||
def create_workspace_files(container, submission)
|
||||
#clear directory (it should be emtpy anyhow)
|
||||
Pathname.new(self.class.local_workspace_path(container)).children.each{ |p| p.rmtree}
|
||||
#clear directory (it should be empty anyhow)
|
||||
#Pathname.new(self.class.local_workspace_path(container)).children.each{ |p| p.rmtree}
|
||||
submission.collect_files.each do |file|
|
||||
FileUtils.mkdir_p(File.join(self.class.local_workspace_path(container), file.path || ''))
|
||||
if file.file_type.binary?
|
||||
@ -85,10 +95,7 @@ class DockerClient
|
||||
Rails.logger.info('destroying container ' + container.to_s)
|
||||
container.stop.kill
|
||||
container.port_bindings.values.each { |port| PortPool.release(port) }
|
||||
local_workspace_path = local_workspace_path(container)
|
||||
if local_workspace_path && Pathname.new(local_workspace_path).exist?
|
||||
Pathname.new(local_workspace_path).children.each{ |p| p.rmtree}
|
||||
end
|
||||
clean_container_workspace(container)
|
||||
container.delete(force: true, v: true)
|
||||
end
|
||||
|
||||
@ -157,6 +164,7 @@ class DockerClient
|
||||
|
||||
def self.mapped_directories(local_workspace_path)
|
||||
remote_workspace_path = local_workspace_path.sub(LOCAL_WORKSPACE_ROOT.to_s, config[:workspace_root])
|
||||
# create the string to be returned
|
||||
["#{remote_workspace_path}:#{CONTAINER_WORKSPACE_PATH}"]
|
||||
end
|
||||
|
||||
@ -170,38 +178,39 @@ class DockerClient
|
||||
`docker pull #{docker_image}` if docker_image
|
||||
end
|
||||
|
||||
def return_container(container)
|
||||
local_workspace_path = self.class.local_workspace_path(container)
|
||||
Pathname.new(local_workspace_path).children.each{ |p| p.rmtree}
|
||||
DockerContainerPool.return_container(container, @execution_environment)
|
||||
def self.return_container(container, execution_environment)
|
||||
clean_container_workspace(container)
|
||||
DockerContainerPool.return_container(container, execution_environment)
|
||||
end
|
||||
private :return_container
|
||||
#private :return_container
|
||||
|
||||
def send_command(command, container, &block)
|
||||
result = {status: :failed, stdout: '', stderr: ''}
|
||||
Timeout.timeout(@execution_environment.permitted_execution_time.to_i) do
|
||||
output = container.exec(['bash', '-c', command])
|
||||
Rails.logger.info "output from container.exec"
|
||||
Rails.logger.info output
|
||||
{status: output[2] == 0 ? :ok : :failed, stdout: output[0].join, stderr: output[1].join}
|
||||
result = {status: output[2] == 0 ? :ok : :failed, stdout: output[0].join, stderr: output[1].join}
|
||||
end
|
||||
# if we use pooling and recylce the containers, put it back. otherwise, destroy it.
|
||||
(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container)
|
||||
result
|
||||
rescue Timeout::Error
|
||||
timeout_occured = true
|
||||
Rails.logger.info('got timeout error for container ' + container.to_s)
|
||||
#container.restart if RECYCLE_CONTAINERS
|
||||
DockerContainerPool.remove_from_all_containers(container, @execution_environment)
|
||||
|
||||
# remove container from pool, then destroy it
|
||||
(DockerContainerPool.config[:active]) ? DockerContainerPool.remove_from_all_containers(container, @execution_environment) :
|
||||
|
||||
# destroy container
|
||||
self.class.destroy_container(container)
|
||||
|
||||
if(RECYCLE_CONTAINERS)
|
||||
# create new container and add it to @all_containers. will be added to @containers on return_container
|
||||
# if we recylce containers, we start a fresh one
|
||||
if(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS)
|
||||
# create new container and add it to @all_containers and @containers.
|
||||
container = self.class.create_container(@execution_environment)
|
||||
DockerContainerPool.add_to_all_containers(container, @execution_environment)
|
||||
end
|
||||
{status: :timeout}
|
||||
ensure
|
||||
Rails.logger.info('send_command ensuring for' + container.to_s)
|
||||
RECYCLE_CONTAINERS ? return_container(container) : self.class.destroy_container(container)
|
||||
end
|
||||
private :send_command
|
||||
|
||||
|
@ -6,8 +6,8 @@ describe 'Editor', js: true do
|
||||
|
||||
before(:each) do
|
||||
visit(sign_in_path)
|
||||
fill_in('Email', with: user.email)
|
||||
fill_in('Password', with: FactoryGirl.attributes_for(:teacher)[:password])
|
||||
fill_in('email', with: user.email)
|
||||
fill_in('password', with: FactoryGirl.attributes_for(:teacher)[:password])
|
||||
click_button(I18n.t('sessions.new.link'))
|
||||
visit(implement_exercise_path(exercise))
|
||||
end
|
||||
@ -83,8 +83,9 @@ describe 'Editor', js: true do
|
||||
describe 'Progress Tab' do
|
||||
before(:each) { click_link(I18n.t('exercises.implement.progress')) }
|
||||
|
||||
it 'contains a button for submitting the exercise' do
|
||||
expect(page).to have_css('#submit')
|
||||
it 'does not contains a button for submitting the exercise' do
|
||||
# the button is only displayed when an correct LTI handshake to a running course happened. This is not the case in the test
|
||||
expect(page).not_to have_css('#submit')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -182,7 +182,7 @@ describe DockerClient, docker: true do
|
||||
end
|
||||
|
||||
it 'deletes the container' do
|
||||
expect(container).to receive(:delete).with(force: true)
|
||||
expect(container).to receive(:delete).with(force: true, v: true)
|
||||
end
|
||||
end
|
||||
|
||||
@ -220,6 +220,7 @@ describe DockerClient, docker: true do
|
||||
end
|
||||
|
||||
it 'raises the error' do
|
||||
pending("retries are disabled")
|
||||
#!TODO Retries is disabled
|
||||
#expect { execute_arbitrary_command }.to raise_error(error)
|
||||
end
|
||||
@ -345,17 +346,20 @@ describe DockerClient, docker: true do
|
||||
end
|
||||
|
||||
it 'provides the command to be executed as input' do
|
||||
pending("we are currently not using any input and for output server send events instead of attach.")
|
||||
expect(container).to receive(:attach).with(stdin: kind_of(StringIO))
|
||||
end
|
||||
|
||||
it 'calls the block' do
|
||||
pending("block is no longer called, see revision 4cbf9970b13362efd4588392cafe4f7fd7cb31c3 to get information how it was done before.")
|
||||
expect(block).to receive(:call)
|
||||
end
|
||||
|
||||
context 'when a timeout occurs' do
|
||||
before(:each) { expect(container).to receive(:attach).and_raise(Timeout::Error) }
|
||||
before(:each) { expect(container).to receive(:exec).and_raise(Timeout::Error) }
|
||||
|
||||
it 'destroys the container asynchronously' do
|
||||
pending("Container is destroyed, but not as expected in this test. ToDo update this test.")
|
||||
expect(Concurrent::Future).to receive(:execute)
|
||||
end
|
||||
|
||||
@ -366,6 +370,7 @@ describe DockerClient, docker: true do
|
||||
|
||||
context 'when the container terminates timely' do
|
||||
it 'destroys the container asynchronously' do
|
||||
pending("Container is destroyed, but not as expected in this test. ToDo update this test.")
|
||||
expect(Concurrent::Future).to receive(:execute)
|
||||
end
|
||||
|
||||
|
Reference in New Issue
Block a user