merge master
This commit is contained in:
@ -1 +0,0 @@
|
||||
defaults
|
38
.github/workflows/ci.yml
vendored
38
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -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
|
||||
|
@ -19,7 +19,7 @@ Metrics/ClassLength:
|
||||
Max: 600
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Max: 220
|
||||
Max: 225
|
||||
|
||||
# It's a very complicated application...
|
||||
#
|
||||
|
19
Gemfile
19
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'
|
||||
|
440
Gemfile.lock
440
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
|
||||
|
@ -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
|
||||
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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 === '/') {
|
||||
|
@ -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()) {
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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 = $('<pre class="mt-2">').attr('id', 'output-' + index);
|
||||
var element = $('<pre class="mb-2">').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 <IMG/> 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'), "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>");
|
||||
// augmented_text = augmented_text.replace(new RegExp(matches[0], 'g'), "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>");
|
||||
augmented_text = augmented_text.replace(new RegExp(_.unescape(matches[0]), 'g'), "<a href='#' data-file='" + matches[1] + "' data-line='" + matches[2] + "'>" + matches[0] + "</a>");
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
|
@ -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("<img")) {
|
||||
output.stdout = _.escape(output.stdout);
|
||||
}
|
||||
|
||||
var element = this.findOrCreateOutputElement(index);
|
||||
// Switch all four lines below to enable the output of images and render <IMG/> tags
|
||||
// Switch all four lines below to enable the output of images and render <IMG/> 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'));
|
||||
|
@ -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));
|
||||
},
|
||||
|
@ -4,9 +4,9 @@ CodeOceanEditorFlowr = {
|
||||
'<div class="card mb-2">' +
|
||||
'<div id="{{headingId}}" role="tab" class="card-header">' +
|
||||
'<div class="card-title mb-0">' +
|
||||
'<a class="collapsed" data-toggle="collapse" data-parent="#flowrHint" href="#{{collapseId}}" aria-expanded="false" aria-controls="{{collapseId}}">' +
|
||||
'<a class="collapsed" data-bs-toggle="collapse" data-bs-parent="#flowrHint" href="#{{collapseId}}" aria-expanded="false" aria-controls="{{collapseId}}">' +
|
||||
'<div class="clearfix" role="button">' +
|
||||
'<i class="fa" aria-hidden="true"></i>' +
|
||||
'<i class="fa-solid" aria-hidden="true"></i>' +
|
||||
'<span>' +
|
||||
'</span>' +
|
||||
'</div>' +
|
||||
@ -14,7 +14,7 @@ CodeOceanEditorFlowr = {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div id="{{collapseId}}" role="tabpanel" aria-labelledby="{{headingId}}" class="card card-collapse collapse">' +
|
||||
'<div class="card-body"></div>' +
|
||||
'<div class="card-body d-grid gap-2"></div>' +
|
||||
'</div>' +
|
||||
'</div>',
|
||||
|
||||
@ -93,7 +93,7 @@ CodeOceanEditorFlowr = {
|
||||
|
||||
var body = resultTile.find('.card-body');
|
||||
body.html(result.body);
|
||||
body.append('<a target="_blank" href="' + questionUrl + '" class="btn btn-primary btn-block">' +
|
||||
body.append('<a target="_blank" href="' + questionUrl + '" class="btn btn-primary">' +
|
||||
'<%= I18n.t('exercises.implement.flowr.go_to_question') %></a>');
|
||||
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');
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 = '<tr data-id="' + exercise.id + '">' +
|
||||
'<td><span class="fa fa-bars"></span></td>' +
|
||||
'<td><span class="fa-solid fa-bars"></span></td>' +
|
||||
'<td>' + exercise.title + '</td>' +
|
||||
'<td><a href="/exercises/' + exercise.id + '"><%= I18n.t('shared.show') %></td>' +
|
||||
'<td><a class="remove-exercise" href="#"><%= I18n.t('shared.destroy') %></td></tr>';
|
||||
@ -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");
|
||||
});
|
||||
|
@ -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 = $('<input>', {
|
||||
var checkbox = $('<input class="form-check-input">', {
|
||||
checked: td.data('value'),
|
||||
type: 'checkbox'
|
||||
});
|
||||
@ -225,9 +225,9 @@ $(document).on('turbolinks:load', function () {
|
||||
const tip = {id: id, title: title}
|
||||
const template =
|
||||
'<div class="list-group-item d-block" data-tip-id=' + tip.id + ' data-id="">' +
|
||||
'<span class="fa fa-bars mr-3"></span>' + tip.title +
|
||||
'<a class="fa fa-eye ml-2" href="/tips/' + tip.id + '" target="_blank"></a>' +
|
||||
'<a class="fa fa-times ml-2 remove-tip" href="#""></a>' +
|
||||
'<span class="fa-solid fa-bars me-3"></span>' + tip.title +
|
||||
'<a class="fa-regular fa-eye ms-2" href="/tips/' + tip.id + '" target="_blank"></a>' +
|
||||
'<a class="fa-solid fa-xmark ms-2 remove-tip" href="#""></a>' +
|
||||
'<div class="list-group nested-sortable-list"></div>' +
|
||||
'</div>';
|
||||
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));
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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<n.options.aliases.length;s++)e.push(n.options.aliases[s].toLowerCase());for(t=o(n.fn,"function")?n.fn():n.fn,a=0;a<e.length;a++)c=e[a],f=c.split("."),1===f.length?Modernizr[f[0]]=t:(!Modernizr[f[0]]||Modernizr[f[0]]instanceof Boolean||(Modernizr[f[0]]=new Boolean(Modernizr[f[0]])),Modernizr[f[0]][f[1]]=t),i.push((t?"":"no-")+f.join("-"))}}function a(e){var n=u.className,s=Modernizr._config.classPrefix||"";if(d&&(n=n.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+s+"no-js(\\s|$)");n=n.replace(o,"$1"+s+"js$2")}Modernizr._config.enableClasses&&(n+=" "+s+e.join(" "+s),d?u.className.baseVal=n:u.className=n)}var i=[],r=[],c={_version:"3.6.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var s=this;setTimeout(function(){n(s[e])},0)},addTest:function(e,n,s){r.push({name:e,fn:n,options:s})},addAsyncTest:function(e){r.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=c,Modernizr=new Modernizr;var f=!1;try{f="WebSocket"in e&&2===e.WebSocket.CLOSING}catch(l){}Modernizr.addTest("websockets",f),Modernizr.addTest("urlparser",function(){var e;try{return e=new URL("http://modernizr.com/"),"http://modernizr.com/"===e.href}catch(n){return!1}});var u=n.documentElement,d="svg"===u.nodeName.toLowerCase();Modernizr.addTest("eventsource","EventSource"in e),t(),a(i),delete c.addTest,delete c.addAsyncTest;for(var p=0;p<Modernizr._q.length;p++)Modernizr._q[p]();e.Modernizr=Modernizr}(window,document);
|
@ -29,12 +29,12 @@
|
||||
// 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 linkInputPlaceholder = "https://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 imageInputPlaceholder = "https://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";
|
||||
@ -193,7 +193,7 @@
|
||||
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
|
||||
// chrome bug ... documented at: https://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985
|
||||
if (navigator.userAgent.match(/Chrome/)) {
|
||||
"X".match(/()./);
|
||||
}
|
||||
@ -1018,7 +1018,7 @@
|
||||
text = 'http://' + text;
|
||||
}
|
||||
|
||||
$(dialog).modal('hide');
|
||||
bootstrap.Modal.getInstance($(dialog)).hide();
|
||||
|
||||
callback(text);
|
||||
return false;
|
||||
@ -1032,7 +1032,7 @@
|
||||
// <div class="modal-dialog">
|
||||
// <div class="modal-content">
|
||||
// <div class="modal-header">
|
||||
// <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
// <button type="button" class="close" data-bs-dismiss="modal" aria-hidden="true">×</button>
|
||||
// <h3 class="modal-title">Modal title</h3>
|
||||
// </div>
|
||||
// <div class="modal-body">
|
||||
@ -1062,7 +1062,7 @@
|
||||
// The header.
|
||||
var header = doc.createElement("div");
|
||||
header.className = "modal-header";
|
||||
header.innerHTML = '<h3 class="modal-title">'+title+'</h3> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>';
|
||||
header.innerHTML = '<h3 class="modal-title">'+title+'</h3> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-hidden="true"></button>';
|
||||
dialogContent.appendChild(header);
|
||||
|
||||
// The body.
|
||||
@ -1082,7 +1082,7 @@
|
||||
|
||||
// The input text box
|
||||
var formGroup = doc.createElement("div");
|
||||
formGroup.className = "form-group";
|
||||
formGroup.className = "mb-3";
|
||||
form.appendChild(formGroup);
|
||||
|
||||
var label = doc.createElement("label");
|
||||
@ -1144,15 +1144,15 @@
|
||||
range.select();
|
||||
}
|
||||
|
||||
$(dialog).on('shown', function () {
|
||||
$(dialog).on('shown.bs.modal', function () {
|
||||
input.focus();
|
||||
});
|
||||
|
||||
$(dialog).on('hidden', function () {
|
||||
$(dialog).on('hidden.bs.modal', function () {
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
});
|
||||
|
||||
$(dialog).modal()
|
||||
new bootstrap.Modal($(dialog)).show();
|
||||
|
||||
}, 0);
|
||||
};
|
||||
@ -1360,8 +1360,8 @@
|
||||
button.appendChild(buttonImage);
|
||||
button.id = id + postfix;
|
||||
button.title = title;
|
||||
button.setAttribute("data-toggle", "tooltip");
|
||||
button.setAttribute("data-placement", "top");
|
||||
button.setAttribute("data-bs-toggle", "tooltip");
|
||||
button.setAttribute("data-bs-placement", "top");
|
||||
if (textOp)
|
||||
button.textOp = textOp;
|
||||
setupButton(button, true);
|
||||
@ -1381,51 +1381,51 @@
|
||||
};
|
||||
|
||||
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);
|
||||
buttons.bold = makeButton("wmd-bold-button", "<%= I18n.t('components.markdown_editor.bold.button_title', default: 'Bold (Ctrl+B)') %>", "m-1 fa-solid 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-solid 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) {
|
||||
buttons.link = makeButton("wmd-link-button", "<%= I18n.t('components.markdown_editor.insert_link.button_title', default: 'Link (Ctrl+L)') %>", "m-1 fa-solid 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) {
|
||||
buttons.image = makeButton("wmd-image-button", "<%= I18n.t('components.markdown_editor.insert_image.button_title', default: 'Image (Ctrl+G)') %>", "m-1 fa-regular fa-image", 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);
|
||||
buttons.quote = makeButton("wmd-quote-button", "<%= I18n.t('components.markdown_editor.blockquoute.button_title', default: 'Blockquote (Ctrl+Q)') %>", "m-1 fa-solid 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-solid 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) {
|
||||
buttons.ulist = makeButton("wmd-ulist-button", "<%= I18n.t('components.markdown_editor.bulleted_list.button_title', default: 'Bulleted List (Ctrl+U)') %>", "m-1 fa-solid 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) {
|
||||
buttons.olist = makeButton("wmd-olist-button", "<%= I18n.t('components.markdown_editor.numbered_list.button_title', default: 'Numbered List (Ctrl+O)') %>", "m-1 fa-solid 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);
|
||||
buttons.heading = makeButton("wmd-heading-button", "<%= I18n.t('components.markdown_editor.heading.button_title', default: 'Heading (Ctrl+H)') %>", "m-1 fa-solid 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 = makeButton("wmd-undo-button", "<%= I18n.t('components.markdown_editor.undo.button_title', default: 'Undo (Ctrl+Z)') %>", "m-1 fa-solid fa-arrow-rotate-left", 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 = makeButton("wmd-redo-button", redoTitle, "m-1 fa-solid fa-arrow-rotate-right", null, group4);
|
||||
buttons.redo.execute = function (manager) { if (manager) manager.redo(); };
|
||||
|
||||
if (helpOptions) {
|
||||
var group5 = makeGroup(5);
|
||||
group5.className = group5.className + " ml-auto";
|
||||
group5.className = group5.className + " ms-auto";
|
||||
var helpButton = document.createElement("button");
|
||||
var helpButtonImage = document.createElement("i");
|
||||
helpButtonImage.className = "m-1 fa fa-info";
|
||||
helpButtonImage.className = "m-1 fa-solid 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.setAttribute("data-bs-toggle", "tooltip");
|
||||
helpButton.setAttribute("data-bs-placement", "top");
|
||||
helpButton.title = helpOptions.title || defaultHelpHoverTitle;
|
||||
helpButton.onclick = helpOptions.handler;
|
||||
|
||||
@ -1793,7 +1793,7 @@
|
||||
//
|
||||
// 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.
|
||||
// see e.g. https://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).
|
||||
|
@ -24,7 +24,7 @@ createPagedownEditor = function( selector, context ) {
|
||||
Markdown.Extra.init(converter);
|
||||
const help = {
|
||||
handler() {
|
||||
window.open('http://daringfireball.net/projects/markdown/syntax');
|
||||
window.open('https://daringfireball.net/projects/markdown/syntax');
|
||||
return false;
|
||||
},
|
||||
title: "<%= I18n.t('components.markdown_editor.help', default: 'Markdown Editing Help') %>"
|
||||
@ -32,7 +32,7 @@ createPagedownEditor = function( selector, context ) {
|
||||
|
||||
const editor = new Markdown.Editor(converter, attr, help);
|
||||
editor.run();
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||
return $(input).data('is_rendered', true);
|
||||
});
|
||||
};
|
||||
|
@ -29,10 +29,14 @@ $(document).on('turbolinks:load', function () {
|
||||
};
|
||||
|
||||
const handleResponse = function (response) {
|
||||
// Always print stdout and stderr
|
||||
printOutput(response);
|
||||
|
||||
// If an error occurred, print it too
|
||||
if (response.status === 'timeout') {
|
||||
printTimeout(response);
|
||||
} else {
|
||||
printOutput(response);
|
||||
} else if (response.status === 'out_of_memory') {
|
||||
printOutOfMemory(response);
|
||||
}
|
||||
};
|
||||
|
||||
@ -71,12 +75,19 @@ $(document).on('turbolinks:load', function () {
|
||||
};
|
||||
|
||||
const printTimeout = function (output) {
|
||||
const element = $.append('<p>');
|
||||
const element = $('<p>');
|
||||
element.addClass('text-danger');
|
||||
element.text($('#shell').data('message-timeout'));
|
||||
$('#output').append(element);
|
||||
};
|
||||
|
||||
const printOutOfMemory = function (output) {
|
||||
const element = $('<p>');
|
||||
element.addClass('text-danger');
|
||||
element.text($('#shell').data('message-out-of-memory'));
|
||||
$('#output').append(element);
|
||||
};
|
||||
|
||||
if ($('#shell').isPresent()) {
|
||||
const command = $('#command')
|
||||
command.focus();
|
||||
|
@ -1,498 +0,0 @@
|
||||
$(document).on('turbolinks:load', function(){
|
||||
(function vendorTableSorter(){
|
||||
/*
|
||||
SortTable
|
||||
version 2
|
||||
7th April 2007
|
||||
Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
|
||||
|
||||
Instructions:
|
||||
Download this file
|
||||
Add <script src="sorttable.js"></script> to your HTML
|
||||
Add class="sortable" to any table you'd like to make sortable
|
||||
Click on the headers to sort
|
||||
|
||||
Thanks to many, many people for contributions and suggestions.
|
||||
Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
|
||||
This basically means: do what you want with it.
|
||||
*/
|
||||
|
||||
|
||||
var stIsIE = /*@cc_on!@*/false;
|
||||
|
||||
sorttable = {
|
||||
init: function() {
|
||||
// quit if this function has already been called
|
||||
if (arguments.callee.done) return;
|
||||
// flag this function so we don't do the same thing twice
|
||||
arguments.callee.done = true;
|
||||
// kill the timer
|
||||
if (_timer) clearInterval(_timer);
|
||||
|
||||
if (!document.createElement || !document.getElementsByTagName) return;
|
||||
|
||||
sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
|
||||
|
||||
forEach(document.getElementsByTagName('table'), function(table) {
|
||||
if (table.className.search(/\bsortable\b/) != -1) {
|
||||
sorttable.makeSortable(table);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
makeSortable: function(table) {
|
||||
if (table.getElementsByTagName('thead').length == 0) {
|
||||
// table doesn't have a tHead. Since it should have, create one and
|
||||
// put the first table row in it.
|
||||
the = document.createElement('thead');
|
||||
the.appendChild(table.rows[0]);
|
||||
table.insertBefore(the,table.firstChild);
|
||||
}
|
||||
// Safari doesn't support table.tHead, sigh
|
||||
if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
|
||||
|
||||
if (table.tHead.rows.length != 1) return; // can't cope with two header rows
|
||||
|
||||
// Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
|
||||
// "total" rows, for example). This is B&R, since what you're supposed
|
||||
// to do is put them in a tfoot. So, if there are sortbottom rows,
|
||||
// for backwards compatibility, move them to tfoot (creating it if needed).
|
||||
sortbottomrows = [];
|
||||
for (var i=0; i<table.rows.length; i++) {
|
||||
if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
|
||||
sortbottomrows[sortbottomrows.length] = table.rows[i];
|
||||
}
|
||||
}
|
||||
if (sortbottomrows) {
|
||||
if (table.tFoot == null) {
|
||||
// table doesn't have a tfoot. Create one.
|
||||
tfo = document.createElement('tfoot');
|
||||
table.appendChild(tfo);
|
||||
}
|
||||
for (var i=0; i<sortbottomrows.length; i++) {
|
||||
tfo.appendChild(sortbottomrows[i]);
|
||||
}
|
||||
delete sortbottomrows;
|
||||
}
|
||||
|
||||
// work through each column and calculate its type
|
||||
headrow = table.tHead.rows[0].cells;
|
||||
for (var i=0; i<headrow.length; i++) {
|
||||
// manually override the type with a sorttable_type attribute
|
||||
if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col
|
||||
mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
|
||||
if (mtch) { override = mtch[1]; }
|
||||
if (mtch && typeof sorttable["sort_"+override] == 'function') {
|
||||
headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
|
||||
} else {
|
||||
headrow[i].sorttable_sortfunction = sorttable.guessType(table,i);
|
||||
}
|
||||
// make it clickable to sort
|
||||
headrow[i].sorttable_columnindex = i;
|
||||
headrow[i].sorttable_tbody = table.tBodies[0];
|
||||
dean_addEvent(headrow[i],"click", sorttable.innerSortFunction = function(e) {
|
||||
|
||||
if (this.className.search(/\bsorttable_sorted\b/) != -1) {
|
||||
// if we're already sorted by this column, just
|
||||
// reverse the table, which is quicker
|
||||
sorttable.reverse(this.sorttable_tbody);
|
||||
this.className = this.className.replace('sorttable_sorted',
|
||||
'sorttable_sorted_reverse');
|
||||
this.removeChild(document.getElementById('sorttable_sortfwdind'));
|
||||
sortrevind = document.createElement('span');
|
||||
sortrevind.id = "sorttable_sortrevind";
|
||||
sortrevind.innerHTML = stIsIE ? ' <font face="webdings">5</font>' : ' ▴';
|
||||
this.appendChild(sortrevind);
|
||||
return;
|
||||
}
|
||||
if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
|
||||
// if we're already sorted by this column in reverse, just
|
||||
// re-reverse the table, which is quicker
|
||||
sorttable.reverse(this.sorttable_tbody);
|
||||
this.className = this.className.replace('sorttable_sorted_reverse',
|
||||
'sorttable_sorted');
|
||||
this.removeChild(document.getElementById('sorttable_sortrevind'));
|
||||
sortfwdind = document.createElement('span');
|
||||
sortfwdind.id = "sorttable_sortfwdind";
|
||||
sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾';
|
||||
this.appendChild(sortfwdind);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove sorttable_sorted classes
|
||||
theadrow = this.parentNode;
|
||||
forEach(theadrow.childNodes, function(cell) {
|
||||
if (cell.nodeType == 1) { // an element
|
||||
cell.className = cell.className.replace('sorttable_sorted_reverse','');
|
||||
cell.className = cell.className.replace('sorttable_sorted','');
|
||||
}
|
||||
});
|
||||
sortfwdind = document.getElementById('sorttable_sortfwdind');
|
||||
if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); }
|
||||
sortrevind = document.getElementById('sorttable_sortrevind');
|
||||
if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); }
|
||||
|
||||
this.className += ' sorttable_sorted';
|
||||
sortfwdind = document.createElement('span');
|
||||
sortfwdind.id = "sorttable_sortfwdind";
|
||||
sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾';
|
||||
this.appendChild(sortfwdind);
|
||||
|
||||
// build an array to sort. This is a Schwartzian transform thing,
|
||||
// i.e., we "decorate" each row with the actual sort key,
|
||||
// sort based on the sort keys, and then put the rows back in order
|
||||
// which is a lot faster because you only do getInnerText once per row
|
||||
row_array = [];
|
||||
col = this.sorttable_columnindex;
|
||||
rows = this.sorttable_tbody.rows;
|
||||
for (var j=0; j<rows.length; j++) {
|
||||
row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]];
|
||||
}
|
||||
/* If you want a stable sort, uncomment the following line */
|
||||
//sorttable.shaker_sort(row_array, this.sorttable_sortfunction);
|
||||
/* and comment out this one */
|
||||
row_array.sort(this.sorttable_sortfunction);
|
||||
|
||||
tb = this.sorttable_tbody;
|
||||
for (var j=0; j<row_array.length; j++) {
|
||||
tb.appendChild(row_array[j][1]);
|
||||
}
|
||||
|
||||
delete row_array;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
guessType: function(table, column) {
|
||||
// guess the type of a column based on its first non-blank row
|
||||
sortfn = sorttable.sort_alpha;
|
||||
for (var i=0; i<table.tBodies[0].rows.length; i++) {
|
||||
text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]);
|
||||
if (text != '') {
|
||||
if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) {
|
||||
return sorttable.sort_numeric;
|
||||
}
|
||||
// check for a date: dd/mm/yyyy or dd/mm/yy
|
||||
// can have / or . or - as separator
|
||||
// can be mm/dd as well
|
||||
possdate = text.match(sorttable.DATE_RE)
|
||||
if (possdate) {
|
||||
// looks like a date
|
||||
first = parseInt(possdate[1]);
|
||||
second = parseInt(possdate[2]);
|
||||
if (first > 12) {
|
||||
// definitely dd/mm
|
||||
return sorttable.sort_ddmm;
|
||||
} else if (second > 12) {
|
||||
return sorttable.sort_mmdd;
|
||||
} else {
|
||||
// looks like a date, but we can't tell which, so assume
|
||||
// that it's dd/mm (English imperialism!) and keep looking
|
||||
sortfn = sorttable.sort_ddmm;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortfn;
|
||||
},
|
||||
|
||||
getInnerText: function(node) {
|
||||
// gets the text we want to use for sorting for a cell.
|
||||
// strips leading and trailing whitespace.
|
||||
// this is *not* a generic getInnerText function; it's special to sorttable.
|
||||
// for example, you can override the cell text with a customkey attribute.
|
||||
// it also gets .value for <input> fields.
|
||||
|
||||
if (!node) return "";
|
||||
|
||||
hasInputs = (typeof node.getElementsByTagName == 'function') &&
|
||||
node.getElementsByTagName('input').length;
|
||||
|
||||
if (node.getAttribute("sorttable_customkey") != null) {
|
||||
return node.getAttribute("sorttable_customkey");
|
||||
}
|
||||
else if (typeof node.textContent != 'undefined' && !hasInputs) {
|
||||
return node.textContent.replace(/^\s+|\s+$/g, '');
|
||||
}
|
||||
else if (typeof node.innerText != 'undefined' && !hasInputs) {
|
||||
return node.innerText.replace(/^\s+|\s+$/g, '');
|
||||
}
|
||||
else if (typeof node.text != 'undefined' && !hasInputs) {
|
||||
return node.text.replace(/^\s+|\s+$/g, '');
|
||||
}
|
||||
else {
|
||||
switch (node.nodeType) {
|
||||
case 3:
|
||||
if (node.nodeName.toLowerCase() == 'input') {
|
||||
return node.value.replace(/^\s+|\s+$/g, '');
|
||||
}
|
||||
case 4:
|
||||
return node.nodeValue.replace(/^\s+|\s+$/g, '');
|
||||
break;
|
||||
case 1:
|
||||
case 11:
|
||||
var innerText = '';
|
||||
for (var i = 0; i < node.childNodes.length; i++) {
|
||||
innerText += sorttable.getInnerText(node.childNodes[i]);
|
||||
}
|
||||
return innerText.replace(/^\s+|\s+$/g, '');
|
||||
break;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reverse: function(tbody) {
|
||||
// reverse the rows in a tbody
|
||||
newrows = [];
|
||||
for (var i=0; i<tbody.rows.length; i++) {
|
||||
newrows[newrows.length] = tbody.rows[i];
|
||||
}
|
||||
for (var i=newrows.length-1; i>=0; i--) {
|
||||
tbody.appendChild(newrows[i]);
|
||||
}
|
||||
delete newrows;
|
||||
},
|
||||
|
||||
/* sort functions
|
||||
each sort function takes two parameters, a and b
|
||||
you are comparing a[0] and b[0] */
|
||||
sort_numeric: function(a,b) {
|
||||
aa = parseFloat(a[0].replace(/[^0-9.-]/g,''));
|
||||
if (isNaN(aa)) aa = 0;
|
||||
bb = parseFloat(b[0].replace(/[^0-9.-]/g,''));
|
||||
if (isNaN(bb)) bb = 0;
|
||||
return aa-bb;
|
||||
},
|
||||
sort_alpha: function(a,b) {
|
||||
if (a[0]==b[0]) return 0;
|
||||
if (a[0]<b[0]) return -1;
|
||||
return 1;
|
||||
},
|
||||
sort_ddmm: function(a,b) {
|
||||
mtch = a[0].match(sorttable.DATE_RE);
|
||||
y = mtch[3]; m = mtch[2]; d = mtch[1];
|
||||
if (m.length == 1) m = '0'+m;
|
||||
if (d.length == 1) d = '0'+d;
|
||||
dt1 = y+m+d;
|
||||
mtch = b[0].match(sorttable.DATE_RE);
|
||||
y = mtch[3]; m = mtch[2]; d = mtch[1];
|
||||
if (m.length == 1) m = '0'+m;
|
||||
if (d.length == 1) d = '0'+d;
|
||||
dt2 = y+m+d;
|
||||
if (dt1==dt2) return 0;
|
||||
if (dt1<dt2) return -1;
|
||||
return 1;
|
||||
},
|
||||
sort_mmdd: function(a,b) {
|
||||
mtch = a[0].match(sorttable.DATE_RE);
|
||||
y = mtch[3]; d = mtch[2]; m = mtch[1];
|
||||
if (m.length == 1) m = '0'+m;
|
||||
if (d.length == 1) d = '0'+d;
|
||||
dt1 = y+m+d;
|
||||
mtch = b[0].match(sorttable.DATE_RE);
|
||||
y = mtch[3]; d = mtch[2]; m = mtch[1];
|
||||
if (m.length == 1) m = '0'+m;
|
||||
if (d.length == 1) d = '0'+d;
|
||||
dt2 = y+m+d;
|
||||
if (dt1==dt2) return 0;
|
||||
if (dt1<dt2) return -1;
|
||||
return 1;
|
||||
},
|
||||
|
||||
shaker_sort: function(list, comp_func) {
|
||||
// A stable sort function to allow multi-level sorting of data
|
||||
// see: http://en.wikipedia.org/wiki/Cocktail_sort
|
||||
// thanks to Joseph Nahmias
|
||||
var b = 0;
|
||||
var t = list.length - 1;
|
||||
var swap = true;
|
||||
|
||||
while(swap) {
|
||||
swap = false;
|
||||
for(var i = b; i < t; ++i) {
|
||||
if ( comp_func(list[i], list[i+1]) > 0 ) {
|
||||
var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
|
||||
swap = true;
|
||||
}
|
||||
} // for
|
||||
t--;
|
||||
|
||||
if (!swap) break;
|
||||
|
||||
for(var i = t; i > b; --i) {
|
||||
if ( comp_func(list[i], list[i-1]) < 0 ) {
|
||||
var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
|
||||
swap = true;
|
||||
}
|
||||
} // for
|
||||
b++;
|
||||
|
||||
} // while(swap)
|
||||
}
|
||||
}
|
||||
|
||||
/* ******************************************************************
|
||||
Supporting functions: bundled here to avoid depending on a library
|
||||
****************************************************************** */
|
||||
|
||||
// Dean Edwards/Matthias Miller/John Resig
|
||||
|
||||
/* for Mozilla/Opera9 */
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener("DOMContentLoaded", sorttable.init, false);
|
||||
}
|
||||
|
||||
/* for Internet Explorer */
|
||||
/*@cc_on @*/
|
||||
/*@if (@_win32)
|
||||
document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>");
|
||||
var script = document.getElementById("__ie_onload");
|
||||
script.onreadystatechange = function() {
|
||||
if (this.readyState == "complete") {
|
||||
sorttable.init(); // call the onload handler
|
||||
}
|
||||
};
|
||||
/*@end @*/
|
||||
|
||||
/* for Safari */
|
||||
if (/WebKit/i.test(navigator.userAgent)) { // sniff
|
||||
var _timer = setInterval(function() {
|
||||
if (/loaded|complete/.test(document.readyState)) {
|
||||
sorttable.init(); // call the onload handler
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
/* for other browsers */
|
||||
window.onload = sorttable.init;
|
||||
|
||||
// written by Dean Edwards, 2005
|
||||
// with input from Tino Zijdel, Matthias Miller, Diego Perini
|
||||
|
||||
// http://dean.edwards.name/weblog/2005/10/add-event/
|
||||
|
||||
function dean_addEvent(element, type, handler) {
|
||||
if (element.addEventListener) {
|
||||
element.addEventListener(type, handler, false);
|
||||
} else {
|
||||
// assign each event handler a unique ID
|
||||
if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
|
||||
// create a hash table of event types for the element
|
||||
if (!element.events) element.events = {};
|
||||
// create a hash table of event handlers for each element/event pair
|
||||
var handlers = element.events[type];
|
||||
if (!handlers) {
|
||||
handlers = element.events[type] = {};
|
||||
// store the existing event handler (if there is one)
|
||||
if (element["on" + type]) {
|
||||
handlers[0] = element["on" + type];
|
||||
}
|
||||
}
|
||||
// store the event handler in the hash table
|
||||
handlers[handler.$$guid] = handler;
|
||||
// assign a global event handler to do all the work
|
||||
element["on" + type] = handleEvent;
|
||||
}
|
||||
};
|
||||
// a counter used to create unique IDs
|
||||
dean_addEvent.guid = 1;
|
||||
|
||||
function removeEvent(element, type, handler) {
|
||||
if (element.removeEventListener) {
|
||||
element.removeEventListener(type, handler, false);
|
||||
} else {
|
||||
// delete the event handler from the hash table
|
||||
if (element.events && element.events[type]) {
|
||||
delete element.events[type][handler.$$guid];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function handleEvent(event) {
|
||||
var returnValue = true;
|
||||
// grab the event object (IE uses a global event object)
|
||||
event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
|
||||
// get a reference to the hash table of event handlers
|
||||
var handlers = this.events[event.type];
|
||||
// execute each event handler
|
||||
for (var i in handlers) {
|
||||
this.$$handleEvent = handlers[i];
|
||||
if (this.$$handleEvent(event) === false) {
|
||||
returnValue = false;
|
||||
}
|
||||
}
|
||||
return returnValue;
|
||||
};
|
||||
|
||||
function fixEvent(event) {
|
||||
// add W3C standard event methods
|
||||
event.preventDefault = fixEvent.preventDefault;
|
||||
event.stopPropagation = fixEvent.stopPropagation;
|
||||
return event;
|
||||
};
|
||||
fixEvent.preventDefault = function() {
|
||||
this.returnValue = false;
|
||||
};
|
||||
fixEvent.stopPropagation = function() {
|
||||
this.cancelBubble = true;
|
||||
}
|
||||
|
||||
// Dean's forEach: http://dean.edwards.name/base/forEach.js
|
||||
/*
|
||||
forEach, version 1.0
|
||||
Copyright 2006, Dean Edwards
|
||||
License: http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
|
||||
// array-like enumeration
|
||||
if (!Array.forEach) { // mozilla already supports this
|
||||
Array.forEach = function(array, block, context) {
|
||||
for (var i = 0; i < array.length; i++) {
|
||||
block.call(context, array[i], i, array);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// generic enumeration
|
||||
Function.prototype.forEach = function(object, block, context) {
|
||||
for (var key in object) {
|
||||
if (typeof this.prototype[key] == "undefined") {
|
||||
block.call(context, object[key], key, object);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// character enumeration
|
||||
String.forEach = function(string, block, context) {
|
||||
Array.forEach(string.split(""), function(chr, index) {
|
||||
block.call(context, chr, index, string);
|
||||
});
|
||||
};
|
||||
|
||||
// globally resolve forEach enumeration
|
||||
var forEach = function(object, block, context) {
|
||||
if (object) {
|
||||
var resolve = Object; // default
|
||||
if (object instanceof Function) {
|
||||
// functions have a "length" property
|
||||
resolve = Function;
|
||||
} else if (object.forEach instanceof Function) {
|
||||
// the object implements a custom forEach method so use that
|
||||
object.forEach(block, context);
|
||||
return;
|
||||
} else if (typeof object == "string") {
|
||||
// the object is a string
|
||||
resolve = String;
|
||||
} else if (typeof object.length == "number") {
|
||||
// the object is array-like
|
||||
resolve = Array;
|
||||
}
|
||||
resolve.forEach(object, block, context);
|
||||
}
|
||||
};
|
||||
}());
|
||||
});
|
@ -10,6 +10,8 @@ $(document).on('turbolinks:load', function() {
|
||||
var fileTypeById = {};
|
||||
|
||||
var showActiveFile = function() {
|
||||
$('tr.active').removeClass('active');
|
||||
$('tr#submission-' + currentSubmission).addClass('active');
|
||||
var session = editor.getSession();
|
||||
var fileType = fileTypeById[active_file.file_type_id];
|
||||
session.setMode(fileType.editor_mode);
|
||||
@ -81,6 +83,7 @@ $(document).on('turbolinks:load', function() {
|
||||
|
||||
$('tr[data-id]>.clickable').each(function(index, element) {
|
||||
element = $(element);
|
||||
element.parent().attr('id', 'submission-' + index);
|
||||
element.click(function() {
|
||||
slider.val(index);
|
||||
slider.change()
|
||||
@ -105,7 +108,7 @@ $(document).on('turbolinks:load', function() {
|
||||
stopReplay = function() {
|
||||
clearInterval(playInterval);
|
||||
playInterval = undefined;
|
||||
playButton.find('span.fa').removeClass('fa-pause').addClass('fa-play')
|
||||
playButton.find('span.fa-solid').removeClass('fa-pause').addClass('fa-play')
|
||||
};
|
||||
|
||||
playButton.on('click', function(event) {
|
||||
@ -124,7 +127,7 @@ $(document).on('turbolinks:load', function() {
|
||||
stopReplay();
|
||||
}
|
||||
}, 1000);
|
||||
playButton.find('span.fa').removeClass('fa-play').addClass('fa-pause')
|
||||
playButton.find('span.fa-solid').removeClass('fa-play').addClass('fa-pause')
|
||||
} else {
|
||||
stopReplay();
|
||||
}
|
||||
|
@ -160,7 +160,6 @@ $(document).on('turbolinks:load', function() {
|
||||
groupRanges += groupWidth;
|
||||
}
|
||||
while (groupRanges < maximum_minutes);
|
||||
console.log(maximum_minutes);
|
||||
|
||||
var clusterCount = 0,
|
||||
sum = 0,
|
||||
|
@ -12,7 +12,16 @@ h1, h2, h3, h4, h5, h6 {
|
||||
color: rgba(70, 70, 70, 1);
|
||||
}
|
||||
|
||||
i.fa, i.far, i.fas {
|
||||
a:not(.dropdown-item, .dropdown-toggle, .dropdown-link, .btn, .page-link), .btn-link {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
i.fa-solid, i.fa-regular, i.fa-solid {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
@ -35,6 +44,10 @@ span.caret {
|
||||
}
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
--bs-btn-disabled-border-color: transparent;
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin: 0;
|
||||
border: 1px solid #CCCCCC;
|
||||
@ -51,13 +64,17 @@ span.caret {
|
||||
.navbar {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-weight: 500;
|
||||
font-size: 0.85rem;
|
||||
.dropdown-item {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.attribute-row + .attribute-row {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.badge-pill {
|
||||
.rounded-pill {
|
||||
font-size: 100%;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
@ -29,11 +29,11 @@
|
||||
border-left-color: #ffffff;
|
||||
}
|
||||
|
||||
.dropdown-submenu.float-left {
|
||||
.dropdown-submenu.float-start {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.dropdown-submenu.float-left > .dropdown-menu {
|
||||
.dropdown-submenu.float-start > .dropdown-menu {
|
||||
left: -100%;
|
||||
margin-left: 10px;
|
||||
-webkit-border-radius: 6px 0 6px 6px;
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Place all the styles related to the Comments controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
// You can use Sass (SCSS) here: https://sass-lang.com/
|
||||
|
||||
.ace_gutter-cell.code-ocean_comment {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3wIUDTMn077U+gAAANhJREFUOMvV0jFOQkEURuEPfCFKaChwASQWhsIOaxYgW7Ci0c4VuBBM6GmsWIC2JsbCykJiRSD0GMVHMy+ZTPSBlfHvZu7NyZ1zh79O5Zu7Bi7RRz30TDHC7TbACB1UkSe1Kt5xg2FxuRc1jHFcMm0e+k/xgceCCmc4wtcOz17jCs0YMMDnL9ytcBgDVj8ILZO/jgHXiY9t2cdbLHGBFk4KcklqYcXLdAt3wXQ3kVlBhgM84AKvRTFL6O0gM8MTZuH8ggnmZaP18Ix7nO8qI0t+Wi/4yP2bbAA4rSby6Nz4AwAAAABJRU5ErkJggg==");
|
||||
|
@ -1,7 +1,3 @@
|
||||
button i.fa-spin {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editor {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
@ -48,10 +44,6 @@ button i.fa-spin {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
#development-environment {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#dummy {
|
||||
display: none;
|
||||
}
|
||||
@ -203,8 +195,8 @@ button i.fa-spin {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.enforce-big-top-margin {
|
||||
margin-top: 15px !important;
|
||||
.enforce-big-bottom-margin {
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
|
||||
.enforce-bottom-margin {
|
||||
|
@ -40,11 +40,11 @@ input[type='file'] {
|
||||
}
|
||||
}
|
||||
|
||||
[data-toggle="collapse"] .fa:before {
|
||||
[data-bs-toggle="collapse"] .fa-solid:before {
|
||||
content: "\f139";
|
||||
}
|
||||
|
||||
[data-toggle="collapse"].collapsed .fa:before {
|
||||
[data-bs-toggle="collapse"].collapsed .fa-solid:before {
|
||||
content: "\f13a";
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
// Place all the styles related to the FileTemplates controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
||||
// You can use Sass (SCSS) here: https://sass-lang.com/
|
||||
|
@ -23,12 +23,6 @@
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
&:not(:last-child) {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
input, select {
|
||||
min-width: 200px !important;
|
||||
}
|
||||
|
@ -58,6 +58,15 @@ div.negative-result {
|
||||
box-shadow: 0px 0px 11px 1px rgba(222,0,0,1);
|
||||
}
|
||||
|
||||
tr.active {
|
||||
filter: brightness(85%);
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
tr:not(.before_deadline,.within_grace_period,.after_late_deadline) {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
tr.highlight {
|
||||
border-top: 2px solid rgba(222,0,0,1);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ class LaExercisesChannel < ApplicationCable::Channel
|
||||
private
|
||||
|
||||
def specific_channel
|
||||
reject unless StudyGroupPolicy.new(current_user, StudyGroup.find_by(id: params[:study_group_id])).stream_la?
|
||||
reject unless StudyGroupPolicy.new(current_user, StudyGroup.find(params[:study_group_id])).stream_la?
|
||||
"la_exercises_#{params[:exercise_id]}_channel_study_group_#{params[:study_group_id]}"
|
||||
end
|
||||
end
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
include ApplicationHelper
|
||||
include Pundit
|
||||
include Pundit::Authorization
|
||||
|
||||
MEMBER_ACTIONS = %i[destroy edit show update].freeze
|
||||
|
||||
@ -15,9 +15,11 @@ class ApplicationController < ActionController::Base
|
||||
rescue_from ActionController::InvalidAuthenticityToken, with: :render_csrf_error
|
||||
|
||||
def current_user
|
||||
::NewRelic::Agent.add_custom_attributes(external_user_id: session[:external_user_id],
|
||||
session_user_id: session[:user_id])
|
||||
@current_user ||= ExternalUser.find_by(id: session[:external_user_id]) || login_from_session || login_from_other_sources || nil
|
||||
@current_user ||= ExternalUser.find_by(id: session[:external_user_id]) ||
|
||||
login_from_session ||
|
||||
login_from_other_sources ||
|
||||
login_from_authentication_token ||
|
||||
nil
|
||||
end
|
||||
|
||||
def require_user!
|
||||
@ -34,6 +36,13 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
end
|
||||
|
||||
def login_from_authentication_token
|
||||
token = AuthenticationToken.find_by(shared_secret: params[:token])
|
||||
return unless token
|
||||
|
||||
auto_login(token.user) if token.expire_at.future?
|
||||
end
|
||||
|
||||
def set_sentry_context
|
||||
return if current_user.blank?
|
||||
|
||||
@ -73,7 +82,7 @@ class ApplicationController < ActionController::Base
|
||||
private :render_error
|
||||
|
||||
def switch_locale(&action)
|
||||
session[:locale] = params[:custom_locale] || params[:locale] || session[:locale]
|
||||
session[:locale] = sanitize_locale(params[:custom_locale] || params[:locale] || session[:locale])
|
||||
locale = session[:locale] || I18n.default_locale
|
||||
Sentry.set_extras(locale: locale)
|
||||
I18n.with_locale(locale, &action)
|
||||
@ -98,4 +107,18 @@ class ApplicationController < ActionController::Base
|
||||
@embed_options
|
||||
end
|
||||
private :load_embed_options
|
||||
|
||||
# Sanitize given locale.
|
||||
#
|
||||
# Return `nil` if the locale is blank or not available.
|
||||
#
|
||||
def sanitize_locale(locale)
|
||||
return if locale.blank?
|
||||
|
||||
locale = locale.downcase.to_sym
|
||||
return unless I18n.available_locales.include?(locale)
|
||||
|
||||
locale
|
||||
end
|
||||
private :sanitize_locale
|
||||
end
|
||||
|
@ -44,7 +44,6 @@ class CodeharborLinksController < ApplicationController
|
||||
|
||||
def set_codeharbor_link
|
||||
@codeharbor_link = CodeharborLink.find(params[:id])
|
||||
@codeharbor_link.user = current_user
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -3,9 +3,6 @@
|
||||
class CommentsController < ApplicationController
|
||||
before_action :set_comment, only: %i[show update destroy]
|
||||
|
||||
# to disable authorization check: comment the line below back in
|
||||
# skip_after_action :verify_authorized
|
||||
|
||||
def authorize!
|
||||
authorize(@comment || @comments)
|
||||
end
|
||||
@ -55,7 +52,7 @@ class CommentsController < ApplicationController
|
||||
|
||||
# PATCH/PUT /comments/1.json
|
||||
def update
|
||||
if @comment.update(comment_params_without_request_id)
|
||||
if @comment.update(comment_params_for_update)
|
||||
render :show, status: :ok, location: @comment
|
||||
else
|
||||
render json: @comment.errors, status: :unprocessable_entity
|
||||
@ -77,6 +74,10 @@ class CommentsController < ApplicationController
|
||||
@comment = Comment.find(params[:id])
|
||||
end
|
||||
|
||||
def comment_params_for_update
|
||||
params.require(:comment).permit(:text)
|
||||
end
|
||||
|
||||
def comment_params_without_request_id
|
||||
comment_params.except :request_id
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ class CommunitySolutionsController < ApplicationController
|
||||
|
||||
# GET /community_solutions
|
||||
def index
|
||||
@community_solutions = CommunitySolution.all
|
||||
@community_solutions = CommunitySolution.all.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -85,7 +85,7 @@ class CommunitySolutionsController < ApplicationController
|
||||
private
|
||||
|
||||
def authorize!
|
||||
authorize(@community_solution)
|
||||
authorize(@community_solution || @community_solutions)
|
||||
end
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
|
@ -21,7 +21,7 @@ module Lti
|
||||
# exercise_id.nil? ==> the user has logged out. All session data is to be destroyed
|
||||
# exercise_id.exists? ==> the user has submitted the results of an exercise to the consumer.
|
||||
# Only the lti_parameters are deleted.
|
||||
def clear_lti_session_data(exercise_id = nil, user_id = nil)
|
||||
def clear_lti_session_data(exercise_id = nil, _user_id = nil)
|
||||
if exercise_id.nil?
|
||||
session.delete(:external_user_id)
|
||||
session.delete(:study_group_id)
|
||||
@ -29,8 +29,10 @@ module Lti
|
||||
session.delete(:lti_exercise_id)
|
||||
session.delete(:lti_parameters_id)
|
||||
end
|
||||
LtiParameter.where(external_users_id: user_id,
|
||||
exercises_id: exercise_id).destroy_all
|
||||
|
||||
# March 2022: We temporarily allow reusing the LTI credentials and don't remove them on purpose.
|
||||
# This allows users to jump between remote and web evaluation with the same behavior.
|
||||
# LtiParameter.where(external_users_id: user_id, exercises_id: exercise_id).destroy_all
|
||||
end
|
||||
|
||||
private :clear_lti_session_data
|
||||
@ -136,7 +138,6 @@ module Lti
|
||||
private :return_to_consumer
|
||||
|
||||
def send_score(submission)
|
||||
::NewRelic::Agent.add_custom_attributes({score: submission.normalized_score, session: session})
|
||||
unless (0..MAXIMUM_SCORE).cover?(submission.normalized_score)
|
||||
raise Error.new("Score #{submission.normalized_score} must be between 0 and #{MAXIMUM_SCORE}!")
|
||||
end
|
||||
|
@ -129,12 +129,7 @@ module RedirectBehavior
|
||||
lti_parameters_id: session[:lti_parameters_id]
|
||||
)
|
||||
|
||||
lti_parameter = LtiParameter.where(external_users_id: @submission.user_id,
|
||||
exercises_id: @submission.exercise_id).last
|
||||
|
||||
path = lti_return_path(submission_id: @submission.id,
|
||||
url: consumer_return_url(build_tool_provider(consumer: @submission.user.consumer,
|
||||
parameters: lti_parameter&.lti_parameters)))
|
||||
path = lti_return_path(submission_id: @submission.id)
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id)
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(path) }
|
||||
|
@ -28,7 +28,7 @@ class ConsumersController < ApplicationController
|
||||
private :consumer_params
|
||||
|
||||
def index
|
||||
@consumers = Consumer.paginate(page: params[:page])
|
||||
@consumers = Consumer.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -12,7 +12,7 @@ class ErrorTemplateAttributesController < ApplicationController
|
||||
# GET /error_template_attributes.json
|
||||
def index
|
||||
@error_template_attributes = ErrorTemplateAttribute.all.order('important DESC', :key,
|
||||
:id).paginate(page: params[:page])
|
||||
:id).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -42,7 +42,7 @@ class ErrorTemplateAttributesController < ApplicationController
|
||||
respond_to do |format|
|
||||
if @error_template_attribute.save
|
||||
format.html do
|
||||
redirect_to @error_template_attribute, notice: 'Error template attribute was successfully created.'
|
||||
redirect_to @error_template_attribute, notice: t('shared.object_created', model: @error_template_attribute.class.model_name.human)
|
||||
end
|
||||
format.json { render :show, status: :created, location: @error_template_attribute }
|
||||
else
|
||||
@ -59,7 +59,7 @@ class ErrorTemplateAttributesController < ApplicationController
|
||||
respond_to do |format|
|
||||
if @error_template_attribute.update(error_template_attribute_params)
|
||||
format.html do
|
||||
redirect_to @error_template_attribute, notice: 'Error template attribute was successfully updated.'
|
||||
redirect_to @error_template_attribute, notice: t('shared.object_updated', model: @error_template_attribute.class.model_name.human)
|
||||
end
|
||||
format.json { render :show, status: :ok, location: @error_template_attribute }
|
||||
else
|
||||
@ -76,7 +76,7 @@ class ErrorTemplateAttributesController < ApplicationController
|
||||
@error_template_attribute.destroy
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to error_template_attributes_url, notice: 'Error template attribute was successfully destroyed.'
|
||||
redirect_to error_template_attributes_url, notice: t('shared.object_destroyed', model: @error_template_attribute.class.model_name.human)
|
||||
end
|
||||
format.json { head :no_content }
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ class ErrorTemplatesController < ApplicationController
|
||||
# GET /error_templates
|
||||
# GET /error_templates.json
|
||||
def index
|
||||
@error_templates = ErrorTemplate.all.order(:execution_environment_id, :name).paginate(page: params[:page])
|
||||
@error_templates = ErrorTemplate.all.order(:execution_environment_id, :name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -40,7 +40,7 @@ class ErrorTemplatesController < ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
if @error_template.save
|
||||
format.html { redirect_to @error_template, notice: 'Error template was successfully created.' }
|
||||
format.html { redirect_to @error_template, notice: t('shared.object_created', model: @error_template.class.model_name.human) }
|
||||
format.json { render :show, status: :created, location: @error_template }
|
||||
else
|
||||
format.html { render :new }
|
||||
@ -55,7 +55,7 @@ class ErrorTemplatesController < ApplicationController
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @error_template.update(error_template_params)
|
||||
format.html { redirect_to @error_template, notice: 'Error template was successfully updated.' }
|
||||
format.html { redirect_to @error_template, notice: t('shared.object_updated', model: @error_template.class.model_name.human) }
|
||||
format.json { render :show, status: :ok, location: @error_template }
|
||||
else
|
||||
format.html { render :edit }
|
||||
@ -70,14 +70,14 @@ class ErrorTemplatesController < ApplicationController
|
||||
authorize!
|
||||
@error_template.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to error_templates_url, notice: 'Error template was successfully destroyed.' }
|
||||
format.html { redirect_to error_templates_url, notice: t('shared.object_destroyed', model: @error_template.class.model_name.human) }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def add_attribute
|
||||
authorize!
|
||||
@error_template.error_template_attributes << ErrorTemplateAttribute.find(params['error_template_attribute_id'])
|
||||
@error_template.error_template_attributes << ErrorTemplateAttribute.find(params[:error_template_attribute_id])
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @error_template }
|
||||
format.json { head :no_content }
|
||||
@ -86,7 +86,7 @@ class ErrorTemplatesController < ApplicationController
|
||||
|
||||
def remove_attribute
|
||||
authorize!
|
||||
@error_template.error_template_attributes.delete(ErrorTemplateAttribute.find(params['error_template_attribute_id']))
|
||||
@error_template.error_template_attributes.delete(ErrorTemplateAttribute.find(params[:error_template_attribute_id]))
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @error_template }
|
||||
format.json { head :no_content }
|
||||
|
@ -30,7 +30,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
def execute_command
|
||||
runner = Runner.for(current_user, @execution_environment)
|
||||
output = runner.execute_command(params[:command], raise_exception: false)
|
||||
render json: output
|
||||
render json: output.except(:messages)
|
||||
end
|
||||
|
||||
def working_time_query
|
||||
@ -44,7 +44,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
exercise_id,
|
||||
CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
|
||||
CASE WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
exercise_id,
|
||||
@ -121,7 +121,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
private :execution_environment_params
|
||||
|
||||
def index
|
||||
@execution_environments = ExecutionEnvironment.all.includes(:user).order(:name).paginate(page: params[:page])
|
||||
@execution_environments = ExecutionEnvironment.all.includes(:user).order(:name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -158,7 +158,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
|
||||
def show
|
||||
if @execution_environment.testing_framework?
|
||||
@testing_framework_adapter = Kernel.const_get(@execution_environment.testing_framework)
|
||||
@testing_framework_adapter = TestingFrameworkAdapter.descendants.find {|klass| klass.name == @execution_environment.testing_framework }
|
||||
end
|
||||
end
|
||||
|
||||
@ -172,8 +172,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
begin
|
||||
Runner.strategy_class.sync_environment(@execution_environment)
|
||||
rescue Runner::Error => e
|
||||
Rails.logger.debug { "Runner error while synchronizing execution environment with id #{@execution_environment.id}: #{e.message}" }
|
||||
Sentry.capture_exception(e)
|
||||
Rails.logger.warn { "Runner error while synchronizing execution environment with id #{@execution_environment.id}: #{e.message}" }
|
||||
redirect_to @execution_environment, alert: t('execution_environments.index.synchronize.failure', error: e.message)
|
||||
else
|
||||
redirect_to @execution_environment, notice: t('execution_environments.index.synchronize.success')
|
||||
|
@ -6,7 +6,7 @@ class ExerciseCollectionsController < ApplicationController
|
||||
before_action :set_exercise_collection, only: %i[show edit update destroy statistics]
|
||||
|
||||
def index
|
||||
@exercise_collections = ExerciseCollection.all.paginate(page: params[:page])
|
||||
@exercise_collections = ExerciseCollection.all.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -11,21 +11,21 @@ class ExercisesController < ApplicationController
|
||||
before_action :set_execution_environments, only: %i[index create edit new update]
|
||||
before_action :set_exercise_and_authorize,
|
||||
only: MEMBER_ACTIONS + %i[clone implement working_times intervention search run statistics submit reload feedback
|
||||
requests_for_comments study_group_dashboard export_external_check export_external_confirm]
|
||||
before_action :set_external_user_and_authorize, only: [:statistics]
|
||||
requests_for_comments study_group_dashboard export_external_check export_external_confirm
|
||||
external_user_statistics]
|
||||
before_action :set_external_user_and_authorize, only: [:external_user_statistics]
|
||||
before_action :set_file_types, only: %i[create edit new update]
|
||||
before_action :set_course_token, only: [:implement]
|
||||
before_action :set_available_tips, only: %i[implement show new edit]
|
||||
|
||||
skip_before_action :verify_authenticity_token,
|
||||
only: %i[import_exercise import_uuid_check export_external_confirm export_external_check]
|
||||
skip_after_action :verify_authorized, only: %i[import_exercise import_uuid_check export_external_confirm]
|
||||
skip_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check export_external_confirm],
|
||||
raise: false
|
||||
skip_before_action :verify_authenticity_token, only: %i[import_exercise import_uuid_check]
|
||||
skip_after_action :verify_authorized, only: %i[import_exercise import_uuid_check]
|
||||
skip_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check], raise: false
|
||||
|
||||
def authorize!
|
||||
authorize(@exercise || @exercises)
|
||||
end
|
||||
|
||||
private :authorize!
|
||||
|
||||
def max_intervention_count_per_day
|
||||
@ -51,7 +51,7 @@ raise: false
|
||||
exercise = @exercise.duplicate(public: false, token: nil, user: current_user)
|
||||
exercise.send(:generate_token)
|
||||
if exercise.save
|
||||
redirect_to(exercise, notice: t('shared.object_cloned', model: Exercise.model_name.human))
|
||||
redirect_to(exercise_path(exercise), notice: t('shared.object_cloned', model: Exercise.model_name.human))
|
||||
else
|
||||
flash[:danger] = t('shared.message_failure')
|
||||
redirect_to(@exercise)
|
||||
@ -67,6 +67,7 @@ raise: false
|
||||
end
|
||||
subpaths.flatten.uniq
|
||||
end
|
||||
|
||||
private :collect_paths
|
||||
|
||||
def create
|
||||
@ -103,7 +104,7 @@ raise: false
|
||||
|
||||
def feedback
|
||||
authorize!
|
||||
@feedbacks = @exercise.user_exercise_feedbacks.paginate(page: params[:page])
|
||||
@feedbacks = @exercise.user_exercise_feedbacks.paginate(page: params[:page], per_page: per_page_param)
|
||||
@submissions = @feedbacks.map do |feedback|
|
||||
feedback.exercise.final_submission(feedback.user)
|
||||
end
|
||||
@ -128,6 +129,7 @@ raise: false
|
||||
end
|
||||
|
||||
def export_external_confirm
|
||||
authorize!
|
||||
@exercise.uuid = SecureRandom.uuid if @exercise.uuid.nil?
|
||||
|
||||
error = ExerciseService::PushExternal.call(
|
||||
@ -176,7 +178,7 @@ raise: false
|
||||
ActiveRecord::Base.transaction do
|
||||
exercise = ::ProformaService::Import.call(zip: tempfile, user: user)
|
||||
exercise.save!
|
||||
return render json: {}, status: :created
|
||||
render json: {}, status: :created
|
||||
end
|
||||
rescue Proforma::ExerciseNotOwned
|
||||
render json: {}, status: :unauthorized
|
||||
@ -192,12 +194,14 @@ raise: false
|
||||
api_key = authorization_header&.split(' ')&.second
|
||||
user_by_codeharbor_token(api_key)
|
||||
end
|
||||
|
||||
private :user_from_api_key
|
||||
|
||||
def user_by_codeharbor_token(api_key)
|
||||
link = CodeharborLink.find_by(api_key: api_key)
|
||||
link&.user
|
||||
end
|
||||
|
||||
private :user_by_codeharbor_token
|
||||
|
||||
def exercise_params
|
||||
@ -225,6 +229,7 @@ raise: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private :exercise_params
|
||||
|
||||
def handle_file_uploads
|
||||
@ -241,6 +246,7 @@ raise: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private :handle_file_uploads
|
||||
|
||||
def handle_exercise_tips
|
||||
@ -258,6 +264,7 @@ raise: false
|
||||
redirect_to(edit_exercise_path(@exercise))
|
||||
end
|
||||
end
|
||||
|
||||
private :handle_exercise_tips
|
||||
|
||||
def update_exercise_tips(exercise_tips, parent_exercise_tip_id, rank)
|
||||
@ -283,6 +290,7 @@ raise: false
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
private :update_exercise_tips
|
||||
|
||||
def implement
|
||||
@ -348,6 +356,7 @@ raise: false
|
||||
@course_token = '702cbd2a-c84c-4b37-923a-692d7d1532d0'
|
||||
end
|
||||
end
|
||||
|
||||
private :set_course_token
|
||||
|
||||
def set_available_tips
|
||||
@ -374,6 +383,7 @@ raise: false
|
||||
# Return an array with top-level tips
|
||||
@tips = nested_tips.values.select {|tip| tip.parent_exercise_tip_id.nil? }
|
||||
end
|
||||
|
||||
private :set_available_tips
|
||||
|
||||
def working_times
|
||||
@ -401,7 +411,8 @@ working_time_accumulated: working_time_accumulated})
|
||||
search_text = params[:search_text]
|
||||
search = Search.new(user: current_user, exercise: @exercise, search: search_text)
|
||||
|
||||
begin search.save
|
||||
begin
|
||||
search.save
|
||||
render(json: {success: 'true'})
|
||||
rescue StandardError
|
||||
render(json: {success: 'false', error: "could not save search: #{$ERROR_INFO}"})
|
||||
@ -410,7 +421,7 @@ working_time_accumulated: working_time_accumulated})
|
||||
|
||||
def index
|
||||
@search = policy_scope(Exercise).ransack(params[:q])
|
||||
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page])
|
||||
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -424,12 +435,14 @@ working_time_accumulated: working_time_accumulated})
|
||||
def set_execution_environments
|
||||
@execution_environments = ExecutionEnvironment.all.order(:name)
|
||||
end
|
||||
|
||||
private :set_execution_environments
|
||||
|
||||
def set_exercise_and_authorize
|
||||
@exercise = Exercise.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
|
||||
private :set_exercise_and_authorize
|
||||
|
||||
def set_external_user_and_authorize
|
||||
@ -438,16 +451,17 @@ working_time_accumulated: working_time_accumulated})
|
||||
authorize!
|
||||
end
|
||||
end
|
||||
|
||||
private :set_external_user_and_authorize
|
||||
|
||||
def set_file_types
|
||||
@file_types = FileType.all.order(:name)
|
||||
end
|
||||
|
||||
private :set_file_types
|
||||
|
||||
def collect_set_and_unset_exercise_tags
|
||||
@search = policy_scope(Tag).ransack(params[:q])
|
||||
@tags = @search.result.order(:name)
|
||||
@tags = policy_scope(Tag)
|
||||
checked_exercise_tags = @exercise.exercise_tags
|
||||
checked_tags = checked_exercise_tags.collect(&:tag).to_set
|
||||
unchecked_tags = Tag.all.to_set.subtract checked_tags
|
||||
@ -455,6 +469,7 @@ working_time_accumulated: working_time_accumulated})
|
||||
ExerciseTag.new(exercise: @exercise, tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
private :collect_set_and_unset_exercise_tags
|
||||
|
||||
def show
|
||||
@ -466,15 +481,43 @@ working_time_accumulated: working_time_accumulated})
|
||||
end
|
||||
|
||||
def statistics
|
||||
if @external_user
|
||||
# Show general statistic page for specific exercise
|
||||
user_statistics = {'InternalUser' => {}, 'ExternalUser' => {}}
|
||||
|
||||
query = Submission.select('user_id, user_type, MAX(score) AS maximum_score, COUNT(id) AS runs')
|
||||
.where(exercise_id: @exercise.id)
|
||||
.group('user_id, user_type')
|
||||
|
||||
query = if policy(@exercise).detailed_statistics?
|
||||
query
|
||||
elsif !policy(@exercise).detailed_statistics? && current_user.study_groups.count.positive?
|
||||
query.where(study_groups: current_user.study_groups.pluck(:id), cause: 'submit')
|
||||
else
|
||||
# e.g. internal user without any study groups, show no submissions
|
||||
query.where('false')
|
||||
end
|
||||
|
||||
query.each do |tuple|
|
||||
user_statistics[tuple['user_type']][tuple['user_id'].to_i] = tuple
|
||||
end
|
||||
|
||||
render locals: {
|
||||
user_statistics: user_statistics,
|
||||
}
|
||||
end
|
||||
|
||||
def external_user_statistics
|
||||
# Render statistics page for one specific external user
|
||||
authorize(@external_user, :statistics?)
|
||||
|
||||
if policy(@exercise).detailed_statistics?
|
||||
@submissions = Submission.where(user: @external_user,
|
||||
exercise_id: @exercise.id).in_study_group_of(current_user).order('created_at')
|
||||
submissions = Submission.where(user: @external_user, exercise: @exercise)
|
||||
.in_study_group_of(current_user)
|
||||
.order('created_at')
|
||||
@show_autosaves = params[:show_autosaves] == 'true' || submissions.none? {|s| s.cause != 'autosave' }
|
||||
submissions = submissions.where.not(cause: 'autosave') unless @show_autosaves
|
||||
interventions = UserExerciseIntervention.where('user_id = ? AND exercise_id = ?', @external_user.id,
|
||||
@exercise.id)
|
||||
@all_events = (@submissions + interventions).sort_by(&:created_at)
|
||||
@all_events = (submissions + interventions).sort_by(&:created_at)
|
||||
@deltas = @all_events.map.with_index do |item, index|
|
||||
delta = item.created_at - @all_events[index - 1].created_at if index.positive?
|
||||
delta.nil? || (delta > StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS) ? 0 : delta
|
||||
@ -486,35 +529,15 @@ working_time_accumulated: working_time_accumulated})
|
||||
else
|
||||
final_submissions = Submission.where(user: @external_user,
|
||||
exercise_id: @exercise.id).in_study_group_of(current_user).final
|
||||
@submissions = []
|
||||
submissions = []
|
||||
%i[before_deadline within_grace_period after_late_deadline].each do |filter|
|
||||
relevant_submission = final_submissions.send(filter).latest
|
||||
@submissions.push relevant_submission if relevant_submission.present?
|
||||
submissions.push relevant_submission if relevant_submission.present?
|
||||
end
|
||||
@all_events = @submissions
|
||||
@all_events = submissions
|
||||
end
|
||||
|
||||
render 'exercises/external_users/statistics'
|
||||
else
|
||||
# Show general statistic page for specific exercise
|
||||
user_statistics = {}
|
||||
additional_filter = if policy(@exercise).detailed_statistics?
|
||||
''
|
||||
elsif !policy(@exercise).detailed_statistics? && current_user.study_groups.count.positive?
|
||||
"AND study_group_id IN (#{current_user.study_groups.pluck(:id).join(', ')}) AND cause = 'submit'"
|
||||
else
|
||||
# e.g. internal user without any study groups, show no submissions
|
||||
'AND FALSE'
|
||||
end
|
||||
query = "SELECT user_id, MAX(score) AS maximum_score, COUNT(id) AS runs
|
||||
FROM submissions WHERE exercise_id = #{@exercise.id} #{additional_filter} GROUP BY
|
||||
user_id;"
|
||||
ApplicationRecord.connection.execute(query).each do |tuple|
|
||||
user_statistics[tuple['user_id'].to_i] = tuple
|
||||
end
|
||||
render locals: {
|
||||
user_statistics: user_statistics,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def submit
|
||||
@ -534,8 +557,6 @@ working_time_accumulated: working_time_accumulated})
|
||||
end
|
||||
|
||||
def transmit_lti_score
|
||||
::NewRelic::Agent.add_custom_attributes({submission: @submission.id,
|
||||
normalized_score: @submission.normalized_score})
|
||||
response = send_score(@submission)
|
||||
|
||||
if response[:status] == 'success'
|
||||
@ -552,6 +573,7 @@ working_time_accumulated: working_time_accumulated})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private :transmit_lti_score
|
||||
|
||||
def update
|
||||
|
@ -10,7 +10,7 @@ class ExternalUsersController < ApplicationController
|
||||
|
||||
def index
|
||||
@search = ExternalUser.ransack(params[:q])
|
||||
@users = @search.result.in_study_group_of(current_user).includes(:consumer).paginate(page: params[:page])
|
||||
@users = @search.result.in_study_group_of(current_user).includes(:consumer).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -32,7 +32,7 @@ class ExternalUsersController < ApplicationController
|
||||
score,
|
||||
id,
|
||||
CASE
|
||||
WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0'
|
||||
WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0'
|
||||
ELSE working_time
|
||||
END AS working_time_new
|
||||
FROM
|
||||
|
@ -19,7 +19,7 @@ class FileTemplatesController < ApplicationController
|
||||
# GET /file_templates
|
||||
# GET /file_templates.json
|
||||
def index
|
||||
@file_templates = FileTemplate.all.order(:file_type_id).paginate(page: params[:page])
|
||||
@file_templates = FileTemplate.all.order(:file_type_id).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -48,7 +48,7 @@ class FileTemplatesController < ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
if @file_template.save
|
||||
format.html { redirect_to @file_template, notice: 'File template was successfully created.' }
|
||||
format.html { redirect_to @file_template, notice: t('shared.object_created', model: @file_template.class.model_name.human) }
|
||||
format.json { render :show, status: :created, location: @file_template }
|
||||
else
|
||||
format.html { render :new }
|
||||
@ -63,7 +63,7 @@ class FileTemplatesController < ApplicationController
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @file_template.update(file_template_params)
|
||||
format.html { redirect_to @file_template, notice: 'File template was successfully updated.' }
|
||||
format.html { redirect_to @file_template, notice: t('shared.object_updated', model: @file_template.class.model_name.human) }
|
||||
format.json { render :show, status: :ok, location: @file_template }
|
||||
else
|
||||
format.html { render :edit }
|
||||
@ -78,7 +78,7 @@ class FileTemplatesController < ApplicationController
|
||||
authorize!
|
||||
@file_template.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to file_templates_url, notice: 'File template was successfully destroyed.' }
|
||||
format.html { redirect_to file_templates_url, notice: t('shared.object_destroyed', model: @file_template.class.model_name.human) }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
@ -33,7 +33,7 @@ class FileTypesController < ApplicationController
|
||||
private :file_type_params
|
||||
|
||||
def index
|
||||
@file_types = FileType.all.includes(:user).order(:name).paginate(page: params[:page])
|
||||
@file_types = FileType.all.includes(:user).order(:name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -6,7 +6,7 @@ class FlowrController < ApplicationController
|
||||
# get the latest submission for this user that also has a test run (i.e. structured_errors if applicable)
|
||||
submission = Submission.joins(:testruns)
|
||||
.where(submissions: {user_id: current_user.id, user_type: current_user.class.name})
|
||||
.order('testruns.created_at DESC').first
|
||||
.merge(Testrun.order(created_at: :desc)).first
|
||||
|
||||
# Return if no submission was found
|
||||
if submission.blank? || @embed_options[:disable_hints] || @embed_options[:hide_test_results]
|
||||
|
@ -6,7 +6,6 @@ class InternalUsersController < ApplicationController
|
||||
before_action :require_activation_token, only: :activate
|
||||
before_action :require_reset_password_token, only: :reset_password
|
||||
before_action :set_user, only: MEMBER_ACTIONS
|
||||
skip_before_action :verify_authenticity_token, only: :activate
|
||||
after_action :verify_authorized, except: %i[activate forgot_password reset_password]
|
||||
|
||||
def activate
|
||||
@ -33,9 +32,15 @@ class InternalUsersController < ApplicationController
|
||||
|
||||
def create
|
||||
@user = InternalUser.new(internal_user_params)
|
||||
@user.role = role_param if current_user.admin?
|
||||
authorize!
|
||||
@user.send(:setup_activation)
|
||||
create_and_respond(object: @user) { @user.send(:send_activation_needed_email!) }
|
||||
create_and_respond(object: @user) do
|
||||
@user.send(:send_activation_needed_email!)
|
||||
# The return value is used as a flash message. If this block does not
|
||||
# have any specific return value, a default success message is shown.
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def deliver_reset_password_instructions
|
||||
@ -63,15 +68,20 @@ class InternalUsersController < ApplicationController
|
||||
|
||||
def index
|
||||
@search = InternalUser.ransack(params[:q])
|
||||
@users = @search.result.includes(:consumer).order(:name).paginate(page: params[:page])
|
||||
@users = @search.result.includes(:consumer).order(:name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def internal_user_params
|
||||
params[:internal_user].permit(:consumer_id, :email, :name, :role) if params[:internal_user].present?
|
||||
params.require(:internal_user).permit(:consumer_id, :email, :name)
|
||||
end
|
||||
private :internal_user_params
|
||||
|
||||
def role_param
|
||||
params.require(:internal_user).permit(:role)[:role]
|
||||
end
|
||||
private :role_param
|
||||
|
||||
def new
|
||||
@user = InternalUser.new
|
||||
authorize!
|
||||
@ -129,6 +139,7 @@ class InternalUsersController < ApplicationController
|
||||
# the form by another user. Otherwise, the update might fail if an
|
||||
# activation_token or password_reset_token is present
|
||||
@user.validate_password = current_user == @user
|
||||
@user.role = role_param if current_user.admin?
|
||||
|
||||
update_and_respond(object: @user, params: internal_user_params)
|
||||
end
|
||||
|
@ -15,7 +15,7 @@ class ProxyExercisesController < ApplicationController
|
||||
user: current_user)
|
||||
proxy_exercise.send(:generate_token)
|
||||
if proxy_exercise.save
|
||||
redirect_to(proxy_exercise, notice: t('shared.object_cloned', model: ProxyExercise.model_name.human))
|
||||
redirect_to(proxy_exercise_path(proxy_exercise), notice: t('shared.object_cloned', model: ProxyExercise.model_name.human))
|
||||
else
|
||||
flash[:danger] = t('shared.message_failure')
|
||||
redirect_to(@proxy_exercise)
|
||||
@ -51,7 +51,7 @@ class ProxyExercisesController < ApplicationController
|
||||
|
||||
def index
|
||||
@search = policy_scope(ProxyExercise).ransack(params[:q])
|
||||
@proxy_exercises = @search.result.order(:title).paginate(page: params[:page])
|
||||
@proxy_exercises = @search.result.order(:title).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -39,8 +39,8 @@ class RemoteEvaluationController < ApplicationController
|
||||
else
|
||||
{
|
||||
message: "Your submission was successfully scored with #{@submission.normalized_score}%. " \
|
||||
'However, your score could not be sent to the e-Learning platform. Please reopen ' \
|
||||
'the exercise through the e-Learning platform and try again.',
|
||||
'However, your score could not be sent to the e-Learning platform. Please check ' \
|
||||
'the submission deadline, reopen the exercise through the e-Learning platform and try again.',
|
||||
status: 410,
|
||||
}
|
||||
end
|
||||
|
@ -1,8 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RequestForCommentsController < ApplicationController
|
||||
include CommonBehavior
|
||||
before_action :require_user!
|
||||
before_action :set_request_for_comment, only: %i[show mark_as_solved set_thank_you_note]
|
||||
before_action :set_request_for_comment, only: %i[show mark_as_solved set_thank_you_note clear_question]
|
||||
before_action :set_study_group_grouping,
|
||||
only: %i[index my_comment_requests rfcs_with_my_comments rfcs_for_exercise]
|
||||
|
||||
@ -23,7 +24,7 @@ class RequestForCommentsController < ApplicationController
|
||||
.where(exercises: {unpublished: false})
|
||||
.includes(submission: [:study_group])
|
||||
.order('created_at DESC')
|
||||
.paginate(page: params[:page], total_entries: @search.result.length)
|
||||
.paginate(page: params[:page], per_page: per_page_param, total_entries: @search.result.length)
|
||||
|
||||
authorize!
|
||||
end
|
||||
@ -36,7 +37,7 @@ class RequestForCommentsController < ApplicationController
|
||||
.ransack(params[:q])
|
||||
@request_for_comments = @search.result
|
||||
.order('created_at DESC')
|
||||
.paginate(page: params[:page])
|
||||
.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
render 'index'
|
||||
end
|
||||
@ -50,7 +51,7 @@ class RequestForCommentsController < ApplicationController
|
||||
.ransack(params[:q])
|
||||
@request_for_comments = @search.result
|
||||
.order('last_comment DESC')
|
||||
.paginate(page: params[:page])
|
||||
.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
render 'index'
|
||||
end
|
||||
@ -65,7 +66,7 @@ class RequestForCommentsController < ApplicationController
|
||||
@request_for_comments = @search.result
|
||||
.joins(:exercise)
|
||||
.order('last_comment DESC')
|
||||
.paginate(page: params[:page])
|
||||
.paginate(page: params[:page], per_page: per_page_param)
|
||||
# let the exercise decide, whether its rfcs should be visible
|
||||
authorize(exercise)
|
||||
render 'index'
|
||||
@ -101,6 +102,12 @@ class RequestForCommentsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
# POST /request_for_comments/1/clear_question
|
||||
def clear_question
|
||||
authorize!
|
||||
update_and_respond(object: @request_for_comment, params: {question: nil})
|
||||
end
|
||||
|
||||
# GET /request_for_comments/1
|
||||
# GET /request_for_comments/1.json
|
||||
def show
|
||||
|
@ -24,7 +24,7 @@ class SessionsController < ApplicationController
|
||||
store_lti_session_data(consumer: @consumer, parameters: params)
|
||||
store_nonce(params[:oauth_nonce])
|
||||
if params[:custom_redirect_target]
|
||||
redirect_to(params[:custom_redirect_target])
|
||||
redirect_to(URI.parse(params[:custom_redirect_target].to_s).path)
|
||||
else
|
||||
redirect_to(implement_exercise_path(@exercise),
|
||||
notice: t("sessions.create_through_lti.session_#{lti_outcome_service?(@exercise.id, @current_user.id) ? 'with' : 'without'}_outcome",
|
||||
@ -43,6 +43,10 @@ class SessionsController < ApplicationController
|
||||
|
||||
def destroy_through_lti
|
||||
@submission = Submission.find(params[:submission_id])
|
||||
authorize(@submission, :show?)
|
||||
lti_parameter = LtiParameter.where(external_users_id: @submission.user_id, exercises_id: @submission.exercise_id).last
|
||||
@url = consumer_return_url(build_tool_provider(consumer: @submission.user.consumer, parameters: lti_parameter&.lti_parameters))
|
||||
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id)
|
||||
end
|
||||
|
||||
|
@ -7,7 +7,7 @@ class StudyGroupsController < ApplicationController
|
||||
|
||||
def index
|
||||
@search = policy_scope(StudyGroup).ransack(params[:q])
|
||||
@study_groups = @search.result.includes(:consumer).order(:name).paginate(page: params[:page])
|
||||
@study_groups = @search.result.includes(:consumer).order(:name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -9,10 +9,10 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_submission, only: %i[download download_file render_file run score show statistics test]
|
||||
before_action :set_testrun, only: %i[run score test]
|
||||
before_action :set_files, only: %i[download show]
|
||||
before_action :set_files_and_specific_file, only: %i[download_file render_file run test]
|
||||
before_action :set_mime_type, only: %i[download_file render_file]
|
||||
skip_before_action :verify_authenticity_token, only: %i[download_file render_file]
|
||||
|
||||
def create
|
||||
@submission = Submission.new(submission_params)
|
||||
@ -27,8 +27,8 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
stringio = Zip::OutputStream.write_buffer do |zio|
|
||||
@files.each do |file|
|
||||
zio.put_next_entry(file.filepath)
|
||||
zio.write(file.content.presence || file.native_file.read)
|
||||
zio.put_next_entry(file.filepath.delete_prefix('/'))
|
||||
zio.write(file.read)
|
||||
end
|
||||
|
||||
# zip exercise description
|
||||
@ -39,7 +39,7 @@ class SubmissionsController < ApplicationController
|
||||
# zip .co file
|
||||
zio.put_next_entry('.co')
|
||||
zio.write(File.read(id_file))
|
||||
File.delete(id_file) if File.exist?(id_file)
|
||||
FileUtils.rm_rf(id_file)
|
||||
|
||||
# zip client scripts
|
||||
scripts_path = 'app/assets/remote_scripts'
|
||||
@ -56,22 +56,18 @@ class SubmissionsController < ApplicationController
|
||||
def download_file
|
||||
raise Pundit::NotAuthorizedError if @embed_options[:disable_download]
|
||||
|
||||
if @file.native_file?
|
||||
send_file(@file.native_file.path)
|
||||
else
|
||||
send_data(@file.content, filename: @file.name_with_extension)
|
||||
end
|
||||
send_data(@file.read, filename: @file.name_with_extension)
|
||||
end
|
||||
|
||||
def index
|
||||
@search = Submission.ransack(params[:q])
|
||||
@submissions = @search.result.includes(:exercise, :user).paginate(page: params[:page])
|
||||
@submissions = @search.result.includes(:exercise, :user).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def render_file
|
||||
if @file.native_file?
|
||||
send_file(@file.native_file.path, disposition: 'inline')
|
||||
send_data(@file.read, filename: @file.name_with_extension, disposition: 'inline')
|
||||
else
|
||||
render(plain: @file.content)
|
||||
end
|
||||
@ -85,10 +81,14 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
hijack do |tubesock|
|
||||
client_socket = tubesock
|
||||
return kill_client_socket(client_socket) if @embed_options[:disable_run]
|
||||
|
||||
client_socket.onopen do |_event|
|
||||
kill_client_socket(client_socket) if @embed_options[:disable_run]
|
||||
end
|
||||
|
||||
client_socket.onclose do |_event|
|
||||
runner_socket&.close(:terminated_by_client)
|
||||
@testrun[:status] ||= :terminated_by_client
|
||||
end
|
||||
|
||||
client_socket.onmessage do |raw_event|
|
||||
@ -97,9 +97,17 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
# Otherwise, we expect to receive a JSON: Parsing.
|
||||
event = JSON.parse(raw_event).deep_symbolize_keys
|
||||
event[:cmd] = event[:cmd].to_sym
|
||||
event[:stream] = event[:stream].to_sym if event.key? :stream
|
||||
|
||||
case event[:cmd].to_sym
|
||||
# We could store the received event. However, it is also echoed by the container
|
||||
# and correctly identified as the original input. Therefore, we don't store
|
||||
# it here to prevent duplicated events.
|
||||
# @testrun[:messages].push(event)
|
||||
|
||||
case event[:cmd]
|
||||
when :client_kill
|
||||
@testrun[:status] = :terminated_by_client
|
||||
close_client_connection(client_socket)
|
||||
Rails.logger.debug('Client exited container.')
|
||||
when :result, :canvasevent, :exception
|
||||
@ -125,68 +133,88 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
@output = +''
|
||||
durations = @submission.run(@file) do |socket|
|
||||
@testrun[:output] = +''
|
||||
durations = @submission.run(@file) do |socket, starting_time|
|
||||
runner_socket = socket
|
||||
@testrun[:starting_time] = starting_time
|
||||
client_socket.send_data JSON.dump({cmd: :status, status: :container_running})
|
||||
|
||||
runner_socket.on :stdout do |data|
|
||||
json_data = prepare data, :stdout
|
||||
@output << json_data[0, max_output_buffer_size - @output.size]
|
||||
client_socket.send_data(json_data)
|
||||
message = retrieve_message_from_output data, :stdout
|
||||
@testrun[:output] << message[:data][0, max_output_buffer_size - @testrun[:output].size] if message[:data]
|
||||
send_and_store client_socket, message
|
||||
end
|
||||
|
||||
runner_socket.on :stderr do |data|
|
||||
json_data = prepare data, :stderr
|
||||
@output << json_data[0, max_output_buffer_size - @output.size]
|
||||
client_socket.send_data(json_data)
|
||||
message = retrieve_message_from_output data, :stderr
|
||||
@testrun[:output] << message[:data][0, max_output_buffer_size - @testrun[:output].size] if message[:data]
|
||||
send_and_store client_socket, message
|
||||
end
|
||||
|
||||
runner_socket.on :exit do |exit_code|
|
||||
@testrun[:exit_code] = exit_code
|
||||
exit_statement =
|
||||
if @output.empty? && exit_code.zero?
|
||||
if @testrun[:output].empty? && exit_code.zero?
|
||||
@testrun[:status] = :ok
|
||||
t('exercises.implement.no_output_exit_successful', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)
|
||||
elsif @output.empty?
|
||||
elsif @testrun[:output].empty?
|
||||
@testrun[:status] = :failed
|
||||
t('exercises.implement.no_output_exit_failure', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)
|
||||
elsif exit_code.zero?
|
||||
@testrun[:status] = :ok
|
||||
"\n#{t('exercises.implement.exit_successful', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)}"
|
||||
else
|
||||
@testrun[:status] = :failed
|
||||
"\n#{t('exercises.implement.exit_failure', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)}"
|
||||
end
|
||||
client_socket.send_data JSON.dump({cmd: :write, stream: :stdout, data: "#{exit_statement}\n"})
|
||||
send_and_store client_socket, {cmd: :write, stream: :stdout, data: "#{exit_statement}\n"}
|
||||
if exit_code == 137
|
||||
send_and_store client_socket, {cmd: :status, status: :out_of_memory}
|
||||
@testrun[:status] = :out_of_memory
|
||||
end
|
||||
|
||||
close_client_connection(client_socket)
|
||||
end
|
||||
end
|
||||
@container_execution_time = durations[:execution_duration]
|
||||
@waiting_for_container_time = durations[:waiting_duration]
|
||||
@testrun[:container_execution_time] = durations[:execution_duration]
|
||||
@testrun[:waiting_for_container_time] = durations[:waiting_duration]
|
||||
rescue Runner::Error::ExecutionTimeout => e
|
||||
client_socket.send_data JSON.dump({cmd: :status, status: :timeout})
|
||||
send_and_store client_socket, {cmd: :status, status: :timeout}
|
||||
close_client_connection(client_socket)
|
||||
Rails.logger.debug { "Running a submission timed out: #{e.message}" }
|
||||
@output = "timeout: #{@output}"
|
||||
@testrun[:status] ||= :timeout
|
||||
@testrun[:output] = "timeout: #{@testrun[:output]}"
|
||||
extract_durations(e)
|
||||
rescue Runner::Error => e
|
||||
client_socket.send_data JSON.dump({cmd: :status, status: :container_depleted})
|
||||
send_and_store client_socket, {cmd: :status, status: :container_depleted}
|
||||
close_client_connection(client_socket)
|
||||
@testrun[:status] ||= :container_depleted
|
||||
Rails.logger.debug { "Runner error while running a submission: #{e.message}" }
|
||||
extract_durations(e)
|
||||
ensure
|
||||
save_run_output
|
||||
save_testrun_output 'run'
|
||||
end
|
||||
|
||||
def score
|
||||
hijack do |tubesock|
|
||||
return if @embed_options[:disable_score]
|
||||
tubesock.onopen do |_event|
|
||||
switch_locale do
|
||||
kill_client_socket(tubesock) if @embed_options[:disable_score]
|
||||
|
||||
# The score is stored separately, we can forward it to the client immediately
|
||||
tubesock.send_data(JSON.dump(@submission.calculate_score))
|
||||
# To enable hints when scoring a submission, uncomment the next line:
|
||||
# send_hints(tubesock, StructuredError.where(submission: @submission))
|
||||
rescue Runner::Error => e
|
||||
tubesock.send_data JSON.dump({cmd: :status, status: :container_depleted})
|
||||
Rails.logger.debug { "Runner error while scoring submission #{@submission.id}: #{e.message}" }
|
||||
ensure
|
||||
kill_client_socket(tubesock)
|
||||
rescue Runner::Error => e
|
||||
extract_durations(e)
|
||||
send_and_store tubesock, {cmd: :status, status: :container_depleted}
|
||||
kill_client_socket(tubesock)
|
||||
Rails.logger.debug { "Runner error while scoring submission #{@submission.id}: #{e.message}" }
|
||||
@testrun[:passed] = false
|
||||
save_testrun_output 'assess'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -196,14 +224,22 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
def test
|
||||
hijack do |tubesock|
|
||||
return kill_client_socket(tubesock) if @embed_options[:disable_run]
|
||||
tubesock.onopen do |_event|
|
||||
switch_locale do
|
||||
kill_client_socket(tubesock) if @embed_options[:disable_run]
|
||||
|
||||
# The score is stored separately, we can forward it to the client immediately
|
||||
tubesock.send_data(JSON.dump(@submission.test(@file)))
|
||||
rescue Runner::Error => e
|
||||
tubesock.send_data JSON.dump({cmd: :status, status: :container_depleted})
|
||||
Rails.logger.debug { "Runner error while testing submission #{@submission.id}: #{e.message}" }
|
||||
ensure
|
||||
kill_client_socket(tubesock)
|
||||
rescue Runner::Error => e
|
||||
extract_durations(e)
|
||||
send_and_store tubesock, {cmd: :status, status: :container_depleted}
|
||||
kill_client_socket(tubesock)
|
||||
Rails.logger.debug { "Runner error while testing submission #{@submission.id}: #{e.message}" }
|
||||
@testrun[:passed] = false
|
||||
save_testrun_output 'assess'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -221,6 +257,7 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def kill_client_socket(client_socket)
|
||||
# We don't want to store this (arbitrary) exit command and redirect it ourselves
|
||||
client_socket.send_data JSON.dump({cmd: :exit})
|
||||
client_socket.close
|
||||
end
|
||||
@ -240,7 +277,7 @@ class SubmissionsController < ApplicationController
|
||||
# parse validation token
|
||||
content = "#{remote_evaluation_mapping.validation_token}\n"
|
||||
# parse remote request url
|
||||
content += "#{request.base_url}/evaluate\n"
|
||||
content += "#{evaluate_url}\n"
|
||||
@submission.files.each do |file|
|
||||
content += "#{file.filepath}=#{file.file_id}\n"
|
||||
end
|
||||
@ -249,21 +286,33 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def extract_durations(error)
|
||||
@container_execution_time = error.execution_duration
|
||||
@waiting_for_container_time = error.waiting_duration
|
||||
@testrun[:starting_time] = error.starting_time
|
||||
@testrun[:container_execution_time] = error.execution_duration
|
||||
@testrun[:waiting_for_container_time] = error.waiting_duration
|
||||
end
|
||||
|
||||
def extract_errors
|
||||
results = []
|
||||
if @output.present?
|
||||
if @testrun[:output].present?
|
||||
@submission.exercise.execution_environment.error_templates.each do |template|
|
||||
pattern = Regexp.new(template.signature).freeze
|
||||
results << StructuredError.create_from_template(template, @output, @submission) if pattern.match(@output)
|
||||
results << StructuredError.create_from_template(template, @testrun[:output], @submission) if pattern.match(@testrun[:output])
|
||||
end
|
||||
end
|
||||
results
|
||||
end
|
||||
|
||||
def send_and_store(client_socket, message)
|
||||
message[:timestamp] = if @testrun[:starting_time]
|
||||
ActiveSupport::Duration.build(Time.zone.now - @testrun[:starting_time])
|
||||
else
|
||||
0.seconds
|
||||
end
|
||||
@testrun[:messages].push message
|
||||
@testrun[:status] = message[:status] if message[:status]
|
||||
client_socket.send_data JSON.dump(message)
|
||||
end
|
||||
|
||||
def max_output_buffer_size
|
||||
if @submission.cause == 'requestComments'
|
||||
5000
|
||||
@ -272,28 +321,25 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def prepare(data, stream)
|
||||
if valid_command? data
|
||||
data
|
||||
else
|
||||
JSON.dump({cmd: :write, stream: stream, data: data})
|
||||
end
|
||||
end
|
||||
|
||||
def sanitize_filename
|
||||
params[:filename].gsub(/\.json$/, '')
|
||||
end
|
||||
|
||||
# save the output of this "run" as a "testrun" (scoring runs are saved in submission.rb)
|
||||
def save_run_output
|
||||
Testrun.create(
|
||||
def save_testrun_output(cause)
|
||||
testrun = Testrun.create!(
|
||||
file: @file,
|
||||
cause: 'run',
|
||||
passed: @testrun[:passed],
|
||||
cause: cause,
|
||||
submission: @submission,
|
||||
output: @output,
|
||||
container_execution_time: @container_execution_time,
|
||||
waiting_for_container_time: @waiting_for_container_time
|
||||
exit_code: @testrun[:exit_code], # might be nil, e.g., when the run did not finish
|
||||
status: @testrun[:status],
|
||||
output: @testrun[:output].presence, # TODO: Remove duplicated saving of the output after creating TestrunMessages
|
||||
container_execution_time: @testrun[:container_execution_time],
|
||||
waiting_for_container_time: @testrun[:waiting_for_container_time]
|
||||
)
|
||||
TestrunMessage.create_for(testrun, @testrun[:messages])
|
||||
TestrunExecutionEnvironment.create(testrun: testrun, execution_environment: @submission.used_execution_environment)
|
||||
end
|
||||
|
||||
def send_hints(tubesock, errors)
|
||||
@ -301,7 +347,7 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
errors = errors.to_a.uniq(&:hint)
|
||||
errors.each do |error|
|
||||
tubesock.send_data JSON.dump({cmd: 'hint', hint: error.hint, description: error.error_template.description})
|
||||
send_and_store tubesock, {cmd: :hint, hint: error.hint, description: error.error_template.description}
|
||||
end
|
||||
end
|
||||
|
||||
@ -327,10 +373,26 @@ class SubmissionsController < ApplicationController
|
||||
authorize!
|
||||
end
|
||||
|
||||
def valid_command?(data)
|
||||
def set_testrun
|
||||
@testrun = {
|
||||
messages: [],
|
||||
exit_code: nil,
|
||||
status: nil,
|
||||
}
|
||||
end
|
||||
|
||||
def retrieve_message_from_output(data, stream)
|
||||
parsed = JSON.parse(data)
|
||||
parsed.instance_of?(Hash) && parsed.key?('cmd')
|
||||
if parsed.instance_of?(Hash) && parsed.key?('cmd')
|
||||
parsed.symbolize_keys!
|
||||
# Symbolize two values if present
|
||||
parsed[:cmd] = parsed[:cmd].to_sym
|
||||
parsed[:stream] = parsed[:stream].to_sym if parsed.key? :stream
|
||||
parsed
|
||||
else
|
||||
{cmd: :write, stream: stream, data: data}
|
||||
end
|
||||
rescue JSON::ParserError
|
||||
false
|
||||
{cmd: :write, stream: stream, data: data}
|
||||
end
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ class TagsController < ApplicationController
|
||||
private :tag_params
|
||||
|
||||
def index
|
||||
@tags = Tag.all.paginate(page: params[:page])
|
||||
@tags = Tag.all.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -34,7 +34,7 @@ class TipsController < ApplicationController
|
||||
private :tip_params
|
||||
|
||||
def index
|
||||
@tips = Tip.all.paginate(page: params[:page])
|
||||
@tips = Tip.all.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
class Runner
|
||||
class Error < ApplicationError
|
||||
attr_accessor :waiting_duration, :execution_duration
|
||||
attr_accessor :waiting_duration, :execution_duration, :starting_time
|
||||
|
||||
class BadRequest < Error; end
|
||||
|
||||
|
@ -18,11 +18,11 @@ module ApplicationHelper
|
||||
end
|
||||
|
||||
def empty
|
||||
tag.i(nil, class: 'empty fa fa-minus')
|
||||
tag.i(nil, class: 'empty fa-solid fa-minus')
|
||||
end
|
||||
|
||||
def label_column(label)
|
||||
tag.div(class: 'col-sm-3') do
|
||||
tag.div(class: 'col-md-3') do
|
||||
tag.strong do
|
||||
I18n.translation_present?("activerecord.attributes.#{label}") ? t("activerecord.attributes.#{label}") : t(label)
|
||||
end
|
||||
@ -31,7 +31,15 @@ module ApplicationHelper
|
||||
private :label_column
|
||||
|
||||
def no
|
||||
tag.i(nil, class: 'fa fa-times')
|
||||
tag.i(nil, class: 'fa-solid fa-xmark')
|
||||
end
|
||||
|
||||
def per_page_param
|
||||
if params[:per_page]
|
||||
[params[:per_page].to_i, 100].min
|
||||
else
|
||||
WillPaginate.per_page
|
||||
end
|
||||
end
|
||||
|
||||
def progress_bar(value)
|
||||
@ -64,13 +72,13 @@ module ApplicationHelper
|
||||
end
|
||||
|
||||
def value_column(value)
|
||||
tag.div(class: 'col-sm-9') do
|
||||
tag.div(class: 'col-md-9') do
|
||||
block_given? ? yield : symbol_for(value)
|
||||
end
|
||||
end
|
||||
private :value_column
|
||||
|
||||
def yes
|
||||
tag.i(nil, class: 'fa fa-check')
|
||||
tag.i(nil, class: 'fa-solid fa-check')
|
||||
end
|
||||
end
|
||||
|
@ -2,7 +2,9 @@
|
||||
|
||||
module StatisticsHelper
|
||||
WORKING_TIME_DELTA_IN_SECONDS = 5.minutes
|
||||
WORKING_TIME_DELTA_IN_SQL_INTERVAL = "'0:05:00'" # yes, a string with quotes
|
||||
def self.working_time_larger_delta
|
||||
@working_time_larger_delta ||= ActiveRecord::Base.sanitize_sql(['working_time >= ?', '0:05:00'])
|
||||
end
|
||||
|
||||
def statistics_data
|
||||
[
|
||||
@ -79,7 +81,7 @@ module StatisticsHelper
|
||||
{
|
||||
key: 'container_requests_per_minute',
|
||||
name: t('statistics.entries.exercises.container_requests_per_minute'),
|
||||
data: (Testrun.where('created_at >= ?', DateTime.now - 1.hour).count.to_f / 60).round(2),
|
||||
data: (Testrun.where(created_at: DateTime.now - 1.hour..).count.to_f / 60).round(2),
|
||||
unit: '/min',
|
||||
},
|
||||
{
|
||||
@ -179,7 +181,7 @@ module StatisticsHelper
|
||||
key: 'rfcs',
|
||||
name: t('activerecord.models.request_for_comment.other'),
|
||||
data: RequestForComment.in_range(from, to)
|
||||
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||
.select(RequestForComment.sanitize_sql(['date_trunc(?, created_at) AS "key", count(id) AS "value"', interval]))
|
||||
.group('key').order('key'),
|
||||
},
|
||||
{
|
||||
@ -187,7 +189,7 @@ module StatisticsHelper
|
||||
name: t('statistics.entries.request_for_comments.percent_solved'),
|
||||
data: RequestForComment.in_range(from, to)
|
||||
.where(solved: true)
|
||||
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||
.select(RequestForComment.sanitize_sql(['date_trunc(?, created_at) AS "key", count(id) AS "value"', interval]))
|
||||
.group('key').order('key'),
|
||||
},
|
||||
{
|
||||
@ -195,14 +197,14 @@ module StatisticsHelper
|
||||
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
|
||||
data: RequestForComment.in_range(from, to).unsolved
|
||||
.where(full_score_reached: true)
|
||||
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||
.select(RequestForComment.sanitize_sql(['date_trunc(?, created_at) AS "key", count(id) AS "value"', interval]))
|
||||
.group('key').order('key'),
|
||||
},
|
||||
{
|
||||
key: 'rfcs_unsolved',
|
||||
name: t('statistics.entries.request_for_comments.percent_unsolved'),
|
||||
data: RequestForComment.in_range(from, to).unsolved
|
||||
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||
.select(RequestForComment.sanitize_sql(['date_trunc(?, created_at) AS "key", count(id) AS "value"', interval]))
|
||||
.group('key').order('key'),
|
||||
},
|
||||
]
|
||||
@ -215,14 +217,14 @@ module StatisticsHelper
|
||||
name: t('statistics.entries.users.active'),
|
||||
data: ExternalUser.joins(:submissions)
|
||||
.where(submissions: {created_at: from..to})
|
||||
.select("date_trunc('#{interval}', submissions.created_at) AS \"key\", count(distinct external_users.id) AS \"value\"")
|
||||
.select(ExternalUser.sanitize_sql(['date_trunc(?, submissions.created_at) AS "key", count(distinct external_users.id) AS "value"', interval]))
|
||||
.group('key').order('key'),
|
||||
},
|
||||
{
|
||||
key: 'submissions',
|
||||
name: t('statistics.entries.exercises.submissions'),
|
||||
data: Submission.where(created_at: from..to)
|
||||
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
|
||||
.select(Submission.sanitize_sql(['date_trunc(?, created_at) AS "key", count(id) AS "value"', interval]))
|
||||
.group('key').order('key'),
|
||||
axis: 'right',
|
||||
},
|
||||
|
@ -10,12 +10,14 @@
|
||||
// JS
|
||||
import 'jquery';
|
||||
import 'jquery-ujs'
|
||||
import 'bootstrap/dist/js/bootstrap.bundle.min';
|
||||
import * as bootstrap from 'bootstrap/dist/js/bootstrap.bundle';
|
||||
import 'chosen-js/chosen.jquery';
|
||||
import 'jstree';
|
||||
import 'underscore';
|
||||
import 'd3';
|
||||
import '@sentry/browser';
|
||||
import 'sorttable';
|
||||
window.bootstrap = bootstrap; // Publish bootstrap in global namespace
|
||||
window._ = _; // Publish underscore's `_` in global namespace
|
||||
window.d3 = d3; // Publish d3 in global namespace
|
||||
window.Sentry = Sentry; // Publish sentry in global namespace
|
||||
@ -40,6 +42,23 @@ import 'jquery-ui/themes/base/resizable.css'
|
||||
import 'jquery-ui/themes/base/selectable.css'
|
||||
import 'jquery-ui/themes/base/sortable.css'
|
||||
|
||||
|
||||
// I18n locales
|
||||
import { I18n } from "i18n-js";
|
||||
import locales from "../../tmp/locales.json";
|
||||
|
||||
// Fetch user locale from html#lang.
|
||||
// This value is being set on `app/views/layouts/application.html.erb` and
|
||||
// is inferred from `ACCEPT-LANGUAGE` header.
|
||||
const userLocale = document.documentElement.lang;
|
||||
|
||||
export const i18n = new I18n();
|
||||
i18n.store(locales);
|
||||
i18n.defaultLocale = "en";
|
||||
i18n.enableFallback = true;
|
||||
i18n.locale = userLocale;
|
||||
window.I18n = i18n;
|
||||
|
||||
// Routes
|
||||
import * as Routes from 'routes.js.erb';
|
||||
window.Routes = Routes;
|
5
app/javascript/d3-tip.js
vendored
Normal file
5
app/javascript/d3-tip.js
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/* eslint no-console:0 */
|
||||
|
||||
// JS
|
||||
import * as d3Tip from 'd3-tip/dist'
|
||||
window.d3.tip = d3Tip;
|
8
app/javascript/highlight.js
Normal file
8
app/javascript/highlight.js
Normal file
@ -0,0 +1,8 @@
|
||||
/* eslint no-console:0 */
|
||||
|
||||
// JS
|
||||
import hljs from 'highlight.js/lib/common'
|
||||
window.hljs = hljs;
|
||||
|
||||
// CSS
|
||||
import 'highlight.js/styles/base16/tomorrow.css'
|
12
app/javascript/packs/d3-tip.js
vendored
12
app/javascript/packs/d3-tip.js
vendored
@ -1,12 +0,0 @@
|
||||
/* 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;
|
@ -1,15 +0,0 @@
|
||||
/* 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/base16/tomorrow.css'
|
@ -1,12 +0,0 @@
|
||||
/* 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 Sortable from 'sortablejs'
|
||||
window.Sortable = Sortable;
|
@ -1,15 +0,0 @@
|
||||
/* 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'
|
5
app/javascript/sortable.js
Normal file
5
app/javascript/sortable.js
Normal file
@ -0,0 +1,5 @@
|
||||
/* eslint no-console:0 */
|
||||
|
||||
// JS
|
||||
import Sortable from 'sortablejs'
|
||||
window.Sortable = Sortable;
|
@ -7,14 +7,14 @@
|
||||
// To reference this file, add <%= stylesheet_pack_tag 'stylesheets' %> to the appropriate
|
||||
// layout file, like app/views/layouts/application.html.slim
|
||||
|
||||
$web-font-path: '';
|
||||
@import '~bootswatch/dist/yeti/variables';
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
@import '~bootswatch/dist/yeti/bootswatch';
|
||||
$web-font-path: '//';
|
||||
@import '../../node_modules/bootswatch/dist/yeti/variables';
|
||||
@import '../../node_modules/bootstrap/scss/bootstrap';
|
||||
@import '../../node_modules/bootswatch/dist/yeti/bootswatch';
|
||||
$fa-font-path: '~@fortawesome/fontawesome-free/webfonts/';
|
||||
@import '~@fortawesome/fontawesome-free/scss/fontawesome';
|
||||
@import '~@fortawesome/fontawesome-free/scss/solid';
|
||||
@import '~@fortawesome/fontawesome-free/scss/regular';
|
||||
@import '~@fortawesome/fontawesome-free/scss/v4-shims';
|
||||
@import '../../node_modules/@fortawesome/fontawesome-free/scss/solid';
|
||||
@import '../../node_modules/@fortawesome/fontawesome-free/scss/regular';
|
||||
@import '../../node_modules/@fortawesome/fontawesome-free/scss/v4-shims';
|
||||
$opensans-path: '~opensans-webkit/fonts/';
|
||||
@import '~opensans-webkit/src/sass/open-sans';
|
||||
@import '../../node_modules/opensans-webkit/src/sass/open-sans';
|
8
app/javascript/vis.js
Normal file
8
app/javascript/vis.js
Normal file
@ -0,0 +1,8 @@
|
||||
/* eslint no-console:0 */
|
||||
|
||||
// JS
|
||||
import 'vis'
|
||||
window.vis = vis;
|
||||
|
||||
// CSS
|
||||
import 'vis-timeline/dist/vis-timeline-graph2d.css';
|
@ -20,10 +20,11 @@ class UserMailer < ApplicationMailer
|
||||
|
||||
def got_new_comment(comment, request_for_comment, commenting_user)
|
||||
# TODO: check whether we can take the last known locale of the receiver?
|
||||
token = AuthenticationToken.generate!(request_for_comment.user)
|
||||
@receiver_displayname = request_for_comment.user.displayname
|
||||
@commenting_user_displayname = commenting_user.displayname
|
||||
@comment_text = comment.text
|
||||
@rfc_link = request_for_comment_url(request_for_comment)
|
||||
@rfc_link = request_for_comment_url(request_for_comment, token: token.shared_secret)
|
||||
mail(
|
||||
subject: t('mailers.user_mailer.got_new_comment.subject',
|
||||
commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email
|
||||
@ -31,10 +32,11 @@ class UserMailer < ApplicationMailer
|
||||
end
|
||||
|
||||
def got_new_comment_for_subscription(comment, subscription, from_user)
|
||||
token = AuthenticationToken.generate!(subscription.user)
|
||||
@receiver_displayname = subscription.user.displayname
|
||||
@author_displayname = from_user.displayname
|
||||
@comment_text = comment.text
|
||||
@rfc_link = request_for_comment_url(subscription.request_for_comment)
|
||||
@rfc_link = request_for_comment_url(subscription.request_for_comment, token: token.shared_secret)
|
||||
@unsubscribe_link = unsubscribe_subscription_url(subscription)
|
||||
mail(
|
||||
subject: t('mailers.user_mailer.got_new_comment_for_subscription.subject',
|
||||
@ -42,11 +44,12 @@ class UserMailer < ApplicationMailer
|
||||
)
|
||||
end
|
||||
|
||||
def send_thank_you_note(request_for_comments, receiver)
|
||||
def send_thank_you_note(request_for_comment, receiver)
|
||||
token = AuthenticationToken.generate!(request_for_comment.user)
|
||||
@receiver_displayname = receiver.displayname
|
||||
@author = request_for_comments.user.displayname
|
||||
@thank_you_note = request_for_comments.thank_you_note
|
||||
@rfc_link = request_for_comment_url(request_for_comments)
|
||||
@author = request_for_comment.user.displayname
|
||||
@thank_you_note = request_for_comment.thank_you_note
|
||||
@rfc_link = request_for_comment_url(request_for_comment, token: token.shared_secret)
|
||||
mail(subject: t('mailers.user_mailer.send_thank_you_note.subject', author: @author), to: receiver.email)
|
||||
end
|
||||
|
||||
|
@ -8,10 +8,19 @@ class ApplicationRecord < ActiveRecord::Base
|
||||
def strip_strings
|
||||
# trim whitespace from beginning and end of string attributes
|
||||
# except for the `content` of CodeOcean::Files
|
||||
attribute_names.without('content').each do |name|
|
||||
# and except the `log` of TestrunMessages or the `output` of Testruns
|
||||
attribute_names.without('content', 'log', 'output').each do |name|
|
||||
if send(name.to_sym).respond_to?(:strip)
|
||||
send("#{name}=".to_sym, send(name).strip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.ransackable_associations(_auth_object = nil)
|
||||
[]
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(_auth_object = nil)
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
15
app/models/authentication_token.rb
Normal file
15
app/models/authentication_token.rb
Normal file
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'securerandom'
|
||||
|
||||
class AuthenticationToken < ApplicationRecord
|
||||
include Creation
|
||||
|
||||
def self.generate!(user)
|
||||
create!(
|
||||
shared_secret: SecureRandom.hex(32),
|
||||
user: user,
|
||||
expire_at: 7.days.from_now
|
||||
)
|
||||
end
|
||||
end
|
@ -56,6 +56,17 @@ module CodeOcean
|
||||
define_method("#{role}?") { self.role == role }
|
||||
end
|
||||
|
||||
def read
|
||||
if native_file?
|
||||
valid = Pathname(native_file.current_path).fnmatch? ::File.join(native_file.root, '**')
|
||||
return nil unless valid
|
||||
|
||||
native_file.read
|
||||
else
|
||||
content
|
||||
end
|
||||
end
|
||||
|
||||
def ancestor_id
|
||||
file_id || id
|
||||
end
|
||||
@ -83,12 +94,7 @@ module CodeOcean
|
||||
end
|
||||
|
||||
def hash_content
|
||||
self.hashed_content = Digest::MD5.new.hexdigest(if file_type.try(:binary?)
|
||||
::File.new(native_file.file.path,
|
||||
'r').read
|
||||
else
|
||||
content
|
||||
end)
|
||||
self.hashed_content = Digest::MD5.new.hexdigest(read || '')
|
||||
end
|
||||
private :hash_content
|
||||
|
||||
|
@ -1,12 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DefaultValues
|
||||
# rubocop:disable Naming/AccessorMethodName
|
||||
def set_default_values_if_present(options = {})
|
||||
options.each do |attribute, value|
|
||||
send(:"#{attribute}=", send(:"#{attribute}") || value) if has_attribute?(attribute)
|
||||
end
|
||||
end
|
||||
private :set_default_values_if_present
|
||||
# rubocop:enable Naming/AccessorMethodName
|
||||
end
|
||||
|
@ -14,4 +14,8 @@ class Consumer < ApplicationRecord
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(_auth_object = nil)
|
||||
%w[id]
|
||||
end
|
||||
end
|
||||
|
@ -16,6 +16,7 @@ class ExecutionEnvironment < ApplicationRecord
|
||||
has_many :exercises
|
||||
belongs_to :file_type
|
||||
has_many :error_templates
|
||||
belongs_to :testrun_execution_environment, optional: true, dependent: :destroy
|
||||
|
||||
scope :with_exercises, -> { where('id IN (SELECT execution_environment_id FROM exercises)') }
|
||||
|
||||
@ -60,6 +61,10 @@ class ExecutionEnvironment < ApplicationRecord
|
||||
exposed_ports.join(', ')
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(_auth_object = nil)
|
||||
%w[id]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_default_values
|
||||
|
@ -94,7 +94,7 @@ class Exercise < ApplicationRecord
|
||||
(SELECT user_id,
|
||||
user_type,
|
||||
score,
|
||||
CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
|
||||
CASE WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
user_type,
|
||||
@ -103,7 +103,7 @@ class Exercise < ApplicationRecord
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id=#{id}) AS foo) AS bar
|
||||
WHERE #{self.class.sanitize_sql(['exercise_id = ?', id])}) AS foo) AS bar
|
||||
GROUP BY user_id, user_type
|
||||
"
|
||||
end
|
||||
@ -118,7 +118,7 @@ class Exercise < ApplicationRecord
|
||||
(created_at - lag(created_at) over (PARTITION BY submissions.user_type, submissions.user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{exercise_id} AND study_group_id = #{study_group_id} #{additional_filter}),
|
||||
WHERE #{self.class.sanitize_sql(['exercise_id = ? and study_group_id = ?', exercise_id, study_group_id])} #{self.class.sanitize_sql(additional_filter)}),
|
||||
working_time_with_deltas_ignored AS (
|
||||
SELECT user_id,
|
||||
user_type,
|
||||
@ -126,7 +126,7 @@ class Exercise < ApplicationRecord
|
||||
sum(CASE WHEN score IS NOT NULL THEN 1 ELSE 0 END)
|
||||
over (ORDER BY user_type, user_id, created_at ASC) AS change_in_score,
|
||||
created_at,
|
||||
CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_filtered
|
||||
CASE WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0' ELSE working_time END AS working_time_filtered
|
||||
FROM working_time_between_submissions
|
||||
),
|
||||
working_times_with_score_expanded AS (
|
||||
@ -251,7 +251,7 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def get_quantiles(quantiles)
|
||||
quantiles_str = "[#{quantiles.join(',')}]"
|
||||
quantiles_str = self.class.sanitize_sql("[#{quantiles.join(',')}]")
|
||||
result = ActiveRecord::Base.transaction do
|
||||
self.class.connection.execute("
|
||||
SET LOCAL intervalstyle = 'iso_8601';
|
||||
@ -263,7 +263,7 @@ class Exercise < ApplicationRecord
|
||||
Max(score) AS max_score,
|
||||
(created_at - Lag(created_at) OVER (partition BY user_id, exercise_id ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{id}
|
||||
WHERE #{self.class.sanitize_sql(['exercise_id = ?', id])}
|
||||
AND user_type = 'ExternalUser'
|
||||
GROUP BY user_id,
|
||||
id,
|
||||
@ -273,7 +273,7 @@ class Exercise < ApplicationRecord
|
||||
Sum(weight) AS max_points
|
||||
FROM files
|
||||
WHERE context_type = 'Exercise'
|
||||
AND context_id = #{id}
|
||||
AND #{self.class.sanitize_sql(['context_id = ?', id])}
|
||||
AND role IN ('teacher_defined_test', 'teacher_defined_linter')
|
||||
GROUP BY context_id),
|
||||
-- filter for rows containing max points
|
||||
@ -342,7 +342,7 @@ class Exercise < ApplicationRecord
|
||||
exercise_id,
|
||||
max_score,
|
||||
CASE
|
||||
WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0'
|
||||
WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0'
|
||||
ELSE working_time
|
||||
END AS working_time_new
|
||||
FROM all_working_times_until_max ), result AS
|
||||
@ -372,11 +372,11 @@ class Exercise < ApplicationRecord
|
||||
end
|
||||
|
||||
def retrieve_working_time_statistics
|
||||
@working_time_statistics = {}
|
||||
@working_time_statistics = {'InternalUser' => {}, 'ExternalUser' => {}}
|
||||
ActiveRecord::Base.transaction do
|
||||
self.class.connection.execute("SET LOCAL intervalstyle = 'postgres'")
|
||||
self.class.connection.execute(user_working_time_query).each do |tuple|
|
||||
@working_time_statistics[tuple['user_id'].to_i] = tuple
|
||||
@working_time_statistics[tuple['user_type']][tuple['user_id'].to_i] = tuple
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -387,14 +387,14 @@ class Exercise < ApplicationRecord
|
||||
self.class.connection.execute("
|
||||
SELECT avg(working_time) as average_time
|
||||
FROM
|
||||
(#{user_working_time_query}) AS baz;
|
||||
(#{self.class.sanitize_sql(user_working_time_query)}) AS baz;
|
||||
").first['average_time']
|
||||
end
|
||||
end
|
||||
|
||||
def average_working_time_for(user_id)
|
||||
def average_working_time_for(user)
|
||||
retrieve_working_time_statistics if @working_time_statistics.nil?
|
||||
@working_time_statistics[user_id]['working_time']
|
||||
@working_time_statistics[user.class.name][user.id]['working_time']
|
||||
end
|
||||
|
||||
def accumulated_working_time_for_only(user)
|
||||
@ -445,7 +445,7 @@ class Exercise < ApplicationRecord
|
||||
|
||||
FILTERED_TIMES_UNTIL_MAX AS
|
||||
(
|
||||
SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
|
||||
SELECT user_id,exercise_id, max_score, CASE WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM ALL_WORKING_TIMES_UNTIL_MAX
|
||||
)
|
||||
SELECT e.external_id AS external_user_id, f.user_id, exercise_id, MAX(max_score) AS max_score, sum(working_time_new) AS working_time
|
||||
@ -597,4 +597,12 @@ cause: %w[submit assess remoteSubmit remoteAssess]}).distinct
|
||||
WHERE exercise_id = #{id}
|
||||
) AS t ON t.fv = submissions.id").distinct
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(_auth_object = nil)
|
||||
%w[title]
|
||||
end
|
||||
|
||||
def self.ransackable_associations(_auth_object = nil)
|
||||
%w[execution_environment]
|
||||
end
|
||||
end
|
||||
|
@ -31,4 +31,8 @@ working_time: time_to_f(item.exercise.average_working_time)}
|
||||
def to_s
|
||||
"#{I18n.t('activerecord.models.exercise_collection.one')}: #{name} (#{id})"
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(_auth_object = nil)
|
||||
%w[id]
|
||||
end
|
||||
end
|
||||
|
@ -246,4 +246,8 @@ class ProxyExercise < ApplicationRecord
|
||||
def select_easiest_exercise(exercises)
|
||||
exercises.order(:expected_difficulty).first
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(_auth_object = nil)
|
||||
%w[title]
|
||||
end
|
||||
end
|
||||
|
@ -22,27 +22,6 @@ class RequestForComment < ApplicationRecord
|
||||
|
||||
# after_save :trigger_rfc_action_cable
|
||||
|
||||
# not used right now, finds the last submission for the respective user and exercise.
|
||||
# might be helpful to check whether the exercise has been solved in the meantime.
|
||||
def last_submission
|
||||
Submission.find_by_sql(" select * from submissions
|
||||
where exercise_id = #{exercise_id} AND
|
||||
user_id = #{user_id}
|
||||
order by created_at desc
|
||||
limit 1").first
|
||||
end
|
||||
|
||||
# not used any longer, since we directly saved the submission_id now.
|
||||
# Was used before that to determine the submission belonging to the request_for_comment.
|
||||
def last_submission_before_creation
|
||||
Submission.find_by_sql(" select * from submissions
|
||||
where exercise_id = #{exercise_id} AND
|
||||
user_id = #{user_id} AND
|
||||
'#{created_at.localtime}' > created_at
|
||||
order by created_at desc
|
||||
limit 1").first
|
||||
end
|
||||
|
||||
def comments_count
|
||||
submission.files.sum {|file| file.comments.size }
|
||||
end
|
||||
@ -89,7 +68,7 @@ class RequestForComment < ApplicationRecord
|
||||
end
|
||||
|
||||
def last_per_user(count = 5)
|
||||
from("(#{row_number_user_sql}) as request_for_comments")
|
||||
from(row_number_user_sql, :request_for_comments)
|
||||
.where('row_number <= ?', count)
|
||||
.group('request_for_comments.id, request_for_comments.user_id, request_for_comments.user_type, ' \
|
||||
'request_for_comments.exercise_id, request_for_comments.file_id, request_for_comments.question, ' \
|
||||
@ -98,10 +77,18 @@ class RequestForComment < ApplicationRecord
|
||||
# ugly, but necessary
|
||||
end
|
||||
|
||||
def ransackable_associations(_auth_object = nil)
|
||||
%w[exercise submission]
|
||||
end
|
||||
|
||||
def ransackable_attributes(_auth_object = nil)
|
||||
%w[solved]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def row_number_user_sql
|
||||
select('id, user_id, user_type, exercise_id, file_id, question, created_at, updated_at, solved, full_score_reached, submission_id, row_number() OVER (PARTITION BY user_id, user_type ORDER BY created_at DESC) as row_number').to_sql
|
||||
select('id, user_id, user_type, exercise_id, file_id, question, created_at, updated_at, solved, full_score_reached, submission_id, row_number() OVER (PARTITION BY user_id, user_type ORDER BY created_at DESC) as row_number')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -62,10 +62,11 @@ class Runner < ApplicationRecord
|
||||
# initializing its Runner::Connection with the given event loop. The Runner::Connection class ensures that
|
||||
# this event loop is stopped after the socket was closed.
|
||||
event_loop = Runner::EventLoop.new
|
||||
socket = @strategy.attach_to_execution(command, event_loop, &block)
|
||||
socket = @strategy.attach_to_execution(command, event_loop, starting_time, &block)
|
||||
event_loop.wait
|
||||
raise socket.error if socket.error.present?
|
||||
rescue Runner::Error => e
|
||||
e.starting_time = starting_time
|
||||
e.execution_duration = Time.zone.now - starting_time
|
||||
raise
|
||||
end
|
||||
@ -74,29 +75,34 @@ class Runner < ApplicationRecord
|
||||
end
|
||||
|
||||
def execute_command(command, raise_exception: true)
|
||||
output = {}
|
||||
stdout = +''
|
||||
stderr = +''
|
||||
output = {
|
||||
stdout: +'',
|
||||
stderr: +'',
|
||||
messages: [],
|
||||
exit_code: 1, # default to error
|
||||
}
|
||||
try = 0
|
||||
|
||||
begin
|
||||
if try.nonzero?
|
||||
request_new_id
|
||||
save
|
||||
end
|
||||
|
||||
exit_code = 1 # default to error
|
||||
execution_time = attach_to_execution(command) do |socket|
|
||||
execution_time = attach_to_execution(command) do |socket, starting_time|
|
||||
socket.on :stderr do |data|
|
||||
stderr << data
|
||||
output[:stderr] << data
|
||||
output[:messages].push({cmd: :write, stream: :stderr, log: data, timestamp: Time.zone.now - starting_time})
|
||||
end
|
||||
socket.on :stdout do |data|
|
||||
stdout << data
|
||||
output[:stdout] << data
|
||||
output[:messages].push({cmd: :write, stream: :stdout, log: data, timestamp: Time.zone.now - starting_time})
|
||||
end
|
||||
socket.on :exit do |received_exit_code|
|
||||
exit_code = received_exit_code
|
||||
output[:exit_code] = received_exit_code
|
||||
end
|
||||
end
|
||||
output.merge!(container_execution_time: execution_time, status: exit_code.zero? ? :ok : :failed)
|
||||
output.merge!(container_execution_time: execution_time, status: output[:exit_code].zero? ? :ok : :failed)
|
||||
rescue Runner::Error::ExecutionTimeout => e
|
||||
Rails.logger.debug { "Running command `#{command}` timed out: #{e.message}" }
|
||||
output.merge!(status: :timeout, container_execution_time: e.execution_duration)
|
||||
@ -115,12 +121,13 @@ class Runner < ApplicationRecord
|
||||
output.merge!(status: :failed, container_execution_time: e.execution_duration)
|
||||
rescue Runner::Error => e
|
||||
Rails.logger.debug { "Running command `#{command}` failed: #{e.message}" }
|
||||
output.merge!(status: :failed, container_execution_time: e.execution_duration)
|
||||
output.merge!(status: :container_depleted, container_execution_time: e.execution_duration)
|
||||
ensure
|
||||
# We forward the exception if requested
|
||||
raise e if raise_exception && defined?(e) && e.present?
|
||||
|
||||
output.merge!(stdout: stdout, stderr: stderr)
|
||||
# If the process was killed with SIGKILL, it is most likely that the OOM killer was triggered.
|
||||
output[:status] = :out_of_memory if output[:exit_code] == 137
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -19,4 +19,12 @@ class StudyGroup < ApplicationRecord
|
||||
def to_s
|
||||
name.presence || "StudyGroup #{id}"
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(_auth_object = nil)
|
||||
%w[name]
|
||||
end
|
||||
|
||||
def self.ransackable_associations(_auth_object = nil)
|
||||
%w[consumer]
|
||||
end
|
||||
end
|
||||
|
@ -9,7 +9,7 @@ class Submission < ApplicationRecord
|
||||
remoteSubmit].freeze
|
||||
FILENAME_URL_PLACEHOLDER = '{filename}'
|
||||
MAX_COMMENTS_ON_RECOMMENDED_RFC = 5
|
||||
OLDEST_RFC_TO_SHOW = 6.months
|
||||
OLDEST_RFC_TO_SHOW = 1.month
|
||||
|
||||
belongs_to :exercise
|
||||
belongs_to :study_group, optional: true
|
||||
@ -46,6 +46,8 @@ class Submission < ApplicationRecord
|
||||
|
||||
validates :cause, inclusion: {in: CAUSES}
|
||||
|
||||
attr_reader :used_execution_environment
|
||||
|
||||
# after_save :trigger_working_times_action_cable
|
||||
|
||||
def build_files_hash(files, attribute)
|
||||
@ -74,7 +76,6 @@ class Submission < ApplicationRecord
|
||||
end
|
||||
|
||||
def normalized_score
|
||||
::NewRelic::Agent.add_custom_attributes({unnormalized_score: score})
|
||||
if !score.nil? && !exercise.maximum_score.nil? && exercise.maximum_score.positive?
|
||||
score / exercise.maximum_score
|
||||
else
|
||||
@ -190,12 +191,17 @@ class Submission < ApplicationRecord
|
||||
result.merge(output)
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(_auth_object = nil)
|
||||
%w[study_group_id]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepared_runner
|
||||
request_time = Time.zone.now
|
||||
begin
|
||||
runner = Runner.for(user, exercise.execution_environment)
|
||||
@used_execution_environment = AwsStudy.get_execution_environment(user, exercise)
|
||||
runner = Runner.for(user, @used_execution_environment)
|
||||
files = collect_files
|
||||
files.reject!(&:teacher_defined_assessment?) if cause == 'run'
|
||||
Rails.logger.debug { "#{Time.zone.now.getutc.inspect}: Copying files to Runner #{runner.id} for #{user_type} #{user_id} and Submission #{id}." }
|
||||
@ -249,10 +255,14 @@ class Submission < ApplicationRecord
|
||||
cause: 'assess', # Required to differ run and assess for RfC show
|
||||
file: file, # Test file that was executed
|
||||
passed: passed,
|
||||
output: testrun_output,
|
||||
exit_code: output[:exit_code],
|
||||
status: output[:status],
|
||||
output: testrun_output.presence,
|
||||
container_execution_time: output[:container_execution_time],
|
||||
waiting_for_container_time: output[:waiting_for_container_time]
|
||||
)
|
||||
TestrunMessage.create_for(testrun, output[:messages])
|
||||
TestrunExecutionEnvironment.create(testrun: testrun, execution_environment: @used_execution_environment)
|
||||
|
||||
filename = file.filepath
|
||||
|
||||
@ -266,6 +276,7 @@ class Submission < ApplicationRecord
|
||||
|
||||
output.merge!(assessment)
|
||||
output.merge!(filename: filename, message: feedback_message(file, output), weight: file.weight)
|
||||
output.except!(:messages)
|
||||
end
|
||||
|
||||
def feedback_message(file, output)
|
||||
|
@ -3,4 +3,22 @@
|
||||
class Testrun < ApplicationRecord
|
||||
belongs_to :file, class_name: 'CodeOcean::File', optional: true
|
||||
belongs_to :submission
|
||||
belongs_to :testrun_execution_environment, optional: true, dependent: :destroy
|
||||
has_many :testrun_messages, dependent: :destroy
|
||||
|
||||
enum status: {
|
||||
ok: 0,
|
||||
failed: 1,
|
||||
container_depleted: 2,
|
||||
timeout: 3,
|
||||
out_of_memory: 4,
|
||||
terminated_by_client: 5,
|
||||
}, _default: :ok, _prefix: true
|
||||
|
||||
validates :exit_code, numericality: {only_integer: true, min: 0, max: 255}, allow_nil: true
|
||||
validates :status, presence: true
|
||||
|
||||
def log
|
||||
testrun_messages.output.select(:log).map(&:log).join.presence
|
||||
end
|
||||
end
|
||||
|
6
app/models/testrun_execution_environment.rb
Normal file
6
app/models/testrun_execution_environment.rb
Normal file
@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TestrunExecutionEnvironment < ApplicationRecord
|
||||
belongs_to :testrun
|
||||
belongs_to :execution_environment
|
||||
end
|
94
app/models/testrun_message.rb
Normal file
94
app/models/testrun_message.rb
Normal file
@ -0,0 +1,94 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TestrunMessage < ApplicationRecord
|
||||
belongs_to :testrun
|
||||
|
||||
enum cmd: {
|
||||
input: 0,
|
||||
write: 1,
|
||||
clear: 2,
|
||||
turtle: 3,
|
||||
turtlebatch: 4,
|
||||
render: 5,
|
||||
exit: 6,
|
||||
status: 7,
|
||||
hint: 8,
|
||||
client_kill: 9,
|
||||
exception: 10,
|
||||
result: 11,
|
||||
canvasevent: 12,
|
||||
timeout: 13, # TODO: Shouldn't be in the data, this is a status and can be removed after the migration finished
|
||||
out_of_memory: 14, # TODO: Shouldn't be in the data, this is a status and can be removed after the migration finished
|
||||
}, _default: :write, _prefix: true
|
||||
|
||||
enum stream: {
|
||||
stdin: 0,
|
||||
stdout: 1,
|
||||
stderr: 2,
|
||||
}, _prefix: true
|
||||
|
||||
validates :cmd, presence: true
|
||||
validates :timestamp, presence: true
|
||||
validates :stream, length: {minimum: 0, allow_nil: false}, if: -> { cmd_write? }
|
||||
validates :log, length: {minimum: 0, allow_nil: false}, if: -> { cmd_write? }
|
||||
validate :either_data_or_log
|
||||
|
||||
default_scope { order(timestamp: :asc) }
|
||||
scope :output, -> { where(cmd: 1, stream: %i[stdout stderr]) }
|
||||
|
||||
def self.create_for(testrun, messages)
|
||||
# We don't want to store anything if the testrun passed
|
||||
return if testrun.passed?
|
||||
|
||||
messages.map! do |message|
|
||||
# We create a new hash and move all known keys
|
||||
result = {}
|
||||
result[:testrun] = testrun
|
||||
result[:log] = (message.delete(:log) || message.delete(:data)) if message[:cmd] == :write || message.key?(:log)
|
||||
result[:timestamp] = message.delete :timestamp
|
||||
result[:stream] = message.delete :stream if message.key?(:stream)
|
||||
result[:cmd] = message.delete :cmd
|
||||
# The remaining keys will be stored in the `data` column
|
||||
result[:data] = message.presence if message.present?
|
||||
result
|
||||
end
|
||||
|
||||
# Before storing all messages, we truncate some to save storage
|
||||
filtered_messages = filter_messages_by_size testrun, messages
|
||||
|
||||
# An array with hashes is passed, all are stored
|
||||
TestrunMessage.create!(filtered_messages)
|
||||
end
|
||||
|
||||
def self.filter_messages_by_size(testrun, messages)
|
||||
limits = if testrun.submission.cause == 'requestComments'
|
||||
{data: {limit: 25, size: 0}, log: {limit: 5000, size: 0}}
|
||||
else
|
||||
{data: {limit: 10, size: 0}, log: {limit: 500, size: 0}}
|
||||
end
|
||||
|
||||
filtered_messages = messages.map do |message|
|
||||
if message.key?(:log) && limits[:log][:size] < limits[:log][:limit]
|
||||
message[:log] = message[:log][0, limits[:log][:limit] - limits[:log][:size]]
|
||||
limits[:log][:size] += message[:log].size
|
||||
elsif message[:data] && limits[:data][:size] < limits[:data][:limit]
|
||||
limits[:data][:size] += 1
|
||||
elsif !message.key?(:log) && limits[:data][:size] < limits[:data][:limit]
|
||||
# Accept short TestrunMessages (e.g. just transporting a status information)
|
||||
# without increasing the `limits[:data][:limit]` before the limit is reached
|
||||
else
|
||||
# Clear all remaining messages
|
||||
message = nil
|
||||
end
|
||||
message
|
||||
end
|
||||
filtered_messages.select(&:present?)
|
||||
end
|
||||
|
||||
def either_data_or_log
|
||||
if [data, log].count(&:present?) > 1
|
||||
errors.add(log, "can't be present if data is also present")
|
||||
end
|
||||
end
|
||||
private :either_data_or_log
|
||||
end
|
@ -6,6 +6,7 @@ class User < ApplicationRecord
|
||||
ROLES = %w[admin teacher learner].freeze
|
||||
|
||||
belongs_to :consumer
|
||||
has_many :authentication_token, dependent: :destroy
|
||||
has_many :study_group_memberships, as: :user
|
||||
has_many :study_groups, through: :study_group_memberships, as: :user
|
||||
has_many :exercises, as: :user
|
||||
@ -40,4 +41,8 @@ class User < ApplicationRecord
|
||||
def to_s
|
||||
displayname
|
||||
end
|
||||
|
||||
def self.ransackable_attributes(_auth_object = nil)
|
||||
%w[name email external_id consumer_id role]
|
||||
end
|
||||
end
|
||||
|
@ -5,7 +5,7 @@ class ExercisePolicy < AdminOrAuthorPolicy
|
||||
admin?
|
||||
end
|
||||
|
||||
%i[show? feedback? statistics? rfcs_for_exercise?].each do |action|
|
||||
%i[show? feedback? statistics? external_user_statistics? rfcs_for_exercise?].each do |action|
|
||||
define_method(action) { admin? || teacher_in_study_group? || (teacher? && @record.public?) || author? }
|
||||
end
|
||||
|
||||
@ -38,7 +38,13 @@ class ExercisePolicy < AdminOrAuthorPolicy
|
||||
if @user.admin?
|
||||
@scope.all
|
||||
elsif @user.teacher?
|
||||
@scope.where('user_id = ? OR public = TRUE', @user.id)
|
||||
@scope.where(
|
||||
'user_id IN (SELECT user_id FROM study_group_memberships WHERE study_group_id IN (?))
|
||||
OR (user_id = ? AND user_type = ?)
|
||||
OR public = TRUE',
|
||||
@user.study_groups.pluck(:id),
|
||||
@user.id, @user.class.name
|
||||
)
|
||||
else
|
||||
@scope.none
|
||||
end
|
||||
|
@ -25,6 +25,10 @@ class RequestForCommentPolicy < ApplicationPolicy
|
||||
admin? || author?
|
||||
end
|
||||
|
||||
def clear_question?
|
||||
admin? || teacher_in_study_group?
|
||||
end
|
||||
|
||||
def edit?
|
||||
admin?
|
||||
end
|
||||
|
@ -126,11 +126,11 @@ module ProformaService
|
||||
|
||||
def add_content_to_task_file(file, task_file)
|
||||
if file.native_file.present?
|
||||
file = ::File.new(file.native_file.file.path, 'r')
|
||||
task_file.content = file.read
|
||||
file_content = file.read
|
||||
task_file.content = file_content
|
||||
task_file.used_by_grader = false
|
||||
task_file.binary = true
|
||||
task_file.mimetype = MimeMagic.by_magic(file).type
|
||||
task_file.mimetype = MimeMagic.by_magic(file_content).type
|
||||
else
|
||||
task_file.content = file.content
|
||||
task_file.used_by_grader = true
|
||||
|
@ -6,7 +6,7 @@ module CodeOcean
|
||||
existing_files = File.where(name: record.name, path: record.path, file_type_id: record.file_type_id,
|
||||
context_id: record.context_id, context_type: record.context_type).to_a
|
||||
if !existing_files.empty? && (!record.context.is_a?(Exercise) || record.context.new_record?)
|
||||
record.errors[:base] << 'Duplicate'
|
||||
record.errors.add(:base, 'Duplicate')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -2,8 +2,8 @@
|
||||
// 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)
|
||||
- append_javascript_pack_tag('vis')
|
||||
- append_stylesheet_pack_tag('vis')
|
||||
|
||||
h1 = t('breadcrumbs.dashboard.show')
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
- if model = Kernel.const_get(controller_path.classify) rescue nil
|
||||
- model = controller_path.classify.constantize rescue nil
|
||||
- if model
|
||||
- object = model.find_by(id: params[:id])
|
||||
- if model.try(:nested_resource?)
|
||||
- root_element = model.model_name.human(count: 2)
|
||||
@ -18,14 +19,17 @@
|
||||
|
||||
- title = "#{active_action} - #{application_name}"
|
||||
- content_for :breadcrumbs do
|
||||
.container
|
||||
ul.breadcrumb
|
||||
.container.mb-4
|
||||
ul.breadcrumb.bg-light.px-3.py-2
|
||||
- if root_element.present?
|
||||
li.breadcrumb-item = root_element
|
||||
li.breadcrumb-item.small
|
||||
= root_element
|
||||
- if current_element.present?
|
||||
li.breadcrumb-item = current_element
|
||||
li.breadcrumb-item.small
|
||||
= current_element
|
||||
- title = "#{object} - #{title}"
|
||||
- else
|
||||
- title = "#{model.model_name.human(count: 2)} - #{title}"
|
||||
li.breadcrumb-item.active = active_action
|
||||
li.breadcrumb-item.active.small
|
||||
= active_action
|
||||
- content_for :title, title
|
||||
|
@ -4,5 +4,4 @@
|
||||
- flash_mapping = {'alert' => 'warning', 'notice' => 'success'}
|
||||
div.alert.flash class="alert-#{flash_mapping.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" ×
|
||||
button.btn-close type="button" data-bs-dismiss="alert" aria-label="Close"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user