Merge branch 'master' into rt/comments

Conflicts:
	app/assets/javascripts/editor.js
This commit is contained in:
Ralf Teusner
2015-08-31 19:23:53 +02:00
6 changed files with 176 additions and 77 deletions

View File

@ -274,6 +274,29 @@ $(function() {
event.preventDefault(); 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) { var handleScoringResponse = function(response) {
printScoringResults(response); printScoringResults(response);
var score = _.reduce(response, function(sum, result) { var score = _.reduce(response, function(sum, result) {
@ -362,18 +385,12 @@ $(function() {
var initializeEditors = function() { var initializeEditors = function() {
$('.editor').each(function(index, element) { $('.editor').each(function(index, element) {
var editor = ace.edit(element); var editor = ace.edit(element);
if (qa_api) { if (qa_api) {
editor.getSession().on("change", function (deltaObject) { editor.getSession().on("change", function (deltaObject) {
qa_api.executeCommand('syncEditor', [active_file, deltaObject]); qa_api.executeCommand('syncEditor', [active_file, deltaObject]);
}); });
} }
// listener for autosave
editor.getSession().on("change", function (deltaObject) {
resetSaveTimer();
});
var document = editor.getSession().getDocument(); var document = editor.getSession().getDocument();
// insert pre-existing code into editor. we have to use insertLines, otherwise the deltas are not properly added // 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'); var file_id = $(element).data('file-id');
@ -412,37 +429,28 @@ $(function() {
var row = e.getDocumentPosition().row; var row = e.getDocumentPosition().row;
e.stop(); e.stop();
var commentModal = $('#comment-modal'); /*
* Register event handlers
*/
if (hasCommentsInRow(editor, row)) { // editor itself
var rowComments = getCommentsForRow(editor, row); editor.on("paste", handlePasteEvent);
var comments = _.pluck(rowComments, 'text').join('\n'); editor.on("copy", handleCopyEvent);
commentModal.find('#other-comments').text(comments); editor.on("guttermousedown", handleSidebarClick);
} else {
commentModal.find('#other-comments').text('none');
}
commentModal.find('#addCommentButton').off('click'); /* // alternative:
commentModal.find('#removeAllButton').off('click'); editor.on("guttermousedown", function(e) {
handleSidebarClick(e);
});
*/
commentModal.find('#addCommentButton').on('click', function(e){ //session
var user_id = $(element).data('user-id'); session.on('annotationRemoval', handleAnnotationRemoval);
var commenttext = commentModal.find('textarea').val(); session.on('annotationChange', handleAnnotationChange);
var file_id = $(element).data('id');
if (commenttext !== "") { // listener for autosave
createComment(user_id, file_id, row, editor, commenttext); session.on("change", function (deltaObject) {
commentModal.modal('hide'); resetSaveTimer();
}
})
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');
}); });
}); });
}; };
@ -451,13 +459,13 @@ $(function() {
return editor.getSession().getAnnotations().some(function(element) { return editor.getSession().getAnnotations().some(function(element) {
return element.row === row; return element.row === row;
}) })
} };
var getCommentsForRow = function (editor, row){ var getCommentsForRow = function (editor, row){
return editor.getSession().getAnnotations().filter(function(element) { return editor.getSession().getAnnotations().filter(function(element) {
return element.row === row; return element.row === row;
}) })
} };
var setAnnotations = function (editor, file_id){ var setAnnotations = function (editor, file_id){
var session = editor.getSession(); var session = editor.getSession();
@ -476,29 +484,27 @@ $(function() {
setAnnotationsCallback(response, session); setAnnotationsCallback(response, session);
}); });
jqrequest.fail(ajaxError); jqrequest.fail(ajaxError);
} };
var setAnnotationsCallback = function (response, session) { var setAnnotationsCallback = function (response, session) {
var annotations = response; var annotations = response;
// add classname and the username in front of each comment
$.each(annotations, function(index, comment){ $.each(annotations, function(index, comment){
comment.className = "code-ocean_comment"; comment.className = "code-ocean_comment";
comment.text = comment.username + ": " + comment.text; comment.text = comment.username + ": " + comment.text;
// comment.text = comment.user_id + ": " + comment.text;
}); });
session.setAnnotations(annotations); session.setAnnotations(annotations);
} }
var deleteComment = function (user_id, file_id, row, editor) { var deleteComment = function (file_id, row, editor) {
var jqxhr = $.ajax({ var jqxhr = $.ajax({
type: 'DELETE', type: 'DELETE',
url: "/comments", url: "/comments",
data: { data: {
row: row, row: row,
file_id: file_id, file_id: file_id }
user_id: user_id
}
}); });
jqxhr.done(function (response) { jqxhr.done(function (response) {
setAnnotations(editor, file_id); setAnnotations(editor, file_id);
@ -506,7 +512,7 @@ $(function() {
jqxhr.fail(ajaxError); jqxhr.fail(ajaxError);
} }
var createComment = function (user_id, file_id, row, editor, commenttext){ var createComment = function (file_id, row, editor, commenttext){
var jqxhr = $.ajax({ var jqxhr = $.ajax({
data: { data: {
comment: { comment: {
@ -524,7 +530,7 @@ $(function() {
setAnnotations(editor, file_id); setAnnotations(editor, file_id);
}); });
jqxhr.fail(ajaxError); jqxhr.fail(ajaxError);
} };
var handleAnnotationRemoval = function(removedAnnotations) { var handleAnnotationRemoval = function(removedAnnotations) {
removedAnnotations.forEach(function(annotation) { removedAnnotations.forEach(function(annotation) {
@ -536,7 +542,7 @@ $(function() {
} }
}) })
}) })
} };
var handleAnnotationChange = function(changedAnnotations) { var handleAnnotationChange = function(changedAnnotations) {
changedAnnotations.forEach(function(annotation) { changedAnnotations.forEach(function(annotation) {
@ -553,7 +559,53 @@ $(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() { var initializeEventHandlers = function() {
$(document).on('click', '#results a', showOutput); $(document).on('click', '#results a', showOutput);
@ -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) { var renderCode = function(event) {
event.preventDefault(); event.preventDefault();
if ($('#render').is(':visible')) { if ($('#render').is(':visible')) {
@ -1044,12 +1121,13 @@ $(function() {
} }
} }
// save on quit
$(window).on("beforeunload", function() { $(window).on("beforeunload", function() {
if(autosaveTimer){ if(autosaveTimer){
autosave(); autosave();
} }
}) });
if ($('#editor').isPresent()) { if ($('#editor').isPresent()) {
if (isBrowserSupported()) { if (isBrowserSupported()) {

View File

@ -26,8 +26,14 @@ module SubmissionScoring
def score_submission(submission) def score_submission(submission)
outputs = collect_test_results(submission) outputs = collect_test_results(submission)
score = outputs.map { |output| score = 0.0
output[:score] * output[:weight] }.reduce(:+) 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) submission.update(score: score)
outputs outputs
end end

View File

@ -56,7 +56,7 @@ class SubmissionsController < ApplicationController
end end
def index def index
@search = Submission.last(100).search(params[:q]) @search = Submission.search(params[:q])
@submissions = @search.result.includes(:exercise, :user).paginate(page: params[:page]) @submissions = @search.result.includes(:exercise, :user).paginate(page: params[:page])
authorize! authorize!
end end

View File

@ -4,6 +4,7 @@ require 'pathname'
class DockerClient class DockerClient
CONTAINER_WORKSPACE_PATH = '/workspace' CONTAINER_WORKSPACE_PATH = '/workspace'
DEFAULT_MEMORY_LIMIT = 256 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) LOCAL_WORKSPACE_ROOT = Rails.root.join('tmp', 'files', Rails.env)
MINIMUM_MEMORY_LIMIT = 4 MINIMUM_MEMORY_LIMIT = 4
RECYCLE_CONTAINERS = true RECYCLE_CONTAINERS = true
@ -17,6 +18,14 @@ class DockerClient
raise(Error, "The Docker host at #{Docker.url} is not reachable!") raise(Error, "The Docker host at #{Docker.url} is not reachable!")
end 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) def command_substitutions(filename)
{class_name: File.basename(filename, File.extname(filename)).camelize, filename: filename, module_name: File.basename(filename, File.extname(filename)).underscore} {class_name: File.basename(filename, File.extname(filename)).camelize, filename: filename, module_name: File.basename(filename, File.extname(filename)).underscore}
end end
@ -51,6 +60,7 @@ class DockerClient
tries ||= 0 tries ||= 0
container = Docker::Container.create(container_creation_options(execution_environment)) container = Docker::Container.create(container_creation_options(execution_environment))
local_workspace_path = generate_local_workspace_path 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) FileUtils.mkdir(local_workspace_path)
container.start(container_start_options(execution_environment, local_workspace_path)) container.start(container_start_options(execution_environment, local_workspace_path))
container.start_time = Time.now container.start_time = Time.now
@ -61,8 +71,8 @@ class DockerClient
end end
def create_workspace_files(container, submission) def create_workspace_files(container, submission)
#clear directory (it should be emtpy anyhow) #clear directory (it should be empty anyhow)
Pathname.new(self.class.local_workspace_path(container)).children.each{ |p| p.rmtree} #Pathname.new(self.class.local_workspace_path(container)).children.each{ |p| p.rmtree}
submission.collect_files.each do |file| submission.collect_files.each do |file|
FileUtils.mkdir_p(File.join(self.class.local_workspace_path(container), file.path || '')) FileUtils.mkdir_p(File.join(self.class.local_workspace_path(container), file.path || ''))
if file.file_type.binary? if file.file_type.binary?
@ -85,10 +95,7 @@ class DockerClient
Rails.logger.info('destroying container ' + container.to_s) Rails.logger.info('destroying container ' + container.to_s)
container.stop.kill container.stop.kill
container.port_bindings.values.each { |port| PortPool.release(port) } container.port_bindings.values.each { |port| PortPool.release(port) }
local_workspace_path = local_workspace_path(container) clean_container_workspace(container)
if local_workspace_path && Pathname.new(local_workspace_path).exist?
Pathname.new(local_workspace_path).children.each{ |p| p.rmtree}
end
container.delete(force: true, v: true) container.delete(force: true, v: true)
end end
@ -157,6 +164,7 @@ class DockerClient
def self.mapped_directories(local_workspace_path) def self.mapped_directories(local_workspace_path)
remote_workspace_path = local_workspace_path.sub(LOCAL_WORKSPACE_ROOT.to_s, config[:workspace_root]) 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}"] ["#{remote_workspace_path}:#{CONTAINER_WORKSPACE_PATH}"]
end end
@ -170,38 +178,39 @@ class DockerClient
`docker pull #{docker_image}` if docker_image `docker pull #{docker_image}` if docker_image
end end
def return_container(container) def self.return_container(container, execution_environment)
local_workspace_path = self.class.local_workspace_path(container) clean_container_workspace(container)
Pathname.new(local_workspace_path).children.each{ |p| p.rmtree} DockerContainerPool.return_container(container, execution_environment)
DockerContainerPool.return_container(container, @execution_environment)
end end
private :return_container #private :return_container
def send_command(command, container, &block) def send_command(command, container, &block)
result = {status: :failed, stdout: '', stderr: ''}
Timeout.timeout(@execution_environment.permitted_execution_time.to_i) do Timeout.timeout(@execution_environment.permitted_execution_time.to_i) do
output = container.exec(['bash', '-c', command]) output = container.exec(['bash', '-c', command])
Rails.logger.info "output from container.exec" Rails.logger.info "output from container.exec"
Rails.logger.info output 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 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 rescue Timeout::Error
timeout_occured = true
Rails.logger.info('got timeout error for container ' + container.to_s) 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 # destroy container
self.class.destroy_container(container) self.class.destroy_container(container)
if(RECYCLE_CONTAINERS) # if we recylce containers, we start a fresh one
# create new container and add it to @all_containers. will be added to @containers on return_container if(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS)
# create new container and add it to @all_containers and @containers.
container = self.class.create_container(@execution_environment) container = self.class.create_container(@execution_environment)
DockerContainerPool.add_to_all_containers(container, @execution_environment) DockerContainerPool.add_to_all_containers(container, @execution_environment)
end end
{status: :timeout} {status: :timeout}
ensure
Rails.logger.info('send_command ensuring for' + container.to_s)
RECYCLE_CONTAINERS ? return_container(container) : self.class.destroy_container(container)
end end
private :send_command private :send_command

View File

@ -6,8 +6,8 @@ describe 'Editor', js: true do
before(:each) do before(:each) do
visit(sign_in_path) visit(sign_in_path)
fill_in('Email', with: user.email) fill_in('email', with: user.email)
fill_in('Password', with: FactoryGirl.attributes_for(:teacher)[:password]) fill_in('password', with: FactoryGirl.attributes_for(:teacher)[:password])
click_button(I18n.t('sessions.new.link')) click_button(I18n.t('sessions.new.link'))
visit(implement_exercise_path(exercise)) visit(implement_exercise_path(exercise))
end end
@ -83,8 +83,9 @@ describe 'Editor', js: true do
describe 'Progress Tab' do describe 'Progress Tab' do
before(:each) { click_link(I18n.t('exercises.implement.progress')) } before(:each) { click_link(I18n.t('exercises.implement.progress')) }
it 'contains a button for submitting the exercise' do it 'does not contains a button for submitting the exercise' do
expect(page).to have_css('#submit') # 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 end
end end

View File

@ -182,7 +182,7 @@ describe DockerClient, docker: true do
end end
it 'deletes the container' do 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
end end
@ -220,6 +220,7 @@ describe DockerClient, docker: true do
end end
it 'raises the error' do it 'raises the error' do
pending("retries are disabled")
#!TODO Retries is disabled #!TODO Retries is disabled
#expect { execute_arbitrary_command }.to raise_error(error) #expect { execute_arbitrary_command }.to raise_error(error)
end end
@ -345,17 +346,20 @@ describe DockerClient, docker: true do
end end
it 'provides the command to be executed as input' do 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)) expect(container).to receive(:attach).with(stdin: kind_of(StringIO))
end end
it 'calls the block' do 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) expect(block).to receive(:call)
end end
context 'when a timeout occurs' do 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 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) expect(Concurrent::Future).to receive(:execute)
end end
@ -366,6 +370,7 @@ describe DockerClient, docker: true do
context 'when the container terminates timely' do context 'when the container terminates timely' do
it 'destroys the container asynchronously' 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) expect(Concurrent::Future).to receive(:execute)
end end