transferred Code Ocean from original repository to GitHub

This commit is contained in:
Hauke Klement
2015-01-22 09:51:49 +01:00
commit 4cbf9970b1
683 changed files with 11979 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
/config/*.production.yml
/coverage
/log
/public/assets
/public/uploads
/tmp
/vagrant/
*.sublime-*

7
Capfile Normal file
View File

@ -0,0 +1,7 @@
require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/puma'
require 'capistrano/puma/nginx'
require 'capistrano/rails'
require 'capistrano/rvm'
require 'capistrano/upload-config'

53
Gemfile Normal file
View File

@ -0,0 +1,53 @@
source 'https://rubygems.org'
gem 'activerecord-jdbcpostgresql-adapter', platform: :jruby
gem 'bcrypt', '~> 3.1.7'
gem 'bootstrap-will_paginate'
gem 'carrierwave'
gem 'coffee-rails', '~> 4.0.0'
gem 'docker-api', require: 'docker'
gem 'factory_girl_rails', '~> 4.0'
gem 'forgery'
gem 'highline'
gem 'jbuilder', '~> 2.0'
gem 'jquery-rails'
gem 'jquery-turbolinks'
gem 'ims-lti'
gem 'kramdown'
gem 'pg', platform: :ruby
gem 'pry'
gem 'puma'
gem 'pundit'
gem 'rails', '~> 4.1.2'
gem 'rails-i18n', '~> 4.0.0'
gem 'ransack'
gem 'rubytree'
gem 'sass-rails', '~> 4.0.3'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'slim'
gem 'sorcery'
gem 'thread_safe'
gem 'turbolinks'
gem 'uglifier', '>= 1.3.0'
gem 'will_paginate', '~> 3.0'
group :development do
gem 'capistrano', '~> 3.2.1'
gem 'capistrano3-puma'
gem 'capistrano-rails', '~> 1.1'
gem 'capistrano-rvm'
gem 'capistrano-upload-config'
gem 'spring'
end
group :test do
gem 'autotest-rails'
gem 'capybara'
gem 'capybara-webkit'
gem 'database_cleaner'
gem 'nyan-cat-formatter'
gem 'rspec-autotest'
gem 'rspec-rails', '>= 3.0.0'
gem 'selenium-webdriver'
gem 'simplecov', :require => false
end

341
Gemfile.lock Normal file
View File

@ -0,0 +1,341 @@
GEM
remote: https://rubygems.org/
specs:
ZenTest (4.11.0)
actionmailer (4.1.9)
actionpack (= 4.1.9)
actionview (= 4.1.9)
mail (~> 2.5, >= 2.5.4)
actionpack (4.1.9)
actionview (= 4.1.9)
activesupport (= 4.1.9)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
actionview (4.1.9)
activesupport (= 4.1.9)
builder (~> 3.1)
erubis (~> 2.7.0)
activemodel (4.1.9)
activesupport (= 4.1.9)
builder (~> 3.1)
activerecord (4.1.9)
activemodel (= 4.1.9)
activesupport (= 4.1.9)
arel (~> 5.0.0)
activerecord-jdbc-adapter (1.3.13)
activerecord (>= 2.2)
activerecord-jdbcpostgresql-adapter (1.3.13)
activerecord-jdbc-adapter (~> 1.3.13)
jdbc-postgres (>= 9.1)
activesupport (4.1.9)
i18n (~> 0.6, >= 0.6.9)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.1)
tzinfo (~> 1.1)
archive-tar-minitar (0.5.2)
arel (5.0.1.20140414130214)
autotest-rails (4.2.1)
ZenTest (~> 4.5)
bcrypt (3.1.9)
bcrypt (3.1.9-java)
bootstrap-will_paginate (0.0.10)
will_paginate
builder (3.2.2)
capistrano (3.2.1)
i18n
rake (>= 10.0.0)
sshkit (~> 1.3)
capistrano-bundler (1.1.3)
capistrano (~> 3.1)
sshkit (~> 1.2)
capistrano-rails (1.1.2)
capistrano (~> 3.1)
capistrano-bundler (~> 1.1)
capistrano-rvm (0.1.2)
capistrano (~> 3.0)
sshkit (~> 1.2)
capistrano-upload-config (0.5.0)
capistrano (>= 3.0)
capistrano3-puma (0.8.3)
capistrano (~> 3.0)
puma (>= 2.6)
capybara (2.4.4)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
capybara-webkit (1.3.1)
capybara (>= 2.0.2, < 2.5.0)
json
carrierwave (0.10.0)
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
json (>= 1.7)
mime-types (>= 1.16)
childprocess (0.5.5)
ffi (~> 1.0, >= 1.0.11)
coderay (1.1.0)
coffee-rails (4.0.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.0)
coffee-script (2.3.0)
coffee-script-source
execjs
coffee-script-source (1.8.0)
colorize (0.7.5)
database_cleaner (1.4.0)
diff-lcs (1.2.5)
docile (1.1.5)
docker-api (1.17.0)
archive-tar-minitar
excon (>= 0.38.0)
json
erubis (2.7.0)
excon (0.43.0)
execjs (2.2.2)
factory_girl (4.5.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.5.0)
factory_girl (~> 4.5.0)
railties (>= 3.0.0)
faraday (0.9.1)
multipart-post (>= 1.2, < 3)
ffi (1.9.6)
ffi (1.9.6-java)
forgery (0.6.0)
highline (1.6.21)
hike (1.2.3)
i18n (0.7.0)
ims-lti (1.1.7)
builder
oauth (~> 0.4.5)
uuid
jbuilder (2.2.6)
activesupport (>= 3.0.0, < 5)
multi_json (~> 1.2)
jdbc-postgres (9.3.1102)
jquery-rails (3.1.2)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
jquery-turbolinks (2.1.0)
railties (>= 3.1.0)
turbolinks
json (1.8.2)
json (1.8.2-java)
jwt (1.2.0)
kramdown (1.5.0)
macaddr (1.7.1)
systemu (~> 2.6.2)
mail (2.6.3)
mime-types (>= 1.16, < 3)
method_source (0.8.2)
mime-types (2.4.3)
mini_portile (0.6.2)
minitest (5.5.1)
multi_json (1.10.1)
multi_xml (0.5.5)
multipart-post (2.0.0)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (2.9.2)
nokogiri (1.6.5)
mini_portile (~> 0.6.0)
nokogiri (1.6.5-java)
nyan-cat-formatter (0.11)
rspec (>= 2.99, >= 2.14.2, < 4)
oauth (0.4.7)
oauth2 (1.0.0)
faraday (>= 0.8, < 0.10)
jwt (~> 1.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (~> 1.2)
pg (0.18.1)
polyamorous (1.1.0)
activerecord (>= 3.0)
pry (0.10.1)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
pry (0.10.1-java)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
spoon (~> 0.0)
puma (2.11.0)
rack (>= 1.1, < 2.0)
puma (2.11.0-java)
rack (>= 1.1, < 2.0)
pundit (0.3.0)
activesupport (>= 3.0.0)
rack (1.5.2)
rack-test (0.6.3)
rack (>= 1.0)
rails (4.1.9)
actionmailer (= 4.1.9)
actionpack (= 4.1.9)
actionview (= 4.1.9)
activemodel (= 4.1.9)
activerecord (= 4.1.9)
activesupport (= 4.1.9)
bundler (>= 1.3.0, < 2.0)
railties (= 4.1.9)
sprockets-rails (~> 2.0)
rails-i18n (4.0.3)
i18n (~> 0.6)
railties (~> 4.0)
railties (4.1.9)
actionpack (= 4.1.9)
activesupport (= 4.1.9)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rake (10.4.2)
ransack (1.6.3)
actionpack (>= 3.0)
activerecord (>= 3.0)
activesupport (>= 3.0)
i18n
polyamorous (~> 1.1)
rdoc (4.2.0)
rspec (3.1.0)
rspec-core (~> 3.1.0)
rspec-expectations (~> 3.1.0)
rspec-mocks (~> 3.1.0)
rspec-autotest (1.0.0)
rspec-core (>= 2.99.0.beta1, < 4.0.0)
rspec-core (3.1.7)
rspec-support (~> 3.1.0)
rspec-expectations (3.1.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.1.0)
rspec-mocks (3.1.3)
rspec-support (~> 3.1.0)
rspec-rails (3.1.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.1.0)
rspec-expectations (~> 3.1.0)
rspec-mocks (~> 3.1.0)
rspec-support (~> 3.1.0)
rspec-support (3.1.2)
rubytree (0.9.4)
json (~> 1.8)
structured_warnings (~> 0.1)
rubyzip (1.1.6)
sass (3.2.19)
sass-rails (4.0.5)
railties (>= 4.0.0, < 5.0)
sass (~> 3.2.2)
sprockets (~> 2.8, < 3.0)
sprockets-rails (~> 2.0)
sdoc (0.4.1)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
selenium-webdriver (2.44.0)
childprocess (~> 0.5)
multi_json (~> 1.0)
rubyzip (~> 1.0)
websocket (~> 1.0)
simplecov (0.9.1)
docile (~> 1.1.0)
multi_json (~> 1.0)
simplecov-html (~> 0.8.0)
simplecov-html (0.8.0)
slim (3.0.1)
temple (~> 0.7.3)
tilt (>= 1.3.3, < 2.1)
slop (3.6.0)
sorcery (0.9.0)
bcrypt (~> 3.1)
oauth (~> 0.4, >= 0.4.4)
oauth2 (>= 0.8.0)
spoon (0.0.4)
ffi
spring (1.2.0)
sprockets (2.12.3)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sprockets-rails (2.2.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
sshkit (1.6.1)
colorize (>= 0.7.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
structured_warnings (0.2.0)
systemu (2.6.4)
temple (0.7.5)
thor (0.19.1)
thread_safe (0.3.4)
thread_safe (0.3.4-java)
tilt (1.4.1)
turbolinks (2.5.3)
coffee-rails
tzinfo (1.2.2)
thread_safe (~> 0.1)
uglifier (2.7.0)
execjs (>= 0.3.0)
json (>= 1.8.0)
uuid (2.3.7)
macaddr (~> 1.0)
websocket (1.2.1)
will_paginate (3.0.7)
xpath (2.0.0)
nokogiri (~> 1.3)
PLATFORMS
java
ruby
DEPENDENCIES
activerecord-jdbcpostgresql-adapter
autotest-rails
bcrypt (~> 3.1.7)
bootstrap-will_paginate
capistrano (~> 3.2.1)
capistrano-rails (~> 1.1)
capistrano-rvm
capistrano-upload-config
capistrano3-puma
capybara
capybara-webkit
carrierwave
coffee-rails (~> 4.0.0)
database_cleaner
docker-api
factory_girl_rails (~> 4.0)
forgery
highline
ims-lti
jbuilder (~> 2.0)
jquery-rails
jquery-turbolinks
kramdown
nyan-cat-formatter
pg
pry
puma
pundit
rails (~> 4.1.2)
rails-i18n (~> 4.0.0)
ransack
rspec-autotest
rspec-rails (>= 3.0.0)
rubytree
sass-rails (~> 4.0.3)
sdoc (~> 0.4.0)
selenium-webdriver
simplecov
slim
sorcery
spring
thread_safe
turbolinks
uglifier (>= 1.3.0)
will_paginate (~> 3.0)

22
README.md Normal file
View File

@ -0,0 +1,22 @@
Code Ocean
==========
## Setup
### Mandatory Steps
- install the Docker client
- run `bundle install`
- create *config/action_mailer.yml*
- create *config/database.yml*
- create *config/secrets.yml*
- customize *config/docker.yml.erb*
In order to execute code submissions using Docker, source code files are written to the file system and are provided to a dedicated Docker container. These files are temporarily written to *Rails.root/tmp/files/*. Please make sure that *workspace_root* in *config/docker.yml.erb* corresponds to that directory or to a linked directory if using a remote Docker server.
### Optional Steps
- create *config/sendmail.yml*
- create *config/smtp.yml*
- if Docker is not supported by your OS, set up a local Docker server using [vagrant-docker](https://github.com/hklement/vagrant-docker)
- create seed data by executing `rake db:seed`

6
Rakefile Normal file
View File

@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__)
Rails.application.load_tasks

0
app/assets/images/.keep Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,22 @@
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//
//= require ace/ace
//= require chosen.jquery.min
//= require jquery.turbolinks
//= require jquery_ujs
//= require jstree/jstree.min
//= require turbolinks
//= require_tree ../../../lib
//= require_tree .

View File

@ -0,0 +1,17 @@
$(function() {
var ANIMATION_DURATION = 500;
$.isController = function(name) {
return $('.container[data-controller="' + name + '"]').isPresent();
};
$.fn.isPresent = function() {
return this.length > 0;
};
$.fn.scrollTo = function(selector) {
$(this).animate({
scrollTop: $(selector).offset().top - $(this).offset().top + $(this).scrollTop()
}, ANIMATION_DURATION);
};
});

View File

@ -0,0 +1,604 @@
$(function() {
var ACE_FILES_PATH = '/assets/ace/';
var ADEQUATE_PERCENTAGE = 50;
var ALT_1_KEY_CODE = 161;
var ALT_2_KEY_CODE = 8220;
var ALT_3_KEY_CODE = 182;
var ALT_4_KEY_CODE = 162;
var ALT_R_KEY_CODE = 174;
var ALT_S_KEY_CODE = 8218;
var ALT_T_KEY_CODE = 8224;
var FILENAME_URL_PLACEHOLDER = '{filename}';
var SUCCESSFULL_PERCENTAGE = 90;
var THEME = 'ace/theme/textmate';
var editors = [];
var active_file = undefined;
var active_frame = undefined;
var running = false;
var flowrUrl = 'http://vm-teusner-webrtc.eaalab.hpi.uni-potsdam.de:3000/api/exceptioninfo?id=&lang=auto'
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 ajaxError = function(response) {
$.flash.danger({
text: (response && response.responseJSON && response.responseJSON.message) || $('#flash').data('message-failure')
});
};
var clearOutput = function(output) {
$('#output pre').remove();
};
var collectFiles = function() {
var editable_editors = _.filter(editors, function(editor) {
return !editor.getReadOnly();
});
return _.map(editable_editors, function(editor) {
return {
content: editor.getValue(),
file_id: $(editor.container).data('file-id')
};
});
};
var configureEditors = function() {
_.each(['modePath', 'themePath', 'workerPath'], function(attribute) {
ace.config.set(attribute, ACE_FILES_PATH);
});
};
var confirmDestroy = function(event) {
event.preventDefault();
if (confirm($(this).data('message-confirm'))) {
destroyFile();
}
};
var confirmReset = function(event) {
event.preventDefault();
if (confirm($(this).data('message-confirm'))) {
resetCode();
}
};
var confirmSubmission = function(event) {
event.preventDefault();
if (confirm($(this).data('message-confirm'))) {
submitCode();
}
};
var createSubmission = function(initiator, filter, callback) {
showSpinner(initiator);
var jqxhr = $.ajax({
data: {
submission: {
cause: $(initiator).data('cause') || $(initiator).prop('id'),
exercise_id: $('#editor').data('exercise-id'),
files_attributes: (filter || _.identity)(collectFiles())
}
},
dataType: 'json',
method: 'POST',
url: $(initiator).data('url') || $('#editor').data('submissions-url')
});
jqxhr.always(hideSpinner);
jqxhr.done(callback);
jqxhr.fail(ajaxError);
};
var destroyFile = function() {
createSubmission($('#destroy-file'), function(files) {
return _.reject(files, function(file) {
return file.file_id === active_file.id;
});
}, function() {
Turbolinks.visit(window.location.pathname);
});
};
var downloadCode = function(event) {
event.preventDefault();
createSubmission(this, null,function(response) {
var url = response.download_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
window.location = url;
});
};
var evaluateCode = function(url, streamed, callback) {
eval('evaluateCode' + (streamed ? 'With' : 'Without') + 'StreamedResponse')(url, callback);
};
var evaluateCodeWithStreamedResponse = function(url, callback) {
var event_source = new EventSource(url);
event_source.addEventListener('close', function(event) {
event_source.close();
hideSpinner();
running = false;
toggleButtonStates();
if (JSON.parse(event.data).code !== 200) {
ajaxError();
showTab(1);
}
});
event_source.addEventListener('error', ajaxError);
event_source.addEventListener('hint', renderHint);
event_source.addEventListener('info', storeContainerInformation);
event_source.addEventListener('output', callback);
event_source.addEventListener('start', callback);
event_source.addEventListener('output', handleStderrOutputForFlowr);
event_source.addEventListener('close', handleStderrOutputForFlowr);
event_source.addEventListener('status', function(event) {
showStatus(JSON.parse(event.data));
});
};
var evaluateCodeWithoutStreamedResponse = function(url, callback) {
var jqxhr = $.ajax({
dataType: 'json',
method: 'GET',
url: url
});
jqxhr.always(hideSpinner);
jqxhr.done(callback);
jqxhr.fail(ajaxError);
};
var findOrCreateOutputElement = function(index) {
if ($('#output-' + index).isPresent()) {
return $('#output-' + index);
} else {
var element = $('<pre>').attr('id', 'output-' + index);
$('#output').append(element);
return element;
}
};
var handleKeyPress = function(event) {
if (event.which === ALT_1_KEY_CODE) {
event.preventDefault();
showTab(0);
} else if (event.which === ALT_2_KEY_CODE) {
showWorkspaceTab(event);
} else if (event.which === ALT_3_KEY_CODE) {
event.preventDefault();
showTab(2);
} else if (event.which === ALT_4_KEY_CODE) {
event.preventDefault();
showTab(3);
} else if (event.which === ALT_R_KEY_CODE) {
event.preventDefault();
$('#run').trigger('click');
} else if (event.which === ALT_S_KEY_CODE) {
event.preventDefault();
$('#assess').trigger('click');
} else if (event.which === ALT_T_KEY_CODE) {
event.preventDefault();
$('#test').trigger('click');
}
};
var handleScoringResponse = function(response) {
printScoringResults(response);
var score = _.reduce(response, function(sum, result) {
return sum + result.score * result.weight;
}, 0).toFixed(2);
$('#score').data('score', score);
renderScore();
showTab(3);
};
var handleTestResponse = function(response) {
clearOutput();
printOutput(response[0], false, 0);
showStatus(response[0]);
showTab(2);
};
var hideSpinner = function() {
$('button i.fa').show();
$('button i.fa-spin').hide();
};
var initializeEditors = function() {
$('.editor').each(function(index, element) {
var editor = ace.edit(element);
editor.setReadOnly($(element).data('read-only') !== undefined);
editor.setShowPrintMargin(false);
editor.setTheme(THEME);
editors.push(editor);
var session = editor.getSession();
session.setMode($(element).data('mode'));
session.setTabSize($(element).data('indent-size'));
session.setUseSoftTabs(true);
session.setUseWrapMode(true);
});
};
var initializeEventHandlers = function() {
$(document).on('click', '#results a', showOutput);
$(document).on('keypress', handleKeyPress);
$('a[data-toggle="tab"]').on('show.bs.tab', storeTab);
$('#assess').on('click', scoreCode);
$('#create-file').on('click', showFileDialog);
$('#destroy-file').on('click', confirmDestroy);
$('#download').on('click', downloadCode);
$('#dropdown-render, #render').on('click', renderCode);
$('#dropdown-run, #run').on('click', runCode);
$('#dropdown-stop, #stop').on('click', stopCode);
$('#dropdown-test, #test').on('click', testCode);
$('#save').on('click', saveCode);
$('#start').on('click', showWorkspaceTab);
$('#start-over').on('click', confirmReset);
$('#submit').on('click', confirmSubmission);
};
var initializeFileTree = function() {
$('#files').jstree($('#files').data('entries'));
$('#files').on('click', 'li.jstree-leaf', function() {
active_file = {
filename: $(this).text(),
id: parseInt($(this).attr('id'))
};
var frame = $('.editor[data-file-id="' + active_file.id + '"]').parent();
showFrame(frame);
toggleButtonStates();
});
};
var initializeTooltips = function() {
$('[data-tooltip]').tooltip();
};
var printChunk = function(event) {
var output = JSON.parse(event.data);
if (output) {
printOutput(output, true, 0);
} else {
clearOutput();
$('#hint').fadeOut();
$('#flowrHint').fadeOut();
showTab(2);
}
};
var printOutput = function(output, colorize, index) {
var element = findOrCreateOutputElement(index);
if (!colorize) {
var stream = _.sortBy([output.stderr || '', output.stdout || ''], function(stream) {
return stream.length;
})[1];
element.append(stream);
} else if (output.stderr) {
element.addClass('text-warning').append(output.stderr);
} else if (output.stdout) {
element.addClass('text-success').append(output.stdout);
} else {
element.addClass('text-muted').text($('#output').data('message-no-output'));
}
};
var printScoringResult = function(result, index) {
$('#results').show();
var element = $('#dummies').children().first().clone();
element.removeClass('panel-default').addClass(result.stderr ? 'panel-danger' : (result.score === 1 ? 'panel-success' : 'panel-warning'));
element.find('.panel-title .number').text(index + 1);
element.find('.row .col-sm-9').eq(0).find('.number').eq(0).text(result.passed);
element.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count);
element.find('.row .col-sm-9').eq(1).find('.number').eq(0).text((result.score * result.weight).toFixed(2));
element.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight);
element.find('.row .col-sm-9').eq(2).text(result.message);
element.find('.row .col-sm-9').eq(3).find('a').attr('href', '#output-' + index);
$('#results ul').first().append(element);
};
var printScoringResults = function(response) {
$('#results ul').first().html('');
$('.test-count .number').html(response.length);
clearOutput();
_.each(response, function(result, index) {
printOutput(result, false, index);
printScoringResult(result, index);
});
};
var renderCode = function(event) {
event.preventDefault();
if ($('#render').is(':visible')) {
createSubmission(this, null, function(response) {
var url = response.render_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
var pop_up_window = window.open(url);
if (pop_up_window) {
pop_up_window.onerror = function(message) {
clearOutput();
printOutput({
stderr: message
}, true, 0);
sendError(message);
showTab(2);
};
}
});
}
};
var renderHint = function(object) {
var hint = object.data || object.hint;
if (hint) {
$('#hint .panel-body').text(hint);
$('#hint').fadeIn();
}
};
var renderProgressBar = function(score, maximum_score) {
var percentage = score / maximum_score * 100;
var progress_bar = $('#score .progress-bar');
progress_bar.removeClass();
if (percentage < ADEQUATE_PERCENTAGE) {
progress_bar.addClass('progress-bar progress-bar-danger');
} else if (percentage < SUCCESSFULL_PERCENTAGE) {
progress_bar.addClass('progress-bar progress-bar-warning');
} else {
progress_bar.addClass('progress-bar progress-bar-success');
}
progress_bar.attr({
'aria-valuemax': maximum_score,
'aria-valuemin': 0,
'aria-valuenow': score
});
progress_bar.css('width', percentage + '%');
};
var renderScore = function() {
var score = $('#score').data('score');
var maxium_score = $('#score').data('maximum-score');
$('.score').html((score || '?') + ' / ' + maxium_score);
renderProgressBar(score, maxium_score);
};
var resetCode = function() {
showSpinner(this);
$.ajax({
dataType: 'json',
method: 'GET',
url: $('#start-over').data('url')
}).success(function(response) {
hideSpinner();
_.each(editors, function(editor) {
var file_id = $(editor.container).data('file-id');
var file = _.find(response.files, function(file) {
return file.id === file_id;
});
editor.setValue(file.content);
});
});
};
var runCode = function(event) {
event.preventDefault();
if ($('#run').is(':visible')) {
createSubmission(this, null, function(response) {
$('#stop').data('url', response.stop_url);
running = true;
showSpinner($('#run'));
toggleButtonStates();
var url = response.run_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
evaluateCode(url, true, printChunk);
});
}
};
var saveCode = function(event) {
event.preventDefault();
createSubmission(this, null, function() {
$.flash.success({
text: $('#save').data('message-success')
});
});
};
var sendError = function(message) {
showSpinner($('#render'));
var jqxhr = $.ajax({
data: {
error: {
message: message
}
},
dataType: 'json',
method: 'POST',
url: $('#editor').data('errors-url')
});
jqxhr.always(hideSpinner);
jqxhr.success(renderHint);
};
var scoreCode = function(event) {
event.preventDefault();
createSubmission(this, null, function(response) {
showSpinner($('#assess'));
var url = response.score_url;
evaluateCode(url, false, handleScoringResponse);
});
};
var showFileDialog = function(event) {
event.preventDefault();
createSubmission(this, null, function(response) {
$('#code_ocean_file_context_id').val(response.id);
$('#modal-file').modal('show');
});
};
var showFrame = function(frame) {
active_frame = frame;
$('.frame').hide();
frame.show();
};
var showMainFile = function() {
var frame = $('.frame[data-role="main_file"]');
var file_id = frame.find('.editor').data('file-id');
active_file = {
filename: frame.data('filename'),
id: file_id
};
$('#files').jstree().select_node(file_id);
showFrame(frame);
toggleButtonStates();
};
var showOutput = function(event) {
event.preventDefault();
showTab(2);
$('#output').scrollTo($(this).attr('href'));
};
var showRequestedTab = function() {
var regexp = /tab=(\d+)/;
if (regexp.test(window.location.search)) {
var index = regexp.exec(window.location.search)[1] - 1;
} else {
var index = localStorage.tab;
}
showTab(index);
};
var showSpinner = function(initiator) {
$(initiator).find('i.fa, i.glyphicon').hide();
$(initiator).find('i.fa-spin').show();
};
var showStatus = function(output) {
if (output.status === 'timeout') {
$.flash.danger({
icon: ['fa', 'fa-clock-o'],
text: $('#editor').data('message-timeout')
});
} else if (output.stderr) {
$.flash.danger({
icon: ['fa', 'fa-bug'],
text: $('#run').data('message-failure')
});
} else {
$.flash.success({
icon: ['fa', 'fa-check'],
text: $('#run').data('message-success')
});
}
};
var showTab = function(index) {
$('a[data-toggle="tab"]').eq(index || 0).tab('show');
};
var showWorkspaceTab = function(event) {
event.preventDefault();
showTab(1);
};
var stopCode = function(event) {
event.preventDefault();
if ($('#stop').is(':visible')) {
var jqxhr = $.ajax({
data: {
container_id: $('#stop').data('container').id
},
dataType: 'json',
method: 'POST',
url: $('#stop').data('url')
});
jqxhr.always(function() {
hideSpinner();
running = false;
toggleButtonStates();
});
jqxhr.fail(ajaxError);
}
};
var storeContainerInformation = function(event) {
$('#stop').data('container', JSON.parse(event.data));
};
var storeTab = function(event) {
localStorage.tab = $(event.target).parent().index();
};
var submitCode = function() {
createSubmission($('#submit'), null, function(response) {
if (response.redirect) {
localStorage.removeItem('tab');
window.location = response.redirect;
}
});
};
var testCode = function(event) {
event.preventDefault();
if ($('#test').is(':visible')) {
createSubmission(this, null, function(response) {
showSpinner($('#test'));
var url = response.test_url.replace(FILENAME_URL_PLACEHOLDER, active_file.filename);
evaluateCode(url, false, handleTestResponse);
});
}
};
var toggleButtonStates = function() {
var is_renderable = active_frame.data('renderable') !== undefined;
var is_runnable = active_frame.data('executable') !== undefined && _.contains(['main_file', 'user_defined_file'], active_frame.data('role'));
var is_testable = active_frame.data('executable') !== undefined && _.contains(['teacher_defined_test', 'user_defined_test'], active_frame.data('role'));
$('#destroy-file').prop('disabled', active_frame.data('role') !== 'user_defined_file');
$('#dropdown-render').toggleClass('disabled', !is_renderable);
$('#dropdown-run').toggleClass('disabled', !(is_runnable && !running));
$('#dropdown-stop').toggleClass('disabled', !(is_runnable && running));
$('#dropdown-test').toggleClass('disabled', !is_testable);
$('#render').toggle(is_renderable);
$('#run').toggle(is_runnable && !running);
$('#stop').toggle(is_runnable && running);
$('#test').toggle(is_testable);
};
if ($('#editor').isPresent()) {
configureEditors();
initializeEditors();
initializeEventHandlers();
initializeFileTree();
initializeTooltips();
renderScore();
showMainFile();
showRequestedTab();
}
var stderrOutput = ''
var handleStderrOutputForFlowr = function(event) {
var json = JSON.parse(event.data);
if (json.stderr) {
stderrOutput += json.stderr;
} else if (json.code) {
var flowrHintBody = $('#flowrHint .panel-body')
jQuery.getJSON(flowrUrl + '&query=' + escape(stderrOutput), function(data) {
for (var question in data.queryResults) {
// replace everything, not only one occurence
var collapsibleTileHtml = flowrResultHtml.replace(/{{collapseId}}/g, 'collapse-' + question).replace(/{{headingId}}/g, 'heading-' + question)
var resultTile = $(collapsibleTileHtml)
resultTile.find('h4 > a').text(data.queryResults[question].title)
resultTile.find('.panel-body').append($(data.queryResults[question].body))
flowrHintBody.append(resultTile)
}
$('#flowrHint').fadeIn()
})
stderrOutput = ''
}
};
});

View File

@ -0,0 +1,7 @@
$(function() {
if ($.isController('execution_environments')) {
if ($('.edit_execution_environment, .new_execution_environment').isPresent()) {
new MarkdownEditor('#execution_environment_help');
}
}
});

View File

@ -0,0 +1,85 @@
$(function() {
var ACE_FILES_PATH = '/assets/ace/';
var TAB_KEY_CODE = 9;
var addFileForm = function(event) {
event.preventDefault();
var element = $('#dummies').children().first().clone();
var html = $('<div>').append(element).html().replace(/index/g, new Date().getTime());
$('#files').append(html);
$('#files select').chosen({
disable_search_threshold: 5,
search_contains: true
});
$('body, html').scrollTo('#add-file');
};
var enableInlineFileCreation = function() {
$('#add-file').on('click', addFileForm);
$('form.edit_exercise, form.new_exercise').on('submit', function() {
$('#dummies').html('');
});
};
var highlightCode = function() {
$('pre code').each(function(index, element) {
hljs.highlightBlock(element);
});
};
var inferFileAttributes = function() {
$(document).on('change', 'input[type="file"]', function(event) {
var filename = $(this).val().split(/\\|\//g).pop();
var parent = $(this).parents('li');
parent.find('input[type="text"]').first().val(filename.split('.')[0]);
});
};
var insertTabAtCursor = function(textarea) {
var selection_start = textarea.get(0).selectionStart;
var selection_end = textarea.get(0).selectionEnd;
textarea.val(textarea.val().substring(0, selection_start) + "\t" + textarea.val().substring(selection_end));
textarea.get(0).selectionStart = selection_start + 1;
textarea.get(0).selectionEnd = selection_start + 1;
};
var observeFileRoleChanges = function() {
$(document).on('change', 'select[name$="[role]"]', function() {
var is_test_file = $(this).val() === 'teacher_defined_test';
var parent = $(this).parents('.panel');
parent.find('[name$="[feedback_message]"]').attr('disabled', !is_test_file);
parent.find('[name$="[weight]"]').attr('disabled', !is_test_file);
});
};
var overrideTextareaTabBehavior = function() {
$('.form-group textarea').on('keydown', function(event) {
if (event.which === TAB_KEY_CODE) {
event.preventDefault();
insertTabAtCursor($(this));
}
});
};
var toggleCodeHeight = function() {
$('code').on('click', function() {
$(this).css({
'max-height': 'initial'
})
});
};
if ($.isController('exercises')) {
if ($('.edit_exercise, .new_exercise').isPresent()) {
new MarkdownEditor('#exercise_instructions');
enableInlineFileCreation();
inferFileAttributes();
observeFileRoleChanges();
overrideTextareaTabBehavior();
}
toggleCodeHeight();
if (window.hljs) {
highlightCode();
}
}
});

View File

@ -0,0 +1,21 @@
$(function() {
$('form').on('click', '.toggle-input', function(event) {
event.preventDefault();
if (!$(this).hasClass('disabled')) {
$(this).hide();
var parent = $(this).parents('.form-group');
var original_input = parent.find('input:not(disabled), select:not(disabled), textarea:not(disabled), .chosen-container');
original_input.attr('disabled', true);
original_input.hide();
var alternative_input = parent.find('.alternative-input');
alternative_input.attr('disabled', false);
alternative_input.show();
alternative_input.trigger('click');
}
});
$('select:visible').chosen({
disable_search_threshold: 5,
search_contains: true
});
});

View File

@ -0,0 +1,16 @@
(function() {
var ACE_FILES_PATH = '/assets/ace/';
window.MarkdownEditor = function(selector) {
ace.config.set('modePath', ACE_FILES_PATH);
var editor = ace.edit($(selector).next()[0]);
editor.on('change', function() {
$(selector).val(editor.getValue());
});
editor.setShowPrintMargin(false);
var session = editor.getSession();
session.setMode('markdown');
session.setUseWrapMode(true);
session.setValue($(selector).val());
};
})();

View File

@ -0,0 +1,69 @@
$(function() {
var ENTER_KEY_CODE = 13;
var clearOutput = function() {
$('#output').html('');
};
var executeCommand = function(command) {
$.ajax({
data: {
command: command
},
method: 'POST',
url: $('#shell').data('url')
}).done(handleResponse);
};
var handleKeyPress = function(event) {
if (event.which === ENTER_KEY_CODE) {
var command = $(this).val();
if (command === 'clear') {
clearOutput();
} else {
printCommand(command);
executeCommand(command);
}
$(this).val('');
}
};
var handleResponse = function(response) {
if (response.status === 'ok') {
printOutput(response);
} else if (response.status === 'timeout') {
printTimeout(response);
}
};
var printCommand = function(command) {
$('#output').append('<p><em>' + command + '</em></p>');
};
var printOutput = function(output) {
var element = $('<p>');
if (output.stderr) {
element.addClass('text-warning');
element.html(output.stderr);
} else if (output.stdout) {
element.addClass('text-success');
element.html(output.stdout);
} else {
element.addClass('text-muted');
element.html($('#output').data('message-no-output'));
}
$('#output').append(element);
};
var printTimeout = function(output) {
var element = $.append('<p>');
element.addClass('text-danger');
element.html($('#shell').data('message-timeout'));
$('#output').append(element);
};
if ($('#shell').isPresent()) {
$('#command').focus();
$('#command').on('keypress', handleKeyPress);
}
});

View File

@ -0,0 +1,17 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any styles
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
* file per style scope.
*
*= require_tree .
*= require_tree ../../../lib
*= require_tree ../../../vendor
*= require_self
*/

