Merge pull request #91 from openHPI/RecommendingExercises
Recommending exercises
This commit is contained in:
@ -323,6 +323,9 @@ configureEditors: function () {
|
|||||||
var button = $('#requestComments');
|
var button = $('#requestComments');
|
||||||
button.prop('disabled', true);
|
button.prop('disabled', true);
|
||||||
button.on('click', function () {
|
button.on('click', function () {
|
||||||
|
if ($('#editor').data('show-interventions') == true){
|
||||||
|
$('#rfc_intervention_text').hide()
|
||||||
|
}
|
||||||
$('#comment-modal').modal('show');
|
$('#comment-modal').modal('show');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -571,6 +574,80 @@ configureEditors: function () {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* interventions
|
||||||
|
* */
|
||||||
|
initializeInterventionTimer: function() {
|
||||||
|
$.ajax({
|
||||||
|
data: {
|
||||||
|
exercise_id: $('#editor').data('exercise-id'),
|
||||||
|
user_id: $('#editor').data('user-id')
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
method: 'GET',
|
||||||
|
// get working times for this exercise
|
||||||
|
url: $('#editor').data('working-times-url'),
|
||||||
|
success: function (data) {
|
||||||
|
var percentile75 = data['working_time_75_percentile'];
|
||||||
|
var accumulatedWorkTimeUser = data['working_time_accumulated'];
|
||||||
|
|
||||||
|
var timeUntilBreak = 20 * 60 * 1000;
|
||||||
|
var minTimeUntilAskQuestion = 15 * 60 * 1000;
|
||||||
|
|
||||||
|
if ((accumulatedWorkTimeUser - percentile75) > 0) {
|
||||||
|
// working time is already over 75 percentile
|
||||||
|
var timeUntilAskQuestion = minTimeUntilAskQuestion;
|
||||||
|
} else {
|
||||||
|
// working time is less than 75 percentile
|
||||||
|
// ensure we give user at least 10 minutes before we bother the user
|
||||||
|
var timeUntilAskForRFC = (percentile75 - accumulatedWorkTimeUser) > minTimeUntilAskQuestion ? (percentile75 - accumulatedWorkTimeUser) : minTimeUntilAskQuestion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if notifications are too close to each other, ensure some time differences between them
|
||||||
|
if (Math.abs(timeUntilAskForRFC - timeUntilBreak) < 5 * 1000 * 60){
|
||||||
|
timeUntilBreak = timeUntilBreak * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
$('#break-intervention-modal').modal('show');
|
||||||
|
$.ajax({
|
||||||
|
data: {
|
||||||
|
intervention_type: 'BreakIntervention'
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
type: 'POST',
|
||||||
|
url: $('#editor').data('intervention-save-url')});
|
||||||
|
}, timeUntilBreak);
|
||||||
|
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
var button = $('#requestComments');
|
||||||
|
if (!button.prop('disabled')){
|
||||||
|
$('#rfc_intervention_text').show();
|
||||||
|
$('#comment-modal').modal('show');
|
||||||
|
$.ajax({
|
||||||
|
data: {
|
||||||
|
intervention_type: 'QuestionIntervention'
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
type: 'POST',
|
||||||
|
url: $('#editor').data('intervention-save-url')});
|
||||||
|
};
|
||||||
|
}, timeUntilAskForRFC);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
initializeSearchButton: function(){
|
||||||
|
$('#btn-search-col').button().click(function(){
|
||||||
|
var search = $('#search-col').val();
|
||||||
|
var course_token = $('#sidebar-collapsed').data('course_token')
|
||||||
|
window.open(`https://open.hpi.de/courses/${course_token}/pinboard?query=${search}`, '_blank');
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#sidebar-search-collapsed').on('click',this.handleSideBarToggle.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
initializeEverything: function() {
|
initializeEverything: function() {
|
||||||
this.initializeRegexes();
|
this.initializeRegexes();
|
||||||
@ -585,6 +662,10 @@ configureEditors: function () {
|
|||||||
this.initializeDescriptionToggle();
|
this.initializeDescriptionToggle();
|
||||||
this.initializeSideBarTooltips();
|
this.initializeSideBarTooltips();
|
||||||
this.initializeTooltips();
|
this.initializeTooltips();
|
||||||
|
if ($('#editor').data('show-interventions') == true){
|
||||||
|
this.initializeInterventionTimer();
|
||||||
|
}
|
||||||
|
this.initializeSearchButton();
|
||||||
this.initPrompt();
|
this.initPrompt();
|
||||||
this.renderScore();
|
this.renderScore();
|
||||||
this.showFirstFile();
|
this.showFirstFile();
|
||||||
|
@ -74,7 +74,12 @@ module Lti
|
|||||||
private :require_valid_consumer_key
|
private :require_valid_consumer_key
|
||||||
|
|
||||||
def require_valid_exercise_token
|
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.get_matching_exercise(@current_user)
|
||||||
|
else
|
||||||
|
@exercise = Exercise.find_by(token: params[:custom_token])
|
||||||
|
end
|
||||||
refuse_lti_launch(message: t('sessions.oauth.invalid_exercise_token')) unless @exercise
|
refuse_lti_launch(message: t('sessions.oauth.invalid_exercise_token')) unless @exercise
|
||||||
end
|
end
|
||||||
private :require_valid_exercise_token
|
private :require_valid_exercise_token
|
||||||
@ -129,19 +134,16 @@ module Lti
|
|||||||
private :set_current_user
|
private :set_current_user
|
||||||
|
|
||||||
def store_lti_session_data(options = {})
|
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,
|
lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id,
|
||||||
external_users_id: current_user.id,
|
external_users_id: @current_user.id,
|
||||||
exercises_id: exercise_id)
|
exercises_id: @exercise.id)
|
||||||
|
|
||||||
lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).to_json
|
lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).to_json
|
||||||
lti_parameters.save!
|
lti_parameters.save!
|
||||||
|
@lti_parameters = lti_parameters
|
||||||
|
|
||||||
session[:consumer_id] = options[:consumer].id
|
session[:consumer_id] = options[:consumer].id
|
||||||
session[:external_user_id] = current_user.id
|
session[:external_user_id] = @current_user.id
|
||||||
end
|
end
|
||||||
private :store_lti_session_data
|
private :store_lti_session_data
|
||||||
|
|
||||||
|
@ -6,9 +6,10 @@ class ExercisesController < ApplicationController
|
|||||||
|
|
||||||
before_action :handle_file_uploads, only: [:create, :update]
|
before_action :handle_file_uploads, only: [:create, :update]
|
||||||
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
|
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
|
||||||
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :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_external_user, only: [:statistics]
|
||||||
before_action :set_file_types, only: [:create, :edit, :new, :update]
|
before_action :set_file_types, only: [:create, :edit, :new, :update]
|
||||||
|
before_action :set_course_token, only: [:implement]
|
||||||
|
|
||||||
skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml]
|
skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml]
|
||||||
skip_after_action :verify_authorized, only: [:import_proforma_xml]
|
skip_after_action :verify_authorized, only: [:import_proforma_xml]
|
||||||
@ -19,6 +20,15 @@ class ExercisesController < ApplicationController
|
|||||||
end
|
end
|
||||||
private :authorize!
|
private :authorize!
|
||||||
|
|
||||||
|
def max_intervention_count
|
||||||
|
3
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def java_course_token
|
||||||
|
"702cbd2a-c84c-4b37-923a-692d7d1532d0"
|
||||||
|
end
|
||||||
|
|
||||||
def batch_update
|
def batch_update
|
||||||
@exercises = Exercise.all
|
@exercises = Exercise.all
|
||||||
authorize!
|
authorize!
|
||||||
@ -54,6 +64,20 @@ class ExercisesController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
@exercise = Exercise.new(exercise_params)
|
@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!
|
authorize!
|
||||||
create_and_respond(object: @exercise)
|
create_and_respond(object: @exercise)
|
||||||
end
|
end
|
||||||
@ -63,6 +87,7 @@ class ExercisesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
|
collect_set_and_unset_exercise_tags
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_proforma_xml
|
def import_proforma_xml
|
||||||
@ -118,7 +143,8 @@ class ExercisesController < ApplicationController
|
|||||||
private :user_by_code_harbor_token
|
private :user_by_code_harbor_token
|
||||||
|
|
||||||
def exercise_params
|
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
|
end
|
||||||
private :exercise_params
|
private :exercise_params
|
||||||
|
|
||||||
@ -139,6 +165,13 @@ class ExercisesController < ApplicationController
|
|||||||
|
|
||||||
def implement
|
def implement
|
||||||
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
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 >= max_intervention_count
|
||||||
|
is_java_course = @course_token && @course_token.eql?(java_course_token)
|
||||||
|
|
||||||
|
@show_interventions = (!is_java_course || user_got_enough_interventions) ? "false" : "true"
|
||||||
|
|
||||||
|
@search = Search.new
|
||||||
|
@search.exercise = @exercise
|
||||||
@submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
|
@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)
|
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension)
|
||||||
@paths = collect_paths(@files)
|
@paths = collect_paths(@files)
|
||||||
@ -150,6 +183,44 @@ class ExercisesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_course_token
|
||||||
|
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
|
||||||
|
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
|
||||||
|
render(json: {working_time_75_percentile: working_time_75_percentile, working_time_accumulated: working_time_accumulated})
|
||||||
|
end
|
||||||
|
|
||||||
|
def intervention
|
||||||
|
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
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@search = policy_scope(Exercise).search(params[:q])
|
@search = policy_scope(Exercise).search(params[:q])
|
||||||
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page])
|
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page])
|
||||||
@ -174,6 +245,8 @@ class ExercisesController < ApplicationController
|
|||||||
|
|
||||||
def new
|
def new
|
||||||
@exercise = Exercise.new
|
@exercise = Exercise.new
|
||||||
|
collect_set_and_unset_exercise_tags
|
||||||
|
|
||||||
authorize!
|
authorize!
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -201,6 +274,16 @@ class ExercisesController < ApplicationController
|
|||||||
end
|
end
|
||||||
private :set_file_types
|
private :set_file_types
|
||||||
|
|
||||||
|
def collect_set_and_unset_exercise_tags
|
||||||
|
@search = policy_scope(Tag).search(params[:q])
|
||||||
|
@tags = @search.result.order(:name)
|
||||||
|
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
|
||||||
|
|
||||||
def show
|
def show
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -252,7 +335,20 @@ class ExercisesController < ApplicationController
|
|||||||
private :transmit_lti_score
|
private :transmit_lti_score
|
||||||
|
|
||||||
def update
|
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
|
end
|
||||||
|
|
||||||
def redirect_after_submit
|
def redirect_after_submit
|
||||||
|
55
app/controllers/interventions_controller.rb
Normal file
55
app/controllers/interventions_controller.rb
Normal file
@ -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
|
80
app/controllers/proxy_exercises_controller.rb
Normal file
80
app/controllers/proxy_exercises_controller.rb
Normal file
@ -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
|
34
app/controllers/searches_controller.rb
Normal file
34
app/controllers/searches_controller.rb
Normal file
@ -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
|
@ -1,7 +1,7 @@
|
|||||||
class SessionsController < ApplicationController
|
class SessionsController < ApplicationController
|
||||||
include Lti
|
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)
|
before_action(method_name, only: :create_through_lti)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -18,7 +18,6 @@ class SessionsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_through_lti
|
def create_through_lti
|
||||||
set_current_user
|
|
||||||
store_lti_session_data(consumer: @consumer, parameters: params)
|
store_lti_session_data(consumer: @consumer, parameters: params)
|
||||||
store_nonce(params[:oauth_nonce])
|
store_nonce(params[:oauth_nonce])
|
||||||
redirect_to(implement_exercise_path(@exercise),
|
redirect_to(implement_exercise_path(@exercise),
|
||||||
|
55
app/controllers/tags_controller.rb
Normal file
55
app/controllers/tags_controller.rb
Normal file
@ -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
|
@ -8,6 +8,11 @@ module User
|
|||||||
has_many :exercises, as: :user
|
has_many :exercises, as: :user
|
||||||
has_many :file_types, as: :user
|
has_many :file_types, as: :user
|
||||||
has_many :submissions, 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
|
||||||
|
accepts_nested_attributes_for :user_proxy_exercise_exercises
|
||||||
|
|
||||||
|
|
||||||
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
|
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
|
||||||
end
|
end
|
||||||
|
@ -12,6 +12,15 @@ class Exercise < ActiveRecord::Base
|
|||||||
belongs_to :execution_environment
|
belongs_to :execution_environment
|
||||||
has_many :submissions
|
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 :external_users, source: :user, source_type: ExternalUser, through: :submissions
|
||||||
has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
|
has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
|
||||||
alias_method :users, :external_users
|
alias_method :users, :external_users
|
||||||
@ -48,6 +57,10 @@ class Exercise < ActiveRecord::Base
|
|||||||
return user_count == 0 ? 0 : submissions.count() / user_count.to_f()
|
return user_count == 0 ? 0 : submissions.count() / user_count.to_f()
|
||||||
end
|
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
|
def user_working_time_query
|
||||||
"""
|
"""
|
||||||
SELECT user_id,
|
SELECT user_id,
|
||||||
@ -58,7 +71,7 @@ class Exercise < ActiveRecord::Base
|
|||||||
FROM
|
FROM
|
||||||
(SELECT user_id,
|
(SELECT user_id,
|
||||||
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
|
ORDER BY created_at)) AS working_time
|
||||||
FROM submissions
|
FROM submissions
|
||||||
WHERE exercise_id=#{id}) AS foo) AS bar
|
WHERE exercise_id=#{id}) AS foo) AS bar
|
||||||
@ -66,6 +79,35 @@ class Exercise < ActiveRecord::Base
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_quantiles(quantiles)
|
||||||
|
quantiles_str = "[" + quantiles.join(",") + "]"
|
||||||
|
result = self.class.connection.execute("""
|
||||||
|
SELECT unnest(PERCENTILE_CONT(ARRAY#{quantiles_str}) WITHIN GROUP (ORDER BY working_time))
|
||||||
|
FROM
|
||||||
|
(
|
||||||
|
SELECT user_id,
|
||||||
|
sum(working_time_new) AS working_time
|
||||||
|
FROM
|
||||||
|
(SELECT user_id,
|
||||||
|
CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new
|
||||||
|
FROM
|
||||||
|
(SELECT user_id,
|
||||||
|
id,
|
||||||
|
(created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
|
||||||
|
ORDER BY created_at)) AS working_time
|
||||||
|
FROM submissions
|
||||||
|
WHERE exercise_id=#{self.id} AND user_type = 'ExternalUser') AS foo) AS bar
|
||||||
|
GROUP BY user_id
|
||||||
|
) AS foo
|
||||||
|
""")
|
||||||
|
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
|
def retrieve_working_time_statistics
|
||||||
@working_time_statistics = {}
|
@working_time_statistics = {}
|
||||||
self.class.connection.execute(user_working_time_query).each do |tuple|
|
self.class.connection.execute(user_working_time_query).each do |tuple|
|
||||||
@ -88,23 +130,25 @@ class Exercise < ActiveRecord::Base
|
|||||||
@working_time_statistics[user_id]["working_time"]
|
@working_time_statistics[user_id]["working_time"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def average_working_time_for_only(user_id)
|
def accumulated_working_time_for_only(user)
|
||||||
self.class.connection.execute("""
|
user_type = user.external_user? ? "ExternalUser" : "InternalUser"
|
||||||
|
Time.parse(self.class.connection.execute("""
|
||||||
SELECT sum(working_time_new) AS working_time
|
SELECT sum(working_time_new) AS working_time
|
||||||
FROM
|
FROM
|
||||||
(SELECT CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new
|
(SELECT CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new
|
||||||
FROM
|
FROM
|
||||||
(SELECT id,
|
(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
|
ORDER BY created_at)) AS working_time
|
||||||
FROM submissions
|
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='#{user_type}') AS foo) AS bar
|
||||||
""").first["working_time"]
|
""").first["working_time"] || "00:00:00").seconds_since_midnight
|
||||||
end
|
end
|
||||||
|
|
||||||
def duplicate(attributes = {})
|
def duplicate(attributes = {})
|
||||||
exercise = dup
|
exercise = dup
|
||||||
exercise.attributes = attributes
|
exercise.attributes = attributes
|
||||||
|
exercise_tags.each { |et| exercise.exercise_tags << et.dup }
|
||||||
files.each { |file| exercise.files << file.dup }
|
files.each { |file| exercise.files << file.dup }
|
||||||
exercise
|
exercise
|
||||||
end
|
end
|
||||||
@ -162,8 +206,12 @@ class Exercise < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
private :generate_token
|
private :generate_token
|
||||||
|
|
||||||
def maximum_score
|
def maximum_score(user = nil)
|
||||||
files.teacher_defined_tests.sum(:weight)
|
if user
|
||||||
|
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0
|
||||||
|
else
|
||||||
|
files.teacher_defined_tests.sum(:weight)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_default_values
|
def set_default_values
|
||||||
|
5
app/models/exercise_collection.rb
Normal file
5
app/models/exercise_collection.rb
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class ExerciseCollection < ActiveRecord::Base
|
||||||
|
|
||||||
|
has_and_belongs_to_many :exercises
|
||||||
|
|
||||||
|
end
|
13
app/models/exercise_tag.rb
Normal file
13
app/models/exercise_tag.rb
Normal file
@ -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
|
16
app/models/intervention.rb
Normal file
16
app/models/intervention.rb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
class Intervention < ActiveRecord::Base
|
||||||
|
|
||||||
|
has_many :user_exercise_interventions
|
||||||
|
has_many :users, through: :user_exercise_interventions, source_type: "ExternalUser"
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
name
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.createDefaultInterventions
|
||||||
|
%w(BreakIntervention QuestionIntervention).each do |name|
|
||||||
|
Intervention.find_or_create_by(name: name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
220
app/models/proxy_exercise.rb
Normal file
220
app/models/proxy_exercise.rb
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
class ProxyExercise < ActiveRecord::Base
|
||||||
|
|
||||||
|
after_initialize :generate_token
|
||||||
|
after_initialize :set_reason
|
||||||
|
|
||||||
|
has_and_belongs_to_many :exercises
|
||||||
|
has_many :user_proxy_exercise_exercises
|
||||||
|
|
||||||
|
def count_files
|
||||||
|
exercises.count
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_reason
|
||||||
|
@reason = {}
|
||||||
|
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
|
||||||
|
|
||||||
|
def get_matching_exercise(user)
|
||||||
|
assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first
|
||||||
|
recommended_exercise =
|
||||||
|
if (assigned_user_proxy_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.debug("find new matching exercise for user #{user.id}" )
|
||||||
|
matching_exercise =
|
||||||
|
begin
|
||||||
|
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, reason: @reason.to_json)
|
||||||
|
matching_exercise
|
||||||
|
end
|
||||||
|
recommended_exercise
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_matching_exercise(user)
|
||||||
|
exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq
|
||||||
|
tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten
|
||||||
|
Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}")
|
||||||
|
|
||||||
|
# find execises
|
||||||
|
potential_recommended_exercises = []
|
||||||
|
exercises.each do |ex|
|
||||||
|
## find exercises which have only tags the user has already seen
|
||||||
|
if (ex.tags - tags_user_has_seen).empty?
|
||||||
|
potential_recommended_exercises << ex
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}")
|
||||||
|
# if all exercises contain tags which the user has never seen, recommend easiest exercise
|
||||||
|
if potential_recommended_exercises.empty?
|
||||||
|
Rails.logger.debug("matched easiest exercise in pool")
|
||||||
|
@reason[:reason] = "easiest exercise in pool. empty potential exercises"
|
||||||
|
select_easiest_exercise(exercises)
|
||||||
|
else
|
||||||
|
recommended_exercise = select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
|
||||||
|
recommended_exercise
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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)
|
||||||
|
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 = {}
|
||||||
|
topic_knowledge_max.keys.each do |tag|
|
||||||
|
current_users_knowledge_lack[tag] = topic_knowledge_user[tag] / topic_knowledge_max[tag]
|
||||||
|
end
|
||||||
|
|
||||||
|
relative_knowledge_improvement = {}
|
||||||
|
potential_recommended_exercises.each do |potex|
|
||||||
|
tags = potex.tags
|
||||||
|
relative_knowledge_improvement[potex] = 0.0
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
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.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.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.debug("review exercise #{ex.id} diff: #{ex.expected_difficulty}")
|
||||||
|
if (ex.expected_difficulty - highest_difficulty_user_has_accessed) <= 1
|
||||||
|
Rails.logger.debug("matched exercise #{ex.id}")
|
||||||
|
return ex
|
||||||
|
else
|
||||||
|
Rails.logger.debug("exercise #{ex.id} is too difficult")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
easiest_exercise = sorted_exercises.min_by{|k,v| v}.first
|
||||||
|
Rails.logger.debug("no match, select easiest exercise as fallback #{easiest_exercise.id}")
|
||||||
|
easiest_exercise
|
||||||
|
end
|
||||||
|
private :find_best_exercise
|
||||||
|
|
||||||
|
# [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 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
|
||||||
|
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 = 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|
|
||||||
|
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
|
||||||
|
private :score
|
||||||
|
|
||||||
|
def get_user_knowledge_and_max_knowledge(user, exercises)
|
||||||
|
# initialize knowledge for each tag with 0
|
||||||
|
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.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.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.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
|
||||||
|
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 1/(1+(Math::E**(-3/(0.5*total_count_tag)*(count_tag-0.5*total_count_tag))))
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_easiest_exercise(exercises)
|
||||||
|
exercises.order(:expected_difficulty).first
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
4
app/models/search.rb
Normal file
4
app/models/search.rb
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
class Search < ActiveRecord::Base
|
||||||
|
belongs_to :user, polymorphic: true
|
||||||
|
belongs_to :exercise
|
||||||
|
end
|
22
app/models/tag.rb
Normal file
22
app/models/tag.rb
Normal file
@ -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
|
8
app/models/user_exercise_feedback.rb
Normal file
8
app/models/user_exercise_feedback.rb
Normal file
@ -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
|
11
app/models/user_exercise_intervention.rb
Normal file
11
app/models/user_exercise_intervention.rb
Normal file
@ -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
|
14
app/models/user_proxy_exercise_exercise.rb
Normal file
14
app/models/user_proxy_exercise_exercise.rb
Normal file
@ -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
|
@ -16,7 +16,7 @@ class ExercisePolicy < AdminOrAuthorPolicy
|
|||||||
define_method(action) { admin? || author?}
|
define_method(action) { admin? || author?}
|
||||||
end
|
end
|
||||||
|
|
||||||
[:implement?, :submit?, :reload?].each do |action|
|
[:implement?, :working_times?, :intervention?, :submit?, :reload?].each do |action|
|
||||||
define_method(action) { everyone }
|
define_method(action) { everyone }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
34
app/policies/intervention_policy.rb
Normal file
34
app/policies/intervention_policy.rb
Normal file
@ -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
|
34
app/policies/proxy_exercise_policy.rb
Normal file
34
app/policies/proxy_exercise_policy.rb
Normal file
@ -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
|
34
app/policies/search_policy.rb
Normal file
34
app/policies/search_policy.rb
Normal file
@ -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
|
34
app/policies/tag_policy.rb
Normal file
34
app/policies/tag_policy.rb
Normal file
@ -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
|
@ -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_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 : '')
|
- 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 : '')
|
- 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
|
- 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="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='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'
|
div id='frames' class='editor-col'
|
||||||
@ -22,3 +23,4 @@
|
|||||||
|
|
||||||
|
|
||||||
= 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')
|
||||||
|
= render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal')
|
@ -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'))
|
= 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?
|
- if @exercise.allow_file_creation and not @exercise.hide_file_tree?
|
||||||
@ -6,6 +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-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-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over'))
|
||||||
|
- if @course_token
|
||||||
|
= 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' : '')
|
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'))
|
= 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'))
|
||||||
@ -24,5 +26,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 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'))
|
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over'))
|
||||||
|
|
||||||
|
- if @course_token
|
||||||
|
= 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?
|
- if @exercise.allow_file_creation?
|
||||||
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))
|
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))
|
@ -32,6 +32,25 @@
|
|||||||
label
|
label
|
||||||
= f.check_box(:allow_auto_completion)
|
= f.check_box(:allow_auto_completion)
|
||||||
= t('activerecord.attributes.exercise.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')
|
h2 = t('activerecord.attributes.exercise.files')
|
||||||
ul#files.list-unstyled.panel-group
|
ul#files.list-unstyled.panel-group
|
||||||
= f.fields_for :files do |files_form|
|
= f.fields_for :files do |files_form|
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
|
h5#rfc_intervention_text style='display: none;' = t('exercises.implement.rfc_intervention.text')
|
||||||
h5 = t('exercises.implement.comment.question')
|
h5 = t('exercises.implement.comment.question')
|
||||||
|
|
||||||
|
|
||||||
textarea.form-control#question(style='resize:none;')
|
textarea.form-control#question(style='resize:none;')
|
||||||
p = ''
|
p = ''
|
||||||
/ data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action.
|
/ data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action.
|
||||||
|
@ -22,3 +22,4 @@
|
|||||||
#questions-column
|
#questions-column
|
||||||
#questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}"
|
#questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}"
|
||||||
= qa_js_tag
|
= qa_js_tag
|
||||||
|
|
||||||
|
@ -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 = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment'))
|
||||||
th = t('.test_files')
|
th = t('.test_files')
|
||||||
th = t('activerecord.attributes.exercise.maximum_score')
|
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
|
th
|
||||||
= t('activerecord.attributes.exercise.public')
|
= t('activerecord.attributes.exercise.public')
|
||||||
- if policy(Exercise).batch_update?
|
- 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 = 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.files.teacher_defined_tests.count
|
||||||
td = exercise.maximum_score
|
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.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('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit?
|
||||||
td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement?
|
td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement?
|
||||||
|
@ -19,6 +19,9 @@ h1
|
|||||||
= row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?)
|
= row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?)
|
||||||
= row(label: 'exercise.embedding_parameters') do
|
= row(label: 'exercise.embedding_parameters') do
|
||||||
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))
|
= 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')
|
h2 = t('activerecord.attributes.exercise.files')
|
||||||
|
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
h5 = t('exercises.implement.break_intervention.text')
|
6
app/views/interventions/_form.html.slim
Normal file
6
app/views/interventions/_form.html.slim
Normal file
@ -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)
|
14
app/views/interventions/index.html.slim
Normal file
14
app/views/interventions/index.html.slim
Normal file
@ -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)
|
4
app/views/interventions/show.html.slim
Normal file
4
app/views/interventions/show.html.slim
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
h1
|
||||||
|
= @intervention.name
|
||||||
|
|
||||||
|
= row(label: 'intervention.name', value: @intervention.name)
|
24
app/views/proxy_exercises/_form.html.slim
Normal file
24
app/views/proxy_exercises/_form.html.slim
Normal file
@ -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)
|
3
app/views/proxy_exercises/edit.html.slim
Normal file
3
app/views/proxy_exercises/edit.html.slim
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
h1 = t('activerecord.models.proxy_exercise.one', model: ProxyExercise.model_name.human)+ ": " + @proxy_exercise.title
|
||||||
|
|
||||||
|
= render('form')
|
35
app/views/proxy_exercises/index.html.slim
Normal file
35
app/views/proxy_exercises/index.html.slim
Normal file
@ -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)
|
3
app/views/proxy_exercises/new.html.slim
Normal file
3
app/views/proxy_exercises/new.html.slim
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
h1 = t('shared.new_model', model: ProxyExercise.model_name.human)
|
||||||
|
|
||||||
|
= render('form')
|
3
app/views/proxy_exercises/reload.json.jbuilder
Normal file
3
app/views/proxy_exercises/reload.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
json.set! :files do
|
||||||
|
json.array! @exercise.files.visible, :content, :id
|
||||||
|
end
|
23
app/views/proxy_exercises/show.html.slim
Normal file
23
app/views/proxy_exercises/show.html.slim
Normal file
@ -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)
|
0
app/views/searches/destroy.html.erb
Normal file
0
app/views/searches/destroy.html.erb
Normal file
6
app/views/tags/_form.html.slim
Normal file
6
app/views/tags/_form.html.slim
Normal file
@ -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)
|
3
app/views/tags/edit.html.slim
Normal file
3
app/views/tags/edit.html.slim
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
h1 = @tag.name
|
||||||
|
|
||||||
|
= render('form')
|
19
app/views/tags/index.html.slim
Normal file
19
app/views/tags/index.html.slim
Normal file
@ -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)
|
3
app/views/tags/new.html.slim
Normal file
3
app/views/tags/new.html.slim
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
h1 = t('shared.new_model', model: Tag.model_name.human)
|
||||||
|
|
||||||
|
= render('form')
|
6
app/views/tags/show.html.slim
Normal file
6
app/views/tags/show.html.slim
Normal file
@ -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)
|
@ -27,6 +27,7 @@ de:
|
|||||||
exercise:
|
exercise:
|
||||||
description: Beschreibung
|
description: Beschreibung
|
||||||
embedding_parameters: Parameter für LTI-Einbettung
|
embedding_parameters: Parameter für LTI-Einbettung
|
||||||
|
tags: Tags
|
||||||
execution_environment: Ausführungsumgebung
|
execution_environment: Ausführungsumgebung
|
||||||
execution_environment_id: Ausführungsumgebung
|
execution_environment_id: Ausführungsumgebung
|
||||||
files: Dateien
|
files: Dateien
|
||||||
@ -34,10 +35,16 @@ de:
|
|||||||
instructions: Anweisungen
|
instructions: Anweisungen
|
||||||
maximum_score: Erreichbare Punktzahl
|
maximum_score: Erreichbare Punktzahl
|
||||||
public: Öffentlich
|
public: Öffentlich
|
||||||
|
selection: Ausgewählt
|
||||||
title: Titel
|
title: Titel
|
||||||
user: Autor
|
user: Autor
|
||||||
allow_auto_completion: "Autovervollständigung aktivieren"
|
allow_auto_completion: "Autovervollständigung aktivieren"
|
||||||
allow_file_creation: "Dateierstellung erlauben"
|
allow_file_creation: "Dateierstellung erlauben"
|
||||||
|
difficulty: Schwierigkeitsgrad
|
||||||
|
worktime: "vermutete Arbeitszeit in Minuten"
|
||||||
|
proxy_exercise:
|
||||||
|
title: Title
|
||||||
|
files_count: Anzahl der Aufgaben
|
||||||
external_user:
|
external_user:
|
||||||
consumer: Konsument
|
consumer: Konsument
|
||||||
email: E-Mail
|
email: E-Mail
|
||||||
@ -68,6 +75,8 @@ de:
|
|||||||
message: Nachricht
|
message: Nachricht
|
||||||
name: Name
|
name: Name
|
||||||
regular_expression: Regulärer Ausdruck
|
regular_expression: Regulärer Ausdruck
|
||||||
|
intervention:
|
||||||
|
name: Name
|
||||||
internal_user:
|
internal_user:
|
||||||
activated: Aktiviert
|
activated: Aktiviert
|
||||||
consumer: Konsument
|
consumer: Konsument
|
||||||
@ -91,6 +100,10 @@ de:
|
|||||||
files: Dateien
|
files: Dateien
|
||||||
score: Punktzahl
|
score: Punktzahl
|
||||||
user: Autor
|
user: Autor
|
||||||
|
tag:
|
||||||
|
name: Name
|
||||||
|
usage: Verwendet
|
||||||
|
difficulty: Anteil an der Aufgabe
|
||||||
file_template:
|
file_template:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
file_type: "Dateityp"
|
file_type: "Dateityp"
|
||||||
@ -111,6 +124,9 @@ de:
|
|||||||
exercise:
|
exercise:
|
||||||
one: Aufgabe
|
one: Aufgabe
|
||||||
other: Aufgaben
|
other: Aufgaben
|
||||||
|
proxy_exercise:
|
||||||
|
one: Proxy Aufgabe
|
||||||
|
other: Proxy Aufgaben
|
||||||
external_user:
|
external_user:
|
||||||
one: Externer Nutzer
|
one: Externer Nutzer
|
||||||
other: Externe Nutzer
|
other: Externe Nutzer
|
||||||
@ -259,7 +275,12 @@ de:
|
|||||||
removeAllOnLine: Meine Kommentare auf dieser Zeile löschen
|
removeAllOnLine: Meine Kommentare auf dieser Zeile löschen
|
||||||
listing: Die neuesten Kommentaranfragen
|
listing: Die neuesten Kommentaranfragen
|
||||||
request: "Kommentaranfrage stellen"
|
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:
|
index:
|
||||||
clone: Duplizieren
|
clone: Duplizieren
|
||||||
implement: Implementieren
|
implement: Implementieren
|
||||||
@ -290,6 +311,9 @@ de:
|
|||||||
tests: Unit Tests
|
tests: Unit Tests
|
||||||
time_difference: 'Arbeitszeit bis hier*'
|
time_difference: 'Arbeitszeit bis hier*'
|
||||||
addendum: '* Differenzen von mehr als 30 Minuten werden ignoriert.'
|
addendum: '* Differenzen von mehr als 30 Minuten werden ignoriert.'
|
||||||
|
proxy_exercises:
|
||||||
|
index:
|
||||||
|
clone: Duplizieren
|
||||||
external_users:
|
external_users:
|
||||||
statistics:
|
statistics:
|
||||||
title: Statistiken für Externe Benutzer
|
title: Statistiken für Externe Benutzer
|
||||||
@ -327,6 +351,8 @@ de:
|
|||||||
success: Sie haben Ihr Passwort erfolgreich geändert.
|
success: Sie haben Ihr Passwort erfolgreich geändert.
|
||||||
show:
|
show:
|
||||||
link: Profil
|
link: Profil
|
||||||
|
search:
|
||||||
|
search_in_forum: "Probleme? Suche hier im Forum"
|
||||||
locales:
|
locales:
|
||||||
de: Deutsch
|
de: Deutsch
|
||||||
en: Englisch
|
en: Englisch
|
||||||
|
@ -48,6 +48,7 @@ en:
|
|||||||
exercise:
|
exercise:
|
||||||
description: Description
|
description: Description
|
||||||
embedding_parameters: LTI Embedding Parameters
|
embedding_parameters: LTI Embedding Parameters
|
||||||
|
tags: Tags
|
||||||
execution_environment: Execution Environment
|
execution_environment: Execution Environment
|
||||||
execution_environment_id: Execution Environment
|
execution_environment_id: Execution Environment
|
||||||
files: Files
|
files: Files
|
||||||
@ -55,10 +56,16 @@ en:
|
|||||||
instructions: Instructions
|
instructions: Instructions
|
||||||
maximum_score: Maximum Score
|
maximum_score: Maximum Score
|
||||||
public: Public
|
public: Public
|
||||||
|
selection: Selected
|
||||||
title: Title
|
title: Title
|
||||||
user: Author
|
user: Author
|
||||||
allow_auto_completion: "Allow auto completion"
|
allow_auto_completion: "Allow auto completion"
|
||||||
allow_file_creation: "Allow file creation"
|
allow_file_creation: "Allow file creation"
|
||||||
|
difficulty: Difficulty
|
||||||
|
worktime: "Expected worktime in minutes"
|
||||||
|
proxy_exercise:
|
||||||
|
title: Title
|
||||||
|
files_count: Exercises Count
|
||||||
external_user:
|
external_user:
|
||||||
consumer: Consumer
|
consumer: Consumer
|
||||||
email: Email
|
email: Email
|
||||||
@ -89,6 +96,8 @@ en:
|
|||||||
message: Message
|
message: Message
|
||||||
name: Name
|
name: Name
|
||||||
regular_expression: Regular Expression
|
regular_expression: Regular Expression
|
||||||
|
intervention:
|
||||||
|
name: Name
|
||||||
internal_user:
|
internal_user:
|
||||||
activated: Activated
|
activated: Activated
|
||||||
consumer: Consumer
|
consumer: Consumer
|
||||||
@ -112,6 +121,10 @@ en:
|
|||||||
files: Files
|
files: Files
|
||||||
score: Score
|
score: Score
|
||||||
user: Author
|
user: Author
|
||||||
|
tag:
|
||||||
|
name: Name
|
||||||
|
usage: Used
|
||||||
|
difficulty: Share on the Exercise
|
||||||
file_template:
|
file_template:
|
||||||
name: "Name"
|
name: "Name"
|
||||||
file_type: "File Type"
|
file_type: "File Type"
|
||||||
@ -132,6 +145,9 @@ en:
|
|||||||
exercise:
|
exercise:
|
||||||
one: Exercise
|
one: Exercise
|
||||||
other: Exercises
|
other: Exercises
|
||||||
|
proxy_exercise:
|
||||||
|
one: Proxy Exercise
|
||||||
|
other: Proxy Exercises
|
||||||
external_user:
|
external_user:
|
||||||
one: External User
|
one: External User
|
||||||
other: External Users
|
other: External Users
|
||||||
@ -281,6 +297,11 @@ en:
|
|||||||
listing: Listing the newest comment requests
|
listing: Listing the newest comment requests
|
||||||
request: "Request Comments"
|
request: "Request Comments"
|
||||||
question: "Please shortly describe your problem or the program part you would like to get feedback for."
|
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:
|
index:
|
||||||
clone: Duplicate
|
clone: Duplicate
|
||||||
implement: Implement
|
implement: Implement
|
||||||
@ -311,6 +332,9 @@ en:
|
|||||||
tests: Unit Test Results
|
tests: Unit Test Results
|
||||||
time_difference: 'Working Time until here*'
|
time_difference: 'Working Time until here*'
|
||||||
addendum: '* Deltas longer than 30 minutes are ignored.'
|
addendum: '* Deltas longer than 30 minutes are ignored.'
|
||||||
|
proxy_exercises:
|
||||||
|
index:
|
||||||
|
clone: Duplicate
|
||||||
external_users:
|
external_users:
|
||||||
statistics:
|
statistics:
|
||||||
title: External User Statistics
|
title: External User Statistics
|
||||||
@ -348,6 +372,8 @@ en:
|
|||||||
success: You successfully changed your password.
|
success: You successfully changed your password.
|
||||||
show:
|
show:
|
||||||
link: Profile
|
link: Profile
|
||||||
|
search:
|
||||||
|
search_in_forum: "Problems? Search here in forum"
|
||||||
locales:
|
locales:
|
||||||
de: German
|
de: German
|
||||||
en: English
|
en: English
|
||||||
|
@ -60,12 +60,46 @@ Rails.application.routes.draw do
|
|||||||
member do
|
member do
|
||||||
post :clone
|
post :clone
|
||||||
get :implement
|
get :implement
|
||||||
|
get :working_times
|
||||||
|
post :intervention
|
||||||
get :statistics
|
get :statistics
|
||||||
get :reload
|
get :reload
|
||||||
post :submit
|
post :submit
|
||||||
end
|
end
|
||||||
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 :searches do
|
||||||
|
member do
|
||||||
|
post :clone
|
||||||
|
get :reload
|
||||||
|
post :submit
|
||||||
|
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 :external_users, only: [:index, :show], concerns: :statistics do
|
||||||
resources :exercises, concerns: :statistics
|
resources :exercises, concerns: :statistics
|
||||||
end
|
end
|
||||||
|
14
db/migrate/20170205163247_create_exercise_collections.rb
Normal file
14
db/migrate/20170205163247_create_exercise_collections.rb
Normal file
@ -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
|
23
db/migrate/20170205165450_create_proxy_exercises.rb
Normal file
23
db/migrate/20170205165450_create_proxy_exercises.rb
Normal file
@ -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
|
23
db/migrate/20170205210357_create_interventions.rb
Normal file
23
db/migrate/20170205210357_create_interventions.rb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
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.integer :accumulated_worktime_s
|
||||||
|
t.text :reason
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table :interventions do |t|
|
||||||
|
t.string :name
|
||||||
|
t.text :markup
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
Intervention.createDefaultInterventions
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
19
db/migrate/20170206141210_add_tags.rb
Normal file
19
db/migrate/20170206141210_add_tags.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
class AddTags < ActiveRecord::Migration
|
||||||
|
|
||||||
|
def change
|
||||||
|
add_column :exercises, :expected_worktime_seconds, :integer, default: 60
|
||||||
|
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: 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
11
db/migrate/20170206152503_add_user_feedback.rb
Normal file
11
db/migrate/20170206152503_add_user_feedback.rb
Normal file
@ -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
|
10
db/migrate/20170228165741_add_search.rb
Normal file
10
db/migrate/20170228165741_add_search.rb
Normal file
@ -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
|
0
deleteme.txt
Normal file
0
deleteme.txt
Normal file
@ -165,6 +165,7 @@ describe Lti do
|
|||||||
|
|
||||||
it 'stores data in the session' do
|
it 'stores data in the session' do
|
||||||
controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user))
|
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(:consumer_id, anything)
|
||||||
expect(controller.session).to receive(:[]=).with(:external_user_id, anything)
|
expect(controller.session).to receive(:[]=).with(:external_user_id, anything)
|
||||||
controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters)
|
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
|
it 'it creates an LtiParameter Object' do
|
||||||
before_count = LtiParameter.count
|
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)
|
controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters)
|
||||||
expect(LtiParameter.count).to eq(before_count + 1)
|
expect(LtiParameter.count).to eq(before_count + 1)
|
||||||
end
|
end
|
||||||
|
@ -28,6 +28,7 @@ describe SessionsController do
|
|||||||
|
|
||||||
describe 'POST #create_through_lti' do
|
describe 'POST #create_through_lti' do
|
||||||
let(:exercise) { FactoryGirl.create(:dummy) }
|
let(:exercise) { FactoryGirl.create(:dummy) }
|
||||||
|
let(:exercise2) { FactoryGirl.create(:dummy) }
|
||||||
let(:nonce) { SecureRandom.hex }
|
let(:nonce) { SecureRandom.hex }
|
||||||
before(:each) { I18n.locale = I18n.default_locale }
|
before(:each) { I18n.locale = I18n.default_locale }
|
||||||
|
|
||||||
@ -129,6 +130,23 @@ describe SessionsController do
|
|||||||
request
|
request
|
||||||
expect(controller).to redirect_to(implement_exercise_path(exercise.id))
|
expect(controller).to redirect_to(implement_exercise_path(exercise.id))
|
||||||
end
|
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
|
||||||
|
|
||||||
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
7
spec/factories/proxy_exercise.rb
Normal file
7
spec/factories/proxy_exercise.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
FactoryGirl.define do
|
||||||
|
factory :proxy_exercise, class: ProxyExercise do
|
||||||
|
token 'dummytoken'
|
||||||
|
title 'Dummy'
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Reference in New Issue
Block a user