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/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 6bb6dc27..fe5a62b9 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,6 +278,21 @@ 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) {
@@ -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) {
@@ -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 95%
rename from app/assets/javascripts/editor/evaluation.js.erb
rename to app/assets/javascripts/editor/evaluation.js
index feced0eb..23e18bf0 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();
     }
   },
diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js
similarity index 91%
rename from app/assets/javascripts/editor/participantsupport.js.erb
rename to app/assets/javascripts/editor/participantsupport.js
index a826cbf0..f0329256 100644
--- a/app/assets/javascripts/editor/participantsupport.js.erb
+++ b/app/assets/javascripts/editor/participantsupport.js
@@ -1,12 +1,12 @@
 CodeOceanEditorFlowr = {
   isFlowrEnabled: true,
-  flowrResultHtml: '
', + flowrResultHtml: '
', handleStderrOutputForFlowr: function () { if (!this.isFlowrEnabled) return; var flowrUrl = $('#flowrHint').data('url'); - var flowrHintBody = $('#flowrHint .panel-body'); + var flowrHintBody = $('#flowrHint .card-body'); var queryParameters = { query: this.flowrOutputBuffer }; @@ -19,8 +19,8 @@ CodeOceanEditorFlowr = { var resultTile = $(collapsibleTileHtml); resultTile.find('h4 > a').text(question.title + ' | Found via ' + question.source); - resultTile.find('.panel-body').html(question.body); - resultTile.find('.panel-body').append('Open this question'); + resultTile.find('.card-body').html(question.body); + resultTile.find('.card-body').append('Open this question'); flowrHintBody.append(resultTile); }); 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 98% rename from app/assets/javascripts/editor/submissions.js.erb rename to app/assets/javascripts/editor/submissions.js index 30f63a75..6c4fd96a 100644 --- a/app/assets/javascripts/editor/submissions.js.erb +++ b/app/assets/javascripts/editor/submissions.js @@ -155,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); @@ -175,7 +175,7 @@ CodeOceanEditorSubmissions = { if ($('#test').is(':visible')) { this.createSubmission('#test', null, function(response) { this.showSpinner($('#test')); - $('#score_div').addClass('hidden'); + $('#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..d01dbf0b 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', { 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..91601b0b 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,9 +14,6 @@ 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')) end 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..72b5ce2f 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 @@ -364,7 +366,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: { 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/submissions_controller.rb b/app/controllers/submissions_controller.rb index 1624e5e0..8d16eb90 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 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..841357e1 100644 --- a/app/controllers/user_exercise_feedbacks_controller.rb +++ b/app/controllers/user_exercise_feedbacks_controller.rb @@ -112,7 +112,7 @@ class UserExerciseFeedbacksController < ApplicationController 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/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 380f4989..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 @@ -59,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] 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..de997a0e 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 diff --git a/app/models/remote_evaluation_mapping.rb b/app/models/remote_evaluation_mapping.rb index be0034b1..8f6f6192 100644 --- a/app/models/remote_evaluation_mapping.rb +++ b/app/models/remote_evaluation_mapping.rb @@ -1,5 +1,5 @@ # 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 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..13eb6ff2 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 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 c09c7f82..65da67f4 100644 --- a/app/views/admin/dashboard/show.html.slim +++ b/app/views/admin/dashboard/show.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) h1 = t('breadcrumbs.dashboard.show') 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 2c760ec5..4568c313 100644 --- a/app/views/exercises/_editor_output.html.slim +++ b/app/views/exercises/_editor_output.html.slim @@ -1,58 +1,61 @@ 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 + 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.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 = 'Gain more insights here' - .panel-body + 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.card.text-white.bg-info data-url=CodeOcean::Config.new(:code_ocean).read[:flowr][:url] role='tab' + .card-header = 'Gain more insights here' + .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 c2e8dd5b..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 +ul.list-unstyled#files - @exercise.files.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}" + 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..eb222e31 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 @@ -39,7 +39,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..a24a6e57 100644 --- a/app/views/user_exercise_feedbacks/_form.html.slim +++ b/app/views/user_exercise_feedbacks/_form.html.slim @@ -1,6 +1,6 @@ = 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') + " " @@ -8,16 +8,16 @@ = render('shared/form_errors', object: @uef) h4 == 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 } + = 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 = 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 } + = 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/code_ocean.yml.example b/config/code_ocean.yml.example index e8cb10d0..30cd4639 100644 --- a/config/code_ocean.yml.example +++ b/config/code_ocean.yml.example @@ -6,7 +6,7 @@ default: &default development: flowr: - enabled: true + enabled: false url: http://example.org:3000/api/exceptioninfo?id=&lang=auto code_pilot: enabled: false 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..844109c3 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,15 +16,13 @@ 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 @@ -37,10 +37,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 +60,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 +85,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..ce43654b 100644 --- a/config/initializers/docker.rb +++ b/config/initializers/docker.rb @@ -1,6 +1,6 @@ DockerClient.initialize_environment unless Rails.env.test? && `which docker`.blank? -if ActiveRecord::Base.connection.tables.present? && DockerContainerPool.config[:active] +if 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 7c1f317b..b65d401e 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 @@ -40,8 +40,6 @@ Rails.application.routes.draw do get 'dashboard', to: 'dashboard#show' end - get '/help', to: 'application#help' - get 'statistics/', to: 'statistics#show' get 'statistics/graphs', to: 'statistics#graphs' get 'statistics/graphs/user-activity', to: 'statistics#user_activity' @@ -65,7 +63,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/20170112151637_create_lti_parameters.rb b/db/migrate/20170112151637_create_lti_parameters.rb index c7613edf..80348843 100644 --- a/db/migrate/20170112151637_create_lti_parameters.rb +++ b/db/migrate/20170112151637_create_lti_parameters.rb @@ -4,7 +4,7 @@ class CreateLtiParameters < ActiveRecord::Migration 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/schema.rb b/db/schema.rb index aab3e92b..f84e45d1 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,402 @@ # # 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_09_04_115948) 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" 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/docker_client.rb b/lib/docker_client.rb index 55c458ca..5ae35e47 100644 --- a/lib/docker_client.rb +++ b/lib/docker_client.rb @@ -82,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." @@ -272,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 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/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..6cb5fa74 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,18 @@ 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' +# 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 - + # rails sudo add-apt-repository -y ppa:chris-lea/node.js 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 +56,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 @@ -97,7 +104,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 +158,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("