From 2d5125dfa23b192e978f1a775cc65219929024e5 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 3 Feb 2017 13:53:32 +0100 Subject: [PATCH 001/143] resize ace editors on toggle of description text --- app/assets/javascripts/editor/editor.js.erb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 50e14514..af411da4 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -167,6 +167,13 @@ configureEditors: function () { $('button i.fa-spin').hide(); }, + + resizeAceEditors: function (){ + $('.editor').each(function (index, element) { + this.resizeParentOfAceEditor(element); + }.bind(this)); + }, + resizeParentOfAceEditor: function (element){ // calculate needed size: window height - position of top of button-bar - 60 for bar itself and margins var windowHeight = window.innerHeight - $('#editor-buttons').offset().top - 60; @@ -559,6 +566,9 @@ configureEditors: function () { $('#description-panel').toggleClass('description-panel'); $('#description-symbol').toggleClass('fa-chevron-down'); $('#description-symbol').toggleClass('fa-chevron-right'); + // resize ace editors + this.resizeAceEditors(); + }, From 0ddcf3a5bb87e87cfcac3313dc4bba130bef2374 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 3 Feb 2017 18:29:22 +0100 Subject: [PATCH 002/143] remove # from socket url... --- app/assets/javascripts/editor/execution.js.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/execution.js.erb b/app/assets/javascripts/editor/execution.js.erb index 668ca81a..8d2c744e 100644 --- a/app/assets/javascripts/editor/execution.js.erb +++ b/app/assets/javascripts/editor/execution.js.erb @@ -6,7 +6,8 @@ CodeOceanEditorWebsocket = { sockURL.pathname = url; sockURL.protocol = '<%= DockerClient.config['ws_client_protocol'] %>'; - return sockURL.toString(); + //return the URL, but strip last # if there is one... + return sockURL.toString().replace(/#$/,''); }, initializeSocket: function(url) { From 76c7ddae9b18490720f419d9a30ca0573e630842 Mon Sep 17 00:00:00 2001 From: Tom Staubitz Date: Mon, 6 Feb 2017 20:30:22 +0100 Subject: [PATCH 003/143] removed relative_url_root for staging environment. The app can be accessed now via port 3000 instead of a subdirectory. --- config/environments/staging.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 7caf841e..745ed57b 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -99,6 +99,5 @@ Rails.application.configure do config.active_record.dump_schema_after_migration = false # Run on subfolder in production environment. - config.relative_url_root = '/co-staging' - + # config.relative_url_root = '/co-staging' end From a52b27bb59569ef936d6904ee84d614ad2aeabc9 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 8 Feb 2017 16:19:41 +0100 Subject: [PATCH 004/143] render html and or markdown for feedback messages --- app/assets/javascripts/editor/editor.js.erb | 2 +- app/controllers/concerns/submission_scoring.rb | 2 +- app/views/shared/_file.html.slim | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index af411da4..0be56058 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -370,7 +370,7 @@ configureEditors: function () { panel.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count); panel.find('.row .col-sm-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2))); panel.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight); - panel.find('.row .col-sm-9').eq(2).text(result.message); + panel.find('.row .col-sm-9').eq(2).html(result.message); if (result.error_messages) panel.find('.row .col-sm-9').eq(3).text(result.error_messages.join(', ')); panel.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index); }, diff --git a/app/controllers/concerns/submission_scoring.rb b/app/controllers/concerns/submission_scoring.rb index c8dfb0b3..16f1f061 100644 --- a/app/controllers/concerns/submission_scoring.rb +++ b/app/controllers/concerns/submission_scoring.rb @@ -25,7 +25,7 @@ module SubmissionScoring def feedback_message(file, score) set_locale - score == Assessor::MAXIMUM_SCORE ? I18n.t('exercises.implement.default_feedback') : file.feedback_message + score == Assessor::MAXIMUM_SCORE ? I18n.t('exercises.implement.default_feedback') : render_markdown(file.feedback_message) end def score_submission(submission) diff --git a/app/views/shared/_file.html.slim b/app/views/shared/_file.html.slim index 1e50388c..bac52d1a 100644 --- a/app/views/shared/_file.html.slim +++ b/app/views/shared/_file.html.slim @@ -5,6 +5,6 @@ = row(label: 'file.hidden', value: file.hidden) = row(label: 'file.read_only', value: file.read_only) - if file.teacher_defined_test? - = row(label: 'file.feedback_message', value: file.feedback_message) + = row(label: 'file.feedback_message', value: render_markdown(file.feedback_message)) = row(label: 'file.weight', value: file.weight) = row(label: 'file.content', value: file.native_file? ? link_to(file.native_file.file.filename, file.native_file.url) : code_tag(file.content)) From 9b8027e4c0b818bbf20e8b1f9208a3d6fd083ac5 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 14 Feb 2017 13:35:21 +0100 Subject: [PATCH 005/143] replaced exit-command for containers from 'exit' to '#exit', otherwise it will always really exit the container (which is then in state: exited) if we execute it in bash. --- app/controllers/submissions_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 16fcd697..361ed866 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -173,7 +173,7 @@ class SubmissionsController < ApplicationController def handle_message(message, tubesock, container) # Handle special commands first - if (/^exit/.match(message)) + if (/^#exit/.match(message)) kill_socket(tubesock) @docker_client.exit_container(container) else From 4a1721c6aa706e9406b2744c8e3c1f0ca6937c2a Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 22 Feb 2017 17:20:26 +0100 Subject: [PATCH 006/143] switch back from structure.sql to schema.rb --- config/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/application.rb b/config/application.rb index a9e3518b..97118237 100644 --- a/config/application.rb +++ b/config/application.rb @@ -28,7 +28,7 @@ module CodeOcean config.eager_load_paths << Rails.root.join('lib') config.assets.precompile += %w( markdown-buttons.png ) - config.active_record.schema_format = :sql + #config.active_record.schema_format = :sql case (RUBY_ENGINE) when 'ruby' From c474ac3d8731d8cc4e708f599b80eba8a27394a1 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 7 Mar 2017 16:00:35 +0100 Subject: [PATCH 007/143] removed invisible control characters --- lib/junit_adapter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/junit_adapter.rb b/lib/junit_adapter.rb index c5b57184..6230a785 100644 --- a/lib/junit_adapter.rb +++ b/lib/junit_adapter.rb @@ -2,7 +2,7 @@ class JunitAdapter < TestingFrameworkAdapter COUNT_REGEXP = /Tests run: (\d+)/ FAILURES_REGEXP = /Failures: (\d+)/ SUCCESS_REGEXP = /OK \((\d+) test[s]?\)/ - ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:\s(.​*)|org\.junit\.ComparisonFailure:\s(.*​)/ + ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:\s(.*)|org\.junit\.ComparisonFailure:\s(.*)/ def self.framework_name 'JUnit' From dec45880b9601e6fb05e2d2a8ce8225a7ab25c72 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 7 Mar 2017 18:09:31 +0100 Subject: [PATCH 008/143] fix edge compatibility in docker config file (wss:// --> wss: ). also clean up hash removal of url and add docker.yml to the files to be deployed by capistrano --- app/assets/javascripts/editor/execution.js.erb | 6 ++++-- config/deploy.rb | 2 +- config/docker.yml.erb | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/editor/execution.js.erb b/app/assets/javascripts/editor/execution.js.erb index 8d2c744e..37c53cb0 100644 --- a/app/assets/javascripts/editor/execution.js.erb +++ b/app/assets/javascripts/editor/execution.js.erb @@ -6,8 +6,10 @@ CodeOceanEditorWebsocket = { sockURL.pathname = url; sockURL.protocol = '<%= DockerClient.config['ws_client_protocol'] %>'; - //return the URL, but strip last # if there is one... - return sockURL.toString().replace(/#$/,''); + // strip anchor if it is in the url + sockURL.hash = '' + + return sockURL.toString(); }, initializeSocket: function(url) { diff --git a/config/deploy.rb b/config/deploy.rb index 173f2b56..f4b10182 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -4,7 +4,7 @@ set :default_env, 'PATH' => '/usr/java/jdk1.8.0_40/bin:$PATH' set :deploy_to, '/var/www/app' set :keep_releases, 3 set :linked_dirs, %w(log public/uploads tmp/cache tmp/files tmp/pids tmp/sockets) -set :linked_files, %w(config/action_mailer.yml config/code_ocean.yml config/database.yml config/newrelic.yml config/secrets.yml config/sendmail.yml config/smtp.yml) +set :linked_files, %w(config/action_mailer.yml config/docker.yml.erb config/code_ocean.yml config/database.yml config/newrelic.yml config/secrets.yml config/sendmail.yml config/smtp.yml) set :log_level, :info set :puma_threads, [0, 16] set :repo_url, 'git@github.com:openHPI/codeocean.git' diff --git a/config/docker.yml.erb b/config/docker.yml.erb index 8fac75d0..c1871de7 100644 --- a/config/docker.yml.erb +++ b/config/docker.yml.erb @@ -32,7 +32,7 @@ production: timeout: 60 workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %> ws_host: ws://localhost:4243 #url to connect rails server to docker host - ws_client_protocol: wss:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production) + ws_client_protocol: 'wss:' #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production) staging: <<: *default @@ -46,7 +46,7 @@ staging: timeout: 60 workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %> ws_host: ws://localhost:4243 #url to connect rails server to docker host - ws_client_protocol: wss:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production) + ws_client_protocol: 'wss:' #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production) test: <<: *default From 3c9ecda0add3413a84efdac3e64d32f532bcaf11 Mon Sep 17 00:00:00 2001 From: Alexander Kastius Date: Mon, 13 Mar 2017 01:07:05 +0100 Subject: [PATCH 009/143] Moved error messages to top of page --- app/views/application/_flash.html.slim | 2 +- lib/assets/stylesheets/flash.css.scss | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/views/application/_flash.html.slim b/app/views/application/_flash.html.slim index 0f854179..2b8563a4 100644 --- a/app/views/application/_flash.html.slim +++ b/app/views/application/_flash.html.slim @@ -1,3 +1,3 @@ -#flash data-message-failure=t('shared.message_failure') +#flash.fixed_error_messages data-message-failure=t('shared.message_failure') - %w[alert danger info notice success warning].each do |severity| p.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)}" id="flash-#{severity}" = flash[severity] diff --git a/lib/assets/stylesheets/flash.css.scss b/lib/assets/stylesheets/flash.css.scss index 1c805ebf..61b85bce 100644 --- a/lib/assets/stylesheets/flash.css.scss +++ b/lib/assets/stylesheets/flash.css.scss @@ -1,3 +1,13 @@ .flash { display: none; } + +.fixed_error_messages { + position: fixed; + top: 0; + left: 0; + width: 100%; + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; +} From b3acc29e255df54f1559c683e384a443e8d8e378 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 14 Mar 2017 13:03:53 +0100 Subject: [PATCH 010/143] positioning of flash message --- lib/assets/stylesheets/flash.css.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/assets/stylesheets/flash.css.scss b/lib/assets/stylesheets/flash.css.scss index 61b85bce..8d2899bf 100644 --- a/lib/assets/stylesheets/flash.css.scss +++ b/lib/assets/stylesheets/flash.css.scss @@ -7,7 +7,7 @@ top: 0; left: 0; width: 100%; - padding-left: 10px; - padding-right: 10px; - padding-top: 10px; + padding-left: 10%; + padding-right: 10%; + padding-top: 2%; } From c37f675893459290f37e1f79c9d2d4ca37d1c4e2 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 15 Mar 2017 11:34:27 +0100 Subject: [PATCH 011/143] clickthrough of alert banners, some more positioning, move them to front. --- app/views/application/_flash.html.slim | 2 +- lib/assets/stylesheets/flash.css.scss | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/views/application/_flash.html.slim b/app/views/application/_flash.html.slim index 2b8563a4..a391ee23 100644 --- a/app/views/application/_flash.html.slim +++ b/app/views/application/_flash.html.slim @@ -1,3 +1,3 @@ -#flash.fixed_error_messages data-message-failure=t('shared.message_failure') +#flash.fixed_error_messages.clickthrough data-message-failure=t('shared.message_failure') - %w[alert danger info notice success warning].each do |severity| p.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)}" id="flash-#{severity}" = flash[severity] diff --git a/lib/assets/stylesheets/flash.css.scss b/lib/assets/stylesheets/flash.css.scss index 8d2899bf..21ca7ec5 100644 --- a/lib/assets/stylesheets/flash.css.scss +++ b/lib/assets/stylesheets/flash.css.scss @@ -4,10 +4,16 @@ .fixed_error_messages { position: fixed; - top: 0; + z-index: 1000; + top: 110px; left: 0; width: 100%; padding-left: 10%; padding-right: 10%; - padding-top: 2%; + padding-top: 0; } + +.clickthrough { + pointer-events: none; +} + From fce6bb3410683326fcfc9e22479596af1d8bb30c Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 15 Mar 2017 11:51:09 +0100 Subject: [PATCH 012/143] CSS alert fixes for the internet explorer --- lib/assets/stylesheets/flash.css.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/assets/stylesheets/flash.css.scss b/lib/assets/stylesheets/flash.css.scss index 21ca7ec5..f96e91fd 100644 --- a/lib/assets/stylesheets/flash.css.scss +++ b/lib/assets/stylesheets/flash.css.scss @@ -15,5 +15,10 @@ .clickthrough { pointer-events: none; + + /* fixes for IE */ + background:white; + opacity:0; + filter:Alpha(opacity=0); } From 8f927d5ac9fa80137c9d5a4115c348d92ee03f5d Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Wed, 15 Mar 2017 16:15:29 +0100 Subject: [PATCH 013/143] some howto text for request_for_comment usage, changed background color of read-only editor. --- app/assets/stylesheets/request-for-comments.css.scss | 1 + app/views/request_for_comments/show.html.erb | 8 ++++++-- config/locales/de.yml | 6 ++++++ config/locales/en.yml | 6 ++++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/request-for-comments.css.scss b/app/assets/stylesheets/request-for-comments.css.scss index 02dab676..a219d21c 100644 --- a/app/assets/stylesheets/request-for-comments.css.scss +++ b/app/assets/stylesheets/request-for-comments.css.scss @@ -1,4 +1,5 @@ #commentitor { margin-top: 2rem; height: 600px; + background-color:#f9f9f9 } \ No newline at end of file diff --git a/app/views/request_for_comments/show.html.erb b/app/views/request_for_comments/show.html.erb index d0fe31af..28574ade 100644 --- a/app/views/request_for_comments/show.html.erb +++ b/app/views/request_for_comments/show.html.erb @@ -41,14 +41,18 @@ <% end %> +
+ <%= t('request_for_comments.howto_title') %>
<%= render_markdown(t('request_for_comments.howto')) %> +
- +
<% submission.files.each do |file| %> - <%= (file.path or "") + "/" + file.name + file.file_type.file_extension %> + <%= (file.path or "") + "/" + file.name + file.file_type.file_extension %>
+    <%= t('request_for_comments.click_here') %>
<%= file.content %>
<% end %> diff --git a/config/locales/de.yml b/config/locales/de.yml index de6fe41a..5b4329df 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -360,7 +360,13 @@ de: body: 'Bitte besuchen Sie %{link}, sofern Sie Ihr Passwort zurücksetzen wollen.' subject: Anweisungen zum Zurücksetzen Ihres Passworts request_for_comments: + click_here: Zum Kommentieren auf die Seitenleiste klicken! comments: Kommentare + howto: | + Um Kommentare zu einer Programmzeile hinzuzufügen, kann einfach auf die jeweilige Zeilennummer auf der linken Seite geklickt werden.
+ Es öffnet sich ein Textfeld, in dem der Kommentar eingetragen werden kann.
+ Mit "Kommentieren" wird der Kommentar dann gesichert und taucht als Sprechblase neben der Zeile auf. + howto_title: 'Anleitung:' index: get_my_comment_requests: Meine Kommentaranfragen all: "Alle Kommentaranfragen" diff --git a/config/locales/en.yml b/config/locales/en.yml index 304d7d0f..5541d68f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -381,7 +381,13 @@ en: body: 'Please visit %{link} if you want to reset your password.' subject: Password reset instructions request_for_comments: + click_here: Click on this sidebar to comment! comments: Comments + howto: | + To leave comments to a specific code line, click on the respective line number.
+ Enter your comment in the popup and save it by clicking "Comment this".
+ Your comment will show up next to the line number as a speech bubble symbol. + howto_title: 'How to comment:' index: all: All Requests for Comments get_my_comment_requests: My Requests for Comments From 0db11884bcdc5a3607d2f22372f7a4985b3d9bae Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Sun, 29 Jan 2017 20:26:45 +0100 Subject: [PATCH 014/143] Extended Exercises by worktime, difficulty and tags, added ProxyExercises as prework for recommendations Tags can be added to exercises in the edit view. Tags can monitored under /tags. Added the concept of ProxyExercises which are a collection of Exercises. They can be found under /proxy_exercises Added Interventions as prework to show interventions later to the user. Added exercise/[:id]/working_time to return the working time of the user in this exercise and the average working time of all users in this exercise --- app/controllers/exercises_controller.rb | 53 +++++++++++- app/controllers/proxy_exercises_controller.rb | 80 +++++++++++++++++++ app/controllers/tags_controller.rb | 55 +++++++++++++ app/models/concerns/user.rb | 4 + app/models/exercise.rb | 10 +++ app/models/exercise_collection.rb | 5 ++ app/models/exercise_tag.rb | 13 +++ app/models/intervention.rb | 15 ++++ app/models/proxy_exercise.rb | 27 +++++++ app/models/tag.rb | 22 +++++ app/models/user_exercise_feedback.rb | 8 ++ app/models/user_exercise_intervention.rb | 11 +++ app/models/user_proxy_exercise_exercise.rb | 14 ++++ app/policies/exercise_policy.rb | 2 +- app/policies/proxy_exercise_policy.rb | 34 ++++++++ app/policies/tag_policy.rb | 34 ++++++++ app/views/exercises/_editor.html.slim | 2 +- app/views/exercises/_form.html.slim | 19 +++++ app/views/exercises/implement.html.slim | 1 + app/views/exercises/index.html.slim | 6 ++ app/views/exercises/show.html.slim | 3 + app/views/proxy_exercises/_form.html.slim | 24 ++++++ app/views/proxy_exercises/edit.html.slim | 3 + app/views/proxy_exercises/index.html.slim | 35 ++++++++ app/views/proxy_exercises/new.html.slim | 3 + .../proxy_exercises/reload.json.jbuilder | 3 + app/views/proxy_exercises/show.html.slim | 23 ++++++ app/views/tags/_form.html.slim | 6 ++ app/views/tags/edit.html.slim | 3 + app/views/tags/index.html.slim | 19 +++++ app/views/tags/new.html.slim | 3 + app/views/tags/show.html.slim | 6 ++ config/locales/de.yml | 17 ++++ config/locales/en.yml | 17 ++++ config/routes.rb | 17 ++++ ...70205163247_create_exercise_collections.rb | 14 ++++ .../20170205165450_create_proxy_exercises.rb | 23 ++++++ .../20170205210357_create_interventions.rb | 16 ++++ db/migrate/20170206141210_add_tags.rb | 19 +++++ .../20170206152503_add_user_feedback.rb | 11 +++ 40 files changed, 675 insertions(+), 5 deletions(-) create mode 100644 app/controllers/proxy_exercises_controller.rb create mode 100644 app/controllers/tags_controller.rb create mode 100644 app/models/exercise_collection.rb create mode 100644 app/models/exercise_tag.rb create mode 100644 app/models/intervention.rb create mode 100644 app/models/proxy_exercise.rb create mode 100644 app/models/tag.rb create mode 100644 app/models/user_exercise_feedback.rb create mode 100644 app/models/user_exercise_intervention.rb create mode 100644 app/models/user_proxy_exercise_exercise.rb create mode 100644 app/policies/proxy_exercise_policy.rb create mode 100644 app/policies/tag_policy.rb create mode 100644 app/views/proxy_exercises/_form.html.slim create mode 100644 app/views/proxy_exercises/edit.html.slim create mode 100644 app/views/proxy_exercises/index.html.slim create mode 100644 app/views/proxy_exercises/new.html.slim create mode 100644 app/views/proxy_exercises/reload.json.jbuilder create mode 100644 app/views/proxy_exercises/show.html.slim create mode 100644 app/views/tags/_form.html.slim create mode 100644 app/views/tags/edit.html.slim create mode 100644 app/views/tags/index.html.slim create mode 100644 app/views/tags/new.html.slim create mode 100644 app/views/tags/show.html.slim create mode 100644 db/migrate/20170205163247_create_exercise_collections.rb create mode 100644 db/migrate/20170205165450_create_proxy_exercises.rb create mode 100644 db/migrate/20170205210357_create_interventions.rb create mode 100644 db/migrate/20170206141210_add_tags.rb create mode 100644 db/migrate/20170206152503_add_user_feedback.rb diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 79314208..f27324ad 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -6,7 +6,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, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit, :reload] + before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :run, :statistics, :submit, :reload] before_action :set_external_user, only: [:statistics] before_action :set_file_types, only: [:create, :edit, :new, :update] @@ -54,6 +54,20 @@ class ExercisesController < ApplicationController def create @exercise = Exercise.new(exercise_params) + collect_set_and_unset_exercise_tags + myparam = exercise_params + checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s } + removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s } + + for et in checked_exercise_tags + et.factor = params[:tag_factors][et.tag_id.to_s][:factor] + et.exercise = @exercise + end + + myparam[:exercise_tags] = checked_exercise_tags + myparam.delete :tag_ids + removed_exercise_tags.map {|et| et.destroy} + authorize! create_and_respond(object: @exercise) end @@ -63,6 +77,7 @@ class ExercisesController < ApplicationController end def edit + collect_set_and_unset_exercise_tags end def import_proforma_xml @@ -118,7 +133,8 @@ class ExercisesController < ApplicationController private :user_by_code_harbor_token def exercise_params - params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name) + params[:exercise][:expected_worktime_seconds] = params[:exercise][:expected_worktime_minutes].to_i * 60 + params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, :expected_worktime_seconds, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name) end private :exercise_params @@ -150,6 +166,12 @@ class ExercisesController < ApplicationController end end + def working_times + working_time_accumulated = Time.parse(@exercise.average_working_time_for_only(current_user.id) || "00:00:00").seconds_since_midnight + working_time_avg = Time.parse(@exercise.average_working_time || "00:00:00").seconds_since_midnight + render(json: {working_time_avg: working_time_avg, working_time_accumulated: working_time_accumulated}) + end + def index @search = policy_scope(Exercise).search(params[:q]) @exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page]) @@ -174,6 +196,8 @@ class ExercisesController < ApplicationController def new @exercise = Exercise.new + collect_set_and_unset_exercise_tags + authorize! end @@ -201,6 +225,16 @@ class ExercisesController < ApplicationController end private :set_file_types + def collect_set_and_unset_exercise_tags + @search = policy_scope(Tag).search(params[:q]) + @tags = @search.result.order(:name) + exercise_tags = @exercise.exercise_tags + tags_set = exercise_tags.collect{|e| e.tag}.to_set + tags_not_set = Tag.all.to_set.subtract tags_set + @exercise_tags = exercise_tags + tags_not_set.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag)} + end + private :collect_set_and_unset_exercise_tags + def show end @@ -252,7 +286,20 @@ class ExercisesController < ApplicationController private :transmit_lti_score def update - update_and_respond(object: @exercise, params: exercise_params) + collect_set_and_unset_exercise_tags + myparam = exercise_params + checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s } + removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s } + + for et in checked_exercise_tags + et.factor = params[:tag_factors][et.tag_id.to_s][:factor] + et.exercise = @exercise + end + + myparam[:exercise_tags] = checked_exercise_tags + myparam.delete :tag_ids + removed_exercise_tags.map {|et| et.destroy} + update_and_respond(object: @exercise, params: myparam) end def redirect_after_submit diff --git a/app/controllers/proxy_exercises_controller.rb b/app/controllers/proxy_exercises_controller.rb new file mode 100644 index 00000000..02fe0220 --- /dev/null +++ b/app/controllers/proxy_exercises_controller.rb @@ -0,0 +1,80 @@ +class ProxyExercisesController < ApplicationController + include CommonBehavior + + before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :reload] + + def authorize! + authorize(@proxy_exercise || @proxy_exercises) + end + private :authorize! + + def clone + proxy_exercise = @proxy_exercise.duplicate(token: nil, exercises: @proxy_exercise.exercises) + proxy_exercise.send(:generate_token) + if proxy_exercise.save + redirect_to(proxy_exercise, notice: t('shared.object_cloned', model: ProxyExercise.model_name.human)) + else + flash[:danger] = t('shared.message_failure') + redirect_to(@proxy_exercise) + end + end + + def create + myparams = proxy_exercise_params + myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject { |c| c.empty? }) + @proxy_exercise = ProxyExercise.new(myparams) + authorize! + + create_and_respond(object: @proxy_exercise) + end + + def destroy + destroy_and_respond(object: @proxy_exercise) + end + + def edit + @search = policy_scope(Exercise).search(params[:q]) + @exercises = @search.result.order(:title) + authorize! + end + + def proxy_exercise_params + params[:proxy_exercise].permit(:description, :title, :exercise_ids => []) + end + private :proxy_exercise_params + + def index + @search = policy_scope(ProxyExercise).search(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]) + @exercises = @search.result.order(:title) + authorize! + end + + def set_exercise + @proxy_exercise = ProxyExercise.find(params[:id]) + authorize! + end + private :set_exercise + + def show + @search = @proxy_exercise.exercises.search + @exercises = @proxy_exercise.exercises.search.result.order(:title) #@search.result.order(:title) + end + + #we might want to think about auth here + def reload + end + + def update + myparams = proxy_exercise_params + myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject { |c| c.blank? }) + update_and_respond(object: @proxy_exercise, params: myparams) + end + +end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb new file mode 100644 index 00000000..ff6925dd --- /dev/null +++ b/app/controllers/tags_controller.rb @@ -0,0 +1,55 @@ +class TagsController < ApplicationController + include CommonBehavior + + before_action :set_tag, only: MEMBER_ACTIONS + + def authorize! + authorize(@tag || @tags) + end + private :authorize! + + def create + @tag = Tag.new(tag_params) + authorize! + create_and_respond(object: @tag) + end + + def destroy + destroy_and_respond(object: @tag) + end + + def edit + end + + def tag_params + params[:tag].permit(:name) + end + private :tag_params + + def index + @tags = Tag.all.paginate(page: params[:page]) + authorize! + end + + def new + @tag = Tag.new + authorize! + end + + def set_tag + @tag = Tag.find(params[:id]) + authorize! + end + private :set_tag + + def show + end + + def update + update_and_respond(object: @tag, params: tag_params) + end + + def to_s + name + end +end diff --git a/app/models/concerns/user.rb b/app/models/concerns/user.rb index ee72a715..330ccacd 100644 --- a/app/models/concerns/user.rb +++ b/app/models/concerns/user.rb @@ -8,6 +8,10 @@ module User has_many :exercises, as: :user has_many :file_types, as: :user has_many :submissions, as: :user + has_many :user_proxy_exercise_exercises, as: :user + has_many :user_exercise_interventions, as: :user + has_many :interventions, through: :user_exercise_interventions + scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') } end diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 29f260c2..37e421a7 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -12,6 +12,15 @@ class Exercise < ActiveRecord::Base belongs_to :execution_environment has_many :submissions + has_and_belongs_to_many :proxy_exercises + has_many :user_proxy_exercise_exercises + has_and_belongs_to_many :exercise_collections + has_many :user_exercise_interventions + has_many :interventions, through: :user_exercise_interventions + has_many :exercise_tags + has_many :tags, through: :exercise_tags + accepts_nested_attributes_for :exercise_tags + has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions alias_method :users, :external_users @@ -105,6 +114,7 @@ class Exercise < ActiveRecord::Base def duplicate(attributes = {}) exercise = dup exercise.attributes = attributes + exercise_tags.each { |et| exercise.exercise_tags << et.dup } files.each { |file| exercise.files << file.dup } exercise end diff --git a/app/models/exercise_collection.rb b/app/models/exercise_collection.rb new file mode 100644 index 00000000..2dca0e9d --- /dev/null +++ b/app/models/exercise_collection.rb @@ -0,0 +1,5 @@ +class ExerciseCollection < ActiveRecord::Base + + has_and_belongs_to_many :exercises + +end \ No newline at end of file diff --git a/app/models/exercise_tag.rb b/app/models/exercise_tag.rb new file mode 100644 index 00000000..4b8ab3e5 --- /dev/null +++ b/app/models/exercise_tag.rb @@ -0,0 +1,13 @@ +class ExerciseTag < ActiveRecord::Base + + belongs_to :tag + belongs_to :exercise + + before_save :destroy_if_empty_exercise_or_tag + + private + def destroy_if_empty_exercise_or_tag + destroy if exercise_id.blank? || tag_id.blank? + end + +end \ No newline at end of file diff --git a/app/models/intervention.rb b/app/models/intervention.rb new file mode 100644 index 00000000..960a4188 --- /dev/null +++ b/app/models/intervention.rb @@ -0,0 +1,15 @@ +class Intervention < ActiveRecord::Base + + NAME = %w(overallSlower longSession syntaxErrors videoNotWatched) + + has_many :user_exercise_interventions + has_many :users, through: :user_exercise_interventions, source_type: "ExternalUser" + #belongs_to :user, polymorphic: true + #belongs_to :external_users, source: :user, source_type: ExternalUser + #belongs_to :internal_users, source: :user, source_type: InternalUser, through: :user_interventions + # alias_method :users, :external_users + #has_many :exercises, through: :user_interventions + + validates :name, inclusion: {in: NAME} + +end \ No newline at end of file diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb new file mode 100644 index 00000000..1f6d47c1 --- /dev/null +++ b/app/models/proxy_exercise.rb @@ -0,0 +1,27 @@ +class ProxyExercise < ActiveRecord::Base + + after_initialize :generate_token + + has_and_belongs_to_many :exercises + has_many :user_proxy_exercise_exercises + + def count_files + exercises.count + end + + def generate_token + self.token ||= SecureRandom.hex(4) + end + private :generate_token + + def duplicate(attributes = {}) + proxy_exercise = dup + proxy_exercise.attributes = attributes + proxy_exercise + end + + def to_s + title + end + +end \ No newline at end of file diff --git a/app/models/tag.rb b/app/models/tag.rb new file mode 100644 index 00000000..002ec687 --- /dev/null +++ b/app/models/tag.rb @@ -0,0 +1,22 @@ +class Tag < ActiveRecord::Base + + has_many :exercise_tags + has_many :exercises, through: :exercise_tags + + validates_uniqueness_of :name + + def destroy + if (can_be_destroyed?) + super + end + end + + def can_be_destroyed? + !exercises.any? + end + + def to_s + name + end + +end \ No newline at end of file diff --git a/app/models/user_exercise_feedback.rb b/app/models/user_exercise_feedback.rb new file mode 100644 index 00000000..d3ec09d5 --- /dev/null +++ b/app/models/user_exercise_feedback.rb @@ -0,0 +1,8 @@ +class UserExerciseFeedback < ActiveRecord::Base + + belongs_to :user, polymorphic: true + belongs_to :exercise + + validates :user_id, uniqueness: { scope: [:exercise_id, :user_type] } + +end \ No newline at end of file diff --git a/app/models/user_exercise_intervention.rb b/app/models/user_exercise_intervention.rb new file mode 100644 index 00000000..60824c34 --- /dev/null +++ b/app/models/user_exercise_intervention.rb @@ -0,0 +1,11 @@ +class UserExerciseIntervention < ActiveRecord::Base + + belongs_to :user, polymorphic: true + belongs_to :intervention + belongs_to :exercise + + validates :user, presence: true + validates :exercise, presence: true + validates :intervention, presence: true + +end \ No newline at end of file diff --git a/app/models/user_proxy_exercise_exercise.rb b/app/models/user_proxy_exercise_exercise.rb new file mode 100644 index 00000000..e7defae6 --- /dev/null +++ b/app/models/user_proxy_exercise_exercise.rb @@ -0,0 +1,14 @@ +class UserProxyExerciseExercise < ActiveRecord::Base + + belongs_to :user, polymorphic: true + belongs_to :exercise + belongs_to :proxy_exercise + + validates :user_id, presence: true + validates :user_type, presence: true + validates :exercise_id, presence: true + validates :proxy_exercise_id, presence: true + + validates :user_id, uniqueness: { scope: [:proxy_exercise_id, :user_type] } + +end \ No newline at end of file diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb index 55f7d16b..c89ad86a 100644 --- a/app/policies/exercise_policy.rb +++ b/app/policies/exercise_policy.rb @@ -16,7 +16,7 @@ class ExercisePolicy < AdminOrAuthorPolicy define_method(action) { admin? || author?} end - [:implement?, :submit?, :reload?].each do |action| + [:implement?, :working_times?, :submit?, :reload?].each do |action| define_method(action) { everyone } end diff --git a/app/policies/proxy_exercise_policy.rb b/app/policies/proxy_exercise_policy.rb new file mode 100644 index 00000000..28de525e --- /dev/null +++ b/app/policies/proxy_exercise_policy.rb @@ -0,0 +1,34 @@ +class ProxyExercisePolicy < AdminOrAuthorPolicy + def author? + @user == @record.author + end + private :author? + + def batch_update? + admin? + end + + def show? + @user.internal_user? + end + + [:clone?, :destroy?, :edit?, :update?].each do |action| + define_method(action) { admin? || author?} + end + + [:reload?].each do |action| + define_method(action) { everyone } + end + + class Scope < Scope + def resolve + if @user.admin? + @scope.all + elsif @user.internal_user? + @scope.where('user_id = ? OR public = TRUE', @user.id) + else + @scope.none + end + end + end +end diff --git a/app/policies/tag_policy.rb b/app/policies/tag_policy.rb new file mode 100644 index 00000000..8325b9fa --- /dev/null +++ b/app/policies/tag_policy.rb @@ -0,0 +1,34 @@ +class TagPolicy < AdminOrAuthorPolicy + def author? + @user == @record.author + end + private :author? + + def batch_update? + admin? + end + + def show? + @user.internal_user? + end + + [:clone?, :destroy?, :edit?, :update?].each do |action| + define_method(action) { admin? || author?} + end + + [:reload?].each do |action| + define_method(action) { everyone } + end + + class Scope < Scope + def resolve + if @user.admin? + @scope.all + elsif @user.internal_user? + @scope.where('user_id = ? OR public = TRUE', @user.id) + else + @scope.none + end + end + end +end diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 56986673..ff18968c 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -21,4 +21,4 @@ button style="display:none" id="autosave" -= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') += render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') \ No newline at end of file diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index c4159254..0811b447 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -32,6 +32,25 @@ label = f.check_box(:allow_auto_completion) = t('activerecord.attributes.exercise.allow_auto_completion') + .form-group + = f.label(t('activerecord.attributes.exercise.difficulty')) + = f.number_field :expected_difficulty, in: 1..10, step: 1 + .form-group + = f.label(t('activerecord.attributes.exercise.worktime')) + = f.number_field "expected_worktime_minutes", value: @exercise.expected_worktime_seconds / 60, in: 1..1000, step: 1 + h2 Tags + .table-responsive + table.table + thead + tr + th = t('activerecord.attributes.exercise.selection') + th = sort_link(@search, :title, t('activerecord.attributes.tag.name')) + th = t('activerecord.attributes.tag.difficulty') + = collection_check_boxes :exercise, :tag_ids, @exercise_tags, :tag_id, :id do |b| + tr + td = b.check_box + td = b.object.tag.name + td = number_field "tag_factors[#{b.object.tag.id}]", :factor, :value => b.object.factor, in: 1..10, step: 1 h2 = t('activerecord.attributes.exercise.files') ul#files.list-unstyled.panel-group = f.fields_for :files do |files_form| diff --git a/app/views/exercises/implement.html.slim b/app/views/exercises/implement.html.slim index 87ff4e1f..300775a6 100644 --- a/app/views/exercises/implement.html.slim +++ b/app/views/exercises/implement.html.slim @@ -22,3 +22,4 @@ #questions-column #questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}" = qa_js_tag + diff --git a/app/views/exercises/index.html.slim b/app/views/exercises/index.html.slim index 9ad5cc25..bd8fe880 100644 --- a/app/views/exercises/index.html.slim +++ b/app/views/exercises/index.html.slim @@ -16,6 +16,9 @@ h1 = Exercise.model_name.human(count: 2) th = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment')) th = t('.test_files') th = t('activerecord.attributes.exercise.maximum_score') + th = t('activerecord.attributes.exercise.tags') + th = t('activerecord.attributes.exercise.difficulty') + th = t('activerecord.attributes.exercise.worktime') th = t('activerecord.attributes.exercise.public') - if policy(Exercise).batch_update? @@ -29,6 +32,9 @@ h1 = Exercise.model_name.human(count: 2) td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) td = exercise.files.teacher_defined_tests.count td = exercise.maximum_score + td = exercise.exercise_tags.count + td = exercise.expected_difficulty + td = (exercise.expected_worktime_seconds / 60).ceil td.public data-value=exercise.public? = symbol_for(exercise.public?) td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit? td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement? diff --git a/app/views/exercises/show.html.slim b/app/views/exercises/show.html.slim index 902f8135..1efbd612 100644 --- a/app/views/exercises/show.html.slim +++ b/app/views/exercises/show.html.slim @@ -19,6 +19,9 @@ h1 = row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?) = row(label: 'exercise.embedding_parameters') do = content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise)) += row(label: 'exercise.difficulty', value: @exercise.expected_difficulty) += row(label: 'exercise.worktime', value: "#{@exercise.expected_worktime_seconds/60} min") += row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", ")) h2 = t('activerecord.attributes.exercise.files') diff --git a/app/views/proxy_exercises/_form.html.slim b/app/views/proxy_exercises/_form.html.slim new file mode 100644 index 00000000..bd57bf06 --- /dev/null +++ b/app/views/proxy_exercises/_form.html.slim @@ -0,0 +1,24 @@ += form_for(@proxy_exercise, multipart: true) do |f| + = render('shared/form_errors', object: @proxy_exercise) + .form-group + = f.label(:title) + = f.text_field(:title, class: 'form-control', required: true) + .form-group + = f.label(:description) + = f.pagedown_editor :description + + h3 Exercises + .table-responsive + table.table + thead + tr + th = t('activerecord.attributes.exercise.selection') + th = sort_link(@search, :title, t('activerecord.attributes.submission.exercise')) + th = sort_link(@search, :created_at, t('shared.created_at')) + = collection_check_boxes :proxy_exercise, :exercise_ids, @exercises, :id, :title do |b| + tr + td = b.check_box + td = link_to(b.object, b.object) + td = l(b.object.created_at, format: :short) + + .actions = render('shared/submit_button', f: f, object: @proxy_exercise) \ No newline at end of file diff --git a/app/views/proxy_exercises/edit.html.slim b/app/views/proxy_exercises/edit.html.slim new file mode 100644 index 00000000..8aa200c9 --- /dev/null +++ b/app/views/proxy_exercises/edit.html.slim @@ -0,0 +1,3 @@ +h1 = t('activerecord.models.proxy_exercise.one', model: ProxyExercise.model_name.human)+ ": " + @proxy_exercise.title + += render('form') diff --git a/app/views/proxy_exercises/index.html.slim b/app/views/proxy_exercises/index.html.slim new file mode 100644 index 00000000..2a8067c1 --- /dev/null +++ b/app/views/proxy_exercises/index.html.slim @@ -0,0 +1,35 @@ +h1 = ProxyExercise.model_name.human(count: 2) + += render(layout: 'shared/form_filters') do |f| + .form-group + = f.label(:title_cont, t('activerecord.attributes.proxy_exercise.title'), class: 'sr-only') + = f.search_field(:title_cont, class: 'form-control', placeholder: t('activerecord.attributes.proxy_exercise.title')) + +.table-responsive + table.table + thead + tr + th = sort_link(@search, :title, t('activerecord.attributes.proxy_exercise.title')) + th = "Token" + th = t('activerecord.attributes.proxy_exercise.files_count') + th colspan=6 = t('shared.actions') + tbody + - @proxy_exercises.each do |proxy_exercise| + tr data-id=proxy_exercise.id + td = link_to(proxy_exercise.title,proxy_exercise) + td = proxy_exercise.token + td = proxy_exercise.count_files + td = link_to(t('shared.edit'), edit_proxy_exercise_path(proxy_exercise)) if policy(proxy_exercise).edit? + + td + .btn-group + button.btn.btn-primary-outline.btn-xs.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button') + span.caret + span.sr-only Toggle Dropdown + ul.dropdown-menu.pull-right role="menu" + li = link_to(t('shared.show'), proxy_exercise) if policy(proxy_exercise).show? + li = link_to(t('shared.destroy'), proxy_exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(proxy_exercise).destroy? + li = link_to(t('.clone'), clone_proxy_exercise_path(proxy_exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(proxy_exercise).clone? + += render('shared/pagination', collection: @proxy_exercises) +p = render('shared/new_button', model: ProxyExercise) diff --git a/app/views/proxy_exercises/new.html.slim b/app/views/proxy_exercises/new.html.slim new file mode 100644 index 00000000..ae59a292 --- /dev/null +++ b/app/views/proxy_exercises/new.html.slim @@ -0,0 +1,3 @@ +h1 = t('shared.new_model', model: ProxyExercise.model_name.human) + += render('form') diff --git a/app/views/proxy_exercises/reload.json.jbuilder b/app/views/proxy_exercises/reload.json.jbuilder new file mode 100644 index 00000000..8e5d4e3c --- /dev/null +++ b/app/views/proxy_exercises/reload.json.jbuilder @@ -0,0 +1,3 @@ +json.set! :files do + json.array! @exercise.files.visible, :content, :id +end diff --git a/app/views/proxy_exercises/show.html.slim b/app/views/proxy_exercises/show.html.slim new file mode 100644 index 00000000..c1888d79 --- /dev/null +++ b/app/views/proxy_exercises/show.html.slim @@ -0,0 +1,23 @@ +- content_for :head do + = javascript_include_tag('http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js') + = stylesheet_link_tag('http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css') + +h1 + = @proxy_exercise.title + - if policy(@proxy_exercise).edit? + = render('shared/edit_button', object: @proxy_exercise) + += row(label: 'exercise.title', value: @proxy_exercise.title) += row(label: 'proxy_exercise.files_count', value: @exercises.count) += row(label: 'exercise.description', value: @proxy_exercise.description) +h3 Exercises +.table-responsive + table.table + thead + tr + th = sort_link(@search, :title, t('activerecord.attributes.submission.exercise')) + th = sort_link(@search, :created_at, t('shared.created_at')) + - @proxy_exercise.exercises.each do |exercise| + tr + td = link_to(exercise.title, exercise) + td = l(exercise.created_at, format: :short) diff --git a/app/views/tags/_form.html.slim b/app/views/tags/_form.html.slim new file mode 100644 index 00000000..4f02a28f --- /dev/null +++ b/app/views/tags/_form.html.slim @@ -0,0 +1,6 @@ += form_for(@tag) do |f| + = render('shared/form_errors', object: @tag) + .form-group + = f.label(:name) + = f.text_field(:name, class: 'form-control', required: true) + .actions = render('shared/submit_button', f: f, object: @tag) diff --git a/app/views/tags/edit.html.slim b/app/views/tags/edit.html.slim new file mode 100644 index 00000000..23c76720 --- /dev/null +++ b/app/views/tags/edit.html.slim @@ -0,0 +1,3 @@ +h1 = @tag.name + += render('form') diff --git a/app/views/tags/index.html.slim b/app/views/tags/index.html.slim new file mode 100644 index 00000000..2d4916af --- /dev/null +++ b/app/views/tags/index.html.slim @@ -0,0 +1,19 @@ +h1 = Tag.model_name.human(count: 2) + +.table-responsive + table.table + thead + tr + th = t('activerecord.attributes.hint.name') + /th = t('activerecord.attributes.hint.locale') + /th colspan=3 = t('shared.actions') + tbody + - @tags.each do |tag| + tr + td = tag.name + td = link_to(t('shared.show'), tag) + td = link_to(t('shared.edit'), edit_tag_path(tag)) + td = link_to(t('shared.destroy'), tag, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if tag.can_be_destroyed? + += render('shared/pagination', collection: @tags) +p = render('shared/new_button', model: Tag, path: new_tag_path) diff --git a/app/views/tags/new.html.slim b/app/views/tags/new.html.slim new file mode 100644 index 00000000..a933bede --- /dev/null +++ b/app/views/tags/new.html.slim @@ -0,0 +1,3 @@ +h1 = t('shared.new_model', model: Hint.model_name.human) + += render('form') diff --git a/app/views/tags/show.html.slim b/app/views/tags/show.html.slim new file mode 100644 index 00000000..81eda745 --- /dev/null +++ b/app/views/tags/show.html.slim @@ -0,0 +1,6 @@ +h1 + = @tag.name + = render('shared/edit_button', object: @tag) + += row(label: 'tag.name', value: @tag.name) += row(label: 'tag.usage', value: @tag.exercises.count) diff --git a/config/locales/de.yml b/config/locales/de.yml index 5b4329df..09c6932c 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -27,6 +27,7 @@ de: exercise: description: Beschreibung embedding_parameters: Parameter für LTI-Einbettung + tags: Tags execution_environment: Ausführungsumgebung execution_environment_id: Ausführungsumgebung files: Dateien @@ -34,10 +35,16 @@ de: instructions: Anweisungen maximum_score: Erreichbare Punktzahl public: Öffentlich + selection: Ausgewählt title: Titel user: Autor allow_auto_completion: "Autovervollständigung aktivieren" allow_file_creation: "Dateierstellung erlauben" + difficulty: Schwierigkeitsgrad + worktime: "vermutete Arbeitszeit in Minuten" + proxy_exercise: + title: Title + files_count: Anzahl der Aufgaben external_user: consumer: Konsument email: E-Mail @@ -91,6 +98,10 @@ de: files: Dateien score: Punktzahl user: Autor + tag: + name: Name + usage: Verwendet + difficulty: Anteil an der Aufgabe file_template: name: "Name" file_type: "Dateityp" @@ -111,6 +122,9 @@ de: exercise: one: Aufgabe other: Aufgaben + proxy_exercise: + one: Proxy Aufgabe + other: Proxy Aufgaben external_user: one: Externer Nutzer other: Externe Nutzer @@ -290,6 +304,9 @@ de: tests: Unit Tests time_difference: 'Arbeitszeit bis hier*' addendum: '* Differenzen von mehr als 30 Minuten werden ignoriert.' + proxy_exercises: + index: + clone: Duplizieren external_users: statistics: title: Statistiken für Externe Benutzer diff --git a/config/locales/en.yml b/config/locales/en.yml index 5541d68f..e3c0bc6f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -48,6 +48,7 @@ en: exercise: description: Description embedding_parameters: LTI Embedding Parameters + tags: Tags execution_environment: Execution Environment execution_environment_id: Execution Environment files: Files @@ -55,10 +56,16 @@ en: instructions: Instructions maximum_score: Maximum Score public: Public + selection: Selected title: Title user: Author allow_auto_completion: "Allow auto completion" allow_file_creation: "Allow file creation" + difficulty: Difficulty + worktime: "Expected worktime in minutes" + proxy_exercise: + title: Title + files_count: Exercises Count external_user: consumer: Consumer email: Email @@ -112,6 +119,10 @@ en: files: Files score: Score user: Author + tag: + name: Name + usage: Used + difficulty: Share on the Exercise file_template: name: "Name" file_type: "File Type" @@ -132,6 +143,9 @@ en: exercise: one: Exercise other: Exercises + proxy_exercise: + one: Proxy Exercise + other: Proxy Exercises external_user: one: External User other: External Users @@ -311,6 +325,9 @@ en: tests: Unit Test Results time_difference: 'Working Time until here*' addendum: '* Deltas longer than 30 minutes are ignored.' + proxy_exercises: + index: + clone: Duplicate external_users: statistics: title: External User Statistics diff --git a/config/routes.rb b/config/routes.rb index b4606f74..87cde74c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -60,12 +60,29 @@ Rails.application.routes.draw do member do post :clone get :implement + get :working_times get :statistics get :reload post :submit end end + resources :proxy_exercises do + member do + post :clone + get :reload + post :submit + end + end + + resources :tags do + member do + post :clone + get :reload + post :submit + end + end + resources :external_users, only: [:index, :show], concerns: :statistics do resources :exercises, concerns: :statistics end diff --git a/db/migrate/20170205163247_create_exercise_collections.rb b/db/migrate/20170205163247_create_exercise_collections.rb new file mode 100644 index 00000000..ef27f756 --- /dev/null +++ b/db/migrate/20170205163247_create_exercise_collections.rb @@ -0,0 +1,14 @@ +class CreateExerciseCollections < ActiveRecord::Migration + def change + create_table :exercise_collections do |t| + t.string :name + t.timestamps + end + + create_table :exercise_collections_exercises, id: false do |t| + t.belongs_to :exercise_collection, index: true + t.belongs_to :exercise, index: true + end + + end +end diff --git a/db/migrate/20170205165450_create_proxy_exercises.rb b/db/migrate/20170205165450_create_proxy_exercises.rb new file mode 100644 index 00000000..fb2704ce --- /dev/null +++ b/db/migrate/20170205165450_create_proxy_exercises.rb @@ -0,0 +1,23 @@ +class CreateProxyExercises < ActiveRecord::Migration + def change + create_table :proxy_exercises do |t| + t.string :title + t.string :description + t.string :token + t.timestamps + end + + create_table :exercises_proxy_exercises, id: false do |t| + t.belongs_to :proxy_exercise, index: true + t.belongs_to :exercise, index: true + t.timestamps + end + + create_table :user_proxy_exercise_exercises do |t| + t.belongs_to :user, polymorphic: true, index: true + t.belongs_to :proxy_exercise, index: true + t.belongs_to :exercise, index: true + t.timestamps + end + end +end diff --git a/db/migrate/20170205210357_create_interventions.rb b/db/migrate/20170205210357_create_interventions.rb new file mode 100644 index 00000000..1b7a8121 --- /dev/null +++ b/db/migrate/20170205210357_create_interventions.rb @@ -0,0 +1,16 @@ +class CreateInterventions < ActiveRecord::Migration + def change + create_table :user_exercise_interventions do |t| + t.belongs_to :user, polymorphic: true + t.belongs_to :exercise + t.belongs_to :intervention + t.timestamps + end + + create_table :interventions do |t| + t.string :name + t.text :markup + t.timestamps + end + end +end diff --git a/db/migrate/20170206141210_add_tags.rb b/db/migrate/20170206141210_add_tags.rb new file mode 100644 index 00000000..fd95b4dc --- /dev/null +++ b/db/migrate/20170206141210_add_tags.rb @@ -0,0 +1,19 @@ +class AddTags < ActiveRecord::Migration + + def change + add_column :exercises, :expected_worktime_seconds, :integer, default: 0 + add_column :exercises, :expected_difficulty, :integer, default: 1 + + create_table :tags do |t| + t.string :name, null: false + t.timestamps + end + + create_table :exercise_tags do |t| + t.belongs_to :exercise + t.belongs_to :tag + t.integer :factor, default: 0 + end + end + +end diff --git a/db/migrate/20170206152503_add_user_feedback.rb b/db/migrate/20170206152503_add_user_feedback.rb new file mode 100644 index 00000000..f62ccd9d --- /dev/null +++ b/db/migrate/20170206152503_add_user_feedback.rb @@ -0,0 +1,11 @@ +class AddUserFeedback < ActiveRecord::Migration + def change + create_table :user_exercise_feedbacks do |t| + t.belongs_to :exercise, null: false + t.belongs_to :user, polymorphic: true, null: false + t.integer :difficulty + t.integer :working_time_seconds + t.string :feedback_text + end + end +end From 4298e7050e37defac4b184d685d9fc3977ae35ed Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 15 Feb 2017 11:32:12 +0100 Subject: [PATCH 015/143] branched from prExtendByProxyExerciseAndTags --- deleteme.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 deleteme.txt diff --git a/deleteme.txt b/deleteme.txt new file mode 100644 index 00000000..e69de29b From 25087232dd84fe7a26bfbdc91c0656c1e3935cc4 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 15 Feb 2017 15:55:17 +0100 Subject: [PATCH 016/143] added relative knowledge loss function --- app/models/concerns/user.rb | 1 + app/models/proxy_exercise.rb | 43 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/app/models/concerns/user.rb b/app/models/concerns/user.rb index 330ccacd..28601cdf 100644 --- a/app/models/concerns/user.rb +++ b/app/models/concerns/user.rb @@ -11,6 +11,7 @@ module User has_many :user_proxy_exercise_exercises, as: :user has_many :user_exercise_interventions, as: :user has_many :interventions, through: :user_exercise_interventions + accepts_nested_attributes_for :user_proxy_exercise_exercises scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') } diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 1f6d47c1..2bf3658b 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -24,4 +24,47 @@ class ProxyExercise < ActiveRecord::Base title end + def selectMatchingExercise(user) + assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first + recommendedExercise = + if (assigned_user_proxy_exercise) + Rails.logger.info("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" ) + assigned_user_proxy_exercise.exercise + else + Rails.logger.info("find new matching exercise for user #{user.id}" ) + matchingExercise = findMatchingExercise(user) + user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matchingExercise, proxy_exercise: self) + matchingExercise + end + recommendedExercise + end + + def findMatchingExercise(user) + exercises.shuffle.first + end + + def score(user, ex) + 1 + end + + def getRelativeKnowledgeLoss(user, execises) + # initialize knowledge for each tag with 0 + topic_knowledge_loss_user = Tag.all.map{|t| [t, 0]}.to_h + topic_knowledge_max = Tag.all.map{|t| [t, 0]}.to_h + execises.each do |ex| + score = score(user, ex) + ex.tags.each do |t| + tag_ratio = ex.exercise_tags.where(tag: t).factor / ex.exercise_tags.inject(0){|sum, et| sum += et.factor } + topic_knowledge = ex.expected_difficulty * tag_ratio + topic_knowledge_loss_user[t] += (1-score) * topic_knowledge + topic_knowledge_max[t] += topic_knowledge + end + end + relative_loss = {} + topic_knowledge_max.keys.each do |t| + relative_loss[t] = topic_knowledge_loss_user[t] / topic_knowledge_max[t] + end + relative_loss + end + end \ No newline at end of file From eadaf9fd1b706d979eeeef2966cd009346cad415 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 15 Feb 2017 17:12:46 +0100 Subject: [PATCH 017/143] added matrix and score/time calculations --- app/models/exercise.rb | 10 +++++++--- app/models/proxy_exercise.rb | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 37e421a7..0f691fde 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -107,7 +107,7 @@ class Exercise < ActiveRecord::Base (created_at - lag(created_at) over (PARTITION BY user_id ORDER BY created_at)) AS working_time FROM submissions - WHERE exercise_id=#{id} and user_id=#{user_id}) AS foo) AS bar + WHERE exercise_id=#{id} and user_id=#{user_id} and user_type='ExternalUser') AS foo) AS bar """).first["working_time"] end @@ -172,8 +172,12 @@ class Exercise < ActiveRecord::Base end private :generate_token - def maximum_score - files.teacher_defined_tests.sum(:weight) + def maximum_score(*user) + if user + submissions.where(user: user, cause: "assess").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0 + else + files.teacher_defined_tests.sum(:weight) + end end def set_default_values diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 2bf3658b..2efa8962 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -43,8 +43,22 @@ class ProxyExercise < ActiveRecord::Base exercises.shuffle.first end + # [score][quantile] + def scoring_matrix + [ + [0 ,0 ,0 ,0 ,0 ], + [0.2,0.2,0.2,0.2,0.1], + [0.5,0.5,0.4,0.4,0.3], + [0.6,0.6,0.5,0.5,0.4], + [1 ,1 ,0.9,0.8,0.7], + ] + end + def score(user, ex) - 1 + points_ratio = ex.maximum_score(user) / ex.maximum_score.to_f + working_time_user = Time.parse(ex.average_working_time_for_only(user.id) || "00:00:00") + scoring_matrix = scoring_matrix + end def getRelativeKnowledgeLoss(user, execises) From 6acd5bb9057b1728439e27979f68bfb5e5001f9e Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 15 Feb 2017 21:19:33 +0100 Subject: [PATCH 018/143] added quantile calculations per exercise, added scoring matrix usage --- app/models/exercise.rb | 24 ++++++++++++++++++++++++ app/models/proxy_exercise.rb | 19 ++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 0f691fde..0a266532 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -75,6 +75,30 @@ class Exercise < ActiveRecord::Base """ end + def getQuantiles(quantiles) + quantiles_str = "[" + quantiles.join(",") + "]" + result = self.class.connection.execute(""" + SELECT unnest(PERCENTILE_CONT(ARRAY#{quantiles_str}) WITHIN GROUP (ORDER BY working_time)) + FROM + ( + SELECT user_id, + sum(working_time_new) AS working_time + FROM + (SELECT user_id, + CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new + FROM + (SELECT user_id, + id, + (created_at - lag(created_at) OVER (PARTITION BY user_id + ORDER BY created_at)) AS working_time + FROM submissions + WHERE exercise_id=69) AS foo) AS bar + GROUP BY user_id + ) AS foo + """) + quantiles.each_with_index.map{|q,i| [q, Time.parse(result[i]["unnest"]).seconds_since_midnight]}.to_h + end + def retrieve_working_time_statistics @working_time_statistics = {} self.class.connection.execute(user_working_time_query).each do |tuple| diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 2efa8962..678aeb4a 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -54,11 +54,24 @@ class ProxyExercise < ActiveRecord::Base ] end + def scoring_matrix_quantiles + [0.2,0.4,0.6,0.8] + end + def score(user, ex) points_ratio = ex.maximum_score(user) / ex.maximum_score.to_f + points_ratio_index = points_ratio.to_i working_time_user = Time.parse(ex.average_working_time_for_only(user.id) || "00:00:00") scoring_matrix = scoring_matrix - + quantiles_working_time = ex.getQuantiles(scoring_matrix_quantiles) + quantile_index = quantile_time.size + quantiles_working_time.each_with_index do |quantile_time, i| + if working_time_user <= quantile_time + quantile_index = i + break + end + end + scoring_matrix[points_ratio_index][quantile_index] end def getRelativeKnowledgeLoss(user, execises) @@ -66,11 +79,11 @@ class ProxyExercise < ActiveRecord::Base topic_knowledge_loss_user = Tag.all.map{|t| [t, 0]}.to_h topic_knowledge_max = Tag.all.map{|t| [t, 0]}.to_h execises.each do |ex| - score = score(user, ex) + user_score_factor = score(user, ex) ex.tags.each do |t| tag_ratio = ex.exercise_tags.where(tag: t).factor / ex.exercise_tags.inject(0){|sum, et| sum += et.factor } topic_knowledge = ex.expected_difficulty * tag_ratio - topic_knowledge_loss_user[t] += (1-score) * topic_knowledge + topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge topic_knowledge_max[t] += topic_knowledge end end From fcb82d29a1d97f3e858a9097560f747c1a513f8b Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 16 Feb 2017 11:39:26 +0100 Subject: [PATCH 019/143] quantiles are returned in array and not hash anymore. optional param failed, fixed --- app/models/exercise.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 0a266532..dc4181b5 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -96,7 +96,7 @@ class Exercise < ActiveRecord::Base GROUP BY user_id ) AS foo """) - quantiles.each_with_index.map{|q,i| [q, Time.parse(result[i]["unnest"]).seconds_since_midnight]}.to_h + quantiles.each_with_index.map{|q,i| Time.parse(result[i]["unnest"]).seconds_since_midnight} end def retrieve_working_time_statistics @@ -196,7 +196,7 @@ class Exercise < ActiveRecord::Base end private :generate_token - def maximum_score(*user) + def maximum_score(user = nil) if user submissions.where(user: user, cause: "assess").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0 else From 37774d8ed51181a6ab2bb2418bad0431a8715517 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 16 Feb 2017 11:40:11 +0100 Subject: [PATCH 020/143] added debug, fixed bugs in knowledge algorithm. was working fine with Exercise 50 and user 1817 --- app/models/proxy_exercise.rb | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 678aeb4a..cd609d01 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -60,35 +60,44 @@ class ProxyExercise < ActiveRecord::Base def score(user, ex) points_ratio = ex.maximum_score(user) / ex.maximum_score.to_f - points_ratio_index = points_ratio.to_i - working_time_user = Time.parse(ex.average_working_time_for_only(user.id) || "00:00:00") - scoring_matrix = scoring_matrix + if points_ratio == 0.0 + Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: points_ratio=#{points_ratio} score: 0" ) + return 0.0 + end + points_ratio_index = ((scoring_matrix.size - 1) * points_ratio).to_i + working_time_user = Time.parse(ex.average_working_time_for_only(user.id) || "00:00:00").seconds_since_midnight quantiles_working_time = ex.getQuantiles(scoring_matrix_quantiles) - quantile_index = quantile_time.size + quantile_index = quantiles_working_time.size quantiles_working_time.each_with_index do |quantile_time, i| if working_time_user <= quantile_time quantile_index = i break end end + Rails.logger.debug( + "scoring user #{user.id} exercise #{ex.id}: worktime #{working_time_user}, points: #{points_ratio}" \ + "(index #{points_ratio_index}) quantiles #{quantiles_working_time} placed into quantile index #{quantile_index} " \ + "score: #{scoring_matrix[points_ratio_index][quantile_index]}") scoring_matrix[points_ratio_index][quantile_index] end def getRelativeKnowledgeLoss(user, execises) # initialize knowledge for each tag with 0 - topic_knowledge_loss_user = Tag.all.map{|t| [t, 0]}.to_h - topic_knowledge_max = Tag.all.map{|t| [t, 0]}.to_h + all_used_tags = execises.inject(Set.new){|tagset, ex| tagset.merge(ex.tags)} + topic_knowledge_loss_user = all_used_tags.map{|t| [t, 0]}.to_h + topic_knowledge_max = all_used_tags.map{|t| [t, 0]}.to_h execises.each do |ex| user_score_factor = score(user, ex) ex.tags.each do |t| - tag_ratio = ex.exercise_tags.where(tag: t).factor / ex.exercise_tags.inject(0){|sum, et| sum += et.factor } + tag_ratio = ex.exercise_tags.where(tag: t).first.factor / ex.exercise_tags.inject(0){|sum, et| sum += et.factor } topic_knowledge = ex.expected_difficulty * tag_ratio topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge topic_knowledge_max[t] += topic_knowledge end end relative_loss = {} - topic_knowledge_max.keys.each do |t| + puts all_used_tags.size + all_used_tags.each do |t| relative_loss[t] = topic_knowledge_loss_user[t] / topic_knowledge_max[t] end relative_loss From f5b96f06cf112dd69b5cff4c264fb6c55080cfa9 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 16 Feb 2017 11:41:35 +0100 Subject: [PATCH 021/143] fixed typo --- app/models/proxy_exercise.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index cd609d01..017f1270 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -81,12 +81,12 @@ class ProxyExercise < ActiveRecord::Base scoring_matrix[points_ratio_index][quantile_index] end - def getRelativeKnowledgeLoss(user, execises) + def getRelativeKnowledgeLoss(user, exercises) # initialize knowledge for each tag with 0 - all_used_tags = execises.inject(Set.new){|tagset, ex| tagset.merge(ex.tags)} + all_used_tags = exercises.inject(Set.new){|tagset, ex| tagset.merge(ex.tags)} topic_knowledge_loss_user = all_used_tags.map{|t| [t, 0]}.to_h topic_knowledge_max = all_used_tags.map{|t| [t, 0]}.to_h - execises.each do |ex| + exercises.each do |ex| user_score_factor = score(user, ex) ex.tags.each do |t| tag_ratio = ex.exercise_tags.where(tag: t).first.factor / ex.exercise_tags.inject(0){|sum, et| sum += et.factor } From 91e4680f85cf8a4cfc7d07399b13b84cb21a1326 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 21 Feb 2017 11:11:40 +0100 Subject: [PATCH 022/143] minor fix --- app/models/proxy_exercise.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 017f1270..82825bda 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -90,13 +90,12 @@ class ProxyExercise < ActiveRecord::Base user_score_factor = score(user, ex) ex.tags.each do |t| tag_ratio = ex.exercise_tags.where(tag: t).first.factor / ex.exercise_tags.inject(0){|sum, et| sum += et.factor } - topic_knowledge = ex.expected_difficulty * tag_ratio - topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge - topic_knowledge_max[t] += topic_knowledge + topic_knowledge_ratio = ex.expected_difficulty * tag_ratio + topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio + topic_knowledge_max[t] += topic_knowledge_ratio end end relative_loss = {} - puts all_used_tags.size all_used_tags.each do |t| relative_loss[t] = topic_knowledge_loss_user[t] / topic_knowledge_max[t] end From 16a3bad453fb15efb33bb024416d9704dff99661 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 21 Feb 2017 13:35:50 +0100 Subject: [PATCH 023/143] renaming method --- app/models/proxy_exercise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 82825bda..f9746c71 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -24,7 +24,7 @@ class ProxyExercise < ActiveRecord::Base title end - def selectMatchingExercise(user) + def getMatchingExercise(user) assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first recommendedExercise = if (assigned_user_proxy_exercise) From d5b2ea42693243848c785bd7501b16c4a6229926 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 21 Feb 2017 18:31:42 +0100 Subject: [PATCH 024/143] added proxy exercise dispatching to LTI module. Submissions now set user before the exercise token gets validated. this we need to set the exercise behind the proxy exercise --- app/controllers/concerns/lti.rb | 17 +++++++++-------- app/controllers/sessions_controller.rb | 3 +-- spec/controllers/sessions_controller_spec.rb | 6 ++++++ spec/factories/proxy_exercise.rb | 7 +++++++ 4 files changed, 23 insertions(+), 10 deletions(-) create mode 100644 spec/factories/proxy_exercise.rb diff --git a/app/controllers/concerns/lti.rb b/app/controllers/concerns/lti.rb index 7c168ec6..7990ec05 100644 --- a/app/controllers/concerns/lti.rb +++ b/app/controllers/concerns/lti.rb @@ -74,7 +74,12 @@ module Lti private :require_valid_consumer_key def require_valid_exercise_token - @exercise = Exercise.find_by(token: params[:custom_token]) + proxy_exercise = ProxyExercise.find_by(token: params[:custom_token]) + unless proxy_exercise.nil? + @exercise = proxy_exercise.getMatchingExercise(@current_user) + else + @exercise = Exercise.find_by(token: params[:custom_token]) + end refuse_lti_launch(message: t('sessions.oauth.invalid_exercise_token')) unless @exercise end private :require_valid_exercise_token @@ -129,19 +134,15 @@ module Lti private :set_current_user def store_lti_session_data(options = {}) - exercise = Exercise.where(token: options[:parameters][:custom_token]).first - exercise_id = exercise.id unless exercise.nil? - - current_user = ExternalUser.find_or_create_by(consumer_id: options[:consumer].id, external_id: options[:parameters][:user_id].to_s) lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id, - external_users_id: current_user.id, - exercises_id: exercise_id) + external_users_id: @current_user.id, + exercises_id: @exercise.id) lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).to_json lti_parameters.save! session[:consumer_id] = options[:consumer].id - session[:external_user_id] = current_user.id + session[:external_user_id] = @current_user.id end private :store_lti_session_data diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index e6bdac8c..8f698d1a 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,7 +1,7 @@ class SessionsController < ApplicationController include Lti - [:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :require_valid_exercise_token].each do |method_name| + [:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :set_current_user, :require_valid_exercise_token].each do |method_name| before_action(method_name, only: :create_through_lti) end @@ -18,7 +18,6 @@ class SessionsController < ApplicationController end def create_through_lti - set_current_user store_lti_session_data(consumer: @consumer, parameters: params) store_nonce(params[:oauth_nonce]) redirect_to(implement_exercise_path(@exercise), diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index ffa957ea..2d837522 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -129,6 +129,12 @@ describe SessionsController do request expect(controller).to redirect_to(implement_exercise_path(exercise.id)) end + + it 'redirects to recommended exercise if requested token of proxy exercise' do + FactoryGirl.create(:proxy_exercise, exercises: [exercise]) + post :create_through_lti, custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id + expect(controller).to redirect_to(implement_exercise_path(exercise.id)) + end end end diff --git a/spec/factories/proxy_exercise.rb b/spec/factories/proxy_exercise.rb new file mode 100644 index 00000000..9c9974d6 --- /dev/null +++ b/spec/factories/proxy_exercise.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :proxy_exercise, class: ProxyExercise do + token 'dummytoken' + title 'Dummy' + end + +end From 04c54549c51b4aa534feff01c7a6753e72fe4c49 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 22 Feb 2017 14:38:40 +0100 Subject: [PATCH 025/143] zwischenstand --- app/models/exercise.rb | 5 +-- app/models/proxy_exercise.rb | 65 +++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index dc4181b5..ac4a60a2 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -92,7 +92,7 @@ class Exercise < ActiveRecord::Base (created_at - lag(created_at) OVER (PARTITION BY user_id ORDER BY created_at)) AS working_time FROM submissions - WHERE exercise_id=69) AS foo) AS bar + WHERE exercise_id=#{self.id}) AS foo) AS bar GROUP BY user_id ) AS foo """) @@ -200,7 +200,8 @@ class Exercise < ActiveRecord::Base if user submissions.where(user: user, cause: "assess").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0 else - files.teacher_defined_tests.sum(:weight) + 5 + #files.teacher_defined_tests.sum(:weight) end end diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index f9746c71..9f958e97 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -40,7 +40,45 @@ class ProxyExercise < ActiveRecord::Base end def findMatchingExercise(user) - exercises.shuffle.first + #exercises.shuffle.first + exercisesUserHasAccessed = user.submissions.where(cause: :assess).map{|s| s.exercise}.uniq + tagsUserHasSeen = exercisesUserHasAccessed.map{|ex| ex.tags}.uniq.flatten + puts "exercisesUserHasAccessed #{exercisesUserHasAccessed}" + + + # find execises + potentialRecommendedExercises = [] + exercises.each do |ex| + ## find exercises which have tags the user has already seen + if (ex.tags - tagsUserHasSeen).empty? + potentialRecommendedExercises << ex + end + end + puts "potentialRecommendedExercises: #{potentialRecommendedExercises}" + recommendedExercise = selectBestMatchingExercise(user, exercisesUserHasAccessed, potentialRecommendedExercises) + recommendedExercise + end + + def selectBestMatchingExercise(user, exercisesUserHasAccessed, potentialRecommendedExercises) + topic_knowledge_user_and_max = getUserKnowledgeAndMaxKnowledge(user, exercisesUserHasAccessed) + puts "topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}" + topic_knowledge_user = topic_knowledge_user_and_max[:user_topic_knowledge] + topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge] + relative_knowledge_improvement = {} + potentialRecommendedExercises.each do |potex| + tags = potex.tags + relative_knowledge_improvement[potex] = 0.0 + tags.each do |tag| + tag_ratio = potex.exercise_tags.where(tag: tag).first.factor / potex.exercise_tags.inject(0){|sum, et| sum += et.factor } + max_topic_knowledge_ratio = potex.expected_difficulty * tag_ratio + old_relative_loss_tag = topic_knowledge_user[tag] / topic_knowledge_max[tag] + new_relative_loss_tag = topic_knowledge_user[tag] / (topic_knowledge_max[tag] + max_topic_knowledge_ratio) + relative_knowledge_improvement[potex] += new_relative_loss_tag - old_relative_loss_tag + end + end + puts "relative improvements #{relative_knowledge_improvement}" + exercise_with_greatest_improvements = relative_knowledge_improvement.max_by{|k,v| v} + exercise_with_greatest_improvements end # [score][quantile] @@ -64,6 +102,8 @@ class ProxyExercise < ActiveRecord::Base Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: points_ratio=#{points_ratio} score: 0" ) return 0.0 end + puts points_ratio + puts ex.maximum_score.to_f points_ratio_index = ((scoring_matrix.size - 1) * points_ratio).to_i working_time_user = Time.parse(ex.average_working_time_for_only(user.id) || "00:00:00").seconds_since_midnight quantiles_working_time = ex.getQuantiles(scoring_matrix_quantiles) @@ -90,9 +130,9 @@ class ProxyExercise < ActiveRecord::Base user_score_factor = score(user, ex) ex.tags.each do |t| tag_ratio = ex.exercise_tags.where(tag: t).first.factor / ex.exercise_tags.inject(0){|sum, et| sum += et.factor } - topic_knowledge_ratio = ex.expected_difficulty * tag_ratio - topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio - topic_knowledge_max[t] += topic_knowledge_ratio + max_topic_knowledge_ratio = ex.expected_difficulty * tag_ratio + topic_knowledge_loss_user[t] += (1 - user_score_factor) * max_topic_knowledge_ratio + topic_knowledge_max[t] += max_topic_knowledge_ratio end end relative_loss = {} @@ -102,4 +142,21 @@ class ProxyExercise < ActiveRecord::Base relative_loss end + def getUserKnowledgeAndMaxKnowledge(user, exercises) + # initialize knowledge for each tag with 0 + all_used_tags = exercises.inject(Set.new){|tagset, ex| tagset.merge(ex.tags)} + topic_knowledge_loss_user = all_used_tags.map{|t| [t, 0]}.to_h + topic_knowledge_max = all_used_tags.map{|t| [t, 0]}.to_h + exercises.each do |ex| + user_score_factor = score(user, ex) + ex.tags.each do |t| + tag_ratio = ex.exercise_tags.where(tag: t).first.factor / ex.exercise_tags.inject(0){|sum, et| sum += et.factor } + topic_knowledge_ratio = ex.expected_difficulty * tag_ratio + topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio + topic_knowledge_max[t] += topic_knowledge_ratio + end + end + {user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max} + end + end \ No newline at end of file From 9bef1d8bb2bef599d42000d07f24e1e348835d36 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 22 Feb 2017 15:58:35 +0100 Subject: [PATCH 026/143] recommendation also with lots of debugging messages looks promising --- app/models/proxy_exercise.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 9f958e97..970286e7 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -62,23 +62,26 @@ class ProxyExercise < ActiveRecord::Base def selectBestMatchingExercise(user, exercisesUserHasAccessed, potentialRecommendedExercises) topic_knowledge_user_and_max = getUserKnowledgeAndMaxKnowledge(user, exercisesUserHasAccessed) puts "topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}" + puts "potentialRecommendedExercises: #{potentialRecommendedExercises.size}" topic_knowledge_user = topic_knowledge_user_and_max[:user_topic_knowledge] topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge] relative_knowledge_improvement = {} potentialRecommendedExercises.each do |potex| tags = potex.tags relative_knowledge_improvement[potex] = 0.0 + puts "potex #{potex}" tags.each do |tag| - tag_ratio = potex.exercise_tags.where(tag: tag).first.factor / potex.exercise_tags.inject(0){|sum, et| sum += et.factor } + tag_ratio = potex.exercise_tags.where(tag: tag).first.factor.to_f / potex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f max_topic_knowledge_ratio = potex.expected_difficulty * tag_ratio old_relative_loss_tag = topic_knowledge_user[tag] / topic_knowledge_max[tag] new_relative_loss_tag = topic_knowledge_user[tag] / (topic_knowledge_max[tag] + max_topic_knowledge_ratio) - relative_knowledge_improvement[potex] += new_relative_loss_tag - old_relative_loss_tag + puts "tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, max_topic_knowledge_ratio #{max_topic_knowledge_ratio} tag_ratio #{tag_ratio}" + relative_knowledge_improvement[potex] += old_relative_loss_tag - new_relative_loss_tag end end puts "relative improvements #{relative_knowledge_improvement}" exercise_with_greatest_improvements = relative_knowledge_improvement.max_by{|k,v| v} - exercise_with_greatest_improvements + exercise_with_greatest_improvements.first end # [score][quantile] @@ -148,10 +151,14 @@ class ProxyExercise < ActiveRecord::Base topic_knowledge_loss_user = all_used_tags.map{|t| [t, 0]}.to_h topic_knowledge_max = all_used_tags.map{|t| [t, 0]}.to_h exercises.each do |ex| + puts "exercise: #{ex}" user_score_factor = score(user, ex) ex.tags.each do |t| - tag_ratio = ex.exercise_tags.where(tag: t).first.factor / ex.exercise_tags.inject(0){|sum, et| sum += et.factor } + tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f + puts "tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum += et.factor }}" + puts "tag_ratio #{tag_ratio}" topic_knowledge_ratio = ex.expected_difficulty * tag_ratio + puts "topic_knowledge_ratio #{topic_knowledge_ratio}" topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio topic_knowledge_max[t] += topic_knowledge_ratio end From d446fcb109c9797e294f9371812ebb51b6c73ec6 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 23 Feb 2017 13:36:36 +0100 Subject: [PATCH 027/143] fixed title in new tag --- app/views/tags/new.html.slim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/tags/new.html.slim b/app/views/tags/new.html.slim index a933bede..e5dbc4ee 100644 --- a/app/views/tags/new.html.slim +++ b/app/views/tags/new.html.slim @@ -1,3 +1,3 @@ -h1 = t('shared.new_model', model: Hint.model_name.human) +h1 = t('shared.new_model', model: Tag.model_name.human) = render('form') From 9935cb30485353855929fd88c4f6f5b62ef22371 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 23 Feb 2017 13:36:53 +0100 Subject: [PATCH 028/143] default value for tag factor 1 --- db/migrate/20170206141210_add_tags.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/db/migrate/20170206141210_add_tags.rb b/db/migrate/20170206141210_add_tags.rb index fd95b4dc..8c0c129a 100644 --- a/db/migrate/20170206141210_add_tags.rb +++ b/db/migrate/20170206141210_add_tags.rb @@ -1,7 +1,7 @@ class AddTags < ActiveRecord::Migration def change - add_column :exercises, :expected_worktime_seconds, :integer, default: 0 + add_column :exercises, :expected_worktime_seconds, :integer, default: 60 add_column :exercises, :expected_difficulty, :integer, default: 1 create_table :tags do |t| @@ -12,7 +12,7 @@ class AddTags < ActiveRecord::Migration create_table :exercise_tags do |t| t.belongs_to :exercise t.belongs_to :tag - t.integer :factor, default: 0 + t.integer :factor, default: 1 end end From 0f67297e2cc966f3f10436bec41486524a425c45 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 23 Feb 2017 13:38:42 +0100 Subject: [PATCH 029/143] recommendation also now returns easiest exercise as recommendation if no tag matched could be found findMatchingExercises only searched for assessed submissions to get processed exercises, fixed this to look at all submissions --- app/models/proxy_exercise.rb | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 970286e7..31a0a8eb 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -41,7 +41,8 @@ class ProxyExercise < ActiveRecord::Base def findMatchingExercise(user) #exercises.shuffle.first - exercisesUserHasAccessed = user.submissions.where(cause: :assess).map{|s| s.exercise}.uniq + # hier vielleicht nur betrachten wenn der user die aufgabe assessed oder submitted hat + exercisesUserHasAccessed = user.submissions.map{|s| s.exercise}.uniq tagsUserHasSeen = exercisesUserHasAccessed.map{|ex| ex.tags}.uniq.flatten puts "exercisesUserHasAccessed #{exercisesUserHasAccessed}" @@ -54,15 +55,19 @@ class ProxyExercise < ActiveRecord::Base potentialRecommendedExercises << ex end end - puts "potentialRecommendedExercises: #{potentialRecommendedExercises}" - recommendedExercise = selectBestMatchingExercise(user, exercisesUserHasAccessed, potentialRecommendedExercises) - recommendedExercise + if potentialRecommendedExercises.empty? + getEasiestExercise(exercises) + else + puts "potentialRecommendedExercises: #{potentialRecommendedExercises}" + recommendedExercise = selectBestMatchingExercise(user, exercisesUserHasAccessed, potentialRecommendedExercises) + recommendedExercise + end end def selectBestMatchingExercise(user, exercisesUserHasAccessed, potentialRecommendedExercises) topic_knowledge_user_and_max = getUserKnowledgeAndMaxKnowledge(user, exercisesUserHasAccessed) puts "topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}" - puts "potentialRecommendedExercises: #{potentialRecommendedExercises.size}" + puts "potentialRecommendedExercises: #{potentialRecommendedExercises.size}: #{potentialRecommendedExercises.map{|p| p.title}}" topic_knowledge_user = topic_knowledge_user_and_max[:user_topic_knowledge] topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge] relative_knowledge_improvement = {} @@ -80,8 +85,8 @@ class ProxyExercise < ActiveRecord::Base end end puts "relative improvements #{relative_knowledge_improvement}" - exercise_with_greatest_improvements = relative_knowledge_improvement.max_by{|k,v| v} - exercise_with_greatest_improvements.first + exercise_with_greatest_improvements = relative_knowledge_improvement.max_by{|k,v| v}.first + exercise_with_greatest_improvements end # [score][quantile] @@ -166,4 +171,8 @@ class ProxyExercise < ActiveRecord::Base {user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max} end + def getEasiestExercise(exercises) + exercises.order(:expected_difficulty).first + end + end \ No newline at end of file From 4796dd5c9d6c6d1ee186bb132eab1b055c3bb01e Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 23 Feb 2017 15:44:16 +0100 Subject: [PATCH 030/143] find solved exercises of users now by fetching submissions with cause assess or submit --- app/models/exercise.rb | 5 ++--- app/models/proxy_exercise.rb | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index ac4a60a2..6d3d62fb 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -198,10 +198,9 @@ class Exercise < ActiveRecord::Base def maximum_score(user = nil) if user - submissions.where(user: user, cause: "assess").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0 + submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0 else - 5 - #files.teacher_defined_tests.sum(:weight) + files.teacher_defined_tests.sum(:weight) end end diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 31a0a8eb..78b8cff5 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -42,11 +42,10 @@ class ProxyExercise < ActiveRecord::Base def findMatchingExercise(user) #exercises.shuffle.first # hier vielleicht nur betrachten wenn der user die aufgabe assessed oder submitted hat - exercisesUserHasAccessed = user.submissions.map{|s| s.exercise}.uniq + exercisesUserHasAccessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq tagsUserHasSeen = exercisesUserHasAccessed.map{|ex| ex.tags}.uniq.flatten puts "exercisesUserHasAccessed #{exercisesUserHasAccessed}" - # find execises potentialRecommendedExercises = [] exercises.each do |ex| From fe8b04fcfc27ae73ed09eb0feeea195c5d95c0af Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 23 Feb 2017 19:12:22 +0100 Subject: [PATCH 031/143] more debugging infos --- app/models/proxy_exercise.rb | 38 ++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 78b8cff5..002e4c88 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -40,24 +40,23 @@ class ProxyExercise < ActiveRecord::Base end def findMatchingExercise(user) - #exercises.shuffle.first - # hier vielleicht nur betrachten wenn der user die aufgabe assessed oder submitted hat exercisesUserHasAccessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq tagsUserHasSeen = exercisesUserHasAccessed.map{|ex| ex.tags}.uniq.flatten - puts "exercisesUserHasAccessed #{exercisesUserHasAccessed}" + Rails.logger.info("exercisesUserHasAccessed #{exercisesUserHasAccessed.map{|e|e.id}.join(",")}") # find execises potentialRecommendedExercises = [] exercises.each do |ex| - ## find exercises which have tags the user has already seen + ## find exercises which have only tags the user has already seen if (ex.tags - tagsUserHasSeen).empty? potentialRecommendedExercises << ex end end + Rails.logger.info("potentialRecommendedExercises: #{potentialRecommendedExercises.map{|e|e.id}}") + # if all exercises contain tags which the user has never seen, recommend easiest exercise if potentialRecommendedExercises.empty? getEasiestExercise(exercises) else - puts "potentialRecommendedExercises: #{potentialRecommendedExercises}" recommendedExercise = selectBestMatchingExercise(user, exercisesUserHasAccessed, potentialRecommendedExercises) recommendedExercise end @@ -66,26 +65,33 @@ class ProxyExercise < ActiveRecord::Base def selectBestMatchingExercise(user, exercisesUserHasAccessed, potentialRecommendedExercises) topic_knowledge_user_and_max = getUserKnowledgeAndMaxKnowledge(user, exercisesUserHasAccessed) puts "topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}" - puts "potentialRecommendedExercises: #{potentialRecommendedExercises.size}: #{potentialRecommendedExercises.map{|p| p.title}}" + puts "potentialRecommendedExercises: #{potentialRecommendedExercises.size}: #{potentialRecommendedExercises.map{|p| p.id}}" topic_knowledge_user = topic_knowledge_user_and_max[:user_topic_knowledge] topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge] + current_users_knowledge_lack = {} + topic_knowledge_max.keys.each do |tag| + current_users_knowledge_lack[tag] = topic_knowledge_user[tag] / topic_knowledge_max[tag] + end + relative_knowledge_improvement = {} potentialRecommendedExercises.each do |potex| tags = potex.tags relative_knowledge_improvement[potex] = 0.0 - puts "potex #{potex}" + Rails.logger.info("review potential exercise #{potex.id}") tags.each do |tag| tag_ratio = potex.exercise_tags.where(tag: tag).first.factor.to_f / potex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f max_topic_knowledge_ratio = potex.expected_difficulty * tag_ratio old_relative_loss_tag = topic_knowledge_user[tag] / topic_knowledge_max[tag] new_relative_loss_tag = topic_knowledge_user[tag] / (topic_knowledge_max[tag] + max_topic_knowledge_ratio) - puts "tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, max_topic_knowledge_ratio #{max_topic_knowledge_ratio} tag_ratio #{tag_ratio}" + puts "tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, min_loss_after_solving #{topic_knowledge_max[tag] + max_topic_knowledge_ratio} tag_ratio #{tag_ratio}" relative_knowledge_improvement[potex] += old_relative_loss_tag - new_relative_loss_tag end end - puts "relative improvements #{relative_knowledge_improvement}" - exercise_with_greatest_improvements = relative_knowledge_improvement.max_by{|k,v| v}.first - exercise_with_greatest_improvements + + best_matching_exercise = relative_knowledge_improvement.max_by{|k,v| v}.first + Rails.logger.info(current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}) + Rails.logger.info("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}") + best_matching_exercise end # [score][quantile] @@ -109,8 +115,6 @@ class ProxyExercise < ActiveRecord::Base Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: points_ratio=#{points_ratio} score: 0" ) return 0.0 end - puts points_ratio - puts ex.maximum_score.to_f points_ratio_index = ((scoring_matrix.size - 1) * points_ratio).to_i working_time_user = Time.parse(ex.average_working_time_for_only(user.id) || "00:00:00").seconds_since_midnight quantiles_working_time = ex.getQuantiles(scoring_matrix_quantiles) @@ -155,14 +159,14 @@ class ProxyExercise < ActiveRecord::Base topic_knowledge_loss_user = all_used_tags.map{|t| [t, 0]}.to_h topic_knowledge_max = all_used_tags.map{|t| [t, 0]}.to_h exercises.each do |ex| - puts "exercise: #{ex}" + Rails.logger.info("exercise: #{ex.id}: #{ex}") user_score_factor = score(user, ex) ex.tags.each do |t| tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f - puts "tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum += et.factor }}" - puts "tag_ratio #{tag_ratio}" + Rails.logger.info("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum += et.factor }}") + Rails.logger.info("tag_ratio #{tag_ratio}") topic_knowledge_ratio = ex.expected_difficulty * tag_ratio - puts "topic_knowledge_ratio #{topic_knowledge_ratio}" + Rails.logger.info("topic_knowledge_ratio #{topic_knowledge_ratio}") topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio topic_knowledge_max[t] += topic_knowledge_ratio end From 01470bff97dbf1a406a31b61736dc48e731520ca Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Fri, 24 Feb 2017 16:35:13 +0100 Subject: [PATCH 032/143] fixed problem with wrong worktime calculations --- app/models/exercise.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 6d3d62fb..2ffe2ab3 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -67,7 +67,7 @@ class Exercise < ActiveRecord::Base FROM (SELECT user_id, id, - (created_at - lag(created_at) over (PARTITION BY user_id + (created_at - lag(created_at) over (PARTITION BY user_id, exercise_id ORDER BY created_at)) AS working_time FROM submissions WHERE exercise_id=#{id}) AS foo) AS bar @@ -89,10 +89,10 @@ class Exercise < ActiveRecord::Base FROM (SELECT user_id, id, - (created_at - lag(created_at) OVER (PARTITION BY user_id + (created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id ORDER BY created_at)) AS working_time FROM submissions - WHERE exercise_id=#{self.id}) AS foo) AS bar + WHERE exercise_id=#{self.id} AND user_type = 'ExternalUser') AS foo) AS bar GROUP BY user_id ) AS foo """) @@ -128,7 +128,7 @@ class Exercise < ActiveRecord::Base (SELECT CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new FROM (SELECT id, - (created_at - lag(created_at) over (PARTITION BY user_id + (created_at - lag(created_at) over (PARTITION BY user_id, exercise_id ORDER BY created_at)) AS working_time FROM submissions WHERE exercise_id=#{id} and user_id=#{user_id} and user_type='ExternalUser') AS foo) AS bar From 5bbc12409761dc8455c20948a305527c0f14e3cb Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Fri, 24 Feb 2017 17:26:24 +0100 Subject: [PATCH 033/143] improvement debug output --- app/models/proxy_exercise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 002e4c88..cc1f3aa7 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -89,7 +89,7 @@ class ProxyExercise < ActiveRecord::Base end best_matching_exercise = relative_knowledge_improvement.max_by{|k,v| v}.first - Rails.logger.info(current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}) + Rails.logger.info("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}) Rails.logger.info("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}") best_matching_exercise end From 1d75af51d2108c326adbf5c0a791de2f9beb3dea Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Sun, 26 Feb 2017 16:40:15 +0100 Subject: [PATCH 034/143] quick fix --- app/models/proxy_exercise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index cc1f3aa7..a2b33cd4 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -89,7 +89,7 @@ class ProxyExercise < ActiveRecord::Base end best_matching_exercise = relative_knowledge_improvement.max_by{|k,v| v}.first - Rails.logger.info("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}) + Rails.logger.info("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}.to_s) Rails.logger.info("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}") best_matching_exercise end From f63b9eeb3c501879003808b87f473a7e5c1ab75a Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Sun, 26 Feb 2017 17:46:37 +0100 Subject: [PATCH 035/143] added that users would only get exercises recommended which are max 1 level more difficult --- app/models/proxy_exercise.rb | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index a2b33cd4..62faeedb 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -83,17 +83,36 @@ class ProxyExercise < ActiveRecord::Base max_topic_knowledge_ratio = potex.expected_difficulty * tag_ratio old_relative_loss_tag = topic_knowledge_user[tag] / topic_knowledge_max[tag] new_relative_loss_tag = topic_knowledge_user[tag] / (topic_knowledge_max[tag] + max_topic_knowledge_ratio) - puts "tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, min_loss_after_solving #{topic_knowledge_max[tag] + max_topic_knowledge_ratio} tag_ratio #{tag_ratio}" + puts "tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, tag_ratio #{tag_ratio}" relative_knowledge_improvement[potex] += old_relative_loss_tag - new_relative_loss_tag end end - - best_matching_exercise = relative_knowledge_improvement.max_by{|k,v| v}.first + highest_difficulty_user_has_accessed = exercisesUserHasAccessed.map{|e| e.expected_difficulty}.sort.last || 0 + best_matching_exercise = find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed) + #best_matching_exercise = relative_knowledge_improvement.max_by{|k,v| v}.first Rails.logger.info("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}.to_s) Rails.logger.info("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}") best_matching_exercise end + def find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed) + Rails.logger.info("select most appropiate exercise for user. his highest difficulty was #{highest_difficulty_user_has_accessed}") + sorted_exercises = relative_knowledge_improvement.sort_by{|k,v| v}.reverse + + sorted_exercises.each do |ex,diff| + Rails.logger.info("review exercise #{ex.id} diff: #{ex.expected_difficulty}") + if (ex.expected_difficulty - highest_difficulty_user_has_accessed) <= 1 + Rails.logger.info("matched #{ex.id}") + return ex + else + Rails.logger.info("ex #{ex.id} is too difficult") + end + end + easiest_exercise = sorted_exercises.min_by{|k,v| v}.first + Rails.logger.info("no match, select easiest exercise as fallback #{easiest_exercise.id}") + easiest_exercise + end + # [score][quantile] def scoring_matrix [ From 1eea3fab4c4fc9251b7ea0d8520bcf82c3fb9a23 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Sun, 26 Feb 2017 18:03:55 +0100 Subject: [PATCH 036/143] lots of renaming to _ names instead of camelCase --- app/controllers/concerns/lti.rb | 2 +- app/models/exercise.rb | 2 +- app/models/proxy_exercise.rb | 55 ++++++++++++++++----------------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/app/controllers/concerns/lti.rb b/app/controllers/concerns/lti.rb index 7990ec05..ce2105bd 100644 --- a/app/controllers/concerns/lti.rb +++ b/app/controllers/concerns/lti.rb @@ -76,7 +76,7 @@ module Lti def require_valid_exercise_token proxy_exercise = ProxyExercise.find_by(token: params[:custom_token]) unless proxy_exercise.nil? - @exercise = proxy_exercise.getMatchingExercise(@current_user) + @exercise = proxy_exercise.get_matching_exercise(@current_user) else @exercise = Exercise.find_by(token: params[:custom_token]) end diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 2ffe2ab3..ad3baeca 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -75,7 +75,7 @@ class Exercise < ActiveRecord::Base """ end - def getQuantiles(quantiles) + def get_quantiles(quantiles) quantiles_str = "[" + quantiles.join(",") + "]" result = self.class.connection.execute(""" SELECT unnest(PERCENTILE_CONT(ARRAY#{quantiles_str}) WITHIN GROUP (ORDER BY working_time)) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 62faeedb..b7ee28c5 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -24,48 +24,48 @@ class ProxyExercise < ActiveRecord::Base title end - def getMatchingExercise(user) + def get_matching_exercise(user) assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first - recommendedExercise = + recommended_exercise = if (assigned_user_proxy_exercise) Rails.logger.info("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" ) assigned_user_proxy_exercise.exercise else Rails.logger.info("find new matching exercise for user #{user.id}" ) - matchingExercise = findMatchingExercise(user) - user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matchingExercise, proxy_exercise: self) - matchingExercise + matching_exercise = find_matching_exercise(user) + user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self) + matching_exercise end - recommendedExercise + recommended_exercise end - def findMatchingExercise(user) - exercisesUserHasAccessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq - tagsUserHasSeen = exercisesUserHasAccessed.map{|ex| ex.tags}.uniq.flatten - Rails.logger.info("exercisesUserHasAccessed #{exercisesUserHasAccessed.map{|e|e.id}.join(",")}") + def find_matching_exercise(user) + exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq + tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten + Rails.logger.info("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}") # find execises - potentialRecommendedExercises = [] + potential_recommended_exercises = [] exercises.each do |ex| ## find exercises which have only tags the user has already seen - if (ex.tags - tagsUserHasSeen).empty? - potentialRecommendedExercises << ex + if (ex.tags - tags_user_has_seen).empty? + potential_recommended_exercises << ex end end - Rails.logger.info("potentialRecommendedExercises: #{potentialRecommendedExercises.map{|e|e.id}}") + Rails.logger.info("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}") # if all exercises contain tags which the user has never seen, recommend easiest exercise - if potentialRecommendedExercises.empty? - getEasiestExercise(exercises) + if potential_recommended_exercises.empty? + select_easiest_exercise(exercises) else - recommendedExercise = selectBestMatchingExercise(user, exercisesUserHasAccessed, potentialRecommendedExercises) - recommendedExercise + recommended_exercise = select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises) + recommended_exercise end end - def selectBestMatchingExercise(user, exercisesUserHasAccessed, potentialRecommendedExercises) - topic_knowledge_user_and_max = getUserKnowledgeAndMaxKnowledge(user, exercisesUserHasAccessed) + def select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises) + topic_knowledge_user_and_max = get_user_knowledge_and_max_knowledge(user, exercises_user_has_accessed) puts "topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}" - puts "potentialRecommendedExercises: #{potentialRecommendedExercises.size}: #{potentialRecommendedExercises.map{|p| p.id}}" + puts "potential_recommended_exercises: #{potential_recommended_exercises.size}: #{potential_recommended_exercises.map{|p| p.id}}" topic_knowledge_user = topic_knowledge_user_and_max[:user_topic_knowledge] topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge] current_users_knowledge_lack = {} @@ -74,7 +74,7 @@ class ProxyExercise < ActiveRecord::Base end relative_knowledge_improvement = {} - potentialRecommendedExercises.each do |potex| + potential_recommended_exercises.each do |potex| tags = potex.tags relative_knowledge_improvement[potex] = 0.0 Rails.logger.info("review potential exercise #{potex.id}") @@ -87,9 +87,8 @@ class ProxyExercise < ActiveRecord::Base relative_knowledge_improvement[potex] += old_relative_loss_tag - new_relative_loss_tag end end - highest_difficulty_user_has_accessed = exercisesUserHasAccessed.map{|e| e.expected_difficulty}.sort.last || 0 + highest_difficulty_user_has_accessed = exercises_user_has_accessed.map{|e| e.expected_difficulty}.sort.last || 0 best_matching_exercise = find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed) - #best_matching_exercise = relative_knowledge_improvement.max_by{|k,v| v}.first Rails.logger.info("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}.to_s) Rails.logger.info("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}") best_matching_exercise @@ -136,7 +135,7 @@ class ProxyExercise < ActiveRecord::Base end points_ratio_index = ((scoring_matrix.size - 1) * points_ratio).to_i working_time_user = Time.parse(ex.average_working_time_for_only(user.id) || "00:00:00").seconds_since_midnight - quantiles_working_time = ex.getQuantiles(scoring_matrix_quantiles) + quantiles_working_time = ex.get_quantiles(scoring_matrix_quantiles) quantile_index = quantiles_working_time.size quantiles_working_time.each_with_index do |quantile_time, i| if working_time_user <= quantile_time @@ -151,7 +150,7 @@ class ProxyExercise < ActiveRecord::Base scoring_matrix[points_ratio_index][quantile_index] end - def getRelativeKnowledgeLoss(user, exercises) + def get_relative_knowledge_loss(user, exercises) # initialize knowledge for each tag with 0 all_used_tags = exercises.inject(Set.new){|tagset, ex| tagset.merge(ex.tags)} topic_knowledge_loss_user = all_used_tags.map{|t| [t, 0]}.to_h @@ -172,7 +171,7 @@ class ProxyExercise < ActiveRecord::Base relative_loss end - def getUserKnowledgeAndMaxKnowledge(user, exercises) + def get_user_knowledge_and_max_knowledge(user, exercises) # initialize knowledge for each tag with 0 all_used_tags = exercises.inject(Set.new){|tagset, ex| tagset.merge(ex.tags)} topic_knowledge_loss_user = all_used_tags.map{|t| [t, 0]}.to_h @@ -193,7 +192,7 @@ class ProxyExercise < ActiveRecord::Base {user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max} end - def getEasiestExercise(exercises) + def select_easiest_exercise(exercises) exercises.order(:expected_difficulty).first end From 7a61d5a9838ee8afdda7536d7605533cbf7900ea Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 27 Feb 2017 10:39:59 +0100 Subject: [PATCH 037/143] tests --- spec/controllers/sessions_controller_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 2d837522..50fef8d6 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -28,6 +28,7 @@ describe SessionsController do describe 'POST #create_through_lti' do let(:exercise) { FactoryGirl.create(:dummy) } + let(:exercise2) { FactoryGirl.create(:dummy) } let(:nonce) { SecureRandom.hex } before(:each) { I18n.locale = I18n.default_locale } @@ -135,6 +136,17 @@ describe SessionsController do post :create_through_lti, custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id expect(controller).to redirect_to(implement_exercise_path(exercise.id)) end + + it 'recommends only exercises who are 1 degree more complicated than what user has seen' do + # dummy user has no exercises finished, therefore his highest difficulty is 0 + FactoryGirl.create(:proxy_exercise, exercises: [exercise, exercise2]) + exercise.expected_difficulty = 3 + exercise.save + exercise2.expected_difficulty = 1 + exercise2.save + post :create_through_lti, custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id + expect(controller).to redirect_to(implement_exercise_path(exercise2.id)) + end end end From 66a2d8c9926bfa0384d3bb6a5f620d4300375fd6 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 27 Feb 2017 10:40:36 +0100 Subject: [PATCH 038/143] minor change --- app/models/proxy_exercise.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index b7ee28c5..17385c3d 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -101,10 +101,10 @@ class ProxyExercise < ActiveRecord::Base sorted_exercises.each do |ex,diff| Rails.logger.info("review exercise #{ex.id} diff: #{ex.expected_difficulty}") if (ex.expected_difficulty - highest_difficulty_user_has_accessed) <= 1 - Rails.logger.info("matched #{ex.id}") + Rails.logger.info("matched exercise #{ex.id}") return ex else - Rails.logger.info("ex #{ex.id} is too difficult") + Rails.logger.info("exercise #{ex.id} is too difficult") end end easiest_exercise = sorted_exercises.min_by{|k,v| v}.first From 355e8af14bcd67e9802b6d3b3999964795fb94f1 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 27 Feb 2017 11:43:06 +0100 Subject: [PATCH 039/143] privatized methods in proxy_exercise --- app/models/proxy_exercise.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 17385c3d..3b5b43c4 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -61,6 +61,7 @@ class ProxyExercise < ActiveRecord::Base recommended_exercise end end + private :find_matching_exercise def select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises) topic_knowledge_user_and_max = get_user_knowledge_and_max_knowledge(user, exercises_user_has_accessed) @@ -93,6 +94,7 @@ class ProxyExercise < ActiveRecord::Base Rails.logger.info("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}") best_matching_exercise end + private :select_best_matching_exercise def find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed) Rails.logger.info("select most appropiate exercise for user. his highest difficulty was #{highest_difficulty_user_has_accessed}") @@ -111,6 +113,7 @@ class ProxyExercise < ActiveRecord::Base Rails.logger.info("no match, select easiest exercise as fallback #{easiest_exercise.id}") easiest_exercise end + private :find_best_exercise # [score][quantile] def scoring_matrix @@ -126,6 +129,7 @@ class ProxyExercise < ActiveRecord::Base def scoring_matrix_quantiles [0.2,0.4,0.6,0.8] end + private :scoring_matrix_quantiles def score(user, ex) points_ratio = ex.maximum_score(user) / ex.maximum_score.to_f @@ -149,6 +153,7 @@ class ProxyExercise < ActiveRecord::Base "score: #{scoring_matrix[points_ratio_index][quantile_index]}") scoring_matrix[points_ratio_index][quantile_index] end + private :score def get_relative_knowledge_loss(user, exercises) # initialize knowledge for each tag with 0 @@ -170,6 +175,7 @@ class ProxyExercise < ActiveRecord::Base end relative_loss end + private :get_relative_knowledge_loss def get_user_knowledge_and_max_knowledge(user, exercises) # initialize knowledge for each tag with 0 @@ -191,6 +197,7 @@ class ProxyExercise < ActiveRecord::Base end {user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max} end + private :get_user_knowledge_and_max_knowledge def select_easiest_exercise(exercises) exercises.order(:expected_difficulty).first From 1f141f440a48992b32c3402c88ba51e7e8e6320c Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 27 Feb 2017 13:50:18 +0100 Subject: [PATCH 040/143] added fallback to recommendation if something went completely wrong --- app/models/proxy_exercise.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 3b5b43c4..6f177778 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -32,7 +32,13 @@ class ProxyExercise < ActiveRecord::Base assigned_user_proxy_exercise.exercise else Rails.logger.info("find new matching exercise for user #{user.id}" ) - matching_exercise = find_matching_exercise(user) + matching_exercise = + begin + find_matching_exercise(user) + rescue #fallback + Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" ) + exercises.shuffle.first + end user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self) matching_exercise end From b41a858762bfa9bfad340e9083f1f28df9804eef Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 27 Feb 2017 15:31:11 +0100 Subject: [PATCH 041/143] changed way working times are returned. builtin protection if exercise is new --- app/controllers/exercises_controller.rb | 4 ++-- app/models/exercise.rb | 13 +++++++++---- app/models/proxy_exercise.rb | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index f27324ad..c2d50125 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -167,8 +167,8 @@ class ExercisesController < ApplicationController end def working_times - working_time_accumulated = Time.parse(@exercise.average_working_time_for_only(current_user.id) || "00:00:00").seconds_since_midnight - working_time_avg = Time.parse(@exercise.average_working_time || "00:00:00").seconds_since_midnight + working_time_accumulated = @exercise.accumulated_working_time_for_only(current_user.id) + working_time_avg = @exercise.get_quantiles([0.75]).first render(json: {working_time_avg: working_time_avg, working_time_accumulated: working_time_accumulated}) end diff --git a/app/models/exercise.rb b/app/models/exercise.rb index ad3baeca..3419d9d8 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -96,7 +96,12 @@ class Exercise < ActiveRecord::Base GROUP BY user_id ) AS foo """) - quantiles.each_with_index.map{|q,i| Time.parse(result[i]["unnest"]).seconds_since_midnight} + if result.count > 0 + quantiles.each_with_index.map{|q,i| Time.parse(result[i]["unnest"]).seconds_since_midnight} + else + quantiles.map{|q| 0} + end + end def retrieve_working_time_statistics @@ -121,8 +126,8 @@ class Exercise < ActiveRecord::Base @working_time_statistics[user_id]["working_time"] end - def average_working_time_for_only(user_id) - self.class.connection.execute(""" + def accumulated_working_time_for_only(user_id) + Time.parse(self.class.connection.execute(""" SELECT sum(working_time_new) AS working_time FROM (SELECT CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new @@ -132,7 +137,7 @@ class Exercise < ActiveRecord::Base ORDER BY created_at)) AS working_time FROM submissions WHERE exercise_id=#{id} and user_id=#{user_id} and user_type='ExternalUser') AS foo) AS bar - """).first["working_time"] + """).first["working_time"] || "00:00:00").seconds_since_midnight end def duplicate(attributes = {}) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 6f177778..fe81f447 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -144,7 +144,7 @@ class ProxyExercise < ActiveRecord::Base return 0.0 end points_ratio_index = ((scoring_matrix.size - 1) * points_ratio).to_i - working_time_user = Time.parse(ex.average_working_time_for_only(user.id) || "00:00:00").seconds_since_midnight + working_time_user = ex.accumulated_working_time_for_only(user.id) quantiles_working_time = ex.get_quantiles(scoring_matrix_quantiles) quantile_index = quantiles_working_time.size quantiles_working_time.each_with_index do |quantile_time, i| From 2caf4b123e365fb90a4fc4110df480a0611d7d3f Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 27 Feb 2017 15:32:51 +0100 Subject: [PATCH 042/143] added intervention modals back into editor.js and html --- app/assets/javascripts/editor/editor.js.erb | 24 +++++++++++++++++++ app/views/exercises/_editor.html.slim | 5 ++-- .../interventions/_working_too_long.html.slim | 13 ++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 app/views/interventions/_working_too_long.html.slim diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 0be56058..9c38aca6 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -571,6 +571,29 @@ configureEditors: function () { }, + /** + * interventions + * */ + initializeInterventionTimer: function() { + $.ajax({ + data: { + exercise_id: $('#editor').data('exercise-id'), + user_id: $('#editor').data('user-id') + }, + dataType: 'json', + method: 'GET', + url: $('#editor').data('working-times-url'), + success: function (data) { + var avg = data['working_time_avg']; + var accu = data['working_time_accumulated']; + $('#avg-working-time').text(`avg time: ${avg} and accumulated time: ${accu}`); + setTimeout(function() { + $('#intervention-modal').modal('show') + }, 10000); + } + }); + }, + initializeEverything: function() { this.initializeRegexes(); @@ -585,6 +608,7 @@ configureEditors: function () { this.initializeDescriptionToggle(); this.initializeSideBarTooltips(); this.initializeTooltips(); + this.initializeInterventionTimer(); this.initPrompt(); this.renderScore(); this.showFirstFile(); diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index ff18968c..df25104d 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -1,7 +1,7 @@ - external_user_external_id = @current_user.respond_to?(:external_id) ? @current_user.external_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - external_user_id = @current_user.respond_to?(:external_id) ? @current_user.id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - consumer_id = @current_user.respond_to?(:external_id) ? @current_user.consumer_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') -#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id +#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files) div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) div id='frames' class='editor-col' @@ -21,4 +21,5 @@ button style="display:none" id="autosave" -= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') \ No newline at end of file += render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') += render('shared/modal', id: 'intervention-modal', title: 'Leg mal eine Pause ein', template: 'interventions/_working_too_long') \ No newline at end of file diff --git a/app/views/interventions/_working_too_long.html.slim b/app/views/interventions/_working_too_long.html.slim new file mode 100644 index 00000000..31e8a963 --- /dev/null +++ b/app/views/interventions/_working_too_long.html.slim @@ -0,0 +1,13 @@ +/h5 = t('exercises.implement.comment.question') + +h5 = 'Aufpassen!' +/textarea.form-control#question(style='resize:none;') +p = 'Uns ist aufgefallen, dass Sie schon sehr lange an dieser Aufgabe sitzen. Wollen Sie nicht vielleicht mal eine Pause einlegen?' +#avg-working-time + +/p = "AVG: #{@working_time_avg}" +/p = "ACCUMULATED: #{@working_time_accumulated}" + +/ data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action. +/ But if we use this button, it will work since the correct cause is supplied +/button#askForCommentsButton.btn.btn-block.btn-primary(type='button' data-cause='requestComments' data-message-success=t('exercises.editor.request_for_comments_sent')) =t('exercises.implement.comment.request') \ No newline at end of file From b82018dd8f05dae94c18966b1070dfa6575ba2b9 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 27 Feb 2017 16:08:15 +0100 Subject: [PATCH 043/143] changed some variable names --- app/assets/javascripts/editor/editor.js.erb | 4 ++-- app/controllers/exercises_controller.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 9c38aca6..f978ad0a 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -584,9 +584,9 @@ configureEditors: function () { method: 'GET', url: $('#editor').data('working-times-url'), success: function (data) { - var avg = data['working_time_avg']; + var percentile75 = data['working_time_75_percentile']; var accu = data['working_time_accumulated']; - $('#avg-working-time').text(`avg time: ${avg} and accumulated time: ${accu}`); + $('#avg-working-time').text(`75th percentile: ${percentile75} and accumulated time: ${accu}`); setTimeout(function() { $('#intervention-modal').modal('show') }, 10000); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index c2d50125..b9f4972b 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -168,8 +168,8 @@ class ExercisesController < ApplicationController def working_times working_time_accumulated = @exercise.accumulated_working_time_for_only(current_user.id) - working_time_avg = @exercise.get_quantiles([0.75]).first - render(json: {working_time_avg: working_time_avg, working_time_accumulated: working_time_accumulated}) + working_time_75_percentile = @exercise.get_quantiles([0.75]).first + render(json: {working_time_75_percentile: working_time_75_percentile, working_time_accumulated: working_time_accumulated}) end def index From 2456f46b2bd1ee7c190ca810eefad01a16d34253 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 27 Feb 2017 17:19:51 +0100 Subject: [PATCH 044/143] changed name of intervention modal, timer in editor.js set text now. some time calculations in editor for interventions --- app/assets/javascripts/editor/editor.js.erb | 28 +++++++++++++++++-- app/views/exercises/_editor.html.slim | 2 +- ...tml.slim => _intervention_modal.html.slim} | 4 +-- 3 files changed, 28 insertions(+), 6 deletions(-) rename app/views/interventions/{_working_too_long.html.slim => _intervention_modal.html.slim} (89%) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index f978ad0a..0e644d7d 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -585,11 +585,33 @@ configureEditors: function () { url: $('#editor').data('working-times-url'), success: function (data) { var percentile75 = data['working_time_75_percentile']; - var accu = data['working_time_accumulated']; - $('#avg-working-time').text(`75th percentile: ${percentile75} and accumulated time: ${accu}`); + var accumulatedWorkTimeUser = data['working_time_accumulated']; + + var timeUntilBreak = 15 * 60; + + if ((accumulatedWorkTimeUser - percentile75) > 0) { + // working time is already over 75 percentile + var timeUntilAskQuestion = 7 * 60; + } else { + // working time is less than 75 percentile + // ensure we give user at least 10 minutes before we bother the user + var timeUntilAskQuestion = (percentile75 - accumulatedWorkTimeUser) > 10 * 60 ? (percentile75 - accumulatedWorkTimeUser) : 10 * 60; + } + + // if notifications are too close to each other, ensure some time differences between them + if (Math.abs(timeUntilAskQuestion - timeUntilBreak) < 5){ + timeUntilBreak = timeUntilBreak * 2; + } + setTimeout(function() { + $('#intervention-text').text(`Willst du eine Pause machen? 75th percentile: ${percentile75} and accumulated time: ${accumulatedWorkTimeUser}`); $('#intervention-modal').modal('show') - }, 10000); + }, timeUntilBreak); + + setTimeout(function() { + $('#intervention-text').text(`Willst du eine Frage stellen?`); + $('#intervention-modal').modal('show') + }, timeUntilAskQuestion); } }); }, diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index df25104d..be82a5b7 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -22,4 +22,4 @@ = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') -= render('shared/modal', id: 'intervention-modal', title: 'Leg mal eine Pause ein', template: 'interventions/_working_too_long') \ No newline at end of file += render('shared/modal', id: 'intervention-modal', title: 'Leg mal eine Pause ein', template: 'interventions/_intervention_modal') \ No newline at end of file diff --git a/app/views/interventions/_working_too_long.html.slim b/app/views/interventions/_intervention_modal.html.slim similarity index 89% rename from app/views/interventions/_working_too_long.html.slim rename to app/views/interventions/_intervention_modal.html.slim index 31e8a963..af783dfa 100644 --- a/app/views/interventions/_working_too_long.html.slim +++ b/app/views/interventions/_intervention_modal.html.slim @@ -2,8 +2,8 @@ h5 = 'Aufpassen!' /textarea.form-control#question(style='resize:none;') -p = 'Uns ist aufgefallen, dass Sie schon sehr lange an dieser Aufgabe sitzen. Wollen Sie nicht vielleicht mal eine Pause einlegen?' -#avg-working-time +p = 'Uns ist aufgefallen, dass Sie schon sehr lange an dieser Aufgabe sitzen.' +#intervention-text /p = "AVG: #{@working_time_avg}" /p = "ACCUMULATED: #{@working_time_accumulated}" From 3d7f5bdf1a48035efe2be41f8ec192c4d83ba14b Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 28 Feb 2017 15:24:53 +0100 Subject: [PATCH 045/143] added intervention controller and stuff --- app/controllers/interventions_controller.rb | 55 +++++++++++++++++++ app/models/intervention.rb | 17 +++--- app/policies/intervention_policy.rb | 34 ++++++++++++ app/views/interventions/_form.html.slim | 6 ++ app/views/interventions/index.html.slim | 14 +++++ app/views/interventions/show.html.slim | 4 ++ config/locales/de.yml | 2 + config/locales/en.yml | 2 + config/routes.rb | 9 +++ .../20170205210357_create_interventions.rb | 6 ++ 10 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 app/controllers/interventions_controller.rb create mode 100644 app/policies/intervention_policy.rb create mode 100644 app/views/interventions/_form.html.slim create mode 100644 app/views/interventions/index.html.slim create mode 100644 app/views/interventions/show.html.slim diff --git a/app/controllers/interventions_controller.rb b/app/controllers/interventions_controller.rb new file mode 100644 index 00000000..b4b5971e --- /dev/null +++ b/app/controllers/interventions_controller.rb @@ -0,0 +1,55 @@ +class InterventionsController < ApplicationController + include CommonBehavior + + before_action :set_intervention, only: MEMBER_ACTIONS + + def authorize! + authorize(@intervention || @interventions) + end + private :authorize! + + def create + #@intervention = Intervention.new(intervention_params) + #authorize! + #create_and_respond(object: @intervention) + end + + def destroy + destroy_and_respond(object: @intervention) + end + + def edit + end + + def intervention_params + params[:intervention].permit(:name) + end + private :intervention_params + + def index + @interventions = Intervention.all.paginate(page: params[:page]) + authorize! + end + + def new + #@intervention = Intervention.new + #authorize! + end + + def set_intervention + @intervention = Intervention.find(params[:id]) + authorize! + end + private :set_intervention + + def show + end + + def update + update_and_respond(object: @intervention, params: intervention_params) + end + + def to_s + name + end +end diff --git a/app/models/intervention.rb b/app/models/intervention.rb index 960a4188..a6693450 100644 --- a/app/models/intervention.rb +++ b/app/models/intervention.rb @@ -1,15 +1,16 @@ class Intervention < ActiveRecord::Base - NAME = %w(overallSlower longSession syntaxErrors videoNotWatched) - has_many :user_exercise_interventions has_many :users, through: :user_exercise_interventions, source_type: "ExternalUser" - #belongs_to :user, polymorphic: true - #belongs_to :external_users, source: :user, source_type: ExternalUser - #belongs_to :internal_users, source: :user, source_type: InternalUser, through: :user_interventions - # alias_method :users, :external_users - #has_many :exercises, through: :user_interventions - validates :name, inclusion: {in: NAME} + def to_s + name + end + + def self.createDefaultInterventions + %w(BreakIntervention QuestionIntervention).each do |name| + Intervention.find_or_create_by(name: name) + end + end end \ No newline at end of file diff --git a/app/policies/intervention_policy.rb b/app/policies/intervention_policy.rb new file mode 100644 index 00000000..b3a25667 --- /dev/null +++ b/app/policies/intervention_policy.rb @@ -0,0 +1,34 @@ +class InterventionPolicy < AdminOrAuthorPolicy + def author? + @user == @record.author + end + private :author? + + def batch_update? + admin? + end + + def show? + @user.internal_user? + end + + [:clone?, :destroy?, :edit?, :update?].each do |action| + define_method(action) { admin? || author?} + end + + [:reload?].each do |action| + define_method(action) { everyone } + end + + class Scope < Scope + def resolve + if @user.admin? + @scope.all + elsif @user.internal_user? + @scope.where('user_id = ? OR public = TRUE', @user.id) + else + @scope.none + end + end + end +end diff --git a/app/views/interventions/_form.html.slim b/app/views/interventions/_form.html.slim new file mode 100644 index 00000000..6ffe7397 --- /dev/null +++ b/app/views/interventions/_form.html.slim @@ -0,0 +1,6 @@ += form_for(@intervention) do |f| + = render('shared/form_errors', object: @intervention) + .form-group + = f.label(:name) + = f.text_field(:name, class: 'form-control', required: true) + .actions = render('shared/submit_button', f: f, object: @intervention) diff --git a/app/views/interventions/index.html.slim b/app/views/interventions/index.html.slim new file mode 100644 index 00000000..fc7afe05 --- /dev/null +++ b/app/views/interventions/index.html.slim @@ -0,0 +1,14 @@ +h1 = Intervention.model_name.human(count: 2) + +.table-responsive + table.table + thead + tr + th = t('activerecord.attributes.intervention.name') + tbody + - @interventions.each do |intervention| + tr + td = intervention.name + td = link_to(t('shared.show'), intervention) + += render('shared/pagination', collection: @interventions) diff --git a/app/views/interventions/show.html.slim b/app/views/interventions/show.html.slim new file mode 100644 index 00000000..f9202240 --- /dev/null +++ b/app/views/interventions/show.html.slim @@ -0,0 +1,4 @@ +h1 + = @intervention.name + += row(label: 'intervention.name', value: @intervention.name) diff --git a/config/locales/de.yml b/config/locales/de.yml index 09c6932c..cda9f8f4 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -75,6 +75,8 @@ de: message: Nachricht name: Name regular_expression: Regulärer Ausdruck + intervention: + name: Name internal_user: activated: Aktiviert consumer: Konsument diff --git a/config/locales/en.yml b/config/locales/en.yml index e3c0bc6f..b0c54a09 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -96,6 +96,8 @@ en: message: Message name: Name regular_expression: Regular Expression + intervention: + name: Name internal_user: activated: Activated consumer: Consumer diff --git a/config/routes.rb b/config/routes.rb index 87cde74c..79f57fe5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,6 +61,7 @@ Rails.application.routes.draw do post :clone get :implement get :working_times + post :intervention get :statistics get :reload post :submit @@ -83,6 +84,14 @@ Rails.application.routes.draw do end end + resources :interventions do + member do + post :clone + get :reload + post :submit + end + end + resources :external_users, only: [:index, :show], concerns: :statistics do resources :exercises, concerns: :statistics end diff --git a/db/migrate/20170205210357_create_interventions.rb b/db/migrate/20170205210357_create_interventions.rb index 1b7a8121..803b8ddc 100644 --- a/db/migrate/20170205210357_create_interventions.rb +++ b/db/migrate/20170205210357_create_interventions.rb @@ -4,6 +4,7 @@ class CreateInterventions < ActiveRecord::Migration t.belongs_to :user, polymorphic: true t.belongs_to :exercise t.belongs_to :intervention + t.integer :accumulated_worktime_s t.timestamps end @@ -12,5 +13,10 @@ class CreateInterventions < ActiveRecord::Migration t.text :markup t.timestamps end + + Intervention.createDefaultInterventions + end + + end From bfc96328c427087990966311e72dddc07d34f404 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 28 Feb 2017 15:26:36 +0100 Subject: [PATCH 046/143] added interventions back to code. added post method to be able to save interventions --- app/assets/javascripts/editor/editor.js.erb | 18 ++++++++++++++---- app/controllers/exercises_controller.rb | 13 +++++++++++-- app/models/exercise.rb | 5 +++-- app/models/proxy_exercise.rb | 2 +- app/policies/exercise_policy.rb | 2 +- 5 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 0e644d7d..3249fe9d 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -545,12 +545,14 @@ configureEditors: function () { $('#output_sidebar_collapsed').addClass('hidden'); $('#output_sidebar_uncollapsed').removeClass('hidden'); $('#output_sidebar').removeClass('output-col-collapsed').addClass('output-col'); + this.resizeAceEditors(); }, hideOutputBar: function() { $('#output_sidebar_collapsed').removeClass('hidden'); $('#output_sidebar_uncollapsed').addClass('hidden'); $('#output_sidebar').removeClass('output-col').addClass('output-col-collapsed'); + this.resizeAceEditors(); }, initializeSideBarTooltips: function() { @@ -587,15 +589,15 @@ configureEditors: function () { var percentile75 = data['working_time_75_percentile']; var accumulatedWorkTimeUser = data['working_time_accumulated']; - var timeUntilBreak = 15 * 60; + var timeUntilBreak = 15 * 60 * 1000; if ((accumulatedWorkTimeUser - percentile75) > 0) { // working time is already over 75 percentile - var timeUntilAskQuestion = 7 * 60; + var timeUntilAskQuestion = 10 * 60 * 1000; } else { // working time is less than 75 percentile // ensure we give user at least 10 minutes before we bother the user - var timeUntilAskQuestion = (percentile75 - accumulatedWorkTimeUser) > 10 * 60 ? (percentile75 - accumulatedWorkTimeUser) : 10 * 60; + var timeUntilAskQuestion = (percentile75 - accumulatedWorkTimeUser) > 10 * 60 * 1000 ? (percentile75 - accumulatedWorkTimeUser) : 10 * 60 * 1000; } // if notifications are too close to each other, ensure some time differences between them @@ -605,7 +607,15 @@ configureEditors: function () { setTimeout(function() { $('#intervention-text').text(`Willst du eine Pause machen? 75th percentile: ${percentile75} and accumulated time: ${accumulatedWorkTimeUser}`); - $('#intervention-modal').modal('show') + $('#intervention-modal').modal('show'); + $.ajax({ + data: { + exercise_id: $('#editor').data('exercise-id'), + user_id: $('#editor').data('user-id') + }, + dataType: 'json', + type: 'POST', + url: "localhost:3000/exercise/intervention"}); }, timeUntilBreak); setTimeout(function() { diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index b9f4972b..9dc950cc 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -6,7 +6,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, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :run, :statistics, :submit, :reload] + before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :run, :statistics, :submit, :reload] before_action :set_external_user, only: [:statistics] before_action :set_file_types, only: [:create, :edit, :new, :update] @@ -167,11 +167,20 @@ class ExercisesController < ApplicationController end def working_times - working_time_accumulated = @exercise.accumulated_working_time_for_only(current_user.id) + working_time_accumulated = @exercise.accumulated_working_time_for_only(current_user) working_time_75_percentile = @exercise.get_quantiles([0.75]).first render(json: {working_time_75_percentile: working_time_75_percentile, working_time_accumulated: working_time_accumulated}) end + def intervention + uei = UserExerciseIntervention.new( + user: current_user, exercise: @exercise, intervention: Intervention.first, + accumulated_worktime: @exercise.accumulated_working_time_for_only(current_user)) + + puts "user: #{current_user}, intervention: #{Intervention.first} #{uei.save}" + render(json: {success: 'true'}) + end + def index @search = policy_scope(Exercise).search(params[:q]) @exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page]) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 3419d9d8..1a399427 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -126,7 +126,8 @@ class Exercise < ActiveRecord::Base @working_time_statistics[user_id]["working_time"] end - def accumulated_working_time_for_only(user_id) + def accumulated_working_time_for_only(user) + user_type = user.external_user? ? "ExternalUser" : "InternalUser" Time.parse(self.class.connection.execute(""" SELECT sum(working_time_new) AS working_time FROM @@ -136,7 +137,7 @@ class Exercise < ActiveRecord::Base (created_at - lag(created_at) over (PARTITION BY user_id, exercise_id ORDER BY created_at)) AS working_time FROM submissions - WHERE exercise_id=#{id} and user_id=#{user_id} and user_type='ExternalUser') AS foo) AS bar + WHERE exercise_id=#{id} and user_id=#{user.id} and user_type='#{user_type}') AS foo) AS bar """).first["working_time"] || "00:00:00").seconds_since_midnight end diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index fe81f447..9558abb1 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -144,7 +144,7 @@ class ProxyExercise < ActiveRecord::Base return 0.0 end points_ratio_index = ((scoring_matrix.size - 1) * points_ratio).to_i - working_time_user = ex.accumulated_working_time_for_only(user.id) + working_time_user = ex.accumulated_working_time_for_only(user) quantiles_working_time = ex.get_quantiles(scoring_matrix_quantiles) quantile_index = quantiles_working_time.size quantiles_working_time.each_with_index do |quantile_time, i| diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb index c89ad86a..6377488b 100644 --- a/app/policies/exercise_policy.rb +++ b/app/policies/exercise_policy.rb @@ -16,7 +16,7 @@ class ExercisePolicy < AdminOrAuthorPolicy define_method(action) { admin? || author?} end - [:implement?, :working_times?, :submit?, :reload?].each do |action| + [:implement?, :working_times?, :intervention?, :submit?, :reload?].each do |action| define_method(action) { everyone } end From 904868394aaef93afc305509b37309b63fd30af4 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 28 Feb 2017 16:06:45 +0100 Subject: [PATCH 047/143] added interventions being saved once they are fired --- app/assets/javascripts/editor/editor.js.erb | 16 +++++++++++++--- app/controllers/exercises_controller.rb | 15 ++++++++++----- app/views/exercises/_editor.html.slim | 2 +- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 3249fe9d..889291be 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -611,16 +611,26 @@ configureEditors: function () { $.ajax({ data: { exercise_id: $('#editor').data('exercise-id'), - user_id: $('#editor').data('user-id') + user_id: $('#editor').data('user-id'), + intervention_type: 'BreakIntervention' }, dataType: 'json', type: 'POST', - url: "localhost:3000/exercise/intervention"}); + url: $('#editor').data('intervention-save-url')}); }, timeUntilBreak); setTimeout(function() { $('#intervention-text').text(`Willst du eine Frage stellen?`); - $('#intervention-modal').modal('show') + $('#intervention-modal').modal('show'); + $.ajax({ + data: { + exercise_id: $('#editor').data('exercise-id'), + user_id: $('#editor').data('user-id'), + intervention_type: 'QuestionIntervention' + }, + dataType: 'json', + type: 'POST', + url: $('#editor').data('intervention-save-url')}); }, timeUntilAskQuestion); } }); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 9dc950cc..e339c1c9 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -173,12 +173,17 @@ class ExercisesController < ApplicationController end def intervention - uei = UserExerciseIntervention.new( - user: current_user, exercise: @exercise, intervention: Intervention.first, - accumulated_worktime: @exercise.accumulated_working_time_for_only(current_user)) + intervention = Intervention.find_by_name(params[:intervention_type]) + unless intervention.nil? + uei = UserExerciseIntervention.new( + user: current_user, exercise: @exercise, intervention: intervention, + accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_user)) + uei.save + render(json: {success: 'true'}) + else + render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"}) + end - puts "user: #{current_user}, intervention: #{Intervention.first} #{uei.save}" - render(json: {success: 'true'}) end def index diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index be82a5b7..b3d9c57b 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -1,7 +1,7 @@ - external_user_external_id = @current_user.respond_to?(:external_id) ? @current_user.external_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - external_user_id = @current_user.respond_to?(:external_id) ? @current_user.id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - consumer_id = @current_user.respond_to?(:external_id) ? @current_user.consumer_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') -#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path +#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path data-intervention-save-url=intervention_exercise_path div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files) div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) div id='frames' class='editor-col' From 350913de79cce95803c1345d2185640e5baaf032 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 28 Feb 2017 16:06:53 +0100 Subject: [PATCH 048/143] fixed LTI Spec --- spec/concerns/lti_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/concerns/lti_spec.rb b/spec/concerns/lti_spec.rb index 95181d3b..c03ef9a5 100644 --- a/spec/concerns/lti_spec.rb +++ b/spec/concerns/lti_spec.rb @@ -165,6 +165,7 @@ describe Lti do it 'stores data in the session' do controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user)) + controller.instance_variable_set(:@exercise, FactoryGirl.create(:fibonacci)) expect(controller.session).to receive(:[]=).with(:consumer_id, anything) expect(controller.session).to receive(:[]=).with(:external_user_id, anything) controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters) @@ -172,6 +173,8 @@ describe Lti do it 'it creates an LtiParameter Object' do before_count = LtiParameter.count + controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user)) + controller.instance_variable_set(:@exercise, FactoryGirl.create(:fibonacci)) controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters) expect(LtiParameter.count).to eq(before_count + 1) end From 3cc56952810154d37e8f04285d63589f46f52b94 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 28 Feb 2017 16:09:07 +0100 Subject: [PATCH 049/143] modal angepasst --- app/views/interventions/_intervention_modal.html.slim | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/interventions/_intervention_modal.html.slim b/app/views/interventions/_intervention_modal.html.slim index af783dfa..027e4f97 100644 --- a/app/views/interventions/_intervention_modal.html.slim +++ b/app/views/interventions/_intervention_modal.html.slim @@ -1,8 +1,7 @@ /h5 = t('exercises.implement.comment.question') -h5 = 'Aufpassen!' +h5 = 'Hinweis' /textarea.form-control#question(style='resize:none;') -p = 'Uns ist aufgefallen, dass Sie schon sehr lange an dieser Aufgabe sitzen.' #intervention-text /p = "AVG: #{@working_time_avg}" From 9c4b981bcb9b2d376f91484cd91f67e70ade0d52 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 28 Feb 2017 16:10:50 +0100 Subject: [PATCH 050/143] removed unnecessary stuff in Ajax --- app/assets/javascripts/editor/editor.js.erb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 889291be..ef632fc1 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -610,8 +610,6 @@ configureEditors: function () { $('#intervention-modal').modal('show'); $.ajax({ data: { - exercise_id: $('#editor').data('exercise-id'), - user_id: $('#editor').data('user-id'), intervention_type: 'BreakIntervention' }, dataType: 'json', @@ -624,8 +622,6 @@ configureEditors: function () { $('#intervention-modal').modal('show'); $.ajax({ data: { - exercise_id: $('#editor').data('exercise-id'), - user_id: $('#editor').data('user-id'), intervention_type: 'QuestionIntervention' }, dataType: 'json', From 17d09accb752c97e8e3ea538542f9f5905b52b51 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 28 Feb 2017 17:29:52 +0100 Subject: [PATCH 051/143] only show interventions if condition is met. right now, only show one intervention per user and exercise --- app/assets/javascripts/editor/editor.js.erb | 4 +++- app/controllers/exercises_controller.rb | 6 ++++++ app/views/exercises/_editor.html.slim | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index ef632fc1..bd2dc69b 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -646,7 +646,9 @@ configureEditors: function () { this.initializeDescriptionToggle(); this.initializeSideBarTooltips(); this.initializeTooltips(); - this.initializeInterventionTimer(); + if ($('#editor').data('show-interventions') == true){ + this.initializeInterventionTimer(); + } this.initPrompt(); this.renderScore(); this.showFirstFile(); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index e339c1c9..97910b18 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -155,6 +155,12 @@ class ExercisesController < ApplicationController def implement redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists? + @show_interventions = + if UserExerciseIntervention.find_by(exercise: @exercise, user: current_user) + "false" + else + "true" + end @submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first @files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension) @paths = collect_paths(@files) diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index b3d9c57b..b4cf7bb8 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -1,7 +1,8 @@ - external_user_external_id = @current_user.respond_to?(:external_id) ? @current_user.external_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - external_user_id = @current_user.respond_to?(:external_id) ? @current_user.id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - consumer_id = @current_user.respond_to?(:external_id) ? @current_user.consumer_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') -#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path data-intervention-save-url=intervention_exercise_path +- show_interventions = @show_interventions || "false" +#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path data-intervention-save-url=intervention_exercise_path data-show-interventions=show_interventions div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files) div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) div id='frames' class='editor-col' From 695b8946f6c2a16dbbb20f06bd9f72590f7aece9 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 1 Mar 2017 11:49:54 +0100 Subject: [PATCH 052/143] added search intervention. search opens new tab with search in the java course (at least in chrome) send only 3 interventions per exercise at maximum --- app/assets/javascripts/editor/editor.js.erb | 10 +++++- app/controllers/exercises_controller.rb | 4 ++- app/controllers/searches_controller.rb | 34 +++++++++++++++++++ app/models/search.rb | 4 +++ app/policies/search_policy.rb | 34 +++++++++++++++++++ app/views/exercises/_editor.html.slim | 2 +- .../_intervention_modal.html.slim | 7 +++- app/views/searches/destroy.html.erb | 0 config/routes.rb | 8 +++++ db/migrate/20170228165741_add_search.rb | 10 ++++++ 10 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 app/controllers/searches_controller.rb create mode 100644 app/models/search.rb create mode 100644 app/policies/search_policy.rb create mode 100644 app/views/searches/destroy.html.erb create mode 100644 db/migrate/20170228165741_add_search.rb diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index bd2dc69b..57e94263 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -618,7 +618,7 @@ configureEditors: function () { }, timeUntilBreak); setTimeout(function() { - $('#intervention-text').text(`Willst du eine Frage stellen?`); + $('#intervention-text').html("Möchtest du eine Frage stellen?"); $('#intervention-modal').modal('show'); $.ajax({ data: { @@ -632,6 +632,13 @@ configureEditors: function () { }); }, + initializeSearchButton: function(){ + $('.btn-search').button().click(function(){ + var search = $('#search_search').val(); + window.open(`https://open.hpi.de/courses/javaeinstieg2017/pinboard?query=${search}`, '_blank', 'toolbar=yes, location=yes, status=yes, menubar=yes, scrollbars=yes'); + }) + }, + initializeEverything: function() { this.initializeRegexes(); @@ -648,6 +655,7 @@ configureEditors: function () { this.initializeTooltips(); if ($('#editor').data('show-interventions') == true){ this.initializeInterventionTimer(); + this.initializeSearchButton(); } this.initPrompt(); this.renderScore(); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 97910b18..3742de4d 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -156,11 +156,13 @@ class ExercisesController < ApplicationController def implement redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists? @show_interventions = - if UserExerciseIntervention.find_by(exercise: @exercise, user: current_user) + if UserExerciseIntervention.where(exercise: @exercise, user: current_user).count >= 3 "false" else "true" end + @search = Search.new + @search.exercise = @exercise @submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first @files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension) @paths = collect_paths(@files) diff --git a/app/controllers/searches_controller.rb b/app/controllers/searches_controller.rb new file mode 100644 index 00000000..8af6b364 --- /dev/null +++ b/app/controllers/searches_controller.rb @@ -0,0 +1,34 @@ +class SearchesController < ApplicationController + include CommonBehavior + + def authorize! + authorize(@search || @searchs) + end + private :authorize! + + + def create + @search = Search.new(search_params) + @search.user = current_user + authorize! + + respond_to do |format| + if @search.save + path = implement_exercise_path(@search.exercise) + respond_with_valid_object(format, path: path, status: :created) + end + end + end + + def search_params + params[:search].permit(:search, :exercise_id) + end + private :search_params + + def index + @search = policy_scope(ProxyExercise).search(params[:q]) + @searches = @search.result.order(:title).paginate(page: params[:page]) + authorize! + end + +end \ No newline at end of file diff --git a/app/models/search.rb b/app/models/search.rb new file mode 100644 index 00000000..f22dbc3e --- /dev/null +++ b/app/models/search.rb @@ -0,0 +1,4 @@ +class Search < ActiveRecord::Base + belongs_to :user, polymorphic: true + belongs_to :exercise +end \ No newline at end of file diff --git a/app/policies/search_policy.rb b/app/policies/search_policy.rb new file mode 100644 index 00000000..9da9a641 --- /dev/null +++ b/app/policies/search_policy.rb @@ -0,0 +1,34 @@ +class SearchPolicy < AdminOrAuthorPolicy + def author? + @user == @record.author + end + private :author? + + def batch_update? + admin? + end + + def show? + @user.internal_user? + end + + [:clone?, :destroy?, :edit?, :update?].each do |action| + define_method(action) { admin? || author?} + end + + [:reload?].each do |action| + define_method(action) { everyone } + end + + class Scope < Scope + def resolve + if @user.admin? + @scope.all + elsif @user.internal_user? + @scope.where('user_id = ? OR public = TRUE', @user.id) + else + @scope.none + end + end + end +end diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index b4cf7bb8..bae90a13 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -23,4 +23,4 @@ = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') -= render('shared/modal', id: 'intervention-modal', title: 'Leg mal eine Pause ein', template: 'interventions/_intervention_modal') \ No newline at end of file += render('shared/modal', id: 'intervention-modal', title: 'Hinweis', template: 'interventions/_intervention_modal') \ No newline at end of file diff --git a/app/views/interventions/_intervention_modal.html.slim b/app/views/interventions/_intervention_modal.html.slim index 027e4f97..a432751d 100644 --- a/app/views/interventions/_intervention_modal.html.slim +++ b/app/views/interventions/_intervention_modal.html.slim @@ -1,9 +1,14 @@ /h5 = t('exercises.implement.comment.question') -h5 = 'Hinweis' /textarea.form-control#question(style='resize:none;') #intervention-text += form_for(@search, multipart: true, target: "_blank") do |f| + .form-group + = f.text_field(:search, class: 'form-control', required: true) + = f.hidden_field :exercise_id + .actions + = f.submit(class: 'btn btn-default btn-search', value: 'Suche', model: @search.class.model_name.human) /p = "AVG: #{@working_time_avg}" /p = "ACCUMULATED: #{@working_time_accumulated}" diff --git a/app/views/searches/destroy.html.erb b/app/views/searches/destroy.html.erb new file mode 100644 index 00000000..e69de29b diff --git a/config/routes.rb b/config/routes.rb index 79f57fe5..a33369d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -84,6 +84,14 @@ Rails.application.routes.draw do end end + resources :searches do + member do + post :clone + get :reload + post :submit + end + end + resources :interventions do member do post :clone diff --git a/db/migrate/20170228165741_add_search.rb b/db/migrate/20170228165741_add_search.rb new file mode 100644 index 00000000..a36d94ff --- /dev/null +++ b/db/migrate/20170228165741_add_search.rb @@ -0,0 +1,10 @@ +class AddSearch < ActiveRecord::Migration + def change + create_table :searches do |t| + t.belongs_to :exercise, null: false + t.belongs_to :user, polymorphic: true, null: false + t.string :search + t.timestamps + end + end +end From 5b50deb70dc341592764f22fe047313e61a35ea5 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 1 Mar 2017 11:58:16 +0100 Subject: [PATCH 053/143] split intervention modal into 2 separate modals --- app/assets/javascripts/editor/editor.js.erb | 6 ++---- app/views/exercises/_editor.html.slim | 3 ++- .../interventions/_break_intervention_modal.html.slim | 11 +++++++++++ ...html.slim => _search_intervention_modal.html.slim} | 0 4 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 app/views/interventions/_break_intervention_modal.html.slim rename app/views/interventions/{_intervention_modal.html.slim => _search_intervention_modal.html.slim} (100%) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 57e94263..f61fa406 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -606,8 +606,7 @@ configureEditors: function () { } setTimeout(function() { - $('#intervention-text').text(`Willst du eine Pause machen? 75th percentile: ${percentile75} and accumulated time: ${accumulatedWorkTimeUser}`); - $('#intervention-modal').modal('show'); + $('#break-intervention-modal').modal('show'); $.ajax({ data: { intervention_type: 'BreakIntervention' @@ -618,8 +617,7 @@ configureEditors: function () { }, timeUntilBreak); setTimeout(function() { - $('#intervention-text').html("Möchtest du eine Frage stellen?"); - $('#intervention-modal').modal('show'); + $('#search-intervention-modal').modal('show'); $.ajax({ data: { intervention_type: 'QuestionIntervention' diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index bae90a13..8b4b2c86 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -23,4 +23,5 @@ = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') -= render('shared/modal', id: 'intervention-modal', title: 'Hinweis', template: 'interventions/_intervention_modal') \ No newline at end of file += render('shared/modal', id: 'break-intervention-modal', title: 'Hinweis', template: 'interventions/_break_intervention_modal') += render('shared/modal', id: 'search-intervention-modal', title: 'Search', template: 'interventions/_search_intervention_modal') \ No newline at end of file diff --git a/app/views/interventions/_break_intervention_modal.html.slim b/app/views/interventions/_break_intervention_modal.html.slim new file mode 100644 index 00000000..16871e86 --- /dev/null +++ b/app/views/interventions/_break_intervention_modal.html.slim @@ -0,0 +1,11 @@ +/h5 = t('exercises.implement.comment.question') + +/textarea.form-control#question(style='resize:none;') +#intervention-text + +/p = "AVG: #{@working_time_avg}" +/p = "ACCUMULATED: #{@working_time_accumulated}" + +/ data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action. +/ But if we use this button, it will work since the correct cause is supplied +/button#askForCommentsButton.btn.btn-block.btn-primary(type='button' data-cause='requestComments' data-message-success=t('exercises.editor.request_for_comments_sent')) =t('exercises.implement.comment.request') \ No newline at end of file diff --git a/app/views/interventions/_intervention_modal.html.slim b/app/views/interventions/_search_intervention_modal.html.slim similarity index 100% rename from app/views/interventions/_intervention_modal.html.slim rename to app/views/interventions/_search_intervention_modal.html.slim From 5d2eb6f381daf318a3eff59c96c97916c9a8b4a3 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 1 Mar 2017 14:29:59 +0100 Subject: [PATCH 054/143] fixed search in firefox --- app/assets/javascripts/editor/editor.js.erb | 2 +- app/views/interventions/_break_intervention_modal.html.slim | 2 +- app/views/interventions/_search_intervention_modal.html.slim | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index f61fa406..0c37140e 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -633,7 +633,7 @@ configureEditors: function () { initializeSearchButton: function(){ $('.btn-search').button().click(function(){ var search = $('#search_search').val(); - window.open(`https://open.hpi.de/courses/javaeinstieg2017/pinboard?query=${search}`, '_blank', 'toolbar=yes, location=yes, status=yes, menubar=yes, scrollbars=yes'); + window.open(`https://open.hpi.de/courses/javaeinstieg2017/pinboard?query=${search}`, '_blank'); }) }, diff --git a/app/views/interventions/_break_intervention_modal.html.slim b/app/views/interventions/_break_intervention_modal.html.slim index 16871e86..e9c222f3 100644 --- a/app/views/interventions/_break_intervention_modal.html.slim +++ b/app/views/interventions/_break_intervention_modal.html.slim @@ -1,7 +1,7 @@ /h5 = t('exercises.implement.comment.question') /textarea.form-control#question(style='resize:none;') -#intervention-text +p = "Uns ist aufgefallen, dass du schon lange an dieser Aufgabe sitzt. Möchtest du vielleicht mal eine Pause machen um auf neue Gedanken zu kommen?" /p = "AVG: #{@working_time_avg}" /p = "ACCUMULATED: #{@working_time_accumulated}" diff --git a/app/views/interventions/_search_intervention_modal.html.slim b/app/views/interventions/_search_intervention_modal.html.slim index a432751d..06f69914 100644 --- a/app/views/interventions/_search_intervention_modal.html.slim +++ b/app/views/interventions/_search_intervention_modal.html.slim @@ -1,14 +1,14 @@ /h5 = t('exercises.implement.comment.question') /textarea.form-control#question(style='resize:none;') -#intervention-text +p = "Hast du Probleme beim Lösen der Aufgabe? Benutz doch einfach die Forensuche:" = form_for(@search, multipart: true, target: "_blank") do |f| .form-group = f.text_field(:search, class: 'form-control', required: true) = f.hidden_field :exercise_id .actions - = f.submit(class: 'btn btn-default btn-search', value: 'Suche', model: @search.class.model_name.human) + = f.submit(class: 'btn btn-default btn-search', value: 'Suche im Forum', model: @search.class.model_name.human) /p = "AVG: #{@working_time_avg}" /p = "ACCUMULATED: #{@working_time_accumulated}" From eb0c79a043e8b09c73aae2767e59c1cafbc76ea6 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 1 Mar 2017 17:29:27 +0100 Subject: [PATCH 055/143] added search bar to the side col --- app/assets/javascripts/editor/editor.js.erb | 9 +++++++-- app/views/exercises/_editor_file_tree.html.slim | 9 +++++++++ .../interventions/_search_intervention_modal.html.slim | 6 ++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 0c37140e..44c9c989 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -631,8 +631,13 @@ configureEditors: function () { }, initializeSearchButton: function(){ - $('.btn-search').button().click(function(){ - var search = $('#search_search').val(); + $('#btn-search-col').button().click(function(){ + var search = $('#search-col').val(); + window.open(`https://open.hpi.de/courses/javaeinstieg2017/pinboard?query=${search}`, '_blank'); + }) + + $('#btn-search-modal').button().click(function(){ + var search = $('#search-modal').val(); window.open(`https://open.hpi.de/courses/javaeinstieg2017/pinboard?query=${search}`, '_blank'); }) }, diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index 16cc705b..c8450687 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -23,6 +23,15 @@ div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over')) + div.enforce-top-margin + = form_for(@search, multipart: true, target: "_blank") do |f| + .form-group + = f.hidden_field :exercise_id + = f.text_field(:search, class: 'form-control', id: "search-col", required: true, placeholder: "Probleme? Suche hier im Forum") + .actions + = button_tag(class: 'btn btn-block btn-primary btn-sm', id: 'btn-search-col', model: @search.class.model_name.human) do + i.fa.fa-search + = 'Suche im Forum' - if @exercise.allow_file_creation? = render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file')) \ No newline at end of file diff --git a/app/views/interventions/_search_intervention_modal.html.slim b/app/views/interventions/_search_intervention_modal.html.slim index 06f69914..c27d5c7a 100644 --- a/app/views/interventions/_search_intervention_modal.html.slim +++ b/app/views/interventions/_search_intervention_modal.html.slim @@ -4,11 +4,13 @@ p = "Hast du Probleme beim Lösen der Aufgabe? Benutz doch einfach die Forensuche:" = form_for(@search, multipart: true, target: "_blank") do |f| .form-group - = f.text_field(:search, class: 'form-control', required: true) + = f.text_field(:search, class: 'form-control', id: 'search-modal', required: true) = f.hidden_field :exercise_id .actions - = f.submit(class: 'btn btn-default btn-search', value: 'Suche im Forum', model: @search.class.model_name.human) + = button_tag(class: 'btn btn-block btn-primary btn-sm', id: 'btn-search-modal', model: @search.class.model_name.human) do + i.fa.fa-search + = 'Suche im Forum' /p = "AVG: #{@working_time_avg}" /p = "ACCUMULATED: #{@working_time_accumulated}" From 0e2a22df4246ff219c410fcfb30d39ce418bba59 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 2 Mar 2017 14:23:19 +0100 Subject: [PATCH 056/143] texte angepasst --- app/views/exercises/_editor.html.slim | 2 +- app/views/interventions/_search_intervention_modal.html.slim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 8b4b2c86..31893ba9 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -24,4 +24,4 @@ = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') = render('shared/modal', id: 'break-intervention-modal', title: 'Hinweis', template: 'interventions/_break_intervention_modal') -= render('shared/modal', id: 'search-intervention-modal', title: 'Search', template: 'interventions/_search_intervention_modal') \ No newline at end of file += render('shared/modal', id: 'search-intervention-modal', title: 'Hast du Probleme beim Lösen der Aufgabe?', template: 'interventions/_search_intervention_modal') \ No newline at end of file diff --git a/app/views/interventions/_search_intervention_modal.html.slim b/app/views/interventions/_search_intervention_modal.html.slim index c27d5c7a..1355a80b 100644 --- a/app/views/interventions/_search_intervention_modal.html.slim +++ b/app/views/interventions/_search_intervention_modal.html.slim @@ -1,7 +1,7 @@ /h5 = t('exercises.implement.comment.question') /textarea.form-control#question(style='resize:none;') -p = "Hast du Probleme beim Lösen der Aufgabe? Benutz doch einfach die Forensuche:" +p = "Benutz doch einfach die Forensuche! Du kannst auch jederzeit in der linken Spalte die Suche nutzen. Alternativ kannst du auch nach Kommentaren zu deinem Programm anfordern." = form_for(@search, multipart: true, target: "_blank") do |f| .form-group = f.text_field(:search, class: 'form-control', id: 'search-modal', required: true) From a481ec0da8047d19380026c81c4ccbddef109c0c Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 2 Mar 2017 14:41:55 +0100 Subject: [PATCH 057/143] always show search on left side. placeholder in search input --- app/assets/javascripts/editor/editor.js.erb | 2 +- .../_search_intervention_modal.html.slim | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 44c9c989..1f6b8fdd 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -658,8 +658,8 @@ configureEditors: function () { this.initializeTooltips(); if ($('#editor').data('show-interventions') == true){ this.initializeInterventionTimer(); - this.initializeSearchButton(); } + this.initializeSearchButton(); this.initPrompt(); this.renderScore(); this.showFirstFile(); diff --git a/app/views/interventions/_search_intervention_modal.html.slim b/app/views/interventions/_search_intervention_modal.html.slim index 1355a80b..0d45aadc 100644 --- a/app/views/interventions/_search_intervention_modal.html.slim +++ b/app/views/interventions/_search_intervention_modal.html.slim @@ -1,19 +1,10 @@ -/h5 = t('exercises.implement.comment.question') - -/textarea.form-control#question(style='resize:none;') p = "Benutz doch einfach die Forensuche! Du kannst auch jederzeit in der linken Spalte die Suche nutzen. Alternativ kannst du auch nach Kommentaren zu deinem Programm anfordern." = form_for(@search, multipart: true, target: "_blank") do |f| .form-group - = f.text_field(:search, class: 'form-control', id: 'search-modal', required: true) + = f.text_field(:search, class: 'form-control', id: 'search-modal', required: true, placeholder: "Probleme? Suche hier im Forum",) = f.hidden_field :exercise_id .actions = button_tag(class: 'btn btn-block btn-primary btn-sm', id: 'btn-search-modal', model: @search.class.model_name.human) do i.fa.fa-search - = 'Suche im Forum' -/p = "AVG: #{@working_time_avg}" -/p = "ACCUMULATED: #{@working_time_accumulated}" - -/ data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action. -/ But if we use this button, it will work since the correct cause is supplied -/button#askForCommentsButton.btn.btn-block.btn-primary(type='button' data-cause='requestComments' data-message-success=t('exercises.editor.request_for_comments_sent')) =t('exercises.implement.comment.request') \ No newline at end of file + = 'Suche im Forum' \ No newline at end of file From 7ef318713b6e4375e757a4f47323edc460db23de Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 2 Mar 2017 16:16:27 +0100 Subject: [PATCH 058/143] added reason vor proxy exercise --- app/models/proxy_exercise.rb | 16 +++++++++++++++- .../20170205210357_create_interventions.rb | 1 + 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 9558abb1..46a04928 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -1,6 +1,7 @@ class ProxyExercise < ActiveRecord::Base after_initialize :generate_token + after_initialize :set_reason has_and_belongs_to_many :exercises has_many :user_proxy_exercise_exercises @@ -9,6 +10,10 @@ class ProxyExercise < ActiveRecord::Base exercises.count end + def set_reason + @reason = {} + end + def generate_token self.token ||= SecureRandom.hex(4) end @@ -37,9 +42,11 @@ class ProxyExercise < ActiveRecord::Base find_matching_exercise(user) rescue #fallback Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" ) + @reason[:reason] = "fallback because of error" + @reason[:error] = "#{$!}" exercises.shuffle.first end - user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self) + user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json) matching_exercise end recommended_exercise @@ -61,6 +68,8 @@ class ProxyExercise < ActiveRecord::Base Rails.logger.info("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}") # if all exercises contain tags which the user has never seen, recommend easiest exercise if potential_recommended_exercises.empty? + Rails.logger.info("matched easiest exercise in pool") + @reason[:reason] = "easiest exercise in pool. empty potential exercises" select_easiest_exercise(exercises) else recommended_exercise = select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises) @@ -96,6 +105,11 @@ class ProxyExercise < ActiveRecord::Base end highest_difficulty_user_has_accessed = exercises_user_has_accessed.map{|e| e.expected_difficulty}.sort.last || 0 best_matching_exercise = find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed) + @reason[:reason] = "best matching exercise" + @reason[:highest_difficulty_user_has_accessed] = highest_difficulty_user_has_accessed + @reason[:current_users_knowledge_lack] = current_users_knowledge_lack + @reason[:relative_knowledge_improvement] = relative_knowledge_improvement + Rails.logger.info("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}.to_s) Rails.logger.info("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}") best_matching_exercise diff --git a/db/migrate/20170205210357_create_interventions.rb b/db/migrate/20170205210357_create_interventions.rb index 803b8ddc..07223e1c 100644 --- a/db/migrate/20170205210357_create_interventions.rb +++ b/db/migrate/20170205210357_create_interventions.rb @@ -5,6 +5,7 @@ class CreateInterventions < ActiveRecord::Migration t.belongs_to :exercise t.belongs_to :intervention t.integer :accumulated_worktime_s + t.text :reason t.timestamps end From 9761dd0a2af039a68425ae23463e1024648df0bf Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 2 Mar 2017 17:14:45 +0100 Subject: [PATCH 059/143] improved search bar and search button. added button to collapsed sidebar --- app/assets/javascripts/editor/editor.js.erb | 2 ++ app/views/exercises/_editor_file_tree.html.slim | 16 +++++++++------- .../_search_intervention_modal.html.slim | 12 ++++++------ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 1f6b8fdd..f1a6d470 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -640,6 +640,8 @@ configureEditors: function () { var search = $('#search-modal').val(); window.open(`https://open.hpi.de/courses/javaeinstieg2017/pinboard?query=${search}`, '_blank'); }) + + $('#sidebar-search-collapsed').on('click',this.handleSideBarToggle.bind(this)); }, diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index c8450687..817b895c 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -6,6 +6,7 @@ div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download')) = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over')) + = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title:'Suche im Forum') div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) @@ -23,15 +24,16 @@ div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over')) - div.enforce-top-margin - = form_for(@search, multipart: true, target: "_blank") do |f| - .form-group - = f.hidden_field :exercise_id + + = form_for(@search, multipart: true, target: "_blank") do |f| + .input-group.enforce-top-margin + = f.hidden_field :exercise_id + .enforce-right-margin = f.text_field(:search, class: 'form-control', id: "search-col", required: true, placeholder: "Probleme? Suche hier im Forum") - .actions - = button_tag(class: 'btn btn-block btn-primary btn-sm', id: 'btn-search-col', model: @search.class.model_name.human) do + .input-group-btn + = button_tag(class: 'btn btn-primary', id: 'btn-search-col', model: @search.class.model_name.human) do i.fa.fa-search - = 'Suche im Forum' + - if @exercise.allow_file_creation? = render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file')) \ No newline at end of file diff --git a/app/views/interventions/_search_intervention_modal.html.slim b/app/views/interventions/_search_intervention_modal.html.slim index 0d45aadc..69771e9a 100644 --- a/app/views/interventions/_search_intervention_modal.html.slim +++ b/app/views/interventions/_search_intervention_modal.html.slim @@ -1,10 +1,10 @@ p = "Benutz doch einfach die Forensuche! Du kannst auch jederzeit in der linken Spalte die Suche nutzen. Alternativ kannst du auch nach Kommentaren zu deinem Programm anfordern." = form_for(@search, multipart: true, target: "_blank") do |f| - .form-group - = f.text_field(:search, class: 'form-control', id: 'search-modal', required: true, placeholder: "Probleme? Suche hier im Forum",) + .input-group.enforce-top-margin = f.hidden_field :exercise_id + .enforce-right-margin + = f.text_field(:search, class: 'form-control', id: 'search-modal', required: true, placeholder: "Probleme? Suche hier im Forum",) + .input-group-btn + = button_tag(class: 'btn btn-block btn-primary', id: 'btn-search-modal', model: @search.class.model_name.human) do + i.fa.fa-search - .actions - = button_tag(class: 'btn btn-block btn-primary btn-sm', id: 'btn-search-modal', model: @search.class.model_name.human) do - i.fa.fa-search - = 'Suche im Forum' \ No newline at end of file From c1209e49727b7b6c611878ecbc336a20306fe926 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 8 Mar 2017 11:33:23 +0100 Subject: [PATCH 060/143] added translation for search bar --- app/views/exercises/_editor_file_tree.html.slim | 4 ++-- app/views/interventions/_search_intervention_modal.html.slim | 2 +- config/locales/de.yml | 2 ++ config/locales/en.yml | 2 ++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index 817b895c..9021e54a 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -6,7 +6,7 @@ div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download')) = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over')) - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title:'Suche im Forum') + = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum')) div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) @@ -29,7 +29,7 @@ div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') .input-group.enforce-top-margin = f.hidden_field :exercise_id .enforce-right-margin - = f.text_field(:search, class: 'form-control', id: "search-col", required: true, placeholder: "Probleme? Suche hier im Forum") + = f.text_field(:search, class: 'form-control', id: "search-col", required: true, placeholder: t('search.search_in_forum')) .input-group-btn = button_tag(class: 'btn btn-primary', id: 'btn-search-col', model: @search.class.model_name.human) do i.fa.fa-search diff --git a/app/views/interventions/_search_intervention_modal.html.slim b/app/views/interventions/_search_intervention_modal.html.slim index 69771e9a..80c6fca8 100644 --- a/app/views/interventions/_search_intervention_modal.html.slim +++ b/app/views/interventions/_search_intervention_modal.html.slim @@ -3,7 +3,7 @@ p = "Benutz doch einfach die Forensuche! Du kannst auch jederzeit in der linken .input-group.enforce-top-margin = f.hidden_field :exercise_id .enforce-right-margin - = f.text_field(:search, class: 'form-control', id: 'search-modal', required: true, placeholder: "Probleme? Suche hier im Forum",) + = f.text_field(:search, class: 'form-control', id: 'search-modal', required: true, placeholder: t('search.search_in_forum')) .input-group-btn = button_tag(class: 'btn btn-block btn-primary', id: 'btn-search-modal', model: @search.class.model_name.human) do i.fa.fa-search diff --git a/config/locales/de.yml b/config/locales/de.yml index cda9f8f4..5f8a7bb8 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -346,6 +346,8 @@ de: success: Sie haben Ihr Passwort erfolgreich geändert. show: link: Profil + search: + search_in_forum: "Probleme? Suche hier im Forum" locales: de: Deutsch en: Englisch diff --git a/config/locales/en.yml b/config/locales/en.yml index b0c54a09..ce9e2170 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -367,6 +367,8 @@ en: success: You successfully changed your password. show: link: Profile + search: + search_in_forum: "Problems? Search here in forum" locales: de: German en: English From 530916d3ef7ac6021a1f349898a54fbf80e2f425 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 8 Mar 2017 11:35:19 +0100 Subject: [PATCH 061/143] added time of user to reach max score in exercise --- app/models/exercise.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 1a399427..15c740f4 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -57,6 +57,10 @@ class Exercise < ActiveRecord::Base return user_count == 0 ? 0 : submissions.count() / user_count.to_f() end + def time_maximum_score(user) + submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC, created_at ASC").first.created_at rescue Time.zone.at(0) + end + def user_working_time_query """ SELECT user_id, From 325c44c1fb96c0615f808dd20a9267f385a6275b Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 8 Mar 2017 11:35:43 +0100 Subject: [PATCH 062/143] added finishing return value in proxy exercise --- app/models/proxy_exercise.rb | 48 ++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 46a04928..f35c5c39 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -175,50 +175,44 @@ class ProxyExercise < ActiveRecord::Base end private :score - def get_relative_knowledge_loss(user, exercises) - # initialize knowledge for each tag with 0 - all_used_tags = exercises.inject(Set.new){|tagset, ex| tagset.merge(ex.tags)} - topic_knowledge_loss_user = all_used_tags.map{|t| [t, 0]}.to_h - topic_knowledge_max = all_used_tags.map{|t| [t, 0]}.to_h - exercises.each do |ex| - user_score_factor = score(user, ex) - ex.tags.each do |t| - tag_ratio = ex.exercise_tags.where(tag: t).first.factor / ex.exercise_tags.inject(0){|sum, et| sum += et.factor } - max_topic_knowledge_ratio = ex.expected_difficulty * tag_ratio - topic_knowledge_loss_user[t] += (1 - user_score_factor) * max_topic_knowledge_ratio - topic_knowledge_max[t] += max_topic_knowledge_ratio - end - end - relative_loss = {} - all_used_tags.each do |t| - relative_loss[t] = topic_knowledge_loss_user[t] / topic_knowledge_max[t] - end - relative_loss - end - private :get_relative_knowledge_loss - def get_user_knowledge_and_max_knowledge(user, exercises) # initialize knowledge for each tag with 0 - all_used_tags = exercises.inject(Set.new){|tagset, ex| tagset.merge(ex.tags)} - topic_knowledge_loss_user = all_used_tags.map{|t| [t, 0]}.to_h - topic_knowledge_max = all_used_tags.map{|t| [t, 0]}.to_h + all_used_tags_with_count = {} exercises.each do |ex| + ex.tags.each do |t| + all_used_tags_with_count[t] ||= 0 + all_used_tags_with_count[t] += 1 + end + end + tags_counter = all_used_tags_with_count.keys.map{|tag| [tag,0]}.to_h + topic_knowledge_loss_user = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h + topic_knowledge_max = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h + exercises_sorted = exercises.sort_by { |ex| ex.time_maximum_score(user)} + exercises_sorted.each do |ex| Rails.logger.info("exercise: #{ex.id}: #{ex}") user_score_factor = score(user, ex) ex.tags.each do |t| + tags_counter[t] += 1 + tag_diminishing_return_factor = tag_diminishing_return_function(tags_counter[t], all_used_tags_with_count[t]) tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f Rails.logger.info("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum += et.factor }}") + Rails.logger.info("tag #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}") Rails.logger.info("tag_ratio #{tag_ratio}") topic_knowledge_ratio = ex.expected_difficulty * tag_ratio Rails.logger.info("topic_knowledge_ratio #{topic_knowledge_ratio}") - topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio - topic_knowledge_max[t] += topic_knowledge_ratio + topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio * tag_diminishing_return_factor + topic_knowledge_max[t] += topic_knowledge_ratio * tag_diminishing_return_factor end end {user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max} end private :get_user_knowledge_and_max_knowledge + def tag_diminishing_return_function(count_tag, total_count_tag) + total_count_tag += 1 # bonus exercise comes on top + return 0.8/(1+(Math::E**(-10/(0.5*total_count_tag)*(count_tag-0.5*total_count_tag))))+0.2 + end + def select_easiest_exercise(exercises) exercises.order(:expected_difficulty).first end From bd0721da2e2bae656ddc859feb68a4da73fd766f Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 8 Mar 2017 14:34:24 +0100 Subject: [PATCH 063/143] deleted search modal. reused roc modal for search modal. also added translations --- app/views/exercises/_editor.html.slim | 3 +-- .../_request_comment_dialogcontent.html.slim | 3 +++ .../_break_intervention_modal.html.slim | 12 +----------- .../_search_intervention_modal.html.slim | 10 ---------- config/locales/de.yml | 7 ++++++- config/locales/en.yml | 5 +++++ 6 files changed, 16 insertions(+), 24 deletions(-) delete mode 100644 app/views/interventions/_search_intervention_modal.html.slim diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 31893ba9..8a43b613 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -23,5 +23,4 @@ = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') -= render('shared/modal', id: 'break-intervention-modal', title: 'Hinweis', template: 'interventions/_break_intervention_modal') -= render('shared/modal', id: 'search-intervention-modal', title: 'Hast du Probleme beim Lösen der Aufgabe?', template: 'interventions/_search_intervention_modal') \ No newline at end of file += render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal') \ No newline at end of file diff --git a/app/views/exercises/_request_comment_dialogcontent.html.slim b/app/views/exercises/_request_comment_dialogcontent.html.slim index 677ccb12..0db575e5 100644 --- a/app/views/exercises/_request_comment_dialogcontent.html.slim +++ b/app/views/exercises/_request_comment_dialogcontent.html.slim @@ -1,4 +1,7 @@ +h5#rfc_intervention_text style='display: none;' = t('exercises.implement.rfc_intervention.text') h5 = t('exercises.implement.comment.question') + + textarea.form-control#question(style='resize:none;') p = '' / data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action. diff --git a/app/views/interventions/_break_intervention_modal.html.slim b/app/views/interventions/_break_intervention_modal.html.slim index e9c222f3..12f0e314 100644 --- a/app/views/interventions/_break_intervention_modal.html.slim +++ b/app/views/interventions/_break_intervention_modal.html.slim @@ -1,11 +1 @@ -/h5 = t('exercises.implement.comment.question') - -/textarea.form-control#question(style='resize:none;') -p = "Uns ist aufgefallen, dass du schon lange an dieser Aufgabe sitzt. Möchtest du vielleicht mal eine Pause machen um auf neue Gedanken zu kommen?" - -/p = "AVG: #{@working_time_avg}" -/p = "ACCUMULATED: #{@working_time_accumulated}" - -/ data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action. -/ But if we use this button, it will work since the correct cause is supplied -/button#askForCommentsButton.btn.btn-block.btn-primary(type='button' data-cause='requestComments' data-message-success=t('exercises.editor.request_for_comments_sent')) =t('exercises.implement.comment.request') \ No newline at end of file +h5 = t('exercises.implement.break_intervention.text') diff --git a/app/views/interventions/_search_intervention_modal.html.slim b/app/views/interventions/_search_intervention_modal.html.slim deleted file mode 100644 index 80c6fca8..00000000 --- a/app/views/interventions/_search_intervention_modal.html.slim +++ /dev/null @@ -1,10 +0,0 @@ -p = "Benutz doch einfach die Forensuche! Du kannst auch jederzeit in der linken Spalte die Suche nutzen. Alternativ kannst du auch nach Kommentaren zu deinem Programm anfordern." -= form_for(@search, multipart: true, target: "_blank") do |f| - .input-group.enforce-top-margin - = f.hidden_field :exercise_id - .enforce-right-margin - = f.text_field(:search, class: 'form-control', id: 'search-modal', required: true, placeholder: t('search.search_in_forum')) - .input-group-btn - = button_tag(class: 'btn btn-block btn-primary', id: 'btn-search-modal', model: @search.class.model_name.human) do - i.fa.fa-search - diff --git a/config/locales/de.yml b/config/locales/de.yml index 5f8a7bb8..3b5dd89a 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -275,7 +275,12 @@ de: removeAllOnLine: Meine Kommentare auf dieser Zeile löschen listing: Die neuesten Kommentaranfragen request: "Kommentaranfrage stellen" - question: "Bitte beschreiben Sie kurz ihre Problem oder nennen Sie den Programmteil, zu dem sie Feedback wünschen." + question: "Bitte beschreiben Sie kurz ihre Probleme oder nennen Sie den Programmteil, zu dem Sie Feedback wünschen." + rfc_intervention: + text: "Es scheint so als würden sie Probleme mit der Aufgabe haben. Wenn Sie möchten, können wir Ihnen helfen!" + break_intervention: + title: "Pause" + text: "Uns ist aufgefallen, dass du schon lange an dieser Aufgabe arbeitest. Möchtest du vielleicht eine Pause machen um auf neue Gedanken zu kommen?" index: clone: Duplizieren implement: Implementieren diff --git a/config/locales/en.yml b/config/locales/en.yml index ce9e2170..213624e8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -297,6 +297,11 @@ en: listing: Listing the newest comment requests request: "Request Comments" question: "Please shortly describe your problem or the program part you would like to get feedback for." + rfc_intervention: + text: "It looks like you may struggle with this exercise. If you like we can help you out!" + break_intervention: + title: "Break" + text: "We recognized that you are already working quite a while on this exercise. We would like to encourage you to take a break and come back later." index: clone: Duplicate implement: Implement From f1bf313280a1b4c7a30d40546de7a1de72ca9bfe Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 8 Mar 2017 14:36:25 +0100 Subject: [PATCH 064/143] changed times for rfc and break intervention to minimum 15 and 20 minutes. roc modal shows some additional text to the modal for less confusion --- app/assets/javascripts/editor/editor.js.erb | 36 ++++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index f1a6d470..f5b43f1d 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -323,6 +323,9 @@ configureEditors: function () { var button = $('#requestComments'); button.prop('disabled', true); button.on('click', function () { + if ($('#editor').data('show-interventions') == true){ + $('#rfc_intervention_text').hide() + } $('#comment-modal').modal('show'); }); @@ -589,15 +592,16 @@ configureEditors: function () { var percentile75 = data['working_time_75_percentile']; var accumulatedWorkTimeUser = data['working_time_accumulated']; - var timeUntilBreak = 15 * 60 * 1000; + var timeUntilBreak = 20 * 60 * 1000; + var minTimeUntilAskQuestion = 15 * 60 * 1000; if ((accumulatedWorkTimeUser - percentile75) > 0) { // working time is already over 75 percentile - var timeUntilAskQuestion = 10 * 60 * 1000; + var timeUntilAskQuestion = minTimeUntilAskQuestion; } else { // working time is less than 75 percentile // ensure we give user at least 10 minutes before we bother the user - var timeUntilAskQuestion = (percentile75 - accumulatedWorkTimeUser) > 10 * 60 * 1000 ? (percentile75 - accumulatedWorkTimeUser) : 10 * 60 * 1000; + var timeUntilAskQuestion = (percentile75 - accumulatedWorkTimeUser) > minTimeUntilAskQuestion ? (percentile75 - accumulatedWorkTimeUser) : minTimeUntilAskQuestion; } // if notifications are too close to each other, ensure some time differences between them @@ -616,15 +620,20 @@ configureEditors: function () { url: $('#editor').data('intervention-save-url')}); }, timeUntilBreak); + setTimeout(function() { - $('#search-intervention-modal').modal('show'); - $.ajax({ - data: { - intervention_type: 'QuestionIntervention' - }, - dataType: 'json', - type: 'POST', - url: $('#editor').data('intervention-save-url')}); + var button = $('#requestComments'); + if (!button.prop('disabled')){ + $('#rfc_intervention_text').show(); + $('#comment-modal').modal('show'); + $.ajax({ + data: { + intervention_type: 'QuestionIntervention' + }, + dataType: 'json', + type: 'POST', + url: $('#editor').data('intervention-save-url')}); + }; }, timeUntilAskQuestion); } }); @@ -636,11 +645,6 @@ configureEditors: function () { window.open(`https://open.hpi.de/courses/javaeinstieg2017/pinboard?query=${search}`, '_blank'); }) - $('#btn-search-modal').button().click(function(){ - var search = $('#search-modal').val(); - window.open(`https://open.hpi.de/courses/javaeinstieg2017/pinboard?query=${search}`, '_blank'); - }) - $('#sidebar-search-collapsed').on('click',this.handleSideBarToggle.bind(this)); }, From 1eda266159aca9a426ab78c9217fa7bbf154e05e Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 8 Mar 2017 15:15:52 +0100 Subject: [PATCH 065/143] fixed calculation of time until intervention pops up --- app/assets/javascripts/editor/editor.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index f5b43f1d..e4318dc2 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -605,7 +605,7 @@ configureEditors: function () { } // if notifications are too close to each other, ensure some time differences between them - if (Math.abs(timeUntilAskQuestion - timeUntilBreak) < 5){ + if (Math.abs(timeUntilAskQuestion - timeUntilBreak) < 5 * 1000 * 60){ timeUntilBreak = timeUntilBreak * 2; } From 4f5c936dd6dd11abba7ec83aa097f3030ce744a9 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 8 Mar 2017 17:00:12 +0100 Subject: [PATCH 066/143] changed formulas for demising return --- app/models/proxy_exercise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index f35c5c39..8fed0fa2 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -210,7 +210,7 @@ class ProxyExercise < ActiveRecord::Base def tag_diminishing_return_function(count_tag, total_count_tag) total_count_tag += 1 # bonus exercise comes on top - return 0.8/(1+(Math::E**(-10/(0.5*total_count_tag)*(count_tag-0.5*total_count_tag))))+0.2 + return 1/(1+(Math::E**(-3/(0.5*total_count_tag)*(count_tag-0.5*total_count_tag)))) end def select_easiest_exercise(exercises) From 12adfde6c28c3d0523ed0f0c07400e977fd44981 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 9 Mar 2017 13:36:15 +0100 Subject: [PATCH 067/143] search bar now searches in forum of the course from where the LTI request came from. alternatively searches in the java 2017 course. show interventions only in the current java course --- app/assets/javascripts/editor/editor.js.erb | 9 ++++--- app/controllers/concerns/lti.rb | 1 + app/controllers/exercises_controller.rb | 27 ++++++++++++++++++- .../exercises/_editor_file_tree.html.slim | 22 ++++++++------- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index e4318dc2..6a9bc960 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -601,11 +601,11 @@ configureEditors: function () { } else { // working time is less than 75 percentile // ensure we give user at least 10 minutes before we bother the user - var timeUntilAskQuestion = (percentile75 - accumulatedWorkTimeUser) > minTimeUntilAskQuestion ? (percentile75 - accumulatedWorkTimeUser) : minTimeUntilAskQuestion; + var timeUntilAskForRFC = (percentile75 - accumulatedWorkTimeUser) > minTimeUntilAskQuestion ? (percentile75 - accumulatedWorkTimeUser) : minTimeUntilAskQuestion; } // if notifications are too close to each other, ensure some time differences between them - if (Math.abs(timeUntilAskQuestion - timeUntilBreak) < 5 * 1000 * 60){ + if (Math.abs(timeUntilAskForRFC - timeUntilBreak) < 5 * 1000 * 60){ timeUntilBreak = timeUntilBreak * 2; } @@ -634,7 +634,7 @@ configureEditors: function () { type: 'POST', url: $('#editor').data('intervention-save-url')}); }; - }, timeUntilAskQuestion); + }, timeUntilAskForRFC); } }); }, @@ -642,7 +642,8 @@ configureEditors: function () { initializeSearchButton: function(){ $('#btn-search-col').button().click(function(){ var search = $('#search-col').val(); - window.open(`https://open.hpi.de/courses/javaeinstieg2017/pinboard?query=${search}`, '_blank'); + var course_token = $('#sidebar-collapsed').data('course_token') + window.open(`https://open.hpi.de/courses/${course_token}/pinboard?query=${search}`, '_blank'); }) $('#sidebar-search-collapsed').on('click',this.handleSideBarToggle.bind(this)); diff --git a/app/controllers/concerns/lti.rb b/app/controllers/concerns/lti.rb index ce2105bd..7483327d 100644 --- a/app/controllers/concerns/lti.rb +++ b/app/controllers/concerns/lti.rb @@ -140,6 +140,7 @@ module Lti lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).to_json lti_parameters.save! + @lti_parameters = lti_parameters session[:consumer_id] = options[:consumer].id session[:external_user_id] = @current_user.id diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 3742de4d..87059bf8 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -9,6 +9,7 @@ class ExercisesController < ApplicationController before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :run, :statistics, :submit, :reload] before_action :set_external_user, only: [:statistics] before_action :set_file_types, only: [:create, :edit, :new, :update] + before_action :set_course_token, only: [:implement] skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml] skip_after_action :verify_authorized, only: [:import_proforma_xml] @@ -155,12 +156,16 @@ class ExercisesController < ApplicationController def implement redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists? + user_got_enough_interventions = UserExerciseIntervention.where(exercise: @exercise, user: current_user).count >= 3 + is_java_course = @course_token && @course_token.eql?(java_course_token) + @show_interventions = - if UserExerciseIntervention.where(exercise: @exercise, user: current_user).count >= 3 + if !is_java_course || user_got_enough_interventions "false" else "true" end + @search = Search.new @search.exercise = @exercise @submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first @@ -174,6 +179,22 @@ class ExercisesController < ApplicationController end end + def set_course_token + if @lti_parameters + lti_json = @lti_parameters.lti_parameters + @course_token = + if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/) + match.captures.first + else + java_course_token + end + else + # no consumer, therefore implementation with internal user + @course_token = java_course_token + end + end + private :set_course_token + def working_times working_time_accumulated = @exercise.accumulated_working_time_for_only(current_user) working_time_75_percentile = @exercise.get_quantiles([0.75]).first @@ -358,4 +379,8 @@ class ExercisesController < ApplicationController redirect_to_lti_return_path end + def java_course_token + "702cbd2a-c84c-4b37-923a-692d7d1532d0" + end + end diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index 9021e54a..6a90b60d 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -1,4 +1,4 @@ -div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') +div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') data-course_token=@course_token = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'', title:t('exercises.editor.expand_action_sidebar')) - if @exercise.allow_file_creation and not @exercise.hide_file_tree? @@ -6,7 +6,8 @@ div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download')) = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over')) - = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum')) + - if @course_token + = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum')) div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) @@ -25,14 +26,15 @@ div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over')) - = form_for(@search, multipart: true, target: "_blank") do |f| - .input-group.enforce-top-margin - = f.hidden_field :exercise_id - .enforce-right-margin - = f.text_field(:search, class: 'form-control', id: "search-col", required: true, placeholder: t('search.search_in_forum')) - .input-group-btn - = button_tag(class: 'btn btn-primary', id: 'btn-search-col', model: @search.class.model_name.human) do - i.fa.fa-search + - if @course_token + = form_for(@search, multipart: true, target: "_blank") do |f| + .input-group.enforce-top-margin + = f.hidden_field :exercise_id + .enforce-right-margin + = f.text_field(:search, class: 'form-control', id: "search-col", required: true, placeholder: t('search.search_in_forum')) + .input-group-btn + = button_tag(class: 'btn btn-primary', id: 'btn-search-col', model: @search.class.model_name.human) do + i.fa.fa-search - if @exercise.allow_file_creation? From 8b67a705466b8b0bb3fa5260efdb4529798d5c2b Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Fri, 10 Mar 2017 18:49:36 +0100 Subject: [PATCH 068/143] commenting, improved readability --- app/assets/javascripts/editor/editor.js.erb | 1 + app/controllers/exercises_controller.rb | 30 ++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 6a9bc960..cff1a889 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -587,6 +587,7 @@ configureEditors: function () { }, dataType: 'json', method: 'GET', + // get working times for this exercise url: $('#editor').data('working-times-url'), success: function (data) { var percentile75 = data['working_time_75_percentile']; diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 87059bf8..62d1be7c 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -20,6 +20,15 @@ class ExercisesController < ApplicationController end private :authorize! + def max_intervention_count + 3 + end + + + def java_course_token + "702cbd2a-c84c-4b37-923a-692d7d1532d0" + end + def batch_update @exercises = Exercise.all authorize! @@ -156,15 +165,10 @@ class ExercisesController < ApplicationController def implement redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists? - user_got_enough_interventions = UserExerciseIntervention.where(exercise: @exercise, user: current_user).count >= 3 + user_got_enough_interventions = UserExerciseIntervention.where(exercise: @exercise, user: current_user).count >= max_intervention_count is_java_course = @course_token && @course_token.eql?(java_course_token) - @show_interventions = - if !is_java_course || user_got_enough_interventions - "false" - else - "true" - end + @show_interventions = (!is_java_course || user_got_enough_interventions) ? "false" : "true" @search = Search.new @search.exercise = @exercise @@ -271,10 +275,10 @@ class ExercisesController < ApplicationController def collect_set_and_unset_exercise_tags @search = policy_scope(Tag).search(params[:q]) @tags = @search.result.order(:name) - exercise_tags = @exercise.exercise_tags - tags_set = exercise_tags.collect{|e| e.tag}.to_set - tags_not_set = Tag.all.to_set.subtract tags_set - @exercise_tags = exercise_tags + tags_not_set.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag)} + checked_exercise_tags = @exercise.exercise_tags + checked_tags = checked_exercise_tags.collect{|e| e.tag}.to_set + unchecked_tags = Tag.all.to_set.subtract checked_tags + @exercise_tags = checked_exercise_tags + unchecked_tags.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag)} end private :collect_set_and_unset_exercise_tags @@ -379,8 +383,4 @@ class ExercisesController < ApplicationController redirect_to_lti_return_path end - def java_course_token - "702cbd2a-c84c-4b37-923a-692d7d1532d0" - end - end From 2284c2c28b10fb8cbfe2f5a12baa3c30fe6f6a98 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 20 Mar 2017 18:00:31 +0100 Subject: [PATCH 069/143] fixed lti course token parsing --- app/controllers/exercises_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 62d1be7c..5afb4f77 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -185,7 +185,7 @@ class ExercisesController < ApplicationController def set_course_token if @lti_parameters - lti_json = @lti_parameters.lti_parameters + lti_json = @lti_parameters.lti_parameters[:lis_outcome_service_url] @course_token = if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/) match.captures.first From 3dfecc3ca89db94d929e2467dbdf8b1e3e486150 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 20 Mar 2017 18:24:10 +0100 Subject: [PATCH 070/143] fix course token parsing --- app/controllers/exercises_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 5afb4f77..ccd7180a 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -184,8 +184,10 @@ class ExercisesController < ApplicationController end def set_course_token - if @lti_parameters - lti_json = @lti_parameters.lti_parameters[:lis_outcome_service_url] + lti_parameters = LtiParameter.find_by(external_users_id: current_user.id, + exercises_id: @exercise.id) + if lti_parameters + lti_json = lti_parameters.lti_parameters[:lis_outcome_service_url] @course_token = if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/) match.captures.first From 76583d8082ddf2519564dd6548bb21777f0a2780 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 20 Mar 2017 18:34:19 +0100 Subject: [PATCH 071/143] final fix matching --- app/controllers/exercises_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index ccd7180a..62799e27 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -187,7 +187,7 @@ class ExercisesController < ApplicationController lti_parameters = LtiParameter.find_by(external_users_id: current_user.id, exercises_id: @exercise.id) if lti_parameters - lti_json = lti_parameters.lti_parameters[:lis_outcome_service_url] + lti_json = lti_parameters.lti_parameters["lis_outcome_service_url"] @course_token = if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/) match.captures.first From 26e93c4b23facd65b8064e22f9055c71ec2b54a0 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 21 Mar 2017 11:25:11 +0100 Subject: [PATCH 072/143] changed puts methods in proxy_exercise.rb to Rails.logger.debug. also changed Rails.logger.info to Rails.logger.debug --- app/models/proxy_exercise.rb | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 8fed0fa2..11b5a545 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -33,10 +33,10 @@ class ProxyExercise < ActiveRecord::Base assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first recommended_exercise = if (assigned_user_proxy_exercise) - Rails.logger.info("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" ) + Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" ) assigned_user_proxy_exercise.exercise else - Rails.logger.info("find new matching exercise for user #{user.id}" ) + Rails.logger.debug("find new matching exercise for user #{user.id}" ) matching_exercise = begin find_matching_exercise(user) @@ -55,7 +55,7 @@ class ProxyExercise < ActiveRecord::Base def find_matching_exercise(user) exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten - Rails.logger.info("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}") + Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}") # find execises potential_recommended_exercises = [] @@ -65,10 +65,10 @@ class ProxyExercise < ActiveRecord::Base potential_recommended_exercises << ex end end - Rails.logger.info("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}") + Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}") # if all exercises contain tags which the user has never seen, recommend easiest exercise if potential_recommended_exercises.empty? - Rails.logger.info("matched easiest exercise in pool") + Rails.logger.debug("matched easiest exercise in pool") @reason[:reason] = "easiest exercise in pool. empty potential exercises" select_easiest_exercise(exercises) else @@ -80,8 +80,8 @@ class ProxyExercise < ActiveRecord::Base def select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises) topic_knowledge_user_and_max = get_user_knowledge_and_max_knowledge(user, exercises_user_has_accessed) - puts "topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}" - puts "potential_recommended_exercises: #{potential_recommended_exercises.size}: #{potential_recommended_exercises.map{|p| p.id}}" + Rails.logger.debug("topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}") + Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.size}: #{potential_recommended_exercises.map{|p| p.id}}") topic_knowledge_user = topic_knowledge_user_and_max[:user_topic_knowledge] topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge] current_users_knowledge_lack = {} @@ -93,13 +93,13 @@ class ProxyExercise < ActiveRecord::Base potential_recommended_exercises.each do |potex| tags = potex.tags relative_knowledge_improvement[potex] = 0.0 - Rails.logger.info("review potential exercise #{potex.id}") + Rails.logger.debug("review potential exercise #{potex.id}") tags.each do |tag| tag_ratio = potex.exercise_tags.where(tag: tag).first.factor.to_f / potex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f max_topic_knowledge_ratio = potex.expected_difficulty * tag_ratio old_relative_loss_tag = topic_knowledge_user[tag] / topic_knowledge_max[tag] new_relative_loss_tag = topic_knowledge_user[tag] / (topic_knowledge_max[tag] + max_topic_knowledge_ratio) - puts "tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, tag_ratio #{tag_ratio}" + Rails.logger.debug("tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, tag_ratio #{tag_ratio}") relative_knowledge_improvement[potex] += old_relative_loss_tag - new_relative_loss_tag end end @@ -110,27 +110,27 @@ class ProxyExercise < ActiveRecord::Base @reason[:current_users_knowledge_lack] = current_users_knowledge_lack @reason[:relative_knowledge_improvement] = relative_knowledge_improvement - Rails.logger.info("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}.to_s) - Rails.logger.info("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}") + Rails.logger.debug("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}.to_s) + Rails.logger.debug("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}") best_matching_exercise end private :select_best_matching_exercise def find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed) - Rails.logger.info("select most appropiate exercise for user. his highest difficulty was #{highest_difficulty_user_has_accessed}") + Rails.logger.debug("select most appropiate exercise for user. his highest difficulty was #{highest_difficulty_user_has_accessed}") sorted_exercises = relative_knowledge_improvement.sort_by{|k,v| v}.reverse sorted_exercises.each do |ex,diff| - Rails.logger.info("review exercise #{ex.id} diff: #{ex.expected_difficulty}") + Rails.logger.debug("review exercise #{ex.id} diff: #{ex.expected_difficulty}") if (ex.expected_difficulty - highest_difficulty_user_has_accessed) <= 1 - Rails.logger.info("matched exercise #{ex.id}") + Rails.logger.debug("matched exercise #{ex.id}") return ex else - Rails.logger.info("exercise #{ex.id} is too difficult") + Rails.logger.debug("exercise #{ex.id} is too difficult") end end easiest_exercise = sorted_exercises.min_by{|k,v| v}.first - Rails.logger.info("no match, select easiest exercise as fallback #{easiest_exercise.id}") + Rails.logger.debug("no match, select easiest exercise as fallback #{easiest_exercise.id}") easiest_exercise end private :find_best_exercise @@ -189,17 +189,17 @@ class ProxyExercise < ActiveRecord::Base topic_knowledge_max = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h exercises_sorted = exercises.sort_by { |ex| ex.time_maximum_score(user)} exercises_sorted.each do |ex| - Rails.logger.info("exercise: #{ex.id}: #{ex}") + Rails.logger.debug("exercise: #{ex.id}: #{ex}") user_score_factor = score(user, ex) ex.tags.each do |t| tags_counter[t] += 1 tag_diminishing_return_factor = tag_diminishing_return_function(tags_counter[t], all_used_tags_with_count[t]) tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f - Rails.logger.info("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum += et.factor }}") - Rails.logger.info("tag #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}") - Rails.logger.info("tag_ratio #{tag_ratio}") + Rails.logger.debug("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum += et.factor }}") + Rails.logger.debug("tag #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}") + Rails.logger.debug("tag_ratio #{tag_ratio}") topic_knowledge_ratio = ex.expected_difficulty * tag_ratio - Rails.logger.info("topic_knowledge_ratio #{topic_knowledge_ratio}") + Rails.logger.debug("topic_knowledge_ratio #{topic_knowledge_ratio}") topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio * tag_diminishing_return_factor topic_knowledge_max[t] += topic_knowledge_ratio * tag_diminishing_return_factor end From 5729a3ba5e6a42ec0e92fffc8aab40cbfe9f62a8 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 21 Mar 2017 11:34:57 +0100 Subject: [PATCH 073/143] removed this.resizeAceEditors() since it was only a test --- app/assets/javascripts/editor/editor.js.erb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index cff1a889..0b446c7c 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -548,14 +548,12 @@ configureEditors: function () { $('#output_sidebar_collapsed').addClass('hidden'); $('#output_sidebar_uncollapsed').removeClass('hidden'); $('#output_sidebar').removeClass('output-col-collapsed').addClass('output-col'); - this.resizeAceEditors(); }, hideOutputBar: function() { $('#output_sidebar_collapsed').removeClass('hidden'); $('#output_sidebar_uncollapsed').addClass('hidden'); $('#output_sidebar').removeClass('output-col').addClass('output-col-collapsed'); - this.resizeAceEditors(); }, initializeSideBarTooltips: function() { From d1d948c71eae789f9fab2128f651b2daec0526b3 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 21 Mar 2017 11:48:05 +0100 Subject: [PATCH 074/143] replaced ticks with string concatenation. Ticks are not possible, since execjs can't handle them. --- app/assets/javascripts/editor/editor.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 0b446c7c..7221c38e 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -642,7 +642,7 @@ configureEditors: function () { $('#btn-search-col').button().click(function(){ var search = $('#search-col').val(); var course_token = $('#sidebar-collapsed').data('course_token') - window.open(`https://open.hpi.de/courses/${course_token}/pinboard?query=${search}`, '_blank'); + window.open("https://open.hpi.de/courses/" + course_token + "/pinboard?query=" + search, '_blank'); }) $('#sidebar-search-collapsed').on('click',this.handleSideBarToggle.bind(this)); From 10bcfc998c0cd3911fce56020a9037c453f7bdc1 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 21 Mar 2017 12:15:50 +0100 Subject: [PATCH 075/143] update schema.rb --- db/schema.rb | 107 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 7 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 6f5accb0..d15a09c9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161214144837) do +ActiveRecord::Schema.define(version: 20170228165741) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -76,22 +76,54 @@ ActiveRecord::Schema.define(version: 20161214144837) do t.boolean "network_enabled" end + create_table "exercise_collections", force: :cascade do |t| + t.string "name" + t.datetime "created_at" + t.datetime "updated_at" + end + + create_table "exercise_collections_exercises", id: false, force: :cascade do |t| + t.integer "exercise_collection_id" + t.integer "exercise_id" + end + + add_index "exercise_collections_exercises", ["exercise_collection_id"], name: "index_exercise_collections_exercises_on_exercise_collection_id", using: :btree + add_index "exercise_collections_exercises", ["exercise_id"], name: "index_exercise_collections_exercises_on_exercise_id", using: :btree + + create_table "exercise_tags", force: :cascade do |t| + t.integer "exercise_id" + t.integer "tag_id" + t.integer "factor", default: 0 + end + create_table "exercises", force: :cascade do |t| t.text "description" t.integer "execution_environment_id" - t.string "title", limit: 255 + t.string "title", limit: 255 t.datetime "created_at" t.datetime "updated_at" t.integer "user_id" t.text "instructions" t.boolean "public" - t.string "user_type", limit: 255 - t.string "token", limit: 255 + t.string "user_type", limit: 255 + t.string "token", limit: 255 t.boolean "hide_file_tree" t.boolean "allow_file_creation" - t.boolean "allow_auto_completion", default: false + t.boolean "allow_auto_completion", default: false + t.integer "expected_worktime_seconds", default: 0 + t.integer "expected_difficulty", default: 1 end + create_table "exercises_proxy_exercises", id: false, force: :cascade do |t| + t.integer "proxy_exercise_id" + t.integer "exercise_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "exercises_proxy_exercises", ["exercise_id"], name: "index_exercises_proxy_exercises_on_exercise_id", using: :btree + add_index "exercises_proxy_exercises", ["proxy_exercise_id"], name: "index_exercises_proxy_exercises_on_proxy_exercise_id", using: :btree + create_table "external_users", force: :cascade do |t| t.integer "consumer_id" t.string "email", limit: 255 @@ -182,11 +214,26 @@ ActiveRecord::Schema.define(version: 20161214144837) do add_index "internal_users", ["remember_me_token"], name: "index_internal_users_on_remember_me_token", using: :btree add_index "internal_users", ["reset_password_token"], name: "index_internal_users_on_reset_password_token", using: :btree + create_table "interventions", force: :cascade do |t| + t.string "name" + t.text "markup" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "lti_parameters", force: :cascade do |t| - t.string "external_user_id" + t.integer "external_users_id" t.integer "consumers_id" t.integer "exercises_id" - t.jsonb "lti_parameters", default: {}, null: false + t.jsonb "lti_parameters", default: {}, null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + create_table "proxy_exercises", force: :cascade do |t| + t.string "title" + t.string "description" + t.string "token" t.datetime "created_at" t.datetime "updated_at" end @@ -203,6 +250,15 @@ ActiveRecord::Schema.define(version: 20161214144837) do t.integer "submission_id" end + create_table "searches", force: :cascade do |t| + t.integer "exercise_id", null: false + t.integer "user_id", null: false + t.string "user_type", null: false + t.string "search" + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "submissions", force: :cascade do |t| t.integer "exercise_id" t.float "score" @@ -213,6 +269,12 @@ ActiveRecord::Schema.define(version: 20161214144837) do t.string "user_type", limit: 255 end + create_table "tags", force: :cascade do |t| + t.string "name", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + create_table "testruns", force: :cascade do |t| t.boolean "passed" t.text "output" @@ -222,4 +284,35 @@ ActiveRecord::Schema.define(version: 20161214144837) do t.datetime "updated_at" end + create_table "user_exercise_feedbacks", force: :cascade do |t| + t.integer "exercise_id", null: false + t.integer "user_id", null: false + t.string "user_type", null: false + t.integer "difficulty" + t.integer "working_time_seconds" + t.string "feedback_text" + end + + create_table "user_exercise_interventions", force: :cascade do |t| + t.integer "user_id" + t.string "user_type" + t.integer "exercise_id" + t.integer "intervention_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + create_table "user_proxy_exercise_exercises", force: :cascade do |t| + t.integer "user_id" + t.string "user_type" + t.integer "proxy_exercise_id" + t.integer "exercise_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "user_proxy_exercise_exercises", ["exercise_id"], name: "index_user_proxy_exercise_exercises_on_exercise_id", using: :btree + add_index "user_proxy_exercise_exercises", ["proxy_exercise_id"], name: "index_user_proxy_exercise_exercises_on_proxy_exercise_id", using: :btree + add_index "user_proxy_exercise_exercises", ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user_type_and_user_id", using: :btree + end From 4a9867b81b414b5a5bdb66a597479c854d827645 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 21 Mar 2017 12:16:39 +0100 Subject: [PATCH 076/143] only show search if parameters are set, prevent calls on nil.. --- app/controllers/exercises_controller.rb | 13 +++++++++---- app/views/exercises/_editor_file_tree.html.slim | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 62799e27..5c536a0e 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -187,12 +187,17 @@ class ExercisesController < ApplicationController lti_parameters = LtiParameter.find_by(external_users_id: current_user.id, exercises_id: @exercise.id) if lti_parameters - lti_json = lti_parameters.lti_parameters["lis_outcome_service_url"] + lti_json = lti_parameters.lti_parameters["launch_presentation_return_url"] + @course_token = - if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/) - match.captures.first + unless lti_json.nil? + if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/) + match.captures.first + else + java_course_token + end else - java_course_token + "" end else # no consumer, therefore implementation with internal user diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index 6a90b60d..2c287eba 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -6,7 +6,7 @@ div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') data = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download')) = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over')) - - if @course_token + - if !@course_token.blank? = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum')) div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') @@ -26,7 +26,7 @@ div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download')) = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over')) - - if @course_token + - if !@course_token.blank? = form_for(@search, multipart: true, target: "_blank") do |f| .input-group.enforce-top-margin = f.hidden_field :exercise_id From b05bb27ed967b89513a91522e22afe2e3f4a2496 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 21 Mar 2017 14:37:32 +0100 Subject: [PATCH 077/143] search is now saved asynchron and without a form which caused some redirection through searches_controller added asynchronous save of search, removed searches_controller, --- app/assets/javascripts/editor/editor.js.erb | 13 +++++-- app/controllers/exercises_controller.rb | 12 ++++++- app/controllers/searches_controller.rb | 34 ------------------- app/policies/exercise_policy.rb | 2 +- app/views/exercises/_editor.html.slim | 2 +- .../exercises/_editor_file_tree.html.slim | 9 ++--- config/routes.rb | 1 + 7 files changed, 28 insertions(+), 45 deletions(-) delete mode 100644 app/controllers/searches_controller.rb diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 7221c38e..ddbe2e54 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -640,9 +640,18 @@ configureEditors: function () { initializeSearchButton: function(){ $('#btn-search-col').button().click(function(){ - var search = $('#search-col').val(); - var course_token = $('#sidebar-collapsed').data('course_token') + var search = $('#search-input-text').val(); + var course_token = $('#editor').data('course_token') + var save_search_url = $('#editor').data('search-save-url') window.open("https://open.hpi.de/courses/" + course_token + "/pinboard?query=" + search, '_blank'); + // save search + $.ajax({ + data: { + search_text: search + }, + dataType: 'json', + type: 'POST', + url: save_search_url}); }) $('#sidebar-search-collapsed').on('click',this.handleSideBarToggle.bind(this)); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 5c536a0e..25361125 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -6,7 +6,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, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :run, :statistics, :submit, :reload] + before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload] before_action :set_external_user, only: [:statistics] before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_course_token, only: [:implement] @@ -223,7 +223,17 @@ class ExercisesController < ApplicationController else render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"}) end + end + def search + search_text = params[:search_text] + search = Search.new(user: current_user, exercise: @exercise, search: search_text) + + begin search.save + render(json: {success: 'true'}) + rescue + render(json: {success: 'false', error: "could not save search: #{$!}"}) + end end def index diff --git a/app/controllers/searches_controller.rb b/app/controllers/searches_controller.rb deleted file mode 100644 index 8af6b364..00000000 --- a/app/controllers/searches_controller.rb +++ /dev/null @@ -1,34 +0,0 @@ -class SearchesController < ApplicationController - include CommonBehavior - - def authorize! - authorize(@search || @searchs) - end - private :authorize! - - - def create - @search = Search.new(search_params) - @search.user = current_user - authorize! - - respond_to do |format| - if @search.save - path = implement_exercise_path(@search.exercise) - respond_with_valid_object(format, path: path, status: :created) - end - end - end - - def search_params - params[:search].permit(:search, :exercise_id) - end - private :search_params - - def index - @search = policy_scope(ProxyExercise).search(params[:q]) - @searches = @search.result.order(:title).paginate(page: params[:page]) - authorize! - end - -end \ No newline at end of file diff --git a/app/policies/exercise_policy.rb b/app/policies/exercise_policy.rb index 6377488b..54d22b87 100644 --- a/app/policies/exercise_policy.rb +++ b/app/policies/exercise_policy.rb @@ -16,7 +16,7 @@ class ExercisePolicy < AdminOrAuthorPolicy define_method(action) { admin? || author?} end - [:implement?, :working_times?, :intervention?, :submit?, :reload?].each do |action| + [:implement?, :working_times?, :intervention?, :search?, :submit?, :reload?].each do |action| define_method(action) { everyone } end diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 8a43b613..5b3c3478 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -2,7 +2,7 @@ - external_user_id = @current_user.respond_to?(:external_id) ? @current_user.id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - consumer_id = @current_user.respond_to?(:external_id) ? @current_user.consumer_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - show_interventions = @show_interventions || "false" -#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path data-intervention-save-url=intervention_exercise_path data-show-interventions=show_interventions +#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path data-intervention-save-url=intervention_exercise_path data-show-interventions=show_interventions data-course_token=@course_token data-search-save-url=search_exercise_path div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files) div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) div id='frames' class='editor-col' diff --git a/app/views/exercises/_editor_file_tree.html.slim b/app/views/exercises/_editor_file_tree.html.slim index 2c287eba..3f8334dc 100644 --- a/app/views/exercises/_editor_file_tree.html.slim +++ b/app/views/exercises/_editor_file_tree.html.slim @@ -1,4 +1,4 @@ -div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') data-course_token=@course_token +div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden') = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-plus-square', id: 'sidebar-collapse-collapsed', label:'', title:t('exercises.editor.expand_action_sidebar')) - if @exercise.allow_file_creation and not @exercise.hide_file_tree? @@ -27,15 +27,12 @@ div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '') = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over')) - if !@course_token.blank? - = form_for(@search, multipart: true, target: "_blank") do |f| .input-group.enforce-top-margin - = f.hidden_field :exercise_id .enforce-right-margin - = f.text_field(:search, class: 'form-control', id: "search-col", required: true, placeholder: t('search.search_in_forum')) + = text_field_tag 'search-input-text', nil, placeholder: t('search.search_in_forum'), class: 'form-control' .input-group-btn - = button_tag(class: 'btn btn-primary', id: 'btn-search-col', model: @search.class.model_name.human) do + = button_tag(class: 'btn btn-primary', id: 'btn-search-col') do i.fa.fa-search - - if @exercise.allow_file_creation? = render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file')) \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index a33369d4..2c572031 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -62,6 +62,7 @@ Rails.application.routes.draw do get :implement get :working_times post :intervention + post :search get :statistics get :reload post :submit From dbfff77a40d0a33f86457484a5f0251faff398aa Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 21 Mar 2017 16:14:49 +0100 Subject: [PATCH 078/143] added missing proxyexercise which caused problems when recommending exercises --- ...321150454_add_reason_to_user_proxy_exercise_exercise.rb | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 db/migrate/20170321150454_add_reason_to_user_proxy_exercise_exercise.rb diff --git a/db/migrate/20170321150454_add_reason_to_user_proxy_exercise_exercise.rb b/db/migrate/20170321150454_add_reason_to_user_proxy_exercise_exercise.rb new file mode 100644 index 00000000..93ab0cf0 --- /dev/null +++ b/db/migrate/20170321150454_add_reason_to_user_proxy_exercise_exercise.rb @@ -0,0 +1,7 @@ +class AddReasonToUserProxyExerciseExercise < ActiveRecord::Migration + def change + change_table :user_proxy_exercise_exercises do |t| + t.string :reason + end + end +end From 7c986f2de7882dd0b758f728abcd38ec304f5e43 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 21 Mar 2017 17:35:46 +0100 Subject: [PATCH 079/143] fixed problem that no new files could be added to an exercise --- app/assets/javascripts/exercises.js.erb | 3 ++- app/views/exercises/_form.html.slim | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/exercises.js.erb b/app/assets/javascripts/exercises.js.erb index 2a17b405..5d202d64 100644 --- a/app/assets/javascripts/exercises.js.erb +++ b/app/assets/javascripts/exercises.js.erb @@ -228,7 +228,8 @@ $(function() { } if ($.isController('exercises')) { - if ($('table').isPresent()) { + // ignore tags table since it is in the dom before other tables + if ($('table:not(#tags-table)').isPresent()) { enableBatchUpdate(); } else if ($('.edit_exercise, .new_exercise').isPresent()) { execution_environments = $('form').data('execution-environments'); diff --git a/app/views/exercises/_form.html.slim b/app/views/exercises/_form.html.slim index 0811b447..5ab8502b 100644 --- a/app/views/exercises/_form.html.slim +++ b/app/views/exercises/_form.html.slim @@ -40,7 +40,7 @@ = f.number_field "expected_worktime_minutes", value: @exercise.expected_worktime_seconds / 60, in: 1..1000, step: 1 h2 Tags .table-responsive - table.table + table.table#tags-table thead tr th = t('activerecord.attributes.exercise.selection') From 8dad3a68e7d0c4a77c0ced1c9252f83102d3ab76 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 22 Mar 2017 10:06:40 +0100 Subject: [PATCH 080/143] removed searches route --- config/routes.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 2c572031..4c8c18eb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -85,14 +85,6 @@ Rails.application.routes.draw do end end - resources :searches do - member do - post :clone - get :reload - post :submit - end - end - resources :interventions do member do post :clone From c13c169657b5fe63973ada26d2725528f582132a Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 22 Mar 2017 15:07:13 +0100 Subject: [PATCH 081/143] creates an autosave submission on opening of the editor. otherwise we lose track of the time between opening the exercise and the first submission --- app/assets/javascripts/editor/editor.js.erb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index ddbe2e54..0668cbc0 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -680,5 +680,7 @@ configureEditors: function () { this.showFirstFile(); $(window).on("beforeunload", this.unloadAutoSave.bind(this)); + // create autosave when the editor is opened the first time + this.autosave().bind(this); } }; \ No newline at end of file From a142e1c73f28acd82da02b9d41f7ada2df653d37 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 22 Mar 2017 17:30:20 +0100 Subject: [PATCH 082/143] save messages returned from runs --- app/controllers/submissions_controller.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 361ed866..2c7ed9f2 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -13,6 +13,10 @@ class SubmissionsController < ApplicationController before_action :set_mime_type, only: [:download_file, :render_file] skip_before_action :verify_authenticity_token, only: [:download_file, :render_file] + def max_message_buffer_size + 500 + end + def authorize! authorize(@submission || @submissions) end @@ -172,15 +176,21 @@ class SubmissionsController < ApplicationController end def handle_message(message, tubesock, container) + @message_buffer ||= "" # Handle special commands first if (/^#exit/.match(message)) kill_socket(tubesock) + @docker_client.exit_container(container) + if !@message_buffer.blank? + Testrun.create(file: @file, submission: @submission, output: @message_buffer) + end else # Filter out information about run_command, test_command, user or working directory run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename]) test_command = @submission.execution_environment.test_command % command_substitutions(params[:filename]) if !(/root|workspace|#{run_command}|#{test_command}/.match(message)) + @message_buffer += message if @message_buffer.size <= max_message_buffer_size parse_message(message, 'stdout', tubesock) end end From 028876da603473b4cbb1c5850af3d2dfc5bc05d5 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 23 Mar 2017 14:08:37 +0100 Subject: [PATCH 083/143] fix javascript for autosave on beginning --- app/assets/javascripts/editor/editor.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 0668cbc0..2c2b8011 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -681,6 +681,6 @@ configureEditors: function () { $(window).on("beforeunload", this.unloadAutoSave.bind(this)); // create autosave when the editor is opened the first time - this.autosave().bind(this); + this.autosave(); } }; \ No newline at end of file From 420d8b3844a8dac8d504377d83724aeef32564e5 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 23 Mar 2017 14:09:11 +0100 Subject: [PATCH 084/143] minor wording changes in locale for german beak intervention --- config/locales/de.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/de.yml b/config/locales/de.yml index 3b5dd89a..19e30cea 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -280,7 +280,7 @@ de: text: "Es scheint so als würden sie Probleme mit der Aufgabe haben. Wenn Sie möchten, können wir Ihnen helfen!" break_intervention: title: "Pause" - text: "Uns ist aufgefallen, dass du schon lange an dieser Aufgabe arbeitest. Möchtest du vielleicht eine Pause machen um auf neue Gedanken zu kommen?" + text: "Uns ist aufgefallen, dass du schon lange an dieser Aufgabe arbeitest. Möchtest du vielleicht später weiter machen um erstmal auf neue Gedanken zu kommen?" index: clone: Duplizieren implement: Implementieren From 4ff600ff45a4fa85c9edc9629d4da667439e5795 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 23 Mar 2017 14:10:09 +0100 Subject: [PATCH 085/143] added index to submissions for better performance in recommending exercises --- db/migrate/20170323130756_add_index_to_submissions.rb | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 db/migrate/20170323130756_add_index_to_submissions.rb diff --git a/db/migrate/20170323130756_add_index_to_submissions.rb b/db/migrate/20170323130756_add_index_to_submissions.rb new file mode 100644 index 00000000..185500d3 --- /dev/null +++ b/db/migrate/20170323130756_add_index_to_submissions.rb @@ -0,0 +1,6 @@ +class AddIndexToSubmissions < ActiveRecord::Migration + def change + add_index :submissions, :exercise_id + add_index :submissions, :user_id + end +end From 0930cba095924a9ce557265b27fee8385db91ed6 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 23 Mar 2017 14:12:26 +0100 Subject: [PATCH 086/143] changed position of saving the run output, so it catches timeouts as well --- app/controllers/submissions_controller.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 2c7ed9f2..5c9978b7 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -143,7 +143,7 @@ class SubmissionsController < ApplicationController tubesock.onmessage do |data| Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data) # Check whether the client send a JSON command and kill container - # if the command is 'client_exit', send it to docker otherwise. + # if the command is 'client_kill', send it to docker otherwise. begin parsed = JSON.parse(data) if parsed['cmd'] == 'client_kill' @@ -170,6 +170,9 @@ class SubmissionsController < ApplicationController end def kill_socket(tubesock) + # save the output of this "run" as a "testrun" (scoring runs are saved in submission_scoring.rb) + save_run_output + # Hijacked connection needs to be notified correctly tubesock.send_data JSON.dump({'cmd' => 'exit'}) tubesock.close @@ -238,6 +241,12 @@ class SubmissionsController < ApplicationController end end + def save_run_output + if !@message_buffer.blank? + Testrun.create(file: @file, submission: @submission, output: @message_buffer) + end + end + def score hijack do |tubesock| Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? From 47693cd62f1608a1b0cfdb272ebf31ef9748f546 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 23 Mar 2017 18:17:14 +0100 Subject: [PATCH 087/143] revert fixes for IE, it made the banners invisible for all other browsers, too. --- lib/assets/stylesheets/flash.css.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/assets/stylesheets/flash.css.scss b/lib/assets/stylesheets/flash.css.scss index f96e91fd..e21e246f 100644 --- a/lib/assets/stylesheets/flash.css.scss +++ b/lib/assets/stylesheets/flash.css.scss @@ -17,8 +17,10 @@ pointer-events: none; /* fixes for IE */ + /* background:white; opacity:0; filter:Alpha(opacity=0); + */ } From 4798ffcfcfdf4446b1a9253ddf4bd74c68a94fea Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 23 Mar 2017 18:52:46 +0100 Subject: [PATCH 088/143] - added abc group separator class to split users into different groups for testing proxy exercises and interventions - shows 2 interventions per user and exercise max now - only show break or rfc intervention to user --- app/assets/javascripts/editor/editor.js.erb | 113 ++++++++++---------- app/controllers/exercises_controller.rb | 15 ++- app/views/exercises/_editor.html.slim | 5 +- lib/user_group_separator.rb | 27 +++++ 4 files changed, 98 insertions(+), 62 deletions(-) create mode 100644 lib/user_group_separator.rb diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 2c2b8011..867f86ba 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -578,64 +578,63 @@ configureEditors: function () { * interventions * */ initializeInterventionTimer: function() { - $.ajax({ - data: { - exercise_id: $('#editor').data('exercise-id'), - user_id: $('#editor').data('user-id') - }, - dataType: 'json', - method: 'GET', - // get working times for this exercise - url: $('#editor').data('working-times-url'), - success: function (data) { - var percentile75 = data['working_time_75_percentile']; - var accumulatedWorkTimeUser = data['working_time_accumulated']; + if ($('#editor').data('rfc-interventions') == true || $('#editor').data('break-interventions') == true) { // split in break or rfc intervention + $.ajax({ + data: { + exercise_id: $('#editor').data('exercise-id'), + user_id: $('#editor').data('user-id') + }, + dataType: 'json', + method: 'GET', + // get working times for this exercise + url: $('#editor').data('working-times-url'), + success: function (data) { + var percentile75 = data['working_time_75_percentile']; + var accumulatedWorkTimeUser = data['working_time_accumulated']; - var timeUntilBreak = 20 * 60 * 1000; - var minTimeUntilAskQuestion = 15 * 60 * 1000; + var minTimeIntervention = 10 * 60 * 1000; - if ((accumulatedWorkTimeUser - percentile75) > 0) { - // working time is already over 75 percentile - var timeUntilAskQuestion = minTimeUntilAskQuestion; - } else { - // working time is less than 75 percentile - // ensure we give user at least 10 minutes before we bother the user - var timeUntilAskForRFC = (percentile75 - accumulatedWorkTimeUser) > minTimeUntilAskQuestion ? (percentile75 - accumulatedWorkTimeUser) : minTimeUntilAskQuestion; + if ((accumulatedWorkTimeUser - percentile75) > 0) { + // working time is already over 75 percentile + var timeUntilIntervention = minTimeIntervention; + } else { + // working time is less than 75 percentile + // ensure we give user at least minTimeIntervention before we bother the user + var timeUntilIntervention = Math.max(percentile75 - accumulatedWorkTimeUser, minTimeIntervention); + } + if ($('#editor').data('break-interventions')){ + setTimeout(function () { + $('#break-intervention-modal').modal('show'); + $.ajax({ + data: { + intervention_type: 'BreakIntervention' + }, + dataType: 'json', + type: 'POST', + url: $('#editor').data('intervention-save-url') + }); + }, 0); + } else if ($('#editor').data('rfc-interventions')) { + setTimeout(function () { + var button = $('#requestComments'); + // only show intervention if user did not requested for a comment already + if (!button.prop('disabled')) { + $('#rfc_intervention_text').show(); + $('#comment-modal').modal('show'); + $.ajax({ + data: { + intervention_type: 'QuestionIntervention' + }, + dataType: 'json', + type: 'POST', + url: $('#editor').data('intervention-save-url') + }); + }; + }, 0); + } } - - // if notifications are too close to each other, ensure some time differences between them - if (Math.abs(timeUntilAskForRFC - timeUntilBreak) < 5 * 1000 * 60){ - timeUntilBreak = timeUntilBreak * 2; - } - - setTimeout(function() { - $('#break-intervention-modal').modal('show'); - $.ajax({ - data: { - intervention_type: 'BreakIntervention' - }, - dataType: 'json', - type: 'POST', - url: $('#editor').data('intervention-save-url')}); - }, timeUntilBreak); - - - setTimeout(function() { - var button = $('#requestComments'); - if (!button.prop('disabled')){ - $('#rfc_intervention_text').show(); - $('#comment-modal').modal('show'); - $.ajax({ - data: { - intervention_type: 'QuestionIntervention' - }, - dataType: 'json', - type: 'POST', - url: $('#editor').data('intervention-save-url')}); - }; - }, timeUntilAskForRFC); - } - }); + }); + } }, initializeSearchButton: function(){ @@ -671,9 +670,7 @@ configureEditors: function () { this.initializeDescriptionToggle(); this.initializeSideBarTooltips(); this.initializeTooltips(); - if ($('#editor').data('show-interventions') == true){ - this.initializeInterventionTimer(); - } + this.initializeInterventionTimer(); this.initializeSearchButton(); this.initPrompt(); this.renderScore(); diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 25361125..6b5d122f 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -21,7 +21,7 @@ class ExercisesController < ApplicationController private :authorize! def max_intervention_count - 3 + 2 end @@ -168,7 +168,18 @@ class ExercisesController < ApplicationController user_got_enough_interventions = UserExerciseIntervention.where(exercise: @exercise, user: current_user).count >= max_intervention_count is_java_course = @course_token && @course_token.eql?(java_course_token) - @show_interventions = (!is_java_course || user_got_enough_interventions) ? "false" : "true" + user_intervention_group = UserGroupSeparator.getInterventionGroup(current_user) + + case user_intervention_group + when :no_intervention + puts "non" + when :break_intervention + puts "break" + @show_break_interventions = (!is_java_course || user_got_enough_interventions) ? "false" : "true" + when :rfc_intervention + puts "rfc" + @show_rfc_interventions = (!is_java_course || user_got_enough_interventions) ? "false" : "true" + end @search = Search.new @search.exercise = @exercise diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index 5b3c3478..4291028f 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -1,8 +1,9 @@ - external_user_external_id = @current_user.respond_to?(:external_id) ? @current_user.external_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - external_user_id = @current_user.respond_to?(:external_id) ? @current_user.id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - consumer_id = @current_user.respond_to?(:external_id) ? @current_user.consumer_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') -- show_interventions = @show_interventions || "false" -#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path data-intervention-save-url=intervention_exercise_path data-show-interventions=show_interventions data-course_token=@course_token data-search-save-url=search_exercise_path +- show_break_interventions = @show_break_interventions || "false" +- show_rfc_interventions = @show_rfc_interventions || "false" +#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path data-intervention-save-url=intervention_exercise_path data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files) div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) div id='frames' class='editor-col' diff --git a/lib/user_group_separator.rb b/lib/user_group_separator.rb new file mode 100644 index 00000000..320cce96 --- /dev/null +++ b/lib/user_group_separator.rb @@ -0,0 +1,27 @@ +class UserGroupSeparator + + # seperates user into 30% no intervention, 30% break intervention, 40% rfc intervention + def self.getInterventionGroup(user) + lastDigitId = user.id % 10 + if lastDigitId < 3 # 0,1,2 + :no_intervention + elsif lastDigitId < 6 # 3,4,5 + :break_intervention + else # 6,7,8,9 + :rfc_intervention + end + end + + # seperates user into 20% dummy assignment, 20% random assignemnt, 60% recommended assignment + def self.getProxyExerciseGroup(user) + lastDigitCreatedAt = user.created_at.to_i % 10 + if lastDigitCreatedAt < 2 # 0,1 + :dummy_assigment + elsif lastDigitCreatedAt < 4 # 2,3 + :random_assigment + else # 4,5,6,7,8,9 + :recommended_assignment + end + end + +end \ No newline at end of file From ef5ebc3b696768cbdda49612296ca57f32ba99f8 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Fri, 24 Mar 2017 12:14:53 +0100 Subject: [PATCH 089/143] splitted user in groups for assigning bonus exercises --- app/models/proxy_exercise.rb | 55 +++++++++++++++++++++++------------- lib/user_group_separator.rb | 3 ++ 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 11b5a545..a425b91b 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -53,28 +53,43 @@ class ProxyExercise < ActiveRecord::Base end def find_matching_exercise(user) - exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq - tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten - Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}") + user_group = UserGroupSeparator.getProxyExerciseGroup(user) + case user_group + when :dummy_assigment + rec_ex = select_easiest_exercise(exercises) + @reason[:reason] = "dummy group" + Rails.logger.debug("assigned user to dummy group, and gave him exercise: #{rec_ex.title}") + rec_ex + when :random_assigment + @reason[:reason] = "random group" + rec_ex = exercises.shuffle.first + Rails.logger.debug("assigned user to random group, and gave him exercise: #{rec_ex.title}") + rec_ex + when :recommended_assignment + exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq.compact + tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten + Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}") - # find execises - potential_recommended_exercises = [] - exercises.each do |ex| - ## find exercises which have only tags the user has already seen - if (ex.tags - tags_user_has_seen).empty? - potential_recommended_exercises << ex - end - end - Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}") - # if all exercises contain tags which the user has never seen, recommend easiest exercise - if potential_recommended_exercises.empty? - Rails.logger.debug("matched easiest exercise in pool") - @reason[:reason] = "easiest exercise in pool. empty potential exercises" - select_easiest_exercise(exercises) - else - recommended_exercise = select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises) - recommended_exercise + # find execises + potential_recommended_exercises = [] + exercises.each do |ex| + ## find exercises which have only tags the user has already seen + if (ex.tags - tags_user_has_seen).empty? + potential_recommended_exercises << ex + end + end + Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}") + # if all exercises contain tags which the user has never seen, recommend easiest exercise + if potential_recommended_exercises.empty? + Rails.logger.debug("matched easiest exercise in pool") + @reason[:reason] = "easiest exercise in pool. empty potential exercises" + select_easiest_exercise(exercises) + else + recommended_exercise = select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises) + recommended_exercise + end end + end private :find_matching_exercise diff --git a/lib/user_group_separator.rb b/lib/user_group_separator.rb index 320cce96..b46757ad 100644 --- a/lib/user_group_separator.rb +++ b/lib/user_group_separator.rb @@ -16,10 +16,13 @@ class UserGroupSeparator def self.getProxyExerciseGroup(user) lastDigitCreatedAt = user.created_at.to_i % 10 if lastDigitCreatedAt < 2 # 0,1 + puts "dummy" :dummy_assigment elsif lastDigitCreatedAt < 4 # 2,3 + puts "random_assignment" :random_assigment else # 4,5,6,7,8,9 + puts "recommended_assignment" :recommended_assignment end end From 94a4937854bbb28d1a46758c91cf92b321564a5d Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Fri, 24 Mar 2017 12:15:41 +0100 Subject: [PATCH 090/143] removed puts --- lib/user_group_separator.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/user_group_separator.rb b/lib/user_group_separator.rb index b46757ad..320cce96 100644 --- a/lib/user_group_separator.rb +++ b/lib/user_group_separator.rb @@ -16,13 +16,10 @@ class UserGroupSeparator def self.getProxyExerciseGroup(user) lastDigitCreatedAt = user.created_at.to_i % 10 if lastDigitCreatedAt < 2 # 0,1 - puts "dummy" :dummy_assigment elsif lastDigitCreatedAt < 4 # 2,3 - puts "random_assignment" :random_assigment else # 4,5,6,7,8,9 - puts "recommended_assignment" :recommended_assignment end end From 7f6c433fe840a62f19c34a3fdd818fcdc3496212 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Fri, 24 Mar 2017 12:17:18 +0100 Subject: [PATCH 091/143] fixed time for interventions --- app/assets/javascripts/editor/editor.js.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 867f86ba..627de632 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -613,7 +613,7 @@ configureEditors: function () { type: 'POST', url: $('#editor').data('intervention-save-url') }); - }, 0); + }, timeUntilIntervention); } else if ($('#editor').data('rfc-interventions')) { setTimeout(function () { var button = $('#requestComments'); @@ -630,7 +630,7 @@ configureEditors: function () { url: $('#editor').data('intervention-save-url') }); }; - }, 0); + }, timeUntilIntervention); } } }); From a7effa7eb3d5f439eb88ea38ed573aae91096e0e Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Fri, 24 Mar 2017 14:07:24 +0100 Subject: [PATCH 092/143] do now show intervention message for rfc modal if clicked on the button --- app/assets/javascripts/editor/editor.js.erb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 627de632..9deff0d6 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -323,9 +323,7 @@ configureEditors: function () { var button = $('#requestComments'); button.prop('disabled', true); button.on('click', function () { - if ($('#editor').data('show-interventions') == true){ - $('#rfc_intervention_text').hide() - } + $('#rfc_intervention_text').hide() $('#comment-modal').modal('show'); }); From f1a9b0613f6b2a838886b1b4d8bd5445c03fe6ab Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Fri, 24 Mar 2017 14:11:08 +0100 Subject: [PATCH 093/143] remove puts --- app/controllers/exercises_controller.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 6b5d122f..4a643a5d 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -172,12 +172,9 @@ class ExercisesController < ApplicationController case user_intervention_group when :no_intervention - puts "non" when :break_intervention - puts "break" @show_break_interventions = (!is_java_course || user_got_enough_interventions) ? "false" : "true" when :rfc_intervention - puts "rfc" @show_rfc_interventions = (!is_java_course || user_got_enough_interventions) ? "false" : "true" end From bdbc372c0c12d73f8c3fb4d653b52af0d980b4e0 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 24 Mar 2017 18:47:30 +0100 Subject: [PATCH 094/143] fixed saving run results. also fixed websocket closing. --- app/controllers/submissions_controller.rb | 12 +++++++----- lib/docker_client.rb | 7 +++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index 5c9978b7..b9a1846e 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -182,12 +182,13 @@ class SubmissionsController < ApplicationController @message_buffer ||= "" # Handle special commands first if (/^#exit/.match(message)) - kill_socket(tubesock) - + # Just call exit_container on the docker_client. + # Do not call kill_socket for the websocket to the client here. + # @docker_client.exit_container closes the socket to the container, + # kill_socket is called in the "on close handler" of the websocket to the container @docker_client.exit_container(container) - if !@message_buffer.blank? - Testrun.create(file: @file, submission: @submission, output: @message_buffer) - end + elsif /^#timeout/.match(message) + @message_buffer = 'timeout: ' + @message_buffer # add information that this run timed out to the buffer else # Filter out information about run_command, test_command, user or working directory run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename]) @@ -243,6 +244,7 @@ class SubmissionsController < ApplicationController def save_run_output if !@message_buffer.blank? + @message_buffer = @message_buffer[(0..max_message_buffer_size-1)] # trim the string to max_message_buffer_size chars Testrun.create(file: @file, submission: @submission, output: @message_buffer) end end diff --git a/lib/docker_client.rb b/lib/docker_client.rb index 28e2a3fb..76451096 100644 --- a/lib/docker_client.rb +++ b/lib/docker_client.rb @@ -255,6 +255,12 @@ class DockerClient if(@tubesock) @tubesock.send_data JSON.dump({'cmd' => 'timeout'}) end + if(@socket) + @socket.send('#timeout') + #sleep one more second to ensure that the message reaches the submissions_controller. + sleep(1) + @socket.close + end kill_container(container) end #ensure @@ -274,6 +280,7 @@ class DockerClient Rails.logger.debug('exiting container ' + container.to_s) # exit the timeout thread if it is still alive exit_thread_if_alive + @socket.close # if we use pooling and recylce the containers, put it back. otherwise, destroy it. (DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container) end From 6c77b0743d71a5cd0744b0cb84be0ff1883058a1 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 24 Mar 2017 18:48:02 +0100 Subject: [PATCH 095/143] updated schema with indexes. --- db/schema.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index d15a09c9..a669ce66 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170228165741) do +ActiveRecord::Schema.define(version: 20170323130756) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -269,6 +269,9 @@ ActiveRecord::Schema.define(version: 20170228165741) do t.string "user_type", limit: 255 end + add_index "submissions", ["exercise_id"], name: "index_submissions_on_exercise_id", using: :btree + add_index "submissions", ["user_id"], name: "index_submissions_on_user_id", using: :btree + create_table "tags", force: :cascade do |t| t.string "name", null: false t.datetime "created_at" @@ -309,6 +312,7 @@ ActiveRecord::Schema.define(version: 20170228165741) do t.integer "exercise_id" t.datetime "created_at" t.datetime "updated_at" + t.string "reason" end add_index "user_proxy_exercise_exercises", ["exercise_id"], name: "index_user_proxy_exercise_exercises_on_exercise_id", using: :btree From 65256cdfd91defd4ad198d771f25f9b58a38c7c3 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Mon, 27 Mar 2017 09:50:10 +0200 Subject: [PATCH 096/143] change intervention groups also to 20 20 60 --- lib/user_group_separator.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/user_group_separator.rb b/lib/user_group_separator.rb index 320cce96..5be05ef6 100644 --- a/lib/user_group_separator.rb +++ b/lib/user_group_separator.rb @@ -1,13 +1,13 @@ class UserGroupSeparator - # seperates user into 30% no intervention, 30% break intervention, 40% rfc intervention + # seperates user into 20% no intervention, 20% break intervention, 60% rfc intervention def self.getInterventionGroup(user) lastDigitId = user.id % 10 - if lastDigitId < 3 # 0,1,2 + if lastDigitId < 2 # 0,1 :no_intervention - elsif lastDigitId < 6 # 3,4,5 + elsif lastDigitId < 4 # 2,3 :break_intervention - else # 6,7,8,9 + else # 4,5,6,7,8,9 :rfc_intervention end end From d6a25b849632adbcc01f9f92f6765f3bb6bf725b Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Mon, 27 Mar 2017 18:52:44 +0200 Subject: [PATCH 097/143] do now assign dummy exercise to random group --- app/models/proxy_exercise.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index a425b91b..bfef52ff 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -62,9 +62,9 @@ class ProxyExercise < ActiveRecord::Base rec_ex when :random_assigment @reason[:reason] = "random group" - rec_ex = exercises.shuffle.first - Rails.logger.debug("assigned user to random group, and gave him exercise: #{rec_ex.title}") - rec_ex + ex = exercises.where("expected_difficulty > 1").shuffle.first + Rails.logger.debug("assigned user to random group, and gave him exercise: #{ex.title}") + ex when :recommended_assignment exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq.compact tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten From e1f1134992f1af41e9e2a045861eab11bba8e397 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 28 Mar 2017 11:51:51 +0200 Subject: [PATCH 098/143] ignore dummy exercise in recommendation --- app/models/proxy_exercise.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index bfef52ff..f731aa61 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -70,9 +70,9 @@ class ProxyExercise < ActiveRecord::Base tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}") - # find execises + # find exercises potential_recommended_exercises = [] - exercises.each do |ex| + exercises.where("expected_difficulty > 1").each do |ex| ## find exercises which have only tags the user has already seen if (ex.tags - tags_user_has_seen).empty? potential_recommended_exercises << ex From b9e93a5b2162f3c738434b8080a3d6e63e148b95 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 28 Mar 2017 12:10:54 +0200 Subject: [PATCH 099/143] fixed accumulated_working_time_for_only in exercise --- app/models/exercise.rb | 57 +++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 15c740f4..94210fd0 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -133,15 +133,54 @@ class Exercise < ActiveRecord::Base def accumulated_working_time_for_only(user) user_type = user.external_user? ? "ExternalUser" : "InternalUser" Time.parse(self.class.connection.execute(""" - SELECT sum(working_time_new) AS working_time - FROM - (SELECT CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new - FROM - (SELECT id, - (created_at - lag(created_at) over (PARTITION BY user_id, exercise_id - ORDER BY created_at)) AS working_time - FROM submissions - WHERE exercise_id=#{id} and user_id=#{user.id} and user_type='#{user_type}') AS foo) AS bar + WITH WORKING_TIME AS + (SELECT user_id, + id, + exercise_id, + max(score) AS max_score, + (created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id + ORDER BY created_at)) AS working_time + FROM submissions + WHERE exercise_id = #{id} AND user_id = #{user.id} AND user_type = '#{user_type}' + GROUP BY user_id, id, exercise_id), + MAX_POINTS AS + (SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role = 'teacher_defined_test' GROUP BY context_id), + + -- filter for rows containing max points + TIME_MAX_SCORE AS + (SELECT * + FROM WORKING_TIME W1, MAX_POINTS MS + WHERE W1.exercise_id = ex_id AND W1.max_score = MS.max_points), + + -- find row containing the first time max points + FIRST_TIME_MAX_SCORE AS + ( SELECT id,USER_id,exercise_id,max_score,working_time, rn + FROM ( + SELECT id,USER_id,exercise_id,max_score,working_time, + ROW_NUMBER() OVER(PARTITION BY user_id, exercise_id ORDER BY id ASC) AS rn + FROM TIME_MAX_SCORE) T + WHERE rn = 1), + + TIMES_UNTIL_MAX_POINTS AS ( + SELECT W.id, W.user_id, W.exercise_id, W.max_score, W.working_time, M.id AS reachedmax_at + FROM WORKING_TIME W, FIRST_TIME_MAX_SCORE M + WHERE W.user_id = M.user_id AND W.exercise_id = M.exercise_id AND W.id <= M.id), + + -- if user never makes it to max points, take all times + ALL_WORKING_TIMES_UNTIL_MAX AS + ((SELECT id, user_id, exercise_id, max_score, working_time FROM TIMES_UNTIL_MAX_POINTS) + UNION ALL + (SELECT id, user_id, exercise_id, max_score, working_time FROM WORKING_TIME W1 + WHERE NOT EXISTS (SELECT 1 FROM FIRST_TIME_MAX_SCORE F WHERE F.user_id = W1.user_id AND F.exercise_id = W1.exercise_id))), + + FILTERED_TIMES_UNTIL_MAX AS + ( + SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= '0:30:00' 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 + FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e + WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id """).first["working_time"] || "00:00:00").seconds_since_midnight end From c8609ffa812a743fcdc404b4c0bf7010765be5dc Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 28 Mar 2017 12:29:36 +0200 Subject: [PATCH 100/143] improved quantile calculation by using only times until user has reached max points --- app/models/exercise.rb | 122 +++++++++++++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 17 deletions(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 94210fd0..21ab412e 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -82,23 +82,111 @@ class Exercise < ActiveRecord::Base def get_quantiles(quantiles) quantiles_str = "[" + quantiles.join(",") + "]" result = self.class.connection.execute(""" - SELECT unnest(PERCENTILE_CONT(ARRAY#{quantiles_str}) WITHIN GROUP (ORDER BY working_time)) - FROM - ( - SELECT user_id, - sum(working_time_new) AS working_time - FROM - (SELECT user_id, - CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new - FROM - (SELECT user_id, - id, - (created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id - ORDER BY created_at)) AS working_time - FROM submissions - WHERE exercise_id=#{self.id} AND user_type = 'ExternalUser') AS foo) AS bar - GROUP BY user_id - ) AS foo + WITH working_time AS + ( + SELECT user_id, + id, + exercise_id, + Max(score) AS max_score, + (created_at - Lag(created_at) OVER (partition BY user_id, exercise_id ORDER BY created_at)) AS working_time + FROM submissions + WHERE exercise_id = #{id} + AND user_type = 'ExternalUser' + GROUP BY user_id, + id, + exercise_id), max_points AS + ( + SELECT context_id AS ex_id, + Sum(weight) AS max_points + FROM files + WHERE context_type = 'Exercise' + AND context_id = #{id} + AND role = 'teacher_defined_test' + GROUP BY context_id), + -- filter for rows containing max points + time_max_score AS + ( + SELECT * + FROM working_time W1, + max_points MS + WHERE w1.exercise_id = ex_id + AND w1.max_score = ms.max_points), + -- find row containing the first time max points + first_time_max_score AS + ( + SELECT id, + user_id, + exercise_id, + max_score, + working_time, + rn + FROM ( + SELECT id, + user_id, + exercise_id, + max_score, + working_time, + Row_number() OVER(partition BY user_id, exercise_id ORDER BY id ASC) AS rn + FROM time_max_score) T + WHERE rn = 1), times_until_max_points AS + ( + SELECT w.id, + w.user_id, + w.exercise_id, + w.max_score, + w.working_time, + m.id AS reachedmax_at + FROM working_time W, + first_time_max_score M + WHERE w.user_id = m.user_id + AND w.exercise_id = m.exercise_id + AND w.id <= m.id), + -- if user never makes it to max points, take all times + all_working_times_until_max AS ( + ( + SELECT id, + user_id, + exercise_id, + max_score, + working_time + FROM times_until_max_points) + UNION ALL + ( + SELECT id, + user_id, + exercise_id, + max_score, + working_time + FROM working_time W1 + WHERE NOT EXISTS + ( + SELECT 1 + FROM first_time_max_score F + WHERE f.user_id = w1.user_id + AND f.exercise_id = w1.exercise_id))), filtered_times_until_max AS + ( + SELECT user_id, + exercise_id, + max_score, + CASE + WHEN working_time >= '0:30:00' THEN '0' + ELSE working_time + END AS working_time_new + FROM all_working_times_until_max ), result AS + ( + 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 + FROM filtered_times_until_max f, + external_users e + WHERE f.user_id = e.id + GROUP BY e.external_id, + f.user_id, + exercise_id ) + SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time)) + FROM result """) if result.count > 0 quantiles.each_with_index.map{|q,i| Time.parse(result[i]["unnest"]).seconds_since_midnight} From 8dda5659a26d53ecaf31d01110d572bf1f1b23b9 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 28 Mar 2017 13:51:31 +0200 Subject: [PATCH 101/143] moved banners a little more to the top --- lib/assets/stylesheets/flash.css.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/assets/stylesheets/flash.css.scss b/lib/assets/stylesheets/flash.css.scss index e21e246f..661526e8 100644 --- a/lib/assets/stylesheets/flash.css.scss +++ b/lib/assets/stylesheets/flash.css.scss @@ -5,7 +5,7 @@ .fixed_error_messages { position: fixed; z-index: 1000; - top: 110px; + top: 20px; left: 0; width: 100%; padding-left: 10%; From 025212c90e049f80237e486df4a96447195f356b Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Tue, 28 Mar 2017 13:52:02 +0200 Subject: [PATCH 102/143] changed retrieval of user data from openHPI to API v2 --- lib/xikolo/client.rb | 6 +++--- lib/xikolo/user_client.rb | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/xikolo/client.rb b/lib/xikolo/client.rb index 06e4ecf4..4e6f22b2 100644 --- a/lib/xikolo/client.rb +++ b/lib/xikolo/client.rb @@ -10,7 +10,7 @@ class Xikolo::Client end def self.user_profile_url(user_id) - return url + 'users/' + user_id + return url + 'v2/users/' + user_id end def self.post_request(url, params) @@ -38,11 +38,11 @@ class Xikolo::Client end def self.accept - 'application/vnd.xikolo.v1, application/json' + 'application/vnd.xikolo.v1, application/vnd.api+json, application/json' end def self.token - 'Token token="'+Rails.application.secrets.openhpi_api_token+'"' + 'Token token='+Rails.application.secrets.openhpi_api_token#+'"' end private diff --git a/lib/xikolo/user_client.rb b/lib/xikolo/user_client.rb index 63412e46..c681c999 100644 --- a/lib/xikolo/user_client.rb +++ b/lib/xikolo/user_client.rb @@ -4,12 +4,10 @@ class Xikolo::UserClient # return default values if user is not found or if there is a server issue: if user - if user['display_name'].present? - name = user['display_name'] - else - name = user['first_name'] - end - return {display_name: name, user_visual: user['user_visual'], language: user['language']} + name = user.dig('data', 'attributes', 'name') || "User " + user_id + user_visual = user.dig('data', 'attributes', 'avatar_url') || ActionController::Base.helpers.image_path('default.png') + language = user.dig('data', 'attributes', 'language') || "DE" + return {display_name: name, user_visual: user_visual, language: language} else return {display_name: "User " + user_id, user_visual: ActionController::Base.helpers.image_path('default.png'), language: "DE"} end From d4a9df709a279ab7a58c770ec688eb4fcb4958c6 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 30 Mar 2017 15:02:44 +0200 Subject: [PATCH 103/143] quickfixed not working Exercise.accumulated_working_time_for_only(user) --- app/models/exercise.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/exercise.rb b/app/models/exercise.rb index 21ab412e..2d7edde2 100644 --- a/app/models/exercise.rb +++ b/app/models/exercise.rb @@ -269,7 +269,7 @@ class Exercise < ActiveRecord::Base 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 FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id - """).first["working_time"] || "00:00:00").seconds_since_midnight + """).first["working_time"]).seconds_since_midnight rescue 0 end def duplicate(attributes = {}) From ad29551bb41599c9c926b9ec4367e3c54a7ba1dd Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 30 Mar 2017 16:54:16 +0200 Subject: [PATCH 104/143] added code to test bonus exercise with different descriptions. --- app/models/proxy_exercise.rb | 19 ++++++++++++++++++- lib/user_group_separator.rb | 13 +++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index f731aa61..67089bb7 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -36,8 +36,22 @@ class ProxyExercise < ActiveRecord::Base Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" ) assigned_user_proxy_exercise.exercise else - Rails.logger.debug("find new matching exercise for user #{user.id}" ) matching_exercise = + if (token.eql? "47f4c736") + group = UserGroupSeparator.getGroupWeek2Testing(user) + Rails.logger.debug("Bonus exercise 47f4c736 assigned user to group #{group}") + case group + when :group_a + exercises.where(id: 348).first + when :group_b + exercises.where(id: 349).first + when :group_c + exercises.where(id: 350).first + when :group_d + exercises.where(id: 351).first + end + else + Rails.logger.debug("find new matching exercise for user #{user.id}" ) begin find_matching_exercise(user) rescue #fallback @@ -46,8 +60,11 @@ class ProxyExercise < ActiveRecord::Base @reason[:error] = "#{$!}" exercises.shuffle.first end + end user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json) matching_exercise + + end recommended_exercise end diff --git a/lib/user_group_separator.rb b/lib/user_group_separator.rb index 5be05ef6..c49ffe44 100644 --- a/lib/user_group_separator.rb +++ b/lib/user_group_separator.rb @@ -24,4 +24,17 @@ class UserGroupSeparator end end + def self.getGroupWeek2Testing(user) + groupById = user.id % 4 + if groupById == 0 + :group_a + elsif groupById == 1 + :group_b + elsif groupById == 2 + :group_c + else # 3 + :group_d + end + end + end \ No newline at end of file From 20c8a95a87406a20d8295b0f2b786d28e136786e Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 30 Mar 2017 16:56:55 +0200 Subject: [PATCH 105/143] quickfix --- app/models/proxy_exercise.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/proxy_exercise.rb b/app/models/proxy_exercise.rb index 67089bb7..84f5e487 100644 --- a/app/models/proxy_exercise.rb +++ b/app/models/proxy_exercise.rb @@ -38,8 +38,9 @@ class ProxyExercise < ActiveRecord::Base else matching_exercise = if (token.eql? "47f4c736") + Rails.logger.debug("Proxy exercise with token 47f4c736, split user in groups..") group = UserGroupSeparator.getGroupWeek2Testing(user) - Rails.logger.debug("Bonus exercise 47f4c736 assigned user to group #{group}") + Rails.logger.debug("user assigned to group #{group}") case group when :group_a exercises.where(id: 348).first From ed485f32e6e3917d3afb58fd938f7c9e06d75169 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Thu, 30 Mar 2017 19:02:09 +0200 Subject: [PATCH 106/143] change mail sending --- app/controllers/comments_controller.rb | 2 +- app/mailers/user_mailer.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index fe7f454c..f6e4f7fb 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -49,7 +49,7 @@ class CommentsController < ApplicationController @comment = Comment.new(comment_params_without_request_id) if comment_params[:request_id] - UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user) + UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user).deliver_now end respond_to do |format| diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index e1773d48..5b1a04fe 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -18,6 +18,6 @@ class UserMailer < ActionMailer::Base @commenting_user_displayname = commenting_user.displayname @comment_text = comment.text @rfc_link = request_for_comment_url(request_for_comment) - mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email).deliver + mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email) end end From 3f398c6047073074e217061823ffe4b2354d6a46 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Thu, 30 Mar 2017 22:06:17 +0200 Subject: [PATCH 107/143] add feedback for exercises --- .../request_for_comments_controller.rb | 23 ++++++++ app/policies/request_for_comment_policy.rb | 4 ++ .../_comment_exercise_dialogcontent.html.slim | 5 ++ app/views/request_for_comments/show.html.erb | 57 ++++++++++++++++++- config/locales/de.yml | 5 +- config/locales/en.yml | 5 +- config/routes.rb | 1 + 7 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 app/views/exercises/_comment_exercise_dialogcontent.html.slim diff --git a/app/controllers/request_for_comments_controller.rb b/app/controllers/request_for_comments_controller.rb index f9e7137e..90e7f871 100644 --- a/app/controllers/request_for_comments_controller.rb +++ b/app/controllers/request_for_comments_controller.rb @@ -32,6 +32,10 @@ class RequestForCommentsController < ApplicationController end end + def submit + + end + # GET /request_for_comments/1 # GET /request_for_comments/1.json def show @@ -63,6 +67,20 @@ class RequestForCommentsController < ApplicationController authorize! end + def create_comment_exercise + old = UserExerciseFeedback.find_by(exercise_id: params[:exercise_id], user_id: current_user.id, user_type: current_user.class.name) + if old + old.delete + end + uef = UserExerciseFeedback.new(comment_params) + + if uef.save + render(json: {success: "true"}) + else + render(json: {success: "false"}) + end + end + # DELETE /request_for_comments/1 # DELETE /request_for_comments/1.json def destroy @@ -74,6 +92,10 @@ class RequestForCommentsController < ApplicationController authorize! end + def comment_params + params.permit(:exercise_id, :feedback_text).merge(user_id: current_user.id, user_type: current_user.class.name) + end + private # Use callbacks to share common setup or constraints between actions. def set_request_for_comment @@ -85,4 +107,5 @@ class RequestForCommentsController < ApplicationController # 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. 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 diff --git a/app/policies/request_for_comment_policy.rb b/app/policies/request_for_comment_policy.rb index f592e3bd..a0762abf 100644 --- a/app/policies/request_for_comment_policy.rb +++ b/app/policies/request_for_comment_policy.rb @@ -27,4 +27,8 @@ class RequestForCommentPolicy < ApplicationPolicy def index? everyone end + + def create_comment_exercise? + everyone + end end diff --git a/app/views/exercises/_comment_exercise_dialogcontent.html.slim b/app/views/exercises/_comment_exercise_dialogcontent.html.slim new file mode 100644 index 00000000..89d1fd41 --- /dev/null +++ b/app/views/exercises/_comment_exercise_dialogcontent.html.slim @@ -0,0 +1,5 @@ +h5 =t('exercises.implement.comment.addComment') +textarea#commentOnExercise.form-control(style='resize:none;') + +p='' +button#addCommentExerciseButton.btn.btn-block.btn-primary(type='button') =t('exercises.implement.comment.addCommentButton') diff --git a/app/views/request_for_comments/show.html.erb b/app/views/request_for_comments/show.html.erb index 28574ade..592ceb6b 100644 --- a/app/views/request_for_comments/show.html.erb +++ b/app/views/request_for_comments/show.html.erb @@ -1,5 +1,5 @@
-

