diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index e94f8140..00000000 --- a/.browserslistrc +++ /dev/null @@ -1 +0,0 @@ -defaults diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f966ebf9..5ebaed00 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-20.04 services: db: - image: postgres:13 + image: postgres:14 env: POSTGRES_PASSWORD: postgres ports: @@ -35,22 +35,27 @@ jobs: - name: Setup Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: 3.1 bundler-cache: true - name: Setup Node uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 18 - name: Get yarn cache directory path id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn config get cacheFolder)" - - name: Manage yarn cache - uses: actions/cache@v2 + run: echo "::set-output name=dir::$(yarn cache dir)" + - name: Manage yarn, webpack and assets cache + uses: actions/cache@v3 # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) id: yarn-cache with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + path: | + ${{ steps.yarn-cache-dir-path.outputs.dir }} + public/assets + public/packs-test + tmp/cache + tmp/webpacker key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- @@ -70,11 +75,16 @@ jobs: env: RAILS_ENV: test run: bundler exec rake db:schema:load + - name: Precompile assets + env: + RAILS_ENV: test + run: bundler exec rake assets:precompile - name: Run tests env: RAILS_ENV: test CC_TEST_REPORTER_ID: true - run: bundle exec rspec --color --format progress --require spec_helper --require rails_helper + NODE_OPTIONS: --openssl-legacy-provider + run: bundle exec rspec --color --format RSpec::Github::Formatter --format progress --require spec_helper --require rails_helper - name: Send coverage to CodeClimate uses: paambaati/codeclimate-action@v3.0.0 @@ -93,8 +103,16 @@ jobs: - name: Setup Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: 2.7 + ruby-version: 3.1 bundler-cache: true - name: Run rubocop - run: bundle exec rubocop --parallel + uses: reviewdog/action-rubocop@v2 + with: + filter_mode: nofilter + rubocop_version: gemfile + rubocop_extensions: rubocop-rails:gemfile rubocop-rspec:gemfile rubocop-performance:gemfile + rubocop_flags: --parallel + reporter: github-check + skip_install: true + use_bundler: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 95f44d0d..7e2ec921 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -31,7 +31,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -42,7 +42,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -56,4 +56,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0bddfa3f..160eceee 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -19,7 +19,7 @@ Metrics/ClassLength: Max: 600 Metrics/ModuleLength: - Max: 220 + Max: 225 # It's a very complicated application... # diff --git a/Gemfile b/Gemfile index c761f07e..79c41778 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,7 @@ gem 'docker-api', require: 'docker' gem 'eventmachine' gem 'factory_bot_rails' gem 'faraday' +gem 'faraday-net_http_persistent' gem 'faye-websocket' gem 'forgery' gem 'highline' @@ -21,34 +22,36 @@ gem 'js-routes' gem 'kramdown' gem 'mimemagic' gem 'net-http-persistent' +gem 'net-imap', require: false +gem 'net-pop', require: false +gem 'net-smtp', require: false gem 'nokogiri' gem 'pagedown-bootstrap-rails' gem 'pg' -gem 'proforma', github: 'openHPI/proforma', tag: 'v0.7.1' +gem 'proforma', github: 'openHPI/proforma', branch: 'v0.5.2' gem 'prometheus_exporter' gem 'pry-byebug' gem 'puma' gem 'pundit' -gem 'rails', '~> 6.1.4' -gem 'rails_admin' +gem 'rails', '~> 6.1.6' +gem 'rails_admin', '< 3.0.0' # Blocked by https://github.com/railsadminteam/rails_admin/issues/3490 gem 'rails-i18n' gem 'rails-timeago' gem 'ransack' gem 'rest-client' -gem 'rubytree', github: 'evolve75/RubyTree' +gem 'rubytree' gem 'rubyzip' gem 'sass-rails' +gem 'shakapacker', '6.5.1' gem 'slim-rails' gem 'sorcery' # Causes a deprecation warning in Rails 6.0+, see: https://github.com/Sorcery/sorcery/pull/255 gem 'telegraf' -gem 'tubesock', github: 'gosukiwi/tubesock', branch: 'patch-1' # Switch to a fork which is compatible with Rails 5 +gem 'tubesock' gem 'turbolinks' -gem 'webpacker' gem 'whenever', require: false # Error Tracing gem 'mnemosyne-ruby' -gem 'newrelic_rpm' gem 'sentry-rails' gem 'sentry-ruby' @@ -56,6 +59,7 @@ group :development, :staging do gem 'better_errors' gem 'binding_of_caller' gem 'bootsnap', require: false + gem 'letter_opener' gem 'listen' gem 'pry-rails' gem 'rack-mini-profiler' @@ -80,6 +84,7 @@ group :test do gem 'rails-controller-testing' gem 'rspec-autotest' gem 'rspec-collection_matchers' + gem 'rspec-github', require: false gem 'rspec-rails' gem 'selenium-webdriver' gem 'shoulda-matchers' diff --git a/Gemfile.lock b/Gemfile.lock index d055eb52..8354d194 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,93 +1,76 @@ -GIT - remote: https://github.com/evolve75/RubyTree.git - revision: 6081d0959b706dcefb85e85faa329ebb2dabcf9e - specs: - rubytree (1.0.2) - json (~> 2.6.1) - structured_warnings (~> 0.4.0) - -GIT - remote: https://github.com/gosukiwi/tubesock.git - revision: 86a5ca4f7d3c3a7b9a727ad91df3b9b4912eda39 - branch: patch-1 - specs: - tubesock (0.2.7) - rack (>= 1.5.0) - websocket (>= 1.1.0) - GIT remote: https://github.com/openHPI/proforma.git - revision: cf61517a5cd765afb9d0d19ea1c692e18e3131d7 - tag: v0.7.1 + revision: 243853e66034bc2afbb9c9661475d9718d007304 + branch: v0.5.2 specs: - proforma (0.7.1) - activemodel (>= 5.2.3, < 8.0.0) - activesupport (>= 5.2.3, < 8.0.0) - nokogiri (>= 1.10.2, < 2.0.0) - rubyzip (>= 1.2.2, < 3.0.0) + proforma (0.5.2) + activemodel (>= 5.2.3, < 7.2.0) + activesupport (>= 5.2.3, < 7.2.0) + nokogiri (~> 1.13) + rubyzip (~> 2.3) GEM remote: https://rubygems.org/ specs: - ZenTest (4.12.0) - actioncable (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) + ZenTest (4.12.1) + actioncable (6.1.6.1) + actionpack (= 6.1.6.1) + activesupport (= 6.1.6.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionmailbox (6.1.6.1) + actionpack (= 6.1.6.1) + activejob (= 6.1.6.1) + activerecord (= 6.1.6.1) + activestorage (= 6.1.6.1) + activesupport (= 6.1.6.1) mail (>= 2.7.1) - actionmailer (6.1.4.4) - actionpack (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionmailer (6.1.6.1) + actionpack (= 6.1.6.1) + actionview (= 6.1.6.1) + activejob (= 6.1.6.1) + activesupport (= 6.1.6.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.4) - actionview (= 6.1.4.4) - activesupport (= 6.1.4.4) + actionpack (6.1.6.1) + actionview (= 6.1.6.1) + activesupport (= 6.1.6.1) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.4) - actionpack (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + actiontext (6.1.6.1) + actionpack (= 6.1.6.1) + activerecord (= 6.1.6.1) + activestorage (= 6.1.6.1) + activesupport (= 6.1.6.1) nokogiri (>= 1.8.5) - actionview (6.1.4.4) - activesupport (= 6.1.4.4) + actionview (6.1.6.1) + activesupport (= 6.1.6.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.1.4.4) - activesupport (= 6.1.4.4) + activejob (6.1.6.1) + activesupport (= 6.1.6.1) globalid (>= 0.3.6) - activemodel (6.1.4.4) - activesupport (= 6.1.4.4) + activemodel (6.1.6.1) + activesupport (= 6.1.6.1) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (6.1.4.4) - activemodel (= 6.1.4.4) - activesupport (= 6.1.4.4) - activestorage (6.1.4.4) - actionpack (= 6.1.4.4) - activejob (= 6.1.4.4) - activerecord (= 6.1.4.4) - activesupport (= 6.1.4.4) - marcel (~> 1.0.0) + activerecord (6.1.6.1) + activemodel (= 6.1.6.1) + activesupport (= 6.1.6.1) + activestorage (6.1.6.1) + actionpack (= 6.1.6.1) + activejob (= 6.1.6.1) + activerecord (= 6.1.6.1) + activesupport (= 6.1.6.1) + marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.4) + activesupport (6.1.6.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -101,7 +84,7 @@ GEM minitest-autotest (~> 1.0) autotest-rails (4.2.1) ZenTest (~> 4.5) - bcrypt (3.1.16) + bcrypt (3.1.18) better_errors (2.9.1) coderay (>= 1.0.0) erubi (>= 1.0.0) @@ -109,8 +92,8 @@ GEM bindex (0.8.1) binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) - bootsnap (1.9.3) - msgpack (~> 1.0) + bootsnap (1.13.0) + msgpack (~> 1.2) bootstrap-will_paginate (1.0.0) will_paginate builder (3.2.4) @@ -118,7 +101,7 @@ GEM amq-protocol (~> 2.3, >= 2.3.1) sorted_set (~> 1, >= 1.0.2) byebug (11.1.3) - capybara (3.36.0) + capybara (3.37.1) addressable matrix mini_mime (>= 0.1.3) @@ -139,7 +122,7 @@ GEM childprocess (4.1.0) chronic (0.10.2) coderay (1.1.3) - concurrent-ruby (1.1.9) + concurrent-ruby (1.1.10) connection_pool (2.2.5) crack (0.4.5) rexml @@ -152,50 +135,36 @@ GEM database_cleaner-core (2.0.1) debug_inspector (1.1.0) diff-lcs (1.5.0) + digest (3.1.0) docile (1.4.0) docker-api (2.2.0) excon (>= 0.47.0) multi_json domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - ecma-re-validator (0.3.0) - regexp_parser (~> 2.0) - erubi (1.10.0) + ecma-re-validator (0.4.0) + regexp_parser (~> 2.2) + erubi (1.11.0) eventmachine (1.2.7) - excon (0.89.0) - factory_bot (6.2.0) + excon (0.92.4) + factory_bot (6.2.1) activesupport (>= 5.0.0) factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) - faraday (1.9.3) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) + faraday (2.5.2) + faraday-net_http (>= 2.0, < 3.1) ruby2_keywords (>= 0.0.4) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.2) - multipart-post (>= 1.2, < 3) - faraday-net_http (1.0.1) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) + faraday-net_http (3.0.0) + faraday-net_http_persistent (2.1.0) + faraday (~> 2.5) + net-http-persistent (~> 4.0) faye-websocket (0.11.1) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) - ffi (1.15.4) + ffi (1.15.5) forgery (0.8.1) + glob (0.3.0) globalid (1.0.0) activesupport (>= 5.0) haml (5.2.2) @@ -206,37 +175,38 @@ GEM headless (2.3.1) highline (2.0.3) http-accept (1.7.0) - http-cookie (1.0.4) + http-cookie (1.0.5) domain_name (~> 0.5) - i18n (1.8.11) + i18n (1.12.0) concurrent-ruby (~> 1.0) - i18n-js (3.9.0) - i18n (>= 0.6.6) - image_processing (1.12.1) + i18n-js (4.0.0) + glob + i18n + image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - ims-lti (1.2.4) + ims-lti (1.2.6) builder (>= 1.0, < 4.0) oauth (>= 0.4.5, < 0.6) influxdb (0.8.1) jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) - jquery-rails (4.4.0) + jquery-rails (4.5.0) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-ui-rails (6.0.1) railties (>= 3.2.16) - js-routes (2.2.0) + js-routes (2.2.4) railties (>= 4) - json (2.6.1) - json_schemer (0.2.18) + json (2.6.2) + json_schemer (0.2.21) ecma-re-validator (~> 0.3) hana (~> 1.3) regexp_parser (~> 2.0) uri_template (~> 0.7) - jwt (2.3.0) + jwt (2.4.1) kaminari (1.2.2) activesupport (>= 4.1.0) kaminari-actionview (= 1.2.2) @@ -249,12 +219,16 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) - kramdown (2.3.1) + kramdown (2.4.0) rexml - listen (3.7.0) + launchy (2.5.0) + addressable (~> 2.7) + letter_opener (1.8.1) + launchy (>= 2.2, < 3) + listen (3.7.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) - loofah (2.13.0) + loofah (2.18.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -270,85 +244,97 @@ GEM rake mini_magick (4.11.0) mini_mime (1.1.2) - mini_portile2 (2.5.3) - minitest (5.15.0) + mini_portile2 (2.8.0) + minitest (5.16.3) minitest-autotest (1.1.1) minitest-server (~> 1.0) path_expander (~> 1.0) minitest-server (1.0.6) minitest (~> 5.0) - mnemosyne-ruby (1.12.0) + mnemosyne-ruby (1.13.0) activesupport (>= 4) bunny - msgpack (1.4.2) + msgpack (1.5.4) multi_json (1.15.0) multi_xml (0.6.0) - multipart-post (2.1.1) nested_form (0.3.2) net-http-persistent (4.0.1) connection_pool (~> 2.2) + net-imap (0.2.3) + digest + net-protocol + strscan + net-pop (0.1.1) + digest + net-protocol + timeout + net-protocol (0.1.3) + timeout + net-smtp (0.3.1) + digest + net-protocol + timeout netrc (0.11.0) - newrelic_rpm (8.2.0) nio4r (2.5.8) - nokogiri (1.11.7) - mini_portile2 (~> 2.5.0) + nokogiri (1.13.8) + mini_portile2 (~> 2.8.0) racc (~> 1.4) nyan-cat-formatter (0.12.0) rspec (>= 2.99, >= 2.14.2, < 4) - oauth (0.5.8) - oauth2 (1.4.7) - faraday (>= 0.8, < 2.0) + oauth (0.5.10) + oauth2 (1.4.10) + faraday (>= 0.17.3, < 3.0) jwt (>= 1.0, < 3.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) pagedown-bootstrap-rails (2.1.4) railties (> 3.1) - parallel (1.21.0) - parser (3.1.0.0) + parallel (1.22.1) + parser (3.1.2.1) ast (~> 2.4.1) - path_expander (1.1.0) - pg (1.2.3) - prometheus_exporter (1.0.1) + path_expander (1.1.1) + pg (1.4.3) + prometheus_exporter (2.0.3) webrick - pry (0.13.1) + pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) - pry-byebug (3.9.0) + pry-byebug (3.10.1) byebug (~> 11.0) - pry (~> 0.13.0) + pry (>= 0.13, < 0.15) pry-rails (0.3.9) pry (>= 0.10.4) - public_suffix (4.0.6) - puma (5.5.2) + public_suffix (4.0.7) + puma (5.6.4) nio4r (~> 2.0) - pundit (2.1.1) + pundit (2.2.0) activesupport (>= 3.0.0) racc (1.6.0) - rack (2.2.3) - rack-mini-profiler (2.3.3) + rack (2.2.4) + rack-mini-profiler (3.0.0) rack (>= 1.2.0) rack-pjax (1.1.0) nokogiri (~> 1.5) rack (>= 1.1) rack-proxy (0.7.2) rack - rack-test (1.1.0) - rack (>= 1.0, < 3) - rails (6.1.4.4) - actioncable (= 6.1.4.4) - actionmailbox (= 6.1.4.4) - actionmailer (= 6.1.4.4) - actionpack (= 6.1.4.4) - actiontext (= 6.1.4.4) - actionview (= 6.1.4.4) - activejob (= 6.1.4.4) - activemodel (= 6.1.4.4) - activerecord (= 6.1.4.4) - activestorage (= 6.1.4.4) - activesupport (= 6.1.4.4) + rack-test (2.0.2) + rack (>= 1.3) + rails (6.1.6.1) + actioncable (= 6.1.6.1) + actionmailbox (= 6.1.6.1) + actionmailer (= 6.1.6.1) + actionpack (= 6.1.6.1) + actiontext (= 6.1.6.1) + actionview (= 6.1.6.1) + activejob (= 6.1.6.1) + activemodel (= 6.1.6.1) + activerecord (= 6.1.6.1) + activestorage (= 6.1.6.1) + activesupport (= 6.1.6.1) bundler (>= 1.15.0) - railties (= 6.1.4.4) + railties (= 6.1.6.1) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -357,14 +343,14 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.4.2) + rails-html-sanitizer (1.4.3) loofah (~> 2.3) - rails-i18n (7.0.1) + rails-i18n (7.0.5) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - rails-timeago (2.19.1) - actionpack (>= 3.1) - activesupport (>= 3.1) + rails-timeago (2.20.0) + actionpack (>= 5.2) + activesupport (>= 5.2) rails_admin (2.2.1) activemodel-serializers-xml (>= 1.0) builder (~> 3.1) @@ -377,23 +363,23 @@ GEM rails (>= 5.0, < 7) remotipart (~> 1.3) sassc-rails (>= 1.3, < 3) - railties (6.1.4.4) - actionpack (= 6.1.4.4) - activesupport (= 6.1.4.4) + railties (6.1.6.1) + actionpack (= 6.1.6.1) + activesupport (= 6.1.6.1) method_source - rake (>= 0.13) + rake (>= 12.2) thor (~> 1.0) - rainbow (3.0.0) + rainbow (3.1.1) rake (13.0.6) - ransack (2.5.0) - activerecord (>= 5.2.4) - activesupport (>= 5.2.4) + ransack (3.2.1) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) i18n - rb-fsevent (0.11.0) + rb-fsevent (0.11.1) rb-inotify (0.10.1) ffi (~> 1.0) - rbtree (0.4.4) - regexp_parser (2.2.0) + rbtree (0.4.5) + regexp_parser (2.5.0) remotipart (1.4.4) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) @@ -401,23 +387,25 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) rexml (3.2.5) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) + rspec (3.11.0) + rspec-core (~> 3.11.0) + rspec-expectations (~> 3.11.0) + rspec-mocks (~> 3.11.0) rspec-autotest (1.0.2) rspec-core (>= 2.99.0.beta1, < 4.0.0) rspec-collection_matchers (1.2.0) rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) + rspec-support (~> 3.11.0) + rspec-github (2.3.1) + rspec-core (~> 3.0) + rspec-mocks (3.11.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-rails (5.0.2) + rspec-support (~> 3.11.0) + rspec-rails (5.1.2) actionpack (>= 5.2) activesupport (>= 5.2) railties (>= 5.2) @@ -425,31 +413,34 @@ GEM rspec-expectations (~> 3.10) rspec-mocks (~> 3.10) rspec-support (~> 3.10) - rspec-support (3.10.3) - rubocop (1.24.1) + rspec-support (3.11.0) + rubocop (1.35.0) + json (~> 2.3) parallel (~> 1.10) - parser (>= 3.0.0.0) + parser (>= 3.1.2.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) - rexml - rubocop-ast (>= 1.15.1, < 2.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.20.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.15.1) - parser (>= 3.0.1.1) - rubocop-performance (1.13.1) + rubocop-ast (1.21.0) + parser (>= 3.1.1.0) + rubocop-performance (1.14.3) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) - rubocop-rails (2.13.1) + rubocop-rails (2.15.2) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.7.0, < 2.0) - rubocop-rspec (2.7.0) - rubocop (~> 1.19) + rubocop-rspec (2.12.1) + rubocop (~> 1.31) ruby-progressbar (1.11.0) ruby-vips (2.1.4) ffi (~> 1.12) ruby2_keywords (0.0.5) + rubytree (2.0.0) + json (~> 2.0, > 2.3.1) rubyzip (2.3.2) sass-rails (6.0.0) sassc-rails (~> 2.1, >= 2.1.1) @@ -461,22 +452,23 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (4.1.0) + selenium-webdriver (4.4.0) childprocess (>= 0.5, < 5.0) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) semantic_range (3.0.0) - sentry-rails (4.8.3) + sentry-rails (5.4.2) railties (>= 5.0) - sentry-ruby-core (~> 4.8.3) - sentry-ruby (4.8.3) + sentry-ruby (~> 5.4.2) + sentry-ruby (5.4.2) concurrent-ruby (~> 1.0, >= 1.0.2) - faraday (~> 1.0) - sentry-ruby-core (= 4.8.3) - sentry-ruby-core (4.8.3) - concurrent-ruby - faraday set (1.0.2) + shakapacker (6.5.1) + activesupport (>= 5.2) + rack-proxy (>= 0.6.1) + railties (>= 5.2) + semantic_range (>= 2.3.0) shoulda-matchers (5.1.0) activesupport (>= 5.2.0) simplecov (0.21.2) @@ -484,15 +476,15 @@ GEM simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) - simplecov_json_formatter (0.1.3) + simplecov_json_formatter (0.1.4) slim (4.1.0) temple (>= 0.7.6, < 0.9) tilt (>= 2.0.6, < 2.1) - slim-rails (3.3.0) + slim-rails (3.5.1) actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 5.0) - sorcery (0.16.2) + sorcery (0.16.3) bcrypt (~> 3.1) oauth (~> 0.5, >= 0.5.5) oauth2 (~> 1.0, >= 0.8.0) @@ -500,44 +492,43 @@ GEM rbtree set (~> 1.0) spring (4.0.0) - sprockets (4.0.2) + sprockets (4.1.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.4.2) actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - ssrf_filter (1.0.7) - structured_warnings (0.4.0) - telegraf (2.0.0) + ssrf_filter (1.0.8) + strscan (3.0.4) + telegraf (2.1.0) influxdb temple (0.8.2) thor (1.2.1) - tilt (2.0.10) + tilt (2.0.11) + timeout (0.3.0) + tubesock (0.2.9) + rack (>= 1.5.0) + websocket (>= 1.1.0) turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - tzinfo (2.0.4) + tzinfo (2.0.5) concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext - unf_ext (0.0.8) - unicode-display_width (2.1.0) + unf_ext (0.0.8.2) + unicode-display_width (2.2.0) uri_template (0.7.0) web-console (4.2.0) actionview (>= 6.0.0) activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webmock (3.14.0) + webmock (3.18.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.4.3) - activesupport (>= 5.2) - rack-proxy (>= 0.6.1) - railties (>= 5.2) - semantic_range (>= 2.3.0) webrick (1.7.0) websocket (1.2.9) websocket-driver (0.7.5) @@ -548,7 +539,7 @@ GEM will_paginate (3.3.1) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.5.3) + zeitwerk (2.6.0) PLATFORMS ruby @@ -569,6 +560,7 @@ DEPENDENCIES eventmachine factory_bot_rails faraday + faraday-net_http_persistent faye-websocket forgery headless @@ -579,11 +571,14 @@ DEPENDENCIES js-routes json_schemer kramdown + letter_opener listen mimemagic mnemosyne-ruby net-http-persistent - newrelic_rpm + net-imap + net-pop + net-smtp nokogiri nyan-cat-formatter pagedown-bootstrap-rails @@ -595,38 +590,39 @@ DEPENDENCIES puma pundit rack-mini-profiler - rails (~> 6.1.4) + rails (~> 6.1.6) rails-controller-testing rails-i18n rails-timeago - rails_admin + rails_admin (< 3.0.0) ransack rest-client rspec-autotest rspec-collection_matchers + rspec-github rspec-rails rubocop rubocop-performance rubocop-rails rubocop-rspec - rubytree! + rubytree rubyzip sass-rails selenium-webdriver sentry-rails sentry-ruby + shakapacker (= 6.5.1) shoulda-matchers simplecov slim-rails sorcery spring telegraf - tubesock! + tubesock turbolinks web-console webmock - webpacker whenever BUNDLED WITH - 2.3.4 + 2.3.17 diff --git a/README.md b/README.md index e6bda9f4..20c2ab5b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The programming courses offered on openHPI include practical programming exercis 3. CodeOcean includes unit tests to provide feedback for learners and score their code. A unit test is defined as a program that either runs the learner’s code in a pre-defined way and compares the provided result with an expectation or the unit test parses the student’s source code and matches it against an exercise-defined string. While the code of the unit tests are hidden, learners can run the unit tests at any time and get instant feedback whether they passed or failed. If the unit tests fail the result is shown together with an error message defined by the MOOC instructors. On the one hand, this feedback helps people to help themselves and provides learners with a hint of their mistake. On the other hand, the automated scoring using unit tests is required to indicate progress for the learners. In the context of a MOOC with thousands of active learners, a manual review by the instructors is not feasible and peer-review of source code has not been implemented in CodeOcean so far. 4. In CodeOcean, learners can ask questions about their program directly within the platform and in context of their current program. Usually, MOOC platforms provide a forum to discuss questions. While this concept also works great for source code in general outside of a MOOC (cf. [StackOverflow](https://stackoverflow.com)), it is an additional barrier for novices to summarize their problem externally. To understand the problem, contextual information is generally of help for others to provide the current solution. When using a dedicated forum, learners are required to provide as much information as necessary to reproduce the issue which beginners might find difficult to identify. As a result, they might copy too few or too much information. In addition, early iterations of the Java courses showed that learners did not format their source code appropriate in forum posts (but as plain text), making it difficult to read. With _Request for Comments_, CodeOcean provides a built-in feature to ask a question in the context of an exercise, thus lowering the barriers to get help. CodeOcean presents the learner’s source code and error message together with the question to fellow students and allows them to add a comment specifically to one line of code. Hence, the previously described issue is solved with a dedicated forum. -CodeOcean is mainly used in the context of MOOCs (such as those offered on openHPI and mooc.house) and has been used by more than 60,000 users as of June 2020. CodeOcean is a stand-alone tool implementing the [Learning Tools Interoperability (LTI)](http://www.imsglobal.org/activity/learning-tools-interoperability) standard to be used in various learning scenarios. By offering an LTI interface, it is accessible from MOOC providers as well as other providers, such as the HPI Schul-Cloud. CodeOcean itself cannot be used directly by learners or other users than the MOOCs instructors or administrators. +CodeOcean is mainly used in the context of MOOCs (such as those offered on openHPI and mooc.house) and has been used by more than 60,000 users as of June 2020. CodeOcean is a stand-alone tool implementing the [Learning Tools Interoperability (LTI)](https://www.imsglobal.org/activity/learning-tools-interoperability) standard to be used in various learning scenarios. By offering an LTI interface, it is accessible from MOOC providers as well as other providers, such as the HPI Schul-Cloud. CodeOcean itself cannot be used directly by learners or other users than the MOOCs instructors or administrators. ## Development Setup @@ -48,7 +48,7 @@ In order to execute code submissions using the [DockerContainerPool](https://git ## Production Setup -- We recommend using [Capistrano](http://capistranorb.com/) for deployment. +- We recommend using [Capistrano](https://capistranorb.com/) for deployment. - Once deployed, CodeOcean assumes to run exclusively under a (sub)domain. If you want to use it under a custom subpath, you can specify the desired path using an environment variable: `RAILS_RELATIVE_URL_ROOT=/codeocean`. Please ensure to rebuild all assets and restart the server to apply the new path. ## Monitoring diff --git a/Vagrantfile b/Vagrantfile index b4b9999b..96a53270 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -10,7 +10,7 @@ Vagrant.configure(2) do |config| v.cpus = 4 end config.vm.network 'forwarded_port', - host_ip: ENV['LISTEN_ADDRESS'] || '127.0.0.1', + host_ip: ENV.fetch('LISTEN_ADDRESS', '127.0.0.1'), host: 7000, guest: 7000 config.vm.synced_folder '.', '/home/vagrant/codeocean' diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 16c88a1d..2a13291d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,12 +14,9 @@ //= require pagedown_bootstrap //= require rails-timeago //= require locales/jquery.timeago.de.js -//= require i18n -//= require i18n/translations // // lib/assets //= require flash -//= require url // // vendor/assets //= require ace/ace diff --git a/app/assets/javascripts/bootstrap-dropdown-submenu.js b/app/assets/javascripts/bootstrap-dropdown-submenu.js index 10809c02..38ad3854 100644 --- a/app/assets/javascripts/bootstrap-dropdown-submenu.js +++ b/app/assets/javascripts/bootstrap-dropdown-submenu.js @@ -1,6 +1,6 @@ $(document).on('turbolinks:load', function() { - var subMenusSelector = 'ul.dropdown-menu [data-toggle=dropdown]'; + var subMenusSelector = 'ul.dropdown-menu [data-bs-toggle=dropdown]'; function openSubMenu(event) { if (this.pathname === '/') { diff --git a/app/assets/javascripts/codeharbor_link.js b/app/assets/javascripts/codeharbor_link.js index 4cd49ece..fdf42859 100644 --- a/app/assets/javascripts/codeharbor_link.js +++ b/app/assets/javascripts/codeharbor_link.js @@ -1,5 +1,5 @@ $(document).on('turbolinks:load', function() { - $('[data-toggle="tooltip"]').tooltip(); + $('[data-bs-toggle="tooltip"]').tooltip(); if($.isController('codeharbor_links')) { if ($('.edit_codeharbor_link, .new_codeharbor_link').isPresent()) { diff --git a/app/assets/javascripts/dashboard.js b/app/assets/javascripts/dashboard.js index 38504849..e8c43444 100644 --- a/app/assets/javascripts/dashboard.js +++ b/app/assets/javascripts/dashboard.js @@ -12,7 +12,7 @@ $(document).on('turbolinks:load', function() { return _.map($('tbody tr[data-id]'), function(element) { return { content: $('td.name', element).text(), - id: $(element).data('id'), + id: `execution_environment_${$(element).data('id')}`, visible: false }; }); @@ -67,7 +67,7 @@ $(document).on('turbolinks:load', function() { var setGroupVisibility = function(response) { _.each(response.docker, function(data) { groups.update({ - id: data.id, + id: `execution_environment_${data.id}`, visible: data.prewarmingPoolSize > 0 }); }); @@ -76,7 +76,7 @@ $(document).on('turbolinks:load', function() { var updateChartData = function(response) { _.each(response.docker, function(data) { dataset.add({ - group: data.id, + group: `execution_environment_${data.id}`, x: vis.moment(), y: data.usedRunners }); diff --git a/app/assets/javascripts/editor.js b/app/assets/javascripts/editor.js index 465e2f49..6dc79b7f 100644 --- a/app/assets/javascripts/editor.js +++ b/app/assets/javascripts/editor.js @@ -15,12 +15,9 @@ $(document).on('turbolinks:load', function(event) { ); if ($('#editor').isPresent() && CodeOceanEditor && event.originalEvent.data.url.includes("/implement")) { - if (CodeOceanEditor.isBrowserSupported()) { - $('#alert').hide(); - // This call will (amon other things) initializeEditors and load the content except for the last line - // It must not be called during page navigation. Otherwise, content will be duplicated! - // Search for insertLines and Turbolinks reload / cache control - CodeOceanEditor.initializeEverything(); - } + // This call will (amon other things) initializeEditors and load the content except for the last line + // It must not be called during page navigation. Otherwise, content will be duplicated! + // Search for insertLines and Turbolinks reload / cache control + CodeOceanEditor.initializeEverything(); } }); diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index cce1c53e..3df79531 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -17,7 +17,7 @@ var CodeOceanEditor = { //Request-For-Comments-Configuration REQUEST_FOR_COMMENTS_DELAY: 0, REQUEST_TOOLTIP_TIME: 5000, - REQUEST_TOOLTIP_DELAY: 10 * 60 * 1000, + REQUEST_TOOLTIP_DELAY: 15 * 60 * 1000, editors: [], editor_for_file: new Map(), @@ -78,7 +78,7 @@ var CodeOceanEditor = { 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;
         }
