Merge branch 'master' into client-routesv2

This commit is contained in:
Niklas Kiefer
2017-04-25 13:50:08 +02:00
99 changed files with 2222 additions and 109 deletions

View File

@ -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();
}
};

View File

@ -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();
},

View File

@ -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');

View File

@ -5,6 +5,7 @@ h1 {
.lead {
font-size: 16px;
color: rgba(70, 70, 70, 1);
}
i.fa {

View File

@ -1,4 +1,5 @@
#commentitor {
margin-top: 2rem;
height: 600px;
background-color:#f9f9f9
}

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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

View File

@ -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

View 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

View 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

View File

@ -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

View File

@ -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),

View File

@ -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?

View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
class ExerciseCollection < ActiveRecord::Base
has_and_belongs_to_many :exercises
end

View 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

View File

@ -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

View 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

View 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
View 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
View 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

View 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

View 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

View 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

View File

@ -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

View 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

View 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

View File

@ -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

View 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

View 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

View 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

View File

@ -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]

View File

@ -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')

View File

@ -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')

View File

@ -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'))

View 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|

View File

@ -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')

View File

@ -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

View File

@ -22,3 +22,4 @@
#questions-column
#questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}"
= qa_js_tag

View File

@ -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?

View File

@ -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')

View File

@ -0,0 +1 @@
h5 = t('exercises.implement.break_intervention.text')

View 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)

View 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)

View File

@ -0,0 +1,4 @@
h1
= @intervention.name
= row(label: 'intervention.name', value: @intervention.name)

View 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)

View File

@ -0,0 +1,3 @@
h1 = t('activerecord.models.proxy_exercise.one', model: ProxyExercise.model_name.human)+ ": " + @proxy_exercise.title
= render('form')

View 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)

View File

@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: ProxyExercise.model_name.human)
= render('form')

View File

@ -0,0 +1,3 @@
json.set! :files do
json.array! @exercise.files.visible, :content, :id
end

View 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)

View File

@ -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"

View File

@ -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>
&nbsp;&nbsp;<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>

View File

View 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))

View 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)

View File

@ -0,0 +1,3 @@
h1 = @tag.name
= render('form')

View 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)

View File

@ -0,0 +1,3 @@
h1 = t('shared.new_model', model: Tag.model_name.human)
= render('form')

View 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)

View 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)

View File

@ -0,0 +1 @@
= render('form')

View File

@ -0,0 +1 @@
= render('form')

View File

@ -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) )