<%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %>

+

<%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %>

<% user = @request_for_comment.user @@ -20,14 +20,18 @@ <%= t('activerecord.attributes.request_for_comments.question')%>: <%= t('request_for_comments.no_question') %> <% end %> + + <% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %> - + <% elsif (@request_for_comment.solved?) %> - + <% else %> <% end %> + + <% if @current_user.admin? && user.is_a?(ExternalUser) %>

@@ -58,10 +62,13 @@ also, all settings from the rails model needed for the editor configuration in t <% end %> <%= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') %> +<%= render('shared/modal', id: 'comment-exercise-modal', title: t('exercises.implement.comment.addCommentExercise'), template: 'exercises/_comment_exercise_dialogcontent') %> From 341cd3a003e3197f1b31035710b2bb257df0f8c5 Mon Sep 17 00:00:00 2001 From: Ralf Teusner Date: Fri, 7 Apr 2017 21:16:20 +0200 Subject: [PATCH 129/143] fix incomplete resizing of ace editors by triggering a resize event --- app/assets/javascripts/editor/editor.js.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 98c8a714..75c09bb4 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -172,6 +172,7 @@ configureEditors: function () { $('.editor').each(function (index, element) { this.resizeParentOfAceEditor(element); }.bind(this)); + window.dispatchEvent(new Event('resize')); }, resizeParentOfAceEditor: function (element){ From 5002f9bbcecda691f82e72918f992fca10a58b01 Mon Sep 17 00:00:00 2001 From: Niklas Kiefer Date: Tue, 11 Apr 2017 12:19:41 +0200 Subject: [PATCH 130/143] allow iframe requests --- app/controllers/application_controller.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 617bab02..dfc25ca9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base MEMBER_ACTIONS = [:destroy, :edit, :show, :update] after_action :verify_authorized, except: [:help, :welcome] - before_action :set_locale + before_action :set_locale, :allow_iframe_requests protect_from_forgery(with: :exception) rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized @@ -29,4 +29,8 @@ class ApplicationController < ActionController::Base def welcome end + + def allow_iframe_requests + response.headers.delete('X-Frame-Options') + end end From 73c3b902a3fdf11c7c6f8bf3c95116eec6b70fe7 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 11 Apr 2017 15:00:35 +0200 Subject: [PATCH 131/143] save progress. added user feedback view and stuff --- .../user_exercise_feedbacks_controller.rb | 42 +++++++++++++++++++ app/policies/user_exercise_feedback_policy.rb | 34 +++++++++++++++ .../user_exercise_feedbacks/_form.html.slim | 8 ++++ .../user_exercise_feedbacks/new.html.slim | 3 ++ config/locales/en.yml | 5 +++ config/routes.rb | 7 ++++ .../20170411090543_improve_user_feedback.rb | 6 +++ 7 files changed, 105 insertions(+) create mode 100644 app/controllers/user_exercise_feedbacks_controller.rb create mode 100644 app/policies/user_exercise_feedback_policy.rb create mode 100644 app/views/user_exercise_feedbacks/_form.html.slim create mode 100644 app/views/user_exercise_feedbacks/new.html.slim create mode 100644 db/migrate/20170411090543_improve_user_feedback.rb diff --git a/app/controllers/user_exercise_feedbacks_controller.rb b/app/controllers/user_exercise_feedbacks_controller.rb new file mode 100644 index 00000000..445b1a83 --- /dev/null +++ b/app/controllers/user_exercise_feedbacks_controller.rb @@ -0,0 +1,42 @@ +class UserExerciseFeedbackController < ApplicationController + include CommonBehavior + + def authorize! + authorize(@uef) + end + private :authorize! + + def create + @tag = Tag.new(tag_params) + authorize! + create_and_respond(object: @tag) + end + + def destroy + destroy_and_respond(object: @tag) + end + + def edit + end + + def uef_params + params[:tag].permit(:feedback_text, :difficulty) + end + private :uef_params + + def new + @uef = UserExerciseFeedback.new + authorize! + end + + def show + end + + def update + update_and_respond(object: @UserExerciseFeedback, params: uef_params) + end + + def to_s + name + end +end \ No newline at end of file diff --git a/app/policies/user_exercise_feedback_policy.rb b/app/policies/user_exercise_feedback_policy.rb new file mode 100644 index 00000000..8325b9fa --- /dev/null +++ b/app/policies/user_exercise_feedback_policy.rb @@ -0,0 +1,34 @@ +class TagPolicy < AdminOrAuthorPolicy + def author? + @user == @record.author + end + private :author? + + def batch_update? + admin? + end + + def show? + @user.internal_user? + end + + [:clone?, :destroy?, :edit?, :update?].each do |action| + define_method(action) { admin? || author?} + end + + [:reload?].each do |action| + define_method(action) { everyone } + end + + class Scope < Scope + def resolve + if @user.admin? + @scope.all + elsif @user.internal_user? + @scope.where('user_id = ? OR public = TRUE', @user.id) + else + @scope.none + end + end + end +end diff --git a/app/views/user_exercise_feedbacks/_form.html.slim b/app/views/user_exercise_feedbacks/_form.html.slim new file mode 100644 index 00000000..60bde323 --- /dev/null +++ b/app/views/user_exercise_feedbacks/_form.html.slim @@ -0,0 +1,8 @@ += form_for(@uef) do |f| + = render('shared/form_errors', object: @uef) + .form-group + = f.label(:feedback_text) + = f.text_field(:feedback_text, class: 'form-control', required: true) + = f.label(:difficulty) + = f.text_field(:difficulty, class: 'form-control', required: true) + .actions = render('shared/submit_button', f: f, object: @uef) diff --git a/app/views/user_exercise_feedbacks/new.html.slim b/app/views/user_exercise_feedbacks/new.html.slim new file mode 100644 index 00000000..7abfbd34 --- /dev/null +++ b/app/views/user_exercise_feedbacks/new.html.slim @@ -0,0 +1,3 @@ +h1 = t('shared.new_model', model: UserExerciseFeedback.model_name.human) + += render('form') diff --git a/config/locales/en.yml b/config/locales/en.yml index 853c703f..eeadb83a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -546,4 +546,9 @@ en: previous_label: '← Previous Page' file_template: no_template_label: "Empty File" + user_exercise_feedback: + easy: "it was easy" + some_what_easy: "it was somewhat easy" + some_what_difficult: "it was somewhat difficult" + difficult: "difficult" diff --git a/config/routes.rb b/config/routes.rb index a88a4c3d..281af37c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -86,6 +86,13 @@ Rails.application.routes.draw do end end + resources :user_exercise_feedbacks do + member do + get :reload + post :submit + end + end + resources :interventions do member do post :clone diff --git a/db/migrate/20170411090543_improve_user_feedback.rb b/db/migrate/20170411090543_improve_user_feedback.rb new file mode 100644 index 00000000..4050ee3a --- /dev/null +++ b/db/migrate/20170411090543_improve_user_feedback.rb @@ -0,0 +1,6 @@ +class ImproveUserFeedback < ActiveRecord::Migration + def change + remove_column :user_exercise_feedbacks, :difficulty + add_column :user_exercise_feedbacks, :difficulty, :string + end +end From e4d28452bf4ee6f4fcb9dcf8a406340fba5c2131 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Tue, 11 Apr 2017 16:29:29 +0200 Subject: [PATCH 132/143] save progress --- app/controllers/exercises_controller.rb | 9 ++++ .../user_exercise_feedbacks_controller.rb | 53 +++++++++++++++---- app/policies/user_exercise_feedback_policy.rb | 6 ++- .../user_exercise_feedbacks/_form.html.slim | 11 ++-- .../user_exercise_feedbacks/edit.html.slim | 0 config/locales/en.yml | 4 ++ 6 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 app/views/user_exercise_feedbacks/edit.html.slim diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 8af8bb8e..fc2848e7 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -164,6 +164,7 @@ class ExercisesController < ApplicationController private :handle_file_uploads def implement + redirect_to_user_feedback redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists? user_solved_exercise = @exercise.has_user_solved(current_user) user_got_enough_interventions = UserExerciseIntervention.where(user: current_user).where("created_at >= ?", Time.zone.now.beginning_of_day).count >= max_intervention_count @@ -409,4 +410,12 @@ class ExercisesController < ApplicationController redirect_to_lti_return_path end + def redirect_to_user_feedback + if UserExerciseFeedback.find_by(exercise: @exercise, user: current_user) + redirect_to(edit_user_exercise_feedback_path(user_exercise_feedback: {exercise_id: @exercise.id})) + else + redirect_to(new_user_exercise_feedback_path(user_exercise_feedback: {exercise_id: @exercise.id})) + end + end + end diff --git a/app/controllers/user_exercise_feedbacks_controller.rb b/app/controllers/user_exercise_feedbacks_controller.rb index 445b1a83..6a835a37 100644 --- a/app/controllers/user_exercise_feedbacks_controller.rb +++ b/app/controllers/user_exercise_feedbacks_controller.rb @@ -1,15 +1,33 @@ -class UserExerciseFeedbackController < ApplicationController +class UserExerciseFeedbacksController < ApplicationController include CommonBehavior + before_action :set_user_exercise_feedback, only: [:edit, :update] + + def comment_presets + [t('user_exercise_feedback.choose'), + t('user_exercise_feedback.easy'), + t('user_exercise_feedback.some_what_easy'), + t('user_exercise_feedback.some_what_difficult'), + t('user_exercise_feedback.difficult')] + end + def authorize! authorize(@uef) end private :authorize! def create - @tag = Tag.new(tag_params) - authorize! - create_and_respond(object: @tag) + if validate_feedback_text(uef_params[:difficulty]) + exercise = Exercise.find(uef_params[:exercise_id]) + if exercise + @uef = UserExerciseFeedback.new(uef_params) + authorize! + create_and_respond(object: @uef, path: proc{implement_exercise_path(exercise)}) + end + else + flash[:danger] = t('shared.message_failure') + redirect_to(:back, id: uef_params[:exercise_id]) + end end def destroy @@ -17,26 +35,43 @@ class UserExerciseFeedbackController < ApplicationController end def edit + @texts = comment_presets + authorize! end def uef_params - params[:tag].permit(:feedback_text, :difficulty) + params[:user_exercise_feedback].permit(:feedback_text, :difficulty, :exercise_id).merge(user_id: current_user.id, user_type: current_user.class.name) end private :uef_params def new + @texts = comment_presets @uef = UserExerciseFeedback.new + @exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id]) authorize! end - def show - end - def update - update_and_respond(object: @UserExerciseFeedback, params: uef_params) + authorize! + if validate_feedback_text(uef_params[:difficulty]) && @exercise + update_and_respond(object: @uef, params: uef_params, path: implement_exercise_path(@exercise)) + else + flash[:danger] = t('shared.message_failure') + redirect_to(:back, id: uef_params[:exercise_id]) + end end def to_s name end + + def set_user_exercise_feedback + puts "params: #{params}" + @exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id]) + @uef = UserExerciseFeedback.find_by(exercise_id: params[:user_exercise_feedback][:exercise_id], user: current_user) + end + + def validate_feedback_text(difficulty_text) + return comment_presets.include? difficulty_text + end end \ No newline at end of file diff --git a/app/policies/user_exercise_feedback_policy.rb b/app/policies/user_exercise_feedback_policy.rb index 8325b9fa..f005cc0d 100644 --- a/app/policies/user_exercise_feedback_policy.rb +++ b/app/policies/user_exercise_feedback_policy.rb @@ -1,4 +1,4 @@ -class TagPolicy < AdminOrAuthorPolicy +class UserExerciseFeedbackPolicy < AdminOrAuthorPolicy def author? @user == @record.author end @@ -8,6 +8,10 @@ class TagPolicy < AdminOrAuthorPolicy admin? end + def create? + everyone + end + def show? @user.internal_user? end diff --git a/app/views/user_exercise_feedbacks/_form.html.slim b/app/views/user_exercise_feedbacks/_form.html.slim index 60bde323..2d6cfd54 100644 --- a/app/views/user_exercise_feedbacks/_form.html.slim +++ b/app/views/user_exercise_feedbacks/_form.html.slim @@ -1,8 +1,11 @@ = form_for(@uef) do |f| = render('shared/form_errors', object: @uef) + h4 + p = t('user_exercise_feedback.description') .form-group - = f.label(:feedback_text) - = f.text_field(:feedback_text, class: 'form-control', required: true) - = f.label(:difficulty) - = f.text_field(:difficulty, class: 'form-control', required: true) + = f.text_area(:feedback_text, class: 'form-control', required: true, :rows => "10") + h4 = t('user_exercise_feedback.difficulty') + = f.collection_radio_buttons :difficulty, @texts, :to_s, :to_s, html_options={class: "radio-inline"} do |b| + = b.label(:class => 'radio') { b.radio_button + b.text } + = f.hidden_field(:exercise_id, :value => @exercise.id) .actions = render('shared/submit_button', f: f, object: @uef) diff --git a/app/views/user_exercise_feedbacks/edit.html.slim b/app/views/user_exercise_feedbacks/edit.html.slim new file mode 100644 index 00000000..e69de29b diff --git a/config/locales/en.yml b/config/locales/en.yml index eeadb83a..99d11c32 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -547,8 +547,12 @@ en: file_template: no_template_label: "Empty File" user_exercise_feedback: + choose: "choose one" easy: "it was easy" some_what_easy: "it was somewhat easy" some_what_difficult: "it was somewhat difficult" difficult: "difficult" + done: "done" + difficulty: "Difficulty of the exercise" + description: "Here you have the chance to comment on the exercise. Feel free to give us feedback on the exercise, the description or difficulty. Did you liked the question or was it too difficult or easy?" From 8ca944558c4a2979189fbb32061cf44b02794f92 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 12 Apr 2017 10:13:23 +0200 Subject: [PATCH 133/143] improved texts of feedback, added ok button --- .../user_exercise_feedbacks_controller.rb | 37 ++++++++++--------- app/models/user_exercise_feedback.rb | 3 ++ .../user_exercise_feedbacks/_form.html.slim | 4 +- .../user_exercise_feedbacks/edit.html.slim | 1 + .../user_exercise_feedbacks/new.html.slim | 2 - config/locales/de.yml | 12 ++++++ config/locales/en.yml | 16 ++++---- 7 files changed, 46 insertions(+), 29 deletions(-) diff --git a/app/controllers/user_exercise_feedbacks_controller.rb b/app/controllers/user_exercise_feedbacks_controller.rb index 6a835a37..4052fbb5 100644 --- a/app/controllers/user_exercise_feedbacks_controller.rb +++ b/app/controllers/user_exercise_feedbacks_controller.rb @@ -4,11 +4,11 @@ class UserExerciseFeedbacksController < ApplicationController before_action :set_user_exercise_feedback, only: [:edit, :update] def comment_presets - [t('user_exercise_feedback.choose'), - t('user_exercise_feedback.easy'), - t('user_exercise_feedback.some_what_easy'), - t('user_exercise_feedback.some_what_difficult'), - t('user_exercise_feedback.difficult')] + [[0,t('user_exercise_feedback.difficulty_easy')], + [1,t('user_exercise_feedback.difficulty_some_what_easy')], + [2,t('user_exercise_feedback.difficulty_ok')], + [3,t('user_exercise_feedback.difficulty_some_what_difficult')], + [4,t('user_exercise_feedback.difficult_too_difficult')]] end def authorize! @@ -17,16 +17,16 @@ class UserExerciseFeedbacksController < ApplicationController private :authorize! def create - if validate_feedback_text(uef_params[:difficulty]) - exercise = Exercise.find(uef_params[:exercise_id]) - if exercise - @uef = UserExerciseFeedback.new(uef_params) + exercise = Exercise.find(uef_params[:exercise_id]) + if exercise + @uef = UserExerciseFeedback.new(uef_params) + if validate_inputs(uef_params) authorize! create_and_respond(object: @uef, path: proc{implement_exercise_path(exercise)}) + else + flash[:danger] = t('shared.message_failure') + redirect_to(:back, id: uef_params[:exercise_id]) end - else - flash[:danger] = t('shared.message_failure') - redirect_to(:back, id: uef_params[:exercise_id]) end end @@ -35,7 +35,7 @@ class UserExerciseFeedbacksController < ApplicationController end def edit - @texts = comment_presets + @texts = comment_presets.to_a authorize! end @@ -45,7 +45,7 @@ class UserExerciseFeedbacksController < ApplicationController private :uef_params def new - @texts = comment_presets + @texts = comment_presets.to_a @uef = UserExerciseFeedback.new @exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id]) authorize! @@ -53,7 +53,7 @@ class UserExerciseFeedbacksController < ApplicationController def update authorize! - if validate_feedback_text(uef_params[:difficulty]) && @exercise + if @exercise && validate_inputs(uef_params) update_and_respond(object: @uef, params: uef_params, path: implement_exercise_path(@exercise)) else flash[:danger] = t('shared.message_failure') @@ -66,12 +66,13 @@ class UserExerciseFeedbacksController < ApplicationController end def set_user_exercise_feedback - puts "params: #{params}" @exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id]) @uef = UserExerciseFeedback.find_by(exercise_id: params[:user_exercise_feedback][:exercise_id], user: current_user) + @selectedDifficulty = @uef.difficulty end - def validate_feedback_text(difficulty_text) - return comment_presets.include? difficulty_text + def validate_inputs(uef_params) + (uef_params[:difficulty].to_i >= 0 && uef_params[:difficulty].to_i < comment_presets.size) rescue false end + end \ No newline at end of file diff --git a/app/models/user_exercise_feedback.rb b/app/models/user_exercise_feedback.rb index d3ec09d5..4ff9626e 100644 --- a/app/models/user_exercise_feedback.rb +++ b/app/models/user_exercise_feedback.rb @@ -5,4 +5,7 @@ class UserExerciseFeedback < ActiveRecord::Base validates :user_id, uniqueness: { scope: [:exercise_id, :user_type] } + def to_s + "User Exercise Feedback" + end end \ No newline at end of file diff --git a/app/views/user_exercise_feedbacks/_form.html.slim b/app/views/user_exercise_feedbacks/_form.html.slim index 2d6cfd54..ebea7859 100644 --- a/app/views/user_exercise_feedbacks/_form.html.slim +++ b/app/views/user_exercise_feedbacks/_form.html.slim @@ -1,11 +1,11 @@ = form_for(@uef) do |f| = render('shared/form_errors', object: @uef) h4 - p = t('user_exercise_feedback.description') + == t('user_exercise_feedback.description') .form-group = f.text_area(:feedback_text, class: 'form-control', required: true, :rows => "10") h4 = t('user_exercise_feedback.difficulty') - = f.collection_radio_buttons :difficulty, @texts, :to_s, :to_s, html_options={class: "radio-inline"} do |b| + = f.collection_radio_buttons :difficulty, @texts, :first, :last, html_options={class: "radio-inline"} do |b| = b.label(:class => 'radio') { b.radio_button + b.text } = f.hidden_field(:exercise_id, :value => @exercise.id) .actions = render('shared/submit_button', f: f, object: @uef) diff --git a/app/views/user_exercise_feedbacks/edit.html.slim b/app/views/user_exercise_feedbacks/edit.html.slim index e69de29b..7e5cfff1 100644 --- a/app/views/user_exercise_feedbacks/edit.html.slim +++ b/app/views/user_exercise_feedbacks/edit.html.slim @@ -0,0 +1 @@ += render('form') diff --git a/app/views/user_exercise_feedbacks/new.html.slim b/app/views/user_exercise_feedbacks/new.html.slim index 7abfbd34..7e5cfff1 100644 --- a/app/views/user_exercise_feedbacks/new.html.slim +++ b/app/views/user_exercise_feedbacks/new.html.slim @@ -1,3 +1 @@ -h1 = t('shared.new_model', model: UserExerciseFeedback.model_name.human) - = render('form') diff --git a/config/locales/de.yml b/config/locales/de.yml index 80f88c2a..f6480823 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -152,6 +152,9 @@ de: submission: one: Abgabe other: Abgaben + user_exercise_feedback: + one: Feedback + other: Feedback errors: messages: together: 'muss zusammen mit %{attribute} definiert werden' @@ -525,3 +528,12 @@ de: previous_label: '← Vorherige Seite' file_template: no_template_label: "Leere Datei" + user_exercise_feedback: + difficulty_easy: "es war zu einfach" + difficulty_some_what_easy: "es war etwas zu einfach" + difficulty_ok: "es war ok" + difficulty_some_what_difficult: "es war etwas zu schwer" + difficult_too_difficult: "es war zu schwer" + difficulty: "Schwierigkeit der Aufgabe" + description: "Wir freuen uns, wenn Sie uns hier Feedback zur Aufgabe zu geben.
Bitte beschreiben Sie, was Ihnen an der Aufgabe gefallen hat und was nicht. Gabs Schwierigkeiten bei der Aufgabe? War die Aufgabe zu leicht oder zu schwer?
Wir freuen uns über jedes Feedback." + diff --git a/config/locales/en.yml b/config/locales/en.yml index 99d11c32..90ba7d10 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -173,6 +173,9 @@ en: submission: one: Submission other: Submissions + user_exercise_feedback: + one: Feedback + other: Feedback errors: messages: together: 'has to be set along with %{attribute}' @@ -548,11 +551,10 @@ en: no_template_label: "Empty File" user_exercise_feedback: choose: "choose one" - easy: "it was easy" - some_what_easy: "it was somewhat easy" - some_what_difficult: "it was somewhat difficult" - difficult: "difficult" - done: "done" + difficulty_easy: "it was too easy" + difficulty_some_what_easy: "it was somewhat easy" + difficulty_ok: "it was just right" + difficulty_some_what_difficult: "it was somewhat difficult" + difficult_too_difficult: "it was too difficult" difficulty: "Difficulty of the exercise" - description: "Here you have the chance to comment on the exercise. Feel free to give us feedback on the exercise, the description or difficulty. Did you liked the question or was it too difficult or easy?" - + description: "Here you have the chance to comment on the exercise. Feel free to give us feedback on the exercise, the description or difficulty. Did you liked the question or was it too difficult or easy?" \ No newline at end of file From 3cf123c61e857a70e5ce480a949ded9ced238999 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 12 Apr 2017 10:57:44 +0200 Subject: [PATCH 134/143] added working time estimation into user feedback --- .../user_exercise_feedbacks_controller.rb | 25 ++++++++++++++++--- .../user_exercise_feedbacks/_form.html.slim | 12 +++++++++ config/locales/de.yml | 8 +++++- config/locales/en.yml | 8 +++++- .../20170411090543_improve_user_feedback.rb | 3 +-- 5 files changed, 49 insertions(+), 7 deletions(-) diff --git a/app/controllers/user_exercise_feedbacks_controller.rb b/app/controllers/user_exercise_feedbacks_controller.rb index 4052fbb5..5b2a46d1 100644 --- a/app/controllers/user_exercise_feedbacks_controller.rb +++ b/app/controllers/user_exercise_feedbacks_controller.rb @@ -11,6 +11,14 @@ class UserExerciseFeedbacksController < ApplicationController [4,t('user_exercise_feedback.difficult_too_difficult')]] end + def time_presets + [[0,t('user_exercise_feedback.estimated_time_less_5')], + [1,t('user_exercise_feedback.estimated_time_5_to_10')], + [2,t('user_exercise_feedback.estimated_time_10_to_20')], + [3,t('user_exercise_feedback.estimated_time_20_to_30')], + [4,t('user_exercise_feedback.estimated_time_more_30')]] + end + def authorize! authorize(@uef) end @@ -36,16 +44,18 @@ class UserExerciseFeedbacksController < ApplicationController def edit @texts = comment_presets.to_a + @times = time_presets.to_a authorize! end def uef_params - params[:user_exercise_feedback].permit(:feedback_text, :difficulty, :exercise_id).merge(user_id: current_user.id, user_type: current_user.class.name) + params[:user_exercise_feedback].permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime).merge(user_id: current_user.id, user_type: current_user.class.name) end private :uef_params def new @texts = comment_presets.to_a + @times = time_presets.to_a @uef = UserExerciseFeedback.new @exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id]) authorize! @@ -68,11 +78,20 @@ class UserExerciseFeedbacksController < ApplicationController def set_user_exercise_feedback @exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id]) @uef = UserExerciseFeedback.find_by(exercise_id: params[:user_exercise_feedback][:exercise_id], user: current_user) - @selectedDifficulty = @uef.difficulty end def validate_inputs(uef_params) - (uef_params[:difficulty].to_i >= 0 && uef_params[:difficulty].to_i < comment_presets.size) rescue false + begin + if uef_params[:difficulty].to_i < 0 || uef_params[:difficulty].to_i >= comment_presets.size + return false + elsif uef_params[:user_estimated_worktime].to_i < 0 || uef_params[:user_estimated_worktime].to_i >= time_presets.size + return false + else + return true + end + rescue + return false + end end end \ No newline at end of file diff --git a/app/views/user_exercise_feedbacks/_form.html.slim b/app/views/user_exercise_feedbacks/_form.html.slim index ebea7859..c3a783aa 100644 --- a/app/views/user_exercise_feedbacks/_form.html.slim +++ b/app/views/user_exercise_feedbacks/_form.html.slim @@ -1,4 +1,13 @@ = form_for(@uef) do |f| + div + span.badge.pull-right.score + + h1 id="exercise-headline" + = t('activerecord.models.user_exercise_feedback.one') + " " + = link_to(@exercise.title, [:implement, @exercise]) + #description-panel.lead.description-panel + u = t('activerecord.attributes.exercise.description') + = render_markdown(@exercise.description) = render('shared/form_errors', object: @uef) h4 == t('user_exercise_feedback.description') @@ -7,5 +16,8 @@ h4 = t('user_exercise_feedback.difficulty') = f.collection_radio_buttons :difficulty, @texts, :first, :last, html_options={class: "radio-inline"} do |b| = b.label(:class => 'radio') { b.radio_button + b.text } + h4 = t('user_exercise_feedback.working_time') + = f.collection_radio_buttons :user_estimated_worktime, @times, :first, :last, html_options={class: "radio-inline"} do |b| + = b.label(:class => 'radio') { b.radio_button + b.text } = f.hidden_field(:exercise_id, :value => @exercise.id) .actions = render('shared/submit_button', f: f, object: @uef) diff --git a/config/locales/de.yml b/config/locales/de.yml index f6480823..0d7f8afd 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -535,5 +535,11 @@ de: difficulty_some_what_difficult: "es war etwas zu schwer" difficult_too_difficult: "es war zu schwer" difficulty: "Schwierigkeit der Aufgabe" - description: "Wir freuen uns, wenn Sie uns hier Feedback zur Aufgabe zu geben.
Bitte beschreiben Sie, was Ihnen an der Aufgabe gefallen hat und was nicht. Gabs Schwierigkeiten bei der Aufgabe? War die Aufgabe zu leicht oder zu schwer?
Wir freuen uns über jedes Feedback." + description: "Wir freuen uns, wenn Sie uns hier Feedback zur Aufgabe zu geben.