View File

@ -0,0 +1,50 @@
h1 {
margin-bottom: 1em;
}
i.fa, i.glyphicon {
margin-right: 0.5em;
}
pre {
background-color: #FAFAFA;
margin: 0;
padding: 0;
}
span.caret {
margin-left: 0.5em;
}
.progress {
margin: 0;
.progress-bar {
line-height: initial;
}
}
.attribute-row + .attribute-row{
margin-top: 0.5em;
}
.badge {
font-size: 100%;
}
.disabled {
cursor: default;
opacity: 0.5;
}
.empty {
opacity: 0.5;
}
.flash {
font-size: 100%;
}
.markdown {
height: 200px;
}

View File

@ -0,0 +1,69 @@
button i.fa-spin {
display: none;
}
.editor {
height: 100%;
width: 100%;
}
.frame {
display: none;
height: 400px;
audio, img, video {
max-width: 100%;
}
}
#editor-buttons {
background-color: #008CBA;
margin-top: 1em;
width: 100%;
button {
font-size: 80%;
}
button, .btn-group {
width: 25%;
}
.btn-group {
button {
width: 75%;
}
button.dropdown-toggle {
width: 25%;
}
}
}
#files {
overflow: scroll;
}
#hint {
display: none;
}
#outputInformation {
#output {
max-height: 500px;
overflow: auto;
margin: 2em 0;
p {
margin: 0.5em;
}
pre + pre {
margin-top: 1em;
}
}
}
#results {
display: none;
}

