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();
};
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()) {

View File

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

View File

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

View File

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

View File

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

View File

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