From 0db11884bcdc5a3607d2f22372f7a4985b3d9bae Mon Sep 17 00:00:00 2001 From: Thomas Hille Date: Sun, 29 Jan 2017 20:26:45 +0100 Subject: [PATCH 01/60] 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 02/60] 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 03/60] 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 04/60] 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 05/60] 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 06/60] 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 07/60] 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 08/60] 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 09/60] 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 10/60] 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 11/60] 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 12/60] 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 13/60] 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 14/60] 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 15/60] 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 16/60] 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 17/60] 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 18/60] 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 19/60] 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 20/60] 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 21/60] 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 22/60] 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 23/60] 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 24/60] 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 25/60] 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 26/60] 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 27/60] 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 28/60] 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 29/60] 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 30/60] 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 31/60] 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 32/60] 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 33/60] 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 34/60] 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 35/60] 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 36/60] 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 37/60] 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 38/60] 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 39/60] 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 40/60] 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 41/60] 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 42/60] 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 43/60] 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 44/60] 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 45/60] 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 46/60] 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 47/60] 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 48/60] 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 49/60] 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 50/60] 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 51/60] 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 52/60] 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 53/60] 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 54/60] 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 55/60] 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 56/60] 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 57/60] 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 58/60] 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 59/60] 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 60/60] 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() {