diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..ded31c0d --- /dev/null +++ b/.babelrc @@ -0,0 +1,18 @@ +{ + "presets": [ + ["env", { + "modules": false, + "targets": { + "browsers": "> 1%", + "uglify": true + }, + "useBuiltIns": true + }] + ], + + "plugins": [ + "syntax-dynamic-import", + "transform-object-rest-spread", + ["transform-class-properties", { "spec": true }] + ] +} diff --git a/.gitignore b/.gitignore index 2b3bb733..6076d364 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,9 @@ /.vagrant *.iml *.DS_Store +/node_modules +/public/packs +/public/packs-test +/node_modules +yarn-debug.log* +.yarn-integrity diff --git a/.postcssrc.yml b/.postcssrc.yml new file mode 100644 index 00000000..150dac3c --- /dev/null +++ b/.postcssrc.yml @@ -0,0 +1,3 @@ +plugins: + postcss-import: {} + postcss-cssnext: {} diff --git a/.rubocop.yml b/.rubocop.yml index 33d49ca3..3c14b841 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,5 @@ AllCops: + TargetRubyVersion: 2.3 Exclude: - bin/* - config/application.rb @@ -7,11 +8,12 @@ AllCops: - db/schema.rb - public/uploads/**/* - tmp/**/* - RunRailsCops: true +Rails: + Enabled: true Metrics/LineLength: Enabled: false require: rubocop-rspec Style/Documentation: Enabled: false -Style/SpaceInsideHashLiteralBraces: +Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space diff --git a/.travis.yml b/.travis.yml index c17424e5..d83858fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,20 @@ sudo: required services: - docker +language: ruby +rvm: + - 2.5.1 +cache: + bundler: true + yarn: true + +env: + global: + - secure: "DkOGGPCrRgV08KGgav3Bl+keZQqb11TINQRVQS2aeMaYR5GW7Rt9zEcZzhUE0JdKVVOvm4Cclft7BO4OyMd6Cq9XnZkOOHY+Yn8Qv923761SKrRgkGUkO8eeVKMawAA8lS53XGrMZWCP2xaLsLQYq8xzinnE3GqstoZJaHLnqVs=" + addons: - code_climate: - repo_token: - secure: "cZoMNjQKB/D7W4B7JDk9PXooy2WCDypu7R4C/Vi0DziZCU9HRwLbdt9aoH5hgHFa7Fe2rHFgflPAAP7h698ozvP0waFtPqLAj+PbEt27LbBDvW8JcvNkKXA0rj5wyTkzuc/0kD+kPB4oDXMak6gZlB9HCJDsa3kdXScQGTVuPdU=" postgresql: "9.6" - firefox: "latest" + firefox: "62.0.3" before_install: - export DISPLAY=:99.0 @@ -19,11 +27,17 @@ before_install: - docker pull openhpi/co_execenv_python - docker pull openhpi/co_execenv_java - mkdir ~/geckodriver - - wget -O ~/geckodriver/download.tar.gz https://github.com/mozilla/geckodriver/releases/download/v0.18.0/geckodriver-v0.18.0-linux64.tar.gz + - wget -O ~/geckodriver/download.tar.gz https://github.com/mozilla/geckodriver/releases/download/v0.23.0/geckodriver-v0.23.0-linux64.tar.gz - tar -xvzf ~/geckodriver/download.tar.gz -C ~/geckodriver/ - rm ~/geckodriver/download.tar.gz - chmod +x ~/geckodriver/geckodriver - export PATH=~/geckodriver/:$PATH + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter + +install: + - bundle install --jobs=3 --retry=3 --deployment --path=${BUNDLE_PATH:-vendor/bundle} + - yarn install before_script: - cp .rspec.travis .rspec @@ -35,10 +49,9 @@ before_script: - cp config/mnemosyne.yml.travis config/mnemosyne.yml - psql --command='CREATE DATABASE travis_ci_test;' --username=postgres - bundle exec rake db:schema:load RAILS_ENV=test + - ./cc-test-reporter before-build -cache: bundler -language: ruby -rvm: -- 2.3.6 - -script: bundle exec rspec --color --format documentation --require spec_helper --require rails_helper && bundle exec codeclimate-test-reporter +script: bundle exec rspec --color --format documentation --require spec_helper --require rails_helper + +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT diff --git a/Gemfile b/Gemfile index 11c91e55..483445d5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,53 +1,48 @@ source 'https://rubygems.org' -gem 'activerecord-jdbcpostgresql-adapter', platform: :jruby gem 'bcrypt' gem 'bootstrap-will_paginate' gem 'carrierwave' gem 'concurrent-ruby' -gem 'concurrent-ruby-ext', platform: :ruby -gem 'activerecord-deprecated_finders', require: 'active_record/deprecated_finders' gem 'docker-api', require: 'docker' gem 'factory_bot_rails' gem 'forgery' gem 'highline' gem 'jbuilder' gem 'jquery-rails' -gem 'jquery-turbolinks' -gem 'ims-lti', '1.1.10' # version 1.1.13 will crash, because @provider.valid_request?(request) on lti.rb line 89 will return false. +gem 'ims-lti', '< 2.0.0' gem 'kramdown' gem 'newrelic_rpm' -gem 'pg', '< 1.0', platform: :ruby +gem 'pg' gem 'pry-byebug' gem 'puma' gem 'pundit' -gem 'rails', '4.2.10' +gem 'rails', '5.2.1' gem 'rails-i18n' gem 'ransack' gem 'rubytree' -gem 'sass-rails', '>= 5.0.7' -gem 'sdoc', group: :doc +gem 'sass-rails' gem 'slim-rails' -gem 'bootstrap_pagedown', '>= 1.1.0' -gem 'pagedown-rails' +gem 'pagedown-bootstrap-rails' gem 'sorcery' -gem 'thread_safe' -gem 'turbolinks', '< 5.0.0' # newer versions prevent loading ACE if the page containing is not accessed directly / refreshed +gem 'turbolinks' gem 'uglifier' -gem 'will_paginate' -gem 'tubesock' +gem 'tubesock', git: 'https://github.com/gosukiwi/tubesock', branch: 'patch-1' # Switch to a fork which is compatible with Rails 5 gem 'faye-websocket' -gem 'eventmachine', '1.0.9.1' # explicitly added, this is used by faye-websocket, version 1.2.5 still has an error in eventmachine.rb:202: [BUG] Segmentation fault, which is not yet fixed and causes the whole ruby process to crash +gem 'eventmachine', '1.0.9.1' # explicitly added, this is used by faye-websocket, newer versions might crash or gem 'nokogiri' -gem 'd3-rails', '~>4.0' +gem 'd3-rails' +gem 'webpacker' gem 'rest-client' gem 'rubyzip' -gem 'mnemosyne-ruby', '~> 1.0' +gem 'mnemosyne-ruby' gem 'whenever', require: false group :development, :staging do - gem 'better_errors', platform: :ruby - gem 'binding_of_caller', platform: :ruby + gem 'bootsnap', require: false + gem 'listen' + gem 'better_errors' + gem 'binding_of_caller' gem 'capistrano' gem 'capistrano3-puma' gem 'capistrano-rails' @@ -56,23 +51,21 @@ group :development, :staging do gem 'rack-mini-profiler' gem 'rubocop', require: false gem 'rubocop-rspec' - gem 'web-console', platform: :ruby + gem 'web-console' end group :development, :test, :staging do - gem 'byebug', platform: :ruby gem 'spring' end group :test do + gem 'rails-controller-testing' gem 'autotest-rails' gem 'capybara' - gem 'capybara-selenium', '>= 0.0.6' + gem 'selenium-webdriver' gem 'headless' - gem 'codeclimate-test-reporter', require: false gem 'database_cleaner' gem 'nyan-cat-formatter' - gem 'rake' gem 'rspec-autotest' gem 'rspec-rails' gem 'simplecov', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 0c196110..21c8bd36 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,75 +1,91 @@ +GIT + remote: https://github.com/gosukiwi/tubesock + revision: 86a5ca4f7d3c3a7b9a727ad91df3b9b4912eda39 + branch: patch-1 + specs: + tubesock (0.2.7) + rack (>= 1.5.0) + websocket (>= 1.1.0) + GEM remote: https://rubygems.org/ specs: ZenTest (4.11.1) - actionmailer (4.2.10) - actionpack (= 4.2.10) - actionview (= 4.2.10) - activejob (= 4.2.10) + actioncable (5.2.1) + actionpack (= 5.2.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailer (5.2.1) + actionpack (= 5.2.1) + actionview (= 5.2.1) + activejob (= 5.2.1) mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.10) - actionview (= 4.2.10) - activesupport (= 4.2.10) - rack (~> 1.6) - rack-test (~> 0.6.2) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) + actionpack (5.2.1) + actionview (= 5.2.1) + activesupport (= 5.2.1) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.10) - activesupport (= 4.2.10) + actionview (5.2.1) + activesupport (= 5.2.1) builder (~> 3.1) - erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (4.2.10) - activesupport (= 4.2.10) - globalid (>= 0.3.0) - activemodel (4.2.10) - activesupport (= 4.2.10) - builder (~> 3.1) - activerecord (4.2.10) - activemodel (= 4.2.10) - activesupport (= 4.2.10) - arel (~> 6.0) - activerecord-deprecated_finders (1.0.4) - activesupport (4.2.10) - i18n (~> 0.7) + activejob (5.2.1) + activesupport (= 5.2.1) + globalid (>= 0.3.6) + activemodel (5.2.1) + activesupport (= 5.2.1) + activerecord (5.2.1) + activemodel (= 5.2.1) + activesupport (= 5.2.1) + arel (>= 9.0) + activestorage (5.2.1) + actionpack (= 5.2.1) + activerecord (= 5.2.1) + marcel (~> 0.3.1) + activesupport (5.2.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) - airbrussh (1.3.0) + airbrussh (1.3.1) sshkit (>= 1.6.1, != 1.7.0) amq-protocol (2.3.0) - arel (6.0.4) + arel (9.0.0) ast (2.4.0) autotest-rails (4.2.1) ZenTest (~> 4.5) - bcrypt (3.1.11) - better_errors (2.4.0) + bcrypt (3.1.12) + better_errors (2.5.0) coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) + bindex (0.5.0) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) + bootsnap (1.3.2) + msgpack (~> 1.0) bootstrap-will_paginate (1.0.0) will_paginate - bootstrap_pagedown (1.1.0) - rails (>= 3.2) builder (3.2.3) - bunny (2.11.0) - amq-protocol (~> 2.3.0) - byebug (10.0.0) - capistrano (3.10.1) + bunny (2.12.0) + amq-protocol (~> 2.3, >= 2.3.0) + byebug (10.0.2) + capistrano (3.11.0) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) sshkit (>= 1.9.0) - capistrano-bundler (1.3.0) + capistrano-bundler (1.4.0) capistrano (~> 3.1) sshkit (~> 1.2) - capistrano-rails (1.3.1) + capistrano-rails (1.4.0) capistrano (~> 3.1) capistrano-bundler (~> 1.1) capistrano-rvm (0.1.2) @@ -81,59 +97,45 @@ GEM capistrano (~> 3.7) capistrano-bundler puma (~> 3.4) - capybara (3.3.1) + capybara (3.10.1) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) - xpath (~> 3.1) - capybara-selenium (0.0.6) - capybara - selenium-webdriver - carrierwave (1.2.2) + regexp_parser (~> 1.2) + xpath (~> 3.2) + carrierwave (1.2.3) activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) chronic (0.10.2) - codeclimate-test-reporter (1.0.7) - simplecov coderay (1.1.2) - coffee-rails (4.2.2) - coffee-script (>= 2.2.0) - railties (>= 4.0.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.12.2) - concurrent-ruby (1.0.5) - concurrent-ruby-ext (1.0.5) - concurrent-ruby (= 1.0.5) + concurrent-ruby (1.1.2) crass (1.0.4) - d3-rails (4.13.0) + d3-rails (5.7.0) railties (>= 3.1) - database_cleaner (1.6.2) + database_cleaner (1.7.0) debug_inspector (0.0.3) diff-lcs (1.3) - docile (1.1.5) - docker-api (1.34.1) + docile (1.3.1) + docker-api (1.34.2) excon (>= 0.47.0) multi_json - domain_name (0.5.20170404) + domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) erubi (1.7.1) - erubis (2.7.0) eventmachine (1.0.9.1) - excon (0.60.0) + excon (0.62.0) execjs (2.7.0) - factory_bot (4.8.2) + factory_bot (4.11.1) activesupport (>= 3.0.0) - factory_bot_rails (4.8.2) - factory_bot (~> 4.8.2) + factory_bot_rails (4.11.1) + factory_bot (~> 4.11.1) railties (>= 3.0.0) - faraday (0.12.2) + faraday (0.15.3) multipart-post (>= 1.2, < 3) faye-websocket (0.10.7) eventmachine (>= 0.12.0) @@ -143,166 +145,178 @@ GEM globalid (0.4.1) activesupport (>= 4.2.0) headless (2.3.1) - highline (1.7.10) + highline (2.0.0) http-cookie (1.0.3) domain_name (~> 0.5) - i18n (0.9.5) + i18n (1.1.1) concurrent-ruby (~> 1.0) - ims-lti (1.1.10) + ims-lti (1.2.2) builder - oauth (~> 0.4.5) - jbuilder (2.7.0) + oauth (>= 0.4.5, < 0.6) + jaro_winkler (1.5.1) + jbuilder (2.8.0) activesupport (>= 4.2.0) multi_json (>= 1.2) - jquery-rails (4.3.1) + jquery-rails (4.3.3) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - jquery-turbolinks (2.1.0) - railties (>= 3.1.0) - turbolinks json (2.1.0) - jwt (1.5.6) - kramdown (1.16.2) - loofah (2.2.2) + jwt (2.1.0) + kramdown (1.17.0) + listen (3.1.5) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + ruby_dep (~> 1.2) + loofah (2.2.3) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.0) + mail (2.7.1) mini_mime (>= 0.1.1) - method_source (0.9.0) - mime-types (3.1) + marcel (0.3.3) + mimemagic (~> 0.3.2) + method_source (0.9.1) + mime-types (3.2.2) mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) - mini_mime (1.0.0) + mime-types-data (3.2018.0812) + mimemagic (0.3.2) + mini_mime (1.0.1) mini_portile2 (2.3.0) minitest (5.11.3) mnemosyne-ruby (1.5.1) activesupport (>= 4) bunny + msgpack (1.2.4) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) net-scp (1.2.1) net-ssh (>= 2.6.5) - net-ssh (4.2.0) + net-ssh (5.0.2) netrc (0.11.0) - newrelic_rpm (4.8.0.341) - nokogiri (1.8.3) + newrelic_rpm (5.4.0.347) + nio4r (2.3.1) + nokogiri (1.8.5) mini_portile2 (~> 2.3.0) nyan-cat-formatter (0.12.0) rspec (>= 2.99, >= 2.14.2, < 4) - oauth (0.4.7) - oauth2 (1.4.0) - faraday (>= 0.8, < 0.13) - jwt (~> 1.0) + oauth (0.5.4) + oauth2 (1.4.1) + faraday (>= 0.8, < 0.16.0) + jwt (>= 1.0, < 3.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - pagedown-rails (1.1.4) + pagedown-bootstrap-rails (2.1.4) railties (> 3.1) parallel (1.12.1) - parser (2.5.0.3) + parser (2.5.3.0) ast (~> 2.4.0) - pg (0.21.0) - polyamorous (1.3.3) - activerecord (>= 3.0) - powerpack (0.1.1) - pry (0.11.3) + pg (1.1.3) + powerpack (0.1.2) + pry (0.12.0) coderay (~> 1.1.0) method_source (~> 0.9.0) pry-byebug (3.6.0) byebug (~> 10.0) pry (~> 0.10) - public_suffix (3.0.2) - puma (3.11.3) - pundit (1.1.0) + public_suffix (3.0.3) + puma (3.12.0) + pundit (2.0.0) activesupport (>= 3.0.0) - rack (1.6.10) - rack-mini-profiler (0.10.7) + rack (2.0.6) + rack-mini-profiler (1.0.0) rack (>= 1.2.0) - rack-test (0.6.3) - rack (>= 1.0) - rails (4.2.10) - actionmailer (= 4.2.10) - actionpack (= 4.2.10) - actionview (= 4.2.10) - activejob (= 4.2.10) - activemodel (= 4.2.10) - activerecord (= 4.2.10) - activesupport (= 4.2.10) - bundler (>= 1.3.0, < 2.0) - railties (= 4.2.10) - sprockets-rails - rails-deprecated_sanitizer (1.0.3) - activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.9) - activesupport (>= 4.2.0, < 5.0) - nokogiri (~> 1.6) - rails-deprecated_sanitizer (>= 1.0.1) + rack-proxy (0.6.5) + rack + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (5.2.1) + actioncable (= 5.2.1) + actionmailer (= 5.2.1) + actionpack (= 5.2.1) + actionview (= 5.2.1) + activejob (= 5.2.1) + activemodel (= 5.2.1) + activerecord (= 5.2.1) + activestorage (= 5.2.1) + activesupport (= 5.2.1) + bundler (>= 1.3.0) + railties (= 5.2.1) + sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.2) + actionpack (~> 5.x, >= 5.0.1) + actionview (~> 5.x, >= 5.0.1) + activesupport (~> 5.x) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) - rails-i18n (4.0.9) - i18n (~> 0.7) - railties (~> 4.0) - railties (4.2.10) - actionpack (= 4.2.10) - activesupport (= 4.2.10) + rails-i18n (5.1.2) + i18n (>= 0.7, < 2) + railties (>= 5.0, < 6) + railties (5.2.1) + actionpack (= 5.2.1) + activesupport (= 5.2.1) + method_source rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) + thor (>= 0.19.0, < 2.0) rainbow (3.0.0) rake (12.3.1) - ransack (1.8.7) - actionpack (>= 3.0) - activerecord (>= 3.0) - activesupport (>= 3.0) + ransack (2.1.0) + actionpack (>= 5.0) + activerecord (>= 5.0) + activesupport (>= 5.0) i18n - polyamorous (~> 1.3.2) rb-fsevent (0.10.3) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) - rdoc (6.0.1) + regexp_parser (1.2.0) rest-client (2.0.2) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rspec (3.7.0) - rspec-core (~> 3.7.0) - rspec-expectations (~> 3.7.0) - rspec-mocks (~> 3.7.0) - rspec-autotest (1.0.0) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-autotest (1.0.2) rspec-core (>= 2.99.0.beta1, < 4.0.0) - rspec-core (3.7.1) - rspec-support (~> 3.7.0) - rspec-expectations (3.7.0) + rspec-core (3.8.0) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) - rspec-mocks (3.7.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.7.0) - rspec-rails (3.7.2) + rspec-support (~> 3.8.0) + rspec-rails (3.8.1) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.7.0) - rspec-expectations (~> 3.7.0) - rspec-mocks (~> 3.7.0) - rspec-support (~> 3.7.0) - rspec-support (3.7.1) - rubocop (0.53.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + rubocop (0.60.0) + jaro_winkler (~> 1.5.1) parallel (~> 1.10) - parser (>= 2.5) + parser (>= 2.5, != 2.5.1.1) powerpack (~> 0.1) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) - unicode-display_width (~> 1.0, >= 1.0.1) - rubocop-rspec (1.24.0) - rubocop (>= 0.53.0) - ruby-progressbar (1.9.0) + unicode-display_width (~> 1.4.0) + rubocop-rspec (1.30.1) + rubocop (>= 0.60.0) + ruby-progressbar (1.10.0) + ruby_dep (1.5.0) rubytree (1.0.0) json (~> 2.1) structured_warnings (~> 0.3) - rubyzip (1.2.1) - sass (3.5.6) + rubyzip (1.2.2) + sass (3.6.0) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) @@ -313,24 +327,22 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sdoc (1.0.0) - rdoc (>= 5.0) - selenium-webdriver (3.13.0) + selenium-webdriver (3.141.0) childprocess (~> 0.5) - rubyzip (~> 1.2) - simplecov (0.15.1) - docile (~> 1.1.0) + rubyzip (~> 1.2, >= 1.2.2) + simplecov (0.16.1) + docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) - slim (3.0.9) + slim (4.0.1) temple (>= 0.7.6, < 0.9) - tilt (>= 1.3.3, < 2.1) - slim-rails (3.1.3) + tilt (>= 2.0.6, < 2.1) + slim-rails (3.2.0) actionpack (>= 3.1) railties (>= 3.1) - slim (~> 3.0) - sorcery (0.11.0) + slim (>= 3.0, < 5.0) + sorcery (0.12.0) bcrypt (~> 3.1) oauth (~> 0.4, >= 0.4.4) oauth2 (~> 1.0, >= 0.8.0) @@ -343,7 +355,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sshkit (1.16.0) + sshkit (1.18.0) net-scp (>= 1.1.2) net-ssh (>= 2.8.0) structured_warnings (0.3.0) @@ -351,58 +363,55 @@ GEM thor (0.20.0) thread_safe (0.3.6) tilt (2.0.8) - tubesock (0.2.7) - rack (>= 1.5.0) - websocket (>= 1.1.0) - turbolinks (2.5.4) - coffee-rails + turbolinks (5.2.0) + turbolinks-source (~> 5.2) + turbolinks-source (5.2.0) tzinfo (1.2.5) thread_safe (~> 0.1) - uglifier (4.1.6) + uglifier (4.1.19) execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext unf_ext (0.0.7.5) - unicode-display_width (1.3.0) - web-console (3.3.0) - activemodel (>= 4.2) - debug_inspector + unicode-display_width (1.4.0) + web-console (3.7.0) + actionview (>= 5.0) + activemodel (>= 5.0) + bindex (>= 0.4.0) + railties (>= 5.0) + webpacker (3.5.5) + activesupport (>= 4.2) + rack-proxy (>= 0.6.1) railties (>= 4.2) - websocket (1.2.5) + websocket (1.2.8) websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.3) whenever (0.10.0) chronic (>= 0.6.3) will_paginate (3.1.6) - xpath (3.1.0) + xpath (3.2.0) nokogiri (~> 1.8) PLATFORMS ruby DEPENDENCIES - activerecord-deprecated_finders - activerecord-jdbcpostgresql-adapter autotest-rails bcrypt better_errors binding_of_caller + bootsnap bootstrap-will_paginate - bootstrap_pagedown (>= 1.1.0) - byebug capistrano capistrano-rails capistrano-rvm capistrano-upload-config capistrano3-puma capybara - capybara-selenium (>= 0.0.6) carrierwave - codeclimate-test-reporter concurrent-ruby - concurrent-ruby-ext - d3-rails (~> 4.0) + d3-rails database_cleaner docker-api eventmachine (= 1.0.9.1) @@ -411,24 +420,24 @@ DEPENDENCIES forgery headless highline - ims-lti (= 1.1.10) + ims-lti (< 2.0.0) jbuilder jquery-rails - jquery-turbolinks kramdown - mnemosyne-ruby (~> 1.0) + listen + mnemosyne-ruby newrelic_rpm nokogiri nyan-cat-formatter - pagedown-rails - pg (< 1.0) + pagedown-bootstrap-rails + pg pry-byebug puma pundit rack-mini-profiler - rails (= 4.2.10) + rails (= 5.2.1) + rails-controller-testing rails-i18n - rake ransack rest-client rspec-autotest @@ -437,19 +446,18 @@ DEPENDENCIES rubocop-rspec rubytree rubyzip - sass-rails (>= 5.0.7) - sdoc + sass-rails + selenium-webdriver simplecov slim-rails sorcery spring - thread_safe - tubesock - turbolinks (< 5.0.0) + tubesock! + turbolinks uglifier web-console + webpacker whenever - will_paginate BUNDLED WITH - 1.16.1 + 1.16.5 diff --git a/LOCAL_SETUP.md b/LOCAL_SETUP.md index 402ad33f..dbe5876c 100644 --- a/LOCAL_SETUP.md +++ b/LOCAL_SETUP.md @@ -59,7 +59,7 @@ All problems that have occurred resulted from a more restrictive rights manageme ### Start server vagrant ssh cd /vagrant -rails s -p 3000 +rails s -p 3000 -b 0.0.0.0 ### Login to CodeOcean 192.168.59.104:3000 diff --git a/README.md b/README.md index 65d36819..7df97f26 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ Code Ocean [![Build Status](https://travis-ci.org/openHPI/codeocean.svg?branch=master)](https://travis-ci.org/openHPI/codeocean) [![Code Climate](https://codeclimate.com/github/openHPI/codeocean/badges/gpa.svg)](https://codeclimate.com/github/openHPI/codeocean) [![Test Coverage](https://codeclimate.com/github/openHPI/codeocean/badges/coverage.svg)](https://codeclimate.com/github/openHPI/codeocean) -[![Dependency Status](https://gemnasium.com/openHPI/codeocean.svg)](https://gemnasium.com/openHPI/codeocean) ## Development Setup diff --git a/Vagrantfile b/Vagrantfile index cc6acf2c..46896e9d 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -7,6 +7,7 @@ Vagrant.configure(2) do |config| v.memory = 8192 end config.vm.network "private_network", ip: "192.168.59.104" + config.vm.network "forwarded_port", guest: 3035, host: 3035 # config.vm.synced_folder "../data", "/vagrant_data" config.vm.provision "shell", path: "provision.sh", privileged: false end diff --git a/app/assets/images/.keep b/app/assets/images/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 9ccb9815..4a3878f5 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -10,20 +10,18 @@ // 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-ui.min -//= require d3 -//= require jquery.turbolinks //= require jquery_ujs -//= require jstree/jstree.min //= require turbolinks -//= require_tree ../../../lib +//= require pagedown_bootstrap +//= require d3 +// +// lib/assets +//= require flash +//= require url +// +// vendor/assets +//= require ace/ace +//= require ace/ext-language_tools +// +// app/assets //= require_tree . -//= require bootstrap_pagedown -//= require markdown.converter -//= require markdown.sanitizer -//= require markdown.editor -//= require ace/ext-language_tools \ No newline at end of file diff --git a/app/assets/javascripts/base.js b/app/assets/javascripts/base.js index 235a6e39..b7263d48 100644 --- a/app/assets/javascripts/base.js +++ b/app/assets/javascripts/base.js @@ -8,20 +8,18 @@ window.CodeOcean = { } }; -$(function() { - var ANIMATION_DURATION = 500; +var ANIMATION_DURATION = 500; - $.isController = function(name) { - return $('.container[data-controller="' + name + '"]').isPresent(); - }; +$.isController = function(name) { + return $('.container[data-controller="' + name + '"]').isPresent(); +}; - $.fn.isPresent = function() { - return this.length > 0; - }; +$.fn.isPresent = function() { + return this.length > 0; +}; - $.fn.scrollTo = function(selector) { - $(this).animate({ - scrollTop: $(selector).offset().top - $(this).offset().top + $(this).scrollTop() - }, ANIMATION_DURATION); - }; -}); +$.fn.scrollTo = function(selector) { + $(this).animate({ + scrollTop: $(selector).offset().top - $(this).offset().top + $(this).scrollTop() + }, ANIMATION_DURATION); +}; diff --git a/app/assets/javascripts/bootstrap-dropdown-submenu.js b/app/assets/javascripts/bootstrap-dropdown-submenu.js index 40850c29..10809c02 100644 --- a/app/assets/javascripts/bootstrap-dropdown-submenu.js +++ b/app/assets/javascripts/bootstrap-dropdown-submenu.js @@ -1,4 +1,4 @@ -$(document).ready(function () { +$(document).on('turbolinks:load', function() { var subMenusSelector = 'ul.dropdown-menu [data-toggle=dropdown]'; @@ -14,7 +14,7 @@ $(document).ready(function () { var menu = $(this).parent().find("ul"); var menupos = menu.offset(); - var newPos; + var newPos = 0.0; if ((menupos.left + menu.width()) + 30 > $(window).width()) { newPos = -menu.width(); } else { diff --git a/app/assets/javascripts/dashboard.js b/app/assets/javascripts/dashboard.js index ed7c5b68..c8b1ae51 100644 --- a/app/assets/javascripts/dashboard.js +++ b/app/assets/javascripts/dashboard.js @@ -1,4 +1,4 @@ -$(function() { +$(document).on('turbolinks:load', function() { var CHART_START = window.vis ? vis.moment().add(-1, 'minute') : undefined; var DEFAULT_REFRESH_INTERVAL = 5000; @@ -51,6 +51,7 @@ $(function() { } else { var jqxhr = $.ajax({ dataType: 'json', + url: '/admin/dashboard', method: 'GET' }); jqxhr.done(function(response) { diff --git a/app/assets/javascripts/editor.js.erb b/app/assets/javascripts/editor.js similarity index 92% rename from app/assets/javascripts/editor.js.erb rename to app/assets/javascripts/editor.js index 47f96fcb..0df0ffe2 100644 --- a/app/assets/javascripts/editor.js.erb +++ b/app/assets/javascripts/editor.js @@ -1,4 +1,4 @@ -$(function() { +$(document).on('turbolinks:load', function() { //Merge all editor components. $.extend( diff --git a/app/assets/javascripts/editor/ajax.js.erb b/app/assets/javascripts/editor/ajax.js similarity index 100% rename from app/assets/javascripts/editor/ajax.js.erb rename to app/assets/javascripts/editor/ajax.js diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 1e254dd3..4e26723a 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -33,6 +33,7 @@ var CodeOceanEditor = { <% @config ||= CodeOcean::Config.new(:code_ocean).read(erb: false) %> sendEvents: <%= @config['codeocean_events'] ? @config['codeocean_events']['enabled'] : false %>, eventURL: "<%= @config['codeocean_events'] ? events_path : '' %>", + fileTypeURL: "<%= file_types_path %>", configureEditors: function () { @@ -43,7 +44,7 @@ configureEditors: function () { confirmDestroy: function (event) { event.preventDefault(); - if (confirm($(this).data('message-confirm'))) { + if (confirm($(event.target).data('message-confirm'))) { this.destroyFile(); } }, @@ -63,7 +64,7 @@ configureEditors: function () { if ($('#output-' + index).isPresent()) { return $('#output-' + index); } else { - var element = $('
').attr('id', 'output-' + index);
+      var element = $('
').attr('id', 'output-' + index);
       $('#output').append(element);
       return element;
     }
@@ -79,13 +80,13 @@ configureEditors: function () {
     }
   },
 
-  getPanelClass: function (result) {
+  getCardClass: function (result) {
     if (result.stderr && !result.score) {
-      return 'panel-danger';
+      return 'card bg-danger text-white';
     } else if (result.score < 1) {
-      return 'panel-warning';
+      return 'card bg-warning text-white';
     } else {
-      return 'panel-success';
+      return 'card bg-success text-white';
     }
   },
 
@@ -107,11 +108,22 @@ configureEditors: function () {
     progress_bar.css('width', percentage + '%');
   },
 
+  // The event ready.jstree is fired too early and thus doesn't work.
+  selectFileInJsTree: function(filetree, file_id) {
+    if (!filetree.hasClass('jstree-loading')) {
+      filetree.jstree("deselect_all");
+      filetree.jstree().select_node(file_id);
+    } else {
+      setTimeout(this.selectFileInJsTree.bind(null, filetree, file_id), 250);
+    }
+  },
+
   showFirstFile: function() {
     var frame = $('.frame[data-role="main_file"]').isPresent() ? $('.frame[data-role="main_file"]') : $('.frame').first();
     var file_id = frame.find('.editor').data('file-id');
     this.setActiveFile(frame.data('filename'), file_id);
-    $('#files').jstree().select_node(file_id);
+    var filetree = $('#files');
+    this.selectFileInJsTree(filetree, file_id);
     this.showFrame(frame);
     this.toggleButtonStates();
   },
@@ -124,11 +136,11 @@ configureEditors: function () {
 
   getProgressBarClass: function (percentage) {
     if (percentage < this.ADEQUATE_PERCENTAGE) {
-      return 'progress-bar progress-bar-striped progress-bar-danger';
+      return 'progress-bar progress-bar-striped bg-danger';
     } else if (percentage < this.SUCCESSFULL_PERCENTAGE) {
-      return 'progress-bar progress-bar-striped progress-bar-warning';
+      return 'progress-bar progress-bar-striped bg-warning';
     } else {
-      return 'progress-bar progress-bar-striped progress-bar-success';
+      return 'progress-bar progress-bar-striped bg-success';
     }
   },
 
@@ -158,7 +170,7 @@ configureEditors: function () {
         category: 'editor_paste',
         data: pasteObject.text,
         exercise_id: $('#editor').data('exercise-id'),
-        file_id: $(this).data('file-id')
+        file_id: $(event.target).data('file-id')
       });
     }
   },
@@ -177,8 +189,8 @@ configureEditors: function () {
   },
 
   resizeParentOfAceEditor: function (element){
-    // calculate needed size: window height - position of top of button-bar - 60 for bar itself and margins
-    var windowHeight = window.innerHeight - $('#editor-buttons').offset().top - 60;
+    // calculate needed size: window height - position of top of ACE editor - height of autosave label below editor - 5 for bar margins
+    var windowHeight = window.innerHeight - $(element).offset().top - $('#autosave-label').height() - 5;
     $(element).parent().height(windowHeight);
   },
 
@@ -266,14 +278,29 @@ configureEditors: function () {
     this.initializeRequestForComments()
   },
 
+  updateEditorModeToFileTypeID: function (editor, fileTypeID) {
+    var newMode = 'ace/mode/text'
+
+    $.ajax(this.fileTypeURL + '/' + fileTypeID, {
+      dataType: 'json'
+    }).done(function (data) {
+        if (data['editor_mode'] !== null) {
+          newMode = data['editor_mode'];
+        }
+    }).fail(_.noop)
+      .always(function () {
+        ace.edit(editor).session.setMode(newMode);
+    });
+  },
+
   initializeFileTree: function () {
     $('#files').jstree($('#files').data('entries'));
     $('#files').on('click', 'li.jstree-leaf', function (event) {
-      active_file = {
-        filename: $(event.target).parent().text(),
-        id: parseInt($(event.target).parent().attr('id'))
-      };
-      var frame = $('[data-file-id="' + active_file.id + '"]').parent();
+      this.setActiveFile(
+        $(event.target).parent().text(),
+        parseInt($(event.target).parent().attr('id'))
+      );
+      var frame = $('[data-file-id="' + this.active_file.id + '"]').parent();
       this.showFrame(frame);
       this.toggleButtonStates();
     }.bind(this));
@@ -296,8 +323,8 @@ configureEditors: function () {
 
   handleSideBarToggle: function() {
     $('#sidebar').toggleClass('sidebar-col').toggleClass('sidebar-col-collapsed');
-    $('#sidebar-collapsed').toggleClass('hidden');
-    $('#sidebar-uncollapsed').toggleClass('hidden');
+    $('#sidebar-collapsed').toggleClass('d-none');
+    $('#sidebar-uncollapsed').toggleClass('d-none');
   },
 
   initializeRegexes: function () {
@@ -369,17 +396,17 @@ configureEditors: function () {
     return Modernizr.websockets;
   },
 
-  populatePanel: function (panel, result, index) {
-    panel.removeClass('panel-default').addClass(this.getPanelClass(result));
-    panel.find('.panel-title .filename').text(result.filename);
-    panel.find('.panel-title .number').text(index + 1);
-    panel.find('.row .col-sm-9').eq(0).find('.number').eq(0).text(result.passed);
-    panel.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count);
-    panel.find('.row .col-sm-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2)));
-    panel.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight);
-    panel.find('.row .col-sm-9').eq(2).html(result.message);
-    if (result.error_messages) panel.find('.row .col-sm-9').eq(3).text(result.error_messages.join(' '));
-    //panel.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index);
+  populateCard: function (card, result, index) {
+    card.addClass(this.getCardClass(result));
+    card.find('.card-title .filename').text(result.filename);
+    card.find('.card-title .number').text(index + 1);
+    card.find('.row .col-sm-9').eq(0).find('.number').eq(0).text(result.passed);
+    card.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count);
+    card.find('.row .col-sm-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2)));
+    card.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight);
+    card.find('.row .col-sm-9').eq(2).html(result.message);
+    if (result.error_messages) card.find('.row .col-sm-9').eq(3).text(result.error_messages.join(' '));
+    //card.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index);
   },
 
   publishCodeOceanEvent: function (payload) {
@@ -409,7 +436,7 @@ configureEditors: function () {
       url: $('#editor').data('errors-url')
     });
     jqxhr.always(this.hideSpinner);
-    jqxhr.success(this.renderHint);
+    jqxhr.done(this.renderHint);
   },
 
   toggleButtonStates: function () {
@@ -556,14 +583,14 @@ configureEditors: function () {
   },
 
   showOutputBar: function() {
-    $('#output_sidebar_collapsed').addClass('hidden');
-    $('#output_sidebar_uncollapsed').removeClass('hidden');
+    $('#output_sidebar_collapsed').addClass('d-none');
+    $('#output_sidebar_uncollapsed').removeClass('d-none');
     $('#output_sidebar').removeClass('output-col-collapsed').addClass('output-col');
   },
 
   hideOutputBar: function() {
-    $('#output_sidebar_collapsed').removeClass('hidden');
-    $('#output_sidebar_uncollapsed').addClass('hidden');
+    $('#output_sidebar_collapsed').removeClass('d-none');
+    $('#output_sidebar_uncollapsed').addClass('d-none');
     $('#output_sidebar').removeClass('output-col').addClass('output-col-collapsed');
   },
 
@@ -572,16 +599,17 @@ configureEditors: function () {
   },
 
   initializeDescriptionToggle: function() {
-    $('#exercise-headline').on('click', this.toggleDescriptionPanel.bind(this));
-    $('a#toggle').on('click', this.toggleDescriptionPanel.bind(this));
+    $('#exercise-headline').on('click', this.toggleDescriptionCard.bind(this));
+    $('a#toggle').on('click', this.toggleDescriptionCard.bind(this));
   },
 
-  toggleDescriptionPanel: function() {
-    $('#description-panel').toggleClass('description-panel-collapsed').toggleClass('description-panel');
+  toggleDescriptionCard: function() {
+    $('#description-card').toggleClass('description-card-collapsed').toggleClass('description-card');
     $('#description-symbol').toggleClass('fa-chevron-down').toggleClass('fa-chevron-right');
     var toggle = $('a#toggle');
     toggle.text(toggle.text() == toggle.data('hide') ? toggle.data('show') : toggle.data('hide'));
     this.resizeAceEditors();
+    event.preventDefault();
   },
 
     /**
diff --git a/app/assets/javascripts/editor/evaluation.js.erb b/app/assets/javascripts/editor/evaluation.js
similarity index 84%
rename from app/assets/javascripts/editor/evaluation.js.erb
rename to app/assets/javascripts/editor/evaluation.js
index 806e3a91..79bd52a8 100644
--- a/app/assets/javascripts/editor/evaluation.js.erb
+++ b/app/assets/javascripts/editor/evaluation.js
@@ -9,7 +9,7 @@ CodeOceanEditorEvaluation = {
     this.clearScoringOutput();
     this.createSubmission('#assess', null, function (response) {
       this.showSpinner($('#assess'));
-      $('#score_div').removeClass('hidden');
+      $('#score_div').removeClass('d-none');
       var url = response.score_url;
       this.initializeSocketForScoring(url);
     }.bind(this));
@@ -26,9 +26,9 @@ CodeOceanEditorEvaluation = {
 
   printScoringResult: function (result, index) {
     $('#results').show();
-    var panel = $('#dummies').children().first().clone();
-    this.populatePanel(panel, result, index);
-    $('#results ul').first().append(panel);
+    var card = $('#dummies').children().first().clone();
+    this.populateCard(card, result, index);
+    $('#results ul').first().append(card);
   },
 
   printScoringResults: function (response) {
@@ -60,7 +60,7 @@ CodeOceanEditorEvaluation = {
   renderHint: function (object) {
     var hint = object.data || object.hint;
     if (hint) {
-      $('#hint .panel-body').text(hint);
+      $('#hint .card-body').text(hint);
       $('#hint').fadeIn();
     }
   },
@@ -152,22 +152,22 @@ CodeOceanEditorEvaluation = {
     var element = this.findOrCreateOutputElement(index);
     if (!colorize) {
       if (output.stdout != undefined && output.stdout != '') {
-        element.append(output.stdout)
-        //element.text(element.text() + output.stdout)
+        //element.append(output.stdout)
+        element.text(element.text() + output.stdout)
       }
 
       if (output.stderr != undefined && output.stderr != '') {
-          element.append('StdErr: ' + output.stderr);
-        //element.text('StdErr: ' + element.text() + output.stderr);
+        //element.append('StdErr: ' + output.stderr);
+        element.text('StdErr: ' + element.text() + output.stderr);
       }
 
     } else if (output.stderr) {
-        element.addClass('text-warning').append(output.stderr);
-        //element.addClass('text-warning').text(element.text() + output.stderr);
+        //element.addClass('text-warning').append(output.stderr);
+        element.addClass('text-warning').text(element.text() + output.stderr);
       this.QaApiOutputBuffer.stderr += output.stderr;
     } else if (output.stdout) {
-        element.addClass('text-success').append(output.stdout);
-        //element.addClass('text-success').text(element.text() + output.stdout);
+        //element.addClass('text-success').append(output.stdout);
+        element.addClass('text-success').text(element.text() + output.stdout);
       this.QaApiOutputBuffer.stdout += output.stdout;
     } else {
       element.addClass('text-muted').text($('#output').data('message-no-output'));
diff --git a/app/assets/javascripts/editor/execution.js.erb b/app/assets/javascripts/editor/execution.js.erb
index a5126462..5f8aa248 100644
--- a/app/assets/javascripts/editor/execution.js.erb
+++ b/app/assets/javascripts/editor/execution.js.erb
@@ -2,8 +2,9 @@ CodeOceanEditorWebsocket = {
   websocket: null,
 
   createSocketUrl: function(url) {
-      var sockURL = new URL(window.location);
-      sockURL.pathname = url;
+      var sockURL = new URL(url, window.location);
+      // not needed any longer, we put it directly into the url: sockURL.pathname = url;
+
       // sanitize socket protocol string, strip trailing slash and other malicious chars if they are there
       sockURL.protocol = '<%= DockerClient.config['ws_client_protocol']&.match(/(\w+):*\/*/)&.to_a&.at(1) %>:';
 
diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb
index 7f9d586d..d417ee9e 100644
--- a/app/assets/javascripts/editor/participantsupport.js.erb
+++ b/app/assets/javascripts/editor/participantsupport.js.erb
@@ -90,7 +90,6 @@ CodeOceanEditorFlowr = {
         var collapsibleTileHtml = self.flowrResultHtml
           .replace(/{{collapseId}}/g, 'collapse-' + index).replace(/{{headingId}}/g, 'heading-' + index);
         var resultTile = $(collapsibleTileHtml);
-
         var questionUrl = 'https://stackoverflow.com/questions/' + result.question_id;
 
         var header = resultTile.find('h4 > a');
@@ -162,7 +161,7 @@ CodeOceanEditorRequestForComments = {
         $.flash.success({text: $('#askForCommentsButton').data('message-success')});
         // trigger a run
         this.runSubmission.call(this, submission);
-      }.bind(this)).error(this.ajaxError.bind(this));
+      }.bind(this)).fail(this.ajaxError.bind(this));
     };
 
     this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this));
diff --git a/app/assets/javascripts/editor/prompt.js.erb b/app/assets/javascripts/editor/prompt.js
similarity index 80%
rename from app/assets/javascripts/editor/prompt.js.erb
rename to app/assets/javascripts/editor/prompt.js
index 1cbdfc8f..e767ba43 100644
--- a/app/assets/javascripts/editor/prompt.js.erb
+++ b/app/assets/javascripts/editor/prompt.js
@@ -2,19 +2,19 @@ CodeOceanEditorPrompt = {
     prompt: '#prompt',
 
     showPrompt: function(msg) {
-        var label = $('#prompt .input-group-addon');
+        var label = $('#prompt .input-group-text');
         var prompt = $(this.prompt);
         label.text(msg.data || label.data('prompt'));
-        if (prompt.isPresent() && prompt.hasClass('hidden')) {
-            prompt.removeClass('hidden');
+        if (prompt.isPresent() && prompt.hasClass('d-none')) {
+            prompt.removeClass('d-none');
         }
         $('#prompt input').focus();
     },
 
     hidePrompt: function() {
         var prompt = $(this.prompt);
-        if (prompt.isPresent() && !prompt.hasClass('hidden')) {
-            prompt.addClass('hidden');
+        if (prompt.isPresent() && !prompt.hasClass('d-none')) {
+            prompt.addClass('d-none');
         }
     },
 
diff --git a/app/assets/javascripts/editor/submissions.js.erb b/app/assets/javascripts/editor/submissions.js
similarity index 95%
rename from app/assets/javascripts/editor/submissions.js.erb
rename to app/assets/javascripts/editor/submissions.js
index 21250f7e..4dae40b5 100644
--- a/app/assets/javascripts/editor/submissions.js.erb
+++ b/app/assets/javascripts/editor/submissions.js
@@ -106,14 +106,16 @@ CodeOceanEditorSubmissions = {
     this.ajax({
       method: 'GET',
       url: $('#start-over').data('url')
-    }).success(function(response) {
+    }).done(function(response) {
       this.hideSpinner();
       _.each(this.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);
+        if(file){
+            editor.setValue(file.content);
+        }
       }.bind(this));
     }.bind(this));
   },
@@ -153,7 +155,7 @@ CodeOceanEditorSubmissions = {
     $('#stop').data('url', submission.stop_url);
     this.running = true;
     this.showSpinner($('#run'));
-    $('#score_div').addClass('hidden');
+    $('#score_div').addClass('d-none');
     this.toggleButtonStates();
     var url = submission.run_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename.replace(/#$/,'')); // remove # if it is the last character, this is not part of the filename and just an anchor
     this.initializeSocketForRunning(url);
@@ -173,8 +175,8 @@ CodeOceanEditorSubmissions = {
     if ($('#test').is(':visible')) {
       this.createSubmission('#test', null, function(response) {
         this.showSpinner($('#test'));
-        $('#score_div').addClass('hidden');
-        var url = response.test_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filenamereplace(/#$/,'')); // remove # if it is the last character, this is not part of the filename and just an anchor
+        $('#score_div').addClass('d-none');
+        var url = response.test_url.replace(this.FILENAME_URL_PLACEHOLDER, this.active_file.filename.replace(/#$/,'')); // remove # if it is the last character, this is not part of the filename and just an anchor
         this.initializeSocketForTesting(url);
       }.bind(this));
     }
diff --git a/app/assets/javascripts/editor/turtle.js.erb b/app/assets/javascripts/editor/turtle.js
similarity index 92%
rename from app/assets/javascripts/editor/turtle.js.erb
rename to app/assets/javascripts/editor/turtle.js
index bc5552eb..1bf80b32 100644
--- a/app/assets/javascripts/editor/turtle.js.erb
+++ b/app/assets/javascripts/editor/turtle.js
@@ -38,10 +38,10 @@ CodeOceanEditorTurtle = {
 
   showCanvas: function () {
     if ($('#turtlediv').isPresent()
-        && this.turtlecanvas.hasClass('hidden')) {
+        && this.turtlecanvas.hasClass('d-none')) {
       // initialize two-column layout
       $('#output-col1').addClass('col-lg-7 col-md-7 two-column');
-      this.turtlecanvas.removeClass('hidden');
+      this.turtlecanvas.removeClass('d-none');
     }
   }
 
diff --git a/app/assets/javascripts/editor/websocket.js.erb b/app/assets/javascripts/editor/websocket.js
similarity index 100%
rename from app/assets/javascripts/editor/websocket.js.erb
rename to app/assets/javascripts/editor/websocket.js
diff --git a/app/assets/javascripts/editor_edit.js.erb b/app/assets/javascripts/editor_edit.js.erb
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/assets/javascripts/error_templates.js b/app/assets/javascripts/error_templates.js
index 1b9b5234..74978157 100644
--- a/app/assets/javascripts/error_templates.js
+++ b/app/assets/javascripts/error_templates.js
@@ -1,4 +1,4 @@
-$(function() {
+$(document).on('turbolinks:load', function() {
     if ($.isController('error_templates')) {
         $('#add-attribute').find('button').on('click', function () {
             $.ajax(location + '/attribute.json', {
@@ -8,9 +8,9 @@ $(function() {
                     dataType: 'json',
                     error_template_attribute_id: $('#add-attribute').find('select').val()
                 }
-            }).success(function () {
+            }).done(function () {
                 location.reload();
-            }).error(function (error) {
+            }).fail(function (error) {
                 $.flash.danger({text: error.statusText});
             });
         });
diff --git a/app/assets/javascripts/execution_environments.js b/app/assets/javascripts/execution_environments.js
index 73bf7634..3f6fdc63 100644
--- a/app/assets/javascripts/execution_environments.js
+++ b/app/assets/javascripts/execution_environments.js
@@ -1,4 +1,4 @@
-$(function() {
+$(document).on('turbolinks:load', function() {
   if ($.isController('execution_environments')) {
     if ($('.edit_execution_environment, .new_execution_environment').isPresent()) {
       new MarkdownEditor('#execution_environment_help');
diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb
index 9029ca5b..a3c46b99 100644
--- a/app/assets/javascripts/exercise_collections.js.erb
+++ b/app/assets/javascripts/exercise_collections.js.erb
@@ -1,4 +1,4 @@
-$(function() {
+$(document).on('turbolinks:load', function() {
   if ($.isController('exercise_collections')) {
     var dataElement = $('#data');
     var exerciseList = $('#exercise-list');
diff --git a/app/assets/javascripts/exercise_graphs.js b/app/assets/javascripts/exercise_graphs.js
index b095e1a5..86014010 100644
--- a/app/assets/javascripts/exercise_graphs.js
+++ b/app/assets/javascripts/exercise_graphs.js
@@ -1,4 +1,4 @@
-$(function() {
+$(document).on('turbolinks:load', function() {
     // /exercises/38/statistics good for testing
 
     if ($.isController('exercises') && $('.graph-functions-2').isPresent()) {
diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb
index bca0c8b0..71167533 100644
--- a/app/assets/javascripts/exercises.js.erb
+++ b/app/assets/javascripts/exercises.js.erb
@@ -1,4 +1,4 @@
-$(function() {
+$(document).on('turbolinks:load', function() {
   // ruby part adds the relative_url_root, if it is set.
   var ACE_FILES_PATH = '<%= (defined?  Rails.application.config.relative_url_root) &&  Rails.application.config.relative_url_root != nil &&  Rails.application.config.relative_url_root != "" ?  Rails.application.config.relative_url_root : "" %>' + '/assets/ace/';
   var THEME = 'ace/theme/textmate';
@@ -66,10 +66,9 @@ $(function() {
     $('#files').append(html);
     $('#files li:last select[name*="file_type_id"]').val(getSelectedExecutionEnvironment().file_type_id);
     $('#files li:last select').chosen(window.CodeOcean.CHOSEN_OPTIONS);
+    $('#files li:last select').remove();
+    $('#files li:last>div:last').removeClass('in').addClass('show')
     $('body, html').scrollTo('#add-file');
-    // if we collapse the file forms by default, we need to click on the new element in order to open it.
-    // however, this crashes for more files (if we add several ones by clicking the add button more often), since the elements are probably not correctly added to the files list.
-    //$('#files li:last>div:first>a>div').click();
 
     // initialize the ace editor for the new textarea.
     // pass the correct index and the last ace editor under the node files. this is the last one, since we just added it.
@@ -93,7 +92,7 @@ $(function() {
 
   var deleteFile = function(event) {
     event.preventDefault();
-    var fileUrl = $(this).data('file-url');
+    var fileUrl = $(event.target).data('file-url');
 
     if (confirm('<%= I18n.t('shared.confirm_destroy') %>')) {
       var jqxhr = $.ajax({
@@ -139,9 +138,9 @@ $(function() {
   var enableBatchUpdate = function() {
     $('thead .batch a').on('click', function(event) {
       event.preventDefault();
-      if (!$(this).data('toggled')) {
-        $(this).data('toggled', true);
-        $(this).text($(this).data('text'));
+      if (!$(event.target).data('toggled')) {
+        $(event.target).data('toggled', true);
+        $(event.target).text($(event.target).data('text'));
         buildCheckboxes();
       } else {
         performBatchUpdate();
@@ -199,7 +198,7 @@ $(function() {
   var observeFileRoleChanges = function() {
     $(document).on('change', 'select[name$="[role]"]', function() {
       var is_test_file = $(this).val() === 'teacher_defined_test';
-      var parent = $(this).parents('.panel');
+      var parent = $(this).parents('.card');
       var fields = parent.find('.test-related-fields');
       if (is_test_file) {
         fields.slideDown();
@@ -262,17 +261,13 @@ $(function() {
       jqxhr.fail(ajaxError);
   }
 
-  if ($.isController('exercises')) {
+  if ($.isController('exercises') || $.isController('submissions')) {
       // ignore tags table since it is in the dom before other tables
     if ($('table:not(#tags-table)').isPresent()) {
       enableBatchUpdate();
     } else if ($('.edit_exercise, .new_exercise').isPresent()) {
       execution_environments = $('form').data('execution-environments');
       file_types = $('form').data('file-types');
-      // new MarkdownEditor('#exercise_instructions');
-      // new MarkdownEditor('#exercise_description')
-      // todo: add an ace editor for each file
-      new PagedownEditor('#exercise_description');
 
       enableInlineFileCreation();
       inferFileAttributes();
diff --git a/app/assets/javascripts/external_users.js b/app/assets/javascripts/external_users.js
index e262e5e5..058fdfa0 100644
--- a/app/assets/javascripts/external_users.js
+++ b/app/assets/javascripts/external_users.js
@@ -1,4 +1,4 @@
-$(function() {
+$(document).on('turbolinks:load', function() {
   var grid = $('#tag-grid');
 
   if ($.isController('external_users') && grid.isPresent()) {
diff --git a/app/assets/javascripts/forms.js b/app/assets/javascripts/forms.js
index c2930ec4..b74840bb 100644
--- a/app/assets/javascripts/forms.js
+++ b/app/assets/javascripts/forms.js
@@ -1,4 +1,4 @@
-$(function() {
+$(document).on('turbolinks:load', function() {
   var CHOSEN_OPTIONS = {
     allow_single_deselect: true,
     disable_search_threshold: 5,
@@ -14,11 +14,11 @@ $(function() {
       var alternative_input = parent.find('.alternative-input');
 
       if (alternative_input.attr('disabled')) {
-        $(this).text($(this).data('text-toggled'));
+        $(this).text($(event.target).data('text-toggled'));
         original_input.attr('disabled', true).hide();
         alternative_input.attr('disabled', false).show();
       } else {
-        $(this).text($(this).data('text-initial'));
+        $(this).text($(event.target).data('text-initial'));
         alternative_input.attr('disabled', true).hide();
         original_input.attr('disabled', false).show();
       }
@@ -26,5 +26,27 @@ $(function() {
   });
 
   window.CodeOcean.CHOSEN_OPTIONS = CHOSEN_OPTIONS;
-  $('select:visible').chosen(CHOSEN_OPTIONS);
+  chosen_inputs = $('select').filter(function(){
+    return !$(this).parents('ul').is('#dummies');
+  });
+
+  // enable chosen hook when editing an exercise to update ace code highlighting
+  if ($.isController('exercises') && $('.edit_exercise, .new_exercise').isPresent()) {
+      chosen_inputs.filter(function(){
+          return $(this).attr('id').includes('file_type_id');
+      }).on('change chosen:ready', function(event, parameter) {
+          // Set ACE editor mode (for code highlighting) on change of file type and after initialization
+          editorInstance = $(event.target).closest('.card-body').find('.editor')[0];
+          selectedFileType = event.target.value;
+          CodeOceanEditor.updateEditorModeToFileTypeID(editorInstance, selectedFileType);
+      })
+  }
+
+  chosen_inputs.chosen(CHOSEN_OPTIONS);
+});
+
+// Remove some elements before going back to an older site. Otherwise, they might not work.
+$(document).on('turbolinks:before-cache', function() {
+    $('.chosen-container').remove();
+    $('#wmd-button-row-description').remove();
 });
diff --git a/app/assets/javascripts/markdown_ace_editor.js b/app/assets/javascripts/markdown_ace_editor.js
index 42e566fe..bd9845f3 100644
--- a/app/assets/javascripts/markdown_ace_editor.js
+++ b/app/assets/javascripts/markdown_ace_editor.js
@@ -9,7 +9,7 @@
         });
         editor.setShowPrintMargin(false);
         var session = editor.getSession();
-        session.setMode('markdown');
+        session.setMode('ace/mode/markdown');
         session.setUseWrapMode(true);
         session.setValue($(selector).val());
     };
diff --git a/app/assets/javascripts/pagedown.js b/app/assets/javascripts/pagedown.js
deleted file mode 100644
index b48c2ae6..00000000
--- a/app/assets/javascripts/pagedown.js
+++ /dev/null
@@ -1,10 +0,0 @@
-(function() {
-    var ACE_FILES_PATH = '/assets/ace/';
-
-    window.PagedownEditor = function(selector) {
-        var converter = Markdown.getSanitizingConverter();
-        var editor    = new Markdown.Editor( converter );
-
-        editor.run();
-    };
-})();
\ No newline at end of file
diff --git a/app/assets/javascripts/pagedown/markdown.editor.js.erb b/app/assets/javascripts/pagedown/markdown.editor.js.erb
new file mode 100644
index 00000000..4f5ecb6d
--- /dev/null
+++ b/app/assets/javascripts/pagedown/markdown.editor.js.erb
@@ -0,0 +1,2131 @@
+// needs Markdown.Converter.js at the moment
+
+(function () {
+
+    var util = {},
+        position = {},
+        ui = {},
+        doc = window.document,
+        re = window.RegExp,
+        nav = window.navigator,
+        SETTINGS = { lineLength: 72 },
+
+        // Used to work around some browser bugs where we can't use feature testing.
+        uaSniffed = {
+            isIE: /msie/.test(nav.userAgent.toLowerCase()),
+            isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()),
+            isOpera: /opera/.test(nav.userAgent.toLowerCase())
+        };
+
+
+    // -------------------------------------------------------------------
+    //  YOUR CHANGES GO HERE
+    //
+    // I've tried to localize the things you are likely to change to
+    // this area.
+    // -------------------------------------------------------------------
+
+    // The text that appears on the upper part of the dialog box when
+    // entering links.
+    var linkDialogTitle = "<%= I18n.t('components.markdown_editor.insert_link.dialog_title', default: 'Insert link') %>";
+    var linkInputLabel = "<%= I18n.t('components.markdown_editor.insert_link.input_label', default: 'Link URL') %>";
+    var linkInputPlaceholder = "http://example.com/ \"optional title\"";
+    var linkInputHelp = "<%= I18n.t('components.markdown_editor.insert_link.input_help', default: 'Enter URL to point link to and optional title to display when mouse is placed over the link') %>";
+
+    var imageDialogTitle = "<%= I18n.t('components.markdown_editor.insert_image.dialog_title', default: 'Insert image') %>";
+    var imageInputLabel = "<%= I18n.t('components.markdown_editor.insert_image.input_label', default: 'Image URL') %>";
+    var imageInputPlaceholder = "http://example.com/images/diagram.jpg \"optional title\"";
+    var imageInputHelp = "<%= I18n.t('components.markdown_editor.insert_link.input_help', default: 'Enter URL where image is located and optional title to display when mouse is placed over the image') %>";
+
+    var defaultHelpHoverTitle = "Markdown Editing Help";
+
+    // -------------------------------------------------------------------
+    //  END OF YOUR CHANGES
+    // -------------------------------------------------------------------
+
+    // help, if given, should have a property "handler", the click handler for the help button,
+    // and can have an optional property "title" for the button's tooltip (defaults to "Markdown Editing Help").
+    // If help isn't given, not help button is created.
+    //
+    // The constructed editor object has the methods:
+    // - getConverter() returns the markdown converter object that was passed to the constructor
+    // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op.
+    // - refreshPreview() forces the preview to be updated. This method is only available after run() was called.
+    Markdown.Editor = function (markdownConverter, idPostfix, help) {
+
+        idPostfix = idPostfix || "";
+
+        var hooks = this.hooks = new Markdown.HookCollection();
+        hooks.addNoop("onPreviewRefresh");       // called with no arguments after the preview has been refreshed
+        hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text
+        hooks.addFalse("insertImageDialog");
+        /* called with one parameter: a callback to be called with the URL of the image. If the application creates
+         * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen
+         * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
+         */
+
+        this.getConverter = function () {
+            return markdownConverter;
+        }
+
+        var that = this,
+            cards;
+
+        this.run = function () {
+            if (cards)
+                return; // already initialized
+
+            cards = new CardCollection(idPostfix);
+            var commandManager = new CommandManager(hooks);
+            var previewManager = new PreviewManager(markdownConverter, cards, function () {
+                hooks.onPreviewRefresh();
+            });
+            var undoManager, uiManager;
+
+            if (!/\?noundo/.test(doc.location.href)) {
+                undoManager = new UndoManager(function () {
+                    previewManager.refresh();
+                    if (uiManager) // not available on the first call
+                        uiManager.setUndoRedoButtonStates();
+                }, cards);
+                this.textOperation = function (f) {
+                    undoManager.setCommandMode();
+                    f();
+                    that.refreshPreview();
+                }
+            }
+
+            uiManager = new UIManager(idPostfix, cards, undoManager, previewManager, commandManager, help);
+            uiManager.setUndoRedoButtonStates();
+
+            var forceRefresh = that.refreshPreview = function () {
+                previewManager.refresh(true);
+            };
+
+            forceRefresh();
+        };
+
+    };
+
+    // before: contains all the text in the input box BEFORE the selection.
+    // after: contains all the text in the input box AFTER the selection.
+    function Chunks() { }
+
+    // startRegex: a regular expression to find the start tag
+    // endRegex: a regular expresssion to find the end tag
+    Chunks.prototype.findTags = function (startRegex, endRegex) {
+
+        var chunkObj = this;
+        var regex;
+
+        if (startRegex) {
+
+            regex = util.extendRegExp(startRegex, "", "$");
+
+            this.before = this.before.replace(regex,
+                function (match) {
+                    chunkObj.startTag = chunkObj.startTag + match;
+                    return "";
+                });
+
+            regex = util.extendRegExp(startRegex, "^", "");
+
+            this.selection = this.selection.replace(regex,
+                function (match) {
+                    chunkObj.startTag = chunkObj.startTag + match;
+                    return "";
+                });
+        }
+
+        if (endRegex) {
+
+            regex = util.extendRegExp(endRegex, "", "$");
+
+            this.selection = this.selection.replace(regex,
+                function (match) {
+                    chunkObj.endTag = match + chunkObj.endTag;
+                    return "";
+                });
+
+            regex = util.extendRegExp(endRegex, "^", "");
+
+            this.after = this.after.replace(regex,
+                function (match) {
+                    chunkObj.endTag = match + chunkObj.endTag;
+                    return "";
+                });
+        }
+    };
+
+    // If remove is false, the whitespace is transferred
+    // to the before/after regions.
+    //
+    // If remove is true, the whitespace disappears.
+    Chunks.prototype.trimWhitespace = function (remove) {
+        var beforeReplacer, afterReplacer, that = this;
+        if (remove) {
+            beforeReplacer = afterReplacer = "";
+        } else {
+            beforeReplacer = function (s) {
+                that.before += s;
+                return "";
+            };
+            afterReplacer = function (s) { that.after = s + that.after; return ""; }
+        }
+
+        this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer);
+    };
+
+
+    Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) {
+
+        if (nLinesBefore === undefined) {
+            nLinesBefore = 1;
+        }
+
+        if (nLinesAfter === undefined) {
+            nLinesAfter = 1;
+        }
+
+        nLinesBefore++;
+        nLinesAfter++;
+
+        var regexText;
+        var replacementText;
+
+        // chrome bug ... documented at: http://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985
+        if (navigator.userAgent.match(/Chrome/)) {
+            "X".match(/()./);
+        }
+
+        this.selection = this.selection.replace(/(^\n*)/, "");
+
+        this.startTag = this.startTag + re.$1;
+
+        this.selection = this.selection.replace(/(\n*$)/, "");
+        this.endTag = this.endTag + re.$1;
+        this.startTag = this.startTag.replace(/(^\n*)/, "");
+        this.before = this.before + re.$1;
+        this.endTag = this.endTag.replace(/(\n*$)/, "");
+        this.after = this.after + re.$1;
+
+        if (this.before) {
+
+            regexText = replacementText = "";
+
+            while (nLinesBefore--) {
+                regexText += "\\n?";
+                replacementText += "\n";
+            }
+
+            if (findExtraNewlines) {
+                regexText = "\\n*";
+            }
+            this.before = this.before.replace(new re(regexText + "$", ""), replacementText);
+        }
+
+        if (this.after) {
+
+            regexText = replacementText = "";
+
+            while (nLinesAfter--) {
+                regexText += "\\n?";
+                replacementText += "\n";
+            }
+            if (findExtraNewlines) {
+                regexText = "\\n*";
+            }
+
+            this.after = this.after.replace(new re(regexText, ""), replacementText);
+        }
+    };
+
+    // end of Chunks
+
+    // A collection of the important regions on the page.
+    // Cached so we don't have to keep traversing the DOM.
+    // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around
+    // this issue:
+    // Internet explorer has problems with CSS sprite buttons that use HTML
+    // lists.  When you click on the background image "button", IE will
+    // select the non-existent link text and discard the selection in the
+    // textarea.  The solution to this is to cache the textarea selection
+    // on the button's mousedown event and set a flag.  In the part of the
+    // code where we need to grab the selection, we check for the flag
+    // and, if it's set, use the cached area instead of querying the
+    // textarea.
+    //
+    // This ONLY affects Internet Explorer (tested on versions 6, 7
+    // and 8) and ONLY on button clicks.  Keyboard shortcuts work
+    // normally since the focus never leaves the textarea.
+    function CardCollection(postfix) {
+        this.buttonBar = doc.getElementById("wmd-button-bar" + postfix);
+        this.preview = doc.getElementById("wmd-preview" + postfix);
+        this.input = doc.getElementById("wmd-input" + postfix);
+    }
+    // Returns true if the DOM element is visible, false if it's hidden.
+    // Checks if display is anything other than none.
+    util.isVisible = function (elem) {
+
+        if (window.getComputedStyle) {
+            // Most browsers
+            return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none";
+        }
+        else if (elem.currentStyle) {
+            // IE
+            return elem.currentStyle["display"] !== "none";
+        }
+    };
+
+
+    // Adds a listener callback to a DOM element which is fired on a specified
+    // event.
+    util.addEvent = function (elem, event, listener) {
+        if (elem.attachEvent) {
+            // IE only.  The "on" is mandatory.
+            elem.attachEvent("on" + event, listener);
+        }
+        else {
+            // Other browsers.
+            elem.addEventListener(event, listener, false);
+        }
+    };
+
+
+    // Removes a listener callback from a DOM element which is fired on a specified
+    // event.
+    util.removeEvent = function (elem, event, listener) {
+        if (elem.detachEvent) {
+            // IE only.  The "on" is mandatory.
+            elem.detachEvent("on" + event, listener);
+        }
+        else {
+            // Other browsers.
+            elem.removeEventListener(event, listener, false);
+        }
+    };
+
+    // Converts \r\n and \r to \n.
+    util.fixEolChars = function (text) {
+        text = text.replace(/\r\n/g, "\n");
+        text = text.replace(/\r/g, "\n");
+        return text;
+    };
+
+    // Extends a regular expression.  Returns a new RegExp
+    // using pre + regex + post as the expression.
+    // Used in a few functions where we have a base
+    // expression and we want to pre- or append some
+    // conditions to it (e.g. adding "$" to the end).
+    // The flags are unchanged.
+    //
+    // regex is a RegExp, pre and post are strings.
+    util.extendRegExp = function (regex, pre, post) {
+
+        if (pre === null || pre === undefined) {
+            pre = "";
+        }
+        if (post === null || post === undefined) {
+            post = "";
+        }
+
+        var pattern = regex.toString();
+        var flags;
+
+        // Replace the flags with empty space and store them.
+        pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) {
+            flags = flagsPart;
+            return "";
+        });
+
+        // Remove the slash delimiters on the regular expression.
+        pattern = pattern.replace(/(^\/|\/$)/g, "");
+        pattern = pre + pattern + post;
+
+        return new re(pattern, flags);
+    };
+
+    // UNFINISHED
+    // The assignment in the while loop makes jslint cranky.
+    // I'll change it to a better loop later.
+    position.getTop = function (elem, isInner) {
+        var result = elem.offsetTop;
+        if (!isInner) {
+            while (elem = elem.offsetParent) {
+                result += elem.offsetTop;
+            }
+        }
+        return result;
+    };
+
+    position.getHeight = function (elem) {
+        return elem.offsetHeight || elem.scrollHeight;
+    };
+
+    position.getWidth = function (elem) {
+        return elem.offsetWidth || elem.scrollWidth;
+    };
+
+    position.getPageSize = function () {
+
+        var scrollWidth, scrollHeight;
+        var innerWidth, innerHeight;
+
+        // It's not very clear which blocks work with which browsers.
+        if (self.innerHeight && self.scrollMaxY) {
+            scrollWidth = doc.body.scrollWidth;
+            scrollHeight = self.innerHeight + self.scrollMaxY;
+        }
+        else if (doc.body.scrollHeight > doc.body.offsetHeight) {
+            scrollWidth = doc.body.scrollWidth;
+            scrollHeight = doc.body.scrollHeight;
+        }
+        else {
+            scrollWidth = doc.body.offsetWidth;
+            scrollHeight = doc.body.offsetHeight;
+        }
+
+        if (self.innerHeight) {
+            // Non-IE browser
+            innerWidth = self.innerWidth;
+            innerHeight = self.innerHeight;
+        }
+        else if (doc.documentElement && doc.documentElement.clientHeight) {
+            // Some versions of IE (IE 6 w/ a DOCTYPE declaration)
+            innerWidth = doc.documentElement.clientWidth;
+            innerHeight = doc.documentElement.clientHeight;
+        }
+        else if (doc.body) {
+            // Other versions of IE
+            innerWidth = doc.body.clientWidth;
+            innerHeight = doc.body.clientHeight;
+        }
+
+        var maxWidth = Math.max(scrollWidth, innerWidth);
+        var maxHeight = Math.max(scrollHeight, innerHeight);
+        return [maxWidth, maxHeight, innerWidth, innerHeight];
+    };
+
+    // Handles pushing and popping TextareaStates for undo/redo commands.
+    // I should rename the stack variables to list.
+    function UndoManager(callback, cards) {
+
+        var undoObj = this;
+        var undoStack = []; // A stack of undo states
+        var stackPtr = 0; // The index of the current state
+        var mode = "none";
+        var lastState; // The last state
+        var timer; // The setTimeout handle for cancelling the timer
+        var inputStateObj;
+
+        // Set the mode for later logic steps.
+        var setMode = function (newMode, noSave) {
+            if (mode != newMode) {
+                mode = newMode;
+                if (!noSave) {
+                    saveState();
+                }
+            }
+
+            if (!uaSniffed.isIE || mode != "moving") {
+                timer = setTimeout(refreshState, 1);
+            }
+            else {
+                inputStateObj = null;
+            }
+        };
+
+        var refreshState = function (isInitialState) {
+            inputStateObj = new TextareaState(cards, isInitialState);
+            timer = undefined;
+        };
+
+        this.setCommandMode = function () {
+            mode = "command";
+            saveState();
+            timer = setTimeout(refreshState, 0);
+        };
+
+        this.canUndo = function () {
+            return stackPtr > 1;
+        };
+
+        this.canRedo = function () {
+            if (undoStack[stackPtr + 1]) {
+                return true;
+            }
+            return false;
+        };
+
+        // Removes the last state and restores it.
+        this.undo = function () {
+
+            if (undoObj.canUndo()) {
+                if (lastState) {
+                    // What about setting state -1 to null or checking for undefined?
+                    lastState.restore();
+                    lastState = null;
+                }
+                else {
+                    undoStack[stackPtr] = new TextareaState(cards);
+                    undoStack[--stackPtr].restore();
+
+                    if (callback) {
+                        callback();
+                    }
+                }
+            }
+
+            mode = "none";
+            cards.input.focus();
+            refreshState();
+        };
+
+        // Redo an action.
+        this.redo = function () {
+
+            if (undoObj.canRedo()) {
+
+                undoStack[++stackPtr].restore();
+
+                if (callback) {
+                    callback();
+                }
+            }
+
+            mode = "none";
+            cards.input.focus();
+            refreshState();
+        };
+
+        // Push the input area state to the stack.
+        var saveState = function () {
+            var currState = inputStateObj || new TextareaState(cards);
+
+            if (!currState) {
+                return false;
+            }
+            if (mode == "moving") {
+                if (!lastState) {
+                    lastState = currState;
+                }
+                return;
+            }
+            if (lastState) {
+                if (undoStack[stackPtr - 1].text != lastState.text) {
+                    undoStack[stackPtr++] = lastState;
+                }
+                lastState = null;
+            }
+            undoStack[stackPtr++] = currState;
+            undoStack[stackPtr + 1] = null;
+            if (callback) {
+                callback();
+            }
+        };
+
+        var handleCtrlYZ = function (event) {
+
+            var handled = false;
+
+            if (event.ctrlKey || event.metaKey) {
+
+                // IE and Opera do not support charCode.
+                var keyCode = event.charCode || event.keyCode;
+                var keyCodeChar = String.fromCharCode(keyCode);
+
+                switch (keyCodeChar) {
+
+                    case "y":
+                        undoObj.redo();
+                        handled = true;
+                        break;
+
+                    case "z":
+                        if (!event.shiftKey) {
+                            undoObj.undo();
+                        }
+                        else {
+                            undoObj.redo();
+                        }
+                        handled = true;
+                        break;
+                }
+            }
+
+            if (handled) {
+                if (event.preventDefault) {
+                    event.preventDefault();
+                }
+                if (window.event) {
+                    window.event.returnValue = false;
+                }
+            }
+        };
+
+        // Set the mode depending on what is going on in the input area.
+        var handleModeChange = function (event) {
+
+            if (!event.ctrlKey && !event.metaKey) {
+
+                var keyCode = event.keyCode;
+
+                if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) {
+                    // 33 - 40: page up/dn and arrow keys
+                    // 63232 - 63235: page up/dn and arrow keys on safari
+                    setMode("moving");
+                }
+                else if (keyCode == 8 || keyCode == 46 || keyCode == 127) {
+                    // 8: backspace
+                    // 46: delete
+                    // 127: delete
+                    setMode("deleting");
+                }
+                else if (keyCode == 13) {
+                    // 13: Enter
+                    setMode("newlines");
+                }
+                else if (keyCode == 27) {
+                    // 27: escape
+                    setMode("escape");
+                }
+                else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {
+                    // 16-20 are shift, etc.
+                    // 91: left window key
+                    // I think this might be a little messed up since there are
+                    // a lot of nonprinting keys above 20.
+                    setMode("typing");
+                }
+            }
+        };
+
+        var setEventHandlers = function () {
+            util.addEvent(cards.input, "keypress", function (event) {
+                // keyCode 89: y
+                // keyCode 90: z
+                if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) {
+                    event.preventDefault();
+                }
+            });
+
+            var handlePaste = function () {
+                if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != cards.input.value)) {
+                    if (timer == undefined) {
+                        mode = "paste";
+                        saveState();
+                        refreshState();
+                    }
+                }
+            };
+
+            util.addEvent(cards.input, "keydown", handleCtrlYZ);
+            util.addEvent(cards.input, "keydown", handleModeChange);
+            util.addEvent(cards.input, "mousedown", function () {
+                setMode("moving");
+            });
+
+            cards.input.onpaste = handlePaste;
+            cards.input.ondrop = handlePaste;
+        };
+
+        var init = function () {
+            setEventHandlers();
+            refreshState(true);
+            saveState();
+        };
+
+        init();
+    }
+
+    // end of UndoManager
+
+    // The input textarea state/contents.
+    // This is used to implement undo/redo by the undo manager.
+    function TextareaState(cards, isInitialState) {
+
+        // Aliases
+        var stateObj = this;
+        var inputArea = cards.input;
+        this.init = function () {
+            if (!util.isVisible(inputArea)) {
+                return;
+            }
+            if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box
+                return;
+            }
+
+            this.setInputAreaSelectionStartEnd();
+            this.scrollTop = inputArea.scrollTop;
+            if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {
+                this.text = inputArea.value;
+            }
+
+        };
+
+        // Sets the selected text in the input box after we've performed an
+        // operation.
+        this.setInputAreaSelection = function () {
+
+            if (!util.isVisible(inputArea)) {
+                return;
+            }
+
+            if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) {
+
+                inputArea.focus();
+                inputArea.selectionStart = stateObj.start;
+                inputArea.selectionEnd = stateObj.end;
+                inputArea.scrollTop = stateObj.scrollTop;
+            }
+            else if (doc.selection) {
+
+                if (doc.activeElement && doc.activeElement !== inputArea) {
+                    return;
+                }
+
+                inputArea.focus();
+                var range = inputArea.createTextRange();
+                range.moveStart("character", -inputArea.value.length);
+                range.moveEnd("character", -inputArea.value.length);
+                range.moveEnd("character", stateObj.end);
+                range.moveStart("character", stateObj.start);
+                range.select();
+            }
+        };
+
+        this.setInputAreaSelectionStartEnd = function () {
+
+            if (!cards.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) {
+
+                stateObj.start = inputArea.selectionStart;
+                stateObj.end = inputArea.selectionEnd;
+            }
+            else if (doc.selection) {
+
+                stateObj.text = util.fixEolChars(inputArea.value);
+
+                // IE loses the selection in the textarea when buttons are
+                // clicked.  On IE we cache the selection. Here, if something is cached,
+                // we take it.
+                var range = cards.ieCachedRange || doc.selection.createRange();
+
+                var fixedRange = util.fixEolChars(range.text);
+                var marker = "\x07";
+                var markedRange = marker + fixedRange + marker;
+                range.text = markedRange;
+                var inputText = util.fixEolChars(inputArea.value);
+
+                range.moveStart("character", -markedRange.length);
+                range.text = fixedRange;
+
+                stateObj.start = inputText.indexOf(marker);
+                stateObj.end = inputText.lastIndexOf(marker) - marker.length;
+
+                var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;
+
+                if (len) {
+                    range.moveStart("character", -fixedRange.length);
+                    while (len--) {
+                        fixedRange += "\n";
+                        stateObj.end += 1;
+                    }
+                    range.text = fixedRange;
+                }
+
+                if (cards.ieCachedRange)
+                    stateObj.scrollTop = cards.ieCachedScrollTop; // this is set alongside with ieCachedRange
+
+                cards.ieCachedRange = null;
+
+                this.setInputAreaSelection();
+            }
+        };
+
+        // Restore this state into the input area.
+        this.restore = function () {
+
+            if (stateObj.text != undefined && stateObj.text != inputArea.value) {
+                inputArea.value = stateObj.text;
+            }
+            this.setInputAreaSelection();
+            inputArea.scrollTop = stateObj.scrollTop;
+        };
+
+        // Gets a collection of HTML chunks from the inptut textarea.
+        this.getChunks = function () {
+
+            var chunk = new Chunks();
+            chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));
+            chunk.startTag = "";
+            chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));
+            chunk.endTag = "";
+            chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));
+            chunk.scrollTop = stateObj.scrollTop;
+
+            return chunk;
+        };
+
+        // Sets the TextareaState properties given a chunk of markdown.
+        this.setChunks = function (chunk) {
+
+            chunk.before = chunk.before + chunk.startTag;
+            chunk.after = chunk.endTag + chunk.after;
+
+            this.start = chunk.before.length;
+            this.end = chunk.before.length + chunk.selection.length;
+            this.text = chunk.before + chunk.selection + chunk.after;
+            this.scrollTop = chunk.scrollTop;
+        };
+        this.init();
+    }
+    function PreviewManager(converter, cards, previewRefreshCallback) {
+
+        var managerObj = this;
+        var timeout;
+        var elapsedTime;
+        var oldInputText;
+        var maxDelay = 3000;
+        var startType = "delayed"; // The other legal value is "manual"
+
+        // Adds event listeners to elements
+        var setupEvents = function (inputElem, listener) {
+
+            util.addEvent(inputElem, "input", listener);
+            inputElem.onpaste = listener;
+            inputElem.ondrop = listener;
+
+            util.addEvent(inputElem, "keypress", listener);
+            util.addEvent(inputElem, "keydown", listener);
+        };
+
+        var getDocScrollTop = function () {
+
+            var result = 0;
+
+            if (window.innerHeight) {
+                result = window.pageYOffset;
+            }
+            else
+            if (doc.documentElement && doc.documentElement.scrollTop) {
+                result = doc.documentElement.scrollTop;
+            }
+            else
+            if (doc.body) {
+                result = doc.body.scrollTop;
+            }
+
+            return result;
+        };
+
+        var makePreviewHtml = function () {
+
+            // If there is no registered preview card
+            // there is nothing to do.
+            if (!cards.preview)
+                return;
+
+
+            var text = cards.input.value;
+            if (text && text == oldInputText) {
+                return; // Input text hasn't changed.
+            }
+            else {
+                oldInputText = text;
+            }
+
+            var prevTime = new Date().getTime();
+
+            text = converter.makeHtml(text);
+
+            // Calculate the processing time of the HTML creation.
+            // It's used as the delay time in the event listener.
+            var currTime = new Date().getTime();
+            elapsedTime = currTime - prevTime;
+
+            pushPreviewHtml(text);
+        };
+
+        // setTimeout is already used.  Used as an event listener.
+        var applyTimeout = function () {
+
+            if (timeout) {
+                clearTimeout(timeout);
+                timeout = undefined;
+            }
+
+            if (startType !== "manual") {
+
+                var delay = 0;
+
+                if (startType === "delayed") {
+                    delay = elapsedTime;
+                }
+
+                if (delay > maxDelay) {
+                    delay = maxDelay;
+                }
+                timeout = setTimeout(makePreviewHtml, delay);
+            }
+        };
+
+        var getScaleFactor = function (card) {
+            if (card.scrollHeight <= card.clientHeight) {
+                return 1;
+            }
+            return card.scrollTop / (card.scrollHeight - card.clientHeight);
+        };
+
+        var setCardScrollTops = function () {
+            if (cards.preview) {
+                cards.preview.scrollTop = (cards.preview.scrollHeight - cards.preview.clientHeight) * getScaleFactor(cards.preview);
+            }
+        };
+
+        this.refresh = function (requiresRefresh) {
+
+            if (requiresRefresh) {
+                oldInputText = "";
+                makePreviewHtml();
+            }
+            else {
+                applyTimeout();
+            }
+        };
+
+        this.processingTime = function () {
+            return elapsedTime;
+        };
+
+        var isFirstTimeFilled = true;
+
+        // IE doesn't let you use innerHTML if the element is contained somewhere in a table
+        // (which is the case for inline editing) -- in that case, detach the element, set the
+        // value, and reattach. Yes, that *is* ridiculous.
+        var ieSafePreviewSet = function (text) {
+            var preview = cards.preview;
+            var parent = preview.parentNode;
+            var sibling = preview.nextSibling;
+            parent.removeChild(preview);
+            preview.innerHTML = text;
+            if (!sibling)
+                parent.appendChild(preview);
+            else
+                parent.insertBefore(preview, sibling);
+        };
+
+        var nonSuckyBrowserPreviewSet = function (text) {
+            cards.preview.innerHTML = text;
+        };
+
+        var previewSetter;
+
+        var previewSet = function (text) {
+            if (previewSetter)
+                return previewSetter(text);
+
+            try {
+                nonSuckyBrowserPreviewSet(text);
+                previewSetter = nonSuckyBrowserPreviewSet;
+            } catch (e) {
+                previewSetter = ieSafePreviewSet;
+                previewSetter(text);
+            }
+        };
+
+        var pushPreviewHtml = function (text) {
+
+            var emptyTop = position.getTop(cards.input) - getDocScrollTop();
+
+            if (cards.preview) {
+                previewSet(text);
+                previewRefreshCallback();
+            }
+
+            setCardScrollTops();
+
+            if (isFirstTimeFilled) {
+                isFirstTimeFilled = false;
+                return;
+            }
+
+            var fullTop = position.getTop(cards.input) - getDocScrollTop();
+
+            if (uaSniffed.isIE) {
+                setTimeout(function () {
+                    window.scrollBy(0, fullTop - emptyTop);
+                }, 0);
+            }
+            else {
+                window.scrollBy(0, fullTop - emptyTop);
+            }
+        };
+
+        var init = function () {
+
+            setupEvents(cards.input, applyTimeout);
+            makePreviewHtml();
+
+            if (cards.preview) {
+                cards.preview.scrollTop = 0;
+            }
+        };
+
+        init();
+    }
+    // This simulates a modal dialog box and asks for the URL when you
+    // click the hyperlink or image buttons.
+    //
+    // text: The html for the input box.
+    // defaultInputText: The default value that appears in the input box.
+    // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.
+    //      It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel
+    //      was chosen).
+    ui.prompt = function (title, inputLabel, inputPlaceholder, inputHelp, callback) {
+
+        // These variables need to be declared at this level since they are used
+        // in multiple functions.
+        var dialog;         // The dialog box.
+        var input;         // The text box where you enter the hyperlink.
+
+
+        if (inputPlaceholder === undefined) {
+            inputPlaceholder = "";
+        }
+
+        // Used as a keydown event handler. Esc dismisses the prompt.
+        // Key code 27 is ESC.
+        var checkEscape = function (key) {
+            var code = (key.charCode || key.keyCode);
+            if (code === 27) {
+                close(true);
+            }
+        };
+
+        // Dismisses the hyperlink input box.
+        // isCancel is true if we don't care about the input text.
+        // isCancel is false if we are going to keep the text.
+        var close = function (isCancel) {
+            util.removeEvent(doc.body, "keydown", checkEscape);
+            var text = input.value;
+
+            if (isCancel) {
+                text = null;
+            }
+            else {
+                // Fixes common pasting errors.
+                text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://');
+                if (!/^(?:https?|ftp):\/\//.test(text))
+                    text = 'http://' + text;
+            }
+
+            $(dialog).modal('hide');
+
+            callback(text);
+            return false;
+        };
+
+
+
+        // Create the text input box form/window.
+        var createDialog = function () {
+            // 
+
+            // The main dialog box.
+            dialog = doc.createElement("div");
+            dialog.className = "modal fade";
+            dialog.style.display = "none";
+
+            var dialogContainer = doc.createElement("div");
+            dialogContainer.className = "modal-dialog";
+            dialog.appendChild(dialogContainer);
+
+            var dialogContent = doc.createElement("div");
+            dialogContent.className = "modal-content";
+            dialogContainer.appendChild(dialogContent);
+
+            // The header.
+            var header = doc.createElement("div");
+            header.className = "modal-header";
+            header.innerHTML = ' ';
+            dialogContent.appendChild(header);
+
+            // The body.
+            var body = doc.createElement("div");
+            body.className = "modal-body";
+            dialogContent.appendChild(body);
+
+            // The footer.
+            var footer = doc.createElement("div");
+            footer.className = "modal-footer";
+            dialogContent.appendChild(footer);
+
+            // The web form container for the text box and buttons.
+            var form = doc.createElement("form");
+            form.onsubmit = function () { return close(false); };
+            body.appendChild(form);
+
+            // The input text box
+            var formGroup = doc.createElement("div");
+            formGroup.className = "form-group";
+            form.appendChild(formGroup);
+
+            var label = doc.createElement("label");
+            label.htmlFor = "url-" + new Date().getTime();
+            label.innerHTML = inputLabel;
+            formGroup.appendChild(label);
+
+            input = doc.createElement("input");
+            input.id = label.htmlFor;
+            input.type = "text";
+            input.className = "form-control";
+            input.placeholder = inputPlaceholder;
+            formGroup.appendChild(input);
+
+            var helpBlock = doc.createElement("span");
+            helpBlock.className = "help-block form-text";
+            helpBlock.innerHTML = inputHelp || '';
+            formGroup.appendChild(helpBlock);
+
+            // The ok button
+            var okButton = doc.createElement("button");
+            okButton.className = "btn btn-primary";
+            okButton.type = "button";
+            okButton.onclick = function () { return close(false); };
+            okButton.innerHTML = "OK";
+
+            // The cancel button
+            var cancelButton = doc.createElement("button");
+            cancelButton.className = "btn btn-secondary";
+            cancelButton.type = "button";
+            cancelButton.onclick = function () { return close(true); };
+            cancelButton.innerHTML = "Cancel";
+
+            footer.appendChild(okButton);
+            footer.appendChild(cancelButton);
+
+            util.addEvent(doc.body, "keydown", checkEscape);
+
+            doc.body.appendChild(dialog);
+
+        };
+
+        // Why is this in a zero-length timeout?
+        // Is it working around a browser bug?
+        setTimeout(function () {
+
+            createDialog();
+
+            var defTextLen = 0;
+            if (input.selectionStart !== undefined) {
+                input.selectionStart = 0;
+                input.selectionEnd = defTextLen;
+            }
+            else if (input.createTextRange) {
+                var range = input.createTextRange();
+                range.collapse(false);
+                range.moveStart("character", -defTextLen);
+                range.moveEnd("character", defTextLen);
+                range.select();
+            }
+
+            $(dialog).on('shown', function () {
+                input.focus();
+            });
+
+            $(dialog).on('hidden', function () {
+                dialog.parentNode.removeChild(dialog);
+            });
+
+            $(dialog).modal()
+
+        }, 0);
+    };
+
+    function UIManager(postfix, cards, undoManager, previewManager, commandManager, helpOptions) {
+
+        var inputBox = cards.input,
+            buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.
+
+        makeSpritedButtonRow();
+
+        var keyEvent = "keydown";
+        if (uaSniffed.isOpera) {
+            keyEvent = "keypress";
+        }
+
+        util.addEvent(inputBox, keyEvent, function (key) {
+
+            // Check to see if we have a button key and, if so execute the callback.
+            if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) {
+
+                var keyCode = key.charCode || key.keyCode;
+                var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
+
+                switch (keyCodeStr) {
+                    case "b":
+                        doClick(buttons.bold);
+                        break;
+                    case "i":
+                        doClick(buttons.italic);
+                        break;
+                    case "l":
+                        doClick(buttons.link);
+                        break;
+                    case "q":
+                        doClick(buttons.quote);
+                        break;
+                    case "k":
+                        doClick(buttons.code);
+                        break;
+                    case "g":
+                        doClick(buttons.image);
+                        break;
+                    case "o":
+                        doClick(buttons.olist);
+                        break;
+                    case "u":
+                        doClick(buttons.ulist);
+                        break;
+                    case "h":
+                        doClick(buttons.heading);
+                        break;
+                    case "r":
+                        doClick(buttons.hr);
+                        break;
+                    case "y":
+                        doClick(buttons.redo);
+                        break;
+                    case "z":
+                        if (key.shiftKey) {
+                            doClick(buttons.redo);
+                        }
+                        else {
+                            doClick(buttons.undo);
+                        }
+                        break;
+                    default:
+                        return;
+                }
+
+
+                if (key.preventDefault) {
+                    key.preventDefault();
+                }
+
+                if (window.event) {
+                    window.event.returnValue = false;
+                }
+            }
+        });
+
+        // Auto-indent on shift-enter
+        util.addEvent(inputBox, "keyup", function (key) {
+            if (key.shiftKey && !key.ctrlKey && !key.metaKey) {
+                var keyCode = key.charCode || key.keyCode;
+                // Character 13 is Enter
+                if (keyCode === 13) {
+                    var fakeButton = {};
+                    fakeButton.textOp = bindCommand("doAutoindent");
+                    doClick(fakeButton);
+                }
+            }
+        });
+
+        // special handler because IE clears the context of the textbox on ESC
+        if (uaSniffed.isIE) {
+            util.addEvent(inputBox, "keydown", function (key) {
+                var code = key.keyCode;
+                if (code === 27) {
+                    return false;
+                }
+            });
+        }
+
+
+        // Perform the button's action.
+        function doClick(button) {
+
+            inputBox.focus();
+
+            if (button.textOp) {
+
+                if (undoManager) {
+                    undoManager.setCommandMode();
+                }
+
+                var state = new TextareaState(cards);
+
+                if (!state) {
+                    return;
+                }
+
+                var chunks = state.getChunks();
+
+                // Some commands launch a "modal" prompt dialog.  Javascript
+                // can't really make a modal dialog box and the WMD code
+                // will continue to execute while the dialog is displayed.
+                // This prevents the dialog pattern I'm used to and means
+                // I can't do something like this:
+                //
+                // var link = CreateLinkDialog();
+                // makeMarkdownLink(link);
+                //
+                // Instead of this straightforward method of handling a
+                // dialog I have to pass any code which would execute
+                // after the dialog is dismissed (e.g. link creation)
+                // in a function parameter.
+                //
+                // Yes this is awkward and I think it sucks, but there's
+                // no real workaround.  Only the image and link code
+                // create dialogs and require the function pointers.
+                var fixupInputArea = function () {
+
+                    inputBox.focus();
+
+                    if (chunks) {
+                        state.setChunks(chunks);
+                    }
+
+                    state.restore();
+                    previewManager.refresh();
+                };
+
+                var noCleanup = button.textOp(chunks, fixupInputArea);
+
+                if (!noCleanup) {
+                    fixupInputArea();
+                }
+
+            }
+
+            if (button.execute) {
+                button.execute(undoManager);
+            }
+        }
+        function setupButton(button, isEnabled) {
+
+            if (isEnabled) {
+                button.disabled = false;
+
+                if (!button.isHelp) {
+                    button.onclick = function () {
+                        if (this.onmouseout) {
+                            this.onmouseout();
+                        }
+                        doClick(this);
+                        return false;
+                    }
+                }
+            }
+            else {
+                button.disabled = true;
+            }
+        }
+
+        function bindCommand(method) {
+            if (typeof method === "string")
+                method = commandManager[method];
+            return function () { method.apply(commandManager, arguments); }
+        }
+
+        function makeSpritedButtonRow() {
+
+            var buttonBar = cards.buttonBar;
+            var buttonRow = document.createElement("div");
+            buttonRow.id = "wmd-button-row" + postfix;
+            buttonRow.className = 'btn-toolbar';
+            buttonRow = buttonBar.appendChild(buttonRow);
+
+            var makeButton = function (id, title, iconClass, textOp, group) {
+                var button = document.createElement("button");
+                button.className = "btn btn-secondary btn-sm";
+                var buttonImage = document.createElement("i");
+                buttonImage.className = iconClass;
+                button.appendChild(buttonImage);
+                button.id = id + postfix;
+                button.title = title;
+                button.setAttribute("data-toggle", "tooltip");
+                button.setAttribute("data-placement", "top");
+                if (textOp)
+                    button.textOp = textOp;
+                setupButton(button, true);
+                if (group) {
+                    group.appendChild(button);
+                } else {
+                    buttonRow.appendChild(button);
+                }
+                return button;
+            };
+            var makeGroup = function (num) {
+                var group = document.createElement("div");
+                group.className = "m-1 btn-group wmd-button-group" + num;
+                group.id = "wmd-button-group" + num + postfix;
+                buttonRow.appendChild(group);
+                return group
+            };
+
+            var group1 = makeGroup(1);
+            buttons.bold = makeButton("wmd-bold-button", "<%= I18n.t('components.markdown_editor.bold.button_title', default: 'Bold (Ctrl+B)') %>", "m-1 fa fa-bold", bindCommand("doBold"), group1);
+            buttons.italic = makeButton("wmd-italic-button", "<%= I18n.t('components.markdown_editor.italic.button_title', default: 'Italic (Ctrl+I)') %>", "m-1 fa fa-italic", bindCommand("doItalic"), group1);
+
+            var group2 = makeGroup(2);
+            buttons.link = makeButton("wmd-link-button", "<%= I18n.t('components.markdown_editor.insert_link.button_title', default: 'Link (Ctrl+L)') %>", "m-1 fa fa-link", bindCommand(function (chunk, postProcessing) {
+                return this.doLinkOrImage(chunk, postProcessing, false);
+            }), group2);
+            buttons.image = makeButton("wmd-image-button", "<%= I18n.t('components.markdown_editor.insert_image.button_title', default: 'Image (Ctrl+G)') %>", "m-1 fa fa-picture-o", bindCommand(function (chunk, postProcessing) {
+                return this.doLinkOrImage(chunk, postProcessing, true);
+            }), group2);
+            buttons.quote = makeButton("wmd-quote-button", "<%= I18n.t('components.markdown_editor.blockquoute.button_title', default: 'Blockquote (Ctrl+Q)') %>", "m-1 fa fa-quote-left", bindCommand("doBlockquote"), group2);
+            buttons.code = makeButton("wmd-code-button", "<%= I18n.t('components.markdown_editor.code_sample.button_title', default: 'Code Sample (Ctrl+K)') %>", "m-1 fa fa-code", bindCommand("doCode"), group2);
+
+            var group3 = makeGroup(3);
+            buttons.ulist = makeButton("wmd-ulist-button", "<%= I18n.t('components.markdown_editor.bulleted_list.button_title', default: 'Bulleted List (Ctrl+U)') %>", "m-1 fa fa-list-ul", bindCommand(function (chunk, postProcessing) {
+                this.doList(chunk, postProcessing, false);
+            }), group3);
+            buttons.olist = makeButton("wmd-olist-button", "<%= I18n.t('components.markdown_editor.numbered_list.button_title', default: 'Numbered List (Ctrl+O)') %>", "m-1 fa fa-list-ol", bindCommand(function (chunk, postProcessing) {
+                this.doList(chunk, postProcessing, true);
+            }), group3);
+            buttons.heading = makeButton("wmd-heading-button", "<%= I18n.t('components.markdown_editor.heading.button_title', default: 'Heading (Ctrl+H)') %>", "m-1 fa fa-font", bindCommand("doHeading"), group3);
+
+            var group4 = makeGroup(4);
+            buttons.undo = makeButton("wmd-undo-button", "<%= I18n.t('components.markdown_editor.undo.button_title', default: 'Undo (Ctrl+Z)') %>", "m-1 fa fa-undo", null, group4);
+            buttons.undo.execute = function (manager) { if (manager) manager.undo(); };
+
+            var redoTitle = /win/.test(nav.platform.toLowerCase()) ?
+                "<%= I18n.t('components.markdown_editor.redo.button_title.win', default: 'Redo (Ctrl+Y)') %>" :
+                "<%= I18n.t('components.markdown_editor.redo.button_title.other', default: 'Redo (Ctrl+Shift+Z)') %>"; // mac and other non-Windows platforms
+
+            buttons.redo = makeButton("wmd-redo-button", redoTitle, "m-1 fa fa-repeat", null, group4);
+            buttons.redo.execute = function (manager) { if (manager) manager.redo(); };
+
+            if (helpOptions) {
+                var group5 = makeGroup(5);
+                group5.className = group5.className + " ml-auto";
+                var helpButton = document.createElement("button");
+                var helpButtonImage = document.createElement("i");
+                helpButtonImage.className = "m-1 fa fa-info";
+                helpButton.appendChild(helpButtonImage);
+                helpButton.className = "btn btn-info btn-sm";
+                helpButton.id = "wmd-help-button" + postfix;
+                helpButton.isHelp = true;
+                helpButton.setAttribute("data-toggle", "tooltip");
+                helpButton.setAttribute("data-placement", "top");
+                helpButton.title = helpOptions.title || defaultHelpHoverTitle;
+                helpButton.onclick = helpOptions.handler;
+
+                setupButton(helpButton, true);
+                group5.appendChild(helpButton);
+                buttons.help = helpButton;
+            }
+
+            setUndoRedoButtonStates();
+        }
+
+        function setUndoRedoButtonStates() {
+            if (undoManager) {
+                setupButton(buttons.undo, undoManager.canUndo());
+                setupButton(buttons.redo, undoManager.canRedo());
+            }
+        }
+        this.setUndoRedoButtonStates = setUndoRedoButtonStates;
+
+    }
+
+    function CommandManager(pluginHooks) {
+        this.hooks = pluginHooks;
+    }
+
+    var commandProto = CommandManager.prototype;
+
+    // The markdown symbols - 4 spaces = code, > = blockquote, etc.
+    commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
+
+    // Remove markdown symbols from the chunk selection.
+    commandProto.unwrap = function (chunk) {
+        var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g");
+        chunk.selection = chunk.selection.replace(txt, "$1 $2");
+    };
+
+    commandProto.wrap = function (chunk, len) {
+        this.unwrap(chunk);
+        var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"),
+            that = this;
+
+        chunk.selection = chunk.selection.replace(regex, function (line, marked) {
+            if (new re("^" + that.prefixes, "").test(line)) {
+                return line;
+            }
+            return marked + "\n";
+        });
+
+        chunk.selection = chunk.selection.replace(/\s+$/, "");
+    };
+
+    commandProto.doBold = function (chunk, postProcessing) {
+        return this.doBorI(chunk, postProcessing, 2, "strong text");
+    };
+
+    commandProto.doItalic = function (chunk, postProcessing) {
+        return this.doBorI(chunk, postProcessing, 1, "emphasized text");
+    };
+
+    // chunk: The selected region that will be enclosed with */**
+    // nStars: 1 for italics, 2 for bold
+    // insertText: If you just click the button without highlighting text, this gets inserted
+    commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) {
+
+        // Get rid of whitespace and fixup newlines.
+        chunk.trimWhitespace();
+        chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
+
+        // Look for stars before and after.  Is the chunk already marked up?
+        // note that these regex matches cannot fail
+        var starsBefore = /(\**$)/.exec(chunk.before)[0];
+        var starsAfter = /(^\**)/.exec(chunk.after)[0];
+
+        var prevStars = Math.min(starsBefore.length, starsAfter.length);
+
+        // Remove stars if we have to since the button acts as a toggle.
+        if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
+            chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
+            chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
+        }
+        else if (!chunk.selection && starsAfter) {
+            // It's not really clear why this code is necessary.  It just moves
+            // some arbitrary stuff around.
+            chunk.after = chunk.after.replace(/^([*_]*)/, "");
+            chunk.before = chunk.before.replace(/(\s?)$/, "");
+            var whitespace = re.$1;
+            chunk.before = chunk.before + starsAfter + whitespace;
+        }
+        else {
+
+            // In most cases, if you don't have any selected text and click the button
+            // you'll get a selected, marked up region with the default text inserted.
+            if (!chunk.selection && !starsAfter) {
+                chunk.selection = insertText;
+            }
+
+            // Add the true markup.
+            var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
+            chunk.before = chunk.before + markup;
+            chunk.after = markup + chunk.after;
+        }
+
+
+    };
+
+    commandProto.stripLinkDefs = function (text, defsToAdd) {
+
+        text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,
+            function (totalMatch, id, link, newlines, title) {
+                defsToAdd[id] = totalMatch.replace(/\s*$/, "");
+                if (newlines) {
+                    // Strip the title and return that separately.
+                    defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
+                    return newlines + title;
+                }
+                return "";
+            });
+
+        return text;
+    };
+
+    commandProto.addLinkDef = function (chunk, linkDef) {
+
+        var refNumber = 0; // The current reference number
+        var defsToAdd = {}; //
+        // Start with a clean slate by removing all previous link definitions.
+        chunk.before = this.stripLinkDefs(chunk.before, defsToAdd);
+        chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd);
+        chunk.after = this.stripLinkDefs(chunk.after, defsToAdd);
+
+        var defs = "";
+        var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;
+
+        var addDefNumber = function (def) {
+            refNumber++;
+            def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, "  [" + refNumber + "]:");
+            defs += "\n" + def;
+        };
+
+        // note that
+        // a) the recursive call to getLink cannot go infinite, because by definition
+        //    of regex, inner is always a proper substring of wholeMatch, and
+        // b) more than one level of nesting is neither supported by the regex
+        //    nor making a lot of sense (the only use case for nesting is a linked image)
+        var getLink = function (wholeMatch, before, inner, afterInner, id, end) {
+            inner = inner.replace(regex, getLink);
+            if (defsToAdd[id]) {
+                addDefNumber(defsToAdd[id]);
+                return before + inner + afterInner + refNumber + end;
+            }
+            return wholeMatch;
+        };
+
+        chunk.before = chunk.before.replace(regex, getLink);
+
+        if (linkDef) {
+            addDefNumber(linkDef);
+        }
+        else {
+            chunk.selection = chunk.selection.replace(regex, getLink);
+        }
+
+        var refOut = refNumber;
+
+        chunk.after = chunk.after.replace(regex, getLink);
+
+        if (chunk.after) {
+            chunk.after = chunk.after.replace(/\n*$/, "");
+        }
+        if (!chunk.after) {
+            chunk.selection = chunk.selection.replace(/\n*$/, "");
+        }
+
+        chunk.after += "\n\n" + defs;
+
+        return refOut;
+    };
+
+    // takes the line as entered into the add link/as image dialog and makes
+    // sure the URL and the optinal title are "nice".
+    function properlyEncoded(linkdef) {
+        return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {
+            link = link.replace(/\?.*$/, function (querypart) {
+                return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical
+            });
+            link = decodeURIComponent(link); // unencode first, to prevent double encoding
+            link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');
+            link = link.replace(/\?.*$/, function (querypart) {
+                return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded
+            });
+            if (title) {
+                title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, "");
+                title = $.trim(title).replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">");
+            }
+            return title ? link + ' "' + title + '"' : link;
+        });
+    }
+
+    commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {
+
+        chunk.trimWhitespace();
+        chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
+        var background;
+
+        if (chunk.endTag.length > 1 && chunk.startTag.length > 0) {
+
+            chunk.startTag = chunk.startTag.replace(/!?\[/, "");
+            chunk.endTag = "";
+            this.addLinkDef(chunk, null);
+
+        }
+        else {
+
+            // We're moving start and end tag back into the selection, since (as we're in the else block) we're not
+            // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the
+            // link text. linkEnteredCallback takes care of escaping any brackets.
+            chunk.selection = chunk.startTag + chunk.selection + chunk.endTag;
+            chunk.startTag = chunk.endTag = "";
+
+            if (/\n\n/.test(chunk.selection)) {
+                this.addLinkDef(chunk, null);
+                return;
+            }
+            var that = this;
+            // The function to be executed when you enter a link and press OK or Cancel.
+            // Marks up the link and adds the ref.
+            var linkEnteredCallback = function (link) {
+
+                if (link !== null) {
+                    // (                          $1
+                    //     [^\\]                  anything that's not a backslash
+                    //     (?:\\\\)*              an even number (this includes zero) of backslashes
+                    // )
+                    // (?=                        followed by
+                    //     [[\]]                  an opening or closing bracket
+                    // )
+                    //
+                    // In other words, a non-escaped bracket. These have to be escaped now to make sure they
+                    // don't count as the end of the link or similar.
+                    // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets),
+                    // the bracket in one match may be the "not a backslash" character in the next match, so it
+                    // should not be consumed by the first match.
+                    // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the
+                    // start of the string, so this also works if the selection begins with a bracket. We cannot solve
+                    // this by anchoring with ^, because in the case that the selection starts with two brackets, this
+                    // would mean a zero-width match at the start. Since zero-width matches advance the string position,
+                    // the first bracket could then not act as the "not a backslash" for the second.
+                    chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1);
+
+                    var linkDef = " [999]: " + properlyEncoded(link);
+
+                    var num = that.addLinkDef(chunk, linkDef);
+                    chunk.startTag = isImage ? "![" : "[";
+                    chunk.endTag = "][" + num + "]";
+
+                    if (!chunk.selection) {
+                        if (isImage) {
+                            chunk.selection = "enter image description here";
+                        }
+                        else {
+                            chunk.selection = "enter link description here";
+                        }
+                    }
+                }
+                postProcessing();
+            };
+
+
+            if (isImage) {
+                if (!this.hooks.insertImageDialog(linkEnteredCallback))
+                    ui.prompt(imageDialogTitle, imageInputLabel, imageInputPlaceholder, imageInputHelp, linkEnteredCallback);
+            }
+            else {
+                ui.prompt(linkDialogTitle, linkInputLabel, linkInputPlaceholder, linkInputHelp, linkEnteredCallback);
+            }
+            return true;
+        }
+    };
+
+    // When making a list, hitting shift-enter will put your cursor on the next line
+    // at the current indent level.
+    commandProto.doAutoindent = function (chunk, postProcessing) {
+
+        var commandMgr = this,
+            fakeSelection = false;
+
+        chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
+        chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
+        chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
+
+        // There's no selection, end the cursor wasn't at the end of the line:
+        // The user wants to split the current list item / code line / blockquote line
+        // (for the latter it doesn't really matter) in two. Temporarily select the
+        // (rest of the) line to achieve this.
+        if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) {
+            chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) {
+                chunk.selection = wholeMatch;
+                return "";
+            });
+            fakeSelection = true;
+        }
+
+        if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) {
+            if (commandMgr.doList) {
+                commandMgr.doList(chunk);
+            }
+        }
+        if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {
+            if (commandMgr.doBlockquote) {
+                commandMgr.doBlockquote(chunk);
+            }
+        }
+        if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
+            if (commandMgr.doCode) {
+                commandMgr.doCode(chunk);
+            }
+        }
+
+        if (fakeSelection) {
+            chunk.after = chunk.selection + chunk.after;
+            chunk.selection = "";
+        }
+    };
+
+    commandProto.doBlockquote = function (chunk, postProcessing) {
+
+        chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,
+            function (totalMatch, newlinesBefore, text, newlinesAfter) {
+                chunk.before += newlinesBefore;
+                chunk.after = newlinesAfter + chunk.after;
+                return text;
+            });
+
+        chunk.before = chunk.before.replace(/(>[ \t]*)$/,
+            function (totalMatch, blankLine) {
+                chunk.selection = blankLine + chunk.selection;
+                return "";
+            });
+
+        chunk.selection = chunk.selection.replace(/^(\s|>)+$/, "");
+        chunk.selection = chunk.selection || "Blockquote";
+
+        // The original code uses a regular expression to find out how much of the
+        // text *directly before* the selection already was a blockquote:
+
+        /*
+        if (chunk.before) {
+        chunk.before = chunk.before.replace(/\n?$/, "\n");
+        }
+        chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,
+        function (totalMatch) {
+        chunk.startTag = totalMatch;
+        return "";
+        });
+        */
+
+        // This comes down to:
+        // Go backwards as many lines a possible, such that each line
+        //  a) starts with ">", or
+        //  b) is almost empty, except for whitespace, or
+        //  c) is preceeded by an unbroken chain of non-empty lines
+        //     leading up to a line that starts with ">" and at least one more character
+        // and in addition
+        //  d) at least one line fulfills a)
+        //
+        // Since this is essentially a backwards-moving regex, it's susceptible to
+        // catastrophic backtracking and can cause the browser to hang;
+        // see e.g. http://meta.stackoverflow.com/questions/9807.
+        //
+        // Hence we replaced this by a simple state machine that just goes through the
+        // lines and checks for a), b), and c).
+
+        var match = "",
+            leftOver = "",
+            line;
+        if (chunk.before) {
+            var lines = chunk.before.replace(/\n$/, "").split("\n");
+            var inChain = false;
+            for (var i = 0; i < lines.length; i++) {
+                var good = false;
+                line = lines[i];
+                inChain = inChain && line.length > 0; // c) any non-empty line continues the chain
+                if (/^>/.test(line)) {                // a)
+                    good = true;
+                    if (!inChain && line.length > 1)  // c) any line that starts with ">" and has at least one more character starts the chain
+                        inChain = true;
+                } else if (/^[ \t]*$/.test(line)) {   // b)
+                    good = true;
+                } else {
+                    good = inChain;                   // c) the line is not empty and does not start with ">", so it matches if and only if we're in the chain
+                }
+                if (good) {
+                    match += line + "\n";
+                } else {
+                    leftOver += match + line;
+                    match = "\n";
+                }
+            }
+            if (!/(^|\n)>/.test(match)) {             // d)
+                leftOver += match;
+                match = "";
+            }
+        }
+
+        chunk.startTag = match;
+        chunk.before = leftOver;
+
+        // end of change
+
+        if (chunk.after) {
+            chunk.after = chunk.after.replace(/^\n?/, "\n");
+        }
+
+        chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,
+            function (totalMatch) {
+                chunk.endTag = totalMatch;
+                return "";
+            }
+        );
+
+        var replaceBlanksInTags = function (useBracket) {
+
+            var replacement = useBracket ? "> " : "";
+
+            if (chunk.startTag) {
+                chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,
+                    function (totalMatch, markdown) {
+                        return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
+                    });
+            }
+            if (chunk.endTag) {
+                chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,
+                    function (totalMatch, markdown) {
+                        return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
+                    });
+            }
+        };
+
+        if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) {
+            this.wrap(chunk, SETTINGS.lineLength - 2);
+            chunk.selection = chunk.selection.replace(/^/gm, "> ");
+            replaceBlanksInTags(true);
+            chunk.skipLines();
+        } else {
+            chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
+            this.unwrap(chunk);
+            replaceBlanksInTags(false);
+
+            if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) {
+                chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
+            }
+
+            if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) {
+                chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n");
+            }
+        }
+
+        chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection);
+
+        if (!/\n/.test(chunk.selection)) {
+            chunk.selection = chunk.selection.replace(/^(> *)/,
+                function (wholeMatch, blanks) {
+                    chunk.startTag += blanks;
+                    return "";
+                });
+        }
+    };
+
+    commandProto.doCode = function (chunk, postProcessing) {
+
+        var hasTextBefore = /\S[ ]*$/.test(chunk.before);
+        var hasTextAfter = /^[ ]*\S/.test(chunk.after);
+
+        // Use 'four space' markdown if the selection is on its own
+        // line or is multiline.
+        if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {
+
+            chunk.before = chunk.before.replace(/[ ]{4}$/,
+                function (totalMatch) {
+                    chunk.selection = totalMatch + chunk.selection;
+                    return "";
+                });
+
+            var nLinesBack = 1;
+            var nLinesForward = 1;
+
+            if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
+                nLinesBack = 0;
+            }
+            if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {
+                nLinesForward = 0;
+            }
+
+            chunk.skipLines(nLinesBack, nLinesForward);
+
+            if (!chunk.selection) {
+                chunk.startTag = "    ";
+                chunk.selection = "enter code here";
+            }
+            else {
+                if (/^[ ]{0,3}\S/m.test(chunk.selection)) {
+                    if (/\n/.test(chunk.selection))
+                        chunk.selection = chunk.selection.replace(/^/gm, "    ");
+                    else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior
+                        chunk.before += "    ";
+                }
+                else {
+                    chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");
+                }
+            }
+        }
+        else {
+            // Use backticks (`) to delimit the code block.
+
+            chunk.trimWhitespace();
+            chunk.findTags(/`/, /`/);
+
+            if (!chunk.startTag && !chunk.endTag) {
+                chunk.startTag = chunk.endTag = "`";
+                if (!chunk.selection) {
+                    chunk.selection = "enter code here";
+                }
+            }
+            else if (chunk.endTag && !chunk.startTag) {
+                chunk.before += chunk.endTag;
+                chunk.endTag = "";
+            }
+            else {
+                chunk.startTag = chunk.endTag = "";
+            }
+        }
+    };
+
+    commandProto.doList = function (chunk, postProcessing, isNumberedList) {
+
+        // These are identical except at the very beginning and end.
+        // Should probably use the regex extension function to make this clearer.
+        var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
+        var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;
+
+        // The default bullet is a dash but others are possible.
+        // This has nothing to do with the particular HTML bullet,
+        // it's just a markdown bullet.
+        var bullet = "-";
+
+        // The number in a numbered list.
+        var num = 1;
+
+        // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
+        var getItemPrefix = function () {
+            var prefix;
+            if (isNumberedList) {
+                prefix = " " + num + ". ";
+                num++;
+            }
+            else {
+                prefix = " " + bullet + " ";
+            }
+            return prefix;
+        };
+
+        // Fixes the prefixes of the other list items.
+        var getPrefixedItem = function (itemText) {
+
+            // The numbering flag is unset when called by autoindent.
+            if (isNumberedList === undefined) {
+                isNumberedList = /^\s*\d/.test(itemText);
+            }
+
+            // Renumber/bullet the list element.
+            itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,
+                function (_) {
+                    return getItemPrefix();
+                });
+
+            return itemText;
+        };
+
+        chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);
+
+        if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) {
+            chunk.before += chunk.startTag;
+            chunk.startTag = "";
+        }
+
+        if (chunk.startTag) {
+
+            var hasDigits = /\d+[.]/.test(chunk.startTag);
+            chunk.startTag = "";
+            chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
+            this.unwrap(chunk);
+            chunk.skipLines();
+
+            if (hasDigits) {
+                // Have to renumber the bullet points if this is a numbered list.
+                chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);
+            }
+            if (isNumberedList == hasDigits) {
+                return;
+            }
+        }
+
+        var nLinesUp = 1;
+
+        chunk.before = chunk.before.replace(previousItemsRegex,
+            function (itemText) {
+                if (/^\s*([*+-])/.test(itemText)) {
+                    bullet = re.$1;
+                }
+                nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
+                return getPrefixedItem(itemText);
+            });
+
+        if (!chunk.selection) {
+            chunk.selection = "List item";
+        }
+
+        var prefix = getItemPrefix();
+
+        var nLinesDown = 1;
+
+        chunk.after = chunk.after.replace(nextItemsRegex,
+            function (itemText) {
+                nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
+                return getPrefixedItem(itemText);
+            });
+
+        chunk.trimWhitespace(true);
+        chunk.skipLines(nLinesUp, nLinesDown, true);
+        chunk.startTag = prefix;
+        var spaces = prefix.replace(/./g, " ");
+        this.wrap(chunk, SETTINGS.lineLength - spaces.length);
+        chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
+
+    };
+
+    commandProto.doHeading = function (chunk, postProcessing) {
+
+        // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
+        chunk.selection = chunk.selection.replace(/\s+/g, " ");
+        chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");
+
+        // If we clicked the button with no selected text, we just
+        // make a level 2 hash header around some default text.
+        if (!chunk.selection) {
+            chunk.startTag = "## ";
+            chunk.selection = "Heading";
+            chunk.endTag = "";
+            return;
+        }
+
+        var headerLevel = 0;     // The existing header level of the selected text.
+
+        // Remove any existing hash heading markdown and save the header level.
+        chunk.findTags(/#+[ ]*/, /[ ]*#+/);
+        if (/#+/.test(chunk.startTag)) {
+            headerLevel = re.lastMatch.length;
+        }
+        chunk.startTag = chunk.endTag = "";
+
+        // Try to get the current header level by looking for - and = in the line
+        // below the selection.
+        chunk.findTags(null, /\s?(-+|=+)/);
+        if (/=+/.test(chunk.endTag)) {
+            headerLevel = 1;
+        }
+        if (/-+/.test(chunk.endTag)) {
+            headerLevel = 2;
+        }
+
+        // Skip to the next line so we can create the header markdown.
+        chunk.startTag = chunk.endTag = "";
+        chunk.skipLines(1, 1);
+
+        // We make a level 2 header if there is no current header.
+        // If there is a header level, we substract one from the header level.
+        // If it's already a level 1 header, it's removed.
+        var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;
+
+        if (headerLevelToCreate > 0) {
+
+            // The button only creates level 1 and 2 underline headers.
+            // Why not have it iterate over hash header levels?  Wouldn't that be easier and cleaner?
+            var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
+            var len = chunk.selection.length;
+            if (len > SETTINGS.lineLength) {
+                len = SETTINGS.lineLength;
+            }
+            chunk.endTag = "\n";
+            while (len--) {
+                chunk.endTag += headerChar;
+            }
+        }
+    };
+
+    commandProto.doHorizontalRule = function (chunk, postProcessing) {
+        chunk.startTag = "----------\n";
+        chunk.selection = "";
+        chunk.skipLines(2, 1, true);
+    }
+
+
+})();
diff --git a/app/assets/javascripts/pagedown/pagedown.js.erb b/app/assets/javascripts/pagedown/pagedown.js.erb
new file mode 100644
index 00000000..cada3497
--- /dev/null
+++ b/app/assets/javascripts/pagedown/pagedown.js.erb
@@ -0,0 +1,43 @@
+//= require markdown.converter
+//  markdown.editor is slightly adjusted to work with Bootstrap 4.
+//  Taken from https://github.com/hughevans/pagedown-bootstrap-rails, V2.1.4
+//= require markdown.editor
+//= require markdown.sanitizer
+//= require markdown.extra
+
+renderPagedown = function() {
+    $(".wmd-output").each(function (i) {
+        const converter = new Markdown.Converter();
+        const content = $(this).html();
+        return $(this).html(converter.makeHtml(content));
+    })
+};
+
+createPagedownEditor = function( selector, context ) {
+    if (context == null) { context = 'body'; }
+    return $(selector, context).each(function(i, input) {
+        if ($(input).data('is_rendered')) {
+            return;
+        }
+        const attr = $(input).attr('id').split('wmd-input')[1];
+        const converter = new Markdown.Converter();
+        Markdown.Extra.init(converter);
+        const help = {
+            handler() {
+                window.open('http://daringfireball.net/projects/markdown/syntax');
+                return false;
+            },
+            title: "<%= I18n.t('components.markdown_editor.help', default: 'Markdown Editing Help') %>"
+        };
+
+        const editor = new Markdown.Editor(converter, attr, help);
+        editor.run();
+        $('[data-toggle="tooltip"]').tooltip();
+        return $(input).data('is_rendered', true);
+    });
+};
+
+$(document).on('turbolinks:load', function() {
+    renderPagedown();
+    return createPagedownEditor('.wmd-input');
+});
diff --git a/app/assets/javascripts/shell.js b/app/assets/javascripts/shell.js
index 7bba5471..7446a655 100644
--- a/app/assets/javascripts/shell.js
+++ b/app/assets/javascripts/shell.js
@@ -1,4 +1,4 @@
-$(function() {
+$(document).on('turbolinks:load', function() {
   var ENTER_KEY_CODE = 13;
 
   var clearOutput = function() {
diff --git a/app/assets/javascripts/sortable.js b/app/assets/javascripts/sortable.js
index c9a767c8..fd9a5070 100644
--- a/app/assets/javascripts/sortable.js
+++ b/app/assets/javascripts/sortable.js
@@ -1,4 +1,4 @@
-$(document).ready(function(){
+$(document).on('turbolinks:load', function(){
   (function vendorTableSorter(){
     /*
       SortTable
diff --git a/app/assets/javascripts/statistics_activity_history.js b/app/assets/javascripts/statistics_activity_history.js
index c355c0b6..c4f03e5b 100644
--- a/app/assets/javascripts/statistics_activity_history.js
+++ b/app/assets/javascripts/statistics_activity_history.js
@@ -1,4 +1,4 @@
-$(document).ready(function () {
+$(document).on('turbolinks:load', function() {
 
   function manageActivityHistory(prefix) {
     var containerId = prefix + '-activity-history';
@@ -49,7 +49,7 @@ $(document).ready(function () {
 
       var refreshData = function (callback) {
         var params = new URLSearchParams(window.location.search.slice(1));
-        var jqxhr = $.ajax(prefix + '-activity-history.json', {
+        var jqxhr = $.ajax('/statistics/graphs/' + prefix + '-activity-history.json', {
           dataType: 'json',
           data: {from: params.get('from'), to: params.get('to'), interval: params.get('interval')},
           method: 'GET'
diff --git a/app/assets/javascripts/statistics_graphs.js b/app/assets/javascripts/statistics_graphs.js
index 0fbe30e2..8caabe1a 100644
--- a/app/assets/javascripts/statistics_graphs.js
+++ b/app/assets/javascripts/statistics_graphs.js
@@ -1,4 +1,4 @@
-$(document).ready(function () {
+$(document).on('turbolinks:load', function() {
   if ($.isController('statistics') && $('.graph#user-activity').isPresent()) {
 
     function manageGraph(containerId, url, refreshAfter) {
@@ -101,7 +101,7 @@ $(document).ready(function () {
       });
     }
 
-    manageGraph('user-activity', 'graphs/user-activity', 10);
-    manageGraph('rfc-activity', 'graphs/rfc-activity', 30);
+    manageGraph('user-activity', '/statistics/graphs/user-activity', 10);
+    manageGraph('rfc-activity', '/statistics/graphs/rfc-activity', 30);
   }
 });
diff --git a/app/assets/javascripts/submission_statistics.js b/app/assets/javascripts/submission_statistics.js
index 8ffdfcf2..7cef0e02 100644
--- a/app/assets/javascripts/submission_statistics.js
+++ b/app/assets/javascripts/submission_statistics.js
@@ -1,34 +1,44 @@
-$(function() {
+$(document).on('turbolinks:load', function() {
 
   var ACE_FILES_PATH = '/assets/ace/';
   var THEME = 'ace/theme/textmate';
 
   var currentSubmission = 0;
   var active_file = undefined;
-  var fileTrees = []
+  var fileTrees = [];
   var editor = undefined;
-  var fileTypeById = {}
+  var fileTypeById = {};
 
   var showActiveFile = function() {
     var session = editor.getSession();
-    var fileType = fileTypeById[active_file.file_type_id]
+    var fileType = fileTypeById[active_file.file_type_id];
     session.setMode(fileType.editor_mode);
     session.setTabSize(fileType.indent_size);
     session.setValue(active_file.content);
     session.setUseSoftTabs(true);
     session.setUseWrapMode(true);
 
+    // The event ready.jstree is fired too early and thus doesn't work.
+    var selectFileInJsTree = function() {
+      if (!filetree.hasClass('jstree-loading')) {
+        filetree.jstree("deselect_all");
+        filetree.jstree().select_node(active_file.file_id);
+      } else {
+        setTimeout(selectFileInJsTree, 250);
+      }
+    };
+
+    filetree = $(fileTrees[currentSubmission]);
+    selectFileInJsTree();
+    // Finally change jstree element to prevent flickering
     showFileTree(currentSubmission);
-    filetree = $(fileTrees[currentSubmission])
-    filetree.jstree("deselect_all");
-    filetree.jstree().select_node(active_file.file_id);
   };
 
   var initializeFileTree = function() {
     $('.files').each(function(index, element) {
       fileTree = $(element).jstree($(element).data('entries'));
       fileTree.on('click', 'li.jstree-leaf', function() {
-        var id = parseInt($(this).attr('id'))
+        var id = parseInt($(this).attr('id'));
         _.each(files[currentSubmission], function(file) {
           if (file.file_id === id) {
             active_file = file;
@@ -42,8 +52,8 @@ $(function() {
 
   var showFileTree = function(index) {
     $('.files').hide();
-    $(fileTrees[index].context).show();
-  }
+    $(fileTrees[index]).show();
+  };
 
   if ($.isController('exercises') && $('#timeline').isPresent()) {
 
@@ -85,7 +95,7 @@ $(function() {
         if (file.name === active_file.name) {
           fileIndex = index;
         }
-      })
+      });
       active_file = currentFiles[fileIndex];
       showActiveFile();
     });
@@ -94,10 +104,10 @@ $(function() {
       clearInterval(playInterval);
       playInterval = undefined;
       playButton.find('span.fa').removeClass('fa-pause').addClass('fa-play')
-    }
+    };
 
     playButton.on('click', function(event) {
-      if (playInterval == undefined) {
+      if (playInterval === undefined) {
         playInterval = setInterval(function() {
           if ($.isController('exercises') && $('#timeline').isPresent() && slider.val() < submissions.length - 1) {
             slider.val(parseInt(slider.val()) + 1);
@@ -112,7 +122,7 @@ $(function() {
       }
     });
 
-    active_file = files[0][0]
+    active_file = files[0][0];
     initializeFileTree();
     showActiveFile();
   }
diff --git a/app/assets/javascripts/working_time_graphs.js b/app/assets/javascripts/working_time_graphs.js
index e4ad3173..69b02e9f 100644
--- a/app/assets/javascripts/working_time_graphs.js
+++ b/app/assets/javascripts/working_time_graphs.js
@@ -1,4 +1,4 @@
-$(function() {
+$(document).on('turbolinks:load', function() {
     // http://localhost:3333/exercises/38/statistics good for testing
     // originally at--> localhost:3333/exercises/69/statistics
 
@@ -30,7 +30,7 @@ $(function() {
           var studentTime = minutes_array[i];
 
           for (var j = 0; j < studentTime; j++){
-              if (minutes_count[j] == undefined){
+              if (minutes_count[j] === undefined){
                   minutes_count[j] = 0;
               } else{
                   minutes_count[j] += 1;
@@ -173,23 +173,23 @@ $(function() {
       //}
 
       // DRAW THE SECOND GRAPH ------------------------------------------------------------------------------
-      //
       function draw_bar_graph() {
-          var group_incrament = 5;
-          var group_ranges = group_incrament; // just for the start
+          var number_of_bars = 40;
+          var group_increment = Math.ceil(maximum_minutes / number_of_bars); // range in minutes
+          var group_ranges = group_increment; // just for the start
           var minutes_array_for_bar = [];
 
           do {
               var section_value = 0;
               for (var i = 0; i < minutes_array.length; i++) {
-                  if ((minutes_array[i] < group_ranges) && (minutes_array[i] >= (group_ranges - group_incrament))) {
+                  if ((minutes_array[i] < group_ranges) && (minutes_array[i] >= (group_ranges - group_increment))) {
                       section_value++;
                   }
               }
               minutes_array_for_bar.push(section_value);
-              group_ranges += group_incrament;
+              group_ranges += group_increment;
           }
-          while (group_ranges < maximum_minutes + group_incrament);
+          while (group_ranges < maximum_minutes + group_increment);
 
           //console.log(minutes_array_for_bar); // this var used as the bars
           //minutes_array_for_bar = [39, 20, 28, 20, 39, 34, 26, 23, 16, 8];
@@ -199,30 +199,30 @@ $(function() {
 
 
           var width_ratio = .8;
+          if (getWidth()*width_ratio > 1000){
+              width_ratio = 1000/getWidth();
+          }
           var height_ratio = .7; // percent of height
 
           var margin = {top: 100, right: 20, bottom: 70, left: 70},//30,50
               width = (getWidth() * width_ratio) - margin.left - margin.right,
               height = (width * height_ratio) - margin.top - margin.bottom;
 
-          var x = d3.scale.ordinal()
-              .rangeRoundBands([0, width], .1);
+          var x = d3.scaleBand()
+              .range([0, width], .1);
 
           var y = d3.scaleLinear()
               .range([0,height-(margin.top + margin.bottom)]);
 
 
-          var xAxis = d3.svg.axis()
-              .scale(x)
-              .orient("bottom")
+          var xAxis = d3.axisBottom(x)
               .ticks(10);
 
 
-          var yAxis = d3.svg.axis()
-              .scale(d3.scaleLinear().domain([0,max_of_array]).range([height,0]))//y
-              .orient("left")
+          var yAxis = d3
+              .axisLeft(d3.scaleLinear().domain([0,max_of_array]).range([height,0]))//y
               .ticks(10)
-              .innerTickSize(-width);
+              .tickSizeInner(-width);
 
           var tip = d3.tip()
               .attr('class', 'd3-tip')
@@ -241,8 +241,8 @@ $(function() {
 
           x.domain(minutes_array_for_bar.map(function (d, i) {
               i++;
-              var high_side = i * group_incrament;
-              var low_side = high_side - group_incrament;
+              var high_side = i * group_increment;
+              var low_side = high_side - group_increment;
               return (low_side+"-"+high_side);
           }));
 
@@ -253,7 +253,14 @@ $(function() {
           svg.append("g")
               .attr("class", "x axis")
               .attr("transform", "translate(0," + height + ")")
-              .call(xAxis);
+              .call(xAxis)
+              .selectAll("text")
+              .style("text-anchor", "end")
+              .attr("dx", "-.8em")
+              .attr("dy", ".15em")
+              .attr("transform", function(d) {
+                  return "rotate(-45)"
+              });
 
           svg.append("g")
               .attr("class", "y axis")
@@ -278,7 +285,7 @@ $(function() {
               .attr("text-anchor", "middle")
               .attr("x", width / 2)
               .attr("y", height)
-              .attr("dy", ((height / 20) + 20) + 'px')
+              .attr("dy", ((height / 20) + 40) + 'px')
               .text("Working Time (Minutes)")
               .style('font-size', 14);
 
@@ -291,10 +298,10 @@ $(function() {
               .data(minutes_array_for_bar)
               .enter().append("rect")
               .attr("class", "bar")
-              .attr("x", function(d,i) {    var bar_incriment = width/ minutes_array_for_bar.length;
-                                            var bar_x = i * bar_incriment;
+              .attr("x", function(d,i) {    var bar_increment = width / minutes_array_for_bar.length;
+                                            var bar_x = i * bar_increment;
                                             return (bar_x)})
-              .attr("width", x.rangeBand())
+              .attr("width", x.bandwidth())
               .attr("y", function(d) { return height - y(d); })
               .attr("height", function(d) { return y(d); })
               .on('mouseover', tip.show)
@@ -311,7 +318,7 @@ $(function() {
               .style('text-decoration', 'underline');
 
       }
-      // draw_bar_graph();
+      draw_bar_graph();
   }
 
 });
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index 47163008..f6b67ead 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -10,10 +10,11 @@
  * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
  * file per style scope.
  *
+ *= require pagedown_bootstrap
+ *
+ * lib/assets
+ *= require flash
+ *
+ * app/assets
  *= require_tree .
- *= require_tree ../../../lib
- *= require_tree ../../../vendor/assets/stylesheets/
- *= require_self
- *= require bootstrap_pagedown
- *= require markdown
 */
diff --git a/app/assets/stylesheets/base.css.scss b/app/assets/stylesheets/base.css.scss
index 127b36ca..0d8a9413 100644
--- a/app/assets/stylesheets/base.css.scss
+++ b/app/assets/stylesheets/base.css.scss
@@ -3,6 +3,10 @@ h1 {
   margin-bottom: 0.5em;
 }
 
+h1, h2, h3, h4, h5, h6 {
+  font-weight: 500;
+}
+
 .lead {
   font-size: 16px;
   color: rgba(70, 70, 70, 1);
@@ -15,28 +19,47 @@ i.fa {
 pre {
   background-color: #FAFAFA;
   margin: 0;
-  padding: 0;
+  padding: .25rem!important;
+  border: 1px solid #CCCCCC;
 }
 
 span.caret {
   margin-left: 0.5em;
 }
 
+.btn {
+  -webkit-font-smoothing: antialiased;
+  font-weight: 500;
+  :not(.btn-lg) {
+    font-size: 0.875rem;
+  }
+}
+
 .progress {
   margin: 0;
+  border: 1px solid #CCCCCC;
+  padding: 0.125rem !important;
+  height: 1.25rem !important;
 
   .progress-bar {
     line-height: initial;
     min-width: 2em;
+    color: white;
   }
 }
 
+.navbar {
+  -webkit-font-smoothing: antialiased;
+  font-weight: 500;
+}
+
 .attribute-row + .attribute-row {
   margin-top: 0.5em;
 }
 
-.badge {
+.badge-pill {
   font-size: 100%;
+  font-weight: 500;
 }
 
 .container[data-controller] {
diff --git a/app/assets/stylesheets/bootstrap-dropdown-submenu.css.scss b/app/assets/stylesheets/bootstrap-dropdown-submenu.css.scss
index d0855298..ceb88020 100644
--- a/app/assets/stylesheets/bootstrap-dropdown-submenu.css.scss
+++ b/app/assets/stylesheets/bootstrap-dropdown-submenu.css.scss
@@ -3,10 +3,14 @@
 }
 
 .dropdown-submenu > .dropdown-menu {
-  top: 0;
+  top: -0.2em;
   left: 100%;
 }
 
+.dropdown-submenu.open > ul.dropdown-menu  {
+  display: block;
+}
+
 .dropdown-submenu > a:after {
   display: block;
   content: " ";
@@ -25,11 +29,11 @@
   border-left-color: #ffffff;
 }
 
-.dropdown-submenu.pull-left {
+.dropdown-submenu.float-left {
   float: none;
 }
 
-.dropdown-submenu.pull-left > .dropdown-menu {
+.dropdown-submenu.float-left > .dropdown-menu {
   left: -100%;
   margin-left: 10px;
   -webkit-border-radius: 6px 0 6px 6px;
diff --git a/app/assets/stylesheets/editor.css.scss b/app/assets/stylesheets/editor.css.scss
index dad17193..dff00f6f 100644
--- a/app/assets/stylesheets/editor.css.scss
+++ b/app/assets/stylesheets/editor.css.scss
@@ -45,10 +45,6 @@ button i.fa-spin {
   width: 100%;
   display: flex;
 
-  button {
-    font-size: 80%;
-  }
-
   button, .btn-group {
     flex-grow: 1;
   }
@@ -144,7 +140,8 @@ button i.fa-spin {
   min-height: 1px;
   padding-left: 15px;
   padding-right: 15px;
-  box-sizing: border-box
+  box-sizing: border-box;
+  margin-left: auto;
 }
 
 .output-col-collapsed {
@@ -155,7 +152,8 @@ button i.fa-spin {
   min-height: 1px;
   padding-left: 15px;
   padding-right: 15px;
-  box-sizing: border-box
+  box-sizing: border-box;
+  margin-left: auto;
 }
 
 .enforce-top-margin {
@@ -166,14 +164,14 @@ button i.fa-spin {
   margin-right: 10px !important;
 }
 
-.description-panel-collapsed {
+.description-card-collapsed {
   -webkit-transition: width 2s;
   transition: width 2s;
   height: 0px;
   visibility: hidden;
 }
 
-.description-panel {
+.description-card {
   height: auto;
   -webkit-transition: height 2s;
   transition: height 2s;
diff --git a/app/assets/stylesheets/exercise_collections.scss b/app/assets/stylesheets/exercise_collections.scss
index 79141988..1b3c6c76 100644
--- a/app/assets/stylesheets/exercise_collections.scss
+++ b/app/assets/stylesheets/exercise_collections.scss
@@ -57,10 +57,6 @@ rect.value-bar {
   }
 }
 
-.table-responsive#exercise-list {
-  max-height: 512px;
-}
-
 .exercise-id-tooltip {
   position: absolute;
   display: none;
diff --git a/app/assets/stylesheets/exercises.css.scss b/app/assets/stylesheets/exercises.css.scss
index 817fdeac..6790776a 100644
--- a/app/assets/stylesheets/exercises.css.scss
+++ b/app/assets/stylesheets/exercises.css.scss
@@ -35,11 +35,19 @@ input[type='file'] {
     margin: 10px 0 10px 0;
   }
 
-  .lead.description-panel-collapsed {
+  .lead.description-card-collapsed {
     margin: 0;
   }
 }
 
+[data-toggle="collapse"] .fa:before {
+  content: "\f139";
+}
+
+[data-toggle="collapse"].collapsed .fa:before {
+  content: "\f13a";
+}
+
 // Graph Settings
 
 .axis path {
diff --git a/app/assets/stylesheets/request-for-comments.css.scss b/app/assets/stylesheets/request-for-comments.css.scss
index 77b0810b..256e96a7 100644
--- a/app/assets/stylesheets/request-for-comments.css.scss
+++ b/app/assets/stylesheets/request-for-comments.css.scss
@@ -68,8 +68,10 @@
     .result {
       margin-right: 10px;
       margin-top: 20px;
-      width: 10px;
-      height: 10px;
+      min-width: 10px;
+      max-width: 10px;
+      min-height: 10px;
+      max-height: 10px;
     }
 
     .passed {
@@ -127,7 +129,7 @@
   background-color:#f9f9f9
 }
 
-.ace_tooltip {
+:not(.allow_ace_tooltip) > .ace_tooltip {
   display: none !important;
 }
 
diff --git a/app/assets/stylesheets/statistics.css.scss b/app/assets/stylesheets/statistics.css.scss
index 4dee2b34..e636e9b0 100644
--- a/app/assets/stylesheets/statistics.css.scss
+++ b/app/assets/stylesheets/statistics.css.scss
@@ -76,8 +76,8 @@ tr.highlight {
   grid-gap: 10px;
 
   > a {
-    color: #fff;
-    text-decoration: none;
+    color: #fff !important;
+    text-decoration: none !important;
 
     > div {
       border: 2px solid #0055ba;
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index c172d462..67c4742e 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -6,7 +6,7 @@ class ApplicationController < ActionController::Base
 
   after_action :verify_authorized, except: [:help, :welcome]
   before_action :set_locale, :allow_iframe_requests
-  protect_from_forgery(with: :exception)
+  protect_from_forgery(with: :exception, prepend: true)
   rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
 
   def current_user
@@ -14,11 +14,8 @@ class ApplicationController < ActionController::Base
     @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
-    redirect_to(:root, alert: t('application.not_authorized'))
+    redirect_to(request.referrer || :root, alert: t('application.not_authorized'))
   end
   private :render_not_authorized
 
diff --git a/app/controllers/code_ocean/errors_controller.rb b/app/controllers/code_ocean/errors_controller.rb
new file mode 100644
index 00000000..6b594dae
--- /dev/null
+++ b/app/controllers/code_ocean/errors_controller.rb
@@ -0,0 +1,45 @@
+module CodeOcean
+  class ErrorsController < ApplicationController
+    before_action :set_execution_environment
+
+    def authorize!
+      authorize(@error || @errors)
+    end
+    private :authorize!
+
+    def create
+      @error = CodeOcean::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
+            head (@error.save ? :created : :unprocessable_entity)
+          end
+        end
+      end
+    end
+
+    def error_params
+      params[:error].permit(:message, :submission_id).merge(execution_environment_id: @execution_environment.id) if params[:error].present?
+    end
+    private :error_params
+
+    def index
+      @errors = CodeOcean::Error.for_execution_environment(@execution_environment).grouped_by_message.paginate(page: params[:page])
+      authorize!
+    end
+
+    def set_execution_environment
+      @execution_environment = ExecutionEnvironment.find(params[:execution_environment_id])
+    end
+    private :set_execution_environment
+
+    def show
+      @error = CodeOcean::Error.find(params[:id])
+      authorize!
+    end
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/code_ocean/files_controller.rb b/app/controllers/code_ocean/files_controller.rb
index a788e4df..dbced0b7 100644
--- a/app/controllers/code_ocean/files_controller.rb
+++ b/app/controllers/code_ocean/files_controller.rb
@@ -41,7 +41,7 @@ module CodeOcean
     end
 
     def file_params
-      params[:code_ocean_file].permit(file_attributes).merge(context_type: 'Submission', role: 'user_defined_file')
+      params[:code_ocean_file].permit(file_attributes).merge(context_type: 'Submission', role: 'user_defined_file') if params[:code_ocean_file].present?
     end
     private :file_params
   end
diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/controllers/concerns/lti.rb b/app/controllers/concerns/lti.rb
index 4ba93615..e8241af3 100644
--- a/app/controllers/concerns/lti.rb
+++ b/app/controllers/concerns/lti.rb
@@ -23,9 +23,9 @@ module Lti
       session.delete(:consumer_id)
       session.delete(:external_user_id)
     else
-      LtiParameter.destroy_all(consumers_id: consumer_id,
-                               external_users_id: user_id,
-                               exercises_id: exercise_id)
+      LtiParameter.where(consumers_id: consumer_id,
+                         external_users_id: user_id,
+                         exercises_id: exercise_id).destroy_all
     end
   end
   private :clear_lti_session_data
@@ -138,7 +138,7 @@ module Lti
                                                     external_users_id: @current_user.id,
                                                     exercises_id: @exercise.id)
 
-    lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).to_json
+    lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).permit!.to_h
     lti_parameters.save!
     @lti_parameters = lti_parameters
 
diff --git a/app/controllers/concerns/remote_evaluation_parameters.rb b/app/controllers/concerns/remote_evaluation_parameters.rb
index 4f0acab2..a88ef7ea 100644
--- a/app/controllers/concerns/remote_evaluation_parameters.rb
+++ b/app/controllers/concerns/remote_evaluation_parameters.rb
@@ -2,7 +2,7 @@ module RemoteEvaluationParameters
   include FileParameters
 
   def remote_evaluation_params
-    remote_evaluation_params = params[:remote_evaluation].permit(:validation_token, files_attributes: file_attributes)
+    remote_evaluation_params = params[:remote_evaluation].permit(:validation_token, files_attributes: file_attributes) if params[:remote_evaluation].present?
   end
   private :remote_evaluation_params
 end
\ No newline at end of file
diff --git a/app/controllers/concerns/submission_parameters.rb b/app/controllers/concerns/submission_parameters.rb
index 6b1781b3..fa5c848b 100644
--- a/app/controllers/concerns/submission_parameters.rb
+++ b/app/controllers/concerns/submission_parameters.rb
@@ -16,7 +16,7 @@ module SubmissionParameters
       current_user_id = current_user.id
       current_user_class_name = current_user.class.name
     end
-    submission_params = params[:submission].permit(:cause, :exercise_id, files_attributes: file_attributes).merge(user_id: current_user_id, user_type: current_user_class_name)
+    submission_params = params[:submission].present? ? 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
diff --git a/app/controllers/consumers_controller.rb b/app/controllers/consumers_controller.rb
index 5c97a58a..2a2f784a 100644
--- a/app/controllers/consumers_controller.rb
+++ b/app/controllers/consumers_controller.rb
@@ -22,7 +22,7 @@ class ConsumersController < ApplicationController
   end
 
   def consumer_params
-    params[:consumer].permit(:name, :oauth_key, :oauth_secret)
+    params[:consumer].permit(:name, :oauth_key, :oauth_secret) if params[:consumer].present?
   end
   private :consumer_params
 
diff --git a/app/controllers/error_template_attributes_controller.rb b/app/controllers/error_template_attributes_controller.rb
index 05de2772..283e5b03 100644
--- a/app/controllers/error_template_attributes_controller.rb
+++ b/app/controllers/error_template_attributes_controller.rb
@@ -81,6 +81,6 @@ class ErrorTemplateAttributesController < ApplicationController
 
     # Never trust parameters from the scary internet, only allow the white list through.
     def error_template_attribute_params
-      params[:error_template_attribute].permit(:key, :description, :regex, :important)
+      params[:error_template_attribute].permit(:key, :description, :regex, :important) if params[:error_template_attribute].present?
     end
 end
diff --git a/app/controllers/error_templates_controller.rb b/app/controllers/error_templates_controller.rb
index 2632abf9..bec956d8 100644
--- a/app/controllers/error_templates_controller.rb
+++ b/app/controllers/error_templates_controller.rb
@@ -99,6 +99,6 @@ class ErrorTemplatesController < ApplicationController
 
     # Never trust parameters from the scary internet, only allow the white list through.
     def error_template_params
-      params[:error_template].permit(:name, :execution_environment_id, :signature, :description, :hint)
+      params[:error_template].permit(:name, :execution_environment_id, :signature, :description, :hint) if params[:error_template].present?
     end
 end
diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb
deleted file mode 100644
index 667b186d..00000000
--- a/app/controllers/errors_controller.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-class ErrorsController < ApplicationController
-  before_action :set_execution_environment
-
-  def authorize!
-    authorize(@error || @errors)
-  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, :submission_id).merge(execution_environment_id: @execution_environment.id)
-  end
-  private :error_params
-
-  def index
-    @errors = Error.for_execution_environment(@execution_environment).grouped_by_message.paginate(page: params[:page])
-    authorize!
-  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
diff --git a/app/controllers/execution_environments_controller.rb b/app/controllers/execution_environments_controller.rb
index c220ae74..917c9f5f 100644
--- a/app/controllers/execution_environments_controller.rb
+++ b/app/controllers/execution_environments_controller.rb
@@ -86,11 +86,11 @@ class ExecutionEnvironmentsController < ApplicationController
     working_time_statistics = {}
     user_statistics = {}
 
-    ActiveRecord::Base.connection.execute(working_time_query).each do |tuple|
+    ApplicationRecord.connection.execute(working_time_query).each do |tuple|
       working_time_statistics[tuple["exercise_id"].to_i] = tuple
     end
 
-    ActiveRecord::Base.connection.execute(user_query).each do |tuple|
+    ApplicationRecord.connection.execute(user_query).each do |tuple|
       user_statistics[tuple["exercise_id"].to_i] = tuple
     end
 
@@ -101,7 +101,7 @@ class ExecutionEnvironmentsController < ApplicationController
   end
 
   def execution_environment_params
-    params[:execution_environment].permit(:docker_image, :exposed_ports, :editor_mode, :file_extension, :file_type_id, :help, :indent_size, :memory_limit, :name, :network_enabled, :permitted_execution_time, :pool_size, :run_command, :test_command, :testing_framework).merge(user_id: current_user.id, user_type: current_user.class.name)
+    params[:execution_environment].permit(:docker_image, :exposed_ports, :editor_mode, :file_extension, :file_type_id, :help, :indent_size, :memory_limit, :name, :network_enabled, :permitted_execution_time, :pool_size, :run_command, :test_command, :testing_framework).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:execution_environment].present?
   end
   private :execution_environment_params
 
diff --git a/app/controllers/exercise_collections_controller.rb b/app/controllers/exercise_collections_controller.rb
index cfb93a71..3e13a3af 100644
--- a/app/controllers/exercise_collections_controller.rb
+++ b/app/controllers/exercise_collections_controller.rb
@@ -51,7 +51,7 @@ class ExerciseCollectionsController < ApplicationController
   end
 
   def exercise_collection_params
-    sanitized_params = params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []).merge(user_type: InternalUser.name)
+    sanitized_params = params[:exercise_collection].present? ? params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []).merge(user_type: InternalUser.name) : {}
     sanitized_params[:exercise_ids] = sanitized_params[:exercise_ids].reject {|v| v.nil? or v == ''}
     sanitized_params.tap {|p| p[:exercise_collection_items] = p[:exercise_ids].map.with_index {|_id, index| ExerciseCollectionItem.find_or_create_by(exercise_id: _id, exercise_collection_id: @exercise_collection.id, position: index)}; p.delete(:exercise_ids)}
   end
diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb
index b506afdf..839aef4c 100644
--- a/app/controllers/exercises_controller.rb
+++ b/app/controllers/exercises_controller.rb
@@ -12,9 +12,9 @@ class ExercisesController < ApplicationController
   before_action :set_file_types, only: [:create, :edit, :new, :update]
   before_action :set_course_token, only: [:implement]
 
-  skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml]
+  skip_before_action :verify_authenticity_token, only: [:import_proforma_xml]
   skip_after_action :verify_authorized, only: [:import_proforma_xml]
-  skip_after_action :verify_policy_scoped, only: [:import_proforma_xml]
+  skip_after_action :verify_policy_scoped, only: [:import_proforma_xml], raise: false
 
   def authorize!
     authorize(@exercise || @exercises)
@@ -77,7 +77,7 @@ class ExercisesController < ApplicationController
   def create
     @exercise = Exercise.new(exercise_params)
     collect_set_and_unset_exercise_tags
-    myparam = exercise_params
+    myparam = exercise_params.present? ? exercise_params : { }
     checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
     removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
 
@@ -160,19 +160,21 @@ class ExercisesController < ApplicationController
   private :user_by_code_harbor_token
 
   def exercise_params
-    params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name)
+    params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:exercise].present?
   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
+    if exercise_params
+      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
@@ -185,28 +187,8 @@ class ExercisesController < ApplicationController
     count_interventions_today = UserExerciseIntervention.where(user: current_user).where("created_at >= ?", Time.zone.now.beginning_of_day).count
     user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user, exercise: @exercise).size >= max_intervention_count_per_exercise
     user_got_enough_interventions = count_interventions_today >= max_intervention_count_per_day or user_got_intervention_in_exercise
-    @is_experimental_course = @course_token and experimental_course?(@course_token)
-
-    @experiment_group = UserGroupSeparator.getInterventionGroup(current_user)
-
-    showInterventions = (@is_experimental_course and not user_solved_exercise and not user_got_enough_interventions) ? "true" : "false"
-
-    case @experiment_group
-      when :rfc_intervention_stale_rfc
-        @show_rfc_interventions = showInterventions
-      when :break_intervention_stale_rfc
-        @show_break_interventions = showInterventions
-      when :no_intervention_stale_rfc
-      when :no_intervention_hide_rfc
-        @hide_rfc_button = "true"
-      when :break_intervention_show_rfc
-        @show_break_interventions = showInterventions
-      when :no_intervention_show_rfc
-      when :rfc_intervention_show_rfc
-        @show_rfc_interventions = showInterventions
-    end
-
 
+    @show_rfc_interventions = (not user_solved_exercise and not user_got_enough_interventions).to_s
 
 
     @search = Search.new
@@ -364,7 +346,7 @@ class ExercisesController < ApplicationController
       query = "SELECT user_id, MAX(score) AS maximum_score, COUNT(id) AS runs
               FROM submissions WHERE exercise_id = #{@exercise.id} GROUP BY
               user_id;"
-      ActiveRecord::Base.connection.execute(query).each do |tuple|
+      ApplicationRecord.connection.execute(query).each do |tuple|
         user_statistics[tuple["user_id"].to_i] = tuple
       end
       render locals: {
@@ -428,11 +410,6 @@ class ExercisesController < ApplicationController
           return
         end
 
-        if @is_experimental_course and (@rfc_group == :hide_rfc)
-          redirect_to_lti_return_path
-          return
-        end
-
         rfc = @submission.own_unsolved_rfc
         if rfc
           # set a message that informs the user that his own RFC should be closed.
diff --git a/app/controllers/external_users_controller.rb b/app/controllers/external_users_controller.rb
index 441edd67..503d323f 100644
--- a/app/controllers/external_users_controller.rb
+++ b/app/controllers/external_users_controller.rb
@@ -57,7 +57,7 @@ class ExternalUsersController < ApplicationController
 
     statistics = {}
 
-    ActiveRecord::Base.connection.execute(working_time_query(params[:tag])).each do |tuple|
+    ApplicationRecord.connection.execute(working_time_query(params[:tag])).each do |tuple|
       statistics[tuple["exercise_id"].to_i] = tuple
     end
 
diff --git a/app/controllers/file_templates_controller.rb b/app/controllers/file_templates_controller.rb
index a6039500..04c2a98a 100644
--- a/app/controllers/file_templates_controller.rb
+++ b/app/controllers/file_templates_controller.rb
@@ -89,6 +89,6 @@ class FileTemplatesController < ApplicationController
 
     # Never trust parameters from the scary internet, only allow the white list through.
     def file_template_params
-      params[:file_template].permit(:name, :file_type_id, :content)
+      params[:file_template].permit(:name, :file_type_id, :content) if params[:file_template].present?
     end
 end
diff --git a/app/controllers/file_types_controller.rb b/app/controllers/file_types_controller.rb
index 9f16da28..d450df3d 100644
--- a/app/controllers/file_types_controller.rb
+++ b/app/controllers/file_types_controller.rb
@@ -23,7 +23,7 @@ class FileTypesController < ApplicationController
   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)
+    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) if params[:file_type].present?
   end
   private :file_type_params
 
@@ -38,7 +38,7 @@ class FileTypesController < ApplicationController
   end
 
   def set_editor_modes
-    @editor_modes = Dir.glob('vendor/assets/javascripts/ace/mode-*.js').map do |filename|
+    @editor_modes = Dir.glob('vendor/assets/javascripts/ace/mode-*.js').sort.map do |filename|
       name = filename.gsub(/\w+\/|mode-|.js$/, '')
       [name, "ace/mode/#{name}"]
     end
diff --git a/app/controllers/hints_controller.rb b/app/controllers/hints_controller.rb
index 827ba6ce..48b86b63 100644
--- a/app/controllers/hints_controller.rb
+++ b/app/controllers/hints_controller.rb
@@ -23,7 +23,7 @@ class HintsController < ApplicationController
   end
 
   def hint_params
-    params[:hint].permit(:locale, :message, :name, :regular_expression).merge(execution_environment_id: @execution_environment.id)
+    params[:hint].permit(:locale, :message, :name, :regular_expression).merge(execution_environment_id: @execution_environment.id) if params[:hint].present?
   end
   private :hint_params
 
diff --git a/app/controllers/internal_users_controller.rb b/app/controllers/internal_users_controller.rb
index b9efb276..6779cff5 100644
--- a/app/controllers/internal_users_controller.rb
+++ b/app/controllers/internal_users_controller.rb
@@ -21,7 +21,7 @@ class InternalUsersController < ApplicationController
       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) }
+        format.json { head :ok }
       else
         respond_with_invalid_object(format, object: @user, template: :reset_password)
       end
@@ -66,7 +66,7 @@ class InternalUsersController < ApplicationController
   end
 
   def internal_user_params
-    params[:internal_user].permit(:consumer_id, :email, :name, :role)
+    params[:internal_user].permit(:consumer_id, :email, :name, :role) if params[:internal_user].present?
   end
   private :internal_user_params
 
@@ -105,7 +105,7 @@ class InternalUsersController < ApplicationController
       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) }
+        format.json { head :ok }
       else
         respond_with_invalid_object(format, object: @user, template: :activate)
       end
diff --git a/app/controllers/interventions_controller.rb b/app/controllers/interventions_controller.rb
index b4b5971e..27bfe4e0 100644
--- a/app/controllers/interventions_controller.rb
+++ b/app/controllers/interventions_controller.rb
@@ -22,7 +22,7 @@ class InterventionsController < ApplicationController
   end
 
   def intervention_params
-    params[:intervention].permit(:name)
+    params[:intervention].permit(:name) if params[:intervention].present?
   end
   private :intervention_params
 
diff --git a/app/controllers/proxy_exercises_controller.rb b/app/controllers/proxy_exercises_controller.rb
index 02fe0220..40bd20ca 100644
--- a/app/controllers/proxy_exercises_controller.rb
+++ b/app/controllers/proxy_exercises_controller.rb
@@ -39,7 +39,7 @@ class ProxyExercisesController < ApplicationController
   end
 
   def proxy_exercise_params
-    params[:proxy_exercise].permit(:description, :title, :exercise_ids  => [])
+    params[:proxy_exercise].permit(:description, :title, :exercise_ids  => []) if params[:proxy_exercise].present?
   end
   private :proxy_exercise_params
 
diff --git a/app/controllers/remote_evaluation_controller.rb b/app/controllers/remote_evaluation_controller.rb
index 36d61f73..5f01860c 100644
--- a/app/controllers/remote_evaluation_controller.rb
+++ b/app/controllers/remote_evaluation_controller.rb
@@ -15,14 +15,12 @@ class RemoteEvaluationController < ApplicationController
 
     # todo extra: validiere, ob files wirklich zur Übung gehören (wenn allowNewFiles-flag nicht gesetzt ist)
     if (remote_evaluation_mapping = RemoteEvaluationMapping.find_by(:validation_token => validation_token))
-      puts remote_evaluation_mapping.exercise_id
-      puts remote_evaluation_mapping.user_id
 
       _params = remote_evaluation_params.except(:validation_token)
       _params[:exercise_id] = remote_evaluation_mapping.exercise_id
       _params[:user_id] = remote_evaluation_mapping.user_id
       _params[:cause] = "remoteAssess"
-      _params[:user_type] = "ExternalUser"
+      _params[:user_type] = remote_evaluation_mapping.user_type
 
       @submission = Submission.create(_params)
       render json: score_submission(@submission)
diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb
index 1624e5e0..32d1e560 100644
--- a/app/controllers/submissions_controller.rb
+++ b/app/controllers/submissions_controller.rb
@@ -115,7 +115,7 @@ class SubmissionsController < ApplicationController
     if @file.native_file?
       send_file(@file.native_file.path, disposition: 'inline')
     else
-      render(text: @file.content)
+      render(plain: @file.content)
     end
   end
 
@@ -140,7 +140,7 @@ class SubmissionsController < ApplicationController
       # probably add:
       # ensure
       #   #guarantee that the thread is releasing the DB connection after it is done
-      #   ActiveRecord::Base.connectionpool.releaseconnection
+      #   ApplicationRecord.connectionpool.releaseconnection
       # end
       Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
 
@@ -329,7 +329,7 @@ class SubmissionsController < ApplicationController
 
   def set_file
     @file = @files.detect { |file| file.name_with_extension == params[:filename] }
-    render(nothing: true, status: 404) unless @file
+    head :not_found unless @file
   end
   private :set_file
 
@@ -362,11 +362,11 @@ class SubmissionsController < ApplicationController
     DockerClient.destroy_container(container)
   rescue Docker::Error::NotFoundError
   ensure
-    render(nothing: true)
+    head :ok
   end
 
   def store_error(stderr)
-    ::Error.create(submission_id: @submission.id, execution_environment_id: @submission.execution_environment.id, message: stderr)
+    CodeOcean::Error.create(submission_id: @submission.id, execution_environment_id: @submission.execution_environment.id, message: stderr)
   end
   private :store_error
 
@@ -398,13 +398,13 @@ class SubmissionsController < ApplicationController
   private :with_server_sent_events
 
   def create_remote_evaluation_mapping
-    user_id = @submission.user_id
+    user = @submission.user
     exercise_id = @submission.exercise_id
 
-    remote_evaluation_mapping = RemoteEvaluationMapping.create(:user_id => user_id, :exercise_id => exercise_id)
+    remote_evaluation_mapping = RemoteEvaluationMapping.create(user: user, exercise_id: exercise_id)
 
     # create .co file
-    path = "tmp/" + user_id.to_s + ".co"
+    path = "tmp/" + user.id.to_s + ".co"
     # parse validation token
     content = "#{remote_evaluation_mapping.validation_token}\n"
     # parse remote request url
diff --git a/app/controllers/subscriptions_controller.rb b/app/controllers/subscriptions_controller.rb
index 232e9f01..f86431e2 100644
--- a/app/controllers/subscriptions_controller.rb
+++ b/app/controllers/subscriptions_controller.rb
@@ -56,7 +56,7 @@ class SubscriptionsController < ApplicationController
   def subscription_params
     current_user_id = current_user.try(:id)
     current_user_class_name = current_user.try(:class).try(:name)
-    params[:subscription].permit(:request_for_comment_id, :subscription_type).merge(user_id: current_user_id, user_type: current_user_class_name, deleted: false)
+    params[:subscription].permit(:request_for_comment_id, :subscription_type).merge(user_id: current_user_id, user_type: current_user_class_name, deleted: false) if params[:subscription].present?
   end
   private :subscription_params
 end
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index ff6925dd..ac462653 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -22,7 +22,7 @@ class TagsController < ApplicationController
   end
 
   def tag_params
-    params[:tag].permit(:name)
+    params[:tag].permit(:name) if params[:tag].present?
   end
   private :tag_params
 
diff --git a/app/controllers/user_exercise_feedbacks_controller.rb b/app/controllers/user_exercise_feedbacks_controller.rb
index 4c350a81..a3c71abc 100644
--- a/app/controllers/user_exercise_feedbacks_controller.rb
+++ b/app/controllers/user_exercise_feedbacks_controller.rb
@@ -1,8 +1,7 @@
 class UserExerciseFeedbacksController < ApplicationController
   include CommonBehavior
 
-  before_action :set_user_exercise_feedback, only: [:edit, :update]
-  before_action :set_user_exercise_feedback_by_id, only: [:show, :destroy]
+  before_action :set_user_exercise_feedback, only: [:edit, :update, :show, :destroy]
 
   def comment_presets
     [[0,t('user_exercise_feedback.difficulty_easy')],
@@ -103,16 +102,12 @@ class UserExerciseFeedbacksController < ApplicationController
   end
 
   def set_user_exercise_feedback
-    @exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id])
-    @uef = UserExerciseFeedback.find_by(exercise_id: params[:user_exercise_feedback][:exercise_id], user: current_user)
-  end
-
-  def set_user_exercise_feedback_by_id
     @uef = UserExerciseFeedback.find(params[:id])
+    @exercise = @uef.exercise
   end
 
   def uef_params
-    params[:user_exercise_feedback].permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime).merge(user_id: current_user.id, user_type: current_user.class.name)
+    params[:user_exercise_feedback].permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:user_exercise_feedback].present?
   end
 
   def validate_inputs(uef_params)
diff --git a/app/helpers/lti_helper.rb b/app/helpers/lti_helper.rb
index 433468a1..ff2d6354 100644
--- a/app/helpers/lti_helper.rb
+++ b/app/helpers/lti_helper.rb
@@ -1,3 +1,5 @@
+require 'oauth/request_proxy/action_controller_request' # Rails 5 changed `Rack::Request` to `ActionDispatch::Request`
+
 module LtiHelper
   def lti_outcome_service?(exercise_id, external_user_id, consumer_id)
     return false if external_user_id == '' || consumer_id == ''
diff --git a/app/helpers/pagedown_form_builder.rb b/app/helpers/pagedown_form_builder.rb
new file mode 100644
index 00000000..d38407f2
--- /dev/null
+++ b/app/helpers/pagedown_form_builder.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+class PagedownFormBuilder < ActionView::Helpers::FormBuilder
+  def pagedown(method, args)
+    # Adopt simple form builder to work with form_for
+    @attribute_name = method
+    @input_html_options = args[:input_html]
+
+    @template.capture do
+      @template.concat wmd_button_bar
+      @template.concat wmd_textarea
+      @template.concat wmd_preview if show_wmd_preview?
+    end
+  end
+
+  private
+
+  def wmd_button_bar
+    @template.content_tag :div, nil, id: "wmd-button-bar-#{base_id}"
+  end
+
+  def wmd_textarea
+    @template.text_area @object_name, @attribute_name,
+                       **@input_html_options,
+                       class: 'form-control wmd-input',
+                       id: "wmd-input-#{base_id}"
+  end
+
+  def wmd_preview
+    @template.content_tag :div, nil,
+                         class: 'wmd-preview',
+                         id: "wmd-preview-#{base_id}"
+  end
+
+  def show_wmd_preview?
+    @input_html_options[:preview].present?
+  end
+
+  def base_id
+    options[:pagedown_id_suffix] || @attribute_name
+  end
+end
diff --git a/app/helpers/request_for_comments_helper.rb b/app/helpers/request_for_comments_helper.rb
deleted file mode 100644
index f46a73e4..00000000
--- a/app/helpers/request_for_comments_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module RequestForCommentsHelper
-end
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
new file mode 100644
index 00000000..a20dfadf
--- /dev/null
+++ b/app/javascript/packs/application.js
@@ -0,0 +1,36 @@
+/* eslint no-console:0 */
+// This file is automatically compiled by Webpack, along with any other files
+// present in this directory. You're encouraged to place your actual application logic in
+// a relevant structure within app/javascript and only use these pack files to reference
+// that code so it'll be compiled.
+//
+// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
+// layout file, like app/views/layouts/application.html.slim
+
+// JS
+import 'jquery'
+import 'bootstrap/dist/js/bootstrap.bundle.min';
+import 'chosen-js/chosen.jquery';
+import 'jstree';
+import 'underscore';
+window._ = _; // Publish underscore's `_` in global namespace
+
+// CSS
+import 'chosen-js/chosen.css';
+import 'jstree/dist/themes/default/style.min.css';
+
+// custom jquery-ui library for minimal mouse interaction support
+import 'jquery-ui/ui/widget'
+import 'jquery-ui/ui/data'
+import 'jquery-ui/ui/disable-selection'
+import 'jquery-ui/ui/scroll-parent'
+import 'jquery-ui/ui/widgets/draggable'
+import 'jquery-ui/ui/widgets/droppable'
+import 'jquery-ui/ui/widgets/resizable'
+import 'jquery-ui/ui/widgets/selectable'
+import 'jquery-ui/ui/widgets/sortable'
+import 'jquery-ui/themes/base/draggable.css'
+import 'jquery-ui/themes/base/core.css'
+import 'jquery-ui/themes/base/resizable.css'
+import 'jquery-ui/themes/base/selectable.css'
+import 'jquery-ui/themes/base/sortable.css'
diff --git a/app/javascript/packs/d3-tip.js b/app/javascript/packs/d3-tip.js
new file mode 100644
index 00000000..73286f90
--- /dev/null
+++ b/app/javascript/packs/d3-tip.js
@@ -0,0 +1,12 @@
+/* eslint no-console:0 */
+// This file is automatically compiled by Webpack, along with any other files
+// present in this directory. You're encouraged to place your actual application logic in
+// a relevant structure within app/javascript and only use these pack files to reference
+// that code so it'll be compiled.
+//
+// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
+// layout file, like app/views/layouts/application.html.slim
+
+// JS
+import d3Tip from 'd3-tip'
+window.d3.tip = d3Tip;
diff --git a/app/javascript/packs/highlight.js b/app/javascript/packs/highlight.js
new file mode 100644
index 00000000..690511fe
--- /dev/null
+++ b/app/javascript/packs/highlight.js
@@ -0,0 +1,15 @@
+/* eslint no-console:0 */
+// This file is automatically compiled by Webpack, along with any other files
+// present in this directory. You're encouraged to place your actual application logic in
+// a relevant structure within app/javascript and only use these pack files to reference
+// that code so it'll be compiled.
+//
+// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
+// layout file, like app/views/layouts/application.html.slim
+
+// JS
+import hljs from 'highlight.js'
+window.hljs = hljs;
+
+// CSS
+import 'highlight.js/styles/default.css'
diff --git a/app/javascript/packs/stylesheets.scss b/app/javascript/packs/stylesheets.scss
new file mode 100644
index 00000000..f7752941
--- /dev/null
+++ b/app/javascript/packs/stylesheets.scss
@@ -0,0 +1,16 @@
+/* eslint no-console:0 */
+// This file is automatically compiled by Webpack, along with any other files
+// present in this directory. You're encouraged to place your actual application logic in
+// a relevant structure within app/javascript and only use these pack files to reference
+// that code so it'll be compiled.
+//
+// To reference this file, add <%= stylesheet_pack_tag 'stylesheets' %> to the appropriate
+// layout file, like app/views/layouts/application.html.slim
+
+@import '~bootswatch/dist/yeti/variables';
+@import '~bootstrap/scss/bootstrap';
+@import '~bootswatch/dist/yeti/bootswatch';
+$fa-font-path: '~font-awesome/fonts';
+@import '~font-awesome/scss/font-awesome';
+$opensans-path: '~opensans-webkit/fonts/';
+@import '~opensans-webkit/src/sass/open-sans';
diff --git a/app/javascript/packs/vis.js b/app/javascript/packs/vis.js
new file mode 100644
index 00000000..a221b6fe
--- /dev/null
+++ b/app/javascript/packs/vis.js
@@ -0,0 +1,15 @@
+/* eslint no-console:0 */
+// This file is automatically compiled by Webpack, along with any other files
+// present in this directory. You're encouraged to place your actual application logic in
+// a relevant structure within app/javascript and only use these pack files to reference
+// that code so it'll be compiled.
+//
+// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
+// layout file, like app/views/layouts/application.html.slim
+
+// JS
+import 'vis'
+window.vis = vis;
+
+// CSS
+import 'vis/dist/vis.min.css'
diff --git a/app/mailers/.keep b/app/mailers/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/models/.keep b/app/models/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/models/anomaly_notification.rb b/app/models/anomaly_notification.rb
index 8c9b90cd..8e9498fa 100644
--- a/app/models/anomaly_notification.rb
+++ b/app/models/anomaly_notification.rb
@@ -1,4 +1,4 @@
-class AnomalyNotification < ActiveRecord::Base
+class AnomalyNotification < ApplicationRecord
   belongs_to :user, polymorphic: true
   belongs_to :exercise
   belongs_to :exercise_collection
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
new file mode 100644
index 00000000..10a4cba8
--- /dev/null
+++ b/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+  self.abstract_class = true
+end
diff --git a/app/models/code_harbor_link.rb b/app/models/code_harbor_link.rb
index 2e219aa9..338461d1 100644
--- a/app/models/code_harbor_link.rb
+++ b/app/models/code_harbor_link.rb
@@ -1,4 +1,4 @@
-class CodeHarborLink < ActiveRecord::Base
+class CodeHarborLink < ApplicationRecord
   validates :oauth2token, presence: true
   validates :user_id, presence: true
 
diff --git a/app/models/code_ocean/error.rb b/app/models/code_ocean/error.rb
new file mode 100644
index 00000000..935962cf
--- /dev/null
+++ b/app/models/code_ocean/error.rb
@@ -0,0 +1,19 @@
+module CodeOcean
+  class Error < ApplicationRecord
+    belongs_to :execution_environment
+
+    scope :for_execution_environment, ->(execution_environment) { where(execution_environment_id: execution_environment.id) }
+    scope :grouped_by_message, -> { select('MAX(created_at) AS created_at, MAX(id) AS id, message, COUNT(id) AS count').group(:message).order('count DESC') }
+
+    validates :execution_environment_id, presence: true
+    validates :message, presence: true
+
+    def self.nested_resource?
+      true
+    end
+
+    def to_s
+      id.to_s
+    end
+  end
+end
diff --git a/app/models/code_ocean/file.rb b/app/models/code_ocean/file.rb
index 22c6e877..a68b585b 100644
--- a/app/models/code_ocean/file.rb
+++ b/app/models/code_ocean/file.rb
@@ -15,7 +15,7 @@ module CodeOcean
     end
   end
 
-  class File < ActiveRecord::Base
+  class File < ApplicationRecord
     include DefaultValues
 
     DEFAULT_WEIGHT = 1.0
@@ -28,12 +28,11 @@ module CodeOcean
     before_validation :set_ancestor_values, if: :incomplete_descendent?
 
     belongs_to :context, polymorphic: true
-    belongs_to :execution_environment
-    belongs_to :file
+    belongs_to :file, class_name: 'CodeOcean::File', optional: true # This is only required for submissions and is validated below
     alias_method :ancestor, :file
     belongs_to :file_type
 
-    has_many :files
+    has_many :files, class_name: 'CodeOcean::File'
     has_many :testruns
     has_many :comments
     alias_method :descendants, :files
@@ -47,6 +46,8 @@ module CodeOcean
       scope :"#{role}s", -> { where(role: role) }
     end
 
+    default_scope { order(name: :asc) }
+
     validates :feedback_message, if: :teacher_defined_test?, presence: true
     validates :feedback_message, absence: true, unless: :teacher_defined_test?
     validates :file_type_id, presence: true
@@ -57,6 +58,7 @@ module CodeOcean
     validates :role, inclusion: {in: ROLES}
     validates :weight, if: :teacher_defined_test?, numericality: true, presence: true
     validates :weight, absence: true, unless: :teacher_defined_test?
+    validates :file, presence: true if :context.is_a?(Submission)
 
     validates_with FileNameValidator, fields: [:name, :path, :file_type_id]
 
@@ -78,6 +80,14 @@ module CodeOcean
     end
     private :content_present?
 
+    def filepath
+      if path.present?
+        ::File.join(path, name_with_extension)
+      else
+        name_with_extension
+      end
+    end
+
     def hash_content
       self.hashed_content = Digest::MD5.new.hexdigest(file_type.try(:binary?) ? ::File.new(native_file.file.path, 'r').read : content)
     end
diff --git a/app/models/comment.rb b/app/models/comment.rb
index de2a265b..b3be54f9 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -1,4 +1,4 @@
-class Comment < ActiveRecord::Base
+class Comment < ApplicationRecord
   # inherit the creation module: encapsulates that this is a polymorphic user, offers some aliases and makes sure that all necessary attributes are set.
   include Creation
   attr_accessor :username, :date, :updated, :editable
diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/app/models/consumer.rb b/app/models/consumer.rb
index 781f02b0..2dfb7c2a 100644
--- a/app/models/consumer.rb
+++ b/app/models/consumer.rb
@@ -1,4 +1,4 @@
-class Consumer < ActiveRecord::Base
+class Consumer < ApplicationRecord
   has_many :users
 
   scope :with_users, -> { where('id IN (SELECT consumer_id FROM internal_users)') }
diff --git a/app/models/error.rb b/app/models/error.rb
deleted file mode 100644
index d52b3a34..00000000
--- a/app/models/error.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class Error < ActiveRecord::Base
-  belongs_to :execution_environment
-
-  scope :for_execution_environment, ->(execution_environment) { where(execution_environment_id: execution_environment.id) }
-  scope :grouped_by_message, -> { select('MAX(created_at) AS created_at, MAX(id) AS id, message, COUNT(id) AS count').group(:message).order('count DESC') }
-
-  validates :execution_environment_id, presence: true
-  validates :message, presence: true
-
-  def self.nested_resource?
-    true
-  end
-end
diff --git a/app/models/error_template.rb b/app/models/error_template.rb
index be4a3279..83c2f360 100644
--- a/app/models/error_template.rb
+++ b/app/models/error_template.rb
@@ -1,8 +1,8 @@
-class ErrorTemplate < ActiveRecord::Base
+class ErrorTemplate < ApplicationRecord
   belongs_to :execution_environment
   has_and_belongs_to_many :error_template_attributes
 
   def to_s
-    "#{id} [#{name}]"
+    name
   end
 end
diff --git a/app/models/error_template_attribute.rb b/app/models/error_template_attribute.rb
index 844c1019..42661132 100644
--- a/app/models/error_template_attribute.rb
+++ b/app/models/error_template_attribute.rb
@@ -1,7 +1,7 @@
-class ErrorTemplateAttribute < ActiveRecord::Base
+class ErrorTemplateAttribute < ApplicationRecord
   has_and_belongs_to_many :error_template
 
   def to_s
-    "#{id} [#{key}]"
+    key
   end
 end
diff --git a/app/models/event.rb b/app/models/event.rb
index 325fecb8..bab444df 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -1,7 +1,7 @@
-class Event < ActiveRecord::Base
+class Event < ApplicationRecord
   belongs_to :user, polymorphic: true
   belongs_to :exercise
-  belongs_to :file
+  belongs_to :file, class_name: 'CodeOcean::File'
 
   validates :category, presence: true
   validates :data, presence: true
diff --git a/app/models/execution_environment.rb b/app/models/execution_environment.rb
index 9cd23a48..613fb082 100644
--- a/app/models/execution_environment.rb
+++ b/app/models/execution_environment.rb
@@ -1,6 +1,6 @@
 require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
 
-class ExecutionEnvironment < ActiveRecord::Base
+class ExecutionEnvironment < ApplicationRecord
   include Creation
   include DefaultValues
 
diff --git a/app/models/exercise.rb b/app/models/exercise.rb
index dbb38758..67f2a0a0 100644
--- a/app/models/exercise.rb
+++ b/app/models/exercise.rb
@@ -1,7 +1,7 @@
 require 'nokogiri'
 require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
 
-class Exercise < ActiveRecord::Base
+class Exercise < ApplicationRecord
   include Context
   include Creation
   include DefaultValues
@@ -23,8 +23,8 @@ class Exercise < ActiveRecord::Base
   accepts_nested_attributes_for :exercise_tags
   has_many :user_exercise_feedbacks
 
-  has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions
-  has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
+  has_many :external_users, source: :user, source_type: 'ExternalUser', through: :submissions
+  has_many :internal_users, source: :user, source_type: 'InternalUser', through: :submissions
   alias_method :users, :external_users
 
   scope :with_submissions, -> { where('id IN (SELECT exercise_id FROM submissions)') }
diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb
index 80461e23..98cc5a63 100644
--- a/app/models/exercise_collection.rb
+++ b/app/models/exercise_collection.rb
@@ -1,4 +1,4 @@
-class ExerciseCollection < ActiveRecord::Base
+class ExerciseCollection < ApplicationRecord
   include TimeHelper
 
   has_many :exercise_collection_items, dependent: :delete_all
diff --git a/app/models/exercise_collection_item.rb b/app/models/exercise_collection_item.rb
index c7b01f20..6147c3a0 100644
--- a/app/models/exercise_collection_item.rb
+++ b/app/models/exercise_collection_item.rb
@@ -1,4 +1,4 @@
-class ExerciseCollectionItem < ActiveRecord::Base
+class ExerciseCollectionItem < ApplicationRecord
   belongs_to :exercise_collection
   belongs_to :exercise
 end
diff --git a/app/models/exercise_tag.rb b/app/models/exercise_tag.rb
index 4b8ab3e5..9ead6f13 100644
--- a/app/models/exercise_tag.rb
+++ b/app/models/exercise_tag.rb
@@ -1,4 +1,4 @@
-class ExerciseTag < ActiveRecord::Base
+class ExerciseTag < ApplicationRecord
 
   belongs_to :tag
   belongs_to :exercise
diff --git a/app/models/external_user.rb b/app/models/external_user.rb
index b7a0ebc5..b877f08d 100644
--- a/app/models/external_user.rb
+++ b/app/models/external_user.rb
@@ -1,4 +1,4 @@
-class ExternalUser < ActiveRecord::Base
+class ExternalUser < ApplicationRecord
   include User
 
   validates :consumer_id, presence: true
diff --git a/app/models/file_template.rb b/app/models/file_template.rb
index ef068e13..2eba68be 100644
--- a/app/models/file_template.rb
+++ b/app/models/file_template.rb
@@ -1,4 +1,4 @@
-class FileTemplate < ActiveRecord::Base
+class FileTemplate < ApplicationRecord
 
   belongs_to :file_type
 
diff --git a/app/models/file_type.rb b/app/models/file_type.rb
index d3b519d5..ee777092 100644
--- a/app/models/file_type.rb
+++ b/app/models/file_type.rb
@@ -1,6 +1,6 @@
 require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
 
-class FileType < ActiveRecord::Base
+class FileType < ApplicationRecord
   include Creation
   include DefaultValues
 
@@ -11,7 +11,7 @@ class FileType < ActiveRecord::Base
   after_initialize :set_default_values
 
   has_many :execution_environments
-  has_many :files
+  has_many :files, class_name: 'CodeOcean::File'
   has_many :file_templates
 
   validates :binary, boolean_presence: true
diff --git a/app/models/hint.rb b/app/models/hint.rb
index a14b29bc..b54ba8a2 100644
--- a/app/models/hint.rb
+++ b/app/models/hint.rb
@@ -1,4 +1,4 @@
-class Hint < ActiveRecord::Base
+class Hint < ApplicationRecord
   belongs_to :execution_environment
 
   validates :execution_environment_id, presence: true
diff --git a/app/models/internal_user.rb b/app/models/internal_user.rb
index 8f1bf04b..e7adda6d 100644
--- a/app/models/internal_user.rb
+++ b/app/models/internal_user.rb
@@ -1,4 +1,4 @@
-class InternalUser < ActiveRecord::Base
+class InternalUser < ApplicationRecord
   include User
 
   authenticates_with_sorcery!
diff --git a/app/models/intervention.rb b/app/models/intervention.rb
index a6693450..43750626 100644
--- a/app/models/intervention.rb
+++ b/app/models/intervention.rb
@@ -1,7 +1,7 @@
-class Intervention < ActiveRecord::Base
+class Intervention < ApplicationRecord
 
   has_many :user_exercise_interventions
-  has_many :users, through: :user_exercise_interventions, source_type: "ExternalUser"
+  has_many :users, through: :user_exercise_interventions, source_type: 'ExternalUser'
 
   def to_s
     name
diff --git a/app/models/lti_parameter.rb b/app/models/lti_parameter.rb
index 3351a6c9..ab92165f 100644
--- a/app/models/lti_parameter.rb
+++ b/app/models/lti_parameter.rb
@@ -1,4 +1,4 @@
-class LtiParameter < ActiveRecord::Base
+class LtiParameter < ApplicationRecord
   belongs_to :consumer, foreign_key: "consumers_id"
   belongs_to :exercise, foreign_key: "exercises_id"
   belongs_to :external_user, foreign_key: "external_users_id"
diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb
index c6da3870..328fffa6 100644
--- a/app/models/proxy_exercise.rb
+++ b/app/models/proxy_exercise.rb
@@ -1,4 +1,4 @@
-class ProxyExercise < ActiveRecord::Base
+class ProxyExercise < ApplicationRecord
 
     after_initialize :generate_token
     after_initialize :set_reason
@@ -36,31 +36,15 @@ class ProxyExercise < ActiveRecord::Base
           Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" )
           assigned_user_proxy_exercise.exercise
         else
+          Rails.logger.debug("find new matching exercise for user #{user.id}" )
           matching_exercise =
-              if (token.eql? "e85689d5")
-                Rails.logger.debug("Proxy exercise with token e85689d5, split user in groups..")
-                group = UserGroupSeparator.getGroupExerciseDescriptionTesting(user)
-                Rails.logger.debug("user assigned to group #{group}")
-                case group
-                  when :group_a
-                    exercises.where(id: 557).first
-                  when :group_b
-                    exercises.where(id: 558).first
-                  when :group_c
-                    exercises.where(id: 559).first
-                  when :group_d
-                    exercises.where(id: 560).first
-                end
-              else
-                Rails.logger.debug("find new matching exercise for user #{user.id}" )
-                begin
-                  find_matching_exercise(user)
-                rescue => e #fallback
-                  Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" )
-                  @reason[:reason] = "fallback because of error"
-                  @reason[:error] = "#{$!}:\n\t#{e.backtrace.join("\n\t")}"
-                  exercises.where("expected_difficulty > 1").shuffle.first # difficulty should be > 1 to prevent dummy exercise from being chosen.
-                end
+              begin
+                find_matching_exercise(user)
+              rescue => e #fallback
+                Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" )
+                @reason[:reason] = "fallback because of error"
+                @reason[:error] = "#{$!}:\n\t#{e.backtrace.join("\n\t")}"
+                exercises.where("expected_difficulty > 1").shuffle.first # difficulty should be > 1 to prevent dummy exercise from being chosen.
               end
           user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
           matching_exercise
@@ -69,42 +53,27 @@ class ProxyExercise < ActiveRecord::Base
     end
 
     def find_matching_exercise(user)
-      user_group = UserGroupSeparator.getProxyExerciseGroup(user)
-      case user_group
-        when :dummy_assigment
-          rec_ex = select_easiest_exercise(exercises)
-          @reason[:reason] = "dummy group"
-          Rails.logger.debug("assigned user to dummy group, and gave him exercise: #{rec_ex.title}")
-          rec_ex
-        when :random_assigment
-          @reason[:reason] = "random group"
-          ex = exercises.where("expected_difficulty > 1").shuffle.first
-          Rails.logger.debug("assigned user to random group, and gave him exercise: #{ex.title}")
-          ex
-        when :recommended_assignment
-          exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq.compact
-          tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten
-          Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}")
+        exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq.compact
+        tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten
+        Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}")
 
-          # find exercises
-          potential_recommended_exercises = []
-          exercises.where("expected_difficulty >= 1").each do |ex|
-            ## find exercises which have only tags the user has already seen
-            if (ex.tags - tags_user_has_seen).empty?
-              potential_recommended_exercises << ex
-            end
+        # find exercises
+        potential_recommended_exercises = []
+        exercises.where("expected_difficulty >= 1").each do |ex|
+          ## find exercises which have only tags the user has already seen
+          if (ex.tags - tags_user_has_seen).empty?
+            potential_recommended_exercises << ex
           end
-          Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}")
-          # if all exercises contain tags which the user has never seen, recommend easiest exercise
-          if potential_recommended_exercises.empty?
-            Rails.logger.debug("matched easiest exercise in pool")
-            @reason[:reason] = "easiest exercise in pool. empty potential exercises"
-            select_easiest_exercise(exercises)
-          else
-            select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
-          end
-      end
-
+        end
+        Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}")
+        # if all exercises contain tags which the user has never seen, recommend easiest exercise
+        if potential_recommended_exercises.empty?
+          Rails.logger.debug("matched easiest exercise in pool")
+          @reason[:reason] = "easiest exercise in pool. empty potential exercises"
+          select_easiest_exercise(exercises)
+        else
+          select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
+        end
     end
     private :find_matching_exercise
 
diff --git a/app/models/remote_evaluation_mapping.rb b/app/models/remote_evaluation_mapping.rb
index be0034b1..a0fba790 100644
--- a/app/models/remote_evaluation_mapping.rb
+++ b/app/models/remote_evaluation_mapping.rb
@@ -1,10 +1,10 @@
 # todo: reference to lti_param_model
-class RemoteEvaluationMapping < ActiveRecord::Base
+class RemoteEvaluationMapping < ApplicationRecord
   before_create :generate_token, unless: :validation_token?
   belongs_to :exercise
-  belongs_to :user
+  belongs_to :user, polymorphic: true
 
   def generate_token
     self.validation_token = SecureRandom.urlsafe_base64
   end
-end
\ No newline at end of file
+end
diff --git a/app/models/request_for_comment.rb b/app/models/request_for_comment.rb
index f7a425f8..fe15409b 100644
--- a/app/models/request_for_comment.rb
+++ b/app/models/request_for_comment.rb
@@ -1,4 +1,4 @@
-class RequestForComment < ActiveRecord::Base
+class RequestForComment < ApplicationRecord
   include Creation
   belongs_to :submission
   belongs_to :exercise
diff --git a/app/models/search.rb b/app/models/search.rb
index f22dbc3e..bbb59e5b 100644
--- a/app/models/search.rb
+++ b/app/models/search.rb
@@ -1,4 +1,4 @@
-class Search < ActiveRecord::Base
+class Search < ApplicationRecord
   belongs_to :user, polymorphic: true
   belongs_to :exercise
 end
\ No newline at end of file
diff --git a/app/models/structured_error.rb b/app/models/structured_error.rb
index 851b3fb8..4d48b97c 100644
--- a/app/models/structured_error.rb
+++ b/app/models/structured_error.rb
@@ -1,7 +1,6 @@
-class StructuredError < ActiveRecord::Base
+class StructuredError < ApplicationRecord
   belongs_to :error_template
   belongs_to :submission
-  belongs_to :file, class_name: 'CodeOcean::File'
 
   has_many :structured_error_attributes
 
diff --git a/app/models/structured_error_attribute.rb b/app/models/structured_error_attribute.rb
index 65bda05e..f5f7615b 100644
--- a/app/models/structured_error_attribute.rb
+++ b/app/models/structured_error_attribute.rb
@@ -1,4 +1,4 @@
-class StructuredErrorAttribute < ActiveRecord::Base
+class StructuredErrorAttribute < ApplicationRecord
   belongs_to :structured_error
   belongs_to :error_template_attribute
 
diff --git a/app/models/submission.rb b/app/models/submission.rb
index 763e1366..e9d3b235 100644
--- a/app/models/submission.rb
+++ b/app/models/submission.rb
@@ -1,4 +1,4 @@
-class Submission < ActiveRecord::Base
+class Submission < ApplicationRecord
   include Context
   include Creation
 
@@ -66,10 +66,6 @@ class Submission < ActiveRecord::Base
   end
 
   def unsolved_rfc
-    # old query
-    # RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) }
-
-    # experimental query:
-    RequestForComment.unsolved.joins('JOIN exercise_collection_items eci ON eci.exercise_id = request_for_comments.exercise_id').where('eci.exercise_collection_id != 3 OR user_id%10 > 3').where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) }
+    RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |( (rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) && (!rfc_element.question.empty?)) }
   end
 end
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
index abe1b513..ed3f3a3e 100644
--- a/app/models/subscription.rb
+++ b/app/models/subscription.rb
@@ -1,4 +1,4 @@
-class Subscription < ActiveRecord::Base
+class Subscription < ApplicationRecord
   belongs_to :user, polymorphic: true
   belongs_to :request_for_comment
 end
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 002ec687..b6de61bb 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -1,4 +1,4 @@
-class Tag < ActiveRecord::Base
+class Tag < ApplicationRecord
 
   has_many :exercise_tags
   has_many :exercises, through: :exercise_tags
diff --git a/app/models/testrun.rb b/app/models/testrun.rb
index a266edc3..46529f98 100644
--- a/app/models/testrun.rb
+++ b/app/models/testrun.rb
@@ -1,4 +1,4 @@
-class Testrun < ActiveRecord::Base
+class Testrun < ApplicationRecord
     belongs_to :file, class_name: 'CodeOcean::File'
     belongs_to :submission
 end
diff --git a/app/models/user_exercise_feedback.rb b/app/models/user_exercise_feedback.rb
index 3075b96e..8d7b697f 100644
--- a/app/models/user_exercise_feedback.rb
+++ b/app/models/user_exercise_feedback.rb
@@ -1,4 +1,4 @@
-class UserExerciseFeedback < ActiveRecord::Base
+class UserExerciseFeedback < ApplicationRecord
   include Creation
 
   belongs_to :exercise
diff --git a/app/models/user_exercise_intervention.rb b/app/models/user_exercise_intervention.rb
index 60824c34..b3008206 100644
--- a/app/models/user_exercise_intervention.rb
+++ b/app/models/user_exercise_intervention.rb
@@ -1,4 +1,4 @@
-class UserExerciseIntervention < ActiveRecord::Base
+class UserExerciseIntervention < ApplicationRecord
 
   belongs_to :user, polymorphic: true
   belongs_to :intervention
diff --git a/app/models/user_proxy_exercise_exercise.rb b/app/models/user_proxy_exercise_exercise.rb
index e7defae6..8a0cbb5d 100644
--- a/app/models/user_proxy_exercise_exercise.rb
+++ b/app/models/user_proxy_exercise_exercise.rb
@@ -1,4 +1,4 @@
-class UserProxyExerciseExercise < ActiveRecord::Base
+class UserProxyExerciseExercise < ApplicationRecord
 
   belongs_to :user, polymorphic: true
   belongs_to :exercise
diff --git a/app/policies/code_ocean/error_policy.rb b/app/policies/code_ocean/error_policy.rb
new file mode 100644
index 00000000..376af8d8
--- /dev/null
+++ b/app/policies/code_ocean/error_policy.rb
@@ -0,0 +1,7 @@
+module CodeOcean
+  class ErrorPolicy < AdminOrAuthorPolicy
+    def author?
+      @user == @record.execution_environment.author
+    end
+  end
+end
diff --git a/app/policies/error_policy.rb b/app/policies/error_policy.rb
deleted file mode 100644
index 632f8b20..00000000
--- a/app/policies/error_policy.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class ErrorPolicy < AdminOrAuthorPolicy
-  def author?
-    @user == @record.execution_environment.author
-  end
-end
diff --git a/app/views/admin/dashboard/show.html.slim b/app/views/admin/dashboard/show.html.slim
index 805f7819..65da67f4 100644
--- a/app/views/admin/dashboard/show.html.slim
+++ b/app/views/admin/dashboard/show.html.slim
@@ -1,9 +1,21 @@
 - content_for :head do
-  = javascript_include_tag(asset_path('vis.min.js', type: :javascript))
-  = stylesheet_link_tag(asset_path('vis.min.css', type: :stylesheet))
+  // Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326.
+     Otherwise, the global variable `vis` might be uninitialized in the assets (race condition)
+  meta name='turbolinks-visit-control' content='reload'
+  = javascript_pack_tag('vis', 'data-turbolinks-track': true)
+  = stylesheet_pack_tag('vis', media: 'all', 'data-turbolinks-track': true)
 
 h1 = t('breadcrumbs.dashboard.show')
 
+h2 Version
+
+div
+  = "Branch:"
+  pre = `git rev-parse --abbrev-ref HEAD`
+div
+  = "Commit:"
+  pre = `git log -n 1 --pretty`
+
 h2 Docker
 
 - if DockerContainerPool.config[:active]
diff --git a/app/views/application/_breadcrumbs.html.slim b/app/views/application/_breadcrumbs.html.slim
index 216dcc7f..7899ae0e 100644
--- a/app/views/application/_breadcrumbs.html.slim
+++ b/app/views/application/_breadcrumbs.html.slim
@@ -1,19 +1,19 @@
 - if current_user.try(:internal_user?)
   ul.breadcrumb
-    - if model = Kernel.const_get(controller_name.classify) rescue nil
+    - if model = Kernel.const_get(controller_path.classify) rescue nil
       - object = model.find_by(id: params[:id])
       - if model.try(:nested_resource?)
-        li = model.model_name.human(count: 2)
+        li.breadcrumb-item = model.model_name.human(count: 2)
         - if object
-          li = object
+          li.breadcrumb-item = object
       - else
-        li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
+        li.breadcrumb-item = 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
+          li.breadcrumb-item = link_to(object, send(:"#{model.model_name.singular}_path", object))
+      li.breadcrumb-item.active
         - if I18n.translation_present?("shared.#{params[:action]}")
           = t("shared.#{params[:action]}")
         - else
           = t("#{controller_name}.index.#{params[:action]}")
     - else
-      li.active = t("breadcrumbs.#{controller_name}.#{params[:action]}")
+      li.breadcrumb-item.active = t("breadcrumbs.#{controller_name}.#{params[:action]}")
diff --git a/app/views/application/_flash.html.slim b/app/views/application/_flash.html.slim
index a1f7191c..4a78c032 100644
--- a/app/views/application/_flash.html.slim
+++ b/app/views/application/_flash.html.slim
@@ -1,6 +1,7 @@
 #flash-container
   #flash.container.fixed_error_messages data-message-failure=t('shared.message_failure')
     - %w[alert danger info notice success warning].each do |severity|
-      div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)}"
-        p id="flash-#{severity}" = flash[severity]
-        span.fa.fa-times
+      div.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)} alert-dismissible fade show"
+        p.mb-0 id="flash-#{severity}" = flash[severity]
+        button type="button" class="close" data-dismiss="alert" aria-label="Close"
+          span.text-white aria-hidden="true" ×
diff --git a/app/views/application/_locale_selector.html.slim b/app/views/application/_locale_selector.html.slim
index 28f4626c..b278ab5a 100644
--- a/app/views/application/_locale_selector.html.slim
+++ b/app/views/application/_locale_selector.html.slim
@@ -1,7 +1,7 @@
-li.dropdown
-  a.dropdown-toggle data-toggle='dropdown' href='#'
+li.nav-item.dropdown
+  a.nav-link.dropdown-toggle.mx-3 data-toggle='dropdown' href='#'
     = t("locales.#{I18n.locale}")
     span.caret
-  ul.dropdown-menu role='menu'
+  ul.dropdown-menu.p-0.mt-1 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)))
+      li = link_to(t("locales.#{locale}"), url_for(params.permit!.merge(locale: locale)), class: 'dropdown-item')
diff --git a/app/views/application/_navigation.html.slim b/app/views/application/_navigation.html.slim
index 24fb2598..006de33b 100644
--- a/app/views/application/_navigation.html.slim
+++ b/app/views/application/_navigation.html.slim
@@ -1,16 +1,16 @@
 - if current_user.try(:internal_user?)
   ul.nav.navbar-nav
-    li.dropdown
-      a.dropdown-toggle data-toggle='dropdown' href='#'
+    li.nav-item.dropdown
+      a.nav-link.dropdown-toggle.mx-3 data-toggle='dropdown' href='#'
         = t('shared.administration')
         span.caret
-      ul.dropdown-menu role='menu'
+      ul.dropdown-menu.p-0.mt-1 role='menu'
         - if current_user.admin?
-          li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path)
-          li = link_to(t('breadcrumbs.statistics.show'), statistics_path)
-          li.divider
+          li = link_to(t('breadcrumbs.dashboard.show'), admin_dashboard_path, class: 'dropdown-item', 'data-turbolinks' => "false")
+          li = link_to(t('breadcrumbs.statistics.show'), statistics_path, class: 'dropdown-item')
+          li.dropdown-divider role='separator'
         = render('navigation_submenu', title: t('activerecord.models.exercise.other'),
-                models: [Exercise, ExerciseCollection, ProxyExercise, Tag], link: exercises_path, cached: true)
+                models: [Exercise, ExerciseCollection, ProxyExercise, Tag, Submission], link: exercises_path, cached: true)
         = render('navigation_submenu', title: t('navigation.sections.users'), models: [InternalUser, ExternalUser],
                 cached: true)
         = render('navigation_collection_link', model: ExecutionEnvironment, cached: true)
diff --git a/app/views/application/_navigation_collection_link.html.slim b/app/views/application/_navigation_collection_link.html.slim
index 412ea0bd..fc45dd4a 100644
--- a/app/views/application/_navigation_collection_link.html.slim
+++ b/app/views/application/_navigation_collection_link.html.slim
@@ -1,2 +1,2 @@
 - if policy(model).index?
-  li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"))
+  li = link_to(model.model_name.human(count: 2), send(:"#{model.model_name.collection}_path"), class: 'dropdown-item')
diff --git a/app/views/application/_navigation_submenu.html.slim b/app/views/application/_navigation_submenu.html.slim
index 23daf814..047955d7 100644
--- a/app/views/application/_navigation_submenu.html.slim
+++ b/app/views/application/_navigation_submenu.html.slim
@@ -1,6 +1,6 @@
-li.dropdown.dropdown-submenu
+li.dropdown-submenu
   - link = link.nil? ? "#" : link
-  a href=link class="dropdown-toggle" data-toggle="dropdown" = title
-  ul class="dropdown-menu"
+  a.dropdown-item.dropdown-toggle href=link data-toggle="dropdown" = title
+  ul.dropdown-menu.p-0
     - models.each do |model|
       = render('navigation_collection_link', model: model, cached: true)
diff --git a/app/views/application/_session.html.slim b/app/views/application/_session.html.slim
index 38e58588..db401908 100644
--- a/app/views/application/_session.html.slim
+++ b/app/views/application/_session.html.slim
@@ -1,19 +1,19 @@
 - if current_user
-  li.dropdown
-    a.dropdown-toggle data-toggle='dropdown' href='#'
+  li.nav-item.dropdown
+    a.nav-link.dropdown-toggle data-toggle='dropdown' href='#'
       i.fa.fa-user
       = current_user
       span.caret
-    ul.dropdown-menu role='menu'
+    ul.dropdown-menu.p-0.mt-1 role='menu'
       - if current_user.internal_user?
-        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('request_for_comments.index.all'), request_for_comments_path)
-        li = link_to(t('request_for_comments.index.get_my_rfc_activity'), my_rfc_activity_path)
-      li = link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_path)
+        li = link_to(t('consumers.show.link'), current_user.consumer, class: 'dropdown-item') if current_user.consumer
+        li = link_to(t('internal_users.show.link'), current_user, class: 'dropdown-item')
+        li = link_to(t('request_for_comments.index.all'), request_for_comments_path, class: 'dropdown-item')
+        li = link_to(t('request_for_comments.index.get_my_rfc_activity'), my_rfc_activity_path, class: 'dropdown-item')
+      li = link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_path, class: 'dropdown-item')
       - if current_user.internal_user?
-        li = link_to(t('sessions.destroy.link'), sign_out_path, method: :delete)
+        li = link_to(t('sessions.destroy.link'), sign_out_path, method: :delete, class: 'dropdown-item')
 - else
-  li = link_to(sign_in_path) do
+  li.nav-item = link_to(sign_in_path, class: 'nav-link') do
     i.fa.fa-sign-in
     = t('sessions.new.link')
diff --git a/app/views/code_harbor_links/index.html.slim b/app/views/code_harbor_links/index.html.slim
index 953985c4..ca4a81d4 100644
--- a/app/views/code_harbor_links/index.html.slim
+++ b/app/views/code_harbor_links/index.html.slim
@@ -9,7 +9,7 @@ h1 = CodeHarborLink.model_name.human(count: 2)
     tbody
       - @code_harbor_links.each do |code_harbor_link|
         tr
-          td = code_harbor_link.oauth2token
+          td = link_to(code_harbor_link.oauth2token, code_harbor_link)
           td = link_to(t('shared.show'), code_harbor_link)
           td = link_to(t('shared.edit'), edit_code_harbor_link_path(code_harbor_link))
           td = link_to(t('shared.destroy'), code_harbor_link, data: {confirm: t('shared.confirm_destroy')}, method: :delete)
diff --git a/app/views/errors/index.html.slim b/app/views/code_ocean/errors/index.html.slim
similarity index 85%
rename from app/views/errors/index.html.slim
rename to app/views/code_ocean/errors/index.html.slim
index 2db0d9de..dbfa2312 100644
--- a/app/views/errors/index.html.slim
+++ b/app/views/code_ocean/errors/index.html.slim
@@ -1,10 +1,10 @@
-h1 = ::Error.model_name.human(count: 2)
+h1 = CodeOcean::Error.model_name.human(count: 2)
 
 .table-responsive
   table.table
     thead
       tr
-        th = t('.count')
+        th = t('errors.index.count')
         th = t('activerecord.attributes.error.message')
         th = t('shared.created_at')
         th = t('shared.actions')
diff --git a/app/views/errors/show.html.slim b/app/views/code_ocean/errors/show.html.slim
similarity index 77%
rename from app/views/errors/show.html.slim
rename to app/views/code_ocean/errors/show.html.slim
index f5bf9a08..1bb6623e 100644
--- a/app/views/errors/show.html.slim
+++ b/app/views/code_ocean/errors/show.html.slim
@@ -1,4 +1,4 @@
-h1 = ::Error.model_name.human
+h1 = CodeOcean::Error.model_name.human
 
 = row(label: 'error.message', value: @error.message)
 = row(label: 'shared.created_at', value: l(@error.created_at, format: :short))
diff --git a/app/views/code_ocean/files/_form.html.slim b/app/views/code_ocean/files/_form.html.slim
index 46c5b2c2..a00912c0 100644
--- a/app/views/code_ocean/files/_form.html.slim
+++ b/app/views/code_ocean/files/_form.html.slim
@@ -12,5 +12,5 @@
     = f.label(:file_template_id, t('activerecord.attributes.file.file_template_id'))
     = f.collection_select(:file_template_id, FileTemplate.all.order(:name), :id, :name, {:include_blank => true}, class: 'form-control')
   = f.hidden_field(:context_id)
-  .hidden#noTemplateLabel data-text=t('file_template.no_template_label')
+  .d-none#noTemplateLabel data-text=t('file_template.no_template_label')
   .actions = render('shared/submit_button', f: f, object: CodeOcean::File.new)
diff --git a/app/views/comments/_form.html.erb b/app/views/comments/_form.html.erb
deleted file mode 100644
index e076a741..00000000
--- a/app/views/comments/_form.html.erb
+++ /dev/null
@@ -1,37 +0,0 @@
-<%= form_for(@comment) do |f| %>
-  <% if @comment.errors.any? %>
-    
-

<%= pluralize(@comment.errors.count, "error") %> prohibited this comment from being saved:

- -
    - <% @comment.errors.full_messages.each do |message| %> -
  • <%= message %>
  • - <% end %> -
-
- <% end %> - -
- <%= f.label :user_id %>
- <%= f.text_field :user_id %> -
-
- <%= f.label :file_id %>
- <%= f.text_field :file_id %> -
-
- <%= f.label :row %>
- <%= f.number_field :row %> -
-
- <%= f.label :column %>
- <%= f.number_field :column %> -
-
- <%= f.label :text %>
- <%= f.text_field :text %> -
-
- <%= f.submit %> -
-<% end %> diff --git a/app/views/comments/_form.html.slim b/app/views/comments/_form.html.slim new file mode 100644 index 00000000..96149d69 --- /dev/null +++ b/app/views/comments/_form.html.slim @@ -0,0 +1,33 @@ += form_for(@comment) do |f| + - if @comment.errors.any? + #error_explanation + h2 + = pluralize(@comment.errors.count, "error") + | prohibited this comment from being saved: + + ul + - @comment.errors.full_messages.each do |message| + li= message + + .field + = f.label :user_id + br/ + = f.text_field :user_id + .field + = f.label :file_id + br/ + = f.text_field :file_id + .field + = f.label :row + br/ + = f.number_field :row + .field + = f.label :column + br/ + = f.number_field :column + .field + = f.label :text + br/ + = f.text_field :text + .actions + = f.submit diff --git a/app/views/comments/edit.html.erb b/app/views/comments/edit.html.erb deleted file mode 100644 index 12ea7f96..00000000 --- a/app/views/comments/edit.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -

Editing comment

- -<%= render 'form' %> - -<%= link_to 'Show', @comment %> | -<%= link_to 'Back', comments_path %> diff --git a/app/views/comments/edit.html.slim b/app/views/comments/edit.html.slim new file mode 100644 index 00000000..dd0f49e3 --- /dev/null +++ b/app/views/comments/edit.html.slim @@ -0,0 +1,7 @@ +h1 Editing comment + += render 'form' + += link_to 'Show', @comment +| | += link_to 'Back', comments_path diff --git a/app/views/comments/index.html.erb b/app/views/comments/index.html.erb deleted file mode 100644 index f9f83ff8..00000000 --- a/app/views/comments/index.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -

Listing comments

- - - - - - - - - - - - - - - <% @comments.each do |comment| %> - - - - - - - - - - - <% end %> - -
UserFileRowColumnText
<%= comment.user %><%= comment.file %><%= comment.row %><%= comment.column %><%= comment.text %><%= link_to 'Show', comment %><%= link_to 'Edit', edit_comment_path(comment) %><%= link_to 'Destroy', comment, method: :delete, data: { confirm: 'Are you sure?' } %>
- -
- -<%= link_to 'New Comment', new_comment_path %> diff --git a/app/views/comments/index.html.slim b/app/views/comments/index.html.slim new file mode 100644 index 00000000..275a5042 --- /dev/null +++ b/app/views/comments/index.html.slim @@ -0,0 +1,24 @@ +h1 Listing comments + +table + thead + tr + th User + th File + th Row + th Column + th Text + th colspan="3" + tbody + - @comments.each do |comment| + tr + td= comment.user + td= comment.file + td= comment.row + td= comment.column + td= comment.text + td= link_to 'Show', comment + td= link_to 'Edit', edit_comment_path(comment) + td= link_to 'Destroy', comment, method: :delete, data: confirm: 'Are you sure?' +br/ += link_to 'New Comment', new_comment_path diff --git a/app/views/comments/new.html.erb b/app/views/comments/new.html.erb deleted file mode 100644 index 07a754a8..00000000 --- a/app/views/comments/new.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -

New comment

- -<%= render 'form' %> - -<%= link_to 'Back', comments_path %> diff --git a/app/views/comments/new.html.slim b/app/views/comments/new.html.slim new file mode 100644 index 00000000..1b985e65 --- /dev/null +++ b/app/views/comments/new.html.slim @@ -0,0 +1,5 @@ +h1 New comment + += render 'form' + += link_to 'Back', comments_path diff --git a/app/views/comments/show.html.erb b/app/views/comments/show.html.erb deleted file mode 100644 index e6955fa0..00000000 --- a/app/views/comments/show.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -

<%= notice %>

- -

- User: - <%= @comment.user %> -

- -

- File: - <%= @comment.file %> -

- -

- Row: - <%= @comment.row %> -

- -

- Column: - <%= @comment.column %> -

- -

- Text: - <%= @comment.text %> -

- -<%= link_to 'Edit', edit_comment_path(@comment) %> | -<%= link_to 'Back', comments_path %> diff --git a/app/views/comments/show.html.slim b/app/views/comments/show.html.slim new file mode 100644 index 00000000..24233d06 --- /dev/null +++ b/app/views/comments/show.html.slim @@ -0,0 +1,25 @@ +p#notice= notice + +p + strong User: + = @comment.user + +p + strong File: + = @comment.file + +p + strong Row: + = @comment.row + +p + strong Column: + = @comment.column + +p + strong Text: + = @comment.text + += link_to 'Edit', edit_comment_path(@comment) +| | += link_to 'Back', comments_path diff --git a/app/views/consumers/index.html.slim b/app/views/consumers/index.html.slim index c05581ed..b707c1f0 100644 --- a/app/views/consumers/index.html.slim +++ b/app/views/consumers/index.html.slim @@ -9,7 +9,7 @@ h1 = Consumer.model_name.human(count: 2) tbody - @consumers.each do |consumer| tr - td = consumer.name + td = link_to(consumer.name, consumer) 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) diff --git a/app/views/error_template_attributes/_form.html.slim b/app/views/error_template_attributes/_form.html.slim index 4fc28b02..72160cfd 100644 --- a/app/views/error_template_attributes/_form.html.slim +++ b/app/views/error_template_attributes/_form.html.slim @@ -9,8 +9,9 @@ .form-group = f.label(:regex) = f.text_field(:regex, class: 'form-control', required: true) - .help-block == t('error_templates.hints.signature') - .form-group - = f.check_box(:important) + .help-block.form-text == t('error_templates.hints.signature') + .form-check.form-group + label.form-check-label + = f.check_box(:important, class: 'form-check-input') = t('activerecord.attributes.error_template_attribute.important') .actions = render('shared/submit_button', f: f, object: @error_template_attribute) diff --git a/app/views/error_template_attributes/index.html.slim b/app/views/error_template_attributes/index.html.slim index 81d5cac9..268b1547 100644 --- a/app/views/error_template_attributes/index.html.slim +++ b/app/views/error_template_attributes/index.html.slim @@ -17,9 +17,10 @@ h1 = ErrorTemplateAttribute.model_name.human(count: 2) span class="fa fa-star" aria-hidden="true" - else span class="fa fa-star-o" aria-hidden="true" - td = error_template_attribute.key + td = link_to(error_template_attribute.key, error_template_attribute) td = error_template_attribute.description - td = error_template_attribute.regex + td + code = error_template_attribute.regex td = link_to(t('shared.show'), error_template_attribute) td = link_to(t('shared.edit'), edit_error_template_attribute_path(error_template_attribute)) td = link_to(t('shared.destroy'), error_template_attribute, data: {confirm: t('shared.confirm_destroy')}, method: :delete) diff --git a/app/views/error_template_attributes/show.html.slim b/app/views/error_template_attributes/show.html.slim index 2bdd01ca..5d6ef58b 100644 --- a/app/views/error_template_attributes/show.html.slim +++ b/app/views/error_template_attributes/show.html.slim @@ -2,7 +2,10 @@ h1 = @error_template_attribute = render('shared/edit_button', object: @error_template_attribute) -- [:key, :description, :regex, :important].each do |attribute| +- [:key, :description].each do |attribute| = row(label: "error_template_attribute.#{attribute}", value: @error_template_attribute.send(attribute)) += row(label: "error_template_attribute.key") do + code = @error_template_attribute.key += row(label: "error_template_attribute.important", value: @error_template_attribute.important) // todo: used by diff --git a/app/views/error_templates/_form.html.slim b/app/views/error_templates/_form.html.slim index d9716ce3..f912ec7b 100644 --- a/app/views/error_templates/_form.html.slim +++ b/app/views/error_templates/_form.html.slim @@ -9,12 +9,12 @@ .form-group = f.label(:signature) = f.text_field(:signature, class: 'form-control') - .help-block == t('error_templates.hints.signature') + .help-block.form-text == t('error_templates.hints.signature') .form-group = f.label(:description) = f.text_field(:description, class: 'form-control') .form-group = f.label(:hint) = f.text_field(:hint, class: 'form-control') - .help-block == t('error_templates.hints.hint_templates') + .help-block.form-text == t('error_templates.hints.hint_templates') .actions = render('shared/submit_button', f: f, object: @error_template) diff --git a/app/views/error_templates/index.html.slim b/app/views/error_templates/index.html.slim index f44b3f67..1f532a86 100644 --- a/app/views/error_templates/index.html.slim +++ b/app/views/error_templates/index.html.slim @@ -11,7 +11,7 @@ h1 = ErrorTemplate.model_name.human(count: 2) tbody - @error_templates.each do |error_template| tr - td = error_template.name + td = link_to(error_template.name, error_template) td = error_template.description td = link_to(error_template.execution_environment) td = link_to(t('shared.show'), error_template) diff --git a/app/views/error_templates/show.html.slim b/app/views/error_templates/show.html.slim index 9936ef7f..ae6ea2a4 100644 --- a/app/views/error_templates/show.html.slim +++ b/app/views/error_templates/show.html.slim @@ -4,10 +4,12 @@ h1 = row(label: 'error_template.name', value: @error_template.name) = row(label: 'exercise.execution_environment', value: link_to(@error_template.execution_environment)) -- [:signature, :description, :hint].each do |attribute| += row(label: "error_template.signature") do + code = @error_template.signature +- [:description, :hint].each do |attribute| = row(label: "error_template.#{attribute}", value: @error_template.send(attribute)) -h3 +h2.mt-4 = t 'error_templates.attributes' .table-responsive @@ -27,9 +29,10 @@ h3 span class="fa fa-star" aria-hidden="true" - else span class="fa fa-star-o" aria-hidden="true" - td = attribute.key + td = link_to(attribute.key, attribute) td = attribute.description - td = attribute.regex + td + code = attribute.regex td = link_to(t('shared.show'), attribute) td = link_to(t('shared.destroy'), attribute_error_template_url(:error_template_attribute_id => attribute.id), :method => :delete) @@ -37,4 +40,4 @@ h3 = collection_select({}, :error_template_attribute_id, ErrorTemplateAttribute.where.not(id: @error_template.error_template_attributes.select(:id).to_a).order('important DESC', :key), :id, :key, {include_blank: false}, class: '') - button.btn.btn-default = t('error_templates.add_attribute') + button.btn.btn-outline-primary = t('error_templates.add_attribute') diff --git a/app/views/execution_environments/_form.html.slim b/app/views/execution_environments/_form.html.slim index e1b02c0a..13ee252e 100644 --- a/app/views/execution_environments/_form.html.slim +++ b/app/views/execution_environments/_form.html.slim @@ -12,17 +12,17 @@ a.toggle-input data={text_initial: t('shared.new'), text_toggled: t('shared.back')} href='#' = t('shared.new') .original-input = 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') + .help-block.form-text == 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') + .help-block.form-text == t('.hints.exposed_ports') .form-group = f.label(:memory_limit) = f.number_field(:memory_limit, class: 'form-control', min: DockerClient::MINIMUM_MEMORY_LIMIT, value: f.object.memory_limit || DockerClient::DEFAULT_MEMORY_LIMIT) - .checkbox - label - = f.check_box(:network_enabled) + .form-check.mb-3 + label.form-check-label + = f.check_box(:network_enabled, class: 'form-check-input') = t('activerecord.attributes.execution_environment.network_enabled') .form-group = f.label(:permitted_execution_time) @@ -33,11 +33,11 @@ .form-group = f.label(:run_command) = f.text_field(:run_command, class: 'form-control', placeholder: 'command %{filename}', required: true) - .help-block == t('.hints.command') + .help-block.form-text == 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') + .help-block.form-text == t('.hints.command') .form-group = f.label(:testing_framework) = f.select(:testing_framework, @testing_framework_adapters, {include_blank: true}, class: 'form-control') diff --git a/app/views/execution_environments/index.html.slim b/app/views/execution_environments/index.html.slim index dc30898f..20e99620 100644 --- a/app/views/execution_environments/index.html.slim +++ b/app/views/execution_environments/index.html.slim @@ -15,7 +15,7 @@ h1 = ExecutionEnvironment.model_name.human(count: 2) tbody - @execution_environments.each do |execution_environment| tr - td = execution_environment.name + td = link_to(execution_environment.name, execution_environment) td = link_to(execution_environment.author, execution_environment.author) td = execution_environment.pool_size td = execution_environment.memory_limit diff --git a/app/views/execution_environments/show.html.slim b/app/views/execution_environments/show.html.slim index da9fd9c8..6d917a61 100644 --- a/app/views/execution_environments/show.html.slim +++ b/app/views/execution_environments/show.html.slim @@ -5,7 +5,10 @@ h1 = row(label: 'execution_environment.name', value: @execution_environment.name) = row(label: 'execution_environment.user', value: link_to(@execution_environment.author, @execution_environment.author)) = row(label: 'execution_environment.file_type', value: @execution_environment.file_type.present? ? link_to(@execution_environment.file_type, @execution_environment.file_type) : nil) -- [:docker_image, :exposed_ports, :memory_limit, :network_enabled, :permitted_execution_time, :pool_size, :run_command, :test_command].each do |attribute| +- [:docker_image, :exposed_ports, :memory_limit, :network_enabled, :permitted_execution_time, :pool_size].each do |attribute| = row(label: "execution_environment.#{attribute}", value: @execution_environment.send(attribute)) +- [:run_command, :test_command].each do |attribute| + = row(label: "execution_environment.#{attribute}") do + code = @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)) diff --git a/app/views/execution_environments/statistics.html.slim b/app/views/execution_environments/statistics.html.slim index 66ee091d..64d046b0 100644 --- a/app/views/execution_environments/statistics.html.slim +++ b/app/views/execution_environments/statistics.html.slim @@ -14,7 +14,7 @@ h1 = @execution_environment - if wts then average_time = wts["average_time"] else 0 - if wts then stddev_time = wts["stddev_time"] else 0 tr - td = link_to exercise.title, controller: "exercises", action: "statistics", id: exercise.id + td = link_to exercise.title, controller: "exercises", action: "statistics", id: exercise.id, 'data-turbolinks' => "false" td = us["users"] td = us["average_score"].to_f.round(4) td = us["maximum_score"].to_f.round(2) diff --git a/app/views/exercise_collections/_add_exercise_modal.slim b/app/views/exercise_collections/_add_exercise_modal.slim index 62080f4f..e0880b58 100644 --- a/app/views/exercise_collections/_add_exercise_modal.slim +++ b/app/views/exercise_collections/_add_exercise_modal.slim @@ -2,7 +2,8 @@ form#exercise-selection .form-group - span.label = t('activerecord.attributes.exercise_collections.exercises') + span.badge = t('activerecord.attributes.exercise_collections.exercises') + .mb-2 = collection_select({}, :exercise_ids, exercises, :id, :title, {}, {id: 'add-exercise-list', class: 'form-control', multiple: true}) button.btn.btn-primary#add-exercises = t('exercise_collections.form.add_exercises') diff --git a/app/views/exercise_collections/_form.html.slim b/app/views/exercise_collections/_form.html.slim index 9c765071..07fc874f 100644 --- a/app/views/exercise_collections/_form.html.slim +++ b/app/views/exercise_collections/_form.html.slim @@ -3,9 +3,10 @@ .form-group = f.label(t('activerecord.attributes.exercise_collections.name')) = f.text_field(:name, class: 'form-control', required: true) - .form-group - = f.label(t('activerecord.attributes.exercise_collections.use_anomaly_detection')) - = f.check_box(:use_anomaly_detection, {class: 'form-control'}) + .form-check.form-group + label.form-check-label + = f.check_box(:use_anomaly_detection, class: 'form-check-input') + = t('activerecord.attributes.exercise_collections.use_anomaly_detection') .form-group = f.label(t('activerecord.attributes.exercise_collections.user')) = f.collection_select(:user_id, InternalUser.order(:name), :id, :name, {}, {class: 'form-control'}) @@ -23,13 +24,13 @@ td span.fa.fa-bars td = item.exercise.title - td = link_to(t('shared.show'), item.exercise) + td = link_to(t('shared.show'), item.exercise, 'data-turbolinks' => "false") td a.remove-exercise href='#' = t('shared.destroy') - .hidden + .d-none = f.collection_select(:exercise_ids, Exercise.all, :id, :title, {}, {id: 'exercise-select', class: 'form-control', multiple: true}) .exercise-actions - button.btn.btn-primary type='button' data-toggle='modal' data-target='#add-exercise-modal' = t('exercise_collections.form.add_exercises') + button.btn.btn-outline-primary type='button' data-toggle='modal' data-target='#add-exercise-modal' = t('exercise_collections.form.add_exercises') button.btn.btn-secondary#sort-button type='button' = t('exercise_collections.form.sort_by_title') .actions = render('shared/submit_button', f: f, object: @exercise_collection) diff --git a/app/views/exercise_collections/show.html.slim b/app/views/exercise_collections/show.html.slim index c55d1d6b..4fa6a8ac 100644 --- a/app/views/exercise_collections/show.html.slim +++ b/app/views/exercise_collections/show.html.slim @@ -7,7 +7,7 @@ h1 = row(label: 'exercise_collections.use_anomaly_detection', value: @exercise_collection.use_anomaly_detection) = row(label: 'exercise_collections.updated_at', value: @exercise_collection.updated_at) -h4 = t('activerecord.attributes.exercise_collections.exercises') +h4.mt-4 = t('activerecord.attributes.exercise_collections.exercises') .table-responsive#exercise-list table.table thead @@ -24,5 +24,5 @@ h4 = t('activerecord.attributes.exercise_collections.exercises') td = exercise_collection_item.position td = link_to(exercise.title, exercise) td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) - td = exercise.user.name - td = link_to(t('shared.statistics'), statistics_exercise_path(exercise)) + td = link_to_if(exercise.user && policy(exercise.user).show?, exercise.user.name, exercise.user) + td = link_to(t('shared.statistics'), statistics_exercise_path(exercise), 'data-turbolinks' => "false") diff --git a/app/views/exercise_collections/statistics.html.slim b/app/views/exercise_collections/statistics.html.slim index 60c49a01..74c9f49d 100644 --- a/app/views/exercise_collections/statistics.html.slim +++ b/app/views/exercise_collections/statistics.html.slim @@ -6,7 +6,7 @@ h1 = @exercise_collection = row(label: 'exercises.statistics.average_worktime', value: @exercise_collection.average_working_time.round(3).to_s + 's') #graph - #data.hidden(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.collection_statistics) data-average-working-time=@exercise_collection.average_working_time) + #data.d-none(data-working-times=ActiveSupport::JSON.encode(@exercise_collection.collection_statistics) data-average-working-time=@exercise_collection.average_working_time) #legend - {time: t('exercises.statistics.average_worktime'), min: 'min. anomaly threshold', diff --git a/app/views/exercises/_code_field.html.slim b/app/views/exercises/_code_field.html.slim index bb2a806f..8fd19eec 100644 --- a/app/views/exercises/_code_field.html.slim +++ b/app/views/exercises/_code_field.html.slim @@ -3,5 +3,5 @@ |   a.toggle-input data={text_initial: t('shared.upload_file'), text_toggled: t('shared.back')} href='#' = t('shared.upload_file') = form.text_area(attribute, class: 'code-field form-control', rows: 16, style: "display:none;") - = form.file_field(attribute, class: 'alternative-input form-control', disabled: true) + = form.file_field(attribute, class: 'alternative-input form-control-file', disabled: true) = render partial: 'editor_edit', locals: { exercise: @exercise } diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index f741cb56..44fc0aec 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -6,8 +6,7 @@ - hide_rfc_button = @hide_rfc_button || false #editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') 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 data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path(@exercise) div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files) - div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) - div id='frames' class='editor-col' + div.editor-col.col.p-0 id='frames' #editor-buttons.btn-group.enforce-bottom-margin = render('editor_button', disabled: true, icon: 'fa fa-ban', id: 'dummy', label: t('exercises.editor.dummy')) = render('editor_button', icon: 'fa fa-desktop', id: 'render', label: t('exercises.editor.render')) @@ -24,6 +23,7 @@ = t('exercises.editor.lastsaved') span button style="display:none" id="autosave" + div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') diff --git a/app/views/exercises/_editor_button.html.slim b/app/views/exercises/_editor_button.html.slim index ae69529e..3c14f0b2 100644 --- a/app/views/exercises/_editor_button.html.slim +++ b/app/views/exercises/_editor_button.html.slim @@ -1,4 +1,4 @@ -button.btn class=local_assigns.fetch(:classes, 'btn-primary btn-sm') *local_assigns.fetch(:data, {}) disabled=local_assigns.fetch(:disabled, false) id=id title=local_assigns[:title] type='button' +button.btn class=local_assigns.fetch(:classes, 'btn-primary') *local_assigns.fetch(:data, {}) disabled=local_assigns.fetch(:disabled, false) id=id title=local_assigns[:title] type='button' i.fa.fa-circle-o-notch.fa-spin - i class=icon + i class=(label.present? ? icon : "#{icon} m-0") = label diff --git a/app/views/exercises/_editor_edit.html.slim b/app/views/exercises/_editor_edit.html.slim index 83f27d68..18ea0109 100644 --- a/app/views/exercises/_editor_edit.html.slim +++ b/app/views/exercises/_editor_edit.html.slim @@ -1,5 +1,5 @@ -#editor-edit.panel-group.row.original-input data-exercise-id=@exercise.id +#editor-edit.original-input data-exercise-id=@exercise.id #frames .edit-frame - .editor-content.hidden - .editor \ No newline at end of file + .editor-content.d-none + .editor.allow_ace_tooltip \ No newline at end of file diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index c51dd855..32612828 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -1,18 +1,18 @@ -div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') - = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'', title:t('exercises.editor.expand_action_sidebar')) +div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'd-none') + = render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'', title:t('exercises.editor.expand_action_sidebar')) - if @exercise.allow_file_creation and not @exercise.hide_file_tree? - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-cause' => 'file', :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus', id: 'create-file-collapsed', label:'', title: t('exercises.editor.create_file')) + = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-cause' => 'file', :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus', id: 'create-file-collapsed', label:'', title: t('exercises.editor.create_file')) - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download')) - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over')) - - if !@course_token.blank? - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum')) + = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download')) + = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over')) + //- if !@course_token.blank? + = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum')) -div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') - = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) +div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'd-none' : '') + = render('editor_button', classes: 'btn-block btn-primary btn', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) - div class=(@exercise.hide_file_tree ? 'hidden' : '') + div class=(@exercise.hide_file_tree ? 'd-none' : '') hr #files data-entries=FileTree.new(files).to_js_tree @@ -20,11 +20,11 @@ div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') hr - if @exercise.allow_file_creation and not @exercise.hide_file_tree? - = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-cause' => 'file'}, icon: 'fa fa-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: 'fa fa-times', id: 'destroy-file', label: t('exercises.editor.destroy_file')) + = render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-cause' => 'file'}, icon: 'fa fa-plus', id: 'create-file', label: t('exercises.editor.create_file')) + = render('editor_button', classes: 'btn-block btn-warning btn', data: {:'data-cause' => 'file', :'data-message-confirm' => t('shared.confirm_destroy')}, icon: 'fa fa-times', id: 'destroy-file', label: t('exercises.editor.destroy_file')) - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) - = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over')) + = render('editor_button', classes: 'btn-block btn-primary btn enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) + = render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over')) //- if !@course_token.blank? .input-group.enforce-top-margin diff --git a/app/views/exercises/_editor_frame.html.slim b/app/views/exercises/_editor_frame.html.slim index eff1541c..8d79ed43 100644 --- a/app/views/exercises/_editor_frame.html.slim +++ b/app/views/exercises/_editor_frame.html.slim @@ -11,5 +11,5 @@ - else = link_to(file.native_file.file.name_with_extension, file.native_file.url) - else - .editor-content.hidden data-file-id=file.ancestor_id = file.content + .editor-content.d-none data-file-id=file.ancestor_id = file.content .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 data-allow-auto-completion=exercise.allow_auto_completion.to_s data-id=file.id \ No newline at end of file diff --git a/app/views/exercises/_editor_output.html.slim b/app/views/exercises/_editor_output.html.slim index f79cc3d1..729ec129 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -1,58 +1,60 @@ div id='output_sidebar_collapsed' - = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: t('exercises.editor.expand_output_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') -div id='output_sidebar_uncollapsed' class='hidden col-sm-12 enforce-bottom-margin' data-message-no-output=t('exercises.implement.no_output') + = render('editor_button', classes: 'btn-block btn-primary btn', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'left'}, title: t('exercises.editor.expand_output_sidebar'), icon: 'fa fa-plus-square', id: 'toggle-sidebar-output-collapsed', label: '') +div.h-100 id='output_sidebar_uncollapsed' class='d-none col-sm-12 enforce-bottom-margin' data-message-no-output=t('exercises.implement.no_output') .row - = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'toggle-sidebar-output', label: t('exercises.editor.collapse_output_sidebar')) + = render('editor_button', classes: 'btn-block btn-primary btn', icon: 'fa fa-minus-square', id: 'toggle-sidebar-output', label: t('exercises.editor.collapse_output_sidebar')) - div.enforce-big-top-margin.hidden id='score_div' - #results - h2 = t('exercises.implement.results') - p.test-count == t('exercises.implement.test_count', count: 0) - ul.list-unstyled - ul#dummies.hidden.list-unstyled - li.panel.panel-default - .panel-heading - h3.panel-title == t('exercises.implement.file', filename: '', number: 0) - .panel-body - = row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) - = row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) - = row(label: 'exercises.implement.feedback') - = row(label: 'exercises.implement.error_messages') - /= row(label: 'exercises.implement.output', value: link_to(t('shared.show'), '#')) - #score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score) - h4 - span == "#{t('activerecord.attributes.submission.score')}: " - span.score - .progress - .progress-bar role='progressbar' + div.position-absolute.d-flex.mb-1.w-100 style="overflow: auto; left: 0; bottom: 0; height: calc(100% - 3rem);" + div.w-100 + div.enforce-big-top-margin.d-none id='score_div' + #results + h2 = t('exercises.implement.results') + p.test-count == t('exercises.implement.test_count', count: 0) + ul.list-unstyled + ul#dummies.d-none.list-unstyled + li.card.mt-2 + .card-header.py-2 + h5.card-title.m-0 == t('exercises.implement.file', filename: '', number: 0) + .card-body.bg-white.text-dark + = row(label: 'exercises.implement.passed_tests', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) + = row(label: 'activerecord.attributes.submission.score', value: t('shared.out_of', maximum_value: 0, value: 0).html_safe) + = row(label: 'exercises.implement.feedback') + = row(label: 'exercises.implement.error_messages') + /= row(label: 'exercises.implement.output', value: link_to(t('shared.show'), '#')) + #score data-maximum-score=@exercise.maximum_score data-score=@submission.try(:score) + h4 + span == "#{t('activerecord.attributes.submission.score')}: " + span.score + .progress + .progress-bar role='progressbar' - br - - if lti_outcome_service?(@exercise.id, external_user_id, consumer_id) - p.text-center = render('editor_button', classes: 'btn-lg btn-success', data: {:'data-url' => submit_exercise_path(@exercise)}, icon: 'fa fa-send', id: 'submit', label: t('exercises.editor.submit')) - - else - p.text-center = render('editor_button', classes: 'btn-lg btn-warning-outline', data: {:'data-placement' => 'bottom', :'data-tooltip' => true}, icon: 'fa fa-clock-o', id: 'submit_outdated', label: t('exercises.editor.exercise_deadline_passed'), title: t('exercises.editor.tooltips.exercise_deadline_passed')) - hr - - div.enforce-big-top-margin - #turtlediv - canvas#turtlecanvas.hidden width=400 height=400 - div.enforce-big-top-margin - #hint - .panel.panel-warning - .panel-heading = t('exercises.implement.hint') - .panel-body - div.enforce-big-top-margin - #prompt.input-group.hidden - span.input-group-addon data-prompt=t('exercises.editor.input') = t('exercises.editor.input') - input#prompt-input.form-control type='text' - span.input-group-btn - button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send') - #error-hints - .heading = t('exercises.implement.error_hints.heading') - ul.body - #output - pre = t('exercises.implement.no_output_yet') - - if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] - #flowrHint.panel.panel-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab' - .panel-heading = t('exercises.implement.flowr.heading') - .panel-body + br + - if lti_outcome_service?(@exercise.id, external_user_id, consumer_id) + p.text-center = render('editor_button', classes: 'btn-lg btn-success', data: {:'data-url' => submit_exercise_path(@exercise)}, icon: 'fa fa-send', id: 'submit', label: t('exercises.editor.submit')) + - else + p.text-center = render('editor_button', classes: 'btn-lg btn-secondary disabled', data: {:'data-placement' => 'bottom', :'data-tooltip' => true}, icon: 'fa fa-clock-o', id: 'submit_outdated', label: t('exercises.editor.exercise_deadline_passed'), title: t('exercises.editor.tooltips.exercise_deadline_passed')) + hr + div.enforce-big-top-margin + #turtlediv + canvas#turtlecanvas.d-none width=400 height=400 + div.enforce-big-top-margin + #hint + .card.bg-warning.text-white + .card-header = t('exercises.implement.hint') + .card-body + div.enforce-big-top-margin + #prompt.input-group.d-none + div.input-group-prepend + span.input-group-text data-prompt=t('exercises.editor.input') = t('exercises.editor.input') + input#prompt-input.form-control type='text' + span.input-group-btn + button#prompt-submit.btn.btn-primary type="button" = t('exercises.editor.send') + #error-hints + .heading = t('exercises.implement.error_hints.heading') + ul.body + #output.mt-2 + pre = t('exercises.implement.no_output_yet') + - if CodeOcean::Config.new(:code_ocean).read[:flowr][:enabled] + #flowrHint.card.text-white.bg-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab' + .card-header = t('exercises.implement.flowr.heading') + .card-body diff --git a/app/views/exercises/_file_form.html.slim b/app/views/exercises/_file_form.html.slim index 57f43d43..d85751fc 100644 --- a/app/views/exercises/_file_form.html.slim +++ b/app/views/exercises/_file_form.html.slim @@ -1,41 +1,42 @@ - id = f.object.id -li.panel.panel-default - .panel-heading role="tab" id="heading" - a.file-heading data-toggle="collapse" href="#collapse#{id}" +li.card.mt-2 + .card-header role="tab" id="heading" + a.file-heading.collapsed data-toggle="collapse" href="#collapse#{id}" div.clearfix role="button" + i class="fa" aria-hidden="true" span = f.object.name - .panel-collapse.collapse class=('in' if f.object.name.nil?) id="collapse#{id}" role="tabpanel" - .panel-body + .card-collapse.collapse class=('in' if f.object.name.nil?) id="collapse#{id}" role="tabpanel" + .card-body - if policy(f.object).destroy? .clearfix - .btn.btn-warning.btn-sm.pull-right.delete-file data-file-url=code_ocean_file_path(id) = t('shared.destroy') + .btn.btn-warning.btn-sm.float-right.delete-file data-file-url=code_ocean_file_path(id) = t('shared.destroy') .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') + .help-block.form-text = 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) + .form-check + label.form-check-label + = f.check_box(:hidden, class: 'form-check-input') = t('activerecord.attributes.file.hidden') - .checkbox - label - = f.check_box(:read_only) + .form-check.mb-3 + label.form-check-label + = f.check_box(:read_only, class: 'form-check-input') = t('activerecord.attributes.file.read_only') .test-related-fields style="display: #{f.object.teacher_defined_test? ? 'initial' : 'none'};" .form-group = f.label(:name, t('activerecord.attributes.file.feedback_message')) = f.text_area(:feedback_message, class: 'form-control', maxlength: 255) - .help-block = t('.hints.feedback_message') + .help-block.form-text = t('.hints.feedback_message') .form-group = f.label(:role, t('activerecord.attributes.file.weight')) = f.number_field(:weight, class: 'form-control', min: 1, step: 'any') diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index 646c359a..b6d0980b 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -1,14 +1,14 @@ - execution_environments = ExecutionEnvironment.where('file_type_id IS NOT NULL').select(:file_type_id, :id) - file_types = FileType.where('file_extension IS NOT NULL').select(:file_extension, :id) -= form_for(@exercise, data: {execution_environments: execution_environments, file_types: file_types}, multipart: true) do |f| += form_for(@exercise, data: {execution_environments: execution_environments, file_types: file_types}, multipart: true, builder: PagedownFormBuilder) do |f| = render('shared/form_errors', object: @exercise) .form-group = f.label(:title) = f.text_field(:title, class: 'form-control', required: true) .form-group = f.label(:description) - = f.pagedown_editor :description + = f.pagedown :description, input_html: { preview: true, rows: 10 } .form-group = f.label(:execution_environment_id) = f.collection_select(:execution_environment_id, @execution_environments, :id, :name, {}, class: 'form-control') @@ -16,34 +16,34 @@ = f.label(:instructions) = f.hidden_field(:instructions) .form-control.markdown - .checkbox - label - = f.check_box(:public) + .form-check + label.form-check-label + = f.check_box(:public, class: 'form-check-input') = t('activerecord.attributes.exercise.public') - .checkbox - label - = f.check_box(:hide_file_tree) + .form-check + label.form-check-label + = f.check_box(:hide_file_tree, class: 'form-check-input') = t('activerecord.attributes.exercise.hide_file_tree') - .checkbox - label - = f.check_box(:allow_file_creation) + .form-check + label.form-check-label + = f.check_box(:allow_file_creation, class: 'form-check-input') = t('activerecord.attributes.exercise.allow_file_creation') - .checkbox - label - = f.check_box(:allow_auto_completion) + .form-check.mb-3 + label.form-check-label + = f.check_box(:allow_auto_completion, class: 'form-check-input') = t('activerecord.attributes.exercise.allow_auto_completion') .form-group = f.label(t('activerecord.attributes.exercise.difficulty')) - = f.number_field :expected_difficulty, in: 1..10, step: 1 + = f.number_field :expected_difficulty, in: 1..10, step: 1, class: 'form-control' h2 = t('exercises.form.tags') - ul.list-unstyled.panel-group - li.panel.panel-default - .panel-heading role="tab" id="heading" + ul.list-unstyled.card-group + li.card + .card-header role="tab" id="heading" a.file-heading data-toggle="collapse" href="#tag-collapse" div.clearfix role="button" span = t('exercises.form.click_to_collapse') - .panel-collapse.collapse id="tag-collapse" role="tabpanel" + .card-collapse.collapse id="tag-collapse" role="tabpanel" .table-responsive table.table#tags-table thead @@ -55,15 +55,15 @@ tr td = b.check_box td = b.object.tag.name - td = number_field "tag_factors[#{b.object.tag.id}]", :factor, :value => b.object.factor, in: 1..10, step: 1 + td = number_field "tag_factors[#{b.object.tag.id}]", :factor, :value => b.object.factor, in: 1..10, step: 1, class: 'form-control-sm' h2 = t('activerecord.attributes.exercise.files') - ul#files.list-unstyled.panel-group + ul#files.list-unstyled = f.fields_for :files do |files_form| = render('file_form', f: files_form) - a#add-file.btn.btn-default.btn-sm.pull-right href='#' = t('.add_file') - ul#dummies.hidden = f.fields_for(:files, CodeOcean::File.new, child_index: 'index') do |files_form| + a#add-file.btn.btn-secondary.btn-sm.float-right href='#' = t('.add_file') + ul#dummies.d-none = f.fields_for(:files, CodeOcean::File.new, child_index: 'index') do |files_form| = render('file_form', f: files_form) .actions = render('shared/submit_button', f: f, object: @exercise) \ No newline at end of file diff --git a/app/views/exercises/_request_comment_dialogcontent.html.slim b/app/views/exercises/_request_comment_dialogcontent.html.slim index 8fb71781..70405ea0 100644 --- a/app/views/exercises/_request_comment_dialogcontent.html.slim +++ b/app/views/exercises/_request_comment_dialogcontent.html.slim @@ -2,7 +2,7 @@ h5#rfc_intervention_text style='display: none;' = t('exercises.implement.rfc_int h5 = t('exercises.implement.comment.question') -textarea.form-control#question(style='resize:none;') +textarea.form-control.flex-grow-1#question(style='resize:none;') p = '' / data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action. / But if we use this button, it will work since the correct cause is supplied diff --git a/app/views/exercises/external_users/statistics.html.slim b/app/views/exercises/external_users/statistics.html.slim index 5d91716a..e89627ed 100644 --- a/app/views/exercises/external_users/statistics.html.slim +++ b/app/views/exercises/external_users/statistics.html.slim @@ -10,21 +10,21 @@ h1 = "#{@exercise} (external user #{@external_user})" - file_types.add(ActiveSupport::JSON.encode(file.file_type)) - all_files.push(submission.files) - .hidden#data data-submissions=ActiveSupport::JSON.encode(@submissions) data-files=ActiveSupport::JSON.encode(all_files) data-file-types=ActiveSupport::JSON.encode(file_types) + .d-none#data data-submissions=ActiveSupport::JSON.encode(@submissions) data-files=ActiveSupport::JSON.encode(all_files) data-file-types=ActiveSupport::JSON.encode(file_types) #stats-editor.row - index = 0 - all_files.each do |files| - .files class=(@exercise.hide_file_tree ? 'hidden col-sm-3' : 'col-sm-3') data-index=index data-entries=FileTree.new(files).to_js_tree + .files class=(@exercise.hide_file_tree ? 'd-none col-sm-3' : 'col-sm-3') data-index=index data-entries=FileTree.new(files).to_js_tree - index += 1 div class=(@exercise.hide_file_tree ? 'col-sm-12' : 'col-sm-9') #current-file.editor .flex-container - button.btn.btn-default id='play-button' + button.btn.btn-secondary id='play-button' span.fa.fa-play #submissions-slider.flex-item - input type='range' orient='horizontal' list='datapoints' min=0 max=@submissions.length-1 value=0 + input type='range' orient='horizontal' list='datapoints' min=0 max=@submissions.length-1 value=0 style="width: 100%" datalist#datapoints - index=0 - @submissions.each do |submission| @@ -59,7 +59,7 @@ h1 = "#{@exercise} (external user #{@external_user})" td = td = @working_times_until[index] if index > 0 p = t('.addendum') - .hidden#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until); + .d-none#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until); div#progress_chart.col-lg-12 .graph-functions-2 diff --git a/app/views/exercises/feedback.html.slim b/app/views/exercises/feedback.html.slim index 508b1478..5090d0e2 100644 --- a/app/views/exercises/feedback.html.slim +++ b/app/views/exercises/feedback.html.slim @@ -8,17 +8,17 @@ h1 = link_to(@exercise, exercise_path(@exercise)) - if @feedbacks.nil? or @feedbacks.size == 0 .no-feedback = t('user_exercise_feedback.no_feedback') - ul.list-unstyled.panel-group + ul.list-unstyled - @feedbacks.each do |feedback| - li.panel.panel-default - .panel-heading role="tab" id="heading" + li.card.mt-2 + .card-header role="tab" id="heading" div.clearfix.feedback-header span.username = link_to(feedback.user.name, statistics_external_user_exercise_path(id: @exercise.id, external_user_id: feedback.user.id)) - if feedback.anomaly_notification i class="fa fa-envelope-o" data-placement="top" data-toggle="tooltip" data-container="body" title=feedback.anomaly_notification.reason span.date = feedback.created_at - .panel-collapse role="tabpanel" - .panel-body.feedback + .card-collapse role="tabpanel" + .card-body.feedback .text = feedback.feedback_text .difficulty = "#{t('user_exercise_feedback.difficulty')} #{feedback.difficulty}" if feedback.difficulty .worktime = "#{t('user_exercise_feedback.working_time')} #{feedback.user_estimated_worktime}" if feedback.user_estimated_worktime diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index a243bd91..1e872cbf 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -2,13 +2,13 @@ #editor-column.col-md-12 .exercise.clearfix div - span.badge.pull-right.score + span.badge.badge-pill.badge-primary.float-right.score h1 id="exercise-headline" i class="fa fa-chevron-down" id="description-symbol" = @exercise.title - #description-panel.lead.description-panel + #description-card.lead.description-card = render_markdown(@exercise.description) a#toggle href="#" data-show=t('shared.show') data-hide=t('shared.hide') = t('shared.hide') diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim index 47198fbb..35da19cb 100644 --- a/app/views/exercises/index.html.slim +++ b/app/views/exercises/index.html.slim @@ -9,45 +9,43 @@ h1 = Exercise.model_name.human(count: 2) = f.search_field(:title_cont, class: 'form-control', placeholder: t('activerecord.attributes.exercise.title')) .table-responsive - table.table + table.table.mt-4 thead tr - th = sort_link(@search, :title, t('activerecord.attributes.exercise.title')) - th = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment')) - th = t('.test_files') - th = t('activerecord.attributes.exercise.maximum_score') - th = t('activerecord.attributes.exercise.tags') - th = t('activerecord.attributes.exercise.difficulty') - th + th.p-1 = sort_link(@search, :title, t('activerecord.attributes.exercise.title')) + th.p-1 = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment')) + th.p-1 = t('.test_files') + th.p-1 = t('activerecord.attributes.exercise.maximum_score') + th.p-1 = t('activerecord.attributes.exercise.tags') + th.p-1 = t('activerecord.attributes.exercise.difficulty') + th.p-1 = t('activerecord.attributes.exercise.public') - if policy(Exercise).batch_update? br span.batch = link_to(t('shared.batch_update'), '#', 'data-text' => t('shared.update', model: t('activerecord.models.exercise.other'))) - th colspan=6 = t('shared.actions') + th.p-1 colspan=6 = t('shared.actions') tbody - @exercises.each do |exercise| tr data-id=exercise.id - td = exercise.title - td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) - td = exercise.files.teacher_defined_tests.count - td = exercise.maximum_score - td = exercise.exercise_tags.count - td = exercise.expected_difficulty - td.public data-value=exercise.public? = symbol_for(exercise.public?) - td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit? - td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement? - td = link_to(t('shared.statistics'), statistics_exercise_path(exercise)) if policy(exercise).statistics? + td.p-1.pt-2 = link_to(exercise.title, exercise, 'data-turbolinks' => "false") if policy(exercise).show? + td.p-1.pt-2 = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) + td.p-1.pt-2 = exercise.files.teacher_defined_tests.count + td.p-1.pt-2 = exercise.maximum_score + td.p-1.pt-2 = exercise.exercise_tags.count + td.p-1.pt-2 = exercise.expected_difficulty + td.p-1.pt-2.public data-value=exercise.public? = symbol_for(exercise.public?) + td.p-1.pt-2 = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit? + td.p-1.pt-2 = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement? + td.p-1.pt-2 = link_to(t('shared.statistics'), statistics_exercise_path(exercise), 'data-turbolinks' => "false") if policy(exercise).statistics? - td + td.p-1 .btn-group - button.btn.btn-primary-outline.btn-xs.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button') - span.caret - span.sr-only Toggle Dropdown - ul.dropdown-menu.pull-right role="menu" - li = link_to(t('shared.show'), exercise) if policy(exercise).show? - li = link_to(t('activerecord.models.user_exercise_feedback.other'), feedback_exercise_path(exercise)) if policy(exercise).feedback? - li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(exercise).destroy? - li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(exercise).clone? + button.btn.btn-outline-primary.btn-sm.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button') + ul.dropdown-menu.float-right role="menu" + li = link_to(t('shared.show'), exercise, 'data-turbolinks' => "false", class: 'dropdown-item') if policy(exercise).show? + li = link_to(t('activerecord.models.user_exercise_feedback.other'), feedback_exercise_path(exercise), class: 'dropdown-item') if policy(exercise).feedback? + li = link_to(t('shared.destroy'), exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete, class: 'dropdown-item') if policy(exercise).destroy? + li = link_to(t('.clone'), clone_exercise_path(exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post, class: 'dropdown-item') if policy(exercise).clone? = render('shared/pagination', collection: @exercises) p = render('shared/new_button', model: Exercise) diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index e3dfc7d5..ee503c99 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -1,6 +1,9 @@ - content_for :head do - = javascript_include_tag('http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js') - = stylesheet_link_tag('http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css') + // Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326. + Otherwise, code might not be highlighted correctly (race condition) + meta name='turbolinks-visit-control' content='reload' + = javascript_pack_tag('highlight', 'data-turbolinks-track': true) + = stylesheet_pack_tag('highlight', media: 'all', 'data-turbolinks-track': true) h1 = @exercise @@ -9,7 +12,7 @@ h1 = row(label: 'exercise.title', value: @exercise.title) = row(label: 'exercise.user', value: link_to_if(policy(@exercise.author).show?, @exercise.author, @exercise.author)) -= row(label: 'exercise.description', value: render_markdown(@exercise.description)) += row(label: 'exercise.description', value: render_markdown(@exercise.description), class: 'm-0') = row(label: 'exercise.execution_environment', value: link_to_if(policy(@exercise.execution_environment).show?, @exercise.execution_environment, @exercise.execution_environment)) /= row(label: 'exercise.instructions', value: render_markdown(@exercise.instructions)) = row(label: 'exercise.maximum_score', value: @exercise.maximum_score) @@ -17,24 +20,23 @@ h1 = row(label: 'exercise.hide_file_tree', value: @exercise.hide_file_tree?) = row(label: 'exercise.allow_file_creation', value: @exercise.allow_file_creation?) = row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?) -= row(label: 'exercise.embedding_parameters') do - = content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise)) = row(label: 'exercise.difficulty', value: @exercise.expected_difficulty) = row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", ")) += row(label: 'exercise.embedding_parameters', class: 'mb-4') do + = content_tag(:input, nil, class: 'form-control mb-4', readonly: true, value: embedding_parameters(@exercise)) -h2 = t('activerecord.attributes.exercise.files') +h2.mt-4 = t('activerecord.attributes.exercise.files') -ul.list-unstyled.panel-group#files - - @exercise.files.order('name').each do |file| - li.panel.panel-default - .panel-heading role="tab" id="heading" - a.file-heading data-toggle="collapse" data-parent="#files" href=".collapse#{file.id}" +ul.list-unstyled#files + - @exercise.files.each do |file| + li.card.mt-2 + .card-header role="tab" id="heading" + a.file-heading.collapsed data-toggle="collapse" data-parent="#files" href=".collapse#{file.id}" div.clearfix role="button" + i class="fa" aria-hidden="true" span = file.name_with_extension - // probably set an icon here that shows that the rows can be collapsed - //span.pull-right.collapse.in class="collapse#{file.id}" ☼ - .panel-collapse.collapse class="collapse#{file.id}" role="tabpanel" - .panel-body + .card-collapse.collapse class="collapse#{file.id}" role="tabpanel" + .card-body - if policy(file).destroy? - .clearfix = link_to(t('shared.destroy'), file, class:'btn btn-warning btn-sm pull-right', data: {confirm: t('shared.confirm_destroy')}, method: :delete) + .clearfix = link_to(t('shared.destroy'), file, class:'btn btn-warning btn-sm float-right', data: {confirm: t('shared.confirm_destroy')}, method: :delete) = render('shared/file', file: file) diff --git a/app/views/exercises/statistics.html.slim b/app/views/exercises/statistics.html.slim index 211cc862..5455162b 100644 --- a/app/views/exercises/statistics.html.slim +++ b/app/views/exercises/statistics.html.slim @@ -1,4 +1,8 @@ -script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js" +- content_for :head do + // Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326. + Otherwise, code might not be highlighted correctly (race condition) + meta name='turbolinks-visit-control' content='reload' + = javascript_pack_tag('d3-tip', 'data-turbolinks-track': true) h1 = @exercise = row(label: '.participants', value: @exercise.users.distinct.count) @@ -28,12 +32,12 @@ h1 = @exercise -working_time = @exercise.average_working_time_for(user.id) or 0 -working_time_array.push working_time hr - .hidden#data data-working-time=ActiveSupport::JSON.encode(working_time_array) + .d-none#data data-working-time=ActiveSupport::JSON.encode(working_time_array) .graph-functions div#chart_1 hr - /div#chart_2 - /hr + div#chart_2 + hr .table-responsive table.table.table-striped.sortable thead diff --git a/app/views/external_users/show.html.slim b/app/views/external_users/show.html.slim index f0028745..33e2dd6b 100644 --- a/app/views/external_users/show.html.slim +++ b/app/views/external_users/show.html.slim @@ -4,9 +4,9 @@ h1 = @user.name //= row(label: 'external_user.email', value: @user.email) = row(label: 'external_user.consumer', value: link_to(@user.consumer, @user.consumer)) -h4 = link_to(t('.exercise_statistics'), statistics_external_user_path(@user)) +h4.mt-4 = link_to(t('.exercise_statistics'), statistics_external_user_path(@user)) -h4 = t('.tag_statistics') +h4.mt-4 = t('.tag_statistics') #loading .spinner = t('.loading_tag_statistics') diff --git a/app/views/file_templates/index.html.slim b/app/views/file_templates/index.html.slim index 3022ea53..ba7c3eb7 100644 --- a/app/views/file_templates/index.html.slim +++ b/app/views/file_templates/index.html.slim @@ -10,7 +10,7 @@ h1 = FileTemplate.model_name.human(count: 2) tbody - @file_templates.each do |file_template| tr - td = file_template.name + td = link_to(file_template.name, file_template) td = link_to(file_template.file_type, file_type_path(file_template.file_type)) td = link_to(t('shared.show'), file_template) td = link_to(t('shared.edit'), edit_file_template_path(file_template)) diff --git a/app/views/file_types/_form.html.slim b/app/views/file_types/_form.html.slim index d36f54cf..e234a457 100644 --- a/app/views/file_types/_form.html.slim +++ b/app/views/file_types/_form.html.slim @@ -12,16 +12,16 @@ .form-group = f.label(:indent_size) = f.number_field(:indent_size, class: 'form-control', placeholder: 2, required: true) - .checkbox - label - = f.check_box(:binary) + .form-check + label.form-check-label + = f.check_box(:binary, class: 'form-check-input') = t('activerecord.attributes.file_type.binary') - .checkbox - label - = f.check_box(:executable) + .form-check + label.form-check-label + = f.check_box(:executable, class: 'form-check-input') = t('activerecord.attributes.file_type.executable') - .checkbox - label - = f.check_box(:renderable) + .form-check.mb-3 + label.form-check-label + = f.check_box(:renderable, class: 'form-check-input') = t('activerecord.attributes.file_type.renderable') .actions = render('shared/submit_button', f: f, object: @file_type) diff --git a/app/views/file_types/index.html.slim b/app/views/file_types/index.html.slim index a8a4d294..95f1394f 100644 --- a/app/views/file_types/index.html.slim +++ b/app/views/file_types/index.html.slim @@ -11,7 +11,7 @@ h1 = FileType.model_name.human(count: 2) tbody - @file_types.each do |file_type| tr - td = file_type.name + td = link_to(file_type.name, file_type) td = link_to(file_type.author, file_type.author) td = file_type.file_extension td = link_to(t('shared.show'), file_type) diff --git a/app/views/file_types/show.json.jbuilder b/app/views/file_types/show.json.jbuilder new file mode 100644 index 00000000..0842614e --- /dev/null +++ b/app/views/file_types/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @file_type, :id, :name, :editor_mode, :file_extension, :executable, :renderable, :binary diff --git a/app/views/hints/_form.html.slim b/app/views/hints/_form.html.slim index 09814d96..21ab6bc1 100644 --- a/app/views/hints/_form.html.slim +++ b/app/views/hints/_form.html.slim @@ -3,15 +3,15 @@ .form-group = f.label(:name) = f.text_field(:name, class: 'form-control', required: true) - .form + .form-group = f.label(:locale) = f.select(:locale, I18n.available_locales.map { |locale| [t("locales.#{locale}"), locale] }, {}, class: 'form-control') .form-group = f.label(:message) = f.text_field(:message, class: 'form-control', placeholder: "'$2' has no method '$1'.", required: true) - .help-block = t('.hints.message') + .help-block.form-text = t('.hints.message') .form-group = f.label(:regular_expression) = f.text_field(:regular_expression, class: 'form-control', placeholder: 'undefined method (\w+) for (\w+)', required: true) - .help-block = t('.hints.regular_expression') + .help-block.form-text = t('.hints.regular_expression') .actions = render('shared/submit_button', f: f, object: @hint) diff --git a/app/views/internal_users/_form.html.slim b/app/views/internal_users/_form.html.slim index f7f89299..4a6ff45c 100644 --- a/app/views/internal_users/_form.html.slim +++ b/app/views/internal_users/_form.html.slim @@ -5,7 +5,7 @@ = f.collection_select(:consumer_id, Consumer.all.sort_by(&:name), :id, :name, {}, class: 'form-control') .form-group = f.label(:email) - = f.text_field(:email, class: 'form-control', required: true) + = f.email_field(:email, class: 'form-control', required: true) .form-group = f.label(:name) = f.text_field(:name, class: 'form-control', required: true) diff --git a/app/views/internal_users/activate.html.slim b/app/views/internal_users/activate.html.slim index cd52abca..25795ed6 100644 --- a/app/views/internal_users/activate.html.slim +++ b/app/views/internal_users/activate.html.slim @@ -9,4 +9,4 @@ h1 = t('.headline') = f.label(:password_confirmation) = f.password_field(:password_confirmation, class: 'form-control', required: true) = f.hidden_field(:activation_token) - .actions = submit_tag(t('.submit'), class: 'btn btn-default') + .actions = submit_tag(t('.submit'), class: 'btn btn-primary') diff --git a/app/views/internal_users/forgot_password.html.slim b/app/views/internal_users/forgot_password.html.slim index 8ccb884a..ca10fdac 100644 --- a/app/views/internal_users/forgot_password.html.slim +++ b/app/views/internal_users/forgot_password.html.slim @@ -3,5 +3,5 @@ h1 = t('.headline') = form_tag do .form-group = label_tag(:email, t('activerecord.attributes.internal_user.email')) - = text_field_tag(:email, params[:email], autofocus: true, class: 'form-control', required: true) - .actions = submit_tag(t('.submit'), class: 'btn btn-default') + = email_field_tag(:email, params[:email], autofocus: true, class: 'form-control', required: true) + .actions = submit_tag(t('.submit'), class: 'btn btn-primary') diff --git a/app/views/internal_users/index.html.slim b/app/views/internal_users/index.html.slim index c4b6c1a0..29f07649 100644 --- a/app/views/internal_users/index.html.slim +++ b/app/views/internal_users/index.html.slim @@ -12,7 +12,7 @@ h1 = InternalUser.model_name.human(count: 2) = f.select(:role_eq, User::ROLES.map { |role| [t("users.roles.#{role}"), role] }, {}, class: 'form-control', prompt: t('activerecord.attributes.internal_user.role')) .table-responsive - table.table + table.table.mt-4 thead tr th = t('activerecord.attributes.internal_user.name') diff --git a/app/views/internal_users/reset_password.html.slim b/app/views/internal_users/reset_password.html.slim index a743ef46..857121c2 100644 --- a/app/views/internal_users/reset_password.html.slim +++ b/app/views/internal_users/reset_password.html.slim @@ -9,4 +9,4 @@ h1 = t('.headline') = f.label(:password_confirmation) = f.password_field(:password_confirmation, class: 'form-control', required: true) = f.hidden_field(:reset_password_token) - .actions = submit_tag(t('.submit'), class: 'btn btn-default') + .actions = submit_tag(t('.submit'), class: 'btn btn-primary') diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 915d1832..64b5549f 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -5,35 +5,30 @@ html lang='en' meta name='viewport' content='width=device-width, initial-scale=1' title = application_name link href=asset_path('favicon.png') rel='icon' type='image/png' - = stylesheet_link_tag(asset_path('bootstrap.min.css', type: :stylesheet)) - = stylesheet_link_tag(asset_path('font-awesome.min.css', type: :stylesheet)) - = stylesheet_link_tag('application', media: 'all', 'data-turbolinks-track' => true) - = javascript_include_tag('application', 'data-turbolinks-track' => true) - = javascript_include_tag(asset_path('underscore-min.js', type: :javascript)) - = javascript_include_tag(asset_path('bootstrap.min.js', type: :javascript)) + = stylesheet_pack_tag('application', media: 'all', 'data-turbolinks-track': true) + = stylesheet_pack_tag('stylesheets', media: 'all', 'data-turbolinks-track': true) + = stylesheet_link_tag('application', media: 'all', 'data-turbolinks-track': true) + = javascript_pack_tag('application', 'data-turbolinks-track': true) + = javascript_include_tag('application', 'data-turbolinks-track': true) = yield(:head) = csrf_meta_tags body - nav.navbar.navbar-default role='navigation' + nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4.py-1 role='navigation' .container - .navbar-header - button.navbar-toggle data-target='#navbar-collapse' data-toggle='collapse' type='button' - span.sr-only Toggle navigation - span.icon-bar - span.icon-bar - span.icon-bar - .navbar-brand - i.fa.fa-code - = application_name + .navbar-brand + i.fa.fa-code + = application_name + button.navbar-toggler data-target='#navbar-collapse' data-toggle='collapse' type='button' aria-expanded='false' aria-label='Toggle navigation' + span.navbar-toggler-icon #navbar-collapse.collapse.navbar-collapse = render('navigation', cached: true) - ul.nav.navbar-nav.navbar-right + ul.nav.navbar-nav.ml-auto = render('locale_selector', cached: true) - li = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}) + li.nav-item.mr-3 = link_to(t('shared.help.link'), '#modal-help', data: {toggle: 'modal'}, class: 'nav-link') = render('session') .container data-controller=controller_name = render('flash') - = render('breadcrumbs') + = render('breadcrumbs') if current_user.try(:internal_user?) - if (controller_name == "exercises" && action_name == "implement") .container-fluid = yield diff --git a/app/views/proxy_exercises/_form.html.slim b/app/views/proxy_exercises/_form.html.slim index bd57bf06..601ef950 100644 --- a/app/views/proxy_exercises/_form.html.slim +++ b/app/views/proxy_exercises/_form.html.slim @@ -1,11 +1,11 @@ -= form_for(@proxy_exercise, multipart: true) do |f| += form_for(@proxy_exercise, multipart: true, builder: PagedownFormBuilder) do |f| = render('shared/form_errors', object: @proxy_exercise) .form-group = f.label(:title) = f.text_field(:title, class: 'form-control', required: true) .form-group = f.label(:description) - = f.pagedown_editor :description + = f.pagedown :description, input_html: { preview: true, rows: 10 } h3 Exercises .table-responsive diff --git a/app/views/proxy_exercises/index.html.slim b/app/views/proxy_exercises/index.html.slim index 80e8084c..a2e7e460 100644 --- a/app/views/proxy_exercises/index.html.slim +++ b/app/views/proxy_exercises/index.html.slim @@ -6,30 +6,30 @@ h1 = ProxyExercise.model_name.human(count: 2) = f.search_field(:title_cont, class: 'form-control', placeholder: t('activerecord.attributes.proxy_exercise.title')) .table-responsive - table.table + table.table.mt-4 thead tr - th = sort_link(@search, :title, t('activerecord.attributes.proxy_exercise.title')) - th = t('activerecord.attributes.exercise.token') - th = t('activerecord.attributes.proxy_exercise.files_count') - th colspan=6 = t('shared.actions') + th.p-1 = sort_link(@search, :title, t('activerecord.attributes.proxy_exercise.title')) + th.p-1 = t('activerecord.attributes.exercise.token') + th.p-1 = t('activerecord.attributes.proxy_exercise.files_count') + th.p-1 colspan=6 = t('shared.actions') tbody - @proxy_exercises.each do |proxy_exercise| tr data-id=proxy_exercise.id - td = link_to(proxy_exercise.title,proxy_exercise) - td = proxy_exercise.token - td = proxy_exercise.count_files - td = link_to(t('shared.edit'), edit_proxy_exercise_path(proxy_exercise)) if policy(proxy_exercise).edit? + td.p-1.pt-2 = link_to(proxy_exercise.title,proxy_exercise) + td.p-1.pt-2 = proxy_exercise.token + td.p-1.pt-2 = proxy_exercise.count_files + td.p-1.pt-2 = link_to(t('shared.edit'), edit_proxy_exercise_path(proxy_exercise)) if policy(proxy_exercise).edit? - td + td.p-1 .btn-group - button.btn.btn-primary-outline.btn-xs.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button') + button.btn.btn-outline-primary.btn-sm.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button') span.caret span.sr-only Toggle Dropdown - ul.dropdown-menu.pull-right role="menu" - li = link_to(t('shared.show'), proxy_exercise) if policy(proxy_exercise).show? - li = link_to(t('shared.destroy'), proxy_exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(proxy_exercise).destroy? - li = link_to(t('.clone'), clone_proxy_exercise_path(proxy_exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(proxy_exercise).clone? + ul.dropdown-menu.float-right role="menu" + li = link_to(t('shared.show'), proxy_exercise, 'data-turbolinks' => "false", class: 'dropdown-item') if policy(proxy_exercise).show? + li = link_to(t('shared.destroy'), proxy_exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete, class: 'dropdown-item') if policy(proxy_exercise).destroy? + li = link_to(t('.clone'), clone_proxy_exercise_path(proxy_exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post, class: 'dropdown-item') if policy(proxy_exercise).clone? = render('shared/pagination', collection: @proxy_exercises) p = render('shared/new_button', model: ProxyExercise) diff --git a/app/views/proxy_exercises/show.html.slim b/app/views/proxy_exercises/show.html.slim index 2649cbb5..07f5061a 100644 --- a/app/views/proxy_exercises/show.html.slim +++ b/app/views/proxy_exercises/show.html.slim @@ -1,6 +1,9 @@ - content_for :head do - = javascript_include_tag('http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js') - = stylesheet_link_tag('http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css') + // Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326. + Otherwise, code might not be highlighted correctly (race condition) + meta name='turbolinks-visit-control' content='reload' + = javascript_pack_tag('highlight', 'data-turbolinks-track': true) + = stylesheet_pack_tag('highlight', media: 'all', 'data-turbolinks-track': true) h1 = @proxy_exercise.title @@ -11,7 +14,8 @@ h1 = row(label: 'proxy_exercise.files_count', value: @exercises.count) = row(label: 'exercise.description', value: @proxy_exercise.description) = row(label: 'exercise.token', value: @proxy_exercise.token) -h3 Exercises + +h2.mt-4 Exercises .table-responsive table.table thead diff --git a/app/views/request_for_comments/_admin_menu.html.slim b/app/views/request_for_comments/_admin_menu.html.slim index 3f71b2ed..cfdfccf1 100644 --- a/app/views/request_for_comments/_admin_menu.html.slim +++ b/app/views/request_for_comments/_admin_menu.html.slim @@ -1,9 +1,8 @@ -br -h4 Admin Menu -h5 - ul - li = link_to "User's current status of this exercise", statistics_external_user_exercise_path(id: @request_for_comment.exercise_id, external_user_id: @request_for_comment.user_id) - li = link_to "All exercises of this user", statistics_external_user_path(id: @request_for_comment.user_id) - ul - li = link_to "Implement the exercise yourself", implement_exercise_path(id: @request_for_comment.exercise_id) - li = link_to "Show the exercise", exercise_path(id: @request_for_comment.exercise_id) +hr +h5.mt-4 Admin Menu +ul.text + li = link_to "User's current status of this exercise", statistics_external_user_exercise_path(id: @request_for_comment.exercise_id, external_user_id: @request_for_comment.user_id) + li = link_to "All exercises of this user", statistics_external_user_path(id: @request_for_comment.user_id) +ul.text + li = link_to "Implement the exercise yourself", implement_exercise_path(id: @request_for_comment.exercise_id) + li = link_to "Show the exercise", exercise_path(id: @request_for_comment.exercise_id) diff --git a/app/views/request_for_comments/_form.html.erb b/app/views/request_for_comments/_form.html.erb deleted file mode 100644 index 81f6ed65..00000000 --- a/app/views/request_for_comments/_form.html.erb +++ /dev/null @@ -1,33 +0,0 @@ -<%= form_for(@request_for_comment) do |f| %> - <% if @request_for_comment.errors.any? %> -
-

<%= pluralize(@request_for_comment.errors.count, "error") %> prohibited this request_for_comment from being saved:

- -
    - <% @request_for_comment.errors.full_messages.each do |message| %> -
  • <%= message %>
  • - <% end %> -
-
- <% end %> - -
- <%= f.label :user_id %>
- <%= f.number_field :user_id %> -
-
- <%= f.label :exercise_id %>
- <%= f.number_field :exercise_id %> -
-
- <%= f.label :file_id %>
- <%= f.number_field :file_id %> -
-
- <%= f.label :user_type %>
- <%= f.text_field :user_type %> -
-
- <%= f.submit %> -
-<% end %> diff --git a/app/views/request_for_comments/_form.html.slim b/app/views/request_for_comments/_form.html.slim new file mode 100644 index 00000000..f24ddd22 --- /dev/null +++ b/app/views/request_for_comments/_form.html.slim @@ -0,0 +1,28 @@ += form_for(@request_for_comment) do |f| + - if @request_for_comment.errors.any? + #error_explanation + h2 + = pluralize(@request_for_comment.errors.count, "error") + | prohibited this request_for_comment from being saved: + ul + - @request_for_comment.errors.full_messages.each do |message| + li= message + + .field + = f.label :user_id + br/ + = f.number_field :user_id + .field + = f.label :exercise_id + br/ + = f.number_field :exercise_id + .field + = f.label :file_id + br/ + = f.number_field :file_id + .field + = f.label :user_type + br/ + = f.text_field :user_type + .actions + = f.submit diff --git a/app/views/request_for_comments/_mark_as_solved.html.slim b/app/views/request_for_comments/_mark_as_solved.html.slim index b3df57fd..cac16d06 100644 --- a/app/views/request_for_comments/_mark_as_solved.html.slim +++ b/app/views/request_for_comments/_mark_as_solved.html.slim @@ -4,4 +4,4 @@ button.btn.btn-primary#mark-as-solved-button = t('request_for_comments.mark_as_s p = t('request_for_comments.write_a_thank_you_node') textarea#thank-you-note button.btn.btn-primary#send-thank-you-note = t('request_for_comments.send_thank_you_note') - button.btn.btn-default#cancel-thank-you-note = t('request_for_comments.cancel_thank_you_note') + button.btn.btn-secondary#cancel-thank-you-note = t('request_for_comments.cancel_thank_you_note') diff --git a/app/views/request_for_comments/index.html.slim b/app/views/request_for_comments/index.html.slim index d253f0ab..94262267 100644 --- a/app/views/request_for_comments/index.html.slim +++ b/app/views/request_for_comments/index.html.slim @@ -9,7 +9,7 @@ h1 = RequestForComment.model_name.human(count: 2) = f.select(:solved_not_eq, [[t('request_for_comments.show_all'), 2], [t('request_for_comments.show_unsolved'), 1], [t('request_for_comments.show_solved'), 0]]) .table-responsive - table.table.sortable + table.table.sortable.mt-4 thead tr th @@ -23,8 +23,7 @@ h1 = RequestForComment.model_name.human(count: 2) th = t('activerecord.attributes.request_for_comments.last_update') tbody - @request_for_comments.each do |request_for_comment| - - do_not_answer = [:rfc_intervention_stale_rfc, :break_intervention_stale_rfc, :no_intervention_stale_rfc].include?(UserGroupSeparator.getInterventionGroup(request_for_comment.user)) and current_user.internal_user? - tr data-id=request_for_comment.id class=('do-not-answer' if do_not_answer) + tr data-id=request_for_comment.id - if request_for_comment.solved? td span class="fa fa-check" aria-hidden="true" @@ -39,7 +38,7 @@ h1 = RequestForComment.model_name.human(count: 2) - else td = '-' td = request_for_comment.comments_count - td = request_for_comment.user.displayname + td = link_to_if(request_for_comment.user && policy(request_for_comment.user).show?, request_for_comment.user.displayname, request_for_comment.user) td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.created_at)) td = t('shared.time.before', time: distance_of_time_in_words_to_now(request_for_comment.last_comment.nil? ? request_for_comment.updated_at : request_for_comment.last_comment)) diff --git a/app/views/request_for_comments/show.html.erb b/app/views/request_for_comments/show.html.slim similarity index 66% rename from app/views/request_for_comments/show.html.erb rename to app/views/request_for_comments/show.html.slim index 5f6a53f5..a55764c4 100644 --- a/app/views/request_for_comments/show.html.erb +++ b/app/views/request_for_comments/show.html.slim @@ -1,116 +1,87 @@ -
-

- <% if @request_for_comment.solved? %> - - <% end %> - <%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %> -

-

- <% - user = @request_for_comment.user - submission = @request_for_comment.submission - testruns = Testrun.where(:submission_id => @request_for_comment.submission) - %> - <%= user.displayname %> | <%= @request_for_comment.created_at.localtime %> -

-
-
-
- <%= t('activerecord.attributes.exercise.description') %> -
-
- - <%= render_markdown(@request_for_comment.exercise.description) %> -
-
+.list-group + h4#exercise_caption.list-group-item-heading data-comment-exercise-url=create_comment_exercise_request_for_comment_path data-exercise-id="#{@request_for_comment.exercise.id}" data-rfc-id="#{@request_for_comment.id}" + - if @request_for_comment.solved? + span.fa.fa-check aria-hidden="true" + = link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) + p.list-group-item-text + - user = @request_for_comment.user + - submission = @request_for_comment.submission + - testruns = Testrun.where(:submission_id => @request_for_comment.submission) + = user.displayname + | | #{@request_for_comment.created_at.localtime} + .rfc + .description + h5 + = t('activerecord.attributes.exercise.description') + .text + span.fa.fa-chevron-up.collapse-button + = render_markdown(@request_for_comment.exercise.description) -
-
- <%= t('activerecord.attributes.request_for_comments.question')%> -
-
- <% question = @request_for_comment.question %> - <%= question.nil? or question.empty? ? t('request_for_comments.no_question') : question %> -
-
+ .question + h5.mt-4 + = t('activerecord.attributes.request_for_comments.question') + .text + - question = @request_for_comment.question + = question.nil? or question.empty? ? t('request_for_comments.no_question') : question - <% if policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved? %> - <%= render('mark_as_solved') %> - <% end %> - - <% if testruns.size > 0 %> -
- <% output_runs = testruns.select { |run| run.cause == 'run' } %> - <% if output_runs.size > 0 %> -
<%= t('request_for_comments.runtime_output') %>
- - <% end %> + - if policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved? + = render('mark_as_solved') - <% assess_runs = testruns.select { |run| run.cause == 'assess' } %> - <% if assess_runs.size > 0 %> -
<%= t('request_for_comments.test_results') %>
-
- <% assess_runs.each do |testrun| %> -
-
- -
- <% end %> -
- <% end %> -
- <% end %> + - if testruns.size > 0 + .testruns + - output_runs = testruns.select {|run| run.cause == 'run'} + - if output_runs.size > 0 + h5.mt-4= t('request_for_comments.runtime_output') + .collapsed.testrun-output.text + span.fa.fa-chevron-down.collapse-button + - output_runs.each do |testrun| + - output = testrun.try(:output) + - if output + - messages = output.scan(/{(?:(?:".+?":".+?")+?,?)+}/) + - messages.map! {|el| JSON.parse(el)} + - messages.keep_if {|message| message['cmd'] == 'write'} + - messages.map! {|message| message['data']} + - output = messages.join '' + pre= output or t('request_for_comments.no_output') - <% if @current_user.admin? && user.is_a?(ExternalUser) %> - <%= render('admin_menu') %> - <% end %> + - assess_runs = testruns.select {|run| run.cause == 'assess'} + - if assess_runs.size > 0 + h5.mt-4= t('request_for_comments.test_results') + .testrun-assess-results + - assess_runs.each do |testrun| + .testrun-container + div class=("result #{testrun.passed ? 'passed' : 'failed'}") + .collapsed.testrun-output.text + span.fa.fa-chevron-down.collapse-button + pre= testrun.output or t('request_for_comments.no_output') -
+ - if @current_user.admin? && user.is_a?(ExternalUser) + = render('admin_menu') -
-
- <%= t('request_for_comments.howto_title') %> -
-
- <%= render_markdown(t('request_for_comments.howto')) %> -
-
-
-
+ hr/ - - -<% submission.files.each do |file| %> - <%= (file.path or "") + "/" + file.name + file.file_type.file_extension %>
-    <%= t('request_for_comments.click_here') %> -
<%= file.content %> -
-<% end %> + .howto + h5.mt-4 + = t('request_for_comments.howto_title') + .text + = render_markdown(t('request_for_comments.howto')) -<%= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') %> +.d-none.sanitizer +/! + | do not put a carriage return in the line below. it will be present in the presentation of the source code, otherwise. + | also, all settings from the rails model needed for the editor configuration in the JavaScript are attached to the editor as data attributes here. +- submission.files.each do |file| + = (file.path or "") + "/" + file.name + file.file_type.file_extension + br/ + |    + i.fa.fa-arrow-down aria-hidden="true" + = t('request_for_comments.click_here') + #commentitor.editor data-file-id="#{file.id}" data-mode="#{file.file_type.editor_mode}" data-read-only="true" + = file.content - diff --git a/app/views/searches/destroy.html.erb b/app/views/searches/destroy.html.erb deleted file mode 100644 index e69de29b..00000000 diff --git a/app/views/sessions/new.html.slim b/app/views/sessions/new.html.slim index e14f70d2..d2cf3211 100644 --- a/app/views/sessions/new.html.slim +++ b/app/views/sessions/new.html.slim @@ -3,13 +3,14 @@ h1 = t('.headline') = form_tag(sessions_path) do .form-group = label_tag(:email, t('activerecord.attributes.internal_user.email')) - = text_field_tag(:email, params[:email], autofocus: true, class: 'form-control', required: true) + = email_field_tag(:email, params[:email], autofocus: true, class: 'form-control', required: true) .form-group = label_tag(:password, t('activerecord.attributes.internal_user.password')) = password_field_tag(:password, nil, class: 'form-control', required: true) - .checkbox - label - = check_box_tag(:remember_me) + .form-check.form-group + label.form-check-label + // Set values 1 and true/false explicit to allow passing a custom HTML class + = check_box_tag(:remember_me, 1, true, class: 'form-check-input') = t('.remember_me') - span.pull-right = link_to(t('.forgot_password'), forgot_password_path) - .actions = submit_tag(t('.link'), class: 'btn btn-default') + span.float-right = link_to(t('.forgot_password'), forgot_password_path) + .actions = submit_tag(t('.link'), class: 'btn btn-primary') diff --git a/app/views/shared/_edit_button.html.slim b/app/views/shared/_edit_button.html.slim index 1fc1870e..36f35639 100644 --- a/app/views/shared/_edit_button.html.slim +++ b/app/views/shared/_edit_button.html.slim @@ -1 +1,3 @@ -= link_to(t('shared.edit'), local_assigns.fetch(:path, send(:"edit_#{object.class.name.underscore}_path", object)), class: 'btn btn-default pull-right') +// default value for fetch will always be evaluated even if it is not returned +- link_target = local_assigns.fetch(:path, false) || send(:"edit_#{object.class.name.underscore}_path", object) += link_to(t('shared.edit'), link_target, class: 'btn btn-secondary float-right') diff --git a/app/views/shared/_file.html.slim b/app/views/shared/_file.html.slim index bac52d1a..cc47be10 100644 --- a/app/views/shared/_file.html.slim +++ b/app/views/shared/_file.html.slim @@ -5,6 +5,6 @@ = row(label: 'file.hidden', value: file.hidden) = row(label: 'file.read_only', value: file.read_only) - if file.teacher_defined_test? - = row(label: 'file.feedback_message', value: render_markdown(file.feedback_message)) + = row(label: 'file.feedback_message', value: render_markdown(file.feedback_message), class: 'm-0') = row(label: 'file.weight', value: file.weight) = row(label: 'file.content', value: file.native_file? ? link_to(file.native_file.file.filename, file.native_file.url) : code_tag(file.content)) diff --git a/app/views/shared/_form_filters.html.slim b/app/views/shared/_form_filters.html.slim index 86297f49..2026ae21 100644 --- a/app/views/shared/_form_filters.html.slim +++ b/app/views/shared/_form_filters.html.slim @@ -1,11 +1,9 @@ -.well +.card.card-body.bg-light = search_form_for(@search, class: 'clearfix filter-form form-inline') do |f| = yield(f) - .btn-group.pull-right - button.btn.btn-default.btn-sm type='submit' = t('shared.apply_filters') - button.btn.btn-default.btn-sm.dropdown-toggle data-toggle='dropdown' type='button' - span.caret - span.sr-only Toggle Dropdown + .btn-group.ml-auto + button.btn.btn-primary type='submit' = t('shared.apply_filters') + button.btn.btn-primary.dropdown-toggle data-toggle='dropdown' type='button' ul.dropdown-menu role='menu' li - a href=request.path = t('shared.reset_filters') + a.dropdown-item href=request.path = t('shared.reset_filters') diff --git a/app/views/shared/_modal.html.slim b/app/views/shared/_modal.html.slim index 87905ea3..3b715a12 100644 --- a/app/views/shared/_modal.html.slim +++ b/app/views/shared/_modal.html.slim @@ -2,10 +2,10 @@ .modal-dialog class=local_assigns[:classes] .modal-content .modal-header + h4#modal-title.modal-title = title button.close data-dismiss='modal' type='button' span aria-hidden=true × span.sr-only Close - h4#modal-title.modal-title = title .modal-body - if local_assigns.has_key?(:body) = body diff --git a/app/views/shared/_new_button.html.slim b/app/views/shared/_new_button.html.slim index 3ed54d2b..9037487b 100644 --- a/app/views/shared/_new_button.html.slim +++ b/app/views/shared/_new_button.html.slim @@ -1,4 +1,6 @@ - if policy(model).new? - a.btn.btn-success href=local_assigns.fetch(:path, send(:"new_#{model.model_name.singular}_path")) + // default value for fetch will always be evaluated even if it is not returned + - href_target = local_assigns.fetch(:path, false) || send(:"new_#{model.model_name.singular}_path") + a.btn.btn-success href=href_target i.fa.fa-plus = t('shared.new_model', model: model.model_name.human) diff --git a/app/views/shared/_pagination.html.slim b/app/views/shared/_pagination.html.slim index 3089d962..cb137076 100644 --- a/app/views/shared/_pagination.html.slim +++ b/app/views/shared/_pagination.html.slim @@ -1,3 +1,2 @@ -- if (pagination = will_paginate(collection, container: false)).present? - .text-center - ul.pagination = pagination +- if (pagination = will_paginate(collection, container: false, renderer: WillPaginate::ActionView::Bootstrap4LinkRenderer)).present? + ul.pagination.justify-content-center = pagination diff --git a/app/views/shared/_submit_button.html.slim b/app/views/shared/_submit_button.html.slim index b7c2fe2b..7657d97d 100644 --- a/app/views/shared/_submit_button.html.slim +++ b/app/views/shared/_submit_button.html.slim @@ -1 +1 @@ -= f.submit(class: 'btn btn-default', value: t(object.new_record? ? 'shared.create' : 'shared.update', model: object.class.model_name.human)) += f.submit(class: 'btn btn-primary', value: t(object.new_record? ? 'shared.create' : 'shared.update', model: object.class.model_name.human)) diff --git a/app/views/statistics/activity_history.html.slim b/app/views/statistics/activity_history.html.slim index b8efa71e..e39e26fa 100644 --- a/app/views/statistics/activity_history.html.slim +++ b/app/views/statistics/activity_history.html.slim @@ -1,6 +1,9 @@ - content_for :head do - = javascript_include_tag(asset_path('vis.min.js', type: :javascript)) - = stylesheet_link_tag(asset_path('vis.min.css', type: :stylesheet)) + // Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326. + Otherwise, the global variable `vis` might be uninitialized in the assets (race condition) + meta name='turbolinks-visit-control' content='reload' + = javascript_pack_tag('vis', 'data-turbolinks-track': true) + = stylesheet_pack_tag('vis', media: 'all', 'data-turbolinks-track': true) .group .title diff --git a/app/views/statistics/graphs.html.slim b/app/views/statistics/graphs.html.slim index 75baf168..f390f7d2 100644 --- a/app/views/statistics/graphs.html.slim +++ b/app/views/statistics/graphs.html.slim @@ -1,17 +1,20 @@ - content_for :head do - = javascript_include_tag(asset_path('vis.min.js', type: :javascript)) - = stylesheet_link_tag(asset_path('vis.min.css', type: :stylesheet)) + // Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326. + Otherwise, the global variable `vis` might be uninitialized in the assets (race condition) + meta name='turbolinks-visit-control' content='reload' + = javascript_pack_tag('vis', 'data-turbolinks-track': true) + = stylesheet_pack_tag('vis', media: 'all', 'data-turbolinks-track': true) .group .title h1 = t('.user_activity') a href=statistics_graphs_user_activity_history_path = t('.history') .spinner - .graph#user-activity + .graph.mb-5#user-activity .group .title h1 = t('.rfc_activity') a href=statistics_graphs_rfc_activity_history_path = t('.history') .spinner - .graph#rfc-activity + .graph.mb-5#rfc-activity diff --git a/app/views/submissions/index.html.slim b/app/views/submissions/index.html.slim index fb6504ef..45ff355f 100644 --- a/app/views/submissions/index.html.slim +++ b/app/views/submissions/index.html.slim @@ -6,10 +6,10 @@ h1 = Submission.model_name.human(count: 2) = f.collection_select(:exercise_id_eq, Exercise.with_submissions, :id, :title, class: 'form-control', prompt: t('activerecord.attributes.submission.exercise')) .form-group = f.label(:cause_eq, t('activerecord.attributes.submission.cause'), class: 'sr-only') - = f.select(:cause_eq, Submission.all.map(&:cause).uniq.sort, class: 'form-control', prompt: t('activerecord.attributes.submission.cause')) + = f.select(:cause_eq, Submission.select(:cause).distinct.map(&:cause).sort, class: 'form-control', prompt: t('activerecord.attributes.submission.cause')) .table-responsive - table.table + table.table.mt-4 thead tr th = sort_link(@search, :exercise_id, t('activerecord.attributes.submission.exercise')) diff --git a/app/views/submissions/show.html.slim b/app/views/submissions/show.html.slim index 1d2ce402..ebf1ba72 100644 --- a/app/views/submissions/show.html.slim +++ b/app/views/submissions/show.html.slim @@ -1,3 +1,10 @@ +- content_for :head do + // Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326. + Otherwise, code might not be highlighted correctly (race condition) + meta name='turbolinks-visit-control' content='reload' + = javascript_pack_tag('highlight', 'data-turbolinks-track': true) + = stylesheet_pack_tag('highlight', media: 'all', 'data-turbolinks-track': true) + h1 = @submission = row(label: 'submission.exercise', value: link_to(@submission.exercise, @submission.exercise)) @@ -5,9 +12,9 @@ h1 = @submission = row(label: 'submission.cause', value: t("submissions.causes.#{@submission.cause}")) = row(label: 'submission.score', value: @submission.score) -h2 = t('activerecord.attributes.submission.files') +h2.mt-4 = t('activerecord.attributes.submission.files') ul.list-unstyled - @files.each do |file| - li.panel.panel-default - .panel-body = render('shared/file', file: file) + li.card.mt-2 + .card-body = render('shared/file', file: file) diff --git a/app/views/submissions/statistics.html.slim b/app/views/submissions/statistics.html.slim index fea23f87..1b07a984 100644 --- a/app/views/submissions/statistics.html.slim +++ b/app/views/submissions/statistics.html.slim @@ -4,7 +4,7 @@ h1 = @submission = row(label: 'submission.score', value: @submission.score) = row(label: '.siblings', value: @submission.siblings.count) -h2 = t('.history') +h2.mt-4 = t('.history') .table-responsive table.table diff --git a/app/views/user_exercise_feedbacks/_form.html.slim b/app/views/user_exercise_feedbacks/_form.html.slim index 46eaf81e..a5fbf63a 100644 --- a/app/views/user_exercise_feedbacks/_form.html.slim +++ b/app/views/user_exercise_feedbacks/_form.html.slim @@ -1,23 +1,22 @@ = form_for(@uef) do |f| div - span.badge.pull-right.score + span.badge.badge-pill.badge-primary.float-right.score h1 id="exercise-headline" - = t('activerecord.models.user_exercise_feedback.one') + " " - = link_to(@exercise.title, [:implement, @exercise]) + = t('activerecord.models.user_exercise_feedback.one') + " " + @exercise.title = render('shared/form_errors', object: @uef) - h4 + p == t('user_exercise_feedback.description') - #description-panel.lead.description-panel + #description-card.lead.description-card u = t('activerecord.attributes.exercise.description') = render_markdown(@exercise.description) .form-group = f.text_area(:feedback_text, class: 'form-control', required: true, :rows => "10") - h4 = t('user_exercise_feedback.difficulty') - = f.collection_radio_buttons :difficulty, @texts, :first, :last, html_options={class: "radio-inline"} do |b| - = b.label(:class => 'radio') { b.radio_button + b.text } - h4 = t('user_exercise_feedback.working_time') - = f.collection_radio_buttons :user_estimated_worktime, @times, :first, :last, html_options={class: "radio-inline"} do |b| - = b.label(:class => 'radio') { b.radio_button + b.text } + h4.mt-4 = t('user_exercise_feedback.difficulty') + = f.collection_radio_buttons :difficulty, @texts, :first, :last, html_options: {class: "form-check-inline"} do |b| + = b.label(:class => 'form-check') { b.radio_button + b.text } + h4.mt-4 = t('user_exercise_feedback.working_time') + = f.collection_radio_buttons :user_estimated_worktime, @times, :first, :last, html_options: {class: "form-check-inline"} do |b| + = b.label(:class => 'form-check') { b.radio_button + b.text } = f.hidden_field(:exercise_id, :value => @exercise.id) .actions = render('shared/submit_button', f: f, object: @uef) diff --git a/bin/bundle b/bin/bundle index 66e9889e..f19acf5b 100755 --- a/bin/bundle +++ b/bin/bundle @@ -1,3 +1,3 @@ #!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) load Gem.bin_path('bundler', 'bundle') diff --git a/bin/rails b/bin/rails index 5191e692..5badb2fd 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,9 @@ #!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' diff --git a/bin/rake b/bin/rake index 17240489..d87d5f57 100755 --- a/bin/rake +++ b/bin/rake @@ -1,4 +1,9 @@ #!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end require_relative '../config/boot' require 'rake' Rake.application.run diff --git a/bin/setup b/bin/setup index acdb2c13..94fd4d79 100755 --- a/bin/setup +++ b/bin/setup @@ -1,29 +1,36 @@ #!/usr/bin/env ruby -require 'pathname' +require 'fileutils' +include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = File.expand_path('..', __dir__) -Dir.chdir APP_ROOT do +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do # This script is a starting point to setup your application. - # Add necessary setup steps to this file: + # Add necessary setup steps to this file. - puts "== Installing dependencies ==" - system "gem install bundler --conservative" - system "bundle check || bundle install" + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') # puts "\n== Copying sample files ==" - # unless File.exist?("config/database.yml") - # system "cp config/database.yml.sample config/database.yml" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" - system "bin/rake db:setup" + system! 'bin/rails db:setup' puts "\n== Removing old logs and tempfiles ==" - system "rm -f log/*" - system "rm -rf tmp/cache" + system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" - system "touch tmp/restart.txt" + system! 'bin/rails restart' end diff --git a/bin/spring b/bin/spring index 253ec37c..fb2ec2eb 100755 --- a/bin/spring +++ b/bin/spring @@ -1,18 +1,17 @@ #!/usr/bin/env ruby -# This file loads spring without using Bundler, in order to be fast -# It gets overwritten when you run the `spring binstub` command +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. unless defined?(Spring) - require "rubygems" - require "bundler" + require 'rubygems' + require 'bundler' - if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m) - ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) - ENV["GEM_HOME"] = "" - Gem.paths = ENV - - gem "spring", match[1] - require "spring/binstub" + lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) + spring = lockfile.specs.detect { |spec| spec.name == "spring" } + if spring + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem 'spring', spring.version + require 'spring/binstub' end end diff --git a/bin/update b/bin/update new file mode 100755 index 00000000..58bfaed5 --- /dev/null +++ b/bin/update @@ -0,0 +1,31 @@ +#!/usr/bin/env ruby +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/webpack b/bin/webpack new file mode 100755 index 00000000..46583272 --- /dev/null +++ b/bin/webpack @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +require "webpacker" +require "webpacker/webpack_runner" +Webpacker::WebpackRunner.run(ARGV) diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server new file mode 100755 index 00000000..faa69f07 --- /dev/null +++ b/bin/webpack-dev-server @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +require "webpacker" +require "webpacker/dev_server_runner" +Webpacker::DevServerRunner.run(ARGV) diff --git a/bin/yarn b/bin/yarn new file mode 100755 index 00000000..460dd565 --- /dev/null +++ b/bin/yarn @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + begin + exec "yarnpkg", *ARGV + rescue Errno::ENOENT + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/config/application.rb b/config/application.rb index 97118237..e778b4e6 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,4 +1,4 @@ -require File.expand_path('../boot', __FILE__) +require_relative 'boot' require 'rails/all' @@ -8,9 +8,13 @@ Bundler.require(*Rails.groups) module CodeOcean class Application < Rails::Application + # Initialize configuration defaults + config.load_defaults 5.2 + # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. + # Application configuration can go into files in config/initializers + # -- all .rb files in that directory are automatically loaded after loading + # the framework and any gems in your application. # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. @@ -21,23 +25,8 @@ module CodeOcean # config.i18n.default_locale = :de config.i18n.available_locales = [:de, :en] - # Do not swallow errors in after_commit/after_rollback callbacks. - config.active_record.raise_in_transactional_callbacks = true - config.autoload_paths << Rails.root.join('lib') config.eager_load_paths << Rails.root.join('lib') config.assets.precompile += %w( markdown-buttons.png ) - - #config.active_record.schema_format = :sql - - case (RUBY_ENGINE) - when 'ruby' - # ... - #this is enabled in prod for testing - config.middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement - when 'jruby' - # plattform specific - java.lang.Class.for_name('javax.crypto.JceSecurity').get_declared_field('isRestricted').tap{|f| f.accessible = true; f.set nil, false} - end end end diff --git a/config/boot.rb b/config/boot.rb index 6b750f00..b9e460ce 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,3 +1,4 @@ -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 00000000..1060cbe2 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: code_ocean_production diff --git a/config/deploy.rb b/config/deploy.rb index be0cd539..6b72d746 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -19,7 +19,6 @@ namespace :deploy do after :compile_assets, :copy_vendor_assets do on roles(fetch(:assets_roles)) do within release_path do - execute :cp, '-r', 'vendor/assets/images/', 'public/assets/' execute :cp, '-r', 'vendor/assets/javascripts/ace', 'public/assets/' end end diff --git a/config/environment.rb b/config/environment.rb index ee8d90dc..d4654f20 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,5 +1,8 @@ # Load the Rails application. -require File.expand_path('../application', __FILE__) +require_relative 'application' + +# LTI 1.x uses OAuth 1.0 +OAUTH_10_SUPPORT = true # Initialize the Rails application. Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb index 7fcbb05d..b91a19b5 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,4 +1,6 @@ Rails.application.configure do + # Verifies that versions and hashed value of the package contents in the project's package.json + config.webpacker.check_yarn_integrity = true # Settings specified here will take precedence over those in config/application.rb. config.web_console.whitelisted_ips = '192.168.0.0/16' @@ -11,21 +13,41 @@ Rails.application.configure do # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching. - config.consider_all_requests_local = true - config.action_controller.perform_caching = false - - config.action_mailer.perform_deliveries = false + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false + config.action_mailer.perform_caching = false + # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. @@ -48,6 +70,9 @@ Rails.application.configure do BetterErrors::Middleware.allow_ip! ENV['TRUSTED_IP'] if ENV['TRUSTED_IP'] - # Delete middleware in order to allow concurrent requests. - config.middleware.delete(Rack::Lock) + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem and might not + # work within a Vagrant environment. + # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + config.file_watcher = ActiveSupport::FileUpdateChecker end diff --git a/config/environments/production.rb b/config/environments/production.rb index 23c4e5b4..86b0fac5 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,4 +1,6 @@ Rails.application.configure do + # Verifies that versions and hashed value of the package contents in the project's package.json + config.webpacker.check_yarn_integrity = false # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. @@ -14,18 +16,17 @@ Rails.application.configure do config.consider_all_requests_local = false config.action_controller.perform_caching = true - # Enable Rack::Cache to put a simple HTTP cache in front of your application - # Add `rack-cache` to your Gemfile before enabling this. - # For large-scale production use, consider using a caching reverse proxy like - # NGINX, varnish or squid. - # config.action_dispatch.rack_cache = true + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. - config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + config.assets.js_compressor = Uglifier.new(harmony: true) + # config.assets.js_compressor = :uglifier # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. @@ -37,10 +38,21 @@ Rails.application.configure do # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true @@ -49,16 +61,16 @@ Rails.application.configure do config.log_level = :error # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + # config.log_tags = [ :subdomain, :uuid, :request_id ] # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = 'http://assets.example.com' + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "code_ocean_#{Rails.env}" + + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. @@ -74,6 +86,16 @@ Rails.application.configure do # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false end diff --git a/config/environments/test.rb b/config/environments/test.rb index a5c78bf9..5d112d40 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -12,9 +12,11 @@ Rails.application.configure do # preloads Rails for running tests, you may have to set it to true. config.eager_load = false - # Configure static file server for tests with Cache-Control for performance. - config.serve_static_files = true - config.static_cache_control = 'public, max-age=3600' + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } # Show full error reports and disable caching. config.consider_all_requests_local = true @@ -26,6 +28,11 @@ Rails.application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + # Store uploaded files on the local file system in a temporary directory + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 00000000..89d2efab --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index d74f7733..7b5df826 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,12 +1,17 @@ # Be sure to restart your server when you modify this file. -# Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = '1.0' +Rails.application.config.tap do |config| -# Add additional assets to the asset load path -# Rails.application.config.assets.paths << Emoji.images_path + # Version of your assets, change this if you want to expire all your assets. + config.assets.version = '1.0' -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -# Rails.application.config.assets.precompile += %w( search.js ) -Rails.application.config.assets.precompile += %w( markdown-buttons.png ) \ No newline at end of file + # Add additional assets to the asset load path. + # config.assets.paths << Emoji.images_path + # Add Yarn node_modules folder to the asset load path. + config.assets.paths << Rails.root.join('node_modules') + + # Precompile additional assets. + # application.js, application.css, and all non-JS/CSS in the app/assets + # folder are already added. + # config.assets.precompile += %w( admin.js admin.css ) +end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 00000000..d3bcaa5e --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# If you are using UJS then enable automatic nonce generation +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index 7f70458d..5a6a32d3 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,5 @@ # Be sure to restart your server when you modify this file. +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/docker.rb b/config/initializers/docker.rb index 809707fb..2ebc597d 100644 --- a/config/initializers/docker.rb +++ b/config/initializers/docker.rb @@ -1,6 +1,8 @@ DockerClient.initialize_environment unless Rails.env.test? && `which docker`.blank? -if ActiveRecord::Base.connection.tables.present? && DockerContainerPool.config[:active] +if !Rake.application.top_level_tasks.to_s.include?('db:') && + ApplicationRecord.connection.tables.present? && + DockerContainerPool.config[:active] DockerContainerPool.start_refill_task at_exit { DockerContainerPool.clean_up } end diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index 33725e95..bbfc3961 100644 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -5,10 +5,10 @@ # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] if respond_to?(:wrap_parameters) + wrap_parameters format: [:json] end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do -# self.include_root_in_json = true +# self.include_root_in_json = true # end diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 00000000..a5eccf81 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,34 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb index bf07d7f8..9ea5ab24 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,7 +28,7 @@ Rails.application.routes.draw do delete '/comment_by_id', to: 'comments#destroy_by_id' put '/comments', to: 'comments#update' - resources :subscriptions do + resources :subscriptions, only: [:create, :destroy] do member do get :unsubscribe, to: 'subscriptions#destroy' end @@ -67,7 +67,7 @@ Rails.application.routes.draw do get :statistics end - resources :errors, only: [:create, :index, :show] + resources :errors, only: [:create, :index, :show], controller: 'code_ocean/errors' resources :hints end diff --git a/config/secrets.yml.example b/config/secrets.yml.example index bc69f9d6..656e1048 100644 --- a/config/secrets.yml.example +++ b/config/secrets.yml.example @@ -5,18 +5,28 @@ # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -# You can use `rake secret` to generate a secure secret key. +# You can use `rails secret` to generate a secure secret key. # Make sure the secrets in this file are kept private # if you're sharing your code publicly. +# Shared secrets are available across all environments. + +# shared: +# api_key: a1B2c3D4e5F6 + +# Environmental secrets are only available for that specific environment. + development: secret_key_base: CHANGE_ME test: secret_key_base: CHANGE_ME -# Do not keep production secrets in the repository, -# instead read values from the environment. +# Do not keep production secrets in the unencrypted secrets file. +# Instead, either read values from the environment. +# Or, use `bin/rails secrets:setup` to configure encrypted secrets +# and move the `production:` environment over there. + production: secret_key_base: CHANGE_ME diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 00000000..c9119b40 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +%w( + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +).each { |path| Spring.watch(path) } diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 00000000..d32f76e8 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket + +# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/config/webpack/development.js b/config/webpack/development.js new file mode 100644 index 00000000..9bee5c01 --- /dev/null +++ b/config/webpack/development.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + +const environment = require('./environment'); + +module.exports = environment; diff --git a/config/webpack/environment.js b/config/webpack/environment.js new file mode 100644 index 00000000..4eed54f6 --- /dev/null +++ b/config/webpack/environment.js @@ -0,0 +1,33 @@ +/* +./config/webpack/environment.js +Info for this file can be found +github.com/rails/webpacker/blob/master/docs/webpack.md +*/ + +const { environment } = require('@rails/webpacker'); +const merge = require('webpack-merge'); +const webpack = require('webpack'); + +// Add an additional plugin of your choosing : ProvidePlugin +environment.plugins.prepend('Provide', new webpack.ProvidePlugin({ + $: 'jquery', + JQuery: 'jquery', + jquery: 'jquery', + 'window.Tether': "tether", + Popper: ['popper.js', 'default'], // for Bootstrap 4 + _: 'underscore', + vis: 'vis', + hljs: 'highlight.js', + }) +); + +const envConfig = module.exports = environment; +const aliasConfig = module.exports = { + resolve: { + alias: { + jquery: 'jquery/src/jquery', + } + } +}; + +module.exports = merge(envConfig.toWebpackConfig(), aliasConfig); diff --git a/config/webpack/production.js b/config/webpack/production.js new file mode 100644 index 00000000..ffd1a49c --- /dev/null +++ b/config/webpack/production.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'production'; + +const environment = require('./environment'); + +module.exports = environment; diff --git a/config/webpack/test.js b/config/webpack/test.js new file mode 100644 index 00000000..9bee5c01 --- /dev/null +++ b/config/webpack/test.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + +const environment = require('./environment'); + +module.exports = environment; diff --git a/config/webpacker.yml b/config/webpacker.yml new file mode 100644 index 00000000..a115a886 --- /dev/null +++ b/config/webpacker.yml @@ -0,0 +1,70 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_output_path: packs + cache_path: tmp/cache/webpacker + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + resolved_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + extensions: + - .js + - .sass + - .scss + - .css + - .module.sass + - .module.scss + - .module.css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: 0.0.0.0 + port: 3035 + public: 0.0.0.0:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + headers: + 'Access-Control-Allow-Origin': '*' + watch_options: + ignored: /node_modules/ + # File Watcher might not work inside Vagrant + poll: true + + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Cache manifest.json for performance + cache_manifest: true diff --git a/db/migrate/20140625134118_create_exercises.rb b/db/migrate/20140625134118_create_exercises.rb index 448befa8..1395ecc3 100644 --- a/db/migrate/20140625134118_create_exercises.rb +++ b/db/migrate/20140625134118_create_exercises.rb @@ -1,4 +1,4 @@ -class CreateExercises < ActiveRecord::Migration +class CreateExercises < ActiveRecord::Migration[4.2] def change create_table :exercises do |t| t.text :description diff --git a/db/migrate/20140626143132_create_execution_environments.rb b/db/migrate/20140626143132_create_execution_environments.rb index d1499f6c..40c50d0b 100644 --- a/db/migrate/20140626143132_create_execution_environments.rb +++ b/db/migrate/20140626143132_create_execution_environments.rb @@ -1,4 +1,4 @@ -class CreateExecutionEnvironments < ActiveRecord::Migration +class CreateExecutionEnvironments < ActiveRecord::Migration[4.2] def change create_table :execution_environments do |t| t.string :docker_image diff --git a/db/migrate/20140626144036_create_submissions.rb b/db/migrate/20140626144036_create_submissions.rb index 8f232204..07242ed1 100644 --- a/db/migrate/20140626144036_create_submissions.rb +++ b/db/migrate/20140626144036_create_submissions.rb @@ -1,4 +1,4 @@ -class CreateSubmissions < ActiveRecord::Migration +class CreateSubmissions < ActiveRecord::Migration[4.2] def change create_table :submissions do |t| t.text :code diff --git a/db/migrate/20140630093736_add_reference_implementation_to_exercises.rb b/db/migrate/20140630093736_add_reference_implementation_to_exercises.rb index 33e5647a..d9a854e2 100644 --- a/db/migrate/20140630093736_add_reference_implementation_to_exercises.rb +++ b/db/migrate/20140630093736_add_reference_implementation_to_exercises.rb @@ -1,4 +1,4 @@ -class AddReferenceImplementationToExercises < ActiveRecord::Migration +class AddReferenceImplementationToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :reference_implementation, :text end diff --git a/db/migrate/20140630111215_add_indent_size_to_execution_environments.rb b/db/migrate/20140630111215_add_indent_size_to_execution_environments.rb index bcf0a6e5..6f2830a8 100644 --- a/db/migrate/20140630111215_add_indent_size_to_execution_environments.rb +++ b/db/migrate/20140630111215_add_indent_size_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddIndentSizeToExecutionEnvironments < ActiveRecord::Migration +class AddIndentSizeToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_column :execution_environments, :indent_size, :integer end diff --git a/db/migrate/20140701120126_create_consumers.rb b/db/migrate/20140701120126_create_consumers.rb index f5ce039f..8efbc84e 100644 --- a/db/migrate/20140701120126_create_consumers.rb +++ b/db/migrate/20140701120126_create_consumers.rb @@ -1,4 +1,4 @@ -class CreateConsumers < ActiveRecord::Migration +class CreateConsumers < ActiveRecord::Migration[4.2] def change create_table :consumers do |t| t.string :name diff --git a/db/migrate/20140701122345_create_users.rb b/db/migrate/20140701122345_create_users.rb index bfec23fa..215e0ffb 100644 --- a/db/migrate/20140701122345_create_users.rb +++ b/db/migrate/20140701122345_create_users.rb @@ -1,4 +1,4 @@ -class CreateUsers < ActiveRecord::Migration +class CreateUsers < ActiveRecord::Migration[4.2] def change create_table :users do |t| t.belongs_to :consumer diff --git a/db/migrate/20140702100130_add_oauth_key_to_consumers.rb b/db/migrate/20140702100130_add_oauth_key_to_consumers.rb index 50c2cfa6..e52d914c 100644 --- a/db/migrate/20140702100130_add_oauth_key_to_consumers.rb +++ b/db/migrate/20140702100130_add_oauth_key_to_consumers.rb @@ -1,4 +1,4 @@ -class AddOauthKeyToConsumers < ActiveRecord::Migration +class AddOauthKeyToConsumers < ActiveRecord::Migration[4.2] def change add_column :consumers, :oauth_key, :string end diff --git a/db/migrate/20140703070749_add_oauth_secret_to_consumers.rb b/db/migrate/20140703070749_add_oauth_secret_to_consumers.rb index 292b1063..61825748 100644 --- a/db/migrate/20140703070749_add_oauth_secret_to_consumers.rb +++ b/db/migrate/20140703070749_add_oauth_secret_to_consumers.rb @@ -1,4 +1,4 @@ -class AddOauthSecretToConsumers < ActiveRecord::Migration +class AddOauthSecretToConsumers < ActiveRecord::Migration[4.2] def change add_column :consumers, :oauth_secret, :string end diff --git a/db/migrate/20140716153147_add_role_to_users.rb b/db/migrate/20140716153147_add_role_to_users.rb index bac1e489..35228ab0 100644 --- a/db/migrate/20140716153147_add_role_to_users.rb +++ b/db/migrate/20140716153147_add_role_to_users.rb @@ -1,4 +1,4 @@ -class AddRoleToUsers < ActiveRecord::Migration +class AddRoleToUsers < ActiveRecord::Migration[4.2] def change add_column :users, :role, :string end diff --git a/db/migrate/20140717074902_add_user_id_to_exercises.rb b/db/migrate/20140717074902_add_user_id_to_exercises.rb index 6a4d9a76..55c75391 100644 --- a/db/migrate/20140717074902_add_user_id_to_exercises.rb +++ b/db/migrate/20140717074902_add_user_id_to_exercises.rb @@ -1,4 +1,4 @@ -class AddUserIdToExercises < ActiveRecord::Migration +class AddUserIdToExercises < ActiveRecord::Migration[4.2] def change add_reference :exercises, :user end diff --git a/db/migrate/20140722125431_add_run_command_to_execution_environments.rb b/db/migrate/20140722125431_add_run_command_to_execution_environments.rb index 391fd253..de7e0f21 100644 --- a/db/migrate/20140722125431_add_run_command_to_execution_environments.rb +++ b/db/migrate/20140722125431_add_run_command_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddRunCommandToExecutionEnvironments < ActiveRecord::Migration +class AddRunCommandToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_column :execution_environments, :run_command, :string end diff --git a/db/migrate/20140723135530_add_test_command_to_execution_environments.rb b/db/migrate/20140723135530_add_test_command_to_execution_environments.rb index 9e766108..fefc57eb 100644 --- a/db/migrate/20140723135530_add_test_command_to_execution_environments.rb +++ b/db/migrate/20140723135530_add_test_command_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddTestCommandToExecutionEnvironments < ActiveRecord::Migration +class AddTestCommandToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_column :execution_environments, :test_command, :string end diff --git a/db/migrate/20140723135747_add_test_code_to_exercises.rb b/db/migrate/20140723135747_add_test_code_to_exercises.rb index da5e1031..8f184f37 100644 --- a/db/migrate/20140723135747_add_test_code_to_exercises.rb +++ b/db/migrate/20140723135747_add_test_code_to_exercises.rb @@ -1,4 +1,4 @@ -class AddTestCodeToExercises < ActiveRecord::Migration +class AddTestCodeToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :test_code, :text end diff --git a/db/migrate/20140724155359_add_cause_to_submissions.rb b/db/migrate/20140724155359_add_cause_to_submissions.rb index b5bddcad..37b273ca 100644 --- a/db/migrate/20140724155359_add_cause_to_submissions.rb +++ b/db/migrate/20140724155359_add_cause_to_submissions.rb @@ -1,4 +1,4 @@ -class AddCauseToSubmissions < ActiveRecord::Migration +class AddCauseToSubmissions < ActiveRecord::Migration[4.2] def change add_column :submissions, :cause, :string end diff --git a/db/migrate/20140730114343_add_template_test_code_to_exercises.rb b/db/migrate/20140730114343_add_template_test_code_to_exercises.rb index 5490715c..a0575cbf 100644 --- a/db/migrate/20140730114343_add_template_test_code_to_exercises.rb +++ b/db/migrate/20140730114343_add_template_test_code_to_exercises.rb @@ -1,4 +1,4 @@ -class AddTemplateTestCodeToExercises < ActiveRecord::Migration +class AddTemplateTestCodeToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :template_test_code, :text end diff --git a/db/migrate/20140730115010_add_supports_user_defined_tests_to_exercises.rb b/db/migrate/20140730115010_add_supports_user_defined_tests_to_exercises.rb index 671396bf..401899b6 100644 --- a/db/migrate/20140730115010_add_supports_user_defined_tests_to_exercises.rb +++ b/db/migrate/20140730115010_add_supports_user_defined_tests_to_exercises.rb @@ -1,4 +1,4 @@ -class AddSupportsUserDefinedTestsToExercises < ActiveRecord::Migration +class AddSupportsUserDefinedTestsToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :supports_user_defined_tests, :boolean end diff --git a/db/migrate/20140805161431_add_testing_framework_to_execution_environments.rb b/db/migrate/20140805161431_add_testing_framework_to_execution_environments.rb index 4d6cb2e3..8b5ea99e 100644 --- a/db/migrate/20140805161431_add_testing_framework_to_execution_environments.rb +++ b/db/migrate/20140805161431_add_testing_framework_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddTestingFrameworkToExecutionEnvironments < ActiveRecord::Migration +class AddTestingFrameworkToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_column :execution_environments, :testing_framework, :string end diff --git a/db/migrate/20140812102114_create_file_types.rb b/db/migrate/20140812102114_create_file_types.rb index 5c1ce8a4..22fd2e4c 100644 --- a/db/migrate/20140812102114_create_file_types.rb +++ b/db/migrate/20140812102114_create_file_types.rb @@ -1,4 +1,4 @@ -class CreateFileTypes < ActiveRecord::Migration +class CreateFileTypes < ActiveRecord::Migration[4.2] def change create_table :file_types do |t| t.string :editor_mode diff --git a/db/migrate/20140812144733_create_files.rb b/db/migrate/20140812144733_create_files.rb index 9729b3bb..789981b9 100644 --- a/db/migrate/20140812144733_create_files.rb +++ b/db/migrate/20140812144733_create_files.rb @@ -1,4 +1,4 @@ -class CreateFiles < ActiveRecord::Migration +class CreateFiles < ActiveRecord::Migration[4.2] def change create_table :files do |t| t.text :content diff --git a/db/migrate/20140812150607_remove_file_type_related_columns_from_execution_environments.rb b/db/migrate/20140812150607_remove_file_type_related_columns_from_execution_environments.rb index 736f6b00..53d14dfd 100644 --- a/db/migrate/20140812150607_remove_file_type_related_columns_from_execution_environments.rb +++ b/db/migrate/20140812150607_remove_file_type_related_columns_from_execution_environments.rb @@ -1,4 +1,4 @@ -class RemoveFileTypeRelatedColumnsFromExecutionEnvironments < ActiveRecord::Migration +class RemoveFileTypeRelatedColumnsFromExecutionEnvironments < ActiveRecord::Migration[4.2] def change remove_column :execution_environments, :editor_mode, :string remove_column :execution_environments, :file_extension, :string diff --git a/db/migrate/20140812150925_remove_file_related_columns_from_exercises.rb b/db/migrate/20140812150925_remove_file_related_columns_from_exercises.rb index c2fb927e..92954d72 100644 --- a/db/migrate/20140812150925_remove_file_related_columns_from_exercises.rb +++ b/db/migrate/20140812150925_remove_file_related_columns_from_exercises.rb @@ -1,4 +1,4 @@ -class RemoveFileRelatedColumnsFromExercises < ActiveRecord::Migration +class RemoveFileRelatedColumnsFromExercises < ActiveRecord::Migration[4.2] def change remove_column :exercises, :reference_implementation, :text remove_column :exercises, :supports_user_defined_tests, :boolean diff --git a/db/migrate/20140813091722_remove_code_from_submissions.rb b/db/migrate/20140813091722_remove_code_from_submissions.rb index 91c4879c..2f8ded5e 100644 --- a/db/migrate/20140813091722_remove_code_from_submissions.rb +++ b/db/migrate/20140813091722_remove_code_from_submissions.rb @@ -1,4 +1,4 @@ -class RemoveCodeFromSubmissions < ActiveRecord::Migration +class RemoveCodeFromSubmissions < ActiveRecord::Migration[4.2] def change remove_column :submissions, :code, :text end diff --git a/db/migrate/20140820170039_add_instructions_to_exercises.rb b/db/migrate/20140820170039_add_instructions_to_exercises.rb index 19c4f4b3..aeab4a95 100644 --- a/db/migrate/20140820170039_add_instructions_to_exercises.rb +++ b/db/migrate/20140820170039_add_instructions_to_exercises.rb @@ -1,4 +1,4 @@ -class AddInstructionsToExercises < ActiveRecord::Migration +class AddInstructionsToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :instructions, :text end diff --git a/db/migrate/20140821064318_add_published_to_exercises.rb b/db/migrate/20140821064318_add_published_to_exercises.rb index c5715cd7..999ba1f5 100644 --- a/db/migrate/20140821064318_add_published_to_exercises.rb +++ b/db/migrate/20140821064318_add_published_to_exercises.rb @@ -1,4 +1,4 @@ -class AddPublishedToExercises < ActiveRecord::Migration +class AddPublishedToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :published, :boolean end diff --git a/db/migrate/20140823172643_add_executable_to_file_types.rb b/db/migrate/20140823172643_add_executable_to_file_types.rb index bb80edae..af2f975f 100644 --- a/db/migrate/20140823172643_add_executable_to_file_types.rb +++ b/db/migrate/20140823172643_add_executable_to_file_types.rb @@ -1,4 +1,4 @@ -class AddExecutableToFileTypes < ActiveRecord::Migration +class AddExecutableToFileTypes < ActiveRecord::Migration[4.2] def change add_column :file_types, :executable, :boolean end diff --git a/db/migrate/20140823173923_add_renderable_to_file_types.rb b/db/migrate/20140823173923_add_renderable_to_file_types.rb index 1917bb7c..034a480a 100644 --- a/db/migrate/20140823173923_add_renderable_to_file_types.rb +++ b/db/migrate/20140823173923_add_renderable_to_file_types.rb @@ -1,4 +1,4 @@ -class AddRenderableToFileTypes < ActiveRecord::Migration +class AddRenderableToFileTypes < ActiveRecord::Migration[4.2] def change add_column :file_types, :renderable, :boolean end diff --git a/db/migrate/20140825121336_create_external_users.rb b/db/migrate/20140825121336_create_external_users.rb index 1d6d5295..e7b9d816 100644 --- a/db/migrate/20140825121336_create_external_users.rb +++ b/db/migrate/20140825121336_create_external_users.rb @@ -1,4 +1,4 @@ -class CreateExternalUsers < ActiveRecord::Migration +class CreateExternalUsers < ActiveRecord::Migration[4.2] def change create_table :external_users do |t| t.belongs_to :consumer diff --git a/db/migrate/20140825125801_create_internal_users.rb b/db/migrate/20140825125801_create_internal_users.rb index db7eee5d..2ff2c342 100644 --- a/db/migrate/20140825125801_create_internal_users.rb +++ b/db/migrate/20140825125801_create_internal_users.rb @@ -1,4 +1,4 @@ -class CreateInternalUsers < ActiveRecord::Migration +class CreateInternalUsers < ActiveRecord::Migration[4.2] def change create_table :internal_users do |t| t.belongs_to :consumer diff --git a/db/migrate/20140825154202_drop_users.rb b/db/migrate/20140825154202_drop_users.rb index 499f5b2c..201b6433 100644 --- a/db/migrate/20140825154202_drop_users.rb +++ b/db/migrate/20140825154202_drop_users.rb @@ -1,4 +1,4 @@ -class DropUsers < ActiveRecord::Migration +class DropUsers < ActiveRecord::Migration[4.2] def change drop_table :users end diff --git a/db/migrate/20140825161350_add_user_type_to_exercises.rb b/db/migrate/20140825161350_add_user_type_to_exercises.rb index c02b71a9..c9c44d47 100644 --- a/db/migrate/20140825161350_add_user_type_to_exercises.rb +++ b/db/migrate/20140825161350_add_user_type_to_exercises.rb @@ -1,4 +1,4 @@ -class AddUserTypeToExercises < ActiveRecord::Migration +class AddUserTypeToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :user_type, :string end diff --git a/db/migrate/20140825161358_add_user_type_to_file_types.rb b/db/migrate/20140825161358_add_user_type_to_file_types.rb index 73d178d6..09eb709e 100644 --- a/db/migrate/20140825161358_add_user_type_to_file_types.rb +++ b/db/migrate/20140825161358_add_user_type_to_file_types.rb @@ -1,4 +1,4 @@ -class AddUserTypeToFileTypes < ActiveRecord::Migration +class AddUserTypeToFileTypes < ActiveRecord::Migration[4.2] def change add_column :file_types, :user_type, :string end diff --git a/db/migrate/20140825161406_add_user_type_to_submissions.rb b/db/migrate/20140825161406_add_user_type_to_submissions.rb index 098e9ab3..fd72ba6c 100644 --- a/db/migrate/20140825161406_add_user_type_to_submissions.rb +++ b/db/migrate/20140825161406_add_user_type_to_submissions.rb @@ -1,4 +1,4 @@ -class AddUserTypeToSubmissions < ActiveRecord::Migration +class AddUserTypeToSubmissions < ActiveRecord::Migration[4.2] def change add_column :submissions, :user_type, :string end diff --git a/db/migrate/20140826073318_sorcery_core.rb b/db/migrate/20140826073318_sorcery_core.rb index d196f588..077e88b8 100644 --- a/db/migrate/20140826073318_sorcery_core.rb +++ b/db/migrate/20140826073318_sorcery_core.rb @@ -1,4 +1,4 @@ -class SorceryCore < ActiveRecord::Migration +class SorceryCore < ActiveRecord::Migration[4.2] def change InternalUser.delete_all add_column :internal_users, :crypted_password, :string, null: false diff --git a/db/migrate/20140826073319_sorcery_brute_force_protection.rb b/db/migrate/20140826073319_sorcery_brute_force_protection.rb index 7bac89d9..abd9d835 100644 --- a/db/migrate/20140826073319_sorcery_brute_force_protection.rb +++ b/db/migrate/20140826073319_sorcery_brute_force_protection.rb @@ -1,4 +1,4 @@ -class SorceryBruteForceProtection < ActiveRecord::Migration +class SorceryBruteForceProtection < ActiveRecord::Migration[4.2] def change add_column :internal_users, :failed_logins_count, :integer, default: 0 add_column :internal_users, :lock_expires_at, :datetime, default: nil diff --git a/db/migrate/20140826073320_sorcery_remember_me.rb b/db/migrate/20140826073320_sorcery_remember_me.rb index 3b14bca8..f232eb09 100644 --- a/db/migrate/20140826073320_sorcery_remember_me.rb +++ b/db/migrate/20140826073320_sorcery_remember_me.rb @@ -1,4 +1,4 @@ -class SorceryRememberMe < ActiveRecord::Migration +class SorceryRememberMe < ActiveRecord::Migration[4.2] def change add_column :internal_users, :remember_me_token, :string, default: nil add_column :internal_users, :remember_me_token_expires_at, :datetime, default: nil diff --git a/db/migrate/20140826073321_sorcery_reset_password.rb b/db/migrate/20140826073321_sorcery_reset_password.rb index 3ca6a3e4..74ab7c8d 100644 --- a/db/migrate/20140826073321_sorcery_reset_password.rb +++ b/db/migrate/20140826073321_sorcery_reset_password.rb @@ -1,4 +1,4 @@ -class SorceryResetPassword < ActiveRecord::Migration +class SorceryResetPassword < ActiveRecord::Migration[4.2] def change add_column :internal_users, :reset_password_token, :string, default: nil add_column :internal_users, :reset_password_token_expires_at, :datetime, default: nil diff --git a/db/migrate/20140826073322_sorcery_user_activation.rb b/db/migrate/20140826073322_sorcery_user_activation.rb index 577273af..47fc60f1 100644 --- a/db/migrate/20140826073322_sorcery_user_activation.rb +++ b/db/migrate/20140826073322_sorcery_user_activation.rb @@ -1,4 +1,4 @@ -class SorceryUserActivation < ActiveRecord::Migration +class SorceryUserActivation < ActiveRecord::Migration[4.2] def change add_column :internal_users, :activation_state, :string, default: nil add_column :internal_users, :activation_token, :string, default: nil diff --git a/db/migrate/20140827065359_add_binary_to_file_types.rb b/db/migrate/20140827065359_add_binary_to_file_types.rb index 2ac3e006..0fa3c65d 100644 --- a/db/migrate/20140827065359_add_binary_to_file_types.rb +++ b/db/migrate/20140827065359_add_binary_to_file_types.rb @@ -1,4 +1,4 @@ -class AddBinaryToFileTypes < ActiveRecord::Migration +class AddBinaryToFileTypes < ActiveRecord::Migration[4.2] def change add_column :file_types, :binary, :boolean end diff --git a/db/migrate/20140827083957_add_native_file_to_files.rb b/db/migrate/20140827083957_add_native_file_to_files.rb index 967bbd92..cafe27f5 100644 --- a/db/migrate/20140827083957_add_native_file_to_files.rb +++ b/db/migrate/20140827083957_add_native_file_to_files.rb @@ -1,4 +1,4 @@ -class AddNativeFileToFiles < ActiveRecord::Migration +class AddNativeFileToFiles < ActiveRecord::Migration[4.2] def change add_column :files, :native_file, :string end diff --git a/db/migrate/20140829141913_create_hints.rb b/db/migrate/20140829141913_create_hints.rb index cf91a20e..ca49c91b 100644 --- a/db/migrate/20140829141913_create_hints.rb +++ b/db/migrate/20140829141913_create_hints.rb @@ -1,4 +1,4 @@ -class CreateHints < ActiveRecord::Migration +class CreateHints < ActiveRecord::Migration[4.2] def change create_table :hints do |t| t.belongs_to :execution_environment diff --git a/db/migrate/20140903093436_remove_not_null_constraints_from_internal_users.rb b/db/migrate/20140903093436_remove_not_null_constraints_from_internal_users.rb index dbd6d05c..d1f11303 100644 --- a/db/migrate/20140903093436_remove_not_null_constraints_from_internal_users.rb +++ b/db/migrate/20140903093436_remove_not_null_constraints_from_internal_users.rb @@ -1,4 +1,4 @@ -class RemoveNotNullConstraintsFromInternalUsers < ActiveRecord::Migration +class RemoveNotNullConstraintsFromInternalUsers < ActiveRecord::Migration[4.2] def change change_column_null(:internal_users, :crypted_password, true) change_column_null(:internal_users, :salt, true) diff --git a/db/migrate/20140903165113_create_errors.rb b/db/migrate/20140903165113_create_errors.rb index 9c14ed14..27534d38 100644 --- a/db/migrate/20140903165113_create_errors.rb +++ b/db/migrate/20140903165113_create_errors.rb @@ -1,4 +1,4 @@ -class CreateErrors < ActiveRecord::Migration +class CreateErrors < ActiveRecord::Migration[4.2] def change create_table :errors do |t| t.belongs_to :execution_environment diff --git a/db/migrate/20140904082810_add_token_to_exercises.rb b/db/migrate/20140904082810_add_token_to_exercises.rb index f184613c..14e7a459 100644 --- a/db/migrate/20140904082810_add_token_to_exercises.rb +++ b/db/migrate/20140904082810_add_token_to_exercises.rb @@ -1,4 +1,4 @@ -class AddTokenToExercises < ActiveRecord::Migration +class AddTokenToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :token, :string end diff --git a/db/migrate/20140909115430_add_file_id_to_exercises.rb b/db/migrate/20140909115430_add_file_id_to_exercises.rb index bfc4991d..09990eac 100644 --- a/db/migrate/20140909115430_add_file_id_to_exercises.rb +++ b/db/migrate/20140909115430_add_file_id_to_exercises.rb @@ -1,4 +1,4 @@ -class AddFileIdToExercises < ActiveRecord::Migration +class AddFileIdToExercises < ActiveRecord::Migration[4.2] def change add_reference :exercises, :file end diff --git a/db/migrate/20140915095420_add_role_to_files.rb b/db/migrate/20140915095420_add_role_to_files.rb index 79ba64b2..47774e4c 100644 --- a/db/migrate/20140915095420_add_role_to_files.rb +++ b/db/migrate/20140915095420_add_role_to_files.rb @@ -1,4 +1,4 @@ -class AddRoleToFiles < ActiveRecord::Migration +class AddRoleToFiles < ActiveRecord::Migration[4.2] def change add_column :files, :role, :string end diff --git a/db/migrate/20140915122846_remove_file_id_from_exercises.rb b/db/migrate/20140915122846_remove_file_id_from_exercises.rb index 73a5effb..955f4d13 100644 --- a/db/migrate/20140915122846_remove_file_id_from_exercises.rb +++ b/db/migrate/20140915122846_remove_file_id_from_exercises.rb @@ -1,4 +1,4 @@ -class RemoveFileIdFromExercises < ActiveRecord::Migration +class RemoveFileIdFromExercises < ActiveRecord::Migration[4.2] def change remove_reference :exercises, :file end diff --git a/db/migrate/20140918063522_add_hashed_content_to_files.rb b/db/migrate/20140918063522_add_hashed_content_to_files.rb index c4606181..44c103f0 100644 --- a/db/migrate/20140918063522_add_hashed_content_to_files.rb +++ b/db/migrate/20140918063522_add_hashed_content_to_files.rb @@ -1,4 +1,4 @@ -class AddHashedContentToFiles < ActiveRecord::Migration +class AddHashedContentToFiles < ActiveRecord::Migration[4.2] def change add_column :files, :hashed_content, :string diff --git a/db/migrate/20140922161120_add_feedback_message_to_files.rb b/db/migrate/20140922161120_add_feedback_message_to_files.rb index 18da6444..a5908e00 100644 --- a/db/migrate/20140922161120_add_feedback_message_to_files.rb +++ b/db/migrate/20140922161120_add_feedback_message_to_files.rb @@ -1,4 +1,4 @@ -class AddFeedbackMessageToFiles < ActiveRecord::Migration +class AddFeedbackMessageToFiles < ActiveRecord::Migration[4.2] def change add_column :files, :feedback_message, :string end diff --git a/db/migrate/20140922161226_add_weight_to_files.rb b/db/migrate/20140922161226_add_weight_to_files.rb index a22388f4..c2664529 100644 --- a/db/migrate/20140922161226_add_weight_to_files.rb +++ b/db/migrate/20140922161226_add_weight_to_files.rb @@ -1,4 +1,4 @@ -class AddWeightToFiles < ActiveRecord::Migration +class AddWeightToFiles < ActiveRecord::Migration[4.2] def change add_column :files, :weight, :float end diff --git a/db/migrate/20141003072729_add_help_to_execution_environments.rb b/db/migrate/20141003072729_add_help_to_execution_environments.rb index 631dd278..057aac53 100644 --- a/db/migrate/20141003072729_add_help_to_execution_environments.rb +++ b/db/migrate/20141003072729_add_help_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddHelpToExecutionEnvironments < ActiveRecord::Migration +class AddHelpToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_column :execution_environments, :help, :text end diff --git a/db/migrate/20141004114747_add_exposed_ports_to_execution_environments.rb b/db/migrate/20141004114747_add_exposed_ports_to_execution_environments.rb index dcc00d03..b930022b 100644 --- a/db/migrate/20141004114747_add_exposed_ports_to_execution_environments.rb +++ b/db/migrate/20141004114747_add_exposed_ports_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddExposedPortsToExecutionEnvironments < ActiveRecord::Migration +class AddExposedPortsToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_column :execution_environments, :exposed_ports, :string end diff --git a/db/migrate/20141009110434_add_permitted_execution_time_to_execution_environments.rb b/db/migrate/20141009110434_add_permitted_execution_time_to_execution_environments.rb index 5bd83ff1..c7c73ca3 100644 --- a/db/migrate/20141009110434_add_permitted_execution_time_to_execution_environments.rb +++ b/db/migrate/20141009110434_add_permitted_execution_time_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddPermittedExecutionTimeToExecutionEnvironments < ActiveRecord::Migration +class AddPermittedExecutionTimeToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_column :execution_environments, :permitted_execution_time, :integer end diff --git a/db/migrate/20141011145303_add_user_id_and_user_type_to_execution_environments.rb b/db/migrate/20141011145303_add_user_id_and_user_type_to_execution_environments.rb index 2acd1797..b0ef78fc 100644 --- a/db/migrate/20141011145303_add_user_id_and_user_type_to_execution_environments.rb +++ b/db/migrate/20141011145303_add_user_id_and_user_type_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddUserIdAndUserTypeToExecutionEnvironments < ActiveRecord::Migration +class AddUserIdAndUserTypeToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_reference :execution_environments, :user add_column :execution_environments, :user_type, :string diff --git a/db/migrate/20141017110211_rename_published_to_public.rb b/db/migrate/20141017110211_rename_published_to_public.rb index e20b2282..f651460c 100644 --- a/db/migrate/20141017110211_rename_published_to_public.rb +++ b/db/migrate/20141017110211_rename_published_to_public.rb @@ -1,4 +1,4 @@ -class RenamePublishedToPublic < ActiveRecord::Migration +class RenamePublishedToPublic < ActiveRecord::Migration[4.2] def change rename_column :exercises, :published, :public end diff --git a/db/migrate/20141031161603_add_path_to_files.rb b/db/migrate/20141031161603_add_path_to_files.rb index 9d5501cf..46b012d4 100644 --- a/db/migrate/20141031161603_add_path_to_files.rb +++ b/db/migrate/20141031161603_add_path_to_files.rb @@ -1,4 +1,4 @@ -class AddPathToFiles < ActiveRecord::Migration +class AddPathToFiles < ActiveRecord::Migration[4.2] def change add_column :files, :path, :string end diff --git a/db/migrate/20141119131607_create_comments.rb b/db/migrate/20141119131607_create_comments.rb index faff81cd..eb697231 100644 --- a/db/migrate/20141119131607_create_comments.rb +++ b/db/migrate/20141119131607_create_comments.rb @@ -1,4 +1,4 @@ -class CreateComments < ActiveRecord::Migration +class CreateComments < ActiveRecord::Migration[4.2] def change create_table :comments do |t| t.references :user, index: true diff --git a/db/migrate/20150128083123_create_teams.rb b/db/migrate/20150128083123_create_teams.rb index cdb0da35..f2f7c543 100644 --- a/db/migrate/20150128083123_create_teams.rb +++ b/db/migrate/20150128083123_create_teams.rb @@ -1,4 +1,4 @@ -class CreateTeams < ActiveRecord::Migration +class CreateTeams < ActiveRecord::Migration[4.2] def change create_table :teams do |t| t.string :name diff --git a/db/migrate/20150128084834_create_internal_users_teams.rb b/db/migrate/20150128084834_create_internal_users_teams.rb index 8fe51717..ece283c2 100644 --- a/db/migrate/20150128084834_create_internal_users_teams.rb +++ b/db/migrate/20150128084834_create_internal_users_teams.rb @@ -1,4 +1,4 @@ -class CreateInternalUsersTeams < ActiveRecord::Migration +class CreateInternalUsersTeams < ActiveRecord::Migration[4.2] def change create_table :internal_users_teams do |t| t.belongs_to :internal_user, index: true diff --git a/db/migrate/20150128093003_add_team_id_to_exercises.rb b/db/migrate/20150128093003_add_team_id_to_exercises.rb index 7911bdf7..04c58cd0 100644 --- a/db/migrate/20150128093003_add_team_id_to_exercises.rb +++ b/db/migrate/20150128093003_add_team_id_to_exercises.rb @@ -1,4 +1,4 @@ -class AddTeamIdToExercises < ActiveRecord::Migration +class AddTeamIdToExercises < ActiveRecord::Migration[4.2] def change add_reference :exercises, :team end diff --git a/db/migrate/20150204080832_add_pool_size_to_execution_environments.rb b/db/migrate/20150204080832_add_pool_size_to_execution_environments.rb index 0980984f..aab35315 100644 --- a/db/migrate/20150204080832_add_pool_size_to_execution_environments.rb +++ b/db/migrate/20150204080832_add_pool_size_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddPoolSizeToExecutionEnvironments < ActiveRecord::Migration +class AddPoolSizeToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_column :execution_environments, :pool_size, :integer diff --git a/db/migrate/20150310150712_add_file_type_id_to_execution_environments.rb b/db/migrate/20150310150712_add_file_type_id_to_execution_environments.rb index d57907c2..214bcec0 100644 --- a/db/migrate/20150310150712_add_file_type_id_to_execution_environments.rb +++ b/db/migrate/20150310150712_add_file_type_id_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddFileTypeIdToExecutionEnvironments < ActiveRecord::Migration +class AddFileTypeIdToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_reference :execution_environments, :file_type end diff --git a/db/migrate/20150317083739_add_memory_limit_to_execution_environments.rb b/db/migrate/20150317083739_add_memory_limit_to_execution_environments.rb index b2ee957c..899b7d50 100644 --- a/db/migrate/20150317083739_add_memory_limit_to_execution_environments.rb +++ b/db/migrate/20150317083739_add_memory_limit_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddMemoryLimitToExecutionEnvironments < ActiveRecord::Migration +class AddMemoryLimitToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_column :execution_environments, :memory_limit, :integer diff --git a/db/migrate/20150317115338_add_network_enabled_to_execution_environments.rb b/db/migrate/20150317115338_add_network_enabled_to_execution_environments.rb index 2d86bdaf..c328a298 100644 --- a/db/migrate/20150317115338_add_network_enabled_to_execution_environments.rb +++ b/db/migrate/20150317115338_add_network_enabled_to_execution_environments.rb @@ -1,4 +1,4 @@ -class AddNetworkEnabledToExecutionEnvironments < ActiveRecord::Migration +class AddNetworkEnabledToExecutionEnvironments < ActiveRecord::Migration[4.2] def change add_column :execution_environments, :network_enabled, :boolean diff --git a/db/migrate/20150327141740_create_request_for_comments.rb b/db/migrate/20150327141740_create_request_for_comments.rb index ad3882d2..a89b947b 100644 --- a/db/migrate/20150327141740_create_request_for_comments.rb +++ b/db/migrate/20150327141740_create_request_for_comments.rb @@ -1,4 +1,4 @@ -class CreateRequestForComments < ActiveRecord::Migration +class CreateRequestForComments < ActiveRecord::Migration[4.2] def change create_table :request_for_comments do |t| t.integer :requestorid, :null => false diff --git a/db/migrate/20150408155923_add_submission_to_error.rb b/db/migrate/20150408155923_add_submission_to_error.rb index d90d1679..e7363e0d 100644 --- a/db/migrate/20150408155923_add_submission_to_error.rb +++ b/db/migrate/20150408155923_add_submission_to_error.rb @@ -1,4 +1,4 @@ -class AddSubmissionToError < ActiveRecord::Migration +class AddSubmissionToError < ActiveRecord::Migration[4.2] def change add_reference :errors, :submission, index: true end diff --git a/db/migrate/20150421074734_add_file_index_to_files.rb b/db/migrate/20150421074734_add_file_index_to_files.rb index ffa20ae8..c18d09f1 100644 --- a/db/migrate/20150421074734_add_file_index_to_files.rb +++ b/db/migrate/20150421074734_add_file_index_to_files.rb @@ -1,4 +1,4 @@ -class AddFileIndexToFiles < ActiveRecord::Migration +class AddFileIndexToFiles < ActiveRecord::Migration[4.2] def change add_index(:files, [:context_id, :context_type]) end diff --git a/db/migrate/20150818141554_add_user_type_to_request_for_comments.rb b/db/migrate/20150818141554_add_user_type_to_request_for_comments.rb index f1450bff..86eff763 100644 --- a/db/migrate/20150818141554_add_user_type_to_request_for_comments.rb +++ b/db/migrate/20150818141554_add_user_type_to_request_for_comments.rb @@ -1,4 +1,4 @@ -class AddUserTypeToRequestForComments < ActiveRecord::Migration +class AddUserTypeToRequestForComments < ActiveRecord::Migration[4.2] def change add_column :request_for_comments, :user_type, :string end diff --git a/db/migrate/20150818142251_correct_column_names.rb b/db/migrate/20150818142251_correct_column_names.rb index dd73814a..0fcb2d70 100644 --- a/db/migrate/20150818142251_correct_column_names.rb +++ b/db/migrate/20150818142251_correct_column_names.rb @@ -1,4 +1,4 @@ -class CorrectColumnNames < ActiveRecord::Migration +class CorrectColumnNames < ActiveRecord::Migration[4.2] def change rename_column :request_for_comments, :requestorid, :requestor_user_id rename_column :request_for_comments, :exerciseid, :exercise_id diff --git a/db/migrate/20150903152727_remove_requestor_from_request_for_comments.rb b/db/migrate/20150903152727_remove_requestor_from_request_for_comments.rb index f9857f43..134649bf 100644 --- a/db/migrate/20150903152727_remove_requestor_from_request_for_comments.rb +++ b/db/migrate/20150903152727_remove_requestor_from_request_for_comments.rb @@ -1,4 +1,4 @@ -class RemoveRequestorFromRequestForComments < ActiveRecord::Migration +class RemoveRequestorFromRequestForComments < ActiveRecord::Migration[4.2] def change rename_column :request_for_comments, :requestor_user_id, :user_id end diff --git a/db/migrate/20150922125415_add_hide_file_tree_to_exercises.rb b/db/migrate/20150922125415_add_hide_file_tree_to_exercises.rb index 9890c35b..fce56b0a 100644 --- a/db/migrate/20150922125415_add_hide_file_tree_to_exercises.rb +++ b/db/migrate/20150922125415_add_hide_file_tree_to_exercises.rb @@ -1,4 +1,4 @@ -class AddHideFileTreeToExercises < ActiveRecord::Migration +class AddHideFileTreeToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :hide_file_tree, :boolean end diff --git a/db/migrate/20160204094409_create_code_harbor_links.rb b/db/migrate/20160204094409_create_code_harbor_links.rb index b87d5c9d..f159362b 100644 --- a/db/migrate/20160204094409_create_code_harbor_links.rb +++ b/db/migrate/20160204094409_create_code_harbor_links.rb @@ -1,4 +1,4 @@ -class CreateCodeHarborLinks < ActiveRecord::Migration +class CreateCodeHarborLinks < ActiveRecord::Migration[4.2] def change create_table :code_harbor_links do |t| t.string :oauth2token diff --git a/db/migrate/20160204111716_add_user_to_code_harbor_link.rb b/db/migrate/20160204111716_add_user_to_code_harbor_link.rb index fe509666..0bf4b575 100644 --- a/db/migrate/20160204111716_add_user_to_code_harbor_link.rb +++ b/db/migrate/20160204111716_add_user_to_code_harbor_link.rb @@ -1,4 +1,4 @@ -class AddUserToCodeHarborLink < ActiveRecord::Migration +class AddUserToCodeHarborLink < ActiveRecord::Migration[4.2] def change add_reference :code_harbor_links, :user, polymorphic: true, index: true end diff --git a/db/migrate/20160302133540_create_testruns.rb b/db/migrate/20160302133540_create_testruns.rb index 7590f2a3..fa622cfe 100644 --- a/db/migrate/20160302133540_create_testruns.rb +++ b/db/migrate/20160302133540_create_testruns.rb @@ -1,4 +1,4 @@ -class CreateTestruns < ActiveRecord::Migration +class CreateTestruns < ActiveRecord::Migration[4.2] def change create_table :testruns do |t| t.boolean :passed diff --git a/db/migrate/20160426114951_add_question_to_request_for_comments.rb b/db/migrate/20160426114951_add_question_to_request_for_comments.rb index 3c9fac17..31c3f60e 100644 --- a/db/migrate/20160426114951_add_question_to_request_for_comments.rb +++ b/db/migrate/20160426114951_add_question_to_request_for_comments.rb @@ -1,4 +1,4 @@ -class AddQuestionToRequestForComments < ActiveRecord::Migration +class AddQuestionToRequestForComments < ActiveRecord::Migration[4.2] def change add_column :request_for_comments, :question, :text end diff --git a/db/migrate/20160510145341_add_allow_file_creation_to_exercises.rb b/db/migrate/20160510145341_add_allow_file_creation_to_exercises.rb index ade1bb26..37df9904 100644 --- a/db/migrate/20160510145341_add_allow_file_creation_to_exercises.rb +++ b/db/migrate/20160510145341_add_allow_file_creation_to_exercises.rb @@ -1,4 +1,4 @@ -class AddAllowFileCreationToExercises < ActiveRecord::Migration +class AddAllowFileCreationToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :allow_file_creation, :boolean end diff --git a/db/migrate/20160512131539_change_comment_text_attribute_to_text_datatype.rb b/db/migrate/20160512131539_change_comment_text_attribute_to_text_datatype.rb index cc039c6a..148ec9ea 100644 --- a/db/migrate/20160512131539_change_comment_text_attribute_to_text_datatype.rb +++ b/db/migrate/20160512131539_change_comment_text_attribute_to_text_datatype.rb @@ -1,4 +1,4 @@ -class ChangeCommentTextAttributeToTextDatatype < ActiveRecord::Migration +class ChangeCommentTextAttributeToTextDatatype < ActiveRecord::Migration[4.2] def up change_column :comments, :text, :text end diff --git a/db/migrate/20160609185708_create_file_templates.rb b/db/migrate/20160609185708_create_file_templates.rb index 43cde0d0..aa111443 100644 --- a/db/migrate/20160609185708_create_file_templates.rb +++ b/db/migrate/20160609185708_create_file_templates.rb @@ -1,4 +1,4 @@ -class CreateFileTemplates < ActiveRecord::Migration +class CreateFileTemplates < ActiveRecord::Migration[4.2] def change create_table :file_templates do |t| t.string :name diff --git a/db/migrate/20160610111602_add_file_template_to_file.rb b/db/migrate/20160610111602_add_file_template_to_file.rb index a595e90b..dbcc83e5 100644 --- a/db/migrate/20160610111602_add_file_template_to_file.rb +++ b/db/migrate/20160610111602_add_file_template_to_file.rb @@ -1,4 +1,4 @@ -class AddFileTemplateToFile < ActiveRecord::Migration +class AddFileTemplateToFile < ActiveRecord::Migration[4.2] def change add_reference :files, :file_template end diff --git a/db/migrate/20160624130951_add_solved_to_request_for_comments.rb b/db/migrate/20160624130951_add_solved_to_request_for_comments.rb index f9061ea0..bfa1e2a1 100644 --- a/db/migrate/20160624130951_add_solved_to_request_for_comments.rb +++ b/db/migrate/20160624130951_add_solved_to_request_for_comments.rb @@ -1,4 +1,4 @@ -class AddSolvedToRequestForComments < ActiveRecord::Migration +class AddSolvedToRequestForComments < ActiveRecord::Migration[4.2] def change add_column :request_for_comments, :solved, :boolean end diff --git a/db/migrate/20160630154310_add_submission_to_request_for_comments.rb b/db/migrate/20160630154310_add_submission_to_request_for_comments.rb index a532bcf9..4e2e1494 100644 --- a/db/migrate/20160630154310_add_submission_to_request_for_comments.rb +++ b/db/migrate/20160630154310_add_submission_to_request_for_comments.rb @@ -1,4 +1,4 @@ -class AddSubmissionToRequestForComments < ActiveRecord::Migration +class AddSubmissionToRequestForComments < ActiveRecord::Migration[4.2] def change add_reference :request_for_comments, :submission end diff --git a/db/migrate/20160701092140_remove_requested_at_from_request_for_comments.rb b/db/migrate/20160701092140_remove_requested_at_from_request_for_comments.rb index bb5611f6..75f2236a 100644 --- a/db/migrate/20160701092140_remove_requested_at_from_request_for_comments.rb +++ b/db/migrate/20160701092140_remove_requested_at_from_request_for_comments.rb @@ -1,4 +1,4 @@ -class RemoveRequestedAtFromRequestForComments < ActiveRecord::Migration +class RemoveRequestedAtFromRequestForComments < ActiveRecord::Migration[4.2] def change remove_column :request_for_comments, :requested_at end diff --git a/db/migrate/20160704143402_remove_teams.rb b/db/migrate/20160704143402_remove_teams.rb index 20b8a204..f093a4f6 100644 --- a/db/migrate/20160704143402_remove_teams.rb +++ b/db/migrate/20160704143402_remove_teams.rb @@ -1,4 +1,4 @@ -class RemoveTeams < ActiveRecord::Migration +class RemoveTeams < ActiveRecord::Migration[4.2] def change remove_reference :exercises, :team drop_table :teams diff --git a/db/migrate/20160907123009_add_allow_auto_completion_to_exercises.rb b/db/migrate/20160907123009_add_allow_auto_completion_to_exercises.rb index 99941e36..03ce4757 100644 --- a/db/migrate/20160907123009_add_allow_auto_completion_to_exercises.rb +++ b/db/migrate/20160907123009_add_allow_auto_completion_to_exercises.rb @@ -1,4 +1,4 @@ -class AddAllowAutoCompletionToExercises < ActiveRecord::Migration +class AddAllowAutoCompletionToExercises < ActiveRecord::Migration[4.2] def change add_column :exercises, :allow_auto_completion, :boolean, default: false end diff --git a/db/migrate/20170112151637_create_lti_parameters.rb b/db/migrate/20170112151637_create_lti_parameters.rb index c7613edf..b6370387 100644 --- a/db/migrate/20170112151637_create_lti_parameters.rb +++ b/db/migrate/20170112151637_create_lti_parameters.rb @@ -1,10 +1,10 @@ -class CreateLtiParameters < ActiveRecord::Migration +class CreateLtiParameters < ActiveRecord::Migration[4.2] def change create_table :lti_parameters do |t| t.belongs_to :external_users t.belongs_to :consumers t.belongs_to :exercises - t.jsonb :lti_parameters, null: false, default: '{}' + t.jsonb :lti_parameters, null: false, default: {} t.timestamps end end diff --git a/db/migrate/20170202170437_create_remote_evaluation_mappings.rb b/db/migrate/20170202170437_create_remote_evaluation_mappings.rb index d102370d..f04cc2ed 100644 --- a/db/migrate/20170202170437_create_remote_evaluation_mappings.rb +++ b/db/migrate/20170202170437_create_remote_evaluation_mappings.rb @@ -1,4 +1,4 @@ -class CreateRemoteEvaluationMappings < ActiveRecord::Migration +class CreateRemoteEvaluationMappings < ActiveRecord::Migration[4.2] def change create_table :remote_evaluation_mappings do |t| t.integer "user_id", null: false diff --git a/db/migrate/20170205163247_create_exercise_collections.rb b/db/migrate/20170205163247_create_exercise_collections.rb index ef27f756..157a96f8 100644 --- a/db/migrate/20170205163247_create_exercise_collections.rb +++ b/db/migrate/20170205163247_create_exercise_collections.rb @@ -1,4 +1,4 @@ -class CreateExerciseCollections < ActiveRecord::Migration +class CreateExerciseCollections < ActiveRecord::Migration[4.2] def change create_table :exercise_collections do |t| t.string :name diff --git a/db/migrate/20170205165450_create_proxy_exercises.rb b/db/migrate/20170205165450_create_proxy_exercises.rb index fb2704ce..7bb9d8cc 100644 --- a/db/migrate/20170205165450_create_proxy_exercises.rb +++ b/db/migrate/20170205165450_create_proxy_exercises.rb @@ -1,4 +1,4 @@ -class CreateProxyExercises < ActiveRecord::Migration +class CreateProxyExercises < ActiveRecord::Migration[4.2] def change create_table :proxy_exercises do |t| t.string :title diff --git a/db/migrate/20170205210357_create_interventions.rb b/db/migrate/20170205210357_create_interventions.rb index 07223e1c..b8eb3132 100644 --- a/db/migrate/20170205210357_create_interventions.rb +++ b/db/migrate/20170205210357_create_interventions.rb @@ -1,4 +1,4 @@ -class CreateInterventions < ActiveRecord::Migration +class CreateInterventions < ActiveRecord::Migration[4.2] def change create_table :user_exercise_interventions do |t| t.belongs_to :user, polymorphic: true diff --git a/db/migrate/20170206141210_add_tags.rb b/db/migrate/20170206141210_add_tags.rb index 8c0c129a..77d048af 100644 --- a/db/migrate/20170206141210_add_tags.rb +++ b/db/migrate/20170206141210_add_tags.rb @@ -1,4 +1,4 @@ -class AddTags < ActiveRecord::Migration +class AddTags < ActiveRecord::Migration[4.2] def change add_column :exercises, :expected_worktime_seconds, :integer, default: 60 diff --git a/db/migrate/20170206152503_add_user_feedback.rb b/db/migrate/20170206152503_add_user_feedback.rb index f62ccd9d..c860e630 100644 --- a/db/migrate/20170206152503_add_user_feedback.rb +++ b/db/migrate/20170206152503_add_user_feedback.rb @@ -1,4 +1,4 @@ -class AddUserFeedback < ActiveRecord::Migration +class AddUserFeedback < ActiveRecord::Migration[4.2] def change create_table :user_exercise_feedbacks do |t| t.belongs_to :exercise, null: false diff --git a/db/migrate/20170228165741_add_search.rb b/db/migrate/20170228165741_add_search.rb index a36d94ff..c7168cc6 100644 --- a/db/migrate/20170228165741_add_search.rb +++ b/db/migrate/20170228165741_add_search.rb @@ -1,4 +1,4 @@ -class AddSearch < ActiveRecord::Migration +class AddSearch < ActiveRecord::Migration[4.2] def change create_table :searches do |t| t.belongs_to :exercise, null: false diff --git a/db/migrate/20170321150454_add_reason_to_user_proxy_exercise_exercise.rb b/db/migrate/20170321150454_add_reason_to_user_proxy_exercise_exercise.rb index 93ab0cf0..f257dcf3 100644 --- a/db/migrate/20170321150454_add_reason_to_user_proxy_exercise_exercise.rb +++ b/db/migrate/20170321150454_add_reason_to_user_proxy_exercise_exercise.rb @@ -1,4 +1,4 @@ -class AddReasonToUserProxyExerciseExercise < ActiveRecord::Migration +class AddReasonToUserProxyExerciseExercise < ActiveRecord::Migration[4.2] def change change_table :user_proxy_exercise_exercises do |t| t.string :reason diff --git a/db/migrate/20170323130756_add_index_to_submissions.rb b/db/migrate/20170323130756_add_index_to_submissions.rb index 185500d3..128f43bb 100644 --- a/db/migrate/20170323130756_add_index_to_submissions.rb +++ b/db/migrate/20170323130756_add_index_to_submissions.rb @@ -1,4 +1,4 @@ -class AddIndexToSubmissions < ActiveRecord::Migration +class AddIndexToSubmissions < ActiveRecord::Migration[4.2] def change add_index :submissions, :exercise_id add_index :submissions, :user_id diff --git a/db/migrate/20170403162848_set_default_for_request_for_comment_solved.rb b/db/migrate/20170403162848_set_default_for_request_for_comment_solved.rb index 6e01dcd4..d952e51a 100644 --- a/db/migrate/20170403162848_set_default_for_request_for_comment_solved.rb +++ b/db/migrate/20170403162848_set_default_for_request_for_comment_solved.rb @@ -1,4 +1,4 @@ -class SetDefaultForRequestForCommentSolved < ActiveRecord::Migration +class SetDefaultForRequestForCommentSolved < ActiveRecord::Migration[4.2] def change change_column_default :request_for_comments, :solved, false RequestForComment.where(solved: nil).update_all(solved: false) diff --git a/db/migrate/20170411090543_improve_user_feedback.rb b/db/migrate/20170411090543_improve_user_feedback.rb index bfbd8a02..bf89aafc 100644 --- a/db/migrate/20170411090543_improve_user_feedback.rb +++ b/db/migrate/20170411090543_improve_user_feedback.rb @@ -1,4 +1,4 @@ -class ImproveUserFeedback < ActiveRecord::Migration +class ImproveUserFeedback < ActiveRecord::Migration[4.2] def change add_column :user_exercise_feedbacks, :user_estimated_worktime, :integer end diff --git a/db/migrate/20170608141612_add_thank_you_note_to_request_for_comments.rb b/db/migrate/20170608141612_add_thank_you_note_to_request_for_comments.rb index fb767aad..b75f3589 100644 --- a/db/migrate/20170608141612_add_thank_you_note_to_request_for_comments.rb +++ b/db/migrate/20170608141612_add_thank_you_note_to_request_for_comments.rb @@ -1,4 +1,4 @@ -class AddThankYouNoteToRequestForComments < ActiveRecord::Migration +class AddThankYouNoteToRequestForComments < ActiveRecord::Migration[4.2] def change add_column :request_for_comments, :thank_you_note, :text end diff --git a/db/migrate/20170703075832_create_error_templates.rb b/db/migrate/20170703075832_create_error_templates.rb index 6f442842..5556f4ea 100644 --- a/db/migrate/20170703075832_create_error_templates.rb +++ b/db/migrate/20170703075832_create_error_templates.rb @@ -1,4 +1,4 @@ -class CreateErrorTemplates < ActiveRecord::Migration +class CreateErrorTemplates < ActiveRecord::Migration[4.2] def change create_table :error_templates do |t| t.belongs_to :execution_environment diff --git a/db/migrate/20170703075959_create_error_template_attributes.rb b/db/migrate/20170703075959_create_error_template_attributes.rb index 3503fcac..aa733b09 100644 --- a/db/migrate/20170703075959_create_error_template_attributes.rb +++ b/db/migrate/20170703075959_create_error_template_attributes.rb @@ -1,4 +1,4 @@ -class CreateErrorTemplateAttributes < ActiveRecord::Migration +class CreateErrorTemplateAttributes < ActiveRecord::Migration[4.2] def change create_table :error_template_attributes do |t| t.belongs_to :error_template diff --git a/db/migrate/20170703080205_create_structured_errors.rb b/db/migrate/20170703080205_create_structured_errors.rb index 560649b4..111923ca 100644 --- a/db/migrate/20170703080205_create_structured_errors.rb +++ b/db/migrate/20170703080205_create_structured_errors.rb @@ -1,4 +1,4 @@ -class CreateStructuredErrors < ActiveRecord::Migration +class CreateStructuredErrors < ActiveRecord::Migration[4.2] def change create_table :structured_errors do |t| t.references :error_template diff --git a/db/migrate/20170703080355_create_structured_error_attributes.rb b/db/migrate/20170703080355_create_structured_error_attributes.rb index aa9ee04e..81bc1d19 100644 --- a/db/migrate/20170703080355_create_structured_error_attributes.rb +++ b/db/migrate/20170703080355_create_structured_error_attributes.rb @@ -1,4 +1,4 @@ -class CreateStructuredErrorAttributes < ActiveRecord::Migration +class CreateStructuredErrorAttributes < ActiveRecord::Migration[4.2] def change create_table :structured_error_attributes do |t| t.belongs_to :structured_error diff --git a/db/migrate/20170711170456_add_description_and_hint_to_error_template.rb b/db/migrate/20170711170456_add_description_and_hint_to_error_template.rb index 62cbee95..11d1985f 100644 --- a/db/migrate/20170711170456_add_description_and_hint_to_error_template.rb +++ b/db/migrate/20170711170456_add_description_and_hint_to_error_template.rb @@ -1,4 +1,4 @@ -class AddDescriptionAndHintToErrorTemplate < ActiveRecord::Migration +class AddDescriptionAndHintToErrorTemplate < ActiveRecord::Migration[4.2] def change add_column :error_templates, :description, :text add_column :error_templates, :hint, :text diff --git a/db/migrate/20170711170928_change_error_template_attribute_relationship_to_n_to_m.rb b/db/migrate/20170711170928_change_error_template_attribute_relationship_to_n_to_m.rb index d10fbda2..32f3a304 100644 --- a/db/migrate/20170711170928_change_error_template_attribute_relationship_to_n_to_m.rb +++ b/db/migrate/20170711170928_change_error_template_attribute_relationship_to_n_to_m.rb @@ -1,4 +1,4 @@ -class ChangeErrorTemplateAttributeRelationshipToNToM < ActiveRecord::Migration +class ChangeErrorTemplateAttributeRelationshipToNToM < ActiveRecord::Migration[4.2] def change remove_belongs_to :error_template_attributes, :error_template create_join_table :error_templates, :error_template_attributes diff --git a/db/migrate/20170719133351_add_match_to_structured_error_attribute.rb b/db/migrate/20170719133351_add_match_to_structured_error_attribute.rb index 7ec3ccc0..51d868ab 100644 --- a/db/migrate/20170719133351_add_match_to_structured_error_attribute.rb +++ b/db/migrate/20170719133351_add_match_to_structured_error_attribute.rb @@ -1,4 +1,4 @@ -class AddMatchToStructuredErrorAttribute < ActiveRecord::Migration +class AddMatchToStructuredErrorAttribute < ActiveRecord::Migration[4.2] def change add_column :structured_error_attributes, :match, :boolean end diff --git a/db/migrate/20170830083601_add_cause_to_testruns.rb b/db/migrate/20170830083601_add_cause_to_testruns.rb index f9a859c4..d737c029 100644 --- a/db/migrate/20170830083601_add_cause_to_testruns.rb +++ b/db/migrate/20170830083601_add_cause_to_testruns.rb @@ -1,4 +1,4 @@ -class AddCauseToTestruns < ActiveRecord::Migration +class AddCauseToTestruns < ActiveRecord::Migration[4.2] def up add_column :testruns, :cause, :string Testrun.reset_column_information diff --git a/db/migrate/20170906124500_create_subscriptions.rb b/db/migrate/20170906124500_create_subscriptions.rb index 5dba42ab..83adc2d7 100644 --- a/db/migrate/20170906124500_create_subscriptions.rb +++ b/db/migrate/20170906124500_create_subscriptions.rb @@ -1,4 +1,4 @@ -class CreateSubscriptions < ActiveRecord::Migration +class CreateSubscriptions < ActiveRecord::Migration[4.2] def change create_table :subscriptions do |t| t.belongs_to :user, polymorphic: true diff --git a/db/migrate/20170913054203_rename_subscription_type.rb b/db/migrate/20170913054203_rename_subscription_type.rb index 76417921..e380bb07 100644 --- a/db/migrate/20170913054203_rename_subscription_type.rb +++ b/db/migrate/20170913054203_rename_subscription_type.rb @@ -1,4 +1,4 @@ -class RenameSubscriptionType < ActiveRecord::Migration +class RenameSubscriptionType < ActiveRecord::Migration[4.2] def change rename_column :subscriptions, :type, :subscription_type end diff --git a/db/migrate/20170920145852_add_deleted_to_subscription.rb b/db/migrate/20170920145852_add_deleted_to_subscription.rb index eb1d47c4..6c071b3a 100644 --- a/db/migrate/20170920145852_add_deleted_to_subscription.rb +++ b/db/migrate/20170920145852_add_deleted_to_subscription.rb @@ -1,4 +1,4 @@ -class AddDeletedToSubscription < ActiveRecord::Migration +class AddDeletedToSubscription < ActiveRecord::Migration[4.2] def change add_column :subscriptions, :deleted, :boolean end diff --git a/db/migrate/20171002131135_remove_expected_working_time.rb b/db/migrate/20171002131135_remove_expected_working_time.rb index eb4bbcb4..b9d6edd3 100644 --- a/db/migrate/20171002131135_remove_expected_working_time.rb +++ b/db/migrate/20171002131135_remove_expected_working_time.rb @@ -1,4 +1,4 @@ -class RemoveExpectedWorkingTime < ActiveRecord::Migration +class RemoveExpectedWorkingTime < ActiveRecord::Migration[4.2] def change remove_column :exercises, :expected_worktime_seconds end diff --git a/db/migrate/20171115121125_add_anomaly_detection_flag_to_exercise_collection.rb b/db/migrate/20171115121125_add_anomaly_detection_flag_to_exercise_collection.rb index 2aad7e28..d9816951 100644 --- a/db/migrate/20171115121125_add_anomaly_detection_flag_to_exercise_collection.rb +++ b/db/migrate/20171115121125_add_anomaly_detection_flag_to_exercise_collection.rb @@ -1,4 +1,4 @@ -class AddAnomalyDetectionFlagToExerciseCollection < ActiveRecord::Migration +class AddAnomalyDetectionFlagToExerciseCollection < ActiveRecord::Migration[4.2] def change add_column :exercise_collections, :use_anomaly_detection, :boolean, :default => false end diff --git a/db/migrate/20171120153705_add_timestamps_to_user_exercise_feedbacks.rb b/db/migrate/20171120153705_add_timestamps_to_user_exercise_feedbacks.rb index 4529d527..7f3bb0c7 100644 --- a/db/migrate/20171120153705_add_timestamps_to_user_exercise_feedbacks.rb +++ b/db/migrate/20171120153705_add_timestamps_to_user_exercise_feedbacks.rb @@ -1,4 +1,4 @@ -class AddTimestampsToUserExerciseFeedbacks < ActiveRecord::Migration +class AddTimestampsToUserExerciseFeedbacks < ActiveRecord::Migration[4.2] def up add_column :user_exercise_feedbacks, :created_at, :datetime, null: false, default: Time.now add_column :user_exercise_feedbacks, :updated_at, :datetime, null: false, default: Time.now diff --git a/db/migrate/20171122124222_add_index_to_exercises.rb b/db/migrate/20171122124222_add_index_to_exercises.rb index cf1f4674..d02e13bd 100644 --- a/db/migrate/20171122124222_add_index_to_exercises.rb +++ b/db/migrate/20171122124222_add_index_to_exercises.rb @@ -1,4 +1,4 @@ -class AddIndexToExercises < ActiveRecord::Migration +class AddIndexToExercises < ActiveRecord::Migration[4.2] def change add_index :exercises, :id end diff --git a/db/migrate/20171210172208_add_user_to_exercise_collection.rb b/db/migrate/20171210172208_add_user_to_exercise_collection.rb index 6dee1adf..6b63120a 100644 --- a/db/migrate/20171210172208_add_user_to_exercise_collection.rb +++ b/db/migrate/20171210172208_add_user_to_exercise_collection.rb @@ -1,4 +1,4 @@ -class AddUserToExerciseCollection < ActiveRecord::Migration +class AddUserToExerciseCollection < ActiveRecord::Migration[4.2] def change add_reference :exercise_collections, :user, polymorphic: true, index: true end diff --git a/db/migrate/20180130101645_add_submission_to_structured_errors.rb b/db/migrate/20180130101645_add_submission_to_structured_errors.rb index 3f7d2a5e..0f6ccec6 100644 --- a/db/migrate/20180130101645_add_submission_to_structured_errors.rb +++ b/db/migrate/20180130101645_add_submission_to_structured_errors.rb @@ -1,4 +1,4 @@ -class AddSubmissionToStructuredErrors < ActiveRecord::Migration +class AddSubmissionToStructuredErrors < ActiveRecord::Migration[4.2] def change add_reference :structured_errors, :submission, index: true end diff --git a/db/migrate/20180130172021_add_reached_full_score_to_request_for_comment.rb b/db/migrate/20180130172021_add_reached_full_score_to_request_for_comment.rb index c29919fd..651ff184 100644 --- a/db/migrate/20180130172021_add_reached_full_score_to_request_for_comment.rb +++ b/db/migrate/20180130172021_add_reached_full_score_to_request_for_comment.rb @@ -1,4 +1,4 @@ -class AddReachedFullScoreToRequestForComment < ActiveRecord::Migration +class AddReachedFullScoreToRequestForComment < ActiveRecord::Migration[4.2] def up add_column :request_for_comments, :full_score_reached, :boolean, default: false RequestForComment.find_each { |rfc| diff --git a/db/migrate/20180202132034_add_times_featured_to_request_for_comments.rb b/db/migrate/20180202132034_add_times_featured_to_request_for_comments.rb index 348292af..53194328 100644 --- a/db/migrate/20180202132034_add_times_featured_to_request_for_comments.rb +++ b/db/migrate/20180202132034_add_times_featured_to_request_for_comments.rb @@ -1,4 +1,4 @@ -class AddTimesFeaturedToRequestForComments < ActiveRecord::Migration +class AddTimesFeaturedToRequestForComments < ActiveRecord::Migration[4.2] def change add_column :request_for_comments, :times_featured, :integer, default: 0 end diff --git a/db/migrate/20180222145909_fix_timestamps_on_feedback.rb b/db/migrate/20180222145909_fix_timestamps_on_feedback.rb index f07372c7..32909752 100644 --- a/db/migrate/20180222145909_fix_timestamps_on_feedback.rb +++ b/db/migrate/20180222145909_fix_timestamps_on_feedback.rb @@ -1,4 +1,4 @@ -class FixTimestampsOnFeedback < ActiveRecord::Migration +class FixTimestampsOnFeedback < ActiveRecord::Migration[4.2] def up change_column_default(:user_exercise_feedbacks, :created_at, nil) change_column_default(:user_exercise_feedbacks, :updated_at, nil) diff --git a/db/migrate/20180226131340_create_anomaly_notifications.rb b/db/migrate/20180226131340_create_anomaly_notifications.rb index 5de3dbe4..c7f4e104 100644 --- a/db/migrate/20180226131340_create_anomaly_notifications.rb +++ b/db/migrate/20180226131340_create_anomaly_notifications.rb @@ -1,4 +1,4 @@ -class CreateAnomalyNotifications < ActiveRecord::Migration +class CreateAnomalyNotifications < ActiveRecord::Migration[4.2] def change create_table :anomaly_notifications do |t| t.belongs_to :user, polymorphic: true, index: true diff --git a/db/migrate/20180515110030_remove_file_id_from_structured_errors.rb b/db/migrate/20180515110030_remove_file_id_from_structured_errors.rb index d5465ccd..c0cd23d1 100644 --- a/db/migrate/20180515110030_remove_file_id_from_structured_errors.rb +++ b/db/migrate/20180515110030_remove_file_id_from_structured_errors.rb @@ -1,4 +1,4 @@ -class RemoveFileIdFromStructuredErrors < ActiveRecord::Migration +class RemoveFileIdFromStructuredErrors < ActiveRecord::Migration[4.2] def change remove_column :structured_errors, :file_id end diff --git a/db/migrate/20180703125302_create_exercise_collection_items.rb b/db/migrate/20180703125302_create_exercise_collection_items.rb index c881c8a5..aa172c62 100644 --- a/db/migrate/20180703125302_create_exercise_collection_items.rb +++ b/db/migrate/20180703125302_create_exercise_collection_items.rb @@ -1,4 +1,4 @@ -class CreateExerciseCollectionItems < ActiveRecord::Migration +class CreateExerciseCollectionItems < ActiveRecord::Migration[4.2] def up rename_table :exercise_collections_exercises, :exercise_collection_items add_column :exercise_collection_items, :position, :integer, default: 0, null: false diff --git a/db/migrate/20180814145059_create_events.rb b/db/migrate/20180814145059_create_events.rb index b6bc8745..5d1eb5e1 100644 --- a/db/migrate/20180814145059_create_events.rb +++ b/db/migrate/20180814145059_create_events.rb @@ -1,4 +1,4 @@ -class CreateEvents < ActiveRecord::Migration +class CreateEvents < ActiveRecord::Migration[4.2] def change create_table :events do |t| t.string :type diff --git a/db/migrate/20180814154055_rename_events_type_to_category.rb b/db/migrate/20180814154055_rename_events_type_to_category.rb index f97200c8..9bfe9d0b 100644 --- a/db/migrate/20180814154055_rename_events_type_to_category.rb +++ b/db/migrate/20180814154055_rename_events_type_to_category.rb @@ -1,4 +1,4 @@ -class RenameEventsTypeToCategory < ActiveRecord::Migration +class RenameEventsTypeToCategory < ActiveRecord::Migration[4.2] def change rename_column :events, :type, :category end diff --git a/db/migrate/20180815115351_remove_event_indices.rb b/db/migrate/20180815115351_remove_event_indices.rb index 2be20c1d..fdf7959d 100644 --- a/db/migrate/20180815115351_remove_event_indices.rb +++ b/db/migrate/20180815115351_remove_event_indices.rb @@ -1,4 +1,4 @@ -class RemoveEventIndices < ActiveRecord::Migration +class RemoveEventIndices < ActiveRecord::Migration[4.2] def change remove_index :events, [:user_type, :user_id] remove_index :events, :exercise_id diff --git a/db/migrate/20180823135317_add_index_for_testrun_submission_id.rb b/db/migrate/20180823135317_add_index_for_testrun_submission_id.rb index d0511dc9..8394660c 100644 --- a/db/migrate/20180823135317_add_index_for_testrun_submission_id.rb +++ b/db/migrate/20180823135317_add_index_for_testrun_submission_id.rb @@ -1,4 +1,4 @@ -class AddIndexForTestrunSubmissionId < ActiveRecord::Migration +class AddIndexForTestrunSubmissionId < ActiveRecord::Migration[4.2] def change add_index :testruns, :submission_id end diff --git a/db/migrate/20180904115948_add_index_for_lti_parameters_external_user_id.rb b/db/migrate/20180904115948_add_index_for_lti_parameters_external_user_id.rb index 5d0ecd46..e18f9c73 100644 --- a/db/migrate/20180904115948_add_index_for_lti_parameters_external_user_id.rb +++ b/db/migrate/20180904115948_add_index_for_lti_parameters_external_user_id.rb @@ -1,4 +1,4 @@ -class AddIndexForLtiParametersExternalUserId < ActiveRecord::Migration +class AddIndexForLtiParametersExternalUserId < ActiveRecord::Migration[4.2] def change add_index :lti_parameters, :external_users_id end diff --git a/db/migrate/20181126163428_add_user_type_to_remote_evaluation_mappings.rb b/db/migrate/20181126163428_add_user_type_to_remote_evaluation_mappings.rb new file mode 100644 index 00000000..0ad50d0f --- /dev/null +++ b/db/migrate/20181126163428_add_user_type_to_remote_evaluation_mappings.rb @@ -0,0 +1,8 @@ +class AddUserTypeToRemoteEvaluationMappings < ActiveRecord::Migration[5.2] + def change + add_column :remote_evaluation_mappings, :user_type, :string + # Update all existing records and set user_type to `ExternalUser` (safe way to prevent any function loss). + # We are not using a default value here on intend to be in line with the other `user_type` columns + RemoteEvaluationMapping.update_all(user_type: 'ExternalUser') + end +end diff --git a/db/schema.rb b/db/schema.rb index aab3e92b..f918e876 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,4 +1,3 @@ -# encoding: UTF-8 # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -11,417 +10,403 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180904115948) do +ActiveRecord::Schema.define(version: 2018_11_26_163428) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "anomaly_notifications", force: :cascade do |t| - t.integer "user_id" - t.string "user_type" - t.integer "exercise_id" - t.integer "exercise_collection_id" - t.string "reason" + t.integer "user_id" + t.string "user_type" + t.integer "exercise_id" + t.integer "exercise_collection_id" + t.string "reason" t.datetime "created_at" t.datetime "updated_at" + t.index ["exercise_collection_id"], name: "index_anomaly_notifications_on_exercise_collection_id" + t.index ["exercise_id"], name: "index_anomaly_notifications_on_exercise_id" + t.index ["user_type", "user_id"], name: "index_anomaly_notifications_on_user_type_and_user_id" end - add_index "anomaly_notifications", ["exercise_collection_id"], name: "index_anomaly_notifications_on_exercise_collection_id", using: :btree - add_index "anomaly_notifications", ["exercise_id"], name: "index_anomaly_notifications_on_exercise_id", using: :btree - add_index "anomaly_notifications", ["user_type", "user_id"], name: "index_anomaly_notifications_on_user_type_and_user_id", using: :btree - create_table "code_harbor_links", force: :cascade do |t| - t.string "oauth2token", limit: 255 + t.string "oauth2token", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.integer "user_id" + t.integer "user_id" + t.index ["user_id"], name: "index_code_harbor_links_on_user_id" end - add_index "code_harbor_links", ["user_id"], name: "index_code_harbor_links_on_user_id", using: :btree - create_table "comments", force: :cascade do |t| - t.integer "user_id" - t.integer "file_id" - t.string "user_type", limit: 255 - t.integer "row" - t.integer "column" - t.text "text" + t.integer "user_id" + t.integer "file_id" + t.string "user_type", limit: 255 + t.integer "row" + t.integer "column" + t.text "text" t.datetime "created_at" t.datetime "updated_at" + t.index ["file_id"], name: "index_comments_on_file_id" + t.index ["user_id"], name: "index_comments_on_user_id" end - add_index "comments", ["file_id"], name: "index_comments_on_file_id", using: :btree - add_index "comments", ["user_id"], name: "index_comments_on_user_id", using: :btree - create_table "consumers", force: :cascade do |t| - t.string "name", limit: 255 + t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "oauth_key", limit: 255 - t.string "oauth_secret", limit: 255 + t.string "oauth_key", limit: 255 + t.string "oauth_secret", limit: 255 end create_table "error_template_attributes", force: :cascade do |t| - t.string "key" - t.string "regex" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "description" - t.boolean "important" + t.string "key" + t.string "regex" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "description" + t.boolean "important" end create_table "error_template_attributes_templates", id: false, force: :cascade do |t| - t.integer "error_template_id", null: false + t.integer "error_template_id", null: false t.integer "error_template_attribute_id", null: false end create_table "error_templates", force: :cascade do |t| - t.integer "execution_environment_id" - t.string "name" - t.string "signature" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.text "description" - t.text "hint" + t.integer "execution_environment_id" + t.string "name" + t.string "signature" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.text "description" + t.text "hint" end create_table "errors", force: :cascade do |t| - t.integer "execution_environment_id" - t.text "message" + t.integer "execution_environment_id" + t.text "message" t.datetime "created_at" t.datetime "updated_at" - t.integer "submission_id" + t.integer "submission_id" + t.index ["submission_id"], name: "index_errors_on_submission_id" end - add_index "errors", ["submission_id"], name: "index_errors_on_submission_id", using: :btree - create_table "events", force: :cascade do |t| - t.string "category" - t.string "data" - t.integer "user_id" - t.string "user_type" - t.integer "exercise_id" - t.integer "file_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "category" + t.string "data" + t.integer "user_id" + t.string "user_type" + t.integer "exercise_id" + t.integer "file_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "execution_environments", force: :cascade do |t| - t.string "docker_image", limit: 255 - t.string "name", limit: 255 + t.string "docker_image", limit: 255 + t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "run_command", limit: 255 - t.string "test_command", limit: 255 - t.string "testing_framework", limit: 255 - t.text "help" - t.string "exposed_ports", limit: 255 - t.integer "permitted_execution_time" - t.integer "user_id" - t.string "user_type", limit: 255 - t.integer "pool_size" - t.integer "file_type_id" - t.integer "memory_limit" - t.boolean "network_enabled" + t.string "run_command", limit: 255 + t.string "test_command", limit: 255 + t.string "testing_framework", limit: 255 + t.text "help" + t.string "exposed_ports", limit: 255 + t.integer "permitted_execution_time" + t.integer "user_id" + t.string "user_type", limit: 255 + t.integer "pool_size" + t.integer "file_type_id" + t.integer "memory_limit" + t.boolean "network_enabled" end create_table "exercise_collection_items", force: :cascade do |t| t.integer "exercise_collection_id" t.integer "exercise_id" - t.integer "position", default: 0, null: false + t.integer "position", default: 0, null: false + t.index ["exercise_collection_id"], name: "index_exercise_collection_items_on_exercise_collection_id" + t.index ["exercise_id"], name: "index_exercise_collection_items_on_exercise_id" end - add_index "exercise_collection_items", ["exercise_collection_id"], name: "index_exercise_collection_items_on_exercise_collection_id", using: :btree - add_index "exercise_collection_items", ["exercise_id"], name: "index_exercise_collection_items_on_exercise_id", using: :btree - create_table "exercise_collections", force: :cascade do |t| - t.string "name" + t.string "name" t.datetime "created_at" t.datetime "updated_at" - t.boolean "use_anomaly_detection", default: false - t.integer "user_id" - t.string "user_type" + t.boolean "use_anomaly_detection", default: false + t.integer "user_id" + t.string "user_type" + t.index ["user_type", "user_id"], name: "index_exercise_collections_on_user_type_and_user_id" end - add_index "exercise_collections", ["user_type", "user_id"], name: "index_exercise_collections_on_user_type_and_user_id", using: :btree - create_table "exercise_tags", force: :cascade do |t| t.integer "exercise_id" t.integer "tag_id" - t.integer "factor", default: 1 + t.integer "factor", default: 1 end create_table "exercises", force: :cascade do |t| - t.text "description" - t.integer "execution_environment_id" - t.string "title", limit: 255 + t.text "description" + t.integer "execution_environment_id" + t.string "title", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.integer "user_id" - t.text "instructions" - t.boolean "public" - t.string "user_type", limit: 255 - t.string "token", limit: 255 - t.boolean "hide_file_tree" - t.boolean "allow_file_creation" - t.boolean "allow_auto_completion", default: false - t.integer "expected_difficulty", default: 1 + t.integer "user_id" + t.text "instructions" + t.boolean "public" + t.string "user_type", limit: 255 + t.string "token", limit: 255 + t.boolean "hide_file_tree" + t.boolean "allow_file_creation" + t.boolean "allow_auto_completion", default: false + t.integer "expected_difficulty", default: 1 + t.index ["id"], name: "index_exercises_on_id" end - add_index "exercises", ["id"], name: "index_exercises_on_id", using: :btree - create_table "exercises_proxy_exercises", id: false, force: :cascade do |t| - t.integer "proxy_exercise_id" - t.integer "exercise_id" + t.integer "proxy_exercise_id" + t.integer "exercise_id" t.datetime "created_at" t.datetime "updated_at" + t.index ["exercise_id"], name: "index_exercises_proxy_exercises_on_exercise_id" + t.index ["proxy_exercise_id"], name: "index_exercises_proxy_exercises_on_proxy_exercise_id" end - add_index "exercises_proxy_exercises", ["exercise_id"], name: "index_exercises_proxy_exercises_on_exercise_id", using: :btree - add_index "exercises_proxy_exercises", ["proxy_exercise_id"], name: "index_exercises_proxy_exercises_on_proxy_exercise_id", using: :btree - create_table "external_users", force: :cascade do |t| - t.integer "consumer_id" - t.string "email", limit: 255 - t.string "external_id", limit: 255 - t.string "name", limit: 255 + t.integer "consumer_id" + t.string "email", limit: 255 + t.string "external_id", limit: 255 + t.string "name", limit: 255 t.datetime "created_at" t.datetime "updated_at" end create_table "file_templates", force: :cascade do |t| - t.string "name", limit: 255 - t.text "content" - t.integer "file_type_id" + t.string "name", limit: 255 + t.text "content" + t.integer "file_type_id" t.datetime "created_at" t.datetime "updated_at" end create_table "file_types", force: :cascade do |t| - t.string "editor_mode", limit: 255 - t.string "file_extension", limit: 255 - t.integer "indent_size" - t.string "name", limit: 255 - t.integer "user_id" + t.string "editor_mode", limit: 255 + t.string "file_extension", limit: 255 + t.integer "indent_size" + t.string "name", limit: 255 + t.integer "user_id" t.datetime "created_at" t.datetime "updated_at" - t.boolean "executable" - t.boolean "renderable" - t.string "user_type", limit: 255 - t.boolean "binary" + t.boolean "executable" + t.boolean "renderable" + t.string "user_type", limit: 255 + t.boolean "binary" end create_table "files", force: :cascade do |t| - t.text "content" - t.integer "context_id" - t.string "context_type", limit: 255 - t.integer "file_id" - t.integer "file_type_id" - t.boolean "hidden" - t.string "name", limit: 255 - t.boolean "read_only" + t.text "content" + t.integer "context_id" + t.string "context_type", limit: 255 + t.integer "file_id" + t.integer "file_type_id" + t.boolean "hidden" + t.string "name", limit: 255 + t.boolean "read_only" t.datetime "created_at" t.datetime "updated_at" - t.string "native_file", limit: 255 - t.string "role", limit: 255 - t.string "hashed_content", limit: 255 - t.string "feedback_message", limit: 255 - t.float "weight" - t.string "path", limit: 255 - t.integer "file_template_id" + t.string "native_file", limit: 255 + t.string "role", limit: 255 + t.string "hashed_content", limit: 255 + t.string "feedback_message", limit: 255 + t.float "weight" + t.string "path", limit: 255 + t.integer "file_template_id" + t.index ["context_id", "context_type"], name: "index_files_on_context_id_and_context_type" end - add_index "files", ["context_id", "context_type"], name: "index_files_on_context_id_and_context_type", using: :btree - create_table "hints", force: :cascade do |t| - t.integer "execution_environment_id" - t.string "locale", limit: 255 - t.text "message" - t.string "name", limit: 255 - t.string "regular_expression", limit: 255 + t.integer "execution_environment_id" + t.string "locale", limit: 255 + t.text "message" + t.string "name", limit: 255 + t.string "regular_expression", limit: 255 t.datetime "created_at" t.datetime "updated_at" end create_table "internal_users", force: :cascade do |t| - t.integer "consumer_id" - t.string "email", limit: 255 - t.string "name", limit: 255 - t.string "role", limit: 255 + t.integer "consumer_id" + t.string "email", limit: 255 + t.string "name", limit: 255 + t.string "role", limit: 255 t.datetime "created_at" t.datetime "updated_at" - t.string "crypted_password", limit: 255 - t.string "salt", limit: 255 - t.integer "failed_logins_count", default: 0 + t.string "crypted_password", limit: 255 + t.string "salt", limit: 255 + t.integer "failed_logins_count", default: 0 t.datetime "lock_expires_at" - t.string "unlock_token", limit: 255 - t.string "remember_me_token", limit: 255 + t.string "unlock_token", limit: 255 + t.string "remember_me_token", limit: 255 t.datetime "remember_me_token_expires_at" - t.string "reset_password_token", limit: 255 + t.string "reset_password_token", limit: 255 t.datetime "reset_password_token_expires_at" t.datetime "reset_password_email_sent_at" - t.string "activation_state", limit: 255 - t.string "activation_token", limit: 255 + t.string "activation_state", limit: 255 + t.string "activation_token", limit: 255 t.datetime "activation_token_expires_at" + t.index ["activation_token"], name: "index_internal_users_on_activation_token" + t.index ["email"], name: "index_internal_users_on_email", unique: true + t.index ["remember_me_token"], name: "index_internal_users_on_remember_me_token" + t.index ["reset_password_token"], name: "index_internal_users_on_reset_password_token" end - add_index "internal_users", ["activation_token"], name: "index_internal_users_on_activation_token", using: :btree - add_index "internal_users", ["email"], name: "index_internal_users_on_email", unique: true, using: :btree - add_index "internal_users", ["remember_me_token"], name: "index_internal_users_on_remember_me_token", using: :btree - add_index "internal_users", ["reset_password_token"], name: "index_internal_users_on_reset_password_token", using: :btree - create_table "interventions", force: :cascade do |t| - t.string "name" - t.text "markup" + t.string "name" + t.text "markup" t.datetime "created_at" t.datetime "updated_at" end create_table "lti_parameters", force: :cascade do |t| - t.integer "external_users_id" - t.integer "consumers_id" - t.integer "exercises_id" - t.jsonb "lti_parameters", default: {}, null: false + t.integer "external_users_id" + t.integer "consumers_id" + t.integer "exercises_id" + t.jsonb "lti_parameters", default: {}, null: false t.datetime "created_at" t.datetime "updated_at" + t.index ["external_users_id"], name: "index_lti_parameters_on_external_users_id" end - add_index "lti_parameters", ["external_users_id"], name: "index_lti_parameters_on_external_users_id", using: :btree - create_table "proxy_exercises", force: :cascade do |t| - t.string "title" - t.string "description" - t.string "token" + t.string "title" + t.string "description" + t.string "token" t.datetime "created_at" t.datetime "updated_at" end create_table "remote_evaluation_mappings", force: :cascade do |t| - t.integer "user_id", null: false - t.integer "exercise_id", null: false - t.string "validation_token", null: false + t.integer "user_id", null: false + t.integer "exercise_id", null: false + t.string "validation_token", null: false t.datetime "created_at" t.datetime "updated_at" + t.string "user_type" end create_table "request_for_comments", force: :cascade do |t| - t.integer "user_id", null: false - t.integer "exercise_id", null: false - t.integer "file_id", null: false + t.integer "user_id", null: false + t.integer "exercise_id", null: false + t.integer "file_id", null: false t.datetime "created_at" t.datetime "updated_at" - t.string "user_type", limit: 255 - t.text "question" - t.boolean "solved", default: false - t.integer "submission_id" - t.text "thank_you_note" - t.boolean "full_score_reached", default: false - t.integer "times_featured", default: 0 + t.string "user_type", limit: 255 + t.text "question" + t.boolean "solved", default: false + t.integer "submission_id" + t.text "thank_you_note" + t.boolean "full_score_reached", default: false + t.integer "times_featured", default: 0 end create_table "searches", force: :cascade do |t| - t.integer "exercise_id", null: false - t.integer "user_id", null: false - t.string "user_type", null: false - t.string "search" + t.integer "exercise_id", null: false + t.integer "user_id", null: false + t.string "user_type", null: false + t.string "search" t.datetime "created_at" t.datetime "updated_at" end create_table "structured_error_attributes", force: :cascade do |t| - t.integer "structured_error_id" - t.integer "error_template_attribute_id" - t.string "value" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "match" + t.integer "structured_error_id" + t.integer "error_template_attribute_id" + t.string "value" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "match" end create_table "structured_errors", force: :cascade do |t| - t.integer "error_template_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.integer "submission_id" + t.integer "error_template_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "submission_id" + t.index ["submission_id"], name: "index_structured_errors_on_submission_id" end - add_index "structured_errors", ["submission_id"], name: "index_structured_errors_on_submission_id", using: :btree - create_table "submissions", force: :cascade do |t| - t.integer "exercise_id" - t.float "score" - t.integer "user_id" + t.integer "exercise_id" + t.float "score" + t.integer "user_id" t.datetime "created_at" t.datetime "updated_at" - t.string "cause", limit: 255 - t.string "user_type", limit: 255 + t.string "cause", limit: 255 + t.string "user_type", limit: 255 + t.index ["exercise_id"], name: "index_submissions_on_exercise_id" + t.index ["user_id"], name: "index_submissions_on_user_id" end - add_index "submissions", ["exercise_id"], name: "index_submissions_on_exercise_id", using: :btree - add_index "submissions", ["user_id"], name: "index_submissions_on_user_id", using: :btree - create_table "subscriptions", force: :cascade do |t| - t.integer "user_id" - t.string "user_type" - t.integer "request_for_comment_id" - t.string "subscription_type" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "deleted" + t.integer "user_id" + t.string "user_type" + t.integer "request_for_comment_id" + t.string "subscription_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "deleted" end create_table "tags", force: :cascade do |t| - t.string "name", null: false + t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" end create_table "testruns", force: :cascade do |t| - t.boolean "passed" - t.text "output" - t.integer "file_id" - t.integer "submission_id" + t.boolean "passed" + t.text "output" + t.integer "file_id" + t.integer "submission_id" t.datetime "created_at" t.datetime "updated_at" - t.string "cause" + t.string "cause" + t.index ["submission_id"], name: "index_testruns_on_submission_id" end - add_index "testruns", ["submission_id"], name: "index_testruns_on_submission_id", using: :btree - create_table "user_exercise_feedbacks", force: :cascade do |t| - t.integer "exercise_id", null: false - t.integer "user_id", null: false - t.string "user_type", null: false - t.integer "difficulty" - t.integer "working_time_seconds" - t.string "feedback_text" - t.integer "user_estimated_worktime" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "exercise_id", null: false + t.integer "user_id", null: false + t.string "user_type", null: false + t.integer "difficulty" + t.integer "working_time_seconds" + t.string "feedback_text" + t.integer "user_estimated_worktime" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "user_exercise_interventions", force: :cascade do |t| - t.integer "user_id" - t.string "user_type" - t.integer "exercise_id" - t.integer "intervention_id" - t.integer "accumulated_worktime_s" - t.text "reason" + t.integer "user_id" + t.string "user_type" + t.integer "exercise_id" + t.integer "intervention_id" + t.integer "accumulated_worktime_s" + t.text "reason" t.datetime "created_at" t.datetime "updated_at" end create_table "user_proxy_exercise_exercises", force: :cascade do |t| - t.integer "user_id" - t.string "user_type" - t.integer "proxy_exercise_id" - t.integer "exercise_id" + t.integer "user_id" + t.string "user_type" + t.integer "proxy_exercise_id" + t.integer "exercise_id" t.datetime "created_at" t.datetime "updated_at" - t.string "reason" + t.string "reason" + t.index ["exercise_id"], name: "index_user_proxy_exercise_exercises_on_exercise_id" + t.index ["proxy_exercise_id"], name: "index_user_proxy_exercise_exercises_on_proxy_exercise_id" + t.index ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user_type_and_user_id" end - add_index "user_proxy_exercise_exercises", ["exercise_id"], name: "index_user_proxy_exercise_exercises_on_exercise_id", using: :btree - add_index "user_proxy_exercise_exercises", ["proxy_exercise_id"], name: "index_user_proxy_exercise_exercises_on_proxy_exercise_id", using: :btree - add_index "user_proxy_exercise_exercises", ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user_type_and_user_id", using: :btree - end diff --git a/db/seeds.rb b/db/seeds.rb index 28d543ef..dab9ab7d 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -18,7 +18,7 @@ end # delete all present records Rails.application.eager_load! -(ActiveRecord::Base.descendants - [ActiveRecord::SchemaMigration]).each(&:delete_all) +(ApplicationRecord.descendants - [ActiveRecord::SchemaMigration]).each(&:delete_all) # delete file uploads FileUtils.rm_rf(Rails.root.join('public', 'uploads')) diff --git a/db/seeds/development.rb b/db/seeds/development.rb index b0c550c0..fab9a989 100644 --- a/db/seeds/development.rb +++ b/db/seeds/development.rb @@ -3,13 +3,15 @@ FactoryBot.create(:consumer) FactoryBot.create(:consumer, name: 'openSAP') # users +# Set default_url_options explicitly, required for rake task +Rails.application.routes.default_url_options = Rails.application.config.action_mailer.default_url_options [:admin, :external_user, :teacher].each { |factory_name| FactoryBot.create(factory_name) } # execution environments ExecutionEnvironment.create_factories # errors -Error.create_factories +CodeOcean::Error.create_factories # exercises @exercises = find_factories_by_class(Exercise).map(&:name).map { |factory_name| [factory_name, FactoryBot.create(factory_name)] }.to_h diff --git a/lib/assets/.keep b/lib/assets/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/assets/javascripts/flash.js b/lib/assets/javascripts/flash.js index 7f53f2bb..d7273864 100644 --- a/lib/assets/javascripts/flash.js +++ b/lib/assets/javascripts/flash.js @@ -1,4 +1,4 @@ -(function() { +$( document ).on('turbolinks:load', function() { var DURATION = 10000; var SEVERITIES = ['danger', 'info', 'success', 'warning']; @@ -48,4 +48,4 @@ generateMethods(); $(showFlashes); -})(); +}); diff --git a/lib/docker_client.rb b/lib/docker_client.rb index b44191aa..5677c047 100644 --- a/lib/docker_client.rb +++ b/lib/docker_client.rb @@ -32,7 +32,11 @@ class DockerClient end def command_substitutions(filename) - {class_name: File.basename(filename, File.extname(filename)).camelize, filename: filename, module_name: File.basename(filename, File.extname(filename)).underscore} + { + class_name: File.basename(filename, File.extname(filename)).camelize, + filename: filename, + module_name: File.basename(filename, File.extname(filename)).underscore + } end private :command_substitutions @@ -78,7 +82,7 @@ class DockerClient Rails.logger.debug "Opening Websocket on URL " + socket_url socket.on :error do |event| - Rails.logger.info "Websocket error: " + event.message + Rails.logger.info "Websocket error: " + event.message.to_s end socket.on :close do |event| Rails.logger.info "Websocket closed." @@ -268,7 +272,7 @@ class DockerClient end #ensure # guarantee that the thread is releasing the DB connection after it is done - # ActiveRecord::Base.connectionpool.releaseconnection + # ApplicationRecord.connectionpool.releaseconnection #end end end @@ -310,7 +314,8 @@ class DockerClient """ Run commands by attaching a websocket to Docker. """ - command = submission.execution_environment.run_command % command_substitutions(filename) + filepath = submission.collect_files.find{|f| f.name_with_extension == filename}.filepath + command = submission.execution_environment.run_command % command_substitutions(filepath) create_workspace_files = proc { create_workspace_files(container, submission) } open_websocket_connection(command, create_workspace_files, block) # actual run command is run in the submissions controller, after all listeners are attached. @@ -320,14 +325,22 @@ class DockerClient """ Stick to existing Docker API with exec command. """ - command = submission.execution_environment.test_command % command_substitutions(filename) + filepath = submission.collect_files.find{|f| f.name_with_extension == filename}.filepath + command = submission.execution_environment.test_command % command_substitutions(filepath) create_workspace_files = proc { create_workspace_files(container, submission) } execute_command(command, create_workspace_files, block) end def self.find_image_by_tag(tag) # todo: cache this. - Docker::Image.all.detect { |image| image.info['RepoTags'].flatten.include?(tag) } + Docker::Image.all.detect do |image| + begin + image.info['RepoTags'].flatten.include?(tag) + rescue + # Skip image if it is not tagged + next + end + end end def self.generate_local_workspace_path diff --git a/lib/docker_container_pool.rb b/lib/docker_container_pool.rb index a2dc1a2e..64cd847c 100644 --- a/lib/docker_container_pool.rb +++ b/lib/docker_container_pool.rb @@ -4,9 +4,9 @@ require 'concurrent/timer_task' class DockerContainerPool - @containers = ThreadSafe::Hash[ExecutionEnvironment.all.map { |execution_environment| [execution_environment.id, ThreadSafe::Array.new] }] + @containers = Concurrent::Hash[ExecutionEnvironment.all.map { |execution_environment| [execution_environment.id, Concurrent::Array.new] }] #as containers are not containing containers in use - @all_containers = ThreadSafe::Hash[ExecutionEnvironment.all.map { |execution_environment| [execution_environment.id, ThreadSafe::Array.new] }] + @all_containers = Concurrent::Hash[ExecutionEnvironment.all.map { |execution_environment| [execution_environment.id, Concurrent::Array.new] }] def self.clean_up Rails.logger.info('Container Pool is now performing a cleanup. ') diff --git a/lib/tasks/.keep b/lib/tasks/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/user_group_separator.rb b/lib/user_group_separator.rb deleted file mode 100644 index 2333b686..00000000 --- a/lib/user_group_separator.rb +++ /dev/null @@ -1,48 +0,0 @@ -class UserGroupSeparator - - # seperates user into 20% no intervention, 20% break intervention, 60% rfc intervention - def self.getInterventionGroup(user) - lastDigitId = user.id % 10 - if lastDigitId < 1 # 0 - :rfc_intervention_stale_rfc - elsif lastDigitId < 2 # 1 - :break_intervention_stale_rfc - elsif lastDigitId < 3 # 2 - :no_intervention_stale_rfc - elsif lastDigitId < 4 # 3 - :no_intervention_hide_rfc - elsif lastDigitId < 5 # 4 - :break_intervention_show_rfc - elsif lastDigitId < 6 # 5 - :no_intervention_show_rfc - else # 6,7,8,9 - :rfc_intervention_show_rfc - end - end - - # seperates user into 20% dummy assignment, 20% random assignemnt, 60% recommended assignment - def self.getProxyExerciseGroup(user) - lastDigitCreatedAt = user.created_at.to_i % 10 - if lastDigitCreatedAt < 2 # 0,1 - :dummy_assigment - elsif lastDigitCreatedAt < 4 # 2,3 - :random_assigment - else # 4,5,6,7,8,9 - :recommended_assignment - end - end - - def self.getGroupExerciseDescriptionTesting(user) - groupById = user.id % 4 - if groupById == 0 - :group_a - elsif groupById == 1 - :group_b - elsif groupById == 2 - :group_c - else # 3 - :group_d - end - end - -end \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..235c9465 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "dependencies": { + "@rails/webpacker": "3.5", + "bootstrap": "^4.1.3", + "bootswatch": "^4.1.3", + "chosen-js": "^1.8.7", + "d3-tip": "^0.9.1", + "font-awesome": "^4.7.0", + "highlight.js": "^9.12.0", + "jquery": "^3.3.1", + "jquery-ui": "^1.12.1", + "jstree": "^3.3.5", + "opensans-webkit": "^1.0.1", + "popper.js": "^1.14.4", + "underscore": "^1.9.1", + "vis": "^4.21.0", + "webpack-merge": "^4.1.4" + }, + "devDependencies": { + "webpack-dev-server": "2.11.2" + }, + "scripts": { + "webpack": "./bin/webpack", + "webpack-dev-server": "./bin/webpack-dev-server" + } +} diff --git a/provision.sh b/provision.sh index 43d72c90..cf4ca496 100644 --- a/provision.sh +++ b/provision.sh @@ -5,8 +5,9 @@ ######## VERSION INFORMATION ######## postgres_version=10 -ruby_version=2.3.6 -rails_version=4.2.10 +ruby_version=2.5.1 +rails_version=5.2.1 +geckodriver_version=0.23.0 ########## INSTALL SCRIPT ########### @@ -19,13 +20,15 @@ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CA sudo apt-get -qq -y install apt-transport-https ca-certificates sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main > /etc/apt/sources.list.d/passenger.list' -# rails -sudo add-apt-repository -y ppa:chris-lea/node.js +# yarn & node +curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - sudo apt-get -qq update # code_ocean -sudo apt-get -qq -y install postgresql-client postgresql-$postgres_version postgresql-server-dev-$postgres_version vagrant +sudo apt-get -qq -y install postgresql-client postgresql-$postgres_version postgresql-server-dev-$postgres_version vagrant yarn nodejs # Docker if [ ! -f /etc/default/docker ] @@ -50,6 +53,7 @@ sudo docker pull openhpi/docker_java sudo docker pull openhpi/docker_ruby sudo docker pull openhpi/docker_python sudo docker pull openhpi/co_execenv_python +sudo docker pull openhpi/co_execenv_node sudo docker pull openhpi/co_execenv_java sudo docker pull openhpi/co_execenv_java_antlr @@ -69,7 +73,6 @@ sudo /usr/local/rvm/bin/rvm alias create default $ruby_version ruby -v # rails -sudo apt-get -qq -y install nodejs sg rvm "/usr/local/rvm/rubies/ruby-$ruby_version/bin/gem install rails -v $rails_version" # sudo gem install bundler @@ -97,7 +100,7 @@ fi # Selenium tests sudo apt-get -qq -y install xvfb firefox -wget --quiet -O ~/geckodriverdownload.tar.gz https://github.com/mozilla/geckodriver/releases/download/v0.19.1/geckodriver-v0.19.1-linux64.tar.gz +wget --quiet -O ~/geckodriverdownload.tar.gz https://github.com/mozilla/geckodriver/releases/download/v$geckodriver_version/geckodriver-v$geckodriver_version-linux64.tar.gz sudo tar -xzf ~/geckodriverdownload.tar.gz -C /usr/local/bin rm ~/geckodriverdownload.tar.gz sudo chmod +x /usr/local/bin/geckodriver @@ -151,3 +154,8 @@ fi # Always set language to English sudo locale-gen en_US en_US.UTF-8 + +# Add host as alias for localhost (allows sending a score to a local Xikolo instance) +sudo tee /etc/hosts -a < - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/fonts/fontawesome-webfont.ttf b/public/fonts/fontawesome-webfont.ttf deleted file mode 100644 index ed9372f8..00000000 Binary files a/public/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/public/fonts/fontawesome-webfont.woff b/public/fonts/fontawesome-webfont.woff deleted file mode 100644 index 8b280b98..00000000 Binary files a/public/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/public/fonts/fontawesome-webfont.woff2 b/public/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 3311d585..00000000 Binary files a/public/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/public/fonts/k3k702ZOKiLJc3WVjuplzA7aC6SjiAOpAWOKfJDfVRY.woff2 b/public/fonts/k3k702ZOKiLJc3WVjuplzA7aC6SjiAOpAWOKfJDfVRY.woff2 deleted file mode 100644 index 918f643f..00000000 Binary files a/public/fonts/k3k702ZOKiLJc3WVjuplzA7aC6SjiAOpAWOKfJDfVRY.woff2 and /dev/null differ diff --git a/public/fonts/k3k702ZOKiLJc3WVjuplzBampu5_7CjHW5spxoeN3Vs.woff2 b/public/fonts/k3k702ZOKiLJc3WVjuplzBampu5_7CjHW5spxoeN3Vs.woff2 deleted file mode 100644 index 51c75cad..00000000 Binary files a/public/fonts/k3k702ZOKiLJc3WVjuplzBampu5_7CjHW5spxoeN3Vs.woff2 and /dev/null differ diff --git a/public/fonts/k3k702ZOKiLJc3WVjuplzBdwxCXfZpKo5kWAx_74bHs.woff2 b/public/fonts/k3k702ZOKiLJc3WVjuplzBdwxCXfZpKo5kWAx_74bHs.woff2 deleted file mode 100644 index 907f88be..00000000 Binary files a/public/fonts/k3k702ZOKiLJc3WVjuplzBdwxCXfZpKo5kWAx_74bHs.woff2 and /dev/null differ diff --git a/public/fonts/k3k702ZOKiLJc3WVjuplzIjoYw3YTyktCCer_ilOlhE.woff2 b/public/fonts/k3k702ZOKiLJc3WVjuplzIjoYw3YTyktCCer_ilOlhE.woff2 deleted file mode 100644 index 0b7045ba..00000000 Binary files a/public/fonts/k3k702ZOKiLJc3WVjuplzIjoYw3YTyktCCer_ilOlhE.woff2 and /dev/null differ diff --git a/public/fonts/k3k702ZOKiLJc3WVjuplzJ6vnaPZw6nYDxM4SVEMFKg.woff2 b/public/fonts/k3k702ZOKiLJc3WVjuplzJ6vnaPZw6nYDxM4SVEMFKg.woff2 deleted file mode 100644 index 4b93c5a6..00000000 Binary files a/public/fonts/k3k702ZOKiLJc3WVjuplzJ6vnaPZw6nYDxM4SVEMFKg.woff2 and /dev/null differ diff --git a/public/fonts/k3k702ZOKiLJc3WVjuplzPgrLsWo7Jk1KvZser0olKY.woff2 b/public/fonts/k3k702ZOKiLJc3WVjuplzPgrLsWo7Jk1KvZser0olKY.woff2 deleted file mode 100644 index 254773a9..00000000 Binary files a/public/fonts/k3k702ZOKiLJc3WVjuplzPgrLsWo7Jk1KvZser0olKY.woff2 and /dev/null differ diff --git a/public/fonts/k3k702ZOKiLJc3WVjuplzPy1_HTwRwgtl1cPga3Fy3Y.woff2 b/public/fonts/k3k702ZOKiLJc3WVjuplzPy1_HTwRwgtl1cPga3Fy3Y.woff2 deleted file mode 100644 index 7ac0e115..00000000 Binary files a/public/fonts/k3k702ZOKiLJc3WVjuplzPy1_HTwRwgtl1cPga3Fy3Y.woff2 and /dev/null differ diff --git a/public/fonts/u-WUoqrET9fUeobQW7jkRYX0hVgzZQUfRDuZrPvH3D8.woff2 b/public/fonts/u-WUoqrET9fUeobQW7jkRYX0hVgzZQUfRDuZrPvH3D8.woff2 deleted file mode 100644 index d1e66d3d..00000000 Binary files a/public/fonts/u-WUoqrET9fUeobQW7jkRYX0hVgzZQUfRDuZrPvH3D8.woff2 and /dev/null differ diff --git a/public/fonts/xjAJXh38I15wypJXxuGMBgt_Rm691LTebKfY2ZkKSmI.woff2 b/public/fonts/xjAJXh38I15wypJXxuGMBgt_Rm691LTebKfY2ZkKSmI.woff2 deleted file mode 100644 index 9f830524..00000000 Binary files a/public/fonts/xjAJXh38I15wypJXxuGMBgt_Rm691LTebKfY2ZkKSmI.woff2 and /dev/null differ diff --git a/public/fonts/xjAJXh38I15wypJXxuGMBl4sYYdJg5dU2qzJEVSuta0.woff2 b/public/fonts/xjAJXh38I15wypJXxuGMBl4sYYdJg5dU2qzJEVSuta0.woff2 deleted file mode 100644 index 5e13fc98..00000000 Binary files a/public/fonts/xjAJXh38I15wypJXxuGMBl4sYYdJg5dU2qzJEVSuta0.woff2 and /dev/null differ diff --git a/public/fonts/xjAJXh38I15wypJXxuGMBlBW26QxpSj-_ZKm_xT4hWw.woff2 b/public/fonts/xjAJXh38I15wypJXxuGMBlBW26QxpSj-_ZKm_xT4hWw.woff2 deleted file mode 100644 index e9fe9445..00000000 Binary files a/public/fonts/xjAJXh38I15wypJXxuGMBlBW26QxpSj-_ZKm_xT4hWw.woff2 and /dev/null differ diff --git a/public/fonts/xjAJXh38I15wypJXxuGMBogp9Q8gbYrhqGlRav_IXfk.woff2 b/public/fonts/xjAJXh38I15wypJXxuGMBogp9Q8gbYrhqGlRav_IXfk.woff2 deleted file mode 100644 index 005b450e..00000000 Binary files a/public/fonts/xjAJXh38I15wypJXxuGMBogp9Q8gbYrhqGlRav_IXfk.woff2 and /dev/null differ diff --git a/public/fonts/xjAJXh38I15wypJXxuGMBqE8kM4xWR1_1bYURRojRGc.woff2 b/public/fonts/xjAJXh38I15wypJXxuGMBqE8kM4xWR1_1bYURRojRGc.woff2 deleted file mode 100644 index 13675489..00000000 Binary files a/public/fonts/xjAJXh38I15wypJXxuGMBqE8kM4xWR1_1bYURRojRGc.woff2 and /dev/null differ diff --git a/public/fonts/xjAJXh38I15wypJXxuGMBtDiNsR5a-9Oe_Ivpu8XWlY.woff2 b/public/fonts/xjAJXh38I15wypJXxuGMBtDiNsR5a-9Oe_Ivpu8XWlY.woff2 deleted file mode 100644 index 8916e5fa..00000000 Binary files a/public/fonts/xjAJXh38I15wypJXxuGMBtDiNsR5a-9Oe_Ivpu8XWlY.woff2 and /dev/null differ diff --git a/public/fonts/xjAJXh38I15wypJXxuGMBvZraR2Tg8w2lzm7kLNL0-w.woff2 b/public/fonts/xjAJXh38I15wypJXxuGMBvZraR2Tg8w2lzm7kLNL0-w.woff2 deleted file mode 100644 index b9a3dbbe..00000000 Binary files a/public/fonts/xjAJXh38I15wypJXxuGMBvZraR2Tg8w2lzm7kLNL0-w.woff2 and /dev/null differ diff --git a/public/fonts/xozscpT2726on7jbcb_pAoX0hVgzZQUfRDuZrPvH3D8.woff2 b/public/fonts/xozscpT2726on7jbcb_pAoX0hVgzZQUfRDuZrPvH3D8.woff2 deleted file mode 100644 index 83f534ff..00000000 Binary files a/public/fonts/xozscpT2726on7jbcb_pAoX0hVgzZQUfRDuZrPvH3D8.woff2 and /dev/null differ diff --git a/public/javascripts/bootstrap.min.js b/public/javascripts/bootstrap.min.js deleted file mode 100644 index 87313dbd..00000000 --- a/public/javascripts/bootstrap.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v3.3.4 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.4",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.4",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.4",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.4",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.4",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport),this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c&&c.$tip&&c.$tip.is(":visible")?void(c.hoverState="in"):(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.options.container?a(this.options.container):this.$element.parent(),p=this.getPosition(o);h="bottom"==h&&k.bottom+m>p.bottom?"top":"top"==h&&k.top-mp.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type)})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.4",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.4",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.4",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=a(document.body).height();"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); \ No newline at end of file diff --git a/public/javascripts/underscore-min.js b/public/javascripts/underscore-min.js deleted file mode 100644 index f01025b7..00000000 --- a/public/javascripts/underscore-min.js +++ /dev/null @@ -1,6 +0,0 @@ -// Underscore.js 1.8.3 -// http://underscorejs.org -// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors -// Underscore may be freely distributed under the MIT license. -(function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); -//# sourceMappingURL=underscore-min.map \ No newline at end of file diff --git a/public/javascripts/vis.min.js b/public/javascripts/vis.min.js deleted file mode 100644 index 7c48489d..00000000 --- a/public/javascripts/vis.min.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * vis.js - * https://github.com/almende/vis - * - * A dynamic, browser-based visualization library. - * - * @version 4.20.0 - * @date 2017-05-21 - * - * @license - * Copyright (C) 2011-2017 Almende B.V, http://almende.com - * - * Vis.js is dual licensed under both - * - * * The Apache 2.0 License - * http://www.apache.org/licenses/LICENSE-2.0 - * - * and - * - * * The MIT License - * http://opensource.org/licenses/MIT - * - * Vis.js may be distributed under either license. - */ -"use strict";!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.vis=e():t.vis=e()}(this,function(){return function(t){function e(o){if(i[o])return i[o].exports;var n=i[o]={exports:{},id:o,loaded:!1};return t[o].call(n.exports,n,n.exports,e),n.loaded=!0,n.exports}var i={};return e.m=t,e.c=i,e.p="",e(0)}([function(t,e,i){var o=i(1);o.extend(e,i(87)),o.extend(e,i(116)),o.extend(e,i(158))},function(t,e,i){function o(t){return t&&t.__esModule?t:{default:t}}var n=i(2),s=o(n),r=i(55),a=o(r),h=i(58),d=o(h),l=i(62),u=o(l),c=i(82),p=i(86);e.isNumber=function(t){return t instanceof Number||"number"==typeof t},e.recursiveDOMDelete=function(t){if(t)for(;!0===t.hasChildNodes();)e.recursiveDOMDelete(t.firstChild),t.removeChild(t.firstChild)},e.giveRange=function(t,e,i,o){if(e==t)return.5;var n=1/(e-t);return Math.max(0,(o-t)*n)},e.isString=function(t){return t instanceof String||"string"==typeof t},e.isDate=function(t){if(t instanceof Date)return!0;if(e.isString(t)){if(f.exec(t))return!0;if(!isNaN(Date.parse(t)))return!0}return!1},e.randomUUID=function(){return p.v4()},e.assignAllKeys=function(t,e){for(var i in t)t.hasOwnProperty(i)&&"object"!==(0,u.default)(t[i])&&(t[i]=e)},e.fillIfDefined=function(t,i){var o=arguments.length>2&&void 0!==arguments[2]&&arguments[2];for(var n in t)void 0!==i[n]&&("object"!==(0,u.default)(i[n])?void 0!==i[n]&&null!==i[n]||void 0===t[n]||!0!==o?t[n]=i[n]:delete t[n]:"object"===(0,u.default)(t[n])&&e.fillIfDefined(t[n],i[n],o))},e.protoExtend=function(t,e){for(var i=1;i3&&void 0!==arguments[3]&&arguments[3];if(Array.isArray(o))throw new TypeError("Arrays are not supported by deepExtend");for(var s=2;s3&&void 0!==arguments[3]&&arguments[3];if(Array.isArray(o))throw new TypeError("Arrays are not supported by deepExtend");for(var s in o)if(o.hasOwnProperty(s)&&-1==t.indexOf(s))if(o[s]&&o[s].constructor===Object)void 0===i[s]&&(i[s]={}),i[s].constructor===Object?e.deepExtend(i[s],o[s]):null===o[s]&&void 0!==i[s]&&!0===n?delete i[s]:i[s]=o[s];else if(Array.isArray(o[s])){i[s]=[];for(var r=0;r=0&&(e="DOMMouseScroll"),t.addEventListener(e,i,o)):t.attachEvent("on"+e,i)},e.removeEventListener=function(t,e,i,o){t.removeEventListener?(void 0===o&&(o=!1),"mousewheel"===e&&navigator.userAgent.indexOf("Firefox")>=0&&(e="DOMMouseScroll"),t.removeEventListener(e,i,o)):t.detachEvent("on"+e,i)},e.preventDefault=function(t){t||(t=window.event),t.preventDefault?t.preventDefault():t.returnValue=!1},e.getTarget=function(t){t||(t=window.event);var e;return t.target?e=t.target:t.srcElement&&(e=t.srcElement),void 0!=e.nodeType&&3==e.nodeType&&(e=e.parentNode),e},e.hasParent=function(t,e){for(var i=t;i;){if(i===e)return!0;i=i.parentNode}return!1},e.option={},e.option.asBoolean=function(t,e){return"function"==typeof t&&(t=t()),null!=t?0!=t:e||null},e.option.asNumber=function(t,e){return"function"==typeof t&&(t=t()),null!=t?Number(t)||e||null:e||null},e.option.asString=function(t,e){return"function"==typeof t&&(t=t()),null!=t?String(t):e||null},e.option.asSize=function(t,i){return"function"==typeof t&&(t=t()),e.isString(t)?t:e.isNumber(t)?t+"px":i||null},e.option.asElement=function(t,e){return"function"==typeof t&&(t=t()),t||e||null},e.hexToRGB=function(t){var e=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;t=t.replace(e,function(t,e,i,o){return e+e+i+i+o+o});var i=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return i?{r:parseInt(i[1],16),g:parseInt(i[2],16),b:parseInt(i[3],16)}:null},e.overrideOpacity=function(t,i){if(-1!=t.indexOf("rgba"))return t;if(-1!=t.indexOf("rgb")){var o=t.substr(t.indexOf("(")+1).replace(")","").split(",");return"rgba("+o[0]+","+o[1]+","+o[2]+","+i+")"}var o=e.hexToRGB(t);return null==o?t:"rgba("+o.r+","+o.g+","+o.b+","+i+")"},e.RGBToHex=function(t,e,i){return"#"+((1<<24)+(t<<16)+(e<<8)+i).toString(16).slice(1)},e.parseColor=function(t){var i;if(!0===e.isString(t)){if(!0===e.isValidRGB(t)){var o=t.substr(4).substr(0,t.length-5).split(",").map(function(t){return parseInt(t)});t=e.RGBToHex(o[0],o[1],o[2])}if(!0===e.isValidHex(t)){var n=e.hexToHSV(t),s={h:n.h,s:.8*n.s,v:Math.min(1,1.02*n.v)},r={h:n.h,s:Math.min(1,1.25*n.s),v:.8*n.v},a=e.HSVToHex(r.h,r.s,r.v),h=e.HSVToHex(s.h,s.s,s.v);i={background:t,border:a,highlight:{background:h,border:a},hover:{background:h,border:a}}}else i={background:t,border:t,highlight:{background:t,border:t},hover:{background:t,border:t}}}else i={},i.background=t.background||void 0,i.border=t.border||void 0,e.isString(t.highlight)?i.highlight={border:t.highlight,background:t.highlight}:(i.highlight={},i.highlight.background=t.highlight&&t.highlight.background||void 0,i.highlight.border=t.highlight&&t.highlight.border||void 0),e.isString(t.hover)?i.hover={border:t.hover,background:t.hover}:(i.hover={},i.hover.background=t.hover&&t.hover.background||void 0,i.hover.border=t.hover&&t.hover.border||void 0);return i},e.RGBToHSV=function(t,e,i){t/=255,e/=255,i/=255;var o=Math.min(t,Math.min(e,i)),n=Math.max(t,Math.max(e,i));if(o==n)return{h:0,s:0,v:o};var s=t==o?e-i:i==o?t-e:i-t;return{h:60*((t==o?3:i==o?1:5)-s/(n-o))/360,s:(n-o)/n,v:n}};var m={split:function(t){var e={};return t.split(";").forEach(function(t){if(""!=t.trim()){var i=t.split(":"),o=i[0].trim(),n=i[1].trim();e[o]=n}}),e},join:function(t){return(0,d.default)(t).map(function(e){return e+": "+t[e]}).join("; ")}};e.addCssText=function(t,i){var o=m.split(t.style.cssText),n=m.split(i),s=e.extend(o,n);t.style.cssText=m.join(s)},e.removeCssText=function(t,e){var i=m.split(t.style.cssText),o=m.split(e);for(var n in o)o.hasOwnProperty(n)&&delete i[n];t.style.cssText=m.join(i)},e.HSVToRGB=function(t,e,i){var o,n,s,r=Math.floor(6*t),a=6*t-r,h=i*(1-e),d=i*(1-a*e),l=i*(1-(1-a)*e);switch(r%6){case 0:o=i,n=l,s=h;break;case 1:o=d,n=i,s=h;break;case 2:o=h,n=i,s=l;break;case 3:o=h,n=d,s=i;break;case 4:o=l,n=h,s=i;break;case 5:o=i,n=h,s=d}return{r:Math.floor(255*o),g:Math.floor(255*n),b:Math.floor(255*s)}},e.HSVToHex=function(t,i,o){var n=e.HSVToRGB(t,i,o);return e.RGBToHex(n.r,n.g,n.b)},e.hexToHSV=function(t){var i=e.hexToRGB(t);return e.RGBToHSV(i.r,i.g,i.b)},e.isValidHex=function(t){return/(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(t)},e.isValidRGB=function(t){return t=t.replace(" ",""),/rgb\((\d{1,3}),(\d{1,3}),(\d{1,3})\)/i.test(t)},e.isValidRGBA=function(t){return t=t.replace(" ",""),/rgba\((\d{1,3}),(\d{1,3}),(\d{1,3}),(.{1,3})\)/i.test(t)},e.selectiveBridgeObject=function(t,i){if("object"==(void 0===i?"undefined":(0,u.default)(i))){for(var o=(0,a.default)(i),n=0;n0&&e(o,t[n-1])<0;n--)t[n]=t[n-1];t[n]=o}return t},e.mergeOptions=function(t,e,i){var o=(arguments.length>3&&void 0!==arguments[3]&&arguments[3],arguments.length>4&&void 0!==arguments[4]?arguments[4]:{});if(null===e[i])t[i]=(0,a.default)(o[i]);else if(void 0!==e[i])if("boolean"==typeof e[i])t[i].enabled=e[i];else{void 0===e[i].enabled&&(t[i].enabled=!0);for(var n in e[i])e[i].hasOwnProperty(n)&&(t[i][n]=e[i][n])}},e.binarySearchCustom=function(t,e,i,o){for(var n=0,s=0,r=t.length-1;s<=r&&n<1e4;){var a=Math.floor((s+r)/2),h=t[a],d=void 0===o?h[i]:h[i][o],l=e(d);if(0==l)return a;-1==l?s=a+1:r=a-1,n++}return-1},e.binarySearchValue=function(t,e,i,o,n){for(var s,r,a,h,d=0,l=0,u=t.length-1,n=void 0!=n?n:function(t,e){return t==e?0:t0)return"before"==o?Math.max(0,h-1):h;if(n(r,e)<0&&n(a,e)>0)return"before"==o?h:Math.min(t.length-1,h+1);n(r,e)<0?l=h+1:u=h-1,d++}return-1},e.easingFunctions={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return t*(2-t)},easeInOutQuad:function(t){return t<.5?2*t*t:(4-2*t)*t-1},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return--t*t*t+1},easeInOutCubic:function(t){return t<.5?4*t*t*t:(t-1)*(2*t-2)*(2*t-2)+1},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return 1- --t*t*t*t},easeInOutQuart:function(t){return t<.5?8*t*t*t*t:1-8*--t*t*t*t},easeInQuint:function(t){return t*t*t*t*t},easeOutQuint:function(t){return 1+--t*t*t*t*t},easeInOutQuint:function(t){return t<.5?16*t*t*t*t*t:1+16*--t*t*t*t*t}},e.getScrollBarWidth=function(){var t=document.createElement("p");t.style.width="100%",t.style.height="200px";var e=document.createElement("div");e.style.position="absolute",e.style.top="0px",e.style.left="0px",e.style.visibility="hidden",e.style.width="200px",e.style.height="150px",e.style.overflow="hidden",e.appendChild(t),document.body.appendChild(e);var i=t.offsetWidth;e.style.overflow="scroll";var o=t.offsetWidth;return i==o&&(o=e.clientWidth),document.body.removeChild(e),i-o},e.topMost=function(t,e){var i=void 0;Array.isArray(e)||(e=[e]);var o=!0,n=!1,r=void 0;try{for(var a,h=(0,s.default)(t);!(o=(a=h.next()).done);o=!0){var d=a.value;if(d){i=d[e[0]];for(var l=1;l=t.length?(this._t=void 0,n(1)):"keys"==e?n(0,i):"values"==e?n(0,t[i]):n(0,[i,t[i]])},"values"),s.Arguments=s.Array,o("keys"),o("values"),o("entries")},function(t,e){t.exports=function(){}},function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},function(t,e){t.exports={}},function(t,e,i){var o=i(10),n=i(12);t.exports=function(t){return o(n(t))}},function(t,e,i){var o=i(11);t.exports=Object("z").propertyIsEnumerable(0)?Object:function(t){return"String"==o(t)?t.split(""):Object(t)}},function(t,e){var i={}.toString;t.exports=function(t){return i.call(t).slice(8,-1)}},function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},function(t,e,i){var o=i(14),n=i(15),s=i(30),r=i(20),a=i(31),h=i(8),d=i(32),l=i(46),u=i(48),c=i(47)("iterator"),p=!([].keys&&"next"in[].keys()),f=function(){return this};t.exports=function(t,e,i,m,v,g,y){d(i,e,m);var b,_,w,x=function(t){if(!p&&t in D)return D[t];switch(t){case"keys":case"values":return function(){return new i(this,t)}}return function(){return new i(this,t)}},k=e+" Iterator",M="values"==v,S=!1,D=t.prototype,O=D[c]||D["@@iterator"]||v&&D[v],C=O||x(v),T=v?M?x("entries"):C:void 0,E="Array"==e?D.entries||O:O;if(E&&(w=u(E.call(new t)))!==Object.prototype&&(l(w,k,!0),o||a(w,c)||r(w,c,f)),M&&O&&"values"!==O.name&&(S=!0,C=function(){return O.call(this)}),o&&!y||!p&&!S&&D[c]||r(D,c,C),h[e]=C,h[k]=f,v)if(b={values:M?C:x("values"),keys:g?C:x("keys"),entries:T},y)for(_ in b)_ in D||s(D,_,b[_]);else n(n.P+n.F*(p||S),e,b);return b}},function(t,e){t.exports=!0},function(t,e,i){var o=i(16),n=i(17),s=i(18),r=i(20),a=function(t,e,i){var h,d,l,u=t&a.F,c=t&a.G,p=t&a.S,f=t&a.P,m=t&a.B,v=t&a.W,g=c?n:n[e]||(n[e]={}),y=g.prototype,b=c?o:p?o[e]:(o[e]||{}).prototype;c&&(i=e);for(h in i)(d=!u&&b&&void 0!==b[h])&&h in g||(l=d?b[h]:i[h],g[h]=c&&"function"!=typeof b[h]?i[h]:m&&d?s(l,o):v&&b[h]==l?function(t){var e=function(e,i,o){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(e);case 2:return new t(e,i)}return new t(e,i,o)}return t.apply(this,arguments)};return e.prototype=t.prototype,e}(l):f&&"function"==typeof l?s(Function.call,l):l,f&&((g.virtual||(g.virtual={}))[h]=l,t&a.R&&y&&!y[h]&&r(y,h,l)))};a.F=1,a.G=2,a.S=4,a.P=8,a.B=16,a.W=32,a.U=64,a.R=128,t.exports=a},function(t,e){var i=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=i)},function(t,e){var i=t.exports={version:"2.4.0"};"number"==typeof __e&&(__e=i)},function(t,e,i){var o=i(19);t.exports=function(t,e,i){if(o(t),void 0===e)return t;switch(i){case 1:return function(i){return t.call(e,i)};case 2:return function(i,o){return t.call(e,i,o)};case 3:return function(i,o,n){return t.call(e,i,o,n)}}return function(){return t.apply(e,arguments)}}},function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},function(t,e,i){var o=i(21),n=i(29);t.exports=i(25)?function(t,e,i){return o.f(t,e,n(1,i))}:function(t,e,i){return t[e]=i,t}},function(t,e,i){var o=i(22),n=i(24),s=i(28),r=Object.defineProperty;e.f=i(25)?Object.defineProperty:function(t,e,i){if(o(t),e=s(e,!0),o(i),n)try{return r(t,e,i)}catch(t){}if("get"in i||"set"in i)throw TypeError("Accessors not supported!");return"value"in i&&(t[e]=i.value),t}},function(t,e,i){var o=i(23);t.exports=function(t){if(!o(t))throw TypeError(t+" is not an object!");return t}},function(t,e){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,e,i){t.exports=!i(25)&&!i(26)(function(){return 7!=Object.defineProperty(i(27)("div"),"a",{get:function(){return 7}}).a})},function(t,e,i){t.exports=!i(26)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,e){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,e,i){var o=i(23),n=i(16).document,s=o(n)&&o(n.createElement);t.exports=function(t){return s?n.createElement(t):{}}},function(t,e,i){var o=i(23);t.exports=function(t,e){if(!o(t))return t;var i,n;if(e&&"function"==typeof(i=t.toString)&&!o(n=i.call(t)))return n;if("function"==typeof(i=t.valueOf)&&!o(n=i.call(t)))return n;if(!e&&"function"==typeof(i=t.toString)&&!o(n=i.call(t)))return n;throw TypeError("Can't convert object to primitive value")}},function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},function(t,e,i){t.exports=i(20)},function(t,e){var i={}.hasOwnProperty;t.exports=function(t,e){return i.call(t,e)}},function(t,e,i){var o=i(33),n=i(29),s=i(46),r={};i(20)(r,i(47)("iterator"),function(){return this}),t.exports=function(t,e,i){t.prototype=o(r,{next:n(1,i)}),s(t,e+" Iterator")}},function(t,e,i){var o=i(22),n=i(34),s=i(44),r=i(41)("IE_PROTO"),a=function(){},h=function(){var t,e=i(27)("iframe"),o=s.length;for(e.style.display="none",i(45).appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write("