View File

@ -0,0 +1,13 @@
code {
background-color: #F8F8F8 !important;
max-height: 100px;
overflow: scroll;
}
input[type='file'] {
display: none;
}
#exercise_template_code {
font-family: monospace;
}

View File

@ -0,0 +1,6 @@
#flowrHint {
display: none;
}
#flowrOutput {
}

View File

@ -0,0 +1,25 @@
.alternative-input {
display: none;
}
.chosen-container {
width: 100% !important;
}
.code-field {
font-family: monospace;
}
.filter-form {
.form-group:not(:last-child) {
margin-right: 1em;
}
input, select {
min-width: 200px !important;
}
}
.toggle-input {
font-size: 80%;
}

View File

@ -0,0 +1,33 @@
class ApplicationController < ActionController::Base
include ApplicationHelper
include Pundit
MEMBER_ACTIONS = [:destroy, :edit, :show, :update]
after_action :verify_authorized, except: [:help, :welcome]
before_action :set_locale
protect_from_forgery(with: :exception)
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
def current_user
@current_user ||= ExternalUser.find_by(id: session[:external_user_id]) || login_from_session || login_from_other_sources
end
def help
end
def render_not_authorized
flash[:danger] = t('application.not_authorized')
redirect_to(:root)
end
private :render_not_authorized
def set_locale
session[:locale] = params[:locale] if params[:locale]
I18n.locale = session[:locale] || I18n.default_locale
end
private :set_locale
def welcome
end
end

