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