Bitte beschreiben Sie, was Ihnen an der Aufgabe gefallen hat und was nicht. Gabs Schwierigkeiten bei der Aufgabe? War die Aufgabe zu leicht oder zu schwer?
Wir freuen uns über jedes Feedback." + estimated_time_less_5: "weniger als 5 Minuten" + estimated_time_5_to_10: "zwischen 5 und 10 Minuten" + estimated_time_10_to_20: "zwischen 10 und 20 Minuten" + estimated_time_20_to_30: "zwischen 20 und 30 Minuten" + estimated_time_more_30: "mehr als 30 Minuten" + working_time: "Geschätze Bearbeitungszeit für diese Aufgabe" diff --git a/config/locales/en.yml b/config/locales/en.yml index 90ba7d10..c8cf5003 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -557,4 +557,10 @@ en: difficulty_some_what_difficult: "it was somewhat difficult" difficult_too_difficult: "it was too difficult" difficulty: "Difficulty of the exercise" - description: "Here you have the chance to comment on the exercise. Feel free to give us feedback on the exercise, the description or difficulty. Did you liked the question or was it too difficult or easy?" \ No newline at end of file + description: "We kindly ask you for feedback for this exercise.

Please describe what you liked on this exercise and what you did not. Was the exercise easy to understand or did you have problems understanding? How was the difficulty of the exercise to you?
We are happy about any feedback." + working_time: "Estimated time working on this exercise" + estimated_time_less_5: "less than 5 minutes" + estimated_time_5_to_10: "between 5 and 10 minutes" + estimated_time_10_to_20: "between 10 and 20 minutes" + estimated_time_20_to_30: "between 20 and 30 minutes" + estimated_time_more_30: "more than 30 minutes" diff --git a/db/migrate/20170411090543_improve_user_feedback.rb b/db/migrate/20170411090543_improve_user_feedback.rb index 4050ee3a..bfbd8a02 100644 --- a/db/migrate/20170411090543_improve_user_feedback.rb +++ b/db/migrate/20170411090543_improve_user_feedback.rb @@ -1,6 +1,5 @@ class ImproveUserFeedback < ActiveRecord::Migration def change - remove_column :user_exercise_feedbacks, :difficulty - add_column :user_exercise_feedbacks, :difficulty, :string + add_column :user_exercise_feedbacks, :user_estimated_worktime, :integer end end From 60e587b6903bdfc325831b6bba46927a3fa0e989 Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Wed, 12 Apr 2017 11:47:39 +0200 Subject: [PATCH 135/143] removed comment on exercise in the RFC view. redirect 10% of user instead of redirecting to the RFC view to the feedback view. redirect all users how submitted to the feedback view if score is less than 100% --- app/controllers/exercises_controller.rb | 11 ++++++-- .../user_exercise_feedbacks_controller.rb | 26 ++++++++++++++++--- .../_comment_exercise_dialogcontent.html.slim | 5 ---- app/views/request_for_comments/show.html.erb | 25 ------------------ .../user_exercise_feedbacks/_form.html.slim | 6 ++--- config/locales/de.yml | 2 +- config/locales/en.yml | 2 +- 7 files changed, 36 insertions(+), 41 deletions(-) delete mode 100644 app/views/exercises/_comment_exercise_dialogcontent.html.slim diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index fc2848e7..2e33d11a 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -164,7 +164,6 @@ class ExercisesController < ApplicationController private :handle_file_uploads def implement - redirect_to_user_feedback redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists? user_solved_exercise = @exercise.has_user_solved(current_user) user_got_enough_interventions = UserExerciseIntervention.where(user: current_user).where("created_at >= ?", Time.zone.now.beginning_of_day).count >= max_intervention_count @@ -381,7 +380,11 @@ class ExercisesController < ApplicationController if @submission.normalized_score == 1.0 # if user is external and has an own rfc, redirect to it and message him to clean up and accept the answer. (we need to check that the user is external, # otherwise an internal user could be shown a false rfc here, since current_user.id is polymorphic, but only makes sense for external users when used with rfcs.) - if current_user.respond_to? :external_id + # redirect 10 percent pseudorandomly to the feedback page + if ((current_user.id + @submission.exercise.created_at.to_i) % 10 == 1) + redirect_to_user_feedback + return + elsif current_user.respond_to? :external_id if rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise, user_id: current_user.id).first # set a message that informs the user that his own RFC should be closed. flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_own_rfc') @@ -406,6 +409,10 @@ class ExercisesController < ApplicationController return end end + else + # redirect to feedback page if score is less than 100 percent + redirect_to_user_feedback + return end redirect_to_lti_return_path end diff --git a/app/controllers/user_exercise_feedbacks_controller.rb b/app/controllers/user_exercise_feedbacks_controller.rb index 5b2a46d1..0d1e1925 100644 --- a/app/controllers/user_exercise_feedbacks_controller.rb +++ b/app/controllers/user_exercise_feedbacks_controller.rb @@ -25,17 +25,27 @@ class UserExerciseFeedbacksController < ApplicationController private :authorize! def create - exercise = Exercise.find(uef_params[:exercise_id]) - if exercise + @exercise = Exercise.find(uef_params[:exercise_id]) + rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first + submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first rescue nil + + if @exercise @uef = UserExerciseFeedback.new(uef_params) if validate_inputs(uef_params) authorize! - create_and_respond(object: @uef, path: proc{implement_exercise_path(exercise)}) + path = + if rfc && submission && submission.normalized_score == 1.0 + request_for_comment_path(rfc) + else + implement_exercise_path(@exercise) + end + create_and_respond(object: @uef, path: proc{path}) else flash[:danger] = t('shared.message_failure') redirect_to(:back, id: uef_params[:exercise_id]) end end + end def destroy @@ -62,9 +72,17 @@ class UserExerciseFeedbacksController < ApplicationController end def update + submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first rescue nil + rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first authorize! if @exercise && validate_inputs(uef_params) - update_and_respond(object: @uef, params: uef_params, path: implement_exercise_path(@exercise)) + path = + if rfc && submission && submission.normalized_score == 1.0 + request_for_comment_path(rfc) + else + implement_exercise_path(@exercise) + end + update_and_respond(object: @uef, params: uef_params, path: path) else flash[:danger] = t('shared.message_failure') redirect_to(:back, id: uef_params[:exercise_id]) diff --git a/app/views/exercises/_comment_exercise_dialogcontent.html.slim b/app/views/exercises/_comment_exercise_dialogcontent.html.slim deleted file mode 100644 index 89d1fd41..00000000 --- a/app/views/exercises/_comment_exercise_dialogcontent.html.slim +++ /dev/null @@ -1,5 +0,0 @@ -h5 =t('exercises.implement.comment.addComment') -textarea#commentOnExercise.form-control(style='resize:none;') - -p='' -button#addCommentExerciseButton.btn.btn-block.btn-primary(type='button') =t('exercises.implement.comment.addCommentButton') diff --git a/app/views/request_for_comments/show.html.erb b/app/views/request_for_comments/show.html.erb index 57c48828..ffc7f661 100644 --- a/app/views/request_for_comments/show.html.erb +++ b/app/views/request_for_comments/show.html.erb @@ -20,7 +20,6 @@ <%= t('activerecord.attributes.request_for_comments.question')%>: <%= t('request_for_comments.no_question') %> <% end %> - <% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %> @@ -62,7 +61,6 @@ also, all settings from the rails model needed for the editor configuration in t <% end %> <%= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.dialogtitle'), template: 'exercises/_comment_dialogcontent') %> -<%= render('shared/modal', id: 'comment-exercise-modal', title: t('exercises.implement.comment.addCommentExercise'), template: 'exercises/_comment_exercise_dialogcontent') %>