View File

@ -0,0 +1,39 @@
module CodeOcean
class FilesController < ApplicationController
include FileParameters
def authorize!
authorize(@file)
end
private :authorize!
def create
@file = CodeOcean::File.new(file_params)
authorize!
respond_to do |format|
if @file.save
format.html { redirect_to(implement_exercise_path(@file.context.exercise, tab: 2), notice: t('shared.object_created', model: File.model_name.human)) }
format.json { render(:show, location: @file, status: :created) }
else
format.html { render(:new) }
format.json { render(json: @file.errors, status: :unprocessable_entity) }
end
end
end
def destroy
@file = CodeOcean::File.find(params[:id])
authorize!
@file.destroy
respond_to do |format|
format.html { redirect_to(@file.context, notice: t('shared.object_destroyed', model: File.model_name.human)) }
format.json { head(:no_content) }
end
end
def file_params
params[:code_ocean_file].permit(file_attributes).merge(context_type: 'Submission', role: 'user_defined_file')
end
private :file_params
end
end

View File

View File

@ -0,0 +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]
end
private :file_attributes
end

View File

@ -0,0 +1,131 @@
require 'oauth/request_proxy/rack_request'
module Lti
extend ActiveSupport::Concern
MAXIMUM_SCORE = 1
MAXIMUM_SESSION_AGE = 60.minutes
SESSION_PARAMETERS = %w[launch_presentation_return_url lis_outcome_service_url lis_result_sourcedid]
def build_tool_provider(options = {})
if options[:consumer] && options[:parameters]
IMS::LTI::ToolProvider.new(options[:consumer].oauth_key, options[:consumer].oauth_secret, options[:parameters])
end
end
private :build_tool_provider
def clear_lti_session_data
session.delete(:consumer_id)
session.delete(:external_user_id)
session.delete(:lti_parameters)
end
private :clear_lti_session_data
def consumer_return_url(provider, options = {})
consumer_return_url = provider.try(:launch_presentation_return_url) || params[:launch_presentation_return_url]
consumer_return_url += "?#{options.to_query}" if consumer_return_url && options.present?
consumer_return_url
end
def external_user_email(provider)
provider.lis_person_contact_email_primary
end
private :external_user_email
def external_user_name(provider)
if provider.lis_person_name_full
provider.lis_person_name_full
elsif provider.lis_person_name_given && provider.lis_person_name_family
"#{provider.lis_person_name_given} #{provider.lis_person_name_family}"
else
provider.lis_person_name_given || provider.lis_person_name_family
end
end
private :external_user_name
def lti_outcome_service?
session[:lti_parameters].try(:has_key?, 'lis_outcome_service_url')
end
private :lti_outcome_service?
def refuse_lti_launch(options = {})
return_to_consumer(lti_errorlog: options[:message], lti_errormsg: t('sessions.oauth.failure'))
end
private :refuse_lti_launch
def require_oauth_parameters
refuse_lti_launch(message: t('sessions.oauth.missing_parameters')) unless params[:oauth_consumer_key] && params[:oauth_signature]
end
private :require_oauth_parameters
def require_unique_oauth_nonce
refuse_lti_launch(message: t('sessions.oauth.used_nonce')) if NonceStore.has?(params[:oauth_nonce])
end
private :require_unique_oauth_nonce
def require_valid_consumer_key
@consumer = Consumer.find_by(oauth_key: params[:oauth_consumer_key])
refuse_lti_launch(message: t('sessions.oauth.invalid_consumer')) unless @consumer
end
private :require_valid_consumer_key
def require_valid_exercise_token
@exercise = Exercise.find_by(token: params[:custom_token])
refuse_lti_launch(message: t('sessions.oauth.invalid_exercise_token')) unless @exercise
end
private :require_valid_exercise_token
def require_valid_oauth_signature
@provider = build_tool_provider(consumer: @consumer, parameters: params)
refuse_lti_launch(message: t('sessions.oauth.invalid_signature')) unless @provider.valid_request?(request)
end
private :require_valid_oauth_signature
def return_to_consumer(options = {})
consumer_return_url = @provider.try(:launch_presentation_return_url) || params[:launch_presentation_return_url]
if consumer_return_url
consumer_return_url += "?#{options.to_query}" if options.present?
redirect_to(consumer_return_url)
else
flash[:danger] = options[:lti_errormsg]
flash[:info] = options[:lti_msg]
redirect_to(:root)
end
end
private :return_to_consumer
def send_score(score)
raise Error.new("Score #{score} must be between 0 and #{MAXIMUM_SCORE}!") unless (0..MAXIMUM_SCORE).include?(score)
provider = build_tool_provider(consumer: Consumer.find_by(id: session[:consumer_id]), parameters: session[:lti_parameters])
if provider.nil?
{status: 'error'}
elsif provider.outcome_service?
response = provider.post_replace_result!(score)
{code: response.response_code, message: response.post_response.body, status: response.code_major}
else
{status: 'unsupported'}
end
end
private :send_score
def set_current_user
@current_user = ExternalUser.find_or_create_by(consumer_id: @consumer.id, external_id: @provider.user_id)
@current_user.update(email: external_user_email(@provider), name: external_user_name(@provider))
end
private :set_current_user
def store_lti_session_data(options = {})
session[:consumer_id] = options[:consumer].id
session[:external_user_id] = @current_user.id
session[:lti_parameters] = options[:parameters].slice(*SESSION_PARAMETERS)
end
private :store_lti_session_data
def store_nonce(nonce)
NonceStore.add(nonce)
end
private :store_nonce
class Error < RuntimeError
end
end

View File

