transferred Code Ocean from original repository to GitHub
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/config/*.production.yml
|
||||
/coverage
|
||||
/log
|
||||
/public/assets
|
||||
/public/uploads
|
||||
/tmp
|
||||
/vagrant/
|
||||
*.sublime-*
|
7
Capfile
Normal file
7
Capfile
Normal 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
53
Gemfile
Normal 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
341
Gemfile.lock
Normal 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
22
README.md
Normal 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
6
Rakefile
Normal 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
0
app/assets/images/.keep
Normal file
BIN
app/assets/images/favicon.png
Normal file
BIN
app/assets/images/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
22
app/assets/javascripts/application.js
Normal file
22
app/assets/javascripts/application.js
Normal 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 .
|
17
app/assets/javascripts/base.js
Normal file
17
app/assets/javascripts/base.js
Normal 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);
|
||||
};
|
||||
});
|
604
app/assets/javascripts/editor.js
Normal file
604
app/assets/javascripts/editor.js
Normal 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 = ''
|
||||
}
|
||||
};
|
||||
});
|
7
app/assets/javascripts/execution_environments.js
Normal file
7
app/assets/javascripts/execution_environments.js
Normal file
@ -0,0 +1,7 @@
|
||||
$(function() {
|
||||
if ($.isController('execution_environments')) {
|
||||
if ($('.edit_execution_environment, .new_execution_environment').isPresent()) {
|
||||
new MarkdownEditor('#execution_environment_help');
|
||||
}
|
||||
}
|
||||
});
|
85
app/assets/javascripts/exercises.js
Normal file
85
app/assets/javascripts/exercises.js
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
21
app/assets/javascripts/forms.js
Normal file
21
app/assets/javascripts/forms.js
Normal 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
|
||||
});
|
||||
});
|
16
app/assets/javascripts/markdown_editor.js
Normal file
16
app/assets/javascripts/markdown_editor.js
Normal 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());
|
||||
};
|
||||
})();
|
69
app/assets/javascripts/shell.js
Normal file
69
app/assets/javascripts/shell.js
Normal 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);
|
||||
}
|
||||
});
|
17
app/assets/stylesheets/application.css
Normal file
17
app/assets/stylesheets/application.css
Normal 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
|
||||
*/
|
50
app/assets/stylesheets/base.css.scss
Normal file
50
app/assets/stylesheets/base.css.scss
Normal 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;
|
||||
}
|
69
app/assets/stylesheets/editor.css.scss
Normal file
69
app/assets/stylesheets/editor.css.scss
Normal 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;
|
||||
}
|
13
app/assets/stylesheets/exercises.css.scss
Normal file
13
app/assets/stylesheets/exercises.css.scss
Normal 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;
|
||||
}
|
6
app/assets/stylesheets/flowrdata.css.scss
Normal file
6
app/assets/stylesheets/flowrdata.css.scss
Normal file
@ -0,0 +1,6 @@
|
||||
#flowrHint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#flowrOutput {
|
||||
}
|
25
app/assets/stylesheets/forms.css.scss
Normal file
25
app/assets/stylesheets/forms.css.scss
Normal 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%;
|
||||
}
|
33
app/controllers/application_controller.rb
Normal file
33
app/controllers/application_controller.rb
Normal 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
|
39
app/controllers/code_ocean/files_controller.rb
Normal file
39
app/controllers/code_ocean/files_controller.rb
Normal 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
|
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
6
app/controllers/concerns/file_parameters.rb
Normal file
6
app/controllers/concerns/file_parameters.rb
Normal 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
|
131
app/controllers/concerns/lti.rb
Normal file
131
app/controllers/concerns/lti.rb
Normal 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
|
20
app/controllers/concerns/submission_parameters.rb
Normal file
20
app/controllers/concerns/submission_parameters.rb
Normal 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
|
19
app/controllers/concerns/submission_scoring.rb
Normal file
19
app/controllers/concerns/submission_scoring.rb
Normal 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
|
69
app/controllers/consumers_controller.rb
Normal file
69
app/controllers/consumers_controller.rb
Normal 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
|
43
app/controllers/errors_controller.rb
Normal file
43
app/controllers/errors_controller.rb
Normal 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
|
98
app/controllers/execution_environments_controller.rb
Normal file
98
app/controllers/execution_environments_controller.rb
Normal 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
|
152
app/controllers/exercises_controller.rb
Normal file
152
app/controllers/exercises_controller.rb
Normal 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
|
16
app/controllers/external_users_controller.rb
Normal file
16
app/controllers/external_users_controller.rb
Normal 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
|
78
app/controllers/file_types_controller.rb
Normal file
78
app/controllers/file_types_controller.rb
Normal 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
|
75
app/controllers/hints_controller.rb
Normal file
75
app/controllers/hints_controller.rb
Normal 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
|
131
app/controllers/internal_users_controller.rb
Normal file
131
app/controllers/internal_users_controller.rb
Normal 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
|
49
app/controllers/sessions_controller.rb
Normal file
49
app/controllers/sessions_controller.rb
Normal 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
|
145
app/controllers/submissions_controller.rb
Normal file
145
app/controllers/submissions_controller.rb
Normal 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
|
78
app/helpers/application_helper.rb
Normal file
78
app/helpers/application_helper.rb
Normal 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
|
5
app/helpers/exercise_helper.rb
Normal file
5
app/helpers/exercise_helper.rb
Normal 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
0
app/mailers/.keep
Normal file
14
app/mailers/user_mailer.rb
Normal file
14
app/mailers/user_mailer.rb
Normal 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
0
app/models/.keep
Normal file
84
app/models/code_ocean/file.rb
Normal file
84
app/models/code_ocean/file.rb
Normal 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
|
0
app/models/concerns/.keep
Normal file
0
app/models/concerns/.keep
Normal file
20
app/models/concerns/context.rb
Normal file
20
app/models/concerns/context.rb
Normal 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
|
12
app/models/concerns/creation.rb
Normal file
12
app/models/concerns/creation.rb
Normal 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
|
30
app/models/concerns/user.rb
Normal file
30
app/models/concerns/user.rb
Normal 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
13
app/models/consumer.rb
Normal 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
14
app/models/error.rb
Normal 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
|
42
app/models/execution_environment.rb
Normal file
42
app/models/execution_environment.rb
Normal 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
60
app/models/exercise.rb
Normal 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
|
6
app/models/external_user.rb
Normal file
6
app/models/external_user.rb
Normal 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
35
app/models/file_type.rb
Normal 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
17
app/models/hint.rb
Normal 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
|
13
app/models/internal_user.rb
Normal file
13
app/models/internal_user.rb
Normal 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
58
app/models/submission.rb
Normal 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
|
5
app/policies/admin_only_policy.rb
Normal file
5
app/policies/admin_only_policy.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class AdminOnlyPolicy < ApplicationPolicy
|
||||
[:create?, :destroy?, :edit?, :index?, :new?, :show?, :update?].each do |action|
|
||||
define_method(action) { admin? }
|
||||
end
|
||||
end
|
9
app/policies/admin_or_author_policy.rb
Normal file
9
app/policies/admin_or_author_policy.rb
Normal 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
|
40
app/policies/application_policy.rb
Normal file
40
app/policies/application_policy.rb
Normal 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
|
23
app/policies/code_ocean/file_policy.rb
Normal file
23
app/policies/code_ocean/file_policy.rb
Normal 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
|
5
app/policies/consumer_policy.rb
Normal file
5
app/policies/consumer_policy.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class ConsumerPolicy < AdminOnlyPolicy
|
||||
def show?
|
||||
super || @user.consumer == @record
|
||||
end
|
||||
end
|
5
app/policies/error_policy.rb
Normal file
5
app/policies/error_policy.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class ErrorPolicy < AdminOrAuthorPolicy
|
||||
def author?
|
||||
@user == @record.execution_environment.author
|
||||
end
|
||||
end
|
10
app/policies/execution_environment_policy.rb
Normal file
10
app/policies/execution_environment_policy.rb
Normal 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
|
24
app/policies/exercise_policy.rb
Normal file
24
app/policies/exercise_policy.rb
Normal 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
|
2
app/policies/external_user_policy.rb
Normal file
2
app/policies/external_user_policy.rb
Normal file
@ -0,0 +1,2 @@
|
||||
class ExternalUserPolicy < AdminOnlyPolicy
|
||||
end
|
6
app/policies/file_type_policy.rb
Normal file
6
app/policies/file_type_policy.rb
Normal file
@ -0,0 +1,6 @@
|
||||
class FileTypePolicy < AdminOrAuthorPolicy
|
||||
def author?
|
||||
@user == @record.author
|
||||
end
|
||||
private :author?
|
||||
end
|
5
app/policies/hint_policy.rb
Normal file
5
app/policies/hint_policy.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class HintPolicy < AdminOrAuthorPolicy
|
||||
def author?
|
||||
@user == @record.execution_environment.author
|
||||
end
|
||||
end
|
9
app/policies/internal_user_policy.rb
Normal file
9
app/policies/internal_user_policy.rb
Normal file
@ -0,0 +1,9 @@
|
||||
class InternalUserPolicy < AdminOnlyPolicy
|
||||
def destroy?
|
||||
super && !@record.admin?
|
||||
end
|
||||
|
||||
def show?
|
||||
super || @record == @user
|
||||
end
|
||||
end
|
18
app/policies/submission_policy.rb
Normal file
18
app/policies/submission_policy.rb
Normal 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
|
7
app/uploaders/file_uploader.rb
Normal file
7
app/uploaders/file_uploader.rb
Normal file
@ -0,0 +1,7 @@
|
||||
class FileUploader < CarrierWave::Uploader::Base
|
||||
storage :file
|
||||
|
||||
def store_dir
|
||||
"uploads/files/#{model.id}"
|
||||
end
|
||||
end
|
19
app/views/application/_breadcrumbs.html.slim
Normal file
19
app/views/application/_breadcrumbs.html.slim
Normal 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]}")
|
3
app/views/application/_flash.html.slim
Normal file
3
app/views/application/_flash.html.slim
Normal 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]
|
7
app/views/application/_locale_selector.html.slim
Normal file
7
app/views/application/_locale_selector.html.slim
Normal 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)))
|
11
app/views/application/_navigation.html.slim
Normal file
11
app/views/application/_navigation.html.slim
Normal 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"))
|
20
app/views/application/_session.html.slim
Normal file
20
app/views/application/_session.html.slim
Normal 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')
|
9
app/views/application/help.html.slim
Normal file
9
app/views/application/help.html.slim
Normal 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)
|
3
app/views/application/welcome.html.slim
Normal file
3
app/views/application/welcome.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = t('breadcrumbs.welcome')
|
||||
|
||||
p = Forgery(:lorem_ipsum).words(100)
|
9
app/views/code_ocean/files/_form.html.slim
Normal file
9
app/views/code_ocean/files/_form.html.slim
Normal 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)
|
1
app/views/code_ocean/files/show.json.jbuilder
Normal file
1
app/views/code_ocean/files/show.json.jbuilder
Normal file
@ -0,0 +1 @@
|
||||
json.extract! @file, :id, :name_with_extension
|
12
app/views/consumers/_form.html.slim
Normal file
12
app/views/consumers/_form.html.slim
Normal 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)
|
3
app/views/consumers/edit.html.slim
Normal file
3
app/views/consumers/edit.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = @consumer
|
||||
|
||||
= render('form')
|
17
app/views/consumers/index.html.slim
Normal file
17
app/views/consumers/index.html.slim
Normal 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)
|
3
app/views/consumers/new.html.slim
Normal file
3
app/views/consumers/new.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = t('shared.new_model', model: Consumer.model_name.human)
|
||||
|
||||
= render('form')
|
8
app/views/consumers/show.html.slim
Normal file
8
app/views/consumers/show.html.slim
Normal 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))
|
17
app/views/errors/index.html.slim
Normal file
17
app/views/errors/index.html.slim
Normal 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))
|
4
app/views/errors/show.html.slim
Normal file
4
app/views/errors/show.html.slim
Normal 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))
|
35
app/views/execution_environments/_form.html.slim
Normal file
35
app/views/execution_environments/_form.html.slim
Normal 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)
|
||||
|
|
||||
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)
|
3
app/views/execution_environments/edit.html.slim
Normal file
3
app/views/execution_environments/edit.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = @execution_environment
|
||||
|
||||
= render('form')
|
23
app/views/execution_environments/index.html.slim
Normal file
23
app/views/execution_environments/index.html.slim
Normal 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)
|
3
app/views/execution_environments/new.html.slim
Normal file
3
app/views/execution_environments/new.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = t('shared.new_model', model: ExecutionEnvironment.model_name.human)
|
||||
|
||||
= render('form')
|
8
app/views/execution_environments/shell.html.slim
Normal file
8
app/views/execution_environments/shell.html.slim
Normal 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')
|
10
app/views/execution_environments/show.html.slim
Normal file
10
app/views/execution_environments/show.html.slim
Normal 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))
|
6
app/views/exercises/_code_field.html.slim
Normal file
6
app/views/exercises/_code_field.html.slim
Normal file
@ -0,0 +1,6 @@
|
||||
.form-group class="form-group-#{attribute.to_s.gsub('_', '-')}"
|
||||
= form.label(attribute, label)
|
||||
|
|
||||
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)
|
34
app/views/exercises/_editor.html.slim
Normal file
34
app/views/exercises/_editor.html.slim
Normal 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'))
|
4
app/views/exercises/_editor_button.html.slim
Normal file
4
app/views/exercises/_editor_button.html.slim
Normal 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
|
9
app/views/exercises/_editor_file_tree.html.slim
Normal file
9
app/views/exercises/_editor_file_tree.html.slim
Normal 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'))
|
13
app/views/exercises/_editor_frame.html.slim
Normal file
13
app/views/exercises/_editor_frame.html.slim
Normal 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
|
31
app/views/exercises/_file_form.html.slim
Normal file
31
app/views/exercises/_file_form.html.slim
Normal 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
Reference in New Issue
Block a user