@@ -216,8 +216,8 @@ var CodeOceanEditor = {
     },
 
     hideSpinner: function () {
-        $('button i.fa, button i.far, button i.fas').show();
-        $('button i.fa-spin').hide();
+        $('button i.fa-solid, button i.fa-regular').show();
+        $('button i.fa-spin').removeClass('d-inline-block').addClass('d-none');
     },
 
 
@@ -235,10 +235,20 @@ var CodeOceanEditor = {
         window.dispatchEvent(new Event('resize'));
     },
 
-    resizeParentOfAceEditor: function (element) {
+    resizeSidebars: function () {
+        $('#content-left-sidebar').height(this.calculateEditorHeight('#content-left-sidebar', false));
+        $('#content-right-sidebar').height(this.calculateEditorHeight('#content-right-sidebar', false));
+    },
+
+    calculateEditorHeight: function (element, considerStatusbar) {
+        let bottom = considerStatusbar ? ($('#statusbar').height() || 0) : 0;
         // 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 - ($('#statusbar').height() || 0) - 5;
-        $(element).parent().height(windowHeight);
+        return window.innerHeight - $(element).offset().top - bottom - 5;
+    },
+
+    resizeParentOfAceEditor: function (element) {
+        const editorHeight = this.calculateEditorHeight(element, true);
+        $(element).parent().height(editorHeight);
     },
 
     initializeEditors: function (own_solution = false) {
@@ -259,6 +269,7 @@ var CodeOceanEditor = {
             // Resize frame on window size change
             $(window).resize(function () {
                 this.resizeParentOfAceEditor(element);
+                this.resizeSidebars();
             }.bind(this));
 
             var editor = ace.edit(element);
@@ -366,11 +377,9 @@ var CodeOceanEditor = {
         }
         filesInstance.jstree(filesInstance.data('entries'));
         filesInstance.on('click', 'li.jstree-leaf > a', function (event) {
-            this.setActiveFile(
-                $(event.target).parent().text(),
-                parseInt($(event.target).parent().attr('id'))
-            );
-            var frame = $('[data-file-id="' + this.active_file.id + '"]').parent();
+            const file_id = parseInt($(event.target).parent().attr('id'));
+            const frame = $('[data-file-id="' + file_id + '"]').parent();
+            this.setActiveFile(frame.data('filename'), file_id);
             this.showFrame(frame);
             this.toggleButtonStates();
         }.bind(this));
@@ -392,6 +401,7 @@ var CodeOceanEditor = {
             tipButton.on('click', this.handleSideBarToggle.bind(this));
         }
         $('#sidebar').on('transitionend', this.resizeAceEditors.bind(this));
+        $('#sidebar').on('transitionend', this.resizeSidebars.bind(this));
     },
 
     handleSideBarToggle: function () {
@@ -435,12 +445,12 @@ var CodeOceanEditor = {
         button.prop('disabled', true);
         button.on('click', function () {
             $('#rfc_intervention_text').hide()
-            $('#comment-modal').modal('show');
+            new bootstrap.Modal($('#comment-modal')).show();
         });
 
         $('#askForCommentsButton').on('click', this.requestComments.bind(this));
         $('#closeAskForCommentsButton').on('click', function () {
-            $('#comment-modal').modal('hide');
+            bootstrap.Modal.getInstance($('#comment-modal')).hide();
         });
 
         setTimeout(function () {
@@ -477,30 +487,24 @@ var CodeOceanEditor = {
         return this.isActiveFileExecutable() && ['teacher_defined_test', 'user_defined_test', 'teacher_defined_linter'].includes(this.active_frame.data('role'));
     },
 
-    isBrowserSupported: function () {
-        //  websockets are used for run, score and test
-        // Also exclude IE and IE 11
-        return Modernizr.websockets && window.navigator.userAgent.indexOf("MSIE") <= 0 && !navigator.userAgent.match(/Trident\/7\./);
-    },
-
     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-md-9').eq(0).find('.number').eq(0).text(result.passed);
+        card.find('.row .col-md-9').eq(0).find('.number').eq(1).text(result.count);
         if (result.weight !== 0) {
-            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-md-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2)));
+            card.find('.row .col-md-9').eq(1).find('.number').eq(1).text(result.weight);
         } else {
             // Hide score row if no score could be achieved
             card.find('.attribute-row.row').eq(1).addClass('d-none');
         }
-        card.find('.row .col-sm-9').eq(2).html(result.message);
+        card.find('.row .col-md-9').eq(2).html(result.message);
 
         // Add error message from code to card
         if (result.error_messages) {
-            const targetNode = card.find('.row .col-sm-9').eq(3);
+            const targetNode = card.find('.row .col-md-9').eq(3);
 
             let errorMessagesToShow = [];
             result.error_messages.forEach(function (item) {
@@ -571,7 +575,7 @@ var CodeOceanEditor = {
             }
             targetNode.append(ul);
         }
-        //card.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index);
+        //card.find('.row .col-md-9').eq(4).find('a').attr('href', '#output-' + index);
     },
 
     createEventHandler: function (eventType, data) {
@@ -652,12 +656,17 @@ var CodeOceanEditor = {
 
                 let matches;
 
-                let augmented_text = text;
+                // Switch both lines below to enable the output of images and render  tags.
+                // Also consider `printOutput` in evaluation.js
+
+                // let augmented_text = element.text();
+                let augmented_text = element.html();
                 while (matches = this.tracepositions_regex.exec(text)) {
                     const frame = $('div.frame[data-filename="' + matches[1] + '"]')
 
                     if (frame.length > 0) {
-                        augmented_text = augmented_text.replace(new RegExp(matches[0], 'g'), "" + matches[0] + "");
+                        // augmented_text = augmented_text.replace(new RegExp(matches[0], 'g'), "" + matches[0] + "");
+                        augmented_text = augmented_text.replace(new RegExp(_.unescape(matches[0]), 'g'), "" + matches[0] + "");
                     }
                 }
                 element.html(augmented_text);
@@ -694,8 +703,8 @@ var CodeOceanEditor = {
     },
 
     showSpinner: function (initiator) {
-        $(initiator).find('i.fa, i.far, i.fas').hide();
-        $(initiator).find('i.fa-spin').show();
+        $(initiator).find('i.fa-solid, i.fa-regular').hide();
+        $(initiator).find('i.fa-spin').addClass('d-inline-block').removeClass('d-none');
     },
 
     showStatus: function (output) {
@@ -703,9 +712,11 @@ var CodeOceanEditor = {
             this.showTimeoutMessage();
         } else if (output.status === 'container_depleted') {
             this.showContainerDepletedMessage();
+        } else if (output.status === 'out_of_memory') {
+            this.showOutOfMemoryMessage();
         } else if (output.stderr) {
             $.flash.danger({
-                icon: ['fa', 'fa-bug'],
+                icon: ['fa-solid', 'fa-bug'],
                 text: $('#run').data('message-failure')
             });
             Sentry.captureException(JSON.stringify(output));
@@ -738,14 +749,21 @@ var CodeOceanEditor = {
 
     showContainerDepletedMessage: function () {
         $.flash.danger({
-            icon: ['fa', 'fa-clock-o'],
+            icon: ['fa-regular', 'fa-clock'],
             text: $('#editor').data('message-depleted')
         });
     },
 
+    showOutOfMemoryMessage: function () {
+        $.flash.info({
+            icon: ['fa-regular', 'fa-clock'],
+            text: $('#editor').data('message-out-of-memory')
+        });
+    },
+
     showTimeoutMessage: function () {
         $.flash.info({
-            icon: ['fa', 'fa-clock-o'],
+            icon: ['fa-regular', 'fa-clock'],
             text: $('#editor').data('message-timeout')
         });
     },
@@ -766,7 +784,7 @@ var CodeOceanEditor = {
         event.preventDefault();
         this.createSubmission('#create-file', null, function (response) {
             $('#code_ocean_file_context_id').val(response.id);
-            $('#modal-file').modal('show');
+            new bootstrap.Modal($('#modal-file')).show();
         }.bind(this));
     },
 
@@ -774,6 +792,7 @@ var CodeOceanEditor = {
         $('#toggle-sidebar-output').on('click', this.hideOutputBar.bind(this));
         $('#toggle-sidebar-output-collapsed').on('click', this.showOutputBar.bind(this));
         $('#output_sidebar').on('transitionend', this.resizeAceEditors.bind(this));
+        $('#output_sidebar').on('transitionend', this.resizeSidebars.bind(this));
     },
 
     showOutputBar: function () {
@@ -789,7 +808,7 @@ var CodeOceanEditor = {
     },
 
     initializeSideBarTooltips: function () {
-        $('[data-toggle="tooltip"]').tooltip()
+        $('[data-bs-toggle="tooltip"]').tooltip()
     },
 
     initializeDescriptionToggle: function () {
@@ -797,12 +816,13 @@ var CodeOceanEditor = {
         $('a#toggle').on('click', this.toggleDescriptionCard.bind(this));
     },
 
-    toggleDescriptionCard: function () {
+    toggleDescriptionCard: function (event) {
         $('#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();
+        this.resizeSidebars();
         event.preventDefault();
     },
 
@@ -835,11 +855,7 @@ var CodeOceanEditor = {
                     const percentile75 = data['working_time_75_percentile'];
                     const accumulatedWorkTimeUser = data['working_time_accumulated'];
 
-                    let minTimeIntervention = 10 * 60 * 1000;
-                    if ($('#editor').data('exercise-id') === 909) {
-                        // 30 minutes for our large Map exercise
-                        minTimeIntervention = 30 * 60 * 1000;
-                    }
+                    let minTimeIntervention = 20 * 60 * 1000;
 
                     let timeUntilIntervention;
                     if ((accumulatedWorkTimeUser - percentile75) > 0) {
@@ -861,17 +877,21 @@ var CodeOceanEditor = {
                             clearInterval(tid);
                             // timeUntilIntervention passed
                             if (editor.data('tips-interventions')) {
-                                $('#tips-intervention-modal').modal('show');
+                                const modal = $('#tips-intervention-modal');
+                                modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
+                                new bootstrap.Modal(modal).show();
                                 $.ajax({
                                     data: {
-                                        intervention_type: 'TipIntervention'
+                                        intervention_type: 'TipsIntervention'
                                     },
                                     dataType: 'json',
                                     type: 'POST',
                                     url: interventionSaveUrl
                                 });
                             } else if (editor.data('break-interventions')) {
-                                $('#break-intervention-modal').modal('show');
+                                const modal = $('#break-intervention-modal');
+                                modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
+                                new bootstrap.Modal(modal).show();
                                 $.ajax({
                                     data: {
                                         intervention_type: 'BreakIntervention'
@@ -885,7 +905,12 @@ var CodeOceanEditor = {
                                 // only show intervention if user did not requested for a comment already
                                 if (!button.prop('disabled')) {
                                     $('#rfc_intervention_text').show();
-                                    $('#comment-modal').modal('show');
+                                    modal = $('#comment-modal');
+                                    modal.find('.modal-footer').html(I18n.t("exercises.implement.intervention.explanation", {duration: Math.round(percentile75 / 60)}));
+                                    modal.on('hidden.bs.modal', function () {
+                                        modal.find('.modal-footer').text('');
+                                    });
+                                    new bootstrap.Modal(modal).show();
                                     $.ajax({
                                         data: {
                                             intervention_type: 'QuestionIntervention'
@@ -928,7 +953,6 @@ var CodeOceanEditor = {
         CodeOceanEditor.editors = [];
         this.initializeRegexes();
         this.initializeCodePilot();
-        $('.score, #development-environment').show();
         this.configureEditors();
         this.initializeEditors();
         this.initializeEventHandlers();
@@ -944,6 +968,7 @@ var CodeOceanEditor = {
         this.renderScore();
         this.showFirstFile();
         this.resizeAceEditors();
+        this.resizeSidebars();
         this.initializeDeadlines();
         CodeOceanEditorTips.initializeEventHandlers();
 
diff --git a/app/assets/javascripts/editor/evaluation.js b/app/assets/javascripts/editor/evaluation.js
index d3f8ed14..6fbec44a 100644
--- a/app/assets/javascripts/editor/evaluation.js
+++ b/app/assets/javascripts/editor/evaluation.js
@@ -1,5 +1,8 @@
 CodeOceanEditorEvaluation = {
     chunkBuffer: [{streamedResponse: true}],
+    // A list of non-printable characters that are not allowed in the code output.
+    // Taken from https://stackoverflow.com/a/69024306
+    nonPrintableRegEx: /[\u0000-\u0008\u000B\u000C\u000F-\u001F\u007F-\u009F\u2000-\u200F\u2028-\u202F\u205F-\u206F\u3000\uFEFF]/g,
 
     /**
      * Scoring-Functions
@@ -99,6 +102,11 @@ CodeOceanEditorEvaluation = {
         })) {
             this.showTimeoutMessage();
         }
+        if (_.some(response, function (result) {
+            return result.status === 'out_of_memory';
+        })) {
+            this.showOutOfMemoryMessage();
+        }
         if (_.some(response, function (result) {
             return result.status === 'container_depleted';
         })) {
@@ -199,26 +207,39 @@ CodeOceanEditorEvaluation = {
             return;
         }
 
+        if (output.stdout !== undefined && !output.stdout.startsWith(" tags
+        // Switch all four lines below to enable the output of images and render  tags.
+        // Also consider `augmentStacktraceInOutput` in editor.js.erb
         if (!colorize) {
             if (output.stdout !== undefined && output.stdout !== '') {
-                //element.append(output.stdout)
-                element.text(element.text() + output.stdout)
+                output.stdout = output.stdout.replace(this.nonPrintableRegEx, "")
+
+                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);
+                output.stderr = output.stderr.replace(this.nonPrintableRegEx, "")
+
+                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);
+            output.stderr = output.stderr.replace(this.nonPrintableRegEx, "")
+
+            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);
+            output.stdout = output.stdout.replace(this.nonPrintableRegEx, "")
+
+            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 b/app/assets/javascripts/editor/execution.js
index 8ac6df4d..ec1784c9 100644
--- a/app/assets/javascripts/editor/execution.js
+++ b/app/assets/javascripts/editor/execution.js
@@ -46,7 +46,6 @@ CodeOceanEditorWebsocket = {
     this.websocket.on('turtlebatch', this.handleTurtlebatchCommand.bind(this));
     this.websocket.on('render', this.renderWebsocketOutput.bind(this));
     this.websocket.on('exit', this.handleExitCommand.bind(this));
-    this.websocket.on('timeout', this.showTimeoutMessage.bind(this));
     this.websocket.on('status', this.showStatus.bind(this));
     this.websocket.on('hint', this.showHint.bind(this));
   },
diff --git a/app/assets/javascripts/editor/participantsupport.js.erb b/app/assets/javascripts/editor/participantsupport.js.erb
index af4cb501..db801aeb 100644
--- a/app/assets/javascripts/editor/participantsupport.js.erb
+++ b/app/assets/javascripts/editor/participantsupport.js.erb
@@ -4,9 +4,9 @@ CodeOceanEditorFlowr = {
     '' +
+        body.append('' +
           '<%= I18n.t('exercises.implement.flowr.go_to_question') %>');
         body.find('.btn').on('click', CodeOceanEditor.createEventHandler('editor_flowr_click_question', questionUrl));
 
@@ -112,7 +112,7 @@ CodeOceanEditorCodePilot = {
   QaApiOutputBuffer: {'stdout': '', 'stderr': ''},
 
   initializeCodePilot: function () {
-    if ($('#questions-column').isPresent() && (typeof QaApi != 'undefined') && QaApi.isBrowserSupported()) {
+    if ($('#questions-column').isPresent() && (typeof QaApi != 'undefined')) {
       $('#editor-column').addClass('col-md-10').removeClass('col-md-12');
       $('#questions-column').addClass('col-md-2');
 
@@ -161,7 +161,7 @@ CodeOceanEditorRequestForComments = {
 
     this.createSubmission($('#requestComments'), null, createRequestForComments.bind(this));
 
-    $('#comment-modal').modal('hide');
+    bootstrap.Modal.getInstance($('#comment-modal')).hide();
     $('#question').val('');
     // we disabled the button to prevent that the user spams RFCs, but decided against this now.
     //var button = $('#requestComments');
diff --git a/app/assets/javascripts/editor/turtle.js b/app/assets/javascripts/editor/turtle.js
index e6463ee4..d388f375 100644
--- a/app/assets/javascripts/editor/turtle.js
+++ b/app/assets/javascripts/editor/turtle.js
@@ -37,15 +37,16 @@ CodeOceanEditorTurtle = {
   },
 
   showCanvas: function () {
-    if ($('#turtlediv').isPresent() && this.turtlecanvas.hasClass('d-none')) {
-      this.turtlecanvas.removeClass('d-none');
+    const turtlediv = $('#turtlediv');
+    if (turtlediv.isPresent() && turtlediv.hasClass('d-none')) {
+      turtlediv.removeClass('d-none');
     }
   },
 
   hideCanvas: function () {
-    const turtlecanvas = $('#turtlecanvas');
-    if ($('#turtlediv').isPresent() && !turtlecanvas.hasClass('d-none')) {
-      turtlecanvas.addClass('d-none');
+    const turtlediv = $('#turtlediv');
+    if (turtlediv.isPresent() && !turtlediv.hasClass('d-none')) {
+      turtlediv.addClass('d-none');
     }
   }
 
diff --git a/app/assets/javascripts/exercise_collections.js.erb b/app/assets/javascripts/exercise_collections.js.erb
index c5930fb5..d0aad22e 100644
--- a/app/assets/javascripts/exercise_collections.js.erb
+++ b/app/assets/javascripts/exercise_collections.js.erb
@@ -172,7 +172,7 @@ $(document).on('turbolinks:load', function() {
         if (collectionExercises.indexOf(exercise.id) === -1) {
           // only add exercises that are not already contained in the collection
           var template = '' +
-            '' +
+            '' +
             '' + exercise.title + '' +
             '<%= I18n.t('shared.show') %>' +
             '<%= I18n.t('shared.destroy') %>';
@@ -187,7 +187,7 @@ $(document).on('turbolinks:load', function() {
         for (var i = 0; i < selectedExercises.length; i++) {
           addExercise(selectedExercises[i].value, selectedExercises[i].label);
         }
-        $('#add-exercise-modal').modal('hide')
+        bootstrap.Modal.getInstance($('#add-exercise-modal')).hide();
         updateExerciseList();
         addExercisesForm.find('select').val('').trigger("chosen:updated");
       });
diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb
index e648d2fe..a91586dd 100644
--- a/app/assets/javascripts/exercises.js.erb
+++ b/app/assets/javascripts/exercises.js.erb
@@ -123,7 +123,7 @@ $(document).on('turbolinks:load', function () {
     var buildCheckboxes = function () {
         $('tbody tr').each(function (index, element) {
             var td = $('td.public', element);
-            var checkbox = $('', {
+            var checkbox = $('', {
                 checked: td.data('value'),
                 type: 'checkbox'
             });
@@ -225,9 +225,9 @@ $(document).on('turbolinks:load', function () {
             const tip = {id: id, title: title}
             const template =
                 '
' + - '' + tip.title + - '' + - '' + + '' + tip.title + + '' + + '' + '
' + '
'; const tipList = $('#tip-list').append(template); @@ -243,7 +243,7 @@ $(document).on('turbolinks:load', function () { for (let i = 0; i < selectedTips.length; i++) { addTip(selectedTips[i].value, selectedTips[i].label); } - $('#add-tips-modal').modal('hide') + bootstrap.Modal.getInstance($('#add-tips-modal')).hide(); updateTipsJSON(); chosenInputTips.val('').trigger("chosen:updated"); }); @@ -257,7 +257,7 @@ $(document).on('turbolinks:load', function () { var highlightCode = function () { $('pre code').each(function (index, element) { - hljs.highlightBlock(element); + hljs.highlightElement(element); }); }; @@ -328,10 +328,7 @@ $(document).on('turbolinks:load', function () { var observeExportButtons = function () { $('.export-start').on('click', function (e) { e.preventDefault(); - $('#export-modal').modal({ - height: 250 - }); - $('#export-modal').modal('show'); + new bootstrap.Modal($('#export-modal')).show(); exportExerciseStart($(this).data().exerciseId); }); $('body').on('click', '.export-retry-button', function () { @@ -382,7 +379,7 @@ $(document).on('turbolinks:load', function () { if (response.status == 'success') { $messageDiv.addClass('export-success'); setTimeout((function () { - $('#export-modal').modal('hide'); + bootstrap.Modal.getInstance($('#export-modal')).hide(); $messageDiv.html('').removeClass('export-success'); }), 3000); } else { @@ -396,7 +393,7 @@ $(document).on('turbolinks:load', function () { }; var overrideTextareaTabBehavior = function () { - $('.form-group textarea[name$="[content]"]').on('keydown', function (event) { + $('.mb-3 textarea[name$="[content]"]').on('keydown', function (event) { if (event.which === TAB_KEY_CODE) { event.preventDefault(); insertTabAtCursor($(this)); diff --git a/app/assets/javascripts/forms.js b/app/assets/javascripts/forms.js index 7e469455..2be40eba 100644 --- a/app/assets/javascripts/forms.js +++ b/app/assets/javascripts/forms.js @@ -9,7 +9,7 @@ $(document).on('turbolinks:load', function() { event.preventDefault(); if (!$(this).hasClass('disabled')) { - var parent = $(this).parents('.form-group'); + var parent = $(this).parents('.mb-3'); var original_input = parent.find('.original-input'); var alternative_input = parent.find('.alternative-input'); diff --git a/app/assets/javascripts/modernizr-custom.js b/app/assets/javascripts/modernizr-custom.js deleted file mode 100644 index 4a047052..00000000 --- a/app/assets/javascripts/modernizr-custom.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! modernizr 3.6.0 (Custom Build) | MIT * - * https://modernizr.com/download/?-eventsource-urlparser-websockets-setclasses !*/ -!function(e,n,s){function o(e,n){return typeof e===n}function t(){var e,n,s,t,a,c,f;for(var l in r)if(r.hasOwnProperty(l)){if(e=[],n=r[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(s=0;s //