@ -0,0 +1,20 @@
module SubmissionParameters
include FileParameters
def reject_illegal_file_attributes!(submission_params)
if exercise = Exercise.find_by(id: submission_params[:exercise_id])
submission_params[:files_attributes].try(:reject!) do |index, file_attributes|
file = CodeOcean::File.find_by(id: file_attributes[:file_id])
file.nil? || file.hidden || file.read_only
end
end
end
private :reject_illegal_file_attributes!
def submission_params
submission_params = params[:submission].permit(:cause, :exercise_id, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
reject_illegal_file_attributes!(submission_params)
submission_params
end
private :submission_params
end

View File

@ -0,0 +1,19 @@
module SubmissionScoring
def execute_test_files(submission)
submission.collect_files.select(&:teacher_defined_test?).map do |file|
output = @docker_client.execute_test_command(submission, file.name_with_extension)
output.merge!(@assessor.assess(output))
output.merge!(filename: file.name_with_extension, message: output[:score] == Assessor::MAXIMUM_SCORE ? I18n.t('exercises.implement.default_feedback') : file.feedback_message, weight: file.weight)
end
end
private :execute_test_files
def score_submission(submission)
@assessor = Assessor.new(execution_environment: submission.execution_environment)
@docker_client = DockerClient.new(execution_environment: submission.execution_environment, user: current_user)
outputs = execute_test_files(submission)
score = outputs.map { |output| output[:score] * output[:weight] }.reduce(:+)
submission.update(score: score)
outputs
end
end

View File

@ -0,0 +1,69 @@
class ConsumersController < ApplicationController
before_action :set_consumer, only: MEMBER_ACTIONS
def authorize!
authorize(@consumer || @consumers)
end
private :authorize!
def create
@consumer = Consumer.new(consumer_params)
authorize!
respond_to do |format|
if @consumer.save
format.html { redirect_to(@consumer, notice: t('shared.object_created', model: Consumer.model_name.human)) }
format.json { render(:show, location: @consumer, status: :created) }
else
format.html { render(:new) }
format.json { render(json: @consumer.errors, status: :unprocessable_entity) }
end
end
end
def destroy
@consumer.destroy
respond_to do |format|
format.html { redirect_to(consumers_url, notice: t('shared.object_destroyed', model: Consumer.model_name.human)) }
format.json { head(:no_content) }
end
end
def edit
end
def consumer_params
params[:consumer].permit(:name, :oauth_key, :oauth_secret)
end
private :consumer_params
def index
@consumers = Consumer.all
authorize!
end
def new
@consumer = Consumer.new(oauth_key: SecureRandom.hex, oauth_secret: SecureRandom.hex)
authorize!
end
def set_consumer
@consumer = Consumer.find(params[:id])
authorize!
end
private :set_consumer
def show
end
def update
respond_to do |format|
if @consumer.update(consumer_params)
format.html { redirect_to(@consumer, notice: t('shared.object_updated', model: Consumer.model_name.human)) }
format.json { render(:show, location: @consumer, status: :ok) }
else
format.html { render(:edit) }
format.json { render(json: @consumer.errors, status: :unprocessable_entity) }
end
end
end
end

View File

@ -0,0 +1,43 @@
class ErrorsController < ApplicationController
before_action :set_execution_environment
def authorize!
authorize(@error || Error.where(execution_environment_id: @execution_environment.id))
end
private :authorize!
def create
@error = Error.new(error_params)
authorize!
hint = Whistleblower.new(execution_environment: @error.execution_environment).generate_hint(@error.message)
respond_to do |format|
format.json do
if hint
render(json: {hint: hint})
else
render(nothing: true, status: @error.save ? :created : :unprocessable_entity)
end
end
end
end
def error_params
params[:error].permit(:message).merge(execution_environment_id: @execution_environment.id)
end
private :error_params
def index
authorize!
@errors = Error.for_execution_environment(@execution_environment)
end
def set_execution_environment
@execution_environment = ExecutionEnvironment.find(params[:execution_environment_id])
end
private :set_execution_environment
def show
@error = Error.find(params[:id])
authorize!
end
end

View File

@ -0,0 +1,98 @@
class ExecutionEnvironmentsController < ApplicationController
before_action :set_docker_images, only: [:create, :edit, :new, :update]
before_action :set_execution_environment, only: MEMBER_ACTIONS + [:execute_command, :shell]
before_action :set_testing_framework_adapters, only: [:create, :edit, :new, :update]
def authorize!
authorize(@execution_environment || @execution_environments)
end
private :authorize!
def create
@execution_environment = ExecutionEnvironment.new(execution_environment_params)
authorize!
respond_to do |format|
if @execution_environment.save
format.html { redirect_to(@execution_environment, notice: t('shared.object_created', model: ExecutionEnvironment.model_name.human)) }
format.json { render(:show, location: @execution_environment, status: :created) }
else
format.html { render(:new) }
format.json { render(json: @execution_environment.errors, status: :unprocessable_entity) }
end
end
end
def destroy
@execution_environment.destroy
respond_to do |format|
format.html { redirect_to(execution_environments_url, notice: t('shared.object_destroyed', model: ExecutionEnvironment.model_name.human)) }
format.json { head(:no_content) }
end
end
def edit
end
def execute_command
@docker_client = DockerClient.new(execution_environment: @execution_environment, user: current_user)
render(json: @docker_client.execute_command(params[:command]))
end
def execution_environment_params
params[:execution_environment].permit(:docker_image, :exposed_ports, :editor_mode, :file_extension, :help, :indent_size, :name, :permitted_execution_time, :run_command, :test_command, :testing_framework).merge(user_id: current_user.id, user_type: current_user.class.name)
end
private :execution_environment_params
def index
@execution_environments = ExecutionEnvironment.all.order(:name)
authorize!
end
def new
@execution_environment = ExecutionEnvironment.new
authorize!
end
def set_docker_images
@docker_images = DockerClient.image_tags.sort
rescue DockerClient::Error => error
@docker_images = []
flash[:warning] = error.message
end
private :set_docker_images
def set_execution_environment
@execution_environment = ExecutionEnvironment.find(params[:id])
authorize!
end
private :set_execution_environment
def set_testing_framework_adapters
Rails.application.eager_load!
@testing_framework_adapters = TestingFrameworkAdapter.descendants.sort_by(&:framework_name).map do |klass|
[klass.framework_name, klass.name]
end
end
private :set_testing_framework_adapters
def shell
end
def show
if @execution_environment.testing_framework?
@testing_framework_adapter = Kernel.const_get(@execution_environment.testing_framework)
end
end
def update
respond_to do |format|
if @execution_environment.update(execution_environment_params)
format.html { redirect_to(@execution_environment, notice: t('shared.object_updated', model: ExecutionEnvironment.model_name.human)) }
format.json { render(:show, location: @execution_environment, status: :ok) }
else
format.html { render(:edit) }
format.json { render(json: @execution_environment.errors, status: :unprocessable_entity) }
end
end
end
end

View File

@ -0,0 +1,152 @@
class ExercisesController < ApplicationController
include Lti
include SubmissionParameters
include SubmissionScoring
before_action :handle_file_uploads, only: [:create, :update]
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit]
before_action :set_file_types, only: [:create, :edit, :new, :update]
def authorize!
authorize(@exercise || @exercises)
end
private :authorize!
def clone
exercise = @exercise.duplicate(public: false, user: current_user)
if exercise.save
redirect_to(exercise, notice: t('shared.object_cloned', model: Exercise.model_name.human))
else
flash[:danger] = t('shared.message_failure')
redirect_to(exercises_path)
end
end
def create
@exercise = Exercise.new(exercise_params)
authorize!
respond_to do |format|
if @exercise.save
format.html { redirect_to(@exercise, notice: t('shared.object_created', model: Exercise.model_name.human)) }
format.json { render(:show, location: @exercise, status: :created) }
else
format.html { render(:new) }
format.json { render(json: @exercise.errors, status: :unprocessable_entity) }
end
end
end
def destroy
@exercise.destroy
respond_to do |format|
format.html { redirect_to(exercises_url, notice: t('shared.object_destroyed', model: Exercise.model_name.human)) }
format.json { head(:no_content) }
end
end
def edit
end
def exercise_params
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
end
private :exercise_params
def handle_file_uploads
exercise_params[:files_attributes].try(:each) do |index, file_attributes|
if file_attributes[:content].respond_to?(:read)
file_params = params[:exercise][:files_attributes][index]
if FileType.find_by(id: file_attributes[:file_type_id]).try(:binary?)
file_params[:content] = nil
file_params[:native_file] = file_attributes[:content]
else
file_params[:content] = file_attributes[:content].read
end
end
end
end
private :handle_file_uploads
def implement
if Submission.exists?(exercise_id: @exercise.id, user_id: current_user.id)
@submission = Submission.where(exercise_id: @exercise.id, user_id: current_user.id).order('created_at DESC').first
@files = @submission.collect_files.select(&:visible)
else
@files = @exercise.files.visible
end
@files = @files.sort_by(&:name_with_extension)
end
def index
@search = policy_scope(Exercise).search(params[:q])
@exercises = @search.result.order(:title)
authorize!
end
def redirect_to_lti_return_path
path = lti_return_path(consumer_id: session[:consumer_id], submission_id: @submission.id, url: consumer_return_url(build_tool_provider(consumer: Consumer.find_by(id: session[:consumer_id]), parameters: session[:lti_parameters])))
respond_to do |format|
format.html { redirect_to(path) }
format.json { render(json: {redirect: path}) }
end
end
private :redirect_to_lti_return_path
def new
@exercise = Exercise.new
authorize!
end
def set_execution_environments
@execution_environments = ExecutionEnvironment.all.order(:name)
end
private :set_execution_environments
def set_exercise
@exercise = Exercise.find(params[:id])
authorize!
end
private :set_exercise
def set_file_types
@file_types = FileType.all.order(:name)
end
private :set_file_types
def show
end
def statistics
end
def submit
@submission = Submission.create(submission_params)
score_submission(@submission)
if lti_outcome_service?
response = send_score(@submission.normalized_score)
if response[:status] == 'success'
redirect_to_lti_return_path
else
respond_to do |format|
format.html { redirect_to(implement_exercise_path(@submission.exercise)) }
format.json { render(json: {message: I18n.t('exercises.submit.failure')}, status: 503) }
end
end
else
redirect_to_lti_return_path
end
end
def update
respond_to do |format|
if @exercise.update(exercise_params)
format.html { redirect_to(@exercise, notice: t('shared.object_updated', model: Exercise.model_name.human)) }
format.json { render(:show, location: @exercise, status: :ok) }
else
format.html { render(:edit) }
format.json { render(json: @exercise.errors, status: :unprocessable_entity) }
end
end
end
end

View File

@ -0,0 +1,16 @@
class ExternalUsersController < ApplicationController
def authorize!
authorize(@user || @users)
end
private :authorize!
def index
@users = ExternalUser.all
authorize!
end
def show
@user = ExternalUser.find(params[:id])
authorize!
end
end

View File

@ -0,0 +1,78 @@
class FileTypesController < ApplicationController
before_action :set_editor_modes, only: [:create, :edit, :new, :update]
before_action :set_file_type, only: MEMBER_ACTIONS
def authorize!
authorize(@file_type || @file_types)
end
private :authorize!
def create
@file_type = FileType.new(file_type_params)
authorize!
respond_to do |format|
if @file_type.save
format.html { redirect_to(@file_type, notice: t('shared.object_created', model: FileType.model_name.human)) }
format.json { render(:show, location: @file_type, status: :created) }
else
format.html { render(:new) }
format.json { render(json: @file_type.errors, status: :unprocessable_entity) }
end
end
end
def destroy
@file_type.destroy
respond_to do |format|
format.html { redirect_to(file_types_url, notice: t('shared.object_destroyed', model: FileType.model_name.human)) }
format.json { head(:no_content) }
end
end
def edit
end
def file_type_params
params[:file_type].permit(:binary, :editor_mode, :executable, :file_extension, :name, :indent_size, :renderable).merge(user_id: current_user.id, user_type: current_user.class.name)
end
private :file_type_params
def index
@file_types = FileType.all.order(:name)
authorize!
end
def new
@file_type = FileType.new
authorize!
end
def set_editor_modes
@editor_modes = Dir.glob('vendor/assets/javascripts/ace/mode-*.js').map do |filename|
name = filename.gsub(/\w+\/|mode-|.js$/, '')
[name, "ace/mode/#{name}"]
end
end
private :set_editor_modes
def set_file_type
@file_type = FileType.find(params[:id])
authorize!
end
private :set_file_type
def show
end
def update
respond_to do |format|
if @file_type.update(file_type_params)
format.html { redirect_to(@file_type, notice: t('shared.object_updated', model: FileType.model_name.human)) }
format.json { render(:show, location: @file_type, status: :ok) }
else
format.html { render(:edit) }
format.json { render(json: @file_type.errors, status: :unprocessable_entity) }
end
end
end
end

View File

@ -0,0 +1,75 @@
class HintsController < ApplicationController
before_action :set_execution_environment
before_action :set_hint, only: MEMBER_ACTIONS
def authorize!
authorize(@hint || @hints)
end
private :authorize!
def create
@hint = Hint.new(hint_params)
authorize!
respond_to do |format|
if @hint.save
format.html { redirect_to(execution_environment_hint_path(@execution_environment, @hint.id), notice: t('shared.object_created', model: Hint.model_name.human)) }
format.json { render(:show, location: @hint, status: :created) }
else
format.html { render(:new) }
format.json { render(json: @hint.errors, status: :unprocessable_entity) }
end
end
end
def destroy
@hint.destroy
respond_to do |format|
format.html { redirect_to(execution_environment_hints_path(@execution_environment), notice: t('shared.object_destroyed', model: Hint.model_name.human)) }
format.json { head(:no_content) }
end
end
def edit
end
def hint_params
params[:hint].permit(:locale, :message, :name, :regular_expression).merge(execution_environment_id: @execution_environment.id)
end
private :hint_params
def index
@hints = Hint.where(execution_environment_id: @execution_environment.id).order(:name)
authorize!
end
def new
@hint = Hint.new
authorize!
end
def set_execution_environment
@execution_environment = ExecutionEnvironment.find(params[:execution_environment_id])
end
private :set_execution_environment
def set_hint
@hint = Hint.find(params[:id])
authorize!
end
private :set_hint
def show
end
def update
respond_to do |format|
if @hint.update(hint_params)
format.html { redirect_to(execution_environment_hint_path(params[:execution_environment_id], @hint.id), notice: t('shared.object_updated', model: Hint.model_name.human)) }
format.json { render(:show, location: @hint, status: :ok) }
else
format.html { render(:edit) }
format.json { render(json: @hint.errors, status: :unprocessable_entity) }
end
end
end
end

View File

@ -0,0 +1,131 @@
class InternalUsersController < ApplicationController
before_action :require_activation_token, only: :activate
before_action :require_reset_password_token, only: :reset_password
before_action :set_user, only: MEMBER_ACTIONS
skip_before_action :verify_authenticity_token, only: :activate
skip_after_action :verify_authorized, only: [:activate, :forgot_password, :reset_password]
def activate
if request.patch? || request.put?
respond_to do |format|
if @user.update(params[:internal_user].permit(:password, :password_confirmation))
@user.activate!
format.html { redirect_to(sign_in_path, notice: t('.success')) }
format.json { render(nothing: true, status: :ok) }
else
format.html { render(:activate) }
format.json { render(json: @user.errors, status: :unprocessable_entity) }
end
end
end
end
def authorize!
authorize(@user || @users)
end
private :authorize!
def create
@user = InternalUser.new(internal_user_params)
authorize!
@user.send(:setup_activation)
respond_to do |format|
if @user.save
@user.send(:send_activation_needed_email!)
format.html { redirect_to(@user, notice: t('shared.object_created', model: InternalUser.model_name.human)) }
format.json { render(:show, location: @user, status: :created) }
else
format.html { render(:new) }
format.json { render(json: @user.errors, status: :unprocessable_entity) }
end
end
end
def destroy
@user.destroy
respond_to do |format|
format.html { redirect_to(internal_users_url, notice: t('shared.object_destroyed', model: InternalUser.model_name.human)) }
format.json { head(:no_content) }
end
end
def edit
end
def forgot_password
if request.get? && current_user
flash[:warning] = t('shared.already_signed_in')
redirect_to(:root)
elsif request.post?
if params[:email].present?
InternalUser.find_by(email: params[:email]).try(:deliver_reset_password_instructions!)
flash[:notice] = t('.success')
redirect_to(:root)
end
end
end
def index
@search = InternalUser.search(params[:q])
@users = @search.result.order(:name)
authorize!
end
def internal_user_params
params[:internal_user].permit(:consumer_id, :email, :name, :role)
end
private :internal_user_params
def new
@user = InternalUser.new
authorize!
end
def require_activation_token
@user = InternalUser.load_from_activation_token(params[:token] || params[:internal_user].try(:[], :activation_token))
render_not_authorized unless @user
end
private :require_activation_token
def require_reset_password_token
@user = InternalUser.load_from_reset_password_token(params[:token] || params[:internal_user].try(:[], :reset_password_token))
render_not_authorized unless @user
end
private :require_reset_password_token
def reset_password
if request.patch? || request.put?
respond_to do |format|
if @user.update(params[:internal_user].permit(:password, :password_confirmation))
@user.change_password!(params[:internal_user][:password])
format.html { redirect_to(sign_in_path, notice: t('.success')) }
format.json { render(nothing: true, status: :ok) }
else
format.html { render(:reset_password) }
format.json { render(json: @user.errors, status: :unprocessable_entity) }
end
end
end
end
def set_user
@user = InternalUser.find(params[:id])
authorize!
end
private :set_user
def show
end
def update
respond_to do |format|
if @user.update(internal_user_params)
format.html { redirect_to(@user, notice: t('shared.object_updated', model: InternalUser.model_name.human)) }
format.json { render(:show, location: @user, status: :ok) }
else
format.html { render(:edit) }
format.json { render(json: @user.errors, status: :unprocessable_entity) }
end
end
end
end

