Merge branch 'master' into client-routesv2
This commit is contained in:
@ -167,6 +167,14 @@ configureEditors: function () {
|
||||
$('button i.fa-spin').hide();
|
||||
},
|
||||
|
||||
|
||||
resizeAceEditors: function (){
|
||||
$('.editor').each(function (index, element) {
|
||||
this.resizeParentOfAceEditor(element);
|
||||
}.bind(this));
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
},
|
||||
|
||||
resizeParentOfAceEditor: function (element){
|
||||
// calculate needed size: window height - position of top of button-bar - 60 for bar itself and margins
|
||||
var windowHeight = window.innerHeight - $('#editor-buttons').offset().top - 60;
|
||||
@ -316,10 +324,14 @@ configureEditors: function () {
|
||||
var button = $('#requestComments');
|
||||
button.prop('disabled', true);
|
||||
button.on('click', function () {
|
||||
$('#rfc_intervention_text').hide()
|
||||
$('#comment-modal').modal('show');
|
||||
});
|
||||
|
||||
$('#askForCommentsButton').on('click', this.requestComments.bind(this));
|
||||
$('#closeAskForCommentsButton').on('click', function(){
|
||||
$('#comment-modal').modal('hide');
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
button.prop('disabled', false);
|
||||
@ -363,7 +375,7 @@ configureEditors: function () {
|
||||
panel.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count);
|
||||
panel.find('.row .col-sm-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2)));
|
||||
panel.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight);
|
||||
panel.find('.row .col-sm-9').eq(2).text(result.message);
|
||||
panel.find('.row .col-sm-9').eq(2).html(result.message);
|
||||
if (result.error_messages) panel.find('.row .col-sm-9').eq(3).text(result.error_messages.join(', '));
|
||||
panel.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index);
|
||||
},
|
||||
@ -559,8 +571,104 @@ configureEditors: function () {
|
||||
$('#description-panel').toggleClass('description-panel');
|
||||
$('#description-symbol').toggleClass('fa-chevron-down');
|
||||
$('#description-symbol').toggleClass('fa-chevron-right');
|
||||
// resize ace editors
|
||||
this.resizeAceEditors();
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* interventions
|
||||
* */
|
||||
initializeInterventionTimer: function() {
|
||||
|
||||
if ($('#editor').data('rfc-interventions') == true || $('#editor').data('break-interventions') == true) { // split in break or rfc intervention
|
||||
window.onblur = function() { window.blurred = true; };
|
||||
window.onfocus = function() { window.blurred = false; };
|
||||
|
||||
var delta = 100; // time in ms to wait for window event before time gets stopped
|
||||
var tid;
|
||||
$.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 minTimeIntervention = 10 * 60 * 1000;
|
||||
|
||||
if ((accumulatedWorkTimeUser - percentile75) > 0) {
|
||||
// working time is already over 75 percentile
|
||||
var timeUntilIntervention = minTimeIntervention;
|
||||
} else {
|
||||
// working time is less than 75 percentile
|
||||
// ensure we give user at least minTimeIntervention before we bother the user
|
||||
var timeUntilIntervention = Math.max(percentile75 - accumulatedWorkTimeUser, minTimeIntervention);
|
||||
}
|
||||
|
||||
tid = setInterval(function() {
|
||||
if ( window.blurred ) { return; }
|
||||
timeUntilIntervention -= delta;
|
||||
if ( timeUntilIntervention <= 0 ) {
|
||||
clearInterval(tid);
|
||||
// timeUntilIntervention passed
|
||||
if ($('#editor').data('break-interventions')) {
|
||||
$('#break-intervention-modal').modal('show');
|
||||
$.ajax({
|
||||
data: {
|
||||
intervention_type: 'BreakIntervention'
|
||||
},
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
url: $('#editor').data('intervention-save-url')
|
||||
});
|
||||
} else if ($('#editor').data('rfc-interventions')){
|
||||
var button = $('#requestComments');
|
||||
// only show intervention if user did not requested for a comment already
|
||||
if (!button.prop('disabled')) {
|
||||
$('#rfc_intervention_text').show();
|
||||
$('#comment-modal').modal('show');
|
||||
$.ajax({
|
||||
data: {
|
||||
intervention_type: 'QuestionIntervention'
|
||||
},
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
url: $('#editor').data('intervention-save-url')
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}, delta);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
initializeSearchButton: function(){
|
||||
$('#btn-search-col').button().click(function(){
|
||||
var search = $('#search-input-text').val();
|
||||
var course_token = $('#editor').data('course_token')
|
||||
var save_search_url = $('#editor').data('search-save-url')
|
||||
window.open("https://open.hpi.de/courses/" + course_token + "/pinboard?query=" + search, '_blank');
|
||||
// save search
|
||||
$.ajax({
|
||||
data: {
|
||||
search_text: search
|
||||
},
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
url: save_search_url});
|
||||
})
|
||||
|
||||
$('#sidebar-search-collapsed').on('click',this.handleSideBarToggle.bind(this));
|
||||
},
|
||||
|
||||
|
||||
initializeEverything: function() {
|
||||
this.initializeRegexes();
|
||||
@ -575,10 +683,14 @@ configureEditors: function () {
|
||||
this.initializeDescriptionToggle();
|
||||
this.initializeSideBarTooltips();
|
||||
this.initializeTooltips();
|
||||
this.initializeInterventionTimer();
|
||||
this.initializeSearchButton();
|
||||
this.initPrompt();
|
||||
this.renderScore();
|
||||
this.showFirstFile();
|
||||
|
||||
$(window).on("beforeunload", this.unloadAutoSave.bind(this));
|
||||
// create autosave when the editor is opened the first time
|
||||
this.autosave();
|
||||
}
|
||||
};
|
@ -6,6 +6,9 @@ CodeOceanEditorWebsocket = {
|
||||
sockURL.pathname = url;
|
||||
sockURL.protocol = '<%= DockerClient.config['ws_client_protocol'] %>';
|
||||
|
||||
// strip anchor if it is in the url
|
||||
sockURL.hash = ''
|
||||
|
||||
return sockURL.toString();
|
||||
},
|
||||
|
||||
|
@ -228,7 +228,8 @@ $(function() {
|
||||
}
|
||||
|
||||
if ($.isController('exercises')) {
|
||||
if ($('table').isPresent()) {
|
||||
// ignore tags table since it is in the dom before other tables
|
||||
if ($('table:not(#tags-table)').isPresent()) {
|
||||
enableBatchUpdate();
|
||||
} else if ($('.edit_exercise, .new_exercise').isPresent()) {
|
||||
execution_environments = $('form').data('execution-environments');
|
||||
|
@ -5,6 +5,7 @@ h1 {
|
||||
|
||||
.lead {
|
||||
font-size: 16px;
|
||||
color: rgba(70, 70, 70, 1);
|
||||
}
|
||||
|
||||
i.fa {
|
||||
|
@ -1,4 +1,5 @@
|
||||
#commentitor {
|
||||
margin-top: 2rem;
|
||||
height: 600px;
|
||||
background-color:#f9f9f9
|
||||
}
|
@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base
|
||||
MEMBER_ACTIONS = [:destroy, :edit, :show, :update]
|
||||
|
||||
after_action :verify_authorized, except: [:help, :welcome]
|
||||
before_action :set_locale
|
||||
before_action :set_locale, :allow_iframe_requests
|
||||
protect_from_forgery(with: :exception)
|
||||
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
|
||||
|
||||
@ -29,4 +29,8 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
def welcome
|
||||
end
|
||||
|
||||
def allow_iframe_requests
|
||||
response.headers.delete('X-Frame-Options')
|
||||
end
|
||||
end
|
||||
|
@ -49,7 +49,7 @@ class CommentsController < ApplicationController
|
||||
@comment = Comment.new(comment_params_without_request_id)
|
||||
|
||||
if comment_params[:request_id]
|
||||
UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user)
|
||||
UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user).deliver_now
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
|
@ -74,7 +74,12 @@ module Lti
|
||||
private :require_valid_consumer_key
|
||||
|
||||
def require_valid_exercise_token
|
||||
@exercise = Exercise.find_by(token: params[:custom_token])
|
||||
proxy_exercise = ProxyExercise.find_by(token: params[:custom_token])
|
||||
unless proxy_exercise.nil?
|
||||
@exercise = proxy_exercise.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
|
||||
end
|
||||
private :require_valid_exercise_token
|
||||
@ -129,19 +134,16 @@ module Lti
|
||||
private :set_current_user
|
||||
|
||||
def store_lti_session_data(options = {})
|
||||
exercise = Exercise.where(token: options[:parameters][:custom_token]).first
|
||||
exercise_id = exercise.id unless exercise.nil?
|
||||
|
||||
current_user = ExternalUser.find_or_create_by(consumer_id: options[:consumer].id, external_id: options[:parameters][:user_id].to_s)
|
||||
lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id,
|
||||
external_users_id: current_user.id,
|
||||
exercises_id: exercise_id)
|
||||
external_users_id: @current_user.id,
|
||||
exercises_id: @exercise.id)
|
||||
|
||||
lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).to_json
|
||||
lti_parameters.save!
|
||||
@lti_parameters = lti_parameters
|
||||
|
||||
session[:consumer_id] = options[:consumer].id
|
||||
session[:external_user_id] = current_user.id
|
||||
session[:external_user_id] = @current_user.id
|
||||
end
|
||||
private :store_lti_session_data
|
||||
|
||||
|
@ -25,7 +25,7 @@ module SubmissionScoring
|
||||
|
||||
def feedback_message(file, score)
|
||||
set_locale
|
||||
score == Assessor::MAXIMUM_SCORE ? I18n.t('exercises.implement.default_feedback') : file.feedback_message
|
||||
score == Assessor::MAXIMUM_SCORE ? I18n.t('exercises.implement.default_feedback') : render_markdown(file.feedback_message)
|
||||
end
|
||||
|
||||
def score_submission(submission)
|
||||
|
@ -40,7 +40,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
exercise_id,
|
||||
CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
exercise_id,
|
||||
|
@ -6,9 +6,10 @@ class ExercisesController < ApplicationController
|
||||
|
||||
before_action :handle_file_uploads, only: [:create, :update]
|
||||
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
|
||||
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit, :reload]
|
||||
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload]
|
||||
before_action :set_external_user, only: [:statistics]
|
||||
before_action :set_file_types, only: [:create, :edit, :new, :update]
|
||||
before_action :set_course_token, only: [:implement]
|
||||
|
||||
skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml]
|
||||
skip_after_action :verify_authorized, only: [:import_proforma_xml]
|
||||
@ -19,6 +20,15 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def max_intervention_count
|
||||
3
|
||||
end
|
||||
|
||||
|
||||
def java_course_token
|
||||
"702cbd2a-c84c-4b37-923a-692d7d1532d0"
|
||||
end
|
||||
|
||||
def batch_update
|
||||
@exercises = Exercise.all
|
||||
authorize!
|
||||
@ -54,6 +64,20 @@ class ExercisesController < ApplicationController
|
||||
|
||||
def create
|
||||
@exercise = Exercise.new(exercise_params)
|
||||
collect_set_and_unset_exercise_tags
|
||||
myparam = exercise_params
|
||||
checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
|
||||
for et in checked_exercise_tags
|
||||
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
|
||||
et.exercise = @exercise
|
||||
end
|
||||
|
||||
myparam[:exercise_tags] = checked_exercise_tags
|
||||
myparam.delete :tag_ids
|
||||
removed_exercise_tags.map {|et| et.destroy}
|
||||
|
||||
authorize!
|
||||
create_and_respond(object: @exercise)
|
||||
end
|
||||
@ -63,6 +87,7 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
def edit
|
||||
collect_set_and_unset_exercise_tags
|
||||
end
|
||||
|
||||
def import_proforma_xml
|
||||
@ -118,7 +143,8 @@ class ExercisesController < ApplicationController
|
||||
private :user_by_code_harbor_token
|
||||
|
||||
def exercise_params
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
params[:exercise][:expected_worktime_seconds] = params[:exercise][:expected_worktime_minutes].to_i * 60
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, :expected_worktime_seconds, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
private :exercise_params
|
||||
|
||||
@ -139,6 +165,22 @@ class ExercisesController < ApplicationController
|
||||
|
||||
def implement
|
||||
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
||||
user_solved_exercise = @exercise.has_user_solved(current_user)
|
||||
user_got_enough_interventions = UserExerciseIntervention.where(user: current_user).where("created_at >= ?", Time.zone.now.beginning_of_day).count >= max_intervention_count
|
||||
is_java_course = @course_token && @course_token.eql?(java_course_token)
|
||||
|
||||
user_intervention_group = UserGroupSeparator.getInterventionGroup(current_user)
|
||||
|
||||
case user_intervention_group
|
||||
when :no_intervention
|
||||
when :break_intervention
|
||||
@show_break_interventions = (user_solved_exercise || !is_java_course || user_got_enough_interventions) ? "false" : "true"
|
||||
when :rfc_intervention
|
||||
@show_rfc_interventions = (user_solved_exercise || !is_java_course || user_got_enough_interventions) ? "false" : "true"
|
||||
end
|
||||
|
||||
@search = Search.new
|
||||
@search.exercise = @exercise
|
||||
@submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
|
||||
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension)
|
||||
@paths = collect_paths(@files)
|
||||
@ -150,6 +192,59 @@ class ExercisesController < ApplicationController
|
||||
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["launch_presentation_return_url"]
|
||||
|
||||
@course_token =
|
||||
unless lti_json.nil?
|
||||
if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/)
|
||||
match.captures.first
|
||||
else
|
||||
java_course_token
|
||||
end
|
||||
else
|
||||
""
|
||||
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 search
|
||||
search_text = params[:search_text]
|
||||
search = Search.new(user: current_user, exercise: @exercise, search: search_text)
|
||||
|
||||
begin search.save
|
||||
render(json: {success: 'true'})
|
||||
rescue
|
||||
render(json: {success: 'false', error: "could not save search: #{$!}"})
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
@search = policy_scope(Exercise).search(params[:q])
|
||||
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page])
|
||||
@ -174,6 +269,8 @@ class ExercisesController < ApplicationController
|
||||
|
||||
def new
|
||||
@exercise = Exercise.new
|
||||
collect_set_and_unset_exercise_tags
|
||||
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -201,6 +298,16 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
private :set_file_types
|
||||
|
||||
def collect_set_and_unset_exercise_tags
|
||||
@search = policy_scope(Tag).search(params[:q])
|
||||
@tags = @search.result.order(:name)
|
||||
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
|
||||
end
|
||||
|
||||
@ -252,7 +359,20 @@ class ExercisesController < ApplicationController
|
||||
private :transmit_lti_score
|
||||
|
||||
def update
|
||||
update_and_respond(object: @exercise, params: exercise_params)
|
||||
collect_set_and_unset_exercise_tags
|
||||
myparam = exercise_params
|
||||
checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
|
||||
for et in checked_exercise_tags
|
||||
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
|
||||
et.exercise = @exercise
|
||||
end
|
||||
|
||||
myparam[:exercise_tags] = checked_exercise_tags
|
||||
myparam.delete :tag_ids
|
||||
removed_exercise_tags.map {|et| et.destroy}
|
||||
update_and_respond(object: @exercise, params: myparam)
|
||||
end
|
||||
|
||||
def redirect_after_submit
|
||||
@ -260,8 +380,12 @@ class ExercisesController < ApplicationController
|
||||
if @submission.normalized_score == 1.0
|
||||
# if user is external and has an own rfc, redirect to it and message him to clean up and accept the answer. (we need to check that the user is external,
|
||||
# otherwise an internal user could be shown a false rfc here, since current_user.id is polymorphic, but only makes sense for external users when used with rfcs.)
|
||||
# redirect 10 percent pseudorandomly to the feedback page
|
||||
if current_user.respond_to? :external_id
|
||||
if rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise, user_id: current_user.id).first
|
||||
if ((current_user.id + @submission.exercise.created_at.to_i) % 10 == 1)
|
||||
redirect_to_user_feedback
|
||||
return
|
||||
elsif rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise, user_id: current_user.id).first
|
||||
# set a message that informs the user that his own RFC should be closed.
|
||||
flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_own_rfc')
|
||||
flash.keep(:notice)
|
||||
@ -273,7 +397,7 @@ class ExercisesController < ApplicationController
|
||||
return
|
||||
|
||||
# else: show open rfc for same exercise if available
|
||||
elsif rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise).where.not(question: nil).order("RANDOM()").first
|
||||
elsif rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < 5) }
|
||||
# set a message that informs the user that his score was perfect and help in RFC is greatly appreciated.
|
||||
flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_rfc')
|
||||
flash.keep(:notice)
|
||||
@ -285,8 +409,25 @@ class ExercisesController < ApplicationController
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
# redirect to feedback page if score is less than 100 percent
|
||||
redirect_to_user_feedback
|
||||
return
|
||||
end
|
||||
redirect_to_lti_return_path
|
||||
end
|
||||
|
||||
def redirect_to_user_feedback
|
||||
url = if UserExerciseFeedback.find_by(exercise: @exercise, user: current_user)
|
||||
edit_user_exercise_feedback_path(user_exercise_feedback: {exercise_id: @exercise.id})
|
||||
else
|
||||
new_user_exercise_feedback_path(user_exercise_feedback: {exercise_id: @exercise.id})
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(url) }
|
||||
format.json { render(json: {redirect: url}) }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -27,7 +27,7 @@ class ExternalUsersController < ApplicationController
|
||||
score,
|
||||
id,
|
||||
CASE
|
||||
WHEN working_time >= '0:30:00' THEN '0'
|
||||
WHEN working_time >= '0:05:00' THEN '0'
|
||||
ELSE working_time
|
||||
END AS working_time_new
|
||||
FROM
|
||||
|
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
|
@ -11,12 +11,14 @@ class RequestForCommentsController < ApplicationController
|
||||
# GET /request_for_comments
|
||||
# GET /request_for_comments.json
|
||||
def index
|
||||
@request_for_comments = RequestForComment.last_per_user(2).order('created_at DESC').paginate(page: params[:page])
|
||||
@search = RequestForComment.last_per_user(2).search(params[:q])
|
||||
@request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def get_my_comment_requests
|
||||
@request_for_comments = RequestForComment.where(user_id: current_user.id).order('created_at DESC').paginate(page: params[:page])
|
||||
@search = RequestForComment.where(user_id: current_user.id).order('created_at DESC').search(params[:q])
|
||||
@request_for_comments = @search.result.paginate(page: params[:page])
|
||||
render 'index'
|
||||
end
|
||||
|
||||
@ -32,6 +34,10 @@ class RequestForCommentsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def submit
|
||||
|
||||
end
|
||||
|
||||
# GET /request_for_comments/1
|
||||
# GET /request_for_comments/1.json
|
||||
def show
|
||||
@ -63,6 +69,20 @@ class RequestForCommentsController < ApplicationController
|
||||
authorize!
|
||||
end
|
||||
|
||||
def create_comment_exercise
|
||||
old = UserExerciseFeedback.find_by(exercise_id: params[:exercise_id], user_id: current_user.id, user_type: current_user.class.name)
|
||||
if old
|
||||
old.delete
|
||||
end
|
||||
uef = UserExerciseFeedback.new(comment_params)
|
||||
|
||||
if uef.save
|
||||
render(json: {success: "true"})
|
||||
else
|
||||
render(json: {success: "false"})
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /request_for_comments/1
|
||||
# DELETE /request_for_comments/1.json
|
||||
def destroy
|
||||
@ -74,6 +94,10 @@ class RequestForCommentsController < ApplicationController
|
||||
authorize!
|
||||
end
|
||||
|
||||
def comment_params
|
||||
params.permit(:exercise_id, :feedback_text).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_request_for_comment
|
||||
@ -85,4 +109,5 @@ class RequestForCommentsController < ApplicationController
|
||||
# we are using the current_user.id here, since internal users are not able to create comments. The external_user.id is a primary key and does not require the consumer_id to be unique.
|
||||
params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,7 +1,7 @@
|
||||
class SessionsController < ApplicationController
|
||||
include Lti
|
||||
|
||||
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :require_valid_exercise_token].each do |method_name|
|
||||
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :set_current_user, :require_valid_exercise_token].each do |method_name|
|
||||
before_action(method_name, only: :create_through_lti)
|
||||
end
|
||||
|
||||
@ -18,7 +18,6 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
|
||||
def create_through_lti
|
||||
set_current_user
|
||||
store_lti_session_data(consumer: @consumer, parameters: params)
|
||||
store_nonce(params[:oauth_nonce])
|
||||
redirect_to(implement_exercise_path(@exercise),
|
||||
|
@ -13,6 +13,10 @@ class SubmissionsController < ApplicationController
|
||||
before_action :set_mime_type, only: [:download_file, :render_file]
|
||||
skip_before_action :verify_authenticity_token, only: [:download_file, :render_file]
|
||||
|
||||
def max_message_buffer_size
|
||||
500
|
||||
end
|
||||
|
||||
def authorize!
|
||||
authorize(@submission || @submissions)
|
||||
end
|
||||
@ -156,7 +160,7 @@ class SubmissionsController < ApplicationController
|
||||
tubesock.onmessage do |data|
|
||||
Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data)
|
||||
# Check whether the client send a JSON command and kill container
|
||||
# if the command is 'client_exit', send it to docker otherwise.
|
||||
# if the command is 'client_kill', send it to docker otherwise.
|
||||
begin
|
||||
parsed = JSON.parse(data)
|
||||
if parsed['cmd'] == 'client_kill'
|
||||
@ -183,21 +187,31 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def kill_socket(tubesock)
|
||||
# save the output of this "run" as a "testrun" (scoring runs are saved in submission_scoring.rb)
|
||||
save_run_output
|
||||
|
||||
# Hijacked connection needs to be notified correctly
|
||||
tubesock.send_data JSON.dump({'cmd' => 'exit'})
|
||||
tubesock.close
|
||||
end
|
||||
|
||||
def handle_message(message, tubesock, container)
|
||||
@message_buffer ||= ""
|
||||
# Handle special commands first
|
||||
if (/^exit/.match(message))
|
||||
kill_socket(tubesock)
|
||||
if (/^#exit/.match(message))
|
||||
# Just call exit_container on the docker_client.
|
||||
# Do not call kill_socket for the websocket to the client here.
|
||||
# @docker_client.exit_container closes the socket to the container,
|
||||
# kill_socket is called in the "on close handler" of the websocket to the container
|
||||
@docker_client.exit_container(container)
|
||||
elsif /^#timeout/.match(message)
|
||||
@message_buffer = 'timeout: ' + @message_buffer # add information that this run timed out to the buffer
|
||||
else
|
||||
# Filter out information about run_command, test_command, user or working directory
|
||||
run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename])
|
||||
test_command = @submission.execution_environment.test_command % command_substitutions(params[:filename])
|
||||
if !(/root|workspace|#{run_command}|#{test_command}/.match(message))
|
||||
@message_buffer += message if @message_buffer.size <= max_message_buffer_size
|
||||
parse_message(message, 'stdout', tubesock)
|
||||
end
|
||||
end
|
||||
@ -245,6 +259,13 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def save_run_output
|
||||
if !@message_buffer.blank?
|
||||
@message_buffer = @message_buffer[(0..max_message_buffer_size-1)] # trim the string to max_message_buffer_size chars
|
||||
Testrun.create(file: @file, submission: @submission, output: @message_buffer)
|
||||
end
|
||||
end
|
||||
|
||||
def score
|
||||
hijack do |tubesock|
|
||||
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
|
||||
|
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
|
115
app/controllers/user_exercise_feedbacks_controller.rb
Normal file
115
app/controllers/user_exercise_feedbacks_controller.rb
Normal file
@ -0,0 +1,115 @@
|
||||
class UserExerciseFeedbacksController < ApplicationController
|
||||
include CommonBehavior
|
||||
|
||||
before_action :set_user_exercise_feedback, only: [:edit, :update]
|
||||
|
||||
def comment_presets
|
||||
[[0,t('user_exercise_feedback.difficulty_easy')],
|
||||
[1,t('user_exercise_feedback.difficulty_some_what_easy')],
|
||||
[2,t('user_exercise_feedback.difficulty_ok')],
|
||||
[3,t('user_exercise_feedback.difficulty_some_what_difficult')],
|
||||
[4,t('user_exercise_feedback.difficult_too_difficult')]]
|
||||
end
|
||||
|
||||
def time_presets
|
||||
[[0,t('user_exercise_feedback.estimated_time_less_5')],
|
||||
[1,t('user_exercise_feedback.estimated_time_5_to_10')],
|
||||
[2,t('user_exercise_feedback.estimated_time_10_to_20')],
|
||||
[3,t('user_exercise_feedback.estimated_time_20_to_30')],
|
||||
[4,t('user_exercise_feedback.estimated_time_more_30')]]
|
||||
end
|
||||
|
||||
def authorize!
|
||||
authorize(@uef)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@exercise = Exercise.find(uef_params[:exercise_id])
|
||||
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first
|
||||
submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first rescue nil
|
||||
|
||||
if @exercise
|
||||
@uef = UserExerciseFeedback.new(uef_params)
|
||||
if validate_inputs(uef_params)
|
||||
authorize!
|
||||
path =
|
||||
if rfc && submission && submission.normalized_score == 1.0
|
||||
request_for_comment_path(rfc)
|
||||
else
|
||||
implement_exercise_path(@exercise)
|
||||
end
|
||||
create_and_respond(object: @uef, path: proc{path})
|
||||
else
|
||||
flash[:danger] = t('shared.message_failure')
|
||||
redirect_to(:back, id: uef_params[:exercise_id])
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def destroy
|
||||
destroy_and_respond(object: @tag)
|
||||
end
|
||||
|
||||
def edit
|
||||
@texts = comment_presets.to_a
|
||||
@times = time_presets.to_a
|
||||
authorize!
|
||||
end
|
||||
|
||||
def uef_params
|
||||
params[:user_exercise_feedback].permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
private :uef_params
|
||||
|
||||
def new
|
||||
@texts = comment_presets.to_a
|
||||
@times = time_presets.to_a
|
||||
@uef = UserExerciseFeedback.new
|
||||
@exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def update
|
||||
submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first rescue nil
|
||||
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first
|
||||
authorize!
|
||||
if @exercise && validate_inputs(uef_params)
|
||||
path =
|
||||
if rfc && submission && submission.normalized_score == 1.0
|
||||
request_for_comment_path(rfc)
|
||||
else
|
||||
implement_exercise_path(@exercise)
|
||||
end
|
||||
update_and_respond(object: @uef, params: uef_params, path: path)
|
||||
else
|
||||
flash[:danger] = t('shared.message_failure')
|
||||
redirect_to(:back, id: uef_params[:exercise_id])
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def set_user_exercise_feedback
|
||||
@exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id])
|
||||
@uef = UserExerciseFeedback.find_by(exercise_id: params[:user_exercise_feedback][:exercise_id], user: current_user)
|
||||
end
|
||||
|
||||
def validate_inputs(uef_params)
|
||||
begin
|
||||
if uef_params[:difficulty].to_i < 0 || uef_params[:difficulty].to_i >= comment_presets.size
|
||||
return false
|
||||
elsif uef_params[:user_estimated_worktime].to_i < 0 || uef_params[:user_estimated_worktime].to_i >= time_presets.size
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
rescue
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
end
|
@ -18,6 +18,6 @@ class UserMailer < ActionMailer::Base
|
||||
@commenting_user_displayname = commenting_user.displayname
|
||||
@comment_text = comment.text
|
||||
@rfc_link = request_for_comment_url(request_for_comment)
|
||||
mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email).deliver
|
||||
mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email)
|
||||
end
|
||||
end
|
||||
|
@ -8,6 +8,11 @@ module User
|
||||
has_many :exercises, as: :user
|
||||
has_many :file_types, as: :user
|
||||
has_many :submissions, as: :user
|
||||
has_many :user_proxy_exercise_exercises, as: :user
|
||||
has_many :user_exercise_interventions, as: :user
|
||||
has_many :interventions, through: :user_exercise_interventions
|
||||
accepts_nested_attributes_for :user_proxy_exercise_exercises
|
||||
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
|
||||
end
|
||||
@ -21,6 +26,6 @@ module User
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
displayname
|
||||
end
|
||||
end
|
||||
|
@ -12,6 +12,15 @@ class Exercise < ActiveRecord::Base
|
||||
belongs_to :execution_environment
|
||||
has_many :submissions
|
||||
|
||||
has_and_belongs_to_many :proxy_exercises
|
||||
has_many :user_proxy_exercise_exercises
|
||||
has_and_belongs_to_many :exercise_collections
|
||||
has_many :user_exercise_interventions
|
||||
has_many :interventions, through: :user_exercise_interventions
|
||||
has_many :exercise_tags
|
||||
has_many :tags, through: :exercise_tags
|
||||
accepts_nested_attributes_for :exercise_tags
|
||||
|
||||
has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions
|
||||
has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
|
||||
alias_method :users, :external_users
|
||||
@ -48,17 +57,21 @@ class Exercise < ActiveRecord::Base
|
||||
return user_count == 0 ? 0 : submissions.count() / user_count.to_f()
|
||||
end
|
||||
|
||||
def time_maximum_score(user)
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC, created_at ASC").first.created_at rescue Time.zone.at(0)
|
||||
end
|
||||
|
||||
def user_working_time_query
|
||||
"""
|
||||
SELECT user_id,
|
||||
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
|
||||
CASE WHEN working_time >= '0:05: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
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id=#{id}) AS foo) AS bar
|
||||
@ -66,6 +79,123 @@ class Exercise < ActiveRecord::Base
|
||||
"""
|
||||
end
|
||||
|
||||
def get_quantiles(quantiles)
|
||||
quantiles_str = "[" + quantiles.join(",") + "]"
|
||||
result = self.class.connection.execute("""
|
||||
WITH working_time AS
|
||||
(
|
||||
SELECT user_id,
|
||||
id,
|
||||
exercise_id,
|
||||
Max(score) AS max_score,
|
||||
(created_at - Lag(created_at) OVER (partition BY user_id, exercise_id ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{id}
|
||||
AND user_type = 'ExternalUser'
|
||||
GROUP BY user_id,
|
||||
id,
|
||||
exercise_id), max_points AS
|
||||
(
|
||||
SELECT context_id AS ex_id,
|
||||
Sum(weight) AS max_points
|
||||
FROM files
|
||||
WHERE context_type = 'Exercise'
|
||||
AND context_id = #{id}
|
||||
AND role = 'teacher_defined_test'
|
||||
GROUP BY context_id),
|
||||
-- filter for rows containing max points
|
||||
time_max_score AS
|
||||
(
|
||||
SELECT *
|
||||
FROM working_time W1,
|
||||
max_points MS
|
||||
WHERE w1.exercise_id = ex_id
|
||||
AND w1.max_score = ms.max_points),
|
||||
-- find row containing the first time max points
|
||||
first_time_max_score AS
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time,
|
||||
rn
|
||||
FROM (
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time,
|
||||
Row_number() OVER(partition BY user_id, exercise_id ORDER BY id ASC) AS rn
|
||||
FROM time_max_score) T
|
||||
WHERE rn = 1), times_until_max_points AS
|
||||
(
|
||||
SELECT w.id,
|
||||
w.user_id,
|
||||
w.exercise_id,
|
||||
w.max_score,
|
||||
w.working_time,
|
||||
m.id AS reachedmax_at
|
||||
FROM working_time W,
|
||||
first_time_max_score M
|
||||
WHERE w.user_id = m.user_id
|
||||
AND w.exercise_id = m.exercise_id
|
||||
AND w.id <= m.id),
|
||||
-- if user never makes it to max points, take all times
|
||||
all_working_times_until_max AS (
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time
|
||||
FROM times_until_max_points)
|
||||
UNION ALL
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time
|
||||
FROM working_time W1
|
||||
WHERE NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM first_time_max_score F
|
||||
WHERE f.user_id = w1.user_id
|
||||
AND f.exercise_id = w1.exercise_id))), filtered_times_until_max AS
|
||||
(
|
||||
SELECT user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
CASE
|
||||
WHEN working_time >= '0:05:00' THEN '0'
|
||||
ELSE working_time
|
||||
END AS working_time_new
|
||||
FROM all_working_times_until_max ), result AS
|
||||
(
|
||||
SELECT e.external_id AS external_user_id,
|
||||
f.user_id,
|
||||
exercise_id,
|
||||
Max(max_score) AS max_score,
|
||||
Sum(working_time_new) AS working_time
|
||||
FROM filtered_times_until_max f,
|
||||
external_users e
|
||||
WHERE f.user_id = e.id
|
||||
GROUP BY e.external_id,
|
||||
f.user_id,
|
||||
exercise_id )
|
||||
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
|
||||
FROM result
|
||||
""")
|
||||
if result.count > 0
|
||||
quantiles.each_with_index.map{|q,i| Time.parse(result[i]["unnest"]).seconds_since_midnight}
|
||||
else
|
||||
quantiles.map{|q| 0}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def retrieve_working_time_statistics
|
||||
@working_time_statistics = {}
|
||||
self.class.connection.execute(user_working_time_query).each do |tuple|
|
||||
@ -88,23 +218,64 @@ class Exercise < ActiveRecord::Base
|
||||
@working_time_statistics[user_id]["working_time"]
|
||||
end
|
||||
|
||||
def average_working_time_for_only(user_id)
|
||||
self.class.connection.execute("""
|
||||
SELECT sum(working_time_new) AS working_time
|
||||
FROM
|
||||
(SELECT CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT id,
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id=#{id} and user_id=#{user_id}) AS foo) AS bar
|
||||
""").first["working_time"]
|
||||
def accumulated_working_time_for_only(user)
|
||||
user_type = user.external_user? ? "ExternalUser" : "InternalUser"
|
||||
Time.parse(self.class.connection.execute("""
|
||||
WITH WORKING_TIME AS
|
||||
(SELECT user_id,
|
||||
id,
|
||||
exercise_id,
|
||||
max(score) AS max_score,
|
||||
(created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{id} AND user_id = #{user.id} AND user_type = '#{user_type}'
|
||||
GROUP BY user_id, id, exercise_id),
|
||||
MAX_POINTS AS
|
||||
(SELECT context_id AS ex_id, sum(weight) AS max_points FROM files WHERE context_type = 'Exercise' AND context_id = #{id} AND role = 'teacher_defined_test' GROUP BY context_id),
|
||||
|
||||
-- filter for rows containing max points
|
||||
TIME_MAX_SCORE AS
|
||||
(SELECT *
|
||||
FROM WORKING_TIME W1, MAX_POINTS MS
|
||||
WHERE W1.exercise_id = ex_id AND W1.max_score = MS.max_points),
|
||||
|
||||
-- find row containing the first time max points
|
||||
FIRST_TIME_MAX_SCORE AS
|
||||
( SELECT id,USER_id,exercise_id,max_score,working_time, rn
|
||||
FROM (
|
||||
SELECT id,USER_id,exercise_id,max_score,working_time,
|
||||
ROW_NUMBER() OVER(PARTITION BY user_id, exercise_id ORDER BY id ASC) AS rn
|
||||
FROM TIME_MAX_SCORE) T
|
||||
WHERE rn = 1),
|
||||
|
||||
TIMES_UNTIL_MAX_POINTS AS (
|
||||
SELECT W.id, W.user_id, W.exercise_id, W.max_score, W.working_time, M.id AS reachedmax_at
|
||||
FROM WORKING_TIME W, FIRST_TIME_MAX_SCORE M
|
||||
WHERE W.user_id = M.user_id AND W.exercise_id = M.exercise_id AND W.id <= M.id),
|
||||
|
||||
-- if user never makes it to max points, take all times
|
||||
ALL_WORKING_TIMES_UNTIL_MAX AS
|
||||
((SELECT id, user_id, exercise_id, max_score, working_time FROM TIMES_UNTIL_MAX_POINTS)
|
||||
UNION ALL
|
||||
(SELECT id, user_id, exercise_id, max_score, working_time FROM WORKING_TIME W1
|
||||
WHERE NOT EXISTS (SELECT 1 FROM FIRST_TIME_MAX_SCORE F WHERE F.user_id = W1.user_id AND F.exercise_id = W1.exercise_id))),
|
||||
|
||||
FILTERED_TIMES_UNTIL_MAX AS
|
||||
(
|
||||
SELECT user_id,exercise_id, max_score, CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM ALL_WORKING_TIMES_UNTIL_MAX
|
||||
)
|
||||
SELECT e.external_id AS external_user_id, f.user_id, exercise_id, MAX(max_score) AS max_score, sum(working_time_new) AS working_time
|
||||
FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e
|
||||
WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id
|
||||
""").first["working_time"]).seconds_since_midnight rescue 0
|
||||
end
|
||||
|
||||
def duplicate(attributes = {})
|
||||
exercise = dup
|
||||
exercise.attributes = attributes
|
||||
exercise_tags.each { |et| exercise.exercise_tags << et.dup }
|
||||
files.each { |file| exercise.files << file.dup }
|
||||
exercise
|
||||
end
|
||||
@ -162,8 +333,16 @@ class Exercise < ActiveRecord::Base
|
||||
end
|
||||
private :generate_token
|
||||
|
||||
def maximum_score
|
||||
files.teacher_defined_tests.sum(:weight)
|
||||
def maximum_score(user = nil)
|
||||
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
|
||||
|
||||
def has_user_solved(user)
|
||||
return maximum_score(user).to_i == maximum_score.to_i
|
||||
end
|
||||
|
||||
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
|
@ -5,8 +5,8 @@ class ExternalUser < ActiveRecord::Base
|
||||
validates :external_id, presence: true
|
||||
|
||||
def displayname
|
||||
result = name
|
||||
if(consumer.name == 'openHPI')
|
||||
result = "User " + id.to_s
|
||||
if(!consumer.nil? && consumer.name == 'openHPI')
|
||||
result = Rails.cache.fetch("#{cache_key}/displayname", expires_in: 12.hours) do
|
||||
Xikolo::UserClient.get(external_id.to_s)[:display_name]
|
||||
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
|
258
app/models/proxy_exercise.rb
Normal file
258
app/models/proxy_exercise.rb
Normal file
@ -0,0 +1,258 @@
|
||||
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
|
||||
matching_exercise =
|
||||
if (token.eql? "47f4c736")
|
||||
Rails.logger.debug("Proxy exercise with token 47f4c736, split user in groups..")
|
||||
group = UserGroupSeparator.getGroupWeek2Testing(user)
|
||||
Rails.logger.debug("user assigned to group #{group}")
|
||||
case group
|
||||
when :group_a
|
||||
exercises.where(id: 348).first
|
||||
when :group_b
|
||||
exercises.where(id: 349).first
|
||||
when :group_c
|
||||
exercises.where(id: 350).first
|
||||
when :group_d
|
||||
exercises.where(id: 351).first
|
||||
end
|
||||
else
|
||||
Rails.logger.debug("find new matching exercise for user #{user.id}" )
|
||||
begin
|
||||
find_matching_exercise(user)
|
||||
rescue #fallback
|
||||
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
|
||||
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)
|
||||
user_group = UserGroupSeparator.getProxyExerciseGroup(user)
|
||||
case user_group
|
||||
when :dummy_assigment
|
||||
rec_ex = select_easiest_exercise(exercises)
|
||||
@reason[:reason] = "dummy group"
|
||||
Rails.logger.debug("assigned user to dummy group, and gave him exercise: #{rec_ex.title}")
|
||||
rec_ex
|
||||
when :random_assigment
|
||||
@reason[:reason] = "random group"
|
||||
ex = exercises.where("expected_difficulty > 1").shuffle.first
|
||||
Rails.logger.debug("assigned user to random group, and gave him exercise: #{ex.title}")
|
||||
ex
|
||||
when :recommended_assignment
|
||||
exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq.compact
|
||||
tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten
|
||||
Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}")
|
||||
|
||||
# find exercises
|
||||
potential_recommended_exercises = []
|
||||
exercises.where("expected_difficulty > 1").each do |ex|
|
||||
## find exercises which have only tags the user has already seen
|
||||
if (ex.tags - tags_user_has_seen).empty?
|
||||
potential_recommended_exercises << ex
|
||||
end
|
||||
end
|
||||
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}")
|
||||
# if all exercises contain tags which the user has never seen, recommend easiest exercise
|
||||
if potential_recommended_exercises.empty?
|
||||
Rails.logger.debug("matched easiest exercise in pool")
|
||||
@reason[:reason] = "easiest exercise in pool. empty potential exercises"
|
||||
select_easiest_exercise(exercises)
|
||||
else
|
||||
recommended_exercise = select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
|
||||
recommended_exercise
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
private :find_matching_exercise
|
||||
|
||||
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)
|
||||
max_score = ex.maximum_score.to_f
|
||||
if max_score <= 0
|
||||
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: score: 0" )
|
||||
return 0.0
|
||||
end
|
||||
points_ratio = ex.maximum_score(user) / max_score
|
||||
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
|
11
app/models/user_exercise_feedback.rb
Normal file
11
app/models/user_exercise_feedback.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class UserExerciseFeedback < ActiveRecord::Base
|
||||
include Creation
|
||||
|
||||
belongs_to :exercise
|
||||
|
||||
validates :user_id, uniqueness: { scope: [:exercise_id, :user_type] }
|
||||
|
||||
def to_s
|
||||
"User Exercise Feedback"
|
||||
end
|
||||
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?}
|
||||
end
|
||||
|
||||
[:implement?, :submit?, :reload?].each do |action|
|
||||
[:implement?, :working_times?, :intervention?, :search?, :submit?, :reload?].each do |action|
|
||||
define_method(action) { everyone }
|
||||
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
|
@ -8,6 +8,10 @@ class RequestForCommentPolicy < ApplicationPolicy
|
||||
everyone
|
||||
end
|
||||
|
||||
def search?
|
||||
everyone
|
||||
end
|
||||
|
||||
def show?
|
||||
everyone
|
||||
end
|
||||
@ -27,4 +31,8 @@ class RequestForCommentPolicy < ApplicationPolicy
|
||||
def index?
|
||||
everyone
|
||||
end
|
||||
|
||||
def create_comment_exercise?
|
||||
everyone
|
||||
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
|
19
app/policies/user_exercise_feedback_policy.rb
Normal file
19
app/policies/user_exercise_feedback_policy.rb
Normal file
@ -0,0 +1,19 @@
|
||||
class UserExerciseFeedbackPolicy < ApplicationPolicy
|
||||
def author?
|
||||
@user == @record.author
|
||||
end
|
||||
private :author?
|
||||
|
||||
def create?
|
||||
everyone
|
||||
end
|
||||
|
||||
def new?
|
||||
everyone
|
||||
end
|
||||
|
||||
[:show? ,:destroy?, :edit?, :update?].each do |action|
|
||||
define_method(action) { admin? || author?}
|
||||
end
|
||||
|
||||
end
|
@ -1,3 +1,3 @@
|
||||
#flash data-message-failure=t('shared.message_failure')
|
||||
#flash.fixed_error_messages.clickthrough data-message-failure=t('shared.message_failure')
|
||||
- %w[alert danger info notice success warning].each do |severity|
|
||||
p.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)}" id="flash-#{severity}" = flash[severity]
|
||||
|
@ -5,5 +5,5 @@ textarea.form-control(style='resize:none;')
|
||||
h5 =t('exercises.implement.comment.others')
|
||||
pre#otherCommentsTextfield
|
||||
p = ''
|
||||
button#addCommentButton.btn.btn-block.btn-primary(type='button') =t('exercises.implement.comment.addComment')
|
||||
button#addCommentButton.btn.btn-block.btn-primary(type='button') =t('exercises.implement.comment.addCommentButton')
|
||||
button#removeAllButton.btn.btn-block.btn-warning(type='button') =t('exercises.implement.comment.removeAllOnLine')
|
@ -1,7 +1,9 @@
|
||||
- external_user_external_id = @current_user.respond_to?(:external_id) ? @current_user.external_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
|
||||
- external_user_id = @current_user.respond_to?(:external_id) ? @current_user.id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
|
||||
- consumer_id = @current_user.respond_to?(:external_id) ? @current_user.consumer_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
|
||||
#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_break_interventions = @show_break_interventions || "false"
|
||||
- show_rfc_interventions = @show_rfc_interventions || "false"
|
||||
#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path data-intervention-save-url=intervention_exercise_path data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path
|
||||
div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files)
|
||||
div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id )
|
||||
div id='frames' class='editor-col'
|
||||
@ -22,3 +24,4 @@
|
||||
|
||||
|
||||
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')
|
||||
= render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal')
|
@ -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-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.blank?
|
||||
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum'))
|
||||
|
||||
div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '')
|
||||
= 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,13 @@ div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '')
|
||||
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download'))
|
||||
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over'))
|
||||
|
||||
- if !@course_token.blank?
|
||||
.input-group.enforce-top-margin
|
||||
.enforce-right-margin
|
||||
= text_field_tag 'search-input-text', nil, placeholder: t('search.search_in_forum'), class: 'form-control'
|
||||
.input-group-btn
|
||||
= button_tag(class: 'btn btn-primary', id: 'btn-search-col') do
|
||||
i.fa.fa-search
|
||||
|
||||
- if @exercise.allow_file_creation?
|
||||
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))
|
@ -32,6 +32,25 @@
|
||||
label
|
||||
= f.check_box(:allow_auto_completion)
|
||||
= t('activerecord.attributes.exercise.allow_auto_completion')
|
||||
.form-group
|
||||
= f.label(t('activerecord.attributes.exercise.difficulty'))
|
||||
= f.number_field :expected_difficulty, in: 1..10, step: 1
|
||||
.form-group
|
||||
= f.label(t('activerecord.attributes.exercise.worktime'))
|
||||
= f.number_field "expected_worktime_minutes", value: @exercise.expected_worktime_seconds / 60, in: 1..1000, step: 1
|
||||
h2 Tags
|
||||
.table-responsive
|
||||
table.table#tags-table
|
||||
thead
|
||||
tr
|
||||
th = t('activerecord.attributes.exercise.selection')
|
||||
th = sort_link(@search, :title, t('activerecord.attributes.tag.name'))
|
||||
th = t('activerecord.attributes.tag.difficulty')
|
||||
= collection_check_boxes :exercise, :tag_ids, @exercise_tags, :tag_id, :id do |b|
|
||||
tr
|
||||
td = b.check_box
|
||||
td = b.object.tag.name
|
||||
td = number_field "tag_factors[#{b.object.tag.id}]", :factor, :value => b.object.factor, in: 1..10, step: 1
|
||||
h2 = t('activerecord.attributes.exercise.files')
|
||||
ul#files.list-unstyled.panel-group
|
||||
= f.fields_for :files do |files_form|
|
||||
|
@ -1,6 +1,11 @@
|
||||
h5#rfc_intervention_text style='display: none;' = t('exercises.implement.rfc_intervention.text')
|
||||
h5 = t('exercises.implement.comment.question')
|
||||
|
||||
|
||||
textarea.form-control#question(style='resize:none;')
|
||||
p = ''
|
||||
/ data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action.
|
||||
/ But if we use this button, it will work since the correct cause is supplied
|
||||
button#askForCommentsButton.btn.btn-block.btn-primary(type='button' data-cause='requestComments' data-message-success=t('exercises.editor.request_for_comments_sent')) =t('exercises.implement.comment.request')
|
||||
/ But if we use this button, it will work since the correct cause is supplied
|
||||
div
|
||||
button#askForCommentsButton.btn.btn-block.btn-primary(type='button' data-cause='requestComments' data-message-success=t('exercises.editor.request_for_comments_sent')) =t('exercises.implement.comment.request')
|
||||
button#closeAskForCommentsButton.btn.btn-block.btn-warning(type='button') =t('activerecord.attributes.request_for_comments.close')
|
||||
|
@ -1,6 +1,8 @@
|
||||
h1 = "#{@exercise} (external user #{@external_user})"
|
||||
- submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id).order("created_at")
|
||||
- current_submission = submissions.first
|
||||
- submissions_and_interventions = (submissions + UserExerciseIntervention.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id)).sort_by { |a| a.created_at }
|
||||
|
||||
- if current_submission
|
||||
- initial_files = current_submission.files.to_a
|
||||
|
||||
@ -41,20 +43,26 @@ h1 = "#{@exercise} (external user #{@external_user})"
|
||||
- ['.time', '.cause', '.score', '.tests', '.time_difference'].each do |title|
|
||||
th.header = t(title)
|
||||
tbody
|
||||
- deltas = submissions.map.with_index {|item, index| delta = item.created_at - submissions[index - 1].created_at if index > 0; if delta == nil or delta > 30*60 then 0 else delta end}
|
||||
- submissions.each_with_index do |submission, index|
|
||||
tr data-id=submission.id
|
||||
td.clickable = submission.created_at.strftime("%F %T")
|
||||
td = submission.cause
|
||||
td = submission.score
|
||||
td
|
||||
-submission.testruns.each do |run|
|
||||
- if run.passed
|
||||
.unit-test-result.positive-result title=run.output
|
||||
- else
|
||||
.unit-test-result.negative-result title=run.output
|
||||
td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0
|
||||
-working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0))
|
||||
- deltas = submissions.map.with_index {|item, index| delta = item.created_at - submissions[index - 1].created_at if index > 0; if delta == nil or delta > 10*60 then 0 else delta end}
|
||||
- submissions_and_interventions.each_with_index do |submission_or_intervention, index|
|
||||
tr data-id=submission_or_intervention.id
|
||||
td.clickable = submission_or_intervention.created_at.strftime("%F %T")
|
||||
- if submission_or_intervention.is_a?(Submission)
|
||||
td = submission_or_intervention.cause
|
||||
td = submission_or_intervention.score
|
||||
td
|
||||
-submission_or_intervention.testruns.each do |run|
|
||||
- if run.passed
|
||||
.unit-test-result.positive-result title=run.output
|
||||
- else
|
||||
.unit-test-result.negative-result title=run.output
|
||||
td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0
|
||||
-working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0))
|
||||
- elsif submission_or_intervention.is_a? UserExerciseIntervention
|
||||
td = submission_or_intervention.intervention.name
|
||||
td =
|
||||
td =
|
||||
td =
|
||||
p = t('.addendum')
|
||||
.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until);
|
||||
div#progress_chart.col-lg-12
|
||||
|
@ -22,3 +22,4 @@
|
||||
#questions-column
|
||||
#questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}"
|
||||
= 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 = t('.test_files')
|
||||
th = t('activerecord.attributes.exercise.maximum_score')
|
||||
th = t('activerecord.attributes.exercise.tags')
|
||||
th = t('activerecord.attributes.exercise.difficulty')
|
||||
th = t('activerecord.attributes.exercise.worktime')
|
||||
th
|
||||
= t('activerecord.attributes.exercise.public')
|
||||
- if policy(Exercise).batch_update?
|
||||
@ -29,6 +32,9 @@ h1 = Exercise.model_name.human(count: 2)
|
||||
td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
|
||||
td = exercise.files.teacher_defined_tests.count
|
||||
td = exercise.maximum_score
|
||||
td = exercise.exercise_tags.count
|
||||
td = exercise.expected_difficulty
|
||||
td = (exercise.expected_worktime_seconds / 60).ceil
|
||||
td.public data-value=exercise.public? = symbol_for(exercise.public?)
|
||||
td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit?
|
||||
td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement?
|
||||
|
@ -19,6 +19,9 @@ h1
|
||||
= row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?)
|
||||
= row(label: 'exercise.embedding_parameters') do
|
||||
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))
|
||||
= row(label: 'exercise.difficulty', value: @exercise.expected_difficulty)
|
||||
= row(label: 'exercise.worktime', value: "#{@exercise.expected_worktime_seconds/60} min")
|
||||
= row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", "))
|
||||
|
||||
h2 = t('activerecord.attributes.exercise.files')
|
||||
|
||||
|
@ -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)
|
@ -1,12 +1,20 @@
|
||||
h1 = RequestForComment.model_name.human(count: 2)
|
||||
|
||||
= render(layout: 'shared/form_filters') do |f|
|
||||
.form-group
|
||||
= f.label(:exercise_title_cont, t('activerecord.attributes.request_for_comments.exercise'), class: 'sr-only')
|
||||
= f.search_field(:exercise_title_cont, class: 'form-control', placeholder: t('activerecord.attributes.request_for_comments.exercise'))
|
||||
.form-group
|
||||
= f.label(:title_cont, t('request_for_comments.solved'), class: 'sr-only')
|
||||
= f.select(:solved_not_eq, [[t('request_for_comments.show_all'), 2], [t('request_for_comments.show_unsolved'), 1], [t('request_for_comments.show_solved'), 0]])
|
||||
|
||||
.table-responsive
|
||||
table.table.sortable
|
||||
thead
|
||||
tr
|
||||
th
|
||||
i class="fa fa-lightbulb-o" aria-hidden="true" title = t('request_for_comments.solved') align="right"
|
||||
th = t('activerecord.attributes.request_for_comments.exercise')
|
||||
th = sort_link(@search, :title, t('activerecord.attributes.request_for_comments.exercise'))
|
||||
th = t('activerecord.attributes.request_for_comments.question')
|
||||
th
|
||||
i class="fa fa-comment" aria-hidden="true" title = t('request_for_comments.comments') align="center"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="list-group">
|
||||
<h4 id ="exercise_caption" class="list-group-item-heading" data-rfc-id = "<%= @request_for_comment.id %>" ><%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %></h4>
|
||||
<h4 id ="exercise_caption" class="list-group-item-heading" data-exercise-id="<%=@request_for_comment.exercise.id%>" data-comment-exercise-url="<%=create_comment_exercise_request_for_comment_path%>" data-rfc-id = "<%= @request_for_comment.id %>" ><%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %></h4>
|
||||
<p class="list-group-item-text">
|
||||
<%
|
||||
user = @request_for_comment.user
|
||||
@ -20,14 +20,17 @@
|
||||
<u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> <%= t('request_for_comments.no_question') %>
|
||||
<% end %>
|
||||
</h5>
|
||||
|
||||
<% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %>
|
||||
<button class="btn btn-default" id="mark-as-solved-button"><%= t('request_for_comments.mark_as_solved') %></button>
|
||||
<button class="btn btn-primary" id="mark-as-solved-button"><%= t('request_for_comments.mark_as_solved') %></button>
|
||||
<% elsif (@request_for_comment.solved?) %>
|
||||
<button type="button" class="btn btn-success"><%= t('request_for_comments.solved') %></button>
|
||||
<button type="button" class="btn btn-success"><%= t('request_for_comments.solved') %></button>
|
||||
<% else %>
|
||||
|
||||
<% end %>
|
||||
|
||||
|
||||
|
||||
<% if @current_user.admin? && user.is_a?(ExternalUser) %>
|
||||
<br>
|
||||
<br>
|
||||
@ -41,14 +44,18 @@
|
||||
</ul>
|
||||
</h5>
|
||||
<% end %>
|
||||
<h5>
|
||||
<u><%= t('request_for_comments.howto_title') %></u><br> <%= render_markdown(t('request_for_comments.howto')) %>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<!--
|
||||
do not put a carriage return in the line below. it will be present in the presentation of the source code, otherwise.
|
||||
also, all settings from the rails model needed for the editor configuration in the JavaScript are attached to the editor as data attributes here.
|
||||
-->
|
||||
<% submission.files.each do |file| %>
|
||||
<%= (file.path or "") + "/" + file.name + file.file_type.file_extension %>
|
||||
<%= (file.path or "") + "/" + file.name + file.file_type.file_extension %><br>
|
||||
<i class="fa fa-arrow-down" aria-hidden="true"></i> <%= t('request_for_comments.click_here') %>
|
||||
<div id='commentitor' class='editor' data-read-only='true' data-file-id='<%=file.id%>' data-mode='<%=file.file_type.editor_mode%>'><%= file.content %>
|
||||
</div>
|
||||
<% end %>
|
||||
@ -58,6 +65,8 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
<script type="text/javascript">
|
||||
|
||||
var solvedButton = $('#mark-as-solved-button');
|
||||
var commentOnExerciseButton = $('#comment-exercise-button');
|
||||
var addCommentExerciseButton = $('#addCommentExerciseButton')
|
||||
|
||||
solvedButton.on('click', function(event){
|
||||
var jqrequest = $.ajax({
|
||||
@ -107,7 +116,14 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
jqrequest.done(function(response){
|
||||
$.each(response, function(index, comment) {
|
||||
comment.className = "code-ocean_comment";
|
||||
comment.text = comment.username + ": " + comment.text
|
||||
|
||||
// if we have tabs or carriage returns in the comment, just add the name and leave it as it is. otherwise: format!
|
||||
if(comment.text.includes("\n") || comment.text.includes("\t")){
|
||||
comment.text = comment.username + ": " + comment.text;
|
||||
} else {
|
||||
comment.text = comment.username + ": " + stringDivider(comment.text, 80, "\n\t\t");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
session.setAnnotations(response);
|
||||
@ -161,6 +177,27 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
function createCommentOnExercise(file_id, row, editor, commenttext){
|
||||
var jqxhr = $.ajax({
|
||||
data: {
|
||||
comment: {
|
||||
file_id: file_id,
|
||||
row: row,
|
||||
column: 0,
|
||||
text: commenttext,
|
||||
request_id: $('h4#exercise_caption').data('rfc-id')
|
||||
}
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'POST',
|
||||
url: "/comments"
|
||||
});
|
||||
jqxhr.done(function(response){
|
||||
setAnnotations(editor, file_id);
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
function handleSidebarClick(e) {
|
||||
var target = e.domEvent.target;
|
||||
var editor = e.editor;
|
||||
@ -193,6 +230,7 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
|
||||
if (commenttext !== "") {
|
||||
createComment(file_id, row, editor, commenttext);
|
||||
commentModal.find('textarea').val('') ;
|
||||
commentModal.modal('hide');
|
||||
}
|
||||
});
|
||||
@ -213,4 +251,20 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
text: message.length > 0 ? message : $('#flash').data('message-failure')
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function stringDivider(str, width, spaceReplacer) {
|
||||
if (str.length>width) {
|
||||
var p=width
|
||||
for (;p>0 && str[p]!=' ';p--) {
|
||||
}
|
||||
if (p>0) {
|
||||
var left = str.substring(0, p);
|
||||
var right = str.substring(p+1);
|
||||
return left + spaceReplacer + stringDivider(right, width, spaceReplacer);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
0
app/views/searches/destroy.html.erb
Normal file
0
app/views/searches/destroy.html.erb
Normal file
@ -5,6 +5,6 @@
|
||||
= row(label: 'file.hidden', value: file.hidden)
|
||||
= row(label: 'file.read_only', value: file.read_only)
|
||||
- if file.teacher_defined_test?
|
||||
= row(label: 'file.feedback_message', value: file.feedback_message)
|
||||
= row(label: 'file.feedback_message', value: render_markdown(file.feedback_message))
|
||||
= row(label: 'file.weight', value: file.weight)
|
||||
= row(label: 'file.content', value: file.native_file? ? link_to(file.native_file.file.filename, file.native_file.url) : code_tag(file.content))
|
||||
|
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)
|
23
app/views/user_exercise_feedbacks/_form.html.slim
Normal file
23
app/views/user_exercise_feedbacks/_form.html.slim
Normal file
@ -0,0 +1,23 @@
|
||||
= form_for(@uef) do |f|
|
||||
div
|
||||
span.badge.pull-right.score
|
||||
|
||||
h1 id="exercise-headline"
|
||||
= t('activerecord.models.user_exercise_feedback.one') + " "
|
||||
= link_to(@exercise.title, [:implement, @exercise])
|
||||
= render('shared/form_errors', object: @uef)
|
||||
h4
|
||||
== t('user_exercise_feedback.description')
|
||||
#description-panel.lead.description-panel
|
||||
u = t('activerecord.attributes.exercise.description')
|
||||
= render_markdown(@exercise.description)
|
||||
.form-group
|
||||
= f.text_area(:feedback_text, class: 'form-control', required: true, :rows => "10")
|
||||
h4 = t('user_exercise_feedback.difficulty')
|
||||
= f.collection_radio_buttons :difficulty, @texts, :first, :last, html_options={class: "radio-inline"} do |b|
|
||||
= b.label(:class => 'radio') { b.radio_button + b.text }
|
||||
h4 = t('user_exercise_feedback.working_time')
|
||||
= f.collection_radio_buttons :user_estimated_worktime, @times, :first, :last, html_options={class: "radio-inline"} do |b|
|
||||
= b.label(:class => 'radio') { b.radio_button + b.text }
|
||||
= f.hidden_field(:exercise_id, :value => @exercise.id)
|
||||
.actions = render('shared/submit_button', f: f, object: @uef)
|
1
app/views/user_exercise_feedbacks/edit.html.slim
Normal file
1
app/views/user_exercise_feedbacks/edit.html.slim
Normal file
@ -0,0 +1 @@
|
||||
= render('form')
|
1
app/views/user_exercise_feedbacks/new.html.slim
Normal file
1
app/views/user_exercise_feedbacks/new.html.slim
Normal file
@ -0,0 +1 @@
|
||||
= render('form')
|
@ -1 +1 @@
|
||||
== t('mailers.user_mailer.got_new_comment.body', receiver_displayname: @receiver_displayname, link: link_to(@rfc_link, @rfc_link), commenting_user_displayname: @commenting_user_displayname, comment_text: @comment_text)
|
||||
== t('mailers.user_mailer.got_new_comment.body', receiver_displayname: @receiver_displayname, link_to_comment: link_to(@rfc_link, @rfc_link), commenting_user_displayname: @commenting_user_displayname, comment_text: @comment_text, link_my_comments: link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_url), link_all_comments: link_to(t('request_for_comments.index.all'), request_for_comments_url) )
|
||||
|
Reference in New Issue
Block a user