Merge pull request #261 from openHPI/feature/la-dashboard

Add LA dashboard architecture
This commit is contained in:
rteusner
2019-03-12 14:30:25 +01:00
committed by GitHub
60 changed files with 4120 additions and 3042 deletions

View File

@ -1,18 +0,0 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": "> 1%",
"uglify": true
},
"useBuiltIns": true
}]
],
"plugins": [
"syntax-dynamic-import",
"transform-object-rest-spread",
["transform-class-properties", { "spec": true }]
]
}

View File

@ -1,3 +0,0 @@
plugins:
postcss-import: {}
postcss-cssnext: {}

View File

@ -5,7 +5,7 @@ services:
language: ruby language: ruby
rvm: rvm:
- 2.5.1 - 2.6.1
cache: cache:
bundler: true bundler: true
yarn: true yarn: true
@ -27,7 +27,7 @@ before_install:
- docker pull openhpi/co_execenv_python - docker pull openhpi/co_execenv_python
- docker pull openhpi/co_execenv_java - docker pull openhpi/co_execenv_java
- mkdir ~/geckodriver - mkdir ~/geckodriver
- wget -O ~/geckodriver/download.tar.gz https://github.com/mozilla/geckodriver/releases/download/v0.23.0/geckodriver-v0.23.0-linux64.tar.gz - wget -O ~/geckodriver/download.tar.gz https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux64.tar.gz
- tar -xvzf ~/geckodriver/download.tar.gz -C ~/geckodriver/ - tar -xvzf ~/geckodriver/download.tar.gz -C ~/geckodriver/
- rm ~/geckodriver/download.tar.gz - rm ~/geckodriver/download.tar.gz
- chmod +x ~/geckodriver/geckodriver - chmod +x ~/geckodriver/geckodriver

View File

@ -17,8 +17,9 @@ gem 'pg'
gem 'pry-byebug' gem 'pry-byebug'
gem 'puma' gem 'puma'
gem 'pundit' gem 'pundit'
gem 'rails', '5.2.1.1' gem 'rails', '5.2.2'
gem 'rails-i18n' gem 'rails-i18n'
gem 'i18n-js'
gem 'ransack' gem 'ransack'
gem 'rubytree' gem 'rubytree'
gem 'sass-rails' gem 'sass-rails'
@ -31,12 +32,12 @@ gem 'tubesock', git: 'https://github.com/gosukiwi/tubesock', branch: 'patch-1' #
gem 'faye-websocket' gem 'faye-websocket'
gem 'eventmachine', '1.0.9.1' # explicitly added, this is used by faye-websocket, newer versions might crash or gem 'eventmachine', '1.0.9.1' # explicitly added, this is used by faye-websocket, newer versions might crash or
gem 'nokogiri' gem 'nokogiri'
gem 'd3-rails'
gem 'webpacker' gem 'webpacker'
gem 'rest-client' gem 'rest-client'
gem 'rubyzip' gem 'rubyzip'
gem 'mnemosyne-ruby' gem 'mnemosyne-ruby'
gem 'whenever', require: false gem 'whenever', require: false
gem 'rails-timeago'
group :development, :staging do group :development, :staging do
gem 'bootsnap', require: false gem 'bootsnap', require: false

View File

@ -10,49 +10,49 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
ZenTest (4.11.1) ZenTest (4.11.2)
actioncable (5.2.1.1) actioncable (5.2.2)
actionpack (= 5.2.1.1) actionpack (= 5.2.2)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailer (5.2.1.1) actionmailer (5.2.2)
actionpack (= 5.2.1.1) actionpack (= 5.2.2)
actionview (= 5.2.1.1) actionview (= 5.2.2)
activejob (= 5.2.1.1) activejob (= 5.2.2)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (5.2.1.1) actionpack (5.2.2)
actionview (= 5.2.1.1) actionview (= 5.2.2)
activesupport (= 5.2.1.1) activesupport (= 5.2.2)
rack (~> 2.0) rack (~> 2.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.1.1) actionview (5.2.2)
activesupport (= 5.2.1.1) activesupport (= 5.2.2)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3) rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.2.1.1) activejob (5.2.2)
activesupport (= 5.2.1.1) activesupport (= 5.2.2)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (5.2.1.1) activemodel (5.2.2)
activesupport (= 5.2.1.1) activesupport (= 5.2.2)
activerecord (5.2.1.1) activerecord (5.2.2)
activemodel (= 5.2.1.1) activemodel (= 5.2.2)
activesupport (= 5.2.1.1) activesupport (= 5.2.2)
arel (>= 9.0) arel (>= 9.0)
activestorage (5.2.1.1) activestorage (5.2.2)
actionpack (= 5.2.1.1) actionpack (= 5.2.2)
activerecord (= 5.2.1.1) activerecord (= 5.2.2)
marcel (~> 0.3.1) marcel (~> 0.3.1)
activesupport (5.2.1.1) activesupport (5.2.2)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
minitest (~> 5.1) minitest (~> 5.1)
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.5.2) addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0) public_suffix (>= 2.0.2, < 4.0)
airbrussh (1.3.1) airbrussh (1.3.1)
sshkit (>= 1.6.1, != 1.7.0) sshkit (>= 1.6.1, != 1.7.0)
@ -62,29 +62,28 @@ GEM
autotest-rails (4.2.1) autotest-rails (4.2.1)
ZenTest (~> 4.5) ZenTest (~> 4.5)
bcrypt (3.1.12) bcrypt (3.1.12)
better_errors (2.5.0) better_errors (2.5.1)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
bindex (0.5.0) bindex (0.5.0)
binding_of_caller (0.8.0) binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.3.2) bootsnap (1.4.1)
msgpack (~> 1.0) msgpack (~> 1.0)
bootstrap-will_paginate (1.0.0) bootstrap-will_paginate (1.0.0)
will_paginate will_paginate
builder (3.2.3) builder (3.2.3)
bunny (2.12.0) bunny (2.14.1)
amq-protocol (~> 2.3, >= 2.3.0) amq-protocol (~> 2.3, >= 2.3.0)
byebug (10.0.2) byebug (11.0.0)
capistrano (3.11.0) capistrano (3.11.0)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
sshkit (>= 1.9.0) sshkit (>= 1.9.0)
capistrano-bundler (1.4.0) capistrano-bundler (1.5.0)
capistrano (~> 3.1) capistrano (~> 3.1)
sshkit (~> 1.2)
capistrano-rails (1.4.0) capistrano-rails (1.4.0)
capistrano (~> 3.1) capistrano (~> 3.1)
capistrano-bundler (~> 1.1) capistrano-bundler (~> 1.1)
@ -97,7 +96,7 @@ GEM
capistrano (~> 3.7) capistrano (~> 3.7)
capistrano-bundler capistrano-bundler
puma (~> 3.4) puma (~> 3.4)
capybara (3.10.1) capybara (3.14.0)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (~> 1.8) nokogiri (~> 1.8)
@ -105,7 +104,7 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (~> 1.2) regexp_parser (~> 1.2)
xpath (~> 3.2) xpath (~> 3.2)
carrierwave (1.2.3) carrierwave (1.3.1)
activemodel (>= 4.0.0) activemodel (>= 4.0.0)
activesupport (>= 4.0.0) activesupport (>= 4.0.0)
mime-types (>= 1.16) mime-types (>= 1.16)
@ -113,10 +112,8 @@ GEM
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2) chronic (0.10.2)
coderay (1.1.2) coderay (1.1.2)
concurrent-ruby (1.1.3) concurrent-ruby (1.1.4)
crass (1.0.4) crass (1.0.4)
d3-rails (5.7.0)
railties (>= 3.1)
database_cleaner (1.7.0) database_cleaner (1.7.0)
debug_inspector (0.0.3) debug_inspector (0.0.3)
diff-lcs (1.3) diff-lcs (1.3)
@ -126,34 +123,36 @@ GEM
multi_json multi_json
domain_name (0.5.20180417) domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
erubi (1.7.1) erubi (1.8.0)
eventmachine (1.0.9.1) eventmachine (1.0.9.1)
excon (0.62.0) excon (0.62.0)
execjs (2.7.0) execjs (2.7.0)
factory_bot (4.11.1) factory_bot (5.0.2)
activesupport (>= 3.0.0) activesupport (>= 4.2.0)
factory_bot_rails (4.11.1) factory_bot_rails (5.0.1)
factory_bot (~> 4.11.1) factory_bot (~> 5.0.0)
railties (>= 3.0.0) railties (>= 4.2.0)
faraday (0.15.3) faraday (0.15.4)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faye-websocket (0.10.7) faye-websocket (0.10.7)
eventmachine (>= 0.12.0) eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1) websocket-driver (>= 0.5.1)
ffi (1.9.25) ffi (1.10.0)
forgery (0.7.0) forgery (0.7.0)
globalid (0.4.1) globalid (0.4.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
headless (2.3.1) headless (2.3.1)
highline (2.0.0) highline (2.0.1)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
i18n (1.1.1) i18n (1.6.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
i18n-js (3.2.1)
i18n (>= 0.6.6)
ims-lti (1.2.2) ims-lti (1.2.2)
builder builder
oauth (>= 0.4.5, < 0.6) oauth (>= 0.4.5, < 0.6)
jaro_winkler (1.5.1) jaro_winkler (1.5.2)
jbuilder (2.8.0) jbuilder (2.8.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
multi_json (>= 1.2) multi_json (>= 1.2)
@ -161,9 +160,9 @@ GEM
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (2.1.0) json (2.2.0)
jwt (2.1.0) jwt (2.1.0)
kramdown (1.17.0) kramdown (2.1.0)
listen (3.1.5) listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
@ -179,25 +178,25 @@ GEM
mime-types (3.2.2) mime-types (3.2.2)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812) mime-types-data (3.2018.0812)
mimemagic (0.3.2) mimemagic (0.3.3)
mini_mime (1.0.1) mini_mime (1.0.1)
mini_portile2 (2.3.0) mini_portile2 (2.4.0)
minitest (5.11.3) minitest (5.11.3)
mnemosyne-ruby (1.5.1) mnemosyne-ruby (1.5.1)
activesupport (>= 4) activesupport (>= 4)
bunny bunny
msgpack (1.2.4) msgpack (1.2.7)
multi_json (1.13.1) multi_json (1.13.1)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
net-scp (1.2.1) net-scp (1.2.1)
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (5.0.2) net-ssh (5.1.0)
netrc (0.11.0) netrc (0.11.0)
newrelic_rpm (5.4.0.347) newrelic_rpm (6.1.0.352)
nio4r (2.3.1) nio4r (2.3.1)
nokogiri (1.8.5) nokogiri (1.10.1)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.4.0)
nyan-cat-formatter (0.12.0) nyan-cat-formatter (0.12.0)
rspec (>= 2.99, >= 2.14.2, < 4) rspec (>= 2.99, >= 2.14.2, < 4)
oauth (0.5.4) oauth (0.5.4)
@ -209,70 +208,74 @@ GEM
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
pagedown-bootstrap-rails (2.1.4) pagedown-bootstrap-rails (2.1.4)
railties (> 3.1) railties (> 3.1)
parallel (1.12.1) parallel (1.14.0)
parser (2.5.3.0) parser (2.6.0.0)
ast (~> 2.4.0) ast (~> 2.4.0)
pg (1.1.3) pg (1.1.4)
powerpack (0.1.2) powerpack (0.1.2)
pry (0.12.0) pry (0.12.2)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.9.0)
pry-byebug (3.6.0) pry-byebug (3.7.0)
byebug (~> 10.0) byebug (~> 11.0)
pry (~> 0.10) pry (~> 0.10)
psych (3.1.0)
public_suffix (3.0.3) public_suffix (3.0.3)
puma (3.12.0) puma (3.12.0)
pundit (2.0.0) pundit (2.0.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
rack (2.0.6) rack (2.0.6)
rack-mini-profiler (1.0.0) rack-mini-profiler (1.0.2)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-proxy (0.6.5) rack-proxy (0.6.5)
rack rack
rack-test (1.1.0) rack-test (1.1.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (5.2.1.1) rails (5.2.2)
actioncable (= 5.2.1.1) actioncable (= 5.2.2)
actionmailer (= 5.2.1.1) actionmailer (= 5.2.2)
actionpack (= 5.2.1.1) actionpack (= 5.2.2)
actionview (= 5.2.1.1) actionview (= 5.2.2)
activejob (= 5.2.1.1) activejob (= 5.2.2)
activemodel (= 5.2.1.1) activemodel (= 5.2.2)
activerecord (= 5.2.1.1) activerecord (= 5.2.2)
activestorage (= 5.2.1.1) activestorage (= 5.2.2)
activesupport (= 5.2.1.1) activesupport (= 5.2.2)
bundler (>= 1.3.0) bundler (>= 1.3.0)
railties (= 5.2.1.1) railties (= 5.2.2)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.2) rails-controller-testing (1.0.4)
actionpack (~> 5.x, >= 5.0.1) actionpack (>= 5.0.1.x)
actionview (~> 5.x, >= 5.0.1) actionview (>= 5.0.1.x)
activesupport (~> 5.x) activesupport (>= 5.0.1.x)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4) rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2) loofah (~> 2.2, >= 2.2.2)
rails-i18n (5.1.2) rails-i18n (5.1.3)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 5.0, < 6) railties (>= 5.0, < 6)
railties (5.2.1.1) rails-timeago (2.17.1)
actionpack (= 5.2.1.1) actionpack (>= 3.1)
activesupport (= 5.2.1.1) activesupport (>= 3.1)
railties (5.2.2)
actionpack (= 5.2.2)
activesupport (= 5.2.2)
method_source method_source
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0) thor (>= 0.19.0, < 2.0)
rainbow (3.0.0) rainbow (3.0.0)
rake (12.3.1) rake (12.3.2)
ransack (2.1.0) ransack (2.1.1)
actionpack (>= 5.0) actionpack (>= 5.0)
activerecord (>= 5.0) activerecord (>= 5.0)
activesupport (>= 5.0) activesupport (>= 5.0)
i18n i18n
rb-fsevent (0.10.3) rb-fsevent (0.10.3)
rb-inotify (0.9.10) rb-inotify (0.10.0)
ffi (>= 0.5.0, < 2) ffi (~> 1.0)
regexp_parser (1.2.0) regexp_parser (1.3.0)
rest-client (2.0.2) rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
@ -291,7 +294,7 @@ GEM
rspec-mocks (3.8.0) rspec-mocks (3.8.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.8.0) rspec-support (~> 3.8.0)
rspec-rails (3.8.1) rspec-rails (3.8.2)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
@ -300,15 +303,16 @@ GEM
rspec-mocks (~> 3.8.0) rspec-mocks (~> 3.8.0)
rspec-support (~> 3.8.0) rspec-support (~> 3.8.0)
rspec-support (3.8.0) rspec-support (3.8.0)
rubocop (0.60.0) rubocop (0.65.0)
jaro_winkler (~> 1.5.1) jaro_winkler (~> 1.5.1)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.5, != 2.5.1.1) parser (>= 2.5, != 2.5.1.1)
powerpack (~> 0.1) powerpack (~> 0.1)
psych (>= 3.1.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.4.0) unicode-display_width (~> 1.4.0)
rubocop-rspec (1.30.1) rubocop-rspec (1.32.0)
rubocop (>= 0.60.0) rubocop (>= 0.60.0)
ruby-progressbar (1.10.0) ruby-progressbar (1.10.0)
ruby_dep (1.5.0) ruby_dep (1.5.0)
@ -316,7 +320,7 @@ GEM
json (~> 2.1) json (~> 2.1)
structured_warnings (~> 0.3) structured_warnings (~> 0.3)
rubyzip (1.2.2) rubyzip (1.2.2)
sass (3.6.0) sass (3.7.3)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
@ -342,7 +346,7 @@ GEM
actionpack (>= 3.1) actionpack (>= 3.1)
railties (>= 3.1) railties (>= 3.1)
slim (>= 3.0, < 5.0) slim (>= 3.0, < 5.0)
sorcery (0.12.0) sorcery (0.13.0)
bcrypt (~> 3.1) bcrypt (~> 3.1)
oauth (~> 0.4, >= 0.4.4) oauth (~> 0.4, >= 0.4.4)
oauth2 (~> 1.0, >= 0.8.0) oauth2 (~> 1.0, >= 0.8.0)
@ -355,31 +359,31 @@ GEM
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.18.0) sshkit (1.18.2)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
structured_warnings (0.3.0) structured_warnings (0.3.0)
temple (0.8.0) temple (0.8.1)
thor (0.20.3) thor (0.20.3)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.8) tilt (2.0.9)
turbolinks (5.2.0) turbolinks (5.2.0)
turbolinks-source (~> 5.2) turbolinks-source (~> 5.2)
turbolinks-source (5.2.0) turbolinks-source (5.2.0)
tzinfo (1.2.5) tzinfo (1.2.5)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (4.1.19) uglifier (4.1.20)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.5) unf_ext (0.0.7.5)
unicode-display_width (1.4.0) unicode-display_width (1.4.1)
web-console (3.7.0) web-console (3.7.0)
actionview (>= 5.0) actionview (>= 5.0)
activemodel (>= 5.0) activemodel (>= 5.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 5.0) railties (>= 5.0)
webpacker (3.5.5) webpacker (4.0.2)
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
@ -411,7 +415,6 @@ DEPENDENCIES
capybara capybara
carrierwave carrierwave
concurrent-ruby concurrent-ruby
d3-rails
database_cleaner database_cleaner
docker-api docker-api
eventmachine (= 1.0.9.1) eventmachine (= 1.0.9.1)
@ -420,6 +423,7 @@ DEPENDENCIES
forgery forgery
headless headless
highline highline
i18n-js
ims-lti (< 2.0.0) ims-lti (< 2.0.0)
jbuilder jbuilder
jquery-rails jquery-rails
@ -435,9 +439,10 @@ DEPENDENCIES
puma puma
pundit pundit
rack-mini-profiler rack-mini-profiler
rails (= 5.2.1.1) rails (= 5.2.2)
rails-controller-testing rails-controller-testing
rails-i18n rails-i18n
rails-timeago
ransack ransack
rest-client rest-client
rspec-autotest rspec-autotest
@ -460,4 +465,4 @@ DEPENDENCIES
whenever whenever
BUNDLED WITH BUNDLED WITH
1.17.1 1.17.2

View File

@ -13,7 +13,10 @@
//= require jquery_ujs //= require jquery_ujs
//= require turbolinks //= require turbolinks
//= require pagedown_bootstrap //= require pagedown_bootstrap
//= require d3 //= require rails-timeago
//= require locales/jquery.timeago.de.js
//= require i18n
//= require i18n/translations
// //
// lib/assets // lib/assets
//= require flash //= require flash

View File

@ -23,3 +23,14 @@ $.fn.scrollTo = function(selector) {
scrollTop: $(selector).offset().top - $(this).offset().top + $(this).scrollTop() scrollTop: $(selector).offset().top - $(this).offset().top + $(this).scrollTop()
}, ANIMATION_DURATION); }, ANIMATION_DURATION);
}; };
// Same as $.replaceWith, just returns the new element instead of the deleted one
$.fn.replaceWithAndReturnNewElement = function(a) {
const $a = $(a);
this.replaceWith($a);
return $a;
};
// Disable the use of web workers for JStree due to JS error
// See https://github.com/vakata/jstree/issues/1717 for details
$.jstree.defaults.core.worker = false;

View File

@ -0,0 +1,13 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
//
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);

View File

@ -0,0 +1,250 @@
$(document).on('turbolinks:load', function() {
if ($.isController('exercises') && $('.teacher_dashboard').isPresent()) {
const exercise_id = $('.teacher_dashboard').data().exerciseId;
const study_group_id = $('.teacher_dashboard').data().studyGroupId;
const specific_channel = { channel: "LaExercisesChannel", exercise_id: exercise_id, study_group_id: study_group_id };
App.la_exercise = App.cable.subscriptions.create(specific_channel, {
connected: function () {
// Called when the subscription is ready for use on the server
},
disconnected: function () {
// Called when the subscription has been terminated by the server
},
received: function (data) {
// Called when there's incoming data on the websocket for this channel
if (data.type === 'rfc') {
handleNewRfCdata(data);
} else if (data.type === 'working_times') {
handleWorkingTimeUpdate(data.working_time_data)
}
}
});
function handleNewRfCdata(data) {
let $row = $('tr[data-id="' + data.id + '"]');
if ($row.length === 0) {
$row = $($('#posted_rfcs')[0].insertRow(0));
}
$row = $row.replaceWithAndReturnNewElement(data.html);
$row.find('time').timeago();
$row.click(function () {
Turbolinks.visit($(this).data("href"));
});
}
function handleWorkingTimeUpdate(data) {
const user_progress = data['user_progress'];
const additional_user_data = data['additional_user_data'];
const user = additional_user_data[additional_user_data.length - 1][0];
const position = userPosition[user.type + user.id]; // TODO validate: will result in undef. if not existent.
// TODO: Do update
}
const graph_data = $('#initial_graph_data').data('graph_data');
let userPosition = {};
drawGraph(graph_data);
function drawGraph(graph_data) {
const user_progress = graph_data['user_progress'];
const additional_user_data = graph_data['additional_user_data'];
function get_minutes (time_stamp) {
try {
hours = time_stamp.split(":")[0];
minutes = time_stamp.split(":")[1];
seconds = time_stamp.split(":")[2];
seconds /= 60;
minutes = parseFloat(hours * 60) + parseInt(minutes) + seconds;
if (minutes > 0){
return minutes;
} else{
return parseFloat(seconds/60);
}
} catch (err) {
return 0;
}
}
function learners_name(index) {
return additional_user_data[additional_user_data.length - 1][index]["name"] + ", ID: " + additional_user_data[additional_user_data.length - 1][index]["id"];
}
function learners_time(group, index) {
if (user_progress[group] !== null && user_progress[group] !== undefined && user_progress[group][index] !== null) {
return user_progress[group][index]
} else {
return 0;
}
}
if (user_progress.length === 0) {
// No data available
$('#no_chart_data').removeClass("d-none");
return;
}
const margin = ({top: 20, right: 20, bottom: 150, left: 80});
const width = $('#chart_stacked').width();
const height = 500;
const users = user_progress[0].length; // # of users
const n = user_progress.length; // # of different sub bars, called buckets
let working_times_in_minutes = d3.range(n).map((index) => {
if (user_progress[index] !== null) {
return user_progress[index].map((time) => get_minutes(time))
} else return new Array(users).fill(0);
});
let xAxis = svg => svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickSizeOuter(0).tickFormat((index) => learners_name(index)));
let yAxis = svg => svg.append("g")
.attr("transform", `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y).tickSizeOuter(0).tickFormat((index) => index));
let color = d3.scaleSequential(d3.interpolateRdYlGn)
.domain([-0.5 * n, 1.5 * n]);
let userAxis = d3.range(users); // the x-values shared by all series
// Calculate the corresponding start and end value of each value;
const yBarValuesGrouped = d3.stack()
.keys(d3.range(n))
(d3.transpose(working_times_in_minutes)) // stacked working_times_in_minutes
.map((data, i) => data.map(([y0, y1]) => [y0, y1, i]));
const maxYSingleBar = d3.max(working_times_in_minutes, y => d3.max(y));
const maxYBarStacked = d3.max(yBarValuesGrouped, y => d3.max(y, d => d[1]));
let x = d3.scaleBand()
.domain(userAxis)
.rangeRound([margin.left, width - margin.right])
.padding(0.08);
let y = d3.scaleLinear()
.domain([0, maxYBarStacked])
.range([height - margin.bottom, margin.top]);
const svg = d3.select("#chart_stacked")
.append("svg")
.attr("width", '100%')
.attr("height", '100%')
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("preserveAspectRatio","xMinYMin meet");
const rect = svg.selectAll("g")
.data(yBarValuesGrouped)
.enter().append("g")
.attr("fill", (d, i) => color(i))
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", (d, i) => x(i))
.attr("y", height - margin.bottom)
.attr("width", x.bandwidth())
.attr("height", 0)
.attr("class", (d) => "bar-stacked-"+d[2]);
svg.append("g")
.attr("class", "x axis")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", function(d) {
return "rotate(-45)"
});
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Y Axis Label
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("x", (-height - margin.top + margin.bottom) / 2)
.attr("dy", "+2em")
.style("text-anchor", "middle")
.text(I18n.t('exercises.study_group_dashboard.time_spent_in_minutes'))
.style('font-size', 14);
// X Axis Label
svg.append("text")
.attr("class", "x axis")
.attr("text-anchor", "middle")
.attr("x", (width + margin.left - margin.right) / 2)
.attr("y", height)
.attr("dy", '-1em')
.text(I18n.t('exercises.study_group_dashboard.learner'))
.style('font-size', 14);
let tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d, i, a) {
return "<strong>Student: </strong><span style='color:orange'>" + learners_name(i) + "</span><br/>" +
"0: " + learners_time(0, i) + "<br/>" +
"1: " + learners_time(1, i) + "<br/>" +
"2: " + learners_time(2, i) + "<br/>" +
"3: " + learners_time(3, i) + "<br/>" +
"4: " + learners_time(4, i);
});
svg.call(tip);
rect.on('mouseenter', tip.show)
.on('mouseout', tip.hide);
function transitionGrouped() {
// Show all sub-bars next to each other
y.domain([0, maxYSingleBar]);
rect.transition()
.duration(500)
.delay((d, i) => i * 20)
.attr("x", (d, i) => x(i) + x.bandwidth() / n * d[2])
.attr("width", x.bandwidth() / n)
.transition()
.attr("y", d => y(d[1] - d[0]))
.attr("height", d => y(0) - y(d[1] - d[0]));
}
function transitionStacked() {
// Show all sub-bars on top of each other
y.domain([0, maxYBarStacked]);
rect.transition()
.duration(500)
.delay((d, i) => i * 20)
.attr("y", d => y(d[1]))
.attr("height", d => y(d[0]) - y(d[1]))
.transition()
.attr("x", (d, i) => x(i))
.attr("width", x.bandwidth());
}
$('#no_chart_data').addClass("d-none");
transitionStacked();
// ToDo: Add button to switch using transitionGrouped();
buildDictionary(additional_user_data);
}
function buildDictionary(users) {
users[users.length - 1].forEach(function(user, index) {
userPosition[user.type + user.id] = index;
});
}
}
});

View File

@ -113,3 +113,7 @@ span.caret {
-webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
} }
} }
.table-row-clickable {
cursor: pointer;
}

View File

@ -84,6 +84,10 @@ div#chart_2 {
background-color: #FAFAFA; background-color: #FAFAFA;
} }
div#chart_stacked {
max-height: 500px;
background-color: #FAFAFA;
}
a.file-heading { a.file-heading {
color: black !important; color: black !important;
@ -115,7 +119,7 @@ a.file-heading {
.d3-tip:after { .d3-tip:after {
box-sizing: border-box; box-sizing: border-box;
display: inline; display: inline;
font-size: 10px; font-size: 14px;
width: 100%; width: 100%;
line-height: 1; line-height: 1;
color: rgba(0, 0, 0, 0.8); color: rgba(0, 0, 0, 0.8);
@ -126,7 +130,7 @@ a.file-heading {
/* Style northward tooltips differently */ /* Style northward tooltips differently */
.d3-tip.n:after { .d3-tip.n:after {
margin: -1px 0 0 0; margin: -3px 0 0 0;
top: 100%; top: 100%;
left: 0; left: 0;
} }

View File

@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@ -0,0 +1,30 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
def disconnect
# Any cleanup work needed when the cable connection is cut.
end
private
def session
# `session` is not available here, so that we need to use `cookies.encrypted` instead
cookies.encrypted[Rails.application.config.session_options[:key]].symbolize_keys
end
def find_verified_user
# Finding the current_user is similar to the code used in application_controller.rb#current_user
current_user = ExternalUser.find_by(id: session[:external_user_id]) || InternalUser.find_by(id: session[:user_id])
if current_user
current_user
else
reject_unauthorized_connection
end
end
end
end

View File

@ -0,0 +1,16 @@
class LaExercisesChannel < ApplicationCable::Channel
def subscribed
stream_from specific_channel
end
def unsubscribed
stop_all_streams
end
private
def specific_channel
reject unless StudyGroupPolicy.new(current_user, StudyGroup.find_by(id: params[:study_group_id])).stream_la?
"la_exercises_#{params[:exercise_id]}_channel_study_group_#{params[:study_group_id]}"
end
end

View File

@ -168,6 +168,7 @@ module Lti
end end
group.users |= [@current_user] # add current user if not already member of the group group.users |= [@current_user] # add current user if not already member of the group
group.save group.save
session[:study_group_id] = group.id
end end
def set_embedding_options def set_embedding_options

View File

@ -16,7 +16,8 @@ module SubmissionParameters
current_user_id = current_user.id current_user_id = current_user.id
current_user_class_name = current_user.class.name current_user_class_name = current_user.class.name
end end
submission_params = params[:submission].present? ? params[:submission].permit(:cause, :exercise_id, files_attributes: file_attributes).merge(user_id: current_user_id, user_type: current_user_class_name) : {} # The study_group_id might not be present in the session (e.g. for internal users), resulting in session[:study_group_id] = nil which is intended.
submission_params = params[:submission].present? ? params[:submission].permit(:cause, :exercise_id, files_attributes: file_attributes).merge(user_id: current_user_id, user_type: current_user_class_name, study_group_id: session[:study_group_id]) : {}
reject_illegal_file_attributes!(submission_params) reject_illegal_file_attributes!(submission_params)
submission_params submission_params
end end

View File

@ -40,7 +40,7 @@ class ExecutionEnvironmentsController < ApplicationController
FROM FROM
(SELECT user_id, (SELECT user_id,
exercise_id, exercise_id,
CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
FROM FROM
(SELECT user_id, (SELECT user_id,
exercise_id, exercise_id,

View File

@ -7,7 +7,7 @@ class ExercisesController < ApplicationController
before_action :handle_file_uploads, only: [:create, :update] before_action :handle_file_uploads, only: [:create, :update]
before_action :set_execution_environments, only: [:create, :edit, :new, :update] before_action :set_execution_environments, only: [:create, :edit, :new, :update]
before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback] before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload, :feedback, :study_group_dashboard]
before_action :set_external_user_and_authorize, only: [:statistics] before_action :set_external_user_and_authorize, only: [:statistics]
before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_file_types, only: [:create, :edit, :new, :update]
before_action :set_course_token, only: [:implement] before_action :set_course_token, only: [:implement]
@ -266,7 +266,7 @@ class ExercisesController < ApplicationController
end end
def index def index
@search = policy_scope(Exercise).search(params[:q]) @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])
authorize! authorize!
end end
@ -319,7 +319,7 @@ class ExercisesController < ApplicationController
private :set_file_types private :set_file_types
def collect_set_and_unset_exercise_tags def collect_set_and_unset_exercise_tags
@search = policy_scope(Tag).search(params[:q]) @search = policy_scope(Tag).ransack(params[:q])
@tags = @search.result.order(:name) @tags = @search.result.order(:name)
checked_exercise_tags = @exercise.exercise_tags checked_exercise_tags = @exercise.exercise_tags
checked_tags = checked_exercise_tags.collect{|e| e.tag}.to_set checked_tags = checked_exercise_tags.collect{|e| e.tag}.to_set
@ -343,7 +343,7 @@ class ExercisesController < ApplicationController
@all_events = (@submissions + interventions).sort_by { |a| a.created_at } @all_events = (@submissions + interventions).sort_by { |a| a.created_at }
@deltas = @all_events.map.with_index do |item, index| @deltas = @all_events.map.with_index do |item, index|
delta = item.created_at - @all_events[index - 1].created_at if index > 0 delta = item.created_at - @all_events[index - 1].created_at if index > 0
if delta == nil or delta > 10 * 60 then 0 else delta end if delta == nil or delta > StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS then 0 else delta end
end end
@working_times_until = [] @working_times_until = []
@all_events.each_with_index do |_, index| @all_events.each_with_index do |_, index|
@ -475,4 +475,15 @@ class ExercisesController < ApplicationController
end end
end end
def study_group_dashboard
authorize!
@study_group_id = params[:study_group_id]
@request_for_comments = RequestForComment.
where(exercise: @exercise).includes(:submission).
where(submissions: {study_group_id: @study_group_id}).
order(created_at: :desc)
@graph_data = @exercise.get_working_times_for_study_group(@study_group_id)
end
end end

View File

@ -27,7 +27,7 @@ class ExternalUsersController < ApplicationController
score, score,
id, id,
CASE CASE
WHEN working_time >= '0:05:00' THEN '0' WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0'
ELSE working_time ELSE working_time
END AS working_time_new END AS working_time_new
FROM FROM

View File

@ -60,7 +60,7 @@ class InternalUsersController < ApplicationController
end end
def index def index
@search = InternalUser.search(params[:q]) @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])
authorize! authorize!
end end

View File

@ -33,7 +33,7 @@ class ProxyExercisesController < ApplicationController
end end
def edit def edit
@search = policy_scope(Exercise).search(params[:q]) @search = policy_scope(Exercise).ransack(params[:q])
@exercises = @search.result.order(:title) @exercises = @search.result.order(:title)
authorize! authorize!
end end
@ -44,14 +44,14 @@ class ProxyExercisesController < ApplicationController
private :proxy_exercise_params private :proxy_exercise_params
def index def index
@search = policy_scope(ProxyExercise).search(params[:q]) @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])
authorize! authorize!
end end
def new def new
@proxy_exercise = ProxyExercise.new @proxy_exercise = ProxyExercise.new
@search = policy_scope(Exercise).search(params[:q]) @search = policy_scope(Exercise).ransack(params[:q])
@exercises = @search.result.order(:title) @exercises = @search.result.order(:title)
authorize! authorize!
end end
@ -63,8 +63,8 @@ class ProxyExercisesController < ApplicationController
private :set_exercise_and_authorize private :set_exercise_and_authorize
def show def show
@search = @proxy_exercise.exercises.search @search = @proxy_exercise.exercises.ransack
@exercises = @proxy_exercise.exercises.search.result.order(:title) #@search.result.order(:title) @exercises = @proxy_exercise.exercises.ransack.result.order(:title) #@search.result.order(:title)
end end
#we might want to think about auth here #we might want to think about auth here

View File

@ -15,7 +15,7 @@ class RequestForCommentsController < ApplicationController
@search = RequestForComment @search = RequestForComment
.last_per_user(2) .last_per_user(2)
.with_last_activity .with_last_activity
.search(params[:q]) .ransack(params[:q])
@request_for_comments = @search.result @request_for_comments = @search.result
.order('created_at DESC') .order('created_at DESC')
.paginate(page: params[:page], total_entries: @search.result.length) .paginate(page: params[:page], total_entries: @search.result.length)
@ -27,7 +27,7 @@ class RequestForCommentsController < ApplicationController
@search = RequestForComment @search = RequestForComment
.with_last_activity .with_last_activity
.where(user_id: current_user.id) .where(user_id: current_user.id)
.search(params[:q]) .ransack(params[:q])
@request_for_comments = @search.result @request_for_comments = @search.result
.order('created_at DESC') .order('created_at DESC')
.paginate(page: params[:page]) .paginate(page: params[:page])
@ -40,7 +40,7 @@ class RequestForCommentsController < ApplicationController
.with_last_activity .with_last_activity
.joins(:comments) # we don't need to outer join here, because we know the user has commented on these .joins(:comments) # we don't need to outer join here, because we know the user has commented on these
.where(comments: {user_id: current_user.id}) .where(comments: {user_id: current_user.id})
.search(params[:q]) .ransack(params[:q])
@request_for_comments = @search.result @request_for_comments = @search.result
.order('last_comment DESC') .order('last_comment DESC')
.paginate(page: params[:page]) .paginate(page: params[:page])
@ -83,17 +83,10 @@ class RequestForCommentsController < ApplicationController
authorize! authorize!
end end
# GET /request_for_comments/new
def new
@request_for_comment = RequestForComment.new
authorize!
end
# GET /request_for_comments/1/edit # GET /request_for_comments/1/edit
def edit def edit
end end
# POST /request_for_comments
# POST /request_for_comments.json # POST /request_for_comments.json
def create def create
# Consider all requests as JSON # Consider all requests as JSON
@ -149,7 +142,7 @@ class RequestForCommentsController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through. # Never trust parameters from the scary internet, only allow the white list through.
def request_for_comment_params def request_for_comment_params
# we are using the current_user.id here, since internal users are not able to create comments. The external_user.id is a primary key and does not require the consumer_id to be unique. # The study_group_id might not be present in the session (e.g. for internal users), resulting in session[:study_group_id] = nil which is intended.
params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name) params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name)
end end

View File

@ -4,17 +4,17 @@ class StudyGroupsController < ApplicationController
before_action :set_group, only: MEMBER_ACTIONS before_action :set_group, only: MEMBER_ACTIONS
def index def index
@search = StudyGroup.search(params[:q]) @search = 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])
authorize! authorize!
end end
def show def show
@search = @study_group.users.search(params[:q]) @search = @study_group.users.ransack(params[:q])
end end
def edit def edit
@search = @study_group.users.search(params[:q]) @search = @study_group.users.ransack(params[:q])
@members = StudyGroupMembership.where(user: @search.result, study_group: @study_group) @members = StudyGroupMembership.where(user: @search.result, study_group: @study_group)
end end

View File

@ -106,7 +106,7 @@ class SubmissionsController < ApplicationController
end end
def index def index
@search = Submission.search(params[:q]) @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])
authorize! authorize!
end end
@ -201,6 +201,8 @@ class SubmissionsController < ApplicationController
save_run_output save_run_output
if @run_output.blank? if @run_output.blank?
@raw_output ||= ''
@run_output ||= ''
parse_message t('exercises.implement.no_output', timestamp: l(Time.now, format: :short)), 'stdout', tubesock parse_message t('exercises.implement.no_output', timestamp: l(Time.now, format: :short)), 'stdout', tubesock
end end

View File

@ -0,0 +1,30 @@
module ActionCableHelper
def trigger_rfc_action_cable
# Context: RfC
if submission.study_group_id.present?
ActionCable.server.broadcast(
"la_exercises_#{exercise_id}_channel_study_group_#{submission.study_group_id}",
type: :rfc,
id: id,
html: (ApplicationController.render(partial: 'request_for_comments/list_entry',
locals: {request_for_comment: self})))
end
end
def trigger_rfc_action_cable_from_comment
# Context: Comment
RequestForComment.find_by(submission: file.context).trigger_rfc_action_cable
end
def trigger_working_times_action_cable
# Context: Submission
if study_group_id.present?
ActionCable.server.broadcast(
"la_exercises_#{exercise_id}_channel_study_group_#{study_group_id}",
type: :working_times,
working_time_data: exercise.get_working_times_for_study_group(study_group_id, user))
end
end
end
# TODO: Check if any user is connected and prevent preparing the data otherwise

View File

@ -1,5 +1,8 @@
module StatisticsHelper module StatisticsHelper
WORKING_TIME_DELTA_IN_SECONDS = 5.minutes
WORKING_TIME_DELTA_IN_SQL_INTERVAL = "'0:05:00'" # yes, a string with quotes
def statistics_data def statistics_data
[ [
{ {

View File

@ -13,7 +13,9 @@ import 'bootstrap/dist/js/bootstrap.bundle.min';
import 'chosen-js/chosen.jquery'; import 'chosen-js/chosen.jquery';
import 'jstree'; import 'jstree';
import 'underscore'; import 'underscore';
import 'd3'
window._ = _; // Publish underscore's `_` in global namespace window._ = _; // Publish underscore's `_` in global namespace
window.d3 = d3; // Publish d3 in global namespace
// CSS // CSS
import 'chosen-js/chosen.css'; import 'chosen-js/chosen.css';

View File

@ -1,8 +1,12 @@
class Comment < ApplicationRecord class Comment < ApplicationRecord
# inherit the creation module: encapsulates that this is a polymorphic user, offers some aliases and makes sure that all necessary attributes are set. # inherit the creation module: encapsulates that this is a polymorphic user, offers some aliases and makes sure that all necessary attributes are set.
include Creation include Creation
include ActionCableHelper
attr_accessor :username, :date, :updated, :editable attr_accessor :username, :date, :updated, :editable
belongs_to :file, class_name: 'CodeOcean::File' belongs_to :file, class_name: 'CodeOcean::File'
belongs_to :user, polymorphic: true belongs_to :user, polymorphic: true
after_save :trigger_rfc_action_cable_from_comment
end end

View File

@ -76,7 +76,7 @@ class Exercise < ApplicationRecord
(SELECT user_id, (SELECT user_id,
user_type, user_type,
score, score,
CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
FROM FROM
(SELECT user_id, (SELECT user_id,
user_type, user_type,
@ -90,6 +90,145 @@ class Exercise < ApplicationRecord
" "
end end
def study_group_working_time_query(exercise_id, study_group_id, additional_filter)
"""
WITH working_time_between_submissions AS (
SELECT submissions.user_id,
submissions.user_type,
score,
created_at,
(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}),
working_time_with_deltas_ignored AS (
SELECT user_id,
user_type,
score,
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
FROM working_time_between_submissions
),
working_times_with_score_expanded AS (
SELECT user_id,
user_type,
created_at,
working_time_filtered,
first_value(score)
over (PARTITION BY user_type, user_id, change_in_score ORDER BY created_at ASC) AS corrected_score
FROM working_time_with_deltas_ignored
),
working_times_with_duplicated_last_row_per_score AS (
SELECT *
FROM working_times_with_score_expanded
UNION ALL
-- Duplicate last row per user and score and make it unique by setting another created_at timestamp.
-- In addition, the working time is set to zero in order to prevent getting a wrong time.
-- This duplication is needed, as we will shift the scores and working times by one and need to ensure not to loose any information.
SELECT DISTINCT ON (user_type, user_id, corrected_score) user_id,
user_type,
created_at + INTERVAL '1us',
'00:00:00' as working_time_filtered,
corrected_score
FROM working_times_with_score_expanded
),
working_times_with_score_not_null_and_shifted AS (
SELECT user_id,
user_type,
coalesce(lag(corrected_score) over (PARTITION BY user_type, user_id ORDER BY created_at ASC),
0) AS shifted_score,
created_at,
working_time_filtered
FROM working_times_with_duplicated_last_row_per_score
),
working_times_to_be_sorted AS (
SELECT user_id,
user_type,
shifted_score AS score,
MIN(created_at) AS start_time,
SUM(working_time_filtered) AS working_time_per_score,
SUM(SUM(working_time_filtered)) over (PARTITION BY user_type, user_id) AS total_working_time
FROM working_times_with_score_not_null_and_shifted
GROUP BY user_id, user_type, score
),
working_times_with_index AS (
SELECT (dense_rank() over (ORDER BY total_working_time, user_type, user_id ASC) - 1) AS index,
user_id,
user_type,
score,
start_time,
working_time_per_score,
total_working_time
FROM working_times_to_be_sorted)
SELECT index,
user_id,
user_type,
name,
score,
start_time,
working_time_per_score,
total_working_time
FROM working_times_with_index
JOIN external_users ON user_type = 'ExternalUser' AND user_id = external_users.id
UNION ALL
SELECT index,
user_id,
user_type,
name,
score,
start_time,
working_time_per_score,
total_working_time
FROM working_times_with_index
JOIN internal_users ON user_type = 'InternalUser' AND user_id = internal_users.id
ORDER BY index, score ASC;
"""
end
def get_working_times_for_study_group(study_group_id, user = nil)
user_progress = []
additional_user_data = []
max_bucket = 4
maximum_score = self.maximum_score
if user.blank?
additional_filter = ''
else
additional_filter = "AND user_id = #{user.id} AND user_type = '#{user.class.name}'"
end
results = self.class.connection.execute(study_group_working_time_query(id, study_group_id, additional_filter)).each do |tuple|
if tuple['score'] <= maximum_score
bucket = tuple['score'] / maximum_score * max_bucket
else
bucket = max_bucket # maximum_score / maximum_score will always be 1
end
user_progress[bucket] ||= []
additional_user_data[bucket] ||= []
additional_user_data[max_bucket + 1] ||= []
user_progress[bucket][tuple['index']] = tuple["working_time_per_score"]
additional_user_data[bucket][tuple['index']] = {start_time: tuple["start_time"], score: tuple["score"]}
additional_user_data[max_bucket + 1][tuple['index']] = {id: tuple['user_id'], type: tuple['user_type'], name: tuple['name']}
end
if results.ntuples > 0
first_index = results[0]['index']
last_index = results[results.ntuples-1]['index']
buckets = last_index - first_index
user_progress.each do |timings_array|
if timings_array.present? && timings_array.length != buckets + 1
timings_array[buckets] = nil
end
end
end
{user_progress: user_progress, additional_user_data: additional_user_data}
end
def get_quantiles(quantiles) def get_quantiles(quantiles)
quantiles_str = "[" + quantiles.join(",") + "]" quantiles_str = "[" + quantiles.join(",") + "]"
result = self.class.connection.execute(""" result = self.class.connection.execute("""
@ -180,7 +319,7 @@ class Exercise < ApplicationRecord
exercise_id, exercise_id,
max_score, max_score,
CASE CASE
WHEN working_time >= '0:05:00' THEN '0' WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0'
ELSE working_time ELSE working_time
END AS working_time_new END AS working_time_new
FROM all_working_times_until_max ), result AS FROM all_working_times_until_max ), result AS
@ -274,7 +413,7 @@ class Exercise < ApplicationRecord
FILTERED_TIMES_UNTIL_MAX AS FILTERED_TIMES_UNTIL_MAX AS
( (
SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new 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
FROM ALL_WORKING_TIMES_UNTIL_MAX 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 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

View File

@ -1,5 +1,7 @@
class RequestForComment < ApplicationRecord class RequestForComment < ApplicationRecord
include Creation include Creation
include ActionCableHelper
belongs_to :submission belongs_to :submission
belongs_to :exercise belongs_to :exercise
belongs_to :file, class_name: 'CodeOcean::File' belongs_to :file, class_name: 'CodeOcean::File'
@ -10,6 +12,8 @@ class RequestForComment < ApplicationRecord
scope :unsolved, -> { where(solved: [false, nil]) } scope :unsolved, -> { where(solved: [false, nil]) }
scope :in_range, -> (from, to) { where(created_at: from..to) } scope :in_range, -> (from, to) { where(created_at: from..to) }
after_save :trigger_rfc_action_cable
def self.last_per_user(n = 5) def self.last_per_user(n = 5)
from("(#{row_number_user_sql}) as request_for_comments") from("(#{row_number_user_sql}) as request_for_comments")
.where("row_number <= ?", n) .where("row_number <= ?", n)

View File

@ -1,12 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
class StudyGroup < ApplicationRecord class StudyGroup < ApplicationRecord
has_many :study_group_memberships has_many :study_group_memberships, dependent: :destroy
# Use `ExternalUser` as `source_type` for now. # Use `ExternalUser` as `source_type` for now.
# Using `User` will lead ActiveRecord to access the inexistent table `users`. # Using `User` will lead ActiveRecord to access the inexistent table `users`.
# Issue created: https://github.com/rails/rails/issues/34531 # Issue created: https://github.com/rails/rails/issues/34531
has_many :users, through: :study_group_memberships, source_type: 'ExternalUser' has_many :users, through: :study_group_memberships, source_type: 'ExternalUser'
has_many :submissions has_many :submissions, dependent: :nullify
belongs_to :consumer belongs_to :consumer
def to_s def to_s

View File

@ -1,6 +1,7 @@
class Submission < ApplicationRecord class Submission < ApplicationRecord
include Context include Context
include Creation include Creation
include ActionCableHelper
CAUSES = %w(assess download file render run save submit test autosave requestComments remoteAssess) CAUSES = %w(assess download file render run save submit test autosave requestComments remoteAssess)
FILENAME_URL_PLACEHOLDER = '{filename}' FILENAME_URL_PLACEHOLDER = '{filename}'
@ -20,6 +21,8 @@ class Submission < ApplicationRecord
validates :cause, inclusion: {in: CAUSES} validates :cause, inclusion: {in: CAUSES}
validates :exercise_id, presence: true validates :exercise_id, presence: true
after_save :trigger_working_times_action_cable
MAX_COMMENTS_ON_RECOMMENDED_RFC = 5 MAX_COMMENTS_ON_RECOMMENDED_RFC = 5
def build_files_hash(files, attribute) def build_files_hash(files, attribute)

View File

@ -25,6 +25,22 @@ class ApplicationPolicy
end end
private :no_one private :no_one
def everyone_in_study_group
study_group = @record.study_group
return false if study_group.blank?
users_in_same_study_group = study_group.users
return false if users_in_same_study_group.blank?
users_in_same_study_group.include? @user
end
private :everyone_in_study_group
def teacher_in_study_group
teacher? && everyone_in_study_group
end
private :teacher_in_study_group
def initialize(user, record) def initialize(user, record)
@user = user @user = user
@record = record @record = record

View File

@ -3,8 +3,8 @@ class ExercisePolicy < AdminOrAuthorPolicy
admin? admin?
end end
def show? [:show?, :study_group_dashboard?].each do |action|
admin? || teacher? define_method(action) { admin? || teacher? }
end end
[:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?].each do |action| [:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?].each do |action|

View File

@ -3,8 +3,8 @@ class StudyGroupPolicy < AdminOnlyPolicy
admin? || teacher? admin? || teacher?
end end
[:show?, :destroy?, :edit?, :update?].each do |action| [:show?, :destroy?, :edit?, :update?, :stream_la?].each do |action|
define_method(action) { admin? || @user.teacher? && @record.users.include?(@user) } define_method(action) { admin? || @user.teacher? && @record.present? && @record.users.include?(@user) }
end end
class Scope < Scope class Scope < Scope

View File

@ -12,14 +12,8 @@ class SubmissionPolicy < ApplicationPolicy
admin? admin?
end end
def everyone_in_study_group
users_in_same_study_group = @record.study_groups.users
users_in_same_study_group.include? @user
end
private :everyone_in_study_group
def teacher_in_study_group def show_study_group?
teacher? && everyone_in_study_group admin? || teacher_in_study_group
end end
private :teacher_in_study_group
end end

View File

@ -58,7 +58,7 @@ h1 = "#{@exercise} (external user #{@external_user})"
td = td =
td = td =
td = @working_times_until[index] if index > 0 td = @working_times_until[index] if index > 0
p = t('.addendum') p = t('.addendum', delta: StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS / 60)
.d-none#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until); .d-none#wtimes data-working_times=ActiveSupport::JSON.encode(@working_times_until);
div#progress_chart.col-lg-12 div#progress_chart.col-lg-12
.graph-functions-2 .graph-functions-2

View File

@ -0,0 +1,37 @@
- content_for :head do
// Force a full page reload, see https://github.com/turbolinks/turbolinks/issues/326.
Otherwise, code might not be highlighted correctly (race condition)
meta name='turbolinks-visit-control' content='reload'
= javascript_pack_tag('d3-tip', 'data-turbolinks-track': true)
h1
= t('.live_dashboard')
div.teacher_dashboard data-exercise-id="#{@exercise.id}" data-study-group-id="#{@study_group_id}"
h4.mt-4
= t('.time_spent_per_learner')
.d-none#initial_graph_data data-graph_data=ActiveSupport::JSON.encode(@graph_data);
div.w-100#chart_stacked
.d-none.badge-info.container.py-2#no_chart_data
i class="fa fa-info" aria-hidden="true"
= t('.no_data_yet')
h4.mt-4
= t('.related_requests_for_comments')
.table-responsive
table.table.table-hover.mt-4
thead
tr
th.text-center
i.mr-0 class="fa fa-lightbulb-o" aria-hidden="true" title = t('request_for_comments.solved')
th.text-center
i.mr-0 class="fa fa-comment" aria-hidden="true" title = t('request_for_comments.comments') align="center"
th.col-12 = t('activerecord.attributes.request_for_comments.question')
th = t('activerecord.attributes.request_for_comments.username')
th.text-nowrap = t('activerecord.attributes.request_for_comments.requested_at')
tbody#posted_rfcs
= render(partial: 'request_for_comments/list_entry', collection: @request_for_comments, as: :request_for_comment)

View File

@ -5,6 +5,7 @@ html lang='en'
meta name='viewport' content='width=device-width, initial-scale=1' meta name='viewport' content='width=device-width, initial-scale=1'
title = application_name title = application_name
link href=asset_path('favicon.png') rel='icon' type='image/png' link href=asset_path('favicon.png') rel='icon' type='image/png'
= action_cable_meta_tag
= stylesheet_pack_tag('application', media: 'all', 'data-turbolinks-track': true) = stylesheet_pack_tag('application', media: 'all', 'data-turbolinks-track': true)
= stylesheet_pack_tag('stylesheets', media: 'all', 'data-turbolinks-track': true) = stylesheet_pack_tag('stylesheets', media: 'all', 'data-turbolinks-track': true)
= stylesheet_link_tag('application', media: 'all', 'data-turbolinks-track': true) = stylesheet_link_tag('application', media: 'all', 'data-turbolinks-track': true)
@ -12,6 +13,10 @@ html lang='en'
= javascript_include_tag('application', 'data-turbolinks-track': true) = javascript_include_tag('application', 'data-turbolinks-track': true)
= yield(:head) = yield(:head)
= csrf_meta_tags = csrf_meta_tags
= timeago_script_tag
script type="text/javascript"
| I18n.defaultLocale = "#{I18n.default_locale}";
| I18n.locale = "#{I18n.locale}";
body body
- unless @embed_options[:hide_navbar] - unless @embed_options[:hide_navbar]
nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4.py-1 role='navigation' nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4.py-1 role='navigation'

View File

@ -0,0 +1,15 @@
tr.table-row-clickable data-id=request_for_comment.id data-href=request_for_comment_path(request_for_comment)
td.p-2
- if request_for_comment.solved?
span.fa.fa-check.fa-2x.text-success aria-hidden="true"
- elsif request_for_comment.full_score_reached
span.fa.fa-check.fa-2x style="color:darkgrey" aria-hidden="true"
- else
= ''
td.text-center = request_for_comment.comments_count
- if request_for_comment.has_attribute?(:question) && request_for_comment.question.present?
td.text-primary = truncate(request_for_comment.question, length: 200)
- else
td.text-black-50.font-italic = t('request_for_comments.no_question')
td = request_for_comment.user
td = timeago_tag request_for_comment.created_at

View File

@ -9,6 +9,9 @@
- testruns = Testrun.where(:submission_id => @request_for_comment.submission) - testruns = Testrun.where(:submission_id => @request_for_comment.submission)
= link_to_if(policy(user).show?, user.displayname, user) = link_to_if(policy(user).show?, user.displayname, user)
| | #{@request_for_comment.created_at.localtime} | | #{@request_for_comment.created_at.localtime}
- if @request_for_comment.submission.study_group.present? && policy(@request_for_comment.submission).show_study_group?
= ' | '
= link_to_if(policy(@request_for_comment.submission.study_group).show?, @request_for_comment.submission.study_group, @request_for_comment.submission.study_group)
.rfc .rfc
.description .description
h5 h5

View File

@ -9,6 +9,7 @@ h1 = @submission
= row(label: 'submission.exercise', value: link_to_if(policy(@submission.exercise).show?, @submission.exercise, @submission.exercise)) = row(label: 'submission.exercise', value: link_to_if(policy(@submission.exercise).show?, @submission.exercise, @submission.exercise))
= row(label: 'submission.user', value: link_to_if(policy(@submission.user).show?, @submission.user, @submission.user)) = row(label: 'submission.user', value: link_to_if(policy(@submission.user).show?, @submission.user, @submission.user))
= row(label: 'submission.study_group', value: link_to_if(policy(@submission.study_group).show?, @submission.study_group, @submission.study_group))
= row(label: 'submission.cause', value: t("submissions.causes.#{@submission.cause}")) = row(label: 'submission.cause', value: t("submissions.causes.#{@submission.cause}"))
= row(label: 'submission.score', value: @submission.score) = row(label: 'submission.score', value: @submission.score)

View File

@ -1,5 +1,5 @@
== t('mailers.user_mailer.send_thank_you_note.body', == t('mailers.user_mailer.send_thank_you_note.body',
receiver_displayname: @receiver_displayname, receiver_displayname: @receiver_displayname,
link_to_comment: link_to(@rfc_link, @rfc_link), link_to_comment: link_to(@rfc_link, @rfc_link),
author: @author.displayname, author: @author,
thank_you_note: @thank_you_note ) thank_you_note: @thank_you_note )

70
babel.config.js Normal file
View File

@ -0,0 +1,70 @@
module.exports = function(api) {
var validEnv = ['development', 'test', 'production']
var currentEnv = api.env()
var isDevelopmentEnv = api.env('development')
var isProductionEnv = api.env('production')
var isTestEnv = api.env('test')
if (!validEnv.includes(currentEnv)) {
throw new Error(
'Please specify a valid `NODE_ENV` or ' +
'`BABEL_ENV` environment variables. Valid values are "development", ' +
'"test", and "production". Instead, received: ' +
JSON.stringify(currentEnv) +
'.'
)
}
return {
presets: [
isTestEnv && [
require('@babel/preset-env').default,
{
targets: {
node: 'current'
}
}
],
(isProductionEnv || isDevelopmentEnv) && [
require('@babel/preset-env').default,
{
forceAllTransforms: true,
useBuiltIns: 'entry',
modules: false,
exclude: ['transform-typeof-symbol']
}
]
].filter(Boolean),
plugins: [
require('babel-plugin-macros'),
require('@babel/plugin-syntax-dynamic-import').default,
isTestEnv && require('babel-plugin-dynamic-import-node'),
require('@babel/plugin-transform-destructuring').default,
[
require('@babel/plugin-proposal-class-properties').default,
{
loose: true
}
],
[
require('@babel/plugin-proposal-object-rest-spread').default,
{
useBuiltIns: true
}
],
[
require('@babel/plugin-transform-runtime').default,
{
helpers: false,
regenerator: true
}
],
[
require('@babel/plugin-transform-regenerator').default,
{
async: false
}
]
].filter(Boolean)
}
};

View File

@ -12,4 +12,8 @@ require "bundler/setup"
require "webpacker" require "webpacker"
require "webpacker/webpack_runner" require "webpacker/webpack_runner"
Webpacker::WebpackRunner.run(ARGV)
APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
Webpacker::WebpackRunner.run(ARGV)
end

View File

@ -12,4 +12,8 @@ require "bundler/setup"
require "webpacker" require "webpacker"
require "webpacker/dev_server_runner" require "webpacker/dev_server_runner"
Webpacker::DevServerRunner.run(ARGV)
APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
Webpacker::DevServerRunner.run(ARGV)
end

View File

@ -28,5 +28,7 @@ module CodeOcean
config.autoload_paths << Rails.root.join('lib') config.autoload_paths << Rails.root.join('lib')
config.eager_load_paths << Rails.root.join('lib') config.eager_load_paths << Rails.root.join('lib')
config.assets.precompile += %w( markdown-buttons.png ) config.assets.precompile += %w( markdown-buttons.png )
config.action_cable.mount_path = '/cable'
end end
end end

View File

@ -1,10 +1,14 @@
development: development:
adapter: async adapter: postgresql
test: test:
adapter: async adapter: postgresql
staging:
adapter: postgresql
production: production:
adapter: redis adapter: postgresql # redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> # all other options below are only used for redis
channel_prefix: code_ocean_production # url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
# channel_prefix: code_ocean_production

View File

@ -101,6 +101,7 @@ de:
files: Dateien files: Dateien
score: Punktzahl score: Punktzahl
user: Autor user: Autor
study_group: Lerngruppe
study_group: study_group:
name: Name name: Name
external_id: Externe ID external_id: Externe ID
@ -347,6 +348,7 @@ de:
implement: Implementieren implement: Implementieren
test_files: Test-Dateien test_files: Test-Dateien
feedback: Feedback feedback: Feedback
study_group_dashboard: Live Dashboard
statistics: statistics:
average_score: Durchschnittliche Punktzahl average_score: Durchschnittliche Punktzahl
final_submissions: Finale Abgaben final_submissions: Finale Abgaben
@ -365,6 +367,13 @@ de:
failure: Beim Übermitteln Ihrer Punktzahl ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut. failure: Beim Übermitteln Ihrer Punktzahl ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.
full_score_redirect_to_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ein anderer Teilnehmer hat eine Frage zu der von Ihnen gelösten Aufgabe. Er würde sich sicherlich sehr über ihre Hilfe und Kommentare freuen. full_score_redirect_to_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ein anderer Teilnehmer hat eine Frage zu der von Ihnen gelösten Aufgabe. Er würde sich sicherlich sehr über ihre Hilfe und Kommentare freuen.
full_score_redirect_to_own_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ihre Frage ist damit wahrscheinlich gelöst? Falls ja, fügen Sie doch den entscheidenden Kniff als Antwort hinzu und markieren die Frage als gelöst, bevor sie das Fenster schließen. full_score_redirect_to_own_rfc: Herzlichen Glückwunsch! Sie haben die maximale Punktzahl für diese Aufgabe an den Kurs übertragen. Ihre Frage ist damit wahrscheinlich gelöst? Falls ja, fügen Sie doch den entscheidenden Kniff als Antwort hinzu und markieren die Frage als gelöst, bevor sie das Fenster schließen.
study_group_dashboard:
live_dashboard: Live Dashboard
time_spent_per_learner: Verwendete Zeit pro Lerner
related_requests_for_comments: Zugehörige Kommentaranfragen
learner: Lerner
time_spent_in_minutes: benötigte Zeit in Minuten
no_data_yet: Bisher sind keine Daten verfügbar
external_users: external_users:
statistics: statistics:
no_data_available: Keine Daten verfügbar. no_data_available: Keine Daten verfügbar.
@ -373,7 +382,7 @@ de:
score: Punktzahl score: Punktzahl
tests: Unit Tests tests: Unit Tests
time_difference: 'Arbeitszeit bis hier*' time_difference: 'Arbeitszeit bis hier*'
addendum: '* Differenzen von mehr als 10 Minuten werden ignoriert.' addendum: '* Differenzen von mehr als %{delta} Minuten werden ignoriert.'
proxy_exercises: proxy_exercises:
index: index:
clone: Duplizieren clone: Duplizieren

View File

@ -101,6 +101,7 @@ en:
files: Files files: Files
score: Score score: Score
user: Author user: Author
study_group: Study Group
study_group: study_group:
name: Name name: Name
external_id: External ID external_id: External ID
@ -347,6 +348,7 @@ en:
implement: Implement implement: Implement
test_files: Test Files test_files: Test Files
feedback: Feedback feedback: Feedback
study_group_dashboard: Live Dashboard
statistics: statistics:
average_score: Average Score average_score: Average Score
final_submissions: Final Submissions final_submissions: Final Submissions
@ -365,6 +367,13 @@ en:
failure: An error occurred while transmitting your score. Please try again later. failure: An error occurred while transmitting your score. Please try again later.
full_score_redirect_to_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Another participant has a question concerning the exercise you just solved. Your help and comments will be greatly appreciated! full_score_redirect_to_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Another participant has a question concerning the exercise you just solved. Your help and comments will be greatly appreciated!
full_score_redirect_to_own_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Your question concerning the exercise is solved? If so, please share the essential insight with your fellows and mark the question as solved, before you close this window! full_score_redirect_to_own_rfc: Congratulations! You achieved and submitted the highest possible score for this exercise. Your question concerning the exercise is solved? If so, please share the essential insight with your fellows and mark the question as solved, before you close this window!
study_group_dashboard:
live_dashboard: Live Dashboard
time_spent_per_learner: Time spent per Learner
related_requests_for_comments: Related Requests for Comments
learner: Learner
time_spent_in_minutes: Time spent in Minutes
no_data_yet: No data available yet
external_users: external_users:
statistics: statistics:
no_data_available: No data available. no_data_available: No data available.
@ -373,7 +382,7 @@ en:
score: Score score: Score
tests: Unit Test Results tests: Unit Test Results
time_difference: 'Working Time until here*' time_difference: 'Working Time until here*'
addendum: '* Deltas longer than 10 minutes are ignored.' addendum: "* Deltas longer than %{delta} minutes are ignored."
proxy_exercises: proxy_exercises:
index: index:
clone: Duplicate clone: Duplicate

View File

@ -83,6 +83,7 @@ Rails.application.routes.draw do
get :feedback get :feedback
get :reload get :reload
post :submit post :submit
get 'study_group_dashboard/:study_group_id', to: 'exercises#study_group_dashboard'
end end
end end
@ -151,4 +152,5 @@ Rails.application.routes.draw do
post "/evaluate", to: 'remote_evaluation#evaluate', via: [:post] post "/evaluate", to: 'remote_evaluation#evaluate', via: [:post]
mount ActionCable.server => '/cable'
end end

View File

@ -18,6 +18,7 @@ environment.plugins.prepend('Provide', new webpack.ProvidePlugin({
_: 'underscore', _: 'underscore',
vis: 'vis', vis: 'vis',
hljs: 'highlight.js', hljs: 'highlight.js',
d3: 'd3',
}) })
); );

View File

@ -3,8 +3,11 @@
default: &default default: &default
source_path: app/javascript source_path: app/javascript
source_entry_path: packs source_entry_path: packs
public_root_path: public
public_output_path: packs public_output_path: packs
cache_path: tmp/cache/webpacker cache_path: tmp/cache/webpacker
check_yarn_integrity: false
webpack_compile_output: false
# Additional paths webpack should lookup modules # Additional paths webpack should lookup modules
# ['app/assets', 'engine/foo/app/assets'] # ['app/assets', 'engine/foo/app/assets']
@ -13,7 +16,25 @@ default: &default
# Reload manifest.json on all requests so we reload latest compiled packs # Reload manifest.json on all requests so we reload latest compiled packs
cache_manifest: false cache_manifest: false
# Extract and emit a css file
extract_css: true
static_assets_extensions:
- .jpg
- .jpeg
- .png
- .gif
- .tiff
- .ico
- .svg
- .eot
- .otf
- .ttf
- .woff
- .woff2
extensions: extensions:
- .mjs
- .js - .js
- .sass - .sass
- .scss - .scss
@ -31,6 +52,9 @@ development:
<<: *default <<: *default
compile: true compile: true
# Verifies that versions and hashed value of the package contents in the project's package.json
check_yarn_integrity: true
# Reference: https://webpack.js.org/configuration/dev-server/ # Reference: https://webpack.js.org/configuration/dev-server/
dev_server: dev_server:
https: false https: false
@ -66,5 +90,8 @@ production:
# Production depends on precompilation of packs prior to booting for performance. # Production depends on precompilation of packs prior to booting for performance.
compile: false compile: false
# Extract and emit a css file
extract_css: true
# Cache manifest.json for performance # Cache manifest.json for performance
cache_manifest: true cache_manifest: true

View File

@ -0,0 +1,6 @@
class AddIndicesForRequestForComments < ActiveRecord::Migration[5.2]
def change
add_index :request_for_comments, :submission_id
add_index :request_for_comments, :exercise_id
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2018_11_29_093207) do ActiveRecord::Schema.define(version: 2019_02_13_131802) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -292,6 +292,8 @@ ActiveRecord::Schema.define(version: 2018_11_29_093207) do
t.text "thank_you_note" t.text "thank_you_note"
t.boolean "full_score_reached", default: false t.boolean "full_score_reached", default: false
t.integer "times_featured", default: 0 t.integer "times_featured", default: 0
t.index ["exercise_id"], name: "index_request_for_comments_on_exercise_id"
t.index ["submission_id"], name: "index_request_for_comments_on_submission_id"
end end
create_table "searches", force: :cascade do |t| create_table "searches", force: :cascade do |t|
@ -415,4 +417,5 @@ ActiveRecord::Schema.define(version: 2018_11_29_093207) do
t.index ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user_type_and_user_id" t.index ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user_type_and_user_id"
end end
add_foreign_key "submissions", "study_groups"
end end

View File

@ -1,9 +1,10 @@
{ {
"dependencies": { "dependencies": {
"@rails/webpacker": "3.5", "@rails/webpacker": "4.0",
"bootstrap": "^4.1.3", "bootstrap": "^4.1.3",
"bootswatch": "^4.1.3", "bootswatch": "^4.1.3",
"chosen-js": "^1.8.7", "chosen-js": "^1.8.7",
"d3": "^5.9.1",
"d3-tip": "^0.9.1", "d3-tip": "^0.9.1",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"highlight.js": "^9.12.0", "highlight.js": "^9.12.0",
@ -17,7 +18,7 @@
"webpack-merge": "^4.1.4" "webpack-merge": "^4.1.4"
}, },
"devDependencies": { "devDependencies": {
"webpack-dev-server": "2.11.2" "webpack-dev-server": "3.2.1"
}, },
"scripts": { "scripts": {
"webpack": "./bin/webpack", "webpack": "./bin/webpack",

12
postcss.config.js Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
plugins: [
require('postcss-import'),
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009'
},
stage: 3
})
]
};

View File

@ -5,9 +5,9 @@
######## VERSION INFORMATION ######## ######## VERSION INFORMATION ########
postgres_version=10 postgres_version=10
ruby_version=2.5.1 ruby_version=2.6.1
rails_version=5.2.1 rails_version=5.2.2
geckodriver_version=0.23.0 geckodriver_version=0.24.0
########## INSTALL SCRIPT ########### ########## INSTALL SCRIPT ###########

View File

@ -0,0 +1,4 @@
require 'factory_bot'
# Use "old" FactoryBot default to allow auto-creating associations for #build
FactoryBot.use_parent_strategy = false

6036
yarn.lock

File diff suppressed because it is too large Load Diff