View File

@ -0,0 +1,49 @@
class SessionsController < ApplicationController
include Lti
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_valid_exercise_token].each do |method_name|
before_action(method_name, only: :create_through_lti)
end
skip_after_action :verify_authorized
skip_before_action :verify_authenticity_token, only: :create_through_lti
def create
if user = login(params[:email], params[:password], params[:remember_me])
redirect_back_or_to(:root, notice: t('.success'))
else
flash.now[:danger] = t('.failure')
render(:new)
end
end
def create_through_lti
set_current_user
store_lti_session_data(consumer: @consumer, parameters: params)
store_nonce(params[:oauth_nonce])
flash[:notice] = I18n.t("sessions.create_through_lti.session_#{lti_outcome_service? ? 'with' : 'without'}_outcome", consumer: @consumer)
redirect_to(implement_exercise_path(@exercise.id))
end
def destroy
if current_user.external?
clear_lti_session_data
else
logout
end
redirect_to(:root, notice: t('.success'))
end
def destroy_through_lti
@consumer = Consumer.find_by(id: params[:consumer_id])
@submission = Submission.find(params[:submission_id])
clear_lti_session_data
end
def new
if current_user
flash[:warning] = t('shared.already_signed_in')
redirect_to(:root)
end
end
end

View File

@ -0,0 +1,145 @@
class SubmissionsController < ApplicationController
include ActionController::Live
include Lti
include SubmissionParameters
include SubmissionScoring
around_action :with_server_sent_events, only: :run
before_action :set_submission, only: [: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_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]
def authorize!
authorize(@submission || @submissions)
end
private :authorize!
def create
@submission = Submission.new(submission_params)
authorize!
respond_to do |format|
format.json do
if @submission.save
render(:show, location: @submission, status: :created)
else
render(nothing: true, status: :unprocessable_entity)
end
end
end
end
def download_file
if @file.native_file?
send_file(@file.native_file.path)
else
send_data(@file.content, filename: @file.name_with_extension)
end
end
def index
@search = Submission.search(params[:q])
@submissions = @search.result.paginate(page: params[:page])
authorize!
end
def render_file
if @file.native_file?
send_file(@file.native_file.path, disposition: 'inline')
else
render(text: @file.content)
end
end
def run
container_id = nil
stderr = ''
output = @docker_client.execute_run_command(@submission, params[:filename]) do |stream, chunk|
unless container_id
container_id = @docker_client.container_id
@server_sent_event.write({id: container_id, ports: @docker_client.assigned_ports}, event: 'info')
end
@server_sent_event.write({stream => chunk}, event: 'output')
stderr += chunk if stream == :stderr
end
@server_sent_event.write(output, event: 'status')
if stderr.present?
if hint = Whistleblower.new(execution_environment: @submission.execution_environment).generate_hint(stderr)
@server_sent_event.write(hint, event: 'hint')
else
store_error(stderr)
end
end
end
def score
render(json: score_submission(@submission))
end
def set_docker_client
@docker_client = DockerClient.new(execution_environment: @submission.execution_environment, user: current_user)
end
private :set_docker_client
def set_file
@file = @files.detect { |file| file.name_with_extension == params[:filename] }
render(nothing: true, status: 404) unless @file
end
private :set_file
def set_files
@files = @submission.collect_files.select(&:visible)
end
private :set_files
def set_mime_type
@mime_type = Mime::Type.lookup_by_extension(@file.file_type.file_extension.gsub(/^\./, ''))
response.headers['Content-Type'] = @mime_type.to_s
end
private :set_mime_type
def set_submission
@submission = Submission.find(params[:id])
authorize!
end
private :set_submission
def show
end
def statistics
end
def stop
container = Docker::Container.get(params[:container_id])
DockerClient.destroy_container(container)
rescue Docker::Error::NotFoundError
ensure
render(nothing: true)
end
def store_error(stderr)
::Error.create(execution_environment_id: @submission.exercise.execution_environment_id, message: stderr)
end
private :store_error
def test
output = @docker_client.execute_test_command(@submission, params[:filename])
render(json: [output])
end
def with_server_sent_events
response.headers['Content-Type'] = 'text/event-stream'
@server_sent_event = SSE.new(response.stream)
@server_sent_event.write(nil, event: 'start')
yield
@server_sent_event.write({code: 200}, event: 'close')
rescue
@server_sent_event.write({code: 500}, event: 'close')
ensure
@server_sent_event.close
end
private :with_server_sent_events
end

View File

@ -0,0 +1,78 @@
module ApplicationHelper
APPLICATION_NAME = 'Code Ocean'
def application_name
APPLICATION_NAME
end
def code_tag(code)
if code.present?
content_tag(:pre) do
content_tag(:code, code)
end
else
empty
end
end
def empty
content_tag(:i, nil, class: 'empty fa fa-minus')
end
def label_column(label)
content_tag(:div, class: 'col-sm-3') do
content_tag(:strong) do
translation_present?("activerecord.attributes.#{label}") ? t("activerecord.attributes.#{label}") : t(label)
end
end
end
private :label_column
def no
content_tag(:i, nil, class: 'glyphicon glyphicon-remove')
end
def progress_bar(value)
content_tag(:div, class: 'progress') do
content_tag(:div, "#{value}%", :'aria-valuemax' => 100, :'aria-valuemin' => 0, :'aria-valuenow' => value, class: 'progress-bar', role: 'progressbar', style: "width: #{value}%;")
end
end
def render_markdown(markdown)
Kramdown::Document.new(markdown).to_html.html_safe
end
def row(options={}, &block)
content_tag(:div, class: 'attribute-row row') do
label_column(options[:label]) + value_column(options[:value], &block)
end
end
def symbol_for(value)
if value.is_a?(FalseClass)
no
elsif value.is_a?(TrueClass)
yes
elsif value.blank?
empty
else
value.to_s
end
end
def translation_present?(key)
I18n.t(key, default: '').present?
end
private :translation_present?
def value_column(value, &block)
content_tag(:div, class: 'col-sm-9') do
block_given? ? yield : symbol_for(value)
end
end
private :value_column
def yes
content_tag(:i, nil, class: 'glyphicon glyphicon-ok')
end
end

View File

@ -0,0 +1,5 @@
module ExerciseHelper
def embedding_parameters(exercise)
"locale=#{I18n.locale}&token=#{exercise.token}"
end
end

0
app/mailers/.keep Normal file
View File

View File

@ -0,0 +1,14 @@
class UserMailer < ActionMailer::Base
def activation_needed_email(user)
@activation_url = activate_internal_user_url(user, token: user.activation_token)
mail(subject: t('mailers.user_mailer.activation_needed.subject'), to: user.email)
end
def activation_success_email(user)
end
def reset_password_email(user)
@reset_password_url = reset_password_internal_user_url(user, token: user.reset_password_token)
mail(subject: t('mailers.user_mailer.reset_password.subject'), to: user.email)
end
end

0
app/models/.keep Normal file
View File

View File

@ -0,0 +1,84 @@
require File.expand_path('../../../uploaders/file_uploader', __FILE__)
module CodeOcean
class File < ActiveRecord::Base
DEFAULT_WEIGHT = 1.0
ROLES = %w[main_file reference_implementation regular_file teacher_defined_test user_defined_file user_defined_test]
TEACHER_DEFINED_ROLES = ROLES - %w[user_defined_file]
after_initialize :set_default_values
before_validation :set_ancestor_values, if: :incomplete_descendent?
before_validation :hash_content, if: :content_present?
belongs_to :context, polymorphic: true
belongs_to :execution_environment
belongs_to :file
alias_method :ancestor, :file
belongs_to :file_type
has_many :files
alias_method :descendants, :files
mount_uploader :native_file, FileUploader
scope :editable, -> { where(read_only: false) }
scope :visible, -> { where(hidden: false) }
validates :feedback_message, if: :teacher_defined_test?, presence: true
validates :feedback_message, absence: true, unless: :teacher_defined_test?
validates :file_type_id, presence: true
validates :hashed_content, if: :content_present?, presence: true
validates :hidden, inclusion: {in: [true, false]}
validates :name, presence: true
validates :read_only, inclusion: {in: [true, false]}
validates :role, inclusion: {in: ROLES}
validates :weight, if: :teacher_defined_test?, numericality: true, presence: true
validates :weight, absence: true, unless: :teacher_defined_test?
ROLES.each do |role|
define_method("#{role}?") { self.role == role }
end
def ancestor_id
file_id || id
end
def content_present?
content? || native_file?
end
private :content_present?
def hash_content
self.hashed_content = Digest::MD5.new.hexdigest(file_type.binary? ? ::File.new(native_file.file.path, 'r').read : content)
end
private :hash_content
def incomplete_descendent?
file_id.present? && file_type_id.blank?
end
private :incomplete_descendent?
def name_with_extension
name + (file_type.file_extension || '')
end
def set_ancestor_values
[:feedback_message, :file_type_id, :hidden, :name, :path, :read_only, :role, :weight].each do |attribute|
send(:"#{attribute}=", ancestor.send(attribute))
end
end
private :set_ancestor_values
def set_default_values
self.content ||= ''
self.hidden ||= false
self.read_only ||= false
self.weight ||= DEFAULT_WEIGHT if teacher_defined_test?
end
private :set_default_values
def visible
!hidden
end
end
end

View File

View File

@ -0,0 +1,20 @@
module Context
extend ActiveSupport::Concern
included do
has_many :files, as: :context, class: CodeOcean::File
accepts_nested_attributes_for :files
end
def add_file(file_attributes)
file = files.create(file_attributes)
save
file
end
def add_file!(file_attributes)
file = files.create!(file_attributes)
save!
file
end
end

View File

@ -0,0 +1,12 @@
module Creation
extend ActiveSupport::Concern
included do
belongs_to :user, polymorphic: true
alias_method :author, :user
alias_method :creator, :user
validates :user_id, presence: true
validates :user_type, presence: true
end
end

View File

@ -0,0 +1,30 @@
module User
extend ActiveSupport::Concern
ROLES = %w[admin teacher]
included do
belongs_to :consumer
has_many :exercises, as: :user
has_many :file_types, as: :user
has_many :submissions, as: :user
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
end
ROLES.each do |role|
define_method("#{role}?") { self.try(:role) == role }
end
def external?
is_a?(ExternalUser)
end
def internal?
is_a?(InternalUser)
end
def to_s
name
end
end

13
app/models/consumer.rb Normal file
View File

@ -0,0 +1,13 @@
class Consumer < ActiveRecord::Base
has_many :users
scope :with_users, -> { where('id IN (SELECT consumer_id FROM internal_users)') }
validates :name, presence: true
validates :oauth_key, presence: true, uniqueness: true
validates :oauth_secret, presence: true
def to_s
name
end
end

14
app/models/error.rb Normal file
View File

@ -0,0 +1,14 @@
class Error < ActiveRecord::Base
belongs_to :execution_environment
scope :for_execution_environment, ->(execution_environment) do
Error.find_by_sql("SELECT MAX(created_at) AS created_at, MAX(id) AS id, message, COUNT(*) AS count FROM errors WHERE #{sanitize_sql_hash_for_conditions(execution_environment_id: execution_environment.id)} GROUP BY message ORDER BY count DESC")
end
validates :execution_environment_id, presence: true
validates :message, presence: true
def self.nested_resource?
true
end
end

View File

@ -0,0 +1,42 @@
class ExecutionEnvironment < ActiveRecord::Base
include Creation
VALIDATION_COMMAND = 'whoami'
has_many :exercises
has_many :hints
scope :with_exercises, -> { where('id IN (SELECT execution_environment_id FROM exercises)') }
validate :valid_test_setup?
validate :working_docker_image?, if: :validate_docker_image?
validates :docker_image, presence: true
validates :name, presence: true
validates :permitted_execution_time, numericality: {only_integer: true}, presence: true
validates :run_command, presence: true
def to_s
name
end
def valid_test_setup?
if test_command? ^ testing_framework?
errors.add(:test_command, I18n.t('activerecord.errors.messages.together', attribute: I18n.t('activerecord.attributes.execution_environment.testing_framework')))
end
end
private :valid_test_setup?
def validate_docker_image?
docker_image.present? && Rails.env != 'test'
end
private :validate_docker_image?
def working_docker_image?
DockerClient.pull(docker_image) unless DockerClient.image_tags.include?(docker_image)
output = DockerClient.new(execution_environment: self).execute_command(VALIDATION_COMMAND)
errors.add(:docker_image, "error: #{output[:stderr]}") if output[:stderr].present?
rescue DockerClient::Error => error
errors.add(:docker_image, "error: #{error}")
end
private :working_docker_image?
end

60
app/models/exercise.rb Normal file
View File

@ -0,0 +1,60 @@
class Exercise < ActiveRecord::Base
include Context
include Creation
after_initialize :generate_token
after_initialize :set_default_values
belongs_to :execution_environment
has_many :submissions
has_many :users, source_type: ExternalUser, through: :submissions
scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
validate :valid_main_file?
validates :description, presence: true
validates :execution_environment_id, presence: true
validates :public, inclusion: {in: [true, false]}
validates :title, presence: true
validates :token, presence: true, uniqueness: true
def average_percentage
(average_score / maximum_score * 100).round if average_score
end
def average_score
ActiveRecord::Base.connection.execute("SELECT AVG(score) AS average_score FROM (SELECT MAX(score) AS score FROM submissions WHERE cause = 'submit' AND exercise_id = '#{id}' GROUP BY user_id) AS maximum_scores")[0]['average_score'].to_f.round(2)
end
def duplicate(attributes)
exercise = dup
exercise.attributes = attributes
files.each { |file| exercise.files << file.dup }
exercise
end
def generate_token
self.token ||= SecureRandom.hex(4)
end
private :generate_token
def maximum_score
files.where(role: 'teacher_defined_test').sum(:weight)
end
def set_default_values
self.public ||= false
end
private :set_default_values
def to_s
title
end
def valid_main_file?
if files.where(role: 'main_file').count > 1
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file'))
end
end
private :valid_main_file?
end

View File

@ -0,0 +1,6 @@
class ExternalUser < ActiveRecord::Base
include User
validates :consumer_id, presence: true
validates :external_id, presence: true
end

35
app/models/file_type.rb Normal file
View File

@ -0,0 +1,35 @@
class FileType < ActiveRecord::Base
include Creation
AUDIO_FILE_EXTENSIONS = %w[.aac .flac .m4a .mp3 .ogg .wav .wma]
IMAGE_FILE_EXTENSIONS = %w[.bmp .gif .jpeg .jpg .png]
VIDEO_FILE_EXTENSIONS = %w[.avi .flv .mkv .mp4 .m4v .ogv .webm]
after_initialize :set_default_values
has_many :files
validates :binary, inclusion: {in: [true, false]}
validates :editor_mode, presence: true, unless: :binary?
validates :executable, inclusion: {in: [true, false]}
validates :indent_size, presence: true, unless: :binary?
validates :name, presence: true
validates :renderable, inclusion: {in: [true, false]}
[:audio, :image, :video].each do |type|
define_method("#{type}?") do
self.class.const_get("#{type.upcase}_FILE_EXTENSIONS").include?(file_extension)
end
end
def set_default_values
self.binary ||= false
self.executable ||= false
self.renderable ||= false
end
private :set_default_values
def to_s
name
end
end

17
app/models/hint.rb Normal file
View File

@ -0,0 +1,17 @@
class Hint < ActiveRecord::Base
belongs_to :execution_environment
validates :execution_environment_id, presence: true
validates :locale, presence: true
validates :message, presence: true
validates :name, presence: true
validates :regular_expression, presence: true
def self.nested_resource?
true
end
def to_s
name
end
end

View File

@ -0,0 +1,13 @@
class InternalUser < ActiveRecord::Base
include User
authenticates_with_sorcery!
validates :email, presence: true, uniqueness: true
validates :password, confirmation: true, on: :update, presence: true, unless: :activated?
validates :role, inclusion: {in: ROLES}
def activated?
activation_state == 'active'
end
end

58
app/models/submission.rb Normal file
View File

@ -0,0 +1,58 @@
class Submission < ActiveRecord::Base
include Context
include Creation
CAUSES = %w[assess download file render run save submit test]
FILENAME_URL_PLACEHOLDER = '{filename}'
belongs_to :exercise
scope :final, -> { where(cause: 'submit') }
scope :intermediate, -> { where.not(cause: 'submit') }
validates :cause, inclusion: {in: CAUSES}
validates :exercise_id, presence: true
def collect_files
ancestors = exercise.files.map(&:id).zip(exercise.files)
descendants = files.map(&:file_id).zip(files)
(ancestors + descendants).to_h.values
end
def execution_environment
exercise.execution_environment
end
[:download, :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 main_file
collect_files.detect(&:main_file?)
end
def normalized_score
score / exercise.maximum_score
end
def percentage
(normalized_score * 100).round
end
[:score, :stop].each do |action|
define_method("#{action}_url") do
Rails.application.routes.url_helpers.send(:"#{action}_submission_path", self)
end
end
def siblings
Submission.where(exercise_id: exercise_id, user_id: user_id, user_type: user_type)
end
def to_s
Submission.model_name.human
end
end

View File

@ -0,0 +1,5 @@
class AdminOnlyPolicy < ApplicationPolicy
[:create?, :destroy?, :edit?, :index?, :new?, :show?, :update?].each do |action|
define_method(action) { admin? }
end
end

View File

@ -0,0 +1,9 @@
class AdminOrAuthorPolicy < ApplicationPolicy
[:create?, :index?, :new?].each do |action|
define_method(action) { @user.internal? }
end
[:destroy?, :edit?, :show?, :update?].each do |action|
define_method(action) { admin? || author? }
end
end

View File

@ -0,0 +1,40 @@
class ApplicationPolicy
def admin?
@user.admin?
end
private :admin?
def everyone
true
end
private :everyone
def initialize(user, record)
@user = user
@record = record
require_user!
end
def no_one
false
end
private :no_one
def require_user!
raise Pundit::NotAuthorizedError unless @user
end
private :require_user!
class Scope
def initialize(user, scope)
@user = user
@scope = scope
require_user!
end
def require_user!
raise Pundit::NotAuthorizedError unless @user
end
private :require_user!
end
end

View File

@ -0,0 +1,23 @@
module CodeOcean
class FilePolicy < AdminOrAuthorPolicy
def author?
@user == @record.context.author
end
def create?
if @record.context.is_a?(Exercise)
admin? || author?
else
author?
end
end
def destroy?
if @record.context.is_a?(Exercise)
admin? || author?
else
no_one
end
end
end
end

View File

@ -0,0 +1,5 @@
class ConsumerPolicy < AdminOnlyPolicy
def show?
super || @user.consumer == @record
end
end

View File

@ -0,0 +1,5 @@
class ErrorPolicy < AdminOrAuthorPolicy
def author?
@user == @record.execution_environment.author
end
end

View File

@ -0,0 +1,10 @@
class ExecutionEnvironmentPolicy < AdminOrAuthorPolicy
def author?
@user == @record.author
end
private :author?
[:execute_command?, :shell?].each do |action|
define_method(action) { admin? || author? }
end
end

View File

@ -0,0 +1,24 @@
class ExercisePolicy < AdminOrAuthorPolicy
def author?
@user == @record.author
end
private :author?
[:clone?, :statistics?].each do |action|
define_method(action) { admin? || author? }
end
[:implement?, :submit?].each do |action|
define_method(action) { everyone }
end
class Scope < Scope
def resolve
if @user.admin?
@scope.all
else
@scope.where("user_id = #{@user.id} OR public = TRUE")
end
end
end
end

View File

@ -0,0 +1,2 @@
class ExternalUserPolicy < AdminOnlyPolicy
end

View File

@ -0,0 +1,6 @@
class FileTypePolicy < AdminOrAuthorPolicy
def author?
@user == @record.author
end
private :author?
end

View File

@ -0,0 +1,5 @@
class HintPolicy < AdminOrAuthorPolicy
def author?
@user == @record.execution_environment.author
end
end

View File

@ -0,0 +1,9 @@
class InternalUserPolicy < AdminOnlyPolicy
def destroy?
super && !@record.admin?
end
def show?
super || @record == @user
end
end

View File

@ -0,0 +1,18 @@
class SubmissionPolicy < ApplicationPolicy
def author?
@user == @record.author
end
private :author?
def create?
everyone
end
[:download_file?, :render_file?, :run?, :score?, :show?, :statistics?, :stop?, :test?].each do |action|
define_method(action) { admin? || author? }
end
def index?
admin?
end
end

View File

@ -0,0 +1,7 @@
class FileUploader < CarrierWave::Uploader::Base
storage :file
def store_dir
"uploads/files/#{model.id}"
end
end

View File

@ -0,0 +1,19 @@
- if current_user.try(:internal?)
ul.breadcrumb
- if model = Kernel.const_get(controller_name.classify) rescue nil
- object = model.find_by(id: params[:id])
- if model.try(:nested_resource?)
li = model.model_name.human(count: 2)
- if object
li = object
- else
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
- if object
li = link_to(object, send(:"#{model.model_name.singular}_path", object))
li.active
- if translation_present?("shared.#{params[:action]}")
= t("shared.#{params[:action]}")
- else
= t("#{controller_name}.index.#{params[:action]}")
- else
li.active = t("breadcrumbs.#{params[:action]}")

View File

@ -0,0 +1,3 @@
#flash data-message-failure=t('shared.message_failure')
- %w[danger info notice success warning].each do |severity|
p.alert.flash class="alert-#{severity == 'notice' ? 'success' : severity}" id="flash-#{severity}" = flash[severity]

View File

@ -0,0 +1,7 @@
li.dropdown
a.dropdown-toggle data-toggle='dropdown' href='#'
= t("locales.#{I18n.locale}")
span.caret
ul.dropdown-menu role='menu'
- I18n.available_locales.sort_by { |locale| t("locales.#{locale}") }.each do |locale|
li = link_to(t("locales.#{locale}"), url_for(params.merge(locale: locale)))

View File

@ -0,0 +1,11 @@
- if current_user.try(:internal?)
ul.nav.navbar-nav
li.dropdown
a.dropdown-toggle data-toggle='dropdown' href='#'
= t('shared.administration')
span.caret
ul.dropdown-menu role='menu'
- models = [ExecutionEnvironment, Exercise, Consumer, ExternalUser, FileType, InternalUser, Submission].sort_by { |model| model.model_name.human(count: 2) }
- models.each do |model|
- if policy(model).index?
li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))

View File

@ -0,0 +1,20 @@
- if current_user
- if current_user.internal?
li.dropdown
a.dropdown-toggle data-toggle='dropdown' href='#'
i.glyphicon.glyphicon-user
= current_user
span.caret
ul.dropdown-menu role='menu'
li = link_to(t('consumers.show.link'), current_user.consumer) if current_user.consumer
li = link_to(t('internal_users.show.link'), current_user)
li = link_to(t('sessions.destroy.link'), sign_out_path, method: :delete)
- else
li
p.navbar-text
i.glyphicon.glyphicon-user
= current_user
- else
li = link_to(sign_in_path) do
i.fa.fa-sign-in
= t('sessions.new.link')

