Merge pull request #261 from openHPI/feature/la-dashboard
Add LA dashboard architecture
This commit is contained in:
18
.babelrc
18
.babelrc
@ -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 }]
|
||||
]
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
plugins:
|
||||
postcss-import: {}
|
||||
postcss-cssnext: {}
|
@ -5,7 +5,7 @@ services:
|
||||
|
||||
language: ruby
|
||||
rvm:
|
||||
- 2.5.1
|
||||
- 2.6.1
|
||||
cache:
|
||||
bundler: true
|
||||
yarn: true
|
||||
@ -27,7 +27,7 @@ before_install:
|
||||
- docker pull openhpi/co_execenv_python
|
||||
- docker pull openhpi/co_execenv_java
|
||||
- mkdir ~/geckodriver
|
||||
- wget -O ~/geckodriver/download.tar.gz https://github.com/mozilla/geckodriver/releases/download/v0.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/
|
||||
- rm ~/geckodriver/download.tar.gz
|
||||
- chmod +x ~/geckodriver/geckodriver
|
||||
|
5
Gemfile
5
Gemfile
@ -17,8 +17,9 @@ gem 'pg'
|
||||
gem 'pry-byebug'
|
||||
gem 'puma'
|
||||
gem 'pundit'
|
||||
gem 'rails', '5.2.1.1'
|
||||
gem 'rails', '5.2.2'
|
||||
gem 'rails-i18n'
|
||||
gem 'i18n-js'
|
||||
gem 'ransack'
|
||||
gem 'rubytree'
|
||||
gem 'sass-rails'
|
||||
@ -31,12 +32,12 @@ gem 'tubesock', git: 'https://github.com/gosukiwi/tubesock', branch: 'patch-1' #
|
||||
gem 'faye-websocket'
|
||||
gem 'eventmachine', '1.0.9.1' # explicitly added, this is used by faye-websocket, newer versions might crash or
|
||||
gem 'nokogiri'
|
||||
gem 'd3-rails'
|
||||
gem 'webpacker'
|
||||
gem 'rest-client'
|
||||
gem 'rubyzip'
|
||||
gem 'mnemosyne-ruby'
|
||||
gem 'whenever', require: false
|
||||
gem 'rails-timeago'
|
||||
|
||||
group :development, :staging do
|
||||
gem 'bootsnap', require: false
|
||||
|
209
Gemfile.lock
209
Gemfile.lock
@ -10,49 +10,49 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ZenTest (4.11.1)
|
||||
actioncable (5.2.1.1)
|
||||
actionpack (= 5.2.1.1)
|
||||
ZenTest (4.11.2)
|
||||
actioncable (5.2.2)
|
||||
actionpack (= 5.2.2)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailer (5.2.1.1)
|
||||
actionpack (= 5.2.1.1)
|
||||
actionview (= 5.2.1.1)
|
||||
activejob (= 5.2.1.1)
|
||||
actionmailer (5.2.2)
|
||||
actionpack (= 5.2.2)
|
||||
actionview (= 5.2.2)
|
||||
activejob (= 5.2.2)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.2.1.1)
|
||||
actionview (= 5.2.1.1)
|
||||
activesupport (= 5.2.1.1)
|
||||
actionpack (5.2.2)
|
||||
actionview (= 5.2.2)
|
||||
activesupport (= 5.2.2)
|
||||
rack (~> 2.0)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.2.1.1)
|
||||
activesupport (= 5.2.1.1)
|
||||
actionview (5.2.2)
|
||||
activesupport (= 5.2.2)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
activejob (5.2.1.1)
|
||||
activesupport (= 5.2.1.1)
|
||||
activejob (5.2.2)
|
||||
activesupport (= 5.2.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.2.1.1)
|
||||
activesupport (= 5.2.1.1)
|
||||
activerecord (5.2.1.1)
|
||||
activemodel (= 5.2.1.1)
|
||||
activesupport (= 5.2.1.1)
|
||||
activemodel (5.2.2)
|
||||
activesupport (= 5.2.2)
|
||||
activerecord (5.2.2)
|
||||
activemodel (= 5.2.2)
|
||||
activesupport (= 5.2.2)
|
||||
arel (>= 9.0)
|
||||
activestorage (5.2.1.1)
|
||||
actionpack (= 5.2.1.1)
|
||||
activerecord (= 5.2.1.1)
|
||||
activestorage (5.2.2)
|
||||
actionpack (= 5.2.2)
|
||||
activerecord (= 5.2.2)
|
||||
marcel (~> 0.3.1)
|
||||
activesupport (5.2.1.1)
|
||||
activesupport (5.2.2)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.5.2)
|
||||
addressable (2.6.0)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
airbrussh (1.3.1)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
@ -62,29 +62,28 @@ GEM
|
||||
autotest-rails (4.2.1)
|
||||
ZenTest (~> 4.5)
|
||||
bcrypt (3.1.12)
|
||||
better_errors (2.5.0)
|
||||
better_errors (2.5.1)
|
||||
coderay (>= 1.0.0)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
bindex (0.5.0)
|
||||
binding_of_caller (0.8.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.3.2)
|
||||
bootsnap (1.4.1)
|
||||
msgpack (~> 1.0)
|
||||
bootstrap-will_paginate (1.0.0)
|
||||
will_paginate
|
||||
builder (3.2.3)
|
||||
bunny (2.12.0)
|
||||
bunny (2.14.1)
|
||||
amq-protocol (~> 2.3, >= 2.3.0)
|
||||
byebug (10.0.2)
|
||||
byebug (11.0.0)
|
||||
capistrano (3.11.0)
|
||||
airbrussh (>= 1.0.0)
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
capistrano-bundler (1.4.0)
|
||||
capistrano-bundler (1.5.0)
|
||||
capistrano (~> 3.1)
|
||||
sshkit (~> 1.2)
|
||||
capistrano-rails (1.4.0)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (~> 1.1)
|
||||
@ -97,7 +96,7 @@ GEM
|
||||
capistrano (~> 3.7)
|
||||
capistrano-bundler
|
||||
puma (~> 3.4)
|
||||
capybara (3.10.1)
|
||||
capybara (3.14.0)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
@ -105,7 +104,7 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (~> 1.2)
|
||||
xpath (~> 3.2)
|
||||
carrierwave (1.2.3)
|
||||
carrierwave (1.3.1)
|
||||
activemodel (>= 4.0.0)
|
||||
activesupport (>= 4.0.0)
|
||||
mime-types (>= 1.16)
|
||||
@ -113,10 +112,8 @@ GEM
|
||||
ffi (~> 1.0, >= 1.0.11)
|
||||
chronic (0.10.2)
|
||||
coderay (1.1.2)
|
||||
concurrent-ruby (1.1.3)
|
||||
concurrent-ruby (1.1.4)
|
||||
crass (1.0.4)
|
||||
d3-rails (5.7.0)
|
||||
railties (>= 3.1)
|
||||
database_cleaner (1.7.0)
|
||||
debug_inspector (0.0.3)
|
||||
diff-lcs (1.3)
|
||||
@ -126,34 +123,36 @@ GEM
|
||||
multi_json
|
||||
domain_name (0.5.20180417)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
erubi (1.7.1)
|
||||
erubi (1.8.0)
|
||||
eventmachine (1.0.9.1)
|
||||
excon (0.62.0)
|
||||
execjs (2.7.0)
|
||||
factory_bot (4.11.1)
|
||||
activesupport (>= 3.0.0)
|
||||
factory_bot_rails (4.11.1)
|
||||
factory_bot (~> 4.11.1)
|
||||
railties (>= 3.0.0)
|
||||
faraday (0.15.3)
|
||||
factory_bot (5.0.2)
|
||||
activesupport (>= 4.2.0)
|
||||
factory_bot_rails (5.0.1)
|
||||
factory_bot (~> 5.0.0)
|
||||
railties (>= 4.2.0)
|
||||
faraday (0.15.4)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faye-websocket (0.10.7)
|
||||
eventmachine (>= 0.12.0)
|
||||
websocket-driver (>= 0.5.1)
|
||||
ffi (1.9.25)
|
||||
ffi (1.10.0)
|
||||
forgery (0.7.0)
|
||||
globalid (0.4.1)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
headless (2.3.1)
|
||||
highline (2.0.0)
|
||||
highline (2.0.1)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
i18n (1.1.1)
|
||||
i18n (1.6.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-js (3.2.1)
|
||||
i18n (>= 0.6.6)
|
||||
ims-lti (1.2.2)
|
||||
builder
|
||||
oauth (>= 0.4.5, < 0.6)
|
||||
jaro_winkler (1.5.1)
|
||||
jaro_winkler (1.5.2)
|
||||
jbuilder (2.8.0)
|
||||
activesupport (>= 4.2.0)
|
||||
multi_json (>= 1.2)
|
||||
@ -161,9 +160,9 @@ GEM
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.1.0)
|
||||
json (2.2.0)
|
||||
jwt (2.1.0)
|
||||
kramdown (1.17.0)
|
||||
kramdown (2.1.0)
|
||||
listen (3.1.5)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
@ -179,25 +178,25 @@ GEM
|
||||
mime-types (3.2.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2018.0812)
|
||||
mimemagic (0.3.2)
|
||||
mimemagic (0.3.3)
|
||||
mini_mime (1.0.1)
|
||||
mini_portile2 (2.3.0)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.11.3)
|
||||
mnemosyne-ruby (1.5.1)
|
||||
activesupport (>= 4)
|
||||
bunny
|
||||
msgpack (1.2.4)
|
||||
msgpack (1.2.7)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
net-scp (1.2.1)
|
||||
net-ssh (>= 2.6.5)
|
||||
net-ssh (5.0.2)
|
||||
net-ssh (5.1.0)
|
||||
netrc (0.11.0)
|
||||
newrelic_rpm (5.4.0.347)
|
||||
newrelic_rpm (6.1.0.352)
|
||||
nio4r (2.3.1)
|
||||
nokogiri (1.8.5)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
nokogiri (1.10.1)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nyan-cat-formatter (0.12.0)
|
||||
rspec (>= 2.99, >= 2.14.2, < 4)
|
||||
oauth (0.5.4)
|
||||
@ -209,70 +208,74 @@ GEM
|
||||
rack (>= 1.2, < 3)
|
||||
pagedown-bootstrap-rails (2.1.4)
|
||||
railties (> 3.1)
|
||||
parallel (1.12.1)
|
||||
parser (2.5.3.0)
|
||||
parallel (1.14.0)
|
||||
parser (2.6.0.0)
|
||||
ast (~> 2.4.0)
|
||||
pg (1.1.3)
|
||||
pg (1.1.4)
|
||||
powerpack (0.1.2)
|
||||
pry (0.12.0)
|
||||
pry (0.12.2)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.9.0)
|
||||
pry-byebug (3.6.0)
|
||||
byebug (~> 10.0)
|
||||
pry-byebug (3.7.0)
|
||||
byebug (~> 11.0)
|
||||
pry (~> 0.10)
|
||||
psych (3.1.0)
|
||||
public_suffix (3.0.3)
|
||||
puma (3.12.0)
|
||||
pundit (2.0.0)
|
||||
pundit (2.0.1)
|
||||
activesupport (>= 3.0.0)
|
||||
rack (2.0.6)
|
||||
rack-mini-profiler (1.0.0)
|
||||
rack-mini-profiler (1.0.2)
|
||||
rack (>= 1.2.0)
|
||||
rack-proxy (0.6.5)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (5.2.1.1)
|
||||
actioncable (= 5.2.1.1)
|
||||
actionmailer (= 5.2.1.1)
|
||||
actionpack (= 5.2.1.1)
|
||||
actionview (= 5.2.1.1)
|
||||
activejob (= 5.2.1.1)
|
||||
activemodel (= 5.2.1.1)
|
||||
activerecord (= 5.2.1.1)
|
||||
activestorage (= 5.2.1.1)
|
||||
activesupport (= 5.2.1.1)
|
||||
rails (5.2.2)
|
||||
actioncable (= 5.2.2)
|
||||
actionmailer (= 5.2.2)
|
||||
actionpack (= 5.2.2)
|
||||
actionview (= 5.2.2)
|
||||
activejob (= 5.2.2)
|
||||
activemodel (= 5.2.2)
|
||||
activerecord (= 5.2.2)
|
||||
activestorage (= 5.2.2)
|
||||
activesupport (= 5.2.2)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 5.2.1.1)
|
||||
railties (= 5.2.2)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.2)
|
||||
actionpack (~> 5.x, >= 5.0.1)
|
||||
actionview (~> 5.x, >= 5.0.1)
|
||||
activesupport (~> 5.x)
|
||||
rails-controller-testing (1.0.4)
|
||||
actionpack (>= 5.0.1.x)
|
||||
actionview (>= 5.0.1.x)
|
||||
activesupport (>= 5.0.1.x)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.0.4)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails-i18n (5.1.2)
|
||||
rails-i18n (5.1.3)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 5.0, < 6)
|
||||
railties (5.2.1.1)
|
||||
actionpack (= 5.2.1.1)
|
||||
activesupport (= 5.2.1.1)
|
||||
rails-timeago (2.17.1)
|
||||
actionpack (>= 3.1)
|
||||
activesupport (>= 3.1)
|
||||
railties (5.2.2)
|
||||
actionpack (= 5.2.2)
|
||||
activesupport (= 5.2.2)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (12.3.1)
|
||||
ransack (2.1.0)
|
||||
rake (12.3.2)
|
||||
ransack (2.1.1)
|
||||
actionpack (>= 5.0)
|
||||
activerecord (>= 5.0)
|
||||
activesupport (>= 5.0)
|
||||
i18n
|
||||
rb-fsevent (0.10.3)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
regexp_parser (1.2.0)
|
||||
rb-inotify (0.10.0)
|
||||
ffi (~> 1.0)
|
||||
regexp_parser (1.3.0)
|
||||
rest-client (2.0.2)
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
@ -291,7 +294,7 @@ GEM
|
||||
rspec-mocks (3.8.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.8.0)
|
||||
rspec-rails (3.8.1)
|
||||
rspec-rails (3.8.2)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
@ -300,15 +303,16 @@ GEM
|
||||
rspec-mocks (~> 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)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.5, != 2.5.1.1)
|
||||
powerpack (~> 0.1)
|
||||
psych (>= 3.1.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (~> 1.4.0)
|
||||
rubocop-rspec (1.30.1)
|
||||
rubocop-rspec (1.32.0)
|
||||
rubocop (>= 0.60.0)
|
||||
ruby-progressbar (1.10.0)
|
||||
ruby_dep (1.5.0)
|
||||
@ -316,7 +320,7 @@ GEM
|
||||
json (~> 2.1)
|
||||
structured_warnings (~> 0.3)
|
||||
rubyzip (1.2.2)
|
||||
sass (3.6.0)
|
||||
sass (3.7.3)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
@ -342,7 +346,7 @@ GEM
|
||||
actionpack (>= 3.1)
|
||||
railties (>= 3.1)
|
||||
slim (>= 3.0, < 5.0)
|
||||
sorcery (0.12.0)
|
||||
sorcery (0.13.0)
|
||||
bcrypt (~> 3.1)
|
||||
oauth (~> 0.4, >= 0.4.4)
|
||||
oauth2 (~> 1.0, >= 0.8.0)
|
||||
@ -355,31 +359,31 @@ GEM
|
||||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
sshkit (1.18.0)
|
||||
sshkit (1.18.2)
|
||||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
structured_warnings (0.3.0)
|
||||
temple (0.8.0)
|
||||
temple (0.8.1)
|
||||
thor (0.20.3)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.8)
|
||||
tilt (2.0.9)
|
||||
turbolinks (5.2.0)
|
||||
turbolinks-source (~> 5.2)
|
||||
turbolinks-source (5.2.0)
|
||||
tzinfo (1.2.5)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (4.1.19)
|
||||
uglifier (4.1.20)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.5)
|
||||
unicode-display_width (1.4.0)
|
||||
unicode-display_width (1.4.1)
|
||||
web-console (3.7.0)
|
||||
actionview (>= 5.0)
|
||||
activemodel (>= 5.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 5.0)
|
||||
webpacker (3.5.5)
|
||||
webpacker (4.0.2)
|
||||
activesupport (>= 4.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
@ -411,7 +415,6 @@ DEPENDENCIES
|
||||
capybara
|
||||
carrierwave
|
||||
concurrent-ruby
|
||||
d3-rails
|
||||
database_cleaner
|
||||
docker-api
|
||||
eventmachine (= 1.0.9.1)
|
||||
@ -420,6 +423,7 @@ DEPENDENCIES
|
||||
forgery
|
||||
headless
|
||||
highline
|
||||
i18n-js
|
||||
ims-lti (< 2.0.0)
|
||||
jbuilder
|
||||
jquery-rails
|
||||
@ -435,9 +439,10 @@ DEPENDENCIES
|
||||
puma
|
||||
pundit
|
||||
rack-mini-profiler
|
||||
rails (= 5.2.1.1)
|
||||
rails (= 5.2.2)
|
||||
rails-controller-testing
|
||||
rails-i18n
|
||||
rails-timeago
|
||||
ransack
|
||||
rest-client
|
||||
rspec-autotest
|
||||
@ -460,4 +465,4 @@ DEPENDENCIES
|
||||
whenever
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.1
|
||||
1.17.2
|
||||
|
@ -13,7 +13,10 @@
|
||||
//= require jquery_ujs
|
||||
//= require turbolinks
|
||||
//= require pagedown_bootstrap
|
||||
//= require d3
|
||||
//= require rails-timeago
|
||||
//= require locales/jquery.timeago.de.js
|
||||
//= require i18n
|
||||
//= require i18n/translations
|
||||
//
|
||||
// lib/assets
|
||||
//= require flash
|
||||
|
@ -23,3 +23,14 @@ $.fn.scrollTo = function(selector) {
|
||||
scrollTop: $(selector).offset().top - $(this).offset().top + $(this).scrollTop()
|
||||
}, 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;
|
||||
|
13
app/assets/javascripts/cable.js
Normal file
13
app/assets/javascripts/cable.js
Normal 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);
|
250
app/assets/javascripts/channels/la_exercises.js
Normal file
250
app/assets/javascripts/channels/la_exercises.js
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -113,3 +113,7 @@ span.caret {
|
||||
-webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
|
||||
}
|
||||
}
|
||||
|
||||
.table-row-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -84,6 +84,10 @@ div#chart_2 {
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
div#chart_stacked {
|
||||
max-height: 500px;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
a.file-heading {
|
||||
color: black !important;
|
||||
@ -115,7 +119,7 @@ a.file-heading {
|
||||
.d3-tip:after {
|
||||
box-sizing: border-box;
|
||||
display: inline;
|
||||
font-size: 10px;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
@ -126,7 +130,7 @@ a.file-heading {
|
||||
|
||||
/* Style northward tooltips differently */
|
||||
.d3-tip.n:after {
|
||||
margin: -1px 0 0 0;
|
||||
margin: -3px 0 0 0;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
}
|
||||
|
4
app/channels/application_cable/channel.rb
Normal file
4
app/channels/application_cable/channel.rb
Normal file
@ -0,0 +1,4 @@
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
end
|
30
app/channels/application_cable/connection.rb
Normal file
30
app/channels/application_cable/connection.rb
Normal 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
|
16
app/channels/la_exercises_channel.rb
Normal file
16
app/channels/la_exercises_channel.rb
Normal 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
|
@ -168,6 +168,7 @@ module Lti
|
||||
end
|
||||
group.users |= [@current_user] # add current user if not already member of the group
|
||||
group.save
|
||||
session[:study_group_id] = group.id
|
||||
end
|
||||
|
||||
def set_embedding_options
|
||||
|
@ -16,7 +16,8 @@ module SubmissionParameters
|
||||
current_user_id = current_user.id
|
||||
current_user_class_name = current_user.class.name
|
||||
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)
|
||||
submission_params
|
||||
end
|
||||
|
@ -40,7 +40,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
FROM
|
||||
(SELECT user_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
|
||||
(SELECT user_id,
|
||||
exercise_id,
|
||||
|
@ -7,7 +7,7 @@ class ExercisesController < ApplicationController
|
||||
|
||||
before_action :handle_file_uploads, only: [:create, :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_file_types, only: [:create, :edit, :new, :update]
|
||||
before_action :set_course_token, only: [:implement]
|
||||
@ -266,7 +266,7 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
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])
|
||||
authorize!
|
||||
end
|
||||
@ -319,7 +319,7 @@ class ExercisesController < ApplicationController
|
||||
private :set_file_types
|
||||
|
||||
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)
|
||||
checked_exercise_tags = @exercise.exercise_tags
|
||||
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 }
|
||||
@deltas = @all_events.map.with_index do |item, index|
|
||||
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
|
||||
@working_times_until = []
|
||||
@all_events.each_with_index do |_, index|
|
||||
@ -475,4 +475,15 @@ class ExercisesController < ApplicationController
|
||||
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
|
||||
|
@ -27,7 +27,7 @@ class ExternalUsersController < ApplicationController
|
||||
score,
|
||||
id,
|
||||
CASE
|
||||
WHEN working_time >= '0:05:00' THEN '0'
|
||||
WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0'
|
||||
ELSE working_time
|
||||
END AS working_time_new
|
||||
FROM
|
||||
|
@ -60,7 +60,7 @@ class InternalUsersController < ApplicationController
|
||||
end
|
||||
|
||||
def index
|
||||
@search = InternalUser.search(params[:q])
|
||||
@search = InternalUser.ransack(params[:q])
|
||||
@users = @search.result.includes(:consumer).order(:name).paginate(page: params[:page])
|
||||
authorize!
|
||||
end
|
||||
|
@ -33,7 +33,7 @@ class ProxyExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
def edit
|
||||
@search = policy_scope(Exercise).search(params[:q])
|
||||
@search = policy_scope(Exercise).ransack(params[:q])
|
||||
@exercises = @search.result.order(:title)
|
||||
authorize!
|
||||
end
|
||||
@ -44,14 +44,14 @@ class ProxyExercisesController < ApplicationController
|
||||
private :proxy_exercise_params
|
||||
|
||||
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])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def new
|
||||
@proxy_exercise = ProxyExercise.new
|
||||
@search = policy_scope(Exercise).search(params[:q])
|
||||
@search = policy_scope(Exercise).ransack(params[:q])
|
||||
@exercises = @search.result.order(:title)
|
||||
authorize!
|
||||
end
|
||||
@ -63,8 +63,8 @@ class ProxyExercisesController < ApplicationController
|
||||
private :set_exercise_and_authorize
|
||||
|
||||
def show
|
||||
@search = @proxy_exercise.exercises.search
|
||||
@exercises = @proxy_exercise.exercises.search.result.order(:title) #@search.result.order(:title)
|
||||
@search = @proxy_exercise.exercises.ransack
|
||||
@exercises = @proxy_exercise.exercises.ransack.result.order(:title) #@search.result.order(:title)
|
||||
end
|
||||
|
||||
#we might want to think about auth here
|
||||
|
@ -15,7 +15,7 @@ class RequestForCommentsController < ApplicationController
|
||||
@search = RequestForComment
|
||||
.last_per_user(2)
|
||||
.with_last_activity
|
||||
.search(params[:q])
|
||||
.ransack(params[:q])
|
||||
@request_for_comments = @search.result
|
||||
.order('created_at DESC')
|
||||
.paginate(page: params[:page], total_entries: @search.result.length)
|
||||
@ -27,7 +27,7 @@ class RequestForCommentsController < ApplicationController
|
||||
@search = RequestForComment
|
||||
.with_last_activity
|
||||
.where(user_id: current_user.id)
|
||||
.search(params[:q])
|
||||
.ransack(params[:q])
|
||||
@request_for_comments = @search.result
|
||||
.order('created_at DESC')
|
||||
.paginate(page: params[:page])
|
||||
@ -40,7 +40,7 @@ class RequestForCommentsController < ApplicationController
|
||||
.with_last_activity
|
||||
.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})
|
||||
.search(params[:q])
|
||||
.ransack(params[:q])
|
||||
@request_for_comments = @search.result
|
||||
.order('last_comment DESC')
|
||||
.paginate(page: params[:page])
|
||||
@ -83,17 +83,10 @@ class RequestForCommentsController < ApplicationController
|
||||
authorize!
|
||||
end
|
||||
|
||||
# GET /request_for_comments/new
|
||||
def new
|
||||
@request_for_comment = RequestForComment.new
|
||||
authorize!
|
||||
end
|
||||
|
||||
# GET /request_for_comments/1/edit
|
||||
def edit
|
||||
end
|
||||
|
||||
# POST /request_for_comments
|
||||
# POST /request_for_comments.json
|
||||
def create
|
||||
# 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.
|
||||
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)
|
||||
end
|
||||
|
||||
|
@ -4,17 +4,17 @@ class StudyGroupsController < ApplicationController
|
||||
before_action :set_group, only: MEMBER_ACTIONS
|
||||
|
||||
def index
|
||||
@search = StudyGroup.search(params[:q])
|
||||
@search = StudyGroup.ransack(params[:q])
|
||||
@study_groups = @search.result.includes(:consumer).order(:name).paginate(page: params[:page])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def show
|
||||
@search = @study_group.users.search(params[:q])
|
||||
@search = @study_group.users.ransack(params[:q])
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
|
@ -106,7 +106,7 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def index
|
||||
@search = Submission.search(params[:q])
|
||||
@search = Submission.ransack(params[:q])
|
||||
@submissions = @search.result.includes(:exercise, :user).paginate(page: params[:page])
|
||||
authorize!
|
||||
end
|
||||
@ -201,6 +201,8 @@ class SubmissionsController < ApplicationController
|
||||
save_run_output
|
||||
|
||||
if @run_output.blank?
|
||||
@raw_output ||= ''
|
||||
@run_output ||= ''
|
||||
parse_message t('exercises.implement.no_output', timestamp: l(Time.now, format: :short)), 'stdout', tubesock
|
||||
end
|
||||
|
||||
|
30
app/helpers/action_cable_helper.rb
Normal file
30
app/helpers/action_cable_helper.rb
Normal 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
|
@ -1,5 +1,8 @@
|
||||
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
|
||||
[
|
||||
{
|
||||
|
@ -13,7 +13,9 @@ import 'bootstrap/dist/js/bootstrap.bundle.min';
|
||||
import 'chosen-js/chosen.jquery';
|
||||
import 'jstree';
|
||||
import 'underscore';
|
||||
import 'd3'
|
||||
window._ = _; // Publish underscore's `_` in global namespace
|
||||
window.d3 = d3; // Publish d3 in global namespace
|
||||
|
||||
// CSS
|
||||
import 'chosen-js/chosen.css';
|
||||
|
@ -1,8 +1,12 @@
|
||||
class Comment < ApplicationRecord
|
||||
# inherit the creation module: encapsulates that this is a polymorphic user, offers some aliases and makes sure that all necessary attributes are set.
|
||||
include Creation
|
||||
include ActionCableHelper
|
||||
|
||||
attr_accessor :username, :date, :updated, :editable
|
||||
|
||||
belongs_to :file, class_name: 'CodeOcean::File'
|
||||
belongs_to :user, polymorphic: true
|
||||
|
||||
after_save :trigger_rfc_action_cable_from_comment
|
||||
end
|
||||
|
@ -76,7 +76,7 @@ class Exercise < ApplicationRecord
|
||||
(SELECT user_id,
|
||||
user_type,
|
||||
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
|
||||
(SELECT user_id,
|
||||
user_type,
|
||||
@ -90,6 +90,145 @@ class Exercise < ApplicationRecord
|
||||
"
|
||||
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)
|
||||
quantiles_str = "[" + quantiles.join(",") + "]"
|
||||
result = self.class.connection.execute("""
|
||||
@ -180,7 +319,7 @@ class Exercise < ApplicationRecord
|
||||
exercise_id,
|
||||
max_score,
|
||||
CASE
|
||||
WHEN working_time >= '0:05:00' THEN '0'
|
||||
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 ), result AS
|
||||
@ -274,7 +413,7 @@ class Exercise < ApplicationRecord
|
||||
|
||||
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
|
||||
)
|
||||
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
|
||||
|
@ -1,5 +1,7 @@
|
||||
class RequestForComment < ApplicationRecord
|
||||
include Creation
|
||||
include ActionCableHelper
|
||||
|
||||
belongs_to :submission
|
||||
belongs_to :exercise
|
||||
belongs_to :file, class_name: 'CodeOcean::File'
|
||||
@ -10,6 +12,8 @@ class RequestForComment < ApplicationRecord
|
||||
scope :unsolved, -> { where(solved: [false, nil]) }
|
||||
scope :in_range, -> (from, to) { where(created_at: from..to) }
|
||||
|
||||
after_save :trigger_rfc_action_cable
|
||||
|
||||
def self.last_per_user(n = 5)
|
||||
from("(#{row_number_user_sql}) as request_for_comments")
|
||||
.where("row_number <= ?", n)
|
||||
|
@ -1,12 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class StudyGroup < ApplicationRecord
|
||||
has_many :study_group_memberships
|
||||
has_many :study_group_memberships, dependent: :destroy
|
||||
# Use `ExternalUser` as `source_type` for now.
|
||||
# Using `User` will lead ActiveRecord to access the inexistent table `users`.
|
||||
# Issue created: https://github.com/rails/rails/issues/34531
|
||||
has_many :users, through: :study_group_memberships, source_type: 'ExternalUser'
|
||||
has_many :submissions
|
||||
has_many :submissions, dependent: :nullify
|
||||
belongs_to :consumer
|
||||
|
||||
def to_s
|
||||
|
@ -1,6 +1,7 @@
|
||||
class Submission < ApplicationRecord
|
||||
include Context
|
||||
include Creation
|
||||
include ActionCableHelper
|
||||
|
||||
CAUSES = %w(assess download file render run save submit test autosave requestComments remoteAssess)
|
||||
FILENAME_URL_PLACEHOLDER = '{filename}'
|
||||
@ -20,6 +21,8 @@ class Submission < ApplicationRecord
|
||||
validates :cause, inclusion: {in: CAUSES}
|
||||
validates :exercise_id, presence: true
|
||||
|
||||
after_save :trigger_working_times_action_cable
|
||||
|
||||
MAX_COMMENTS_ON_RECOMMENDED_RFC = 5
|
||||
|
||||
def build_files_hash(files, attribute)
|
||||
|
@ -25,6 +25,22 @@ class ApplicationPolicy
|
||||
end
|
||||
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)
|
||||
@user = user
|
||||
@record = record
|
||||
|
@ -3,8 +3,8 @@ class ExercisePolicy < AdminOrAuthorPolicy
|
||||
admin?
|
||||
end
|
||||
|
||||
def show?
|
||||
admin? || teacher?
|
||||
[:show?, :study_group_dashboard?].each do |action|
|
||||
define_method(action) { admin? || teacher? }
|
||||
end
|
||||
|
||||
[:clone?, :destroy?, :edit?, :statistics?, :update?, :feedback?].each do |action|
|
||||
|
@ -3,8 +3,8 @@ class StudyGroupPolicy < AdminOnlyPolicy
|
||||
admin? || teacher?
|
||||
end
|
||||
|
||||
[:show?, :destroy?, :edit?, :update?].each do |action|
|
||||
define_method(action) { admin? || @user.teacher? && @record.users.include?(@user) }
|
||||
[:show?, :destroy?, :edit?, :update?, :stream_la?].each do |action|
|
||||
define_method(action) { admin? || @user.teacher? && @record.present? && @record.users.include?(@user) }
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
|
@ -12,14 +12,8 @@ class SubmissionPolicy < ApplicationPolicy
|
||||
admin?
|
||||
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
|
||||
teacher? && everyone_in_study_group
|
||||
def show_study_group?
|
||||
admin? || teacher_in_study_group
|
||||
end
|
||||
private :teacher_in_study_group
|
||||
end
|
||||
|
@ -58,7 +58,7 @@ h1 = "#{@exercise} (external user #{@external_user})"
|
||||
td =
|
||||
td =
|
||||
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);
|
||||
div#progress_chart.col-lg-12
|
||||
.graph-functions-2
|
||||
|
37
app/views/exercises/study_group_dashboard.html.slim
Normal file
37
app/views/exercises/study_group_dashboard.html.slim
Normal 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)
|
@ -5,6 +5,7 @@ html lang='en'
|
||||
meta name='viewport' content='width=device-width, initial-scale=1'
|
||||
title = application_name
|
||||
link href=asset_path('favicon.png') rel='icon' type='image/png'
|
||||
= action_cable_meta_tag
|
||||
= stylesheet_pack_tag('application', media: 'all', 'data-turbolinks-track': true)
|
||||
= stylesheet_pack_tag('stylesheets', media: 'all', 'data-turbolinks-track': true)
|
||||
= stylesheet_link_tag('application', media: 'all', 'data-turbolinks-track': true)
|
||||
@ -12,6 +13,10 @@ html lang='en'
|
||||
= javascript_include_tag('application', 'data-turbolinks-track': true)
|
||||
= yield(:head)
|
||||
= csrf_meta_tags
|
||||
= timeago_script_tag
|
||||
script type="text/javascript"
|
||||
| I18n.defaultLocale = "#{I18n.default_locale}";
|
||||
| I18n.locale = "#{I18n.locale}";
|
||||
body
|
||||
- unless @embed_options[:hide_navbar]
|
||||
nav.navbar.navbar-dark.bg-dark.navbar-expand-md.mb-4.py-1 role='navigation'
|
||||
|
15
app/views/request_for_comments/_list_entry.html.slim
Normal file
15
app/views/request_for_comments/_list_entry.html.slim
Normal 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
|
@ -9,6 +9,9 @@
|
||||
- testruns = Testrun.where(:submission_id => @request_for_comment.submission)
|
||||
= link_to_if(policy(user).show?, user.displayname, user)
|
||||
| | #{@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
|
||||
.description
|
||||
h5
|
||||
|
@ -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.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.score', value: @submission.score)
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
== t('mailers.user_mailer.send_thank_you_note.body',
|
||||
receiver_displayname: @receiver_displayname,
|
||||
link_to_comment: link_to(@rfc_link, @rfc_link),
|
||||
author: @author.displayname,
|
||||
author: @author,
|
||||
thank_you_note: @thank_you_note )
|
||||
|
70
babel.config.js
Normal file
70
babel.config.js
Normal 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)
|
||||
}
|
||||
};
|
@ -12,4 +12,8 @@ require "bundler/setup"
|
||||
|
||||
require "webpacker"
|
||||
require "webpacker/webpack_runner"
|
||||
Webpacker::WebpackRunner.run(ARGV)
|
||||
|
||||
APP_ROOT = File.expand_path("..", __dir__)
|
||||
Dir.chdir(APP_ROOT) do
|
||||
Webpacker::WebpackRunner.run(ARGV)
|
||||
end
|
||||
|
@ -12,4 +12,8 @@ require "bundler/setup"
|
||||
|
||||
require "webpacker"
|
||||
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
|
||||
|
@ -28,5 +28,7 @@ module CodeOcean
|
||||
config.autoload_paths << Rails.root.join('lib')
|
||||
config.eager_load_paths << Rails.root.join('lib')
|
||||
config.assets.precompile += %w( markdown-buttons.png )
|
||||
|
||||
config.action_cable.mount_path = '/cable'
|
||||
end
|
||||
end
|
||||
|
@ -1,10 +1,14 @@
|
||||
development:
|
||||
adapter: async
|
||||
adapter: postgresql
|
||||
|
||||
test:
|
||||
adapter: async
|
||||
adapter: postgresql
|
||||
|
||||
staging:
|
||||
adapter: postgresql
|
||||
|
||||
production:
|
||||
adapter: redis
|
||||
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
|
||||
channel_prefix: code_ocean_production
|
||||
adapter: postgresql # redis
|
||||
# all other options below are only used for redis
|
||||
# url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
|
||||
# channel_prefix: code_ocean_production
|
||||
|
@ -101,6 +101,7 @@ de:
|
||||
files: Dateien
|
||||
score: Punktzahl
|
||||
user: Autor
|
||||
study_group: Lerngruppe
|
||||
study_group:
|
||||
name: Name
|
||||
external_id: Externe ID
|
||||
@ -347,6 +348,7 @@ de:
|
||||
implement: Implementieren
|
||||
test_files: Test-Dateien
|
||||
feedback: Feedback
|
||||
study_group_dashboard: Live Dashboard
|
||||
statistics:
|
||||
average_score: Durchschnittliche Punktzahl
|
||||
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.
|
||||
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.
|
||||
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:
|
||||
statistics:
|
||||
no_data_available: Keine Daten verfügbar.
|
||||
@ -373,7 +382,7 @@ de:
|
||||
score: Punktzahl
|
||||
tests: Unit Tests
|
||||
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:
|
||||
index:
|
||||
clone: Duplizieren
|
||||
|
@ -101,6 +101,7 @@ en:
|
||||
files: Files
|
||||
score: Score
|
||||
user: Author
|
||||
study_group: Study Group
|
||||
study_group:
|
||||
name: Name
|
||||
external_id: External ID
|
||||
@ -347,6 +348,7 @@ en:
|
||||
implement: Implement
|
||||
test_files: Test Files
|
||||
feedback: Feedback
|
||||
study_group_dashboard: Live Dashboard
|
||||
statistics:
|
||||
average_score: Average Score
|
||||
final_submissions: Final Submissions
|
||||
@ -365,6 +367,13 @@ en:
|
||||
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_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:
|
||||
statistics:
|
||||
no_data_available: No data available.
|
||||
@ -373,7 +382,7 @@ en:
|
||||
score: Score
|
||||
tests: Unit Test Results
|
||||
time_difference: 'Working Time until here*'
|
||||
addendum: '* Deltas longer than 10 minutes are ignored.'
|
||||
addendum: "* Deltas longer than %{delta} minutes are ignored."
|
||||
proxy_exercises:
|
||||
index:
|
||||
clone: Duplicate
|
||||
|
@ -83,6 +83,7 @@ Rails.application.routes.draw do
|
||||
get :feedback
|
||||
get :reload
|
||||
post :submit
|
||||
get 'study_group_dashboard/:study_group_id', to: 'exercises#study_group_dashboard'
|
||||
end
|
||||
end
|
||||
|
||||
@ -151,4 +152,5 @@ Rails.application.routes.draw do
|
||||
|
||||
post "/evaluate", to: 'remote_evaluation#evaluate', via: [:post]
|
||||
|
||||
mount ActionCable.server => '/cable'
|
||||
end
|
||||
|
@ -18,6 +18,7 @@ environment.plugins.prepend('Provide', new webpack.ProvidePlugin({
|
||||
_: 'underscore',
|
||||
vis: 'vis',
|
||||
hljs: 'highlight.js',
|
||||
d3: 'd3',
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -3,8 +3,11 @@
|
||||
default: &default
|
||||
source_path: app/javascript
|
||||
source_entry_path: packs
|
||||
public_root_path: public
|
||||
public_output_path: packs
|
||||
cache_path: tmp/cache/webpacker
|
||||
check_yarn_integrity: false
|
||||
webpack_compile_output: false
|
||||
|
||||
# Additional paths webpack should lookup modules
|
||||
# ['app/assets', 'engine/foo/app/assets']
|
||||
@ -13,7 +16,25 @@ default: &default
|
||||
# Reload manifest.json on all requests so we reload latest compiled packs
|
||||
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:
|
||||
- .mjs
|
||||
- .js
|
||||
- .sass
|
||||
- .scss
|
||||
@ -31,6 +52,9 @@ development:
|
||||
<<: *default
|
||||
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/
|
||||
dev_server:
|
||||
https: false
|
||||
@ -66,5 +90,8 @@ production:
|
||||
# Production depends on precompilation of packs prior to booting for performance.
|
||||
compile: false
|
||||
|
||||
# Extract and emit a css file
|
||||
extract_css: true
|
||||
|
||||
# Cache manifest.json for performance
|
||||
cache_manifest: true
|
||||
|
@ -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
|
@ -10,7 +10,7 @@
|
||||
#
|
||||
# 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
|
||||
enable_extension "plpgsql"
|
||||
@ -292,6 +292,8 @@ ActiveRecord::Schema.define(version: 2018_11_29_093207) do
|
||||
t.text "thank_you_note"
|
||||
t.boolean "full_score_reached", default: false
|
||||
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
|
||||
|
||||
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"
|
||||
end
|
||||
|
||||
add_foreign_key "submissions", "study_groups"
|
||||
end
|
||||
|
@ -1,9 +1,10 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@rails/webpacker": "3.5",
|
||||
"@rails/webpacker": "4.0",
|
||||
"bootstrap": "^4.1.3",
|
||||
"bootswatch": "^4.1.3",
|
||||
"chosen-js": "^1.8.7",
|
||||
"d3": "^5.9.1",
|
||||
"d3-tip": "^0.9.1",
|
||||
"font-awesome": "^4.7.0",
|
||||
"highlight.js": "^9.12.0",
|
||||
@ -17,7 +18,7 @@
|
||||
"webpack-merge": "^4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack-dev-server": "2.11.2"
|
||||
"webpack-dev-server": "3.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"webpack": "./bin/webpack",
|
||||
|
12
postcss.config.js
Normal file
12
postcss.config.js
Normal 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
|
||||
})
|
||||
]
|
||||
};
|
@ -5,9 +5,9 @@
|
||||
######## VERSION INFORMATION ########
|
||||
|
||||
postgres_version=10
|
||||
ruby_version=2.5.1
|
||||
rails_version=5.2.1
|
||||
geckodriver_version=0.23.0
|
||||
ruby_version=2.6.1
|
||||
rails_version=5.2.2
|
||||
geckodriver_version=0.24.0
|
||||
|
||||
########## INSTALL SCRIPT ###########
|
||||
|
||||
|
4
spec/support/factory_bot.rb
Normal file
4
spec/support/factory_bot.rb
Normal file
@ -0,0 +1,4 @@
|
||||
require 'factory_bot'
|
||||
|
||||
# Use "old" FactoryBot default to allow auto-creating associations for #build
|
||||
FactoryBot.use_parent_strategy = false
|
Reference in New Issue
Block a user