View File

@ -0,0 +1,9 @@
- unless local_assigns[:modal]
h1 = t('shared.help.headline')
h2 = t('shared.help.general_help')
== Forgery(:lorem_ipsum).paragraphs(10, html: true)
- if local_assigns.has_key?(:execution_environment)
h2 = t('shared.help.execution_environment_specific_help', execution_environment: execution_environment)
= render_markdown(execution_environment.help)

View File

@ -0,0 +1,3 @@
h1 = t('breadcrumbs.welcome')
p = Forgery(:lorem_ipsum).words(100)

View File

@ -0,0 +1,9 @@
= form_for(CodeOcean::File.new) do |f|
.form-group
= f.label(:name, t('activerecord.attributes.file.name'))
= f.text_field(:name, class: 'form-control', required: true)
.form-group
= f.label(:file_type_id, t('activerecord.attributes.file.file_type_id'))
= f.collection_select(:file_type_id, FileType.where(binary: false), :id, :name, {}, class: 'form-control')
= f.hidden_field(:context_id)
.actions = render('shared/submit_button', f: f, object: CodeOcean::File.new)

View File

@ -0,0 +1 @@
json.extract! @file, :id, :name_with_extension

View File

@ -0,0 +1,12 @@
= form_for(@consumer, multipart: true) do |f|
= render('shared/form_errors', object: @consumer)
.form-group
= f.label(:name)
= f.text_field(:name, class: 'form-control', required: true)
.form-group
= f.label(:oauth_key)
= f.text_field(:oauth_key, class: 'form-control', required: true)
.form-group
= f.label(:oauth_secret)
= f.text_field(:oauth_secret, class: 'form-control', required: true)
.actions = render('shared/submit_button', f: f, object: @consumer)

View File

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

View File

@ -0,0 +1,17 @@
h1 = Consumer.model_name.human(count: 2)
.table-responsive
table.table
thead
tr
th = t('activerecord.attributes.consumer.name')
th colspan=3 = t('shared.actions')
tbody
- @consumers.each do |consumer|
tr
td = consumer.name
td = link_to(t('shared.show'), consumer)
td = link_to(t('shared.edit'), edit_consumer_path(consumer))
td = link_to(t('shared.destroy'), consumer, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
p = render('shared/new_button', model: Consumer)

View File

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

View File

@ -0,0 +1,8 @@
h1
= @consumer
= render('shared/edit_button', object: @consumer) if policy(@consumer).edit?
= row(label: 'consumer.name', value: @consumer.name)
- %w[oauth_key oauth_secret].each do |attribute|
= row(label: "consumer.#{attribute}") do
= content_tag(:input, nil, class: 'form-control', readonly: true, value: @consumer.send(attribute))

View File

@ -0,0 +1,17 @@
h1 = ::Error.model_name.human(count: 2)
.table-responsive
table.table
thead
tr
th = t('.count')
th = t('activerecord.attributes.error.message')
th = t('shared.created_at')
th = t('shared.actions')
tbody
- @errors.each do |error|
tr
td = error.count
td = error.message
td = l(error.created_at, format: :short)
td = link_to(t('shared.show'), execution_environment_error_path(params[:execution_environment_id], error))

View File

@ -0,0 +1,4 @@
h1 = ::Error.model_name.human
= row(label: 'error.message', value: @error.message)
= row(label: 'shared.created_at', value: l(@error.created_at, format: :short))

View File

@ -0,0 +1,35 @@
= form_for(@execution_environment) do |f|
= render('shared/form_errors', object: @execution_environment)
.form-group
= f.label(:name)
= f.text_field(:name, class: 'form-control', required: true)
.form-group
= f.label(:docker_image)
| &nbsp;
a.toggle-input href='#' = t('shared.new')
= f.select(:docker_image, @docker_images, {}, class: 'form-control')
= f.text_field(:docker_image, class: 'alternative-input form-control', disabled: true)
.help-block == t('.hints.docker_image')
.form-group
= f.label(:exposed_ports)
= f.text_field(:exposed_ports, class: 'form-control', placeholder: '3000, 4000')
.help-block == t('.hints.exposed_ports')
.form-group
= f.label(:permitted_execution_time)
= f.number_field(:permitted_execution_time, class: 'form-control', min: 1)
.form-group
= f.label(:run_command)
= f.text_field(:run_command, class: 'form-control', placeholder: 'command %{filename}', required: true)
.help-block == t('.hints.command')
.form-group
= f.label(:test_command)
= f.text_field(:test_command, class: 'form-control', placeholder: 'command %{filename}')
.help-block == t('.hints.command')
.form-group
= f.label(:testing_framework)
= f.select(:testing_framework, @testing_framework_adapters, {include_blank: true}, class: 'form-control')
.form-group
= f.label(:help)
= f.hidden_field(:help)
.form-control.markdown
.actions = render('shared/submit_button', f: f, object: @execution_environment)

View File

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

View File

@ -0,0 +1,23 @@
h1 = ExecutionEnvironment.model_name.human(count: 2)
.table-responsive
table.table
thead
tr
th = t('activerecord.attributes.execution_environment.name')
th = t('activerecord.attributes.execution_environment.user')
th colspan=4 = t('shared.actions')
th colspan=2 = t('shared.resources')
tbody
- @execution_environments.each do |execution_environment|
tr
td = execution_environment.name
td = link_to(execution_environment.author, execution_environment.author)
td = link_to(t('shared.show'), execution_environment)
td = link_to(t('shared.edit'), edit_execution_environment_path(execution_environment))
td = link_to(t('shared.destroy'), execution_environment, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
td = link_to(t('.shell'), shell_execution_environment_path(execution_environment))
td = link_to(t('activerecord.models.error.other'), execution_environment_errors_path(execution_environment.id))
td = link_to(t('activerecord.models.hint.other'), execution_environment_hints_path(execution_environment.id))
p = render('shared/new_button', model: ExecutionEnvironment)

View File

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

View File

@ -0,0 +1,8 @@
h1 = @execution_environment
#shell data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @execution_environment.permitted_execution_time) data-url=execute_command_execution_environment_path(@execution_environment)
.form-group
label for='command' = t('.command')
input#command.form-control type='text'
pre#output data-message-no-output=t('exercises.implement.no_output')
p = t('exercises.implement.no_output_yet')

View File

@ -0,0 +1,10 @@
h1
= @execution_environment
= render('shared/edit_button', object: @execution_environment)
= row(label: 'execution_environment.name', value: @execution_environment.name)
= row(label: 'execution_environment.user', value: link_to(@execution_environment.author, @execution_environment.author))
- [:docker_image, :exposed_ports, :permitted_execution_time, :run_command, :test_command].each do |attribute|
= row(label: "execution_environment.#{attribute}", value: @execution_environment.send(attribute))
= row(label: 'execution_environment.testing_framework', value: @testing_framework_adapter.try(:framework_name))
= row(label: 'execution_environment.help', value: render_markdown(@execution_environment.help))

View File

@ -0,0 +1,6 @@
.form-group class="form-group-#{attribute.to_s.gsub('_', '-')}"
= form.label(attribute, label)
| &nbsp;
a.toggle-input href='#' = t('shared.upload_file')
= form.text_area(attribute, class: 'code-field form-control', rows: 5)
= form.file_field(attribute, class: 'alternative-input form-control', disabled: true)

View File

@ -0,0 +1,34 @@
#editor.row data-exercise-id=exercise.id data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path
.col-sm-3 = render('editor_file_tree', files: @files)
#frames.col-sm-9
- @files.each do |file|
= render('editor_frame', exercise: exercise, file: file)
#editor-buttons.btn-group
= render('editor_button', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => exercise_path(exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over'))
= render('editor_button', data: {:'data-message-success' => t('submissions.create.success'), :'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-save', id: 'save', label: t('exercises.editor.save'), title: t('.tooltips.save'))
.btn-group
= render('editor_button', icon: 'fa fa-desktop', id: 'render', label: t('exercises.editor.render'))
= render('editor_button', data: {:'data-message-failure' => t('exercises.editor.run_failure'), :'data-message-success' => t('exercises.editor.run_success'), :'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-play', id: 'run', label: t('exercises.editor.run'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r'))
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-stop', id: 'stop', label: t('exercises.editor.stop'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + r'))
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-rocket', id: 'test', label: t('exercises.editor.test'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + t'))
button.btn.btn-primary.dropdown-toggle data-toggle='dropdown' type='button'
span.caret
span.sr-only Toggle Dropdown
ul.dropdown-menu role='menu'
li
a#dropdown-render data-cause='render' href='#'
i.fa.fa-desktop
= t('exercises.editor.render')
li
a#dropdown-run data-cause='run' href='#'
i.fa.fa-play
= t('exercises.editor.run')
li
a#dropdown-stop href='#'
i.fa.fa-stop
= t('exercises.editor.stop')
li
a#dropdown-test data-cause='test' href='#'
i.fa.fa-rocket
= t('exercises.editor.test')
= render('editor_button', data: {:'data-placement' => 'top', :'data-tooltip' => true}, icon: 'fa fa-trophy', id: 'assess', label: t('exercises.editor.score'), title: t('shared.tooltips.shortcut', shortcut: 'ALT + s'))

View File

@ -0,0 +1,4 @@
button.btn class=(local_assigns[:classes] || 'btn-primary') *(local_assigns[:data] || {}) id=id title=local_assigns[:title] type='button'
i.fa.fa-circle-o-notch.fa-spin
i class=icon
= label

View File

@ -0,0 +1,9 @@
#files data-entries=FileTree.new(files).to_js_tree
hr
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-cause' => 'file'}, icon: 'glyphicon glyphicon-plus', id: 'create-file', label: t('exercises.editor.create_file'))
= render('editor_button', classes: 'btn-block btn-warning btn-sm', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, icon: 'glyphicon glyphicon-trash', id: 'destroy-file', label: t('exercises.editor.destroy_file'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download'))
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))

View File

@ -0,0 +1,13 @@
.frame data-executable=file.file_type.executable? data-filename=file.name_with_extension data-renderable=file.file_type.renderable? data-role=file.role
- if file.file_type.binary?
- if file.file_type.renderable?
- if file.file_type.audio?
= audio_tag(file.native_file.url, controls: true)
- elsif file.file_type.image?
= image_tag(file.native_file.url)
- elsif file.file_type.video?
= video_tag(file.native_file.url, controls: true)
- else
= link_to(file.native_file.file.name_with_extension, file.native_file.url)
- else
.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 = file.content

View File

@ -0,0 +1,31 @@
li.panel.panel-default
.panel-body
.form-group
= f.label(:name, t('activerecord.attributes.file.name'))
= f.text_field(:name, class: 'form-control')
.form-group
= f.label(:path, t('activerecord.attributes.file.path'))
= f.text_field(:path, class: 'form-control')
.help-block = t('.hints.path')
.form-group
= f.label(:file_type_id, t('activerecord.attributes.file.file_type_id'))
= f.collection_select(:file_type_id, @file_types, :id, :name, {}, class: 'form-control')
.form-group
= f.label(:role, t('activerecord.attributes.file.role'))
= f.select(:role, CodeOcean::File::TEACHER_DEFINED_ROLES.map { |role| [t("files.roles.#{role}"), role] }, {include_blank: true}, class: 'form-control')
.checkbox
label
= f.check_box(:hidden)
= t('activerecord.attributes.file.hidden')
.checkbox
label
= f.check_box(:read_only)
= t('activerecord.attributes.file.read_only')
.form-group
= f.label(:name, t('activerecord.attributes.file.feedback_message'))
= f.text_area(:feedback_message, class: 'form-control', disabled: !f.object.teacher_defined_test?, maxlength: 255)
.help-block = t('.hints.feedback_message')
.form-group
= f.label(:role, t('activerecord.attributes.file.weight'))
= f.number_field(:weight, class: 'form-control', disabled: !f.object.teacher_defined_test?, min: 1, step: 'any')
= render('code_field', attribute: :content, form: f, label: t('activerecord.attributes.file.content'))

Some files were not shown because too many files have changed in this diff Show More