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

View File

@ -28,7 +28,7 @@ module CodeOcean
config.eager_load_paths << Rails.root.join('lib')
config.assets.precompile += %w( markdown-buttons.png )
config.active_record.schema_format = :sql
#config.active_record.schema_format = :sql
case (RUBY_ENGINE)
when 'ruby'

View File

@ -4,7 +4,7 @@ set :default_env, 'PATH' => '/usr/java/jdk1.8.0_40/bin:$PATH'
set :deploy_to, '/var/www/app'
set :keep_releases, 3
set :linked_dirs, %w(log public/uploads tmp/cache tmp/files tmp/pids tmp/sockets)
set :linked_files, %w(config/action_mailer.yml config/code_ocean.yml config/database.yml config/newrelic.yml config/secrets.yml config/sendmail.yml config/smtp.yml)
set :linked_files, %w(config/action_mailer.yml config/docker.yml.erb config/code_ocean.yml config/database.yml config/newrelic.yml config/secrets.yml config/sendmail.yml config/smtp.yml)
set :log_level, :info
set :puma_threads, [0, 16]
set :repo_url, 'git@github.com:openHPI/codeocean.git'

View File

@ -32,7 +32,7 @@ production:
timeout: 60
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
ws_host: ws://localhost:4243 #url to connect rails server to docker host
ws_client_protocol: wss:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
ws_client_protocol: 'wss:' #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
staging:
<<: *default
@ -46,7 +46,7 @@ staging:
timeout: 60
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
ws_host: ws://localhost:4243 #url to connect rails server to docker host
ws_client_protocol: wss:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
ws_client_protocol: 'wss:' #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
test:
<<: *default

View File

@ -27,6 +27,7 @@ de:
exercise:
description: Beschreibung
embedding_parameters: Parameter für LTI-Einbettung
tags: Tags
execution_environment: Ausführungsumgebung
execution_environment_id: Ausführungsumgebung
files: Dateien
@ -34,10 +35,16 @@ de:
instructions: Anweisungen
maximum_score: Erreichbare Punktzahl
public: Öffentlich
selection: Ausgewählt
title: Titel
user: Autor
allow_auto_completion: "Autovervollständigung aktivieren"
allow_file_creation: "Dateierstellung erlauben"
difficulty: Schwierigkeitsgrad
worktime: "vermutete Arbeitszeit in Minuten"
proxy_exercise:
title: Title
files_count: Anzahl der Aufgaben
external_user:
consumer: Konsument
email: E-Mail
@ -68,6 +75,8 @@ de:
message: Nachricht
name: Name
regular_expression: Regulärer Ausdruck
intervention:
name: Name
internal_user:
activated: Aktiviert
consumer: Konsument
@ -84,6 +93,7 @@ de:
username: Benutzername
requested_at: Angefragezeitpunkt
question: "Frage"
close: "Fenster schließen"
submission:
cause: Anlass
code: Code
@ -91,6 +101,10 @@ de:
files: Dateien
score: Punktzahl
user: Autor
tag:
name: Name
usage: Verwendet
difficulty: Anteil an der Aufgabe
file_template:
name: "Name"
file_type: "Dateityp"
@ -111,6 +125,9 @@ de:
exercise:
one: Aufgabe
other: Aufgaben
proxy_exercise:
one: Proxy Aufgabe
other: Proxy Aufgaben
external_user:
one: Externer Nutzer
other: Externe Nutzer
@ -129,9 +146,15 @@ de:
internal_user:
one: Interner Nutzer
other: Interne Nutzer
request_for_comment:
one: Kommentaranfrage
other: Kommentaranfragen
submission:
one: Abgabe
other: Abgaben
user_exercise_feedback:
one: Feedback
other: Feedback
errors:
messages:
together: 'muss zusammen mit %{attribute} definiert werden'
@ -254,12 +277,19 @@ de:
line: Zeile
dialogtitle: Kommentar hinzufügen
others: Andere Kommentare auf dieser Zeile
addCommentExercise: Aufgabe kommentieren
addyours: Fügen Sie Ihren Kommentar hinzu
addComment: Kommentieren
addComment: Hier haben Sie die Möglichkeit Ihr Feedback zu dieser Aufgabe zu geben. Dies bezieht sich ausdrücklich NICHT auf die hier sichtbare Lösung des Teilnehmers, sondern nur auf die Aufgabe. Fanden Sie die Aufgabe zu leicht oder schwer? War die Beschreibung der Aufgabe verständlich und vollständig?
addCommentButton: Kommentar abschicken
removeAllOnLine: Meine Kommentare auf dieser Zeile löschen
listing: Die neuesten Kommentaranfragen
request: "Kommentaranfrage stellen"
question: "Bitte beschreiben Sie kurz ihre Problem oder nennen Sie den Programmteil, zu dem sie Feedback wünschen."
question: "Bitte beschreiben Sie kurz ihre Probleme oder nennen Sie den Programmteil, zu dem Sie Feedback wünschen."
rfc_intervention:
text: "Es scheint so als würden Sie Probleme mit der Aufgabe haben. Wenn Sie möchten, können wir Ihnen helfen!"
break_intervention:
title: "Pause"
text: "Uns ist aufgefallen, dass du schon lange an dieser Aufgabe arbeitest. Möchtest du vielleicht später weiter machen um erstmal auf neue Gedanken zu kommen?"
index:
clone: Duplizieren
implement: Implementieren
@ -290,6 +320,9 @@ de:
tests: Unit Tests
time_difference: 'Arbeitszeit bis hier*'
addendum: '* Differenzen von mehr als 30 Minuten werden ignoriert.'
proxy_exercises:
index:
clone: Duplizieren
external_users:
statistics:
title: Statistiken für Externe Benutzer
@ -327,6 +360,8 @@ de:
success: Sie haben Ihr Passwort erfolgreich geändert.
show:
link: Profil
search:
search_in_forum: "Probleme? Suche hier im Forum"
locales:
de: Deutsch
en: Englisch
@ -343,7 +378,15 @@ de:
Hallo %{receiver_displayname}, <br>
<br>
es gibt einen neuen Kommentar von %{commenting_user_displayname} zu Ihrer Kommentaranfrage auf CodeOcean. <br>
Sie finden ihn hier: %{link} <br>
<br>
%{commenting_user_displayname} schreibt: %{comment_text}<br>
<br>
Sie finden ihre Kommentaranfrage hier: %{link_to_comment} <br>
<br>
Falls Sie beim Klick auf diesen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.<br>
<br>
Eine Übersicht Ihrer Kommentaranfragen gibt es hier: %{link_my_comments} <br>
Alle Kommentaranfragen aller Benutzer finden Sie hier: %{link_all_comments} <br>
<br>
Diese Mail wurde automatisch von CodeOcean verschickt.<br>
<br>
@ -352,7 +395,15 @@ de:
Dear %{receiver_displayname}, <br>
<br>
you received a new comment from %{commenting_user_displayname} to your request for comments on CodeOcean. <br>
You can find it here: %{link} <br>
<br>
%{commenting_user_displayname} wrote: %{comment_text} <br>
<br>
You can find your request for comments here: %{link_to_comment} <br>
<br>
If you receive an error that you are not authorized to perform this action when clicking the link, please log-in through any course exercise beforehand and click the link again. <br>
<br>
An overview of all your comments can be accessed here: %{link_my_comments} <br>
All comments of all participants are available here: %{link_all_comments} <br>
<br>
This mail was automatically sent by CodeOcean. <br>
subject: Sie haben einen neuen Kommentar von %{commenting_user_displayname} auf CodeOcean erhalten.
@ -360,13 +411,23 @@ de:
body: 'Bitte besuchen Sie %{link}, sofern Sie Ihr Passwort zurücksetzen wollen.'
subject: Anweisungen zum Zurücksetzen Ihres Passworts
request_for_comments:
click_here: Zum Kommentieren auf die Seitenleiste klicken!
comments: Kommentare
howto: |
Um Kommentare zu einer Programmzeile hinzuzufügen, kann einfach auf die jeweilige Zeilennummer auf der linken Seite geklickt werden. <br>
Es öffnet sich ein Textfeld, in dem der Kommentar eingetragen werden kann. <br>
Mit "Kommentieren" wird der Kommentar dann gesichert und taucht als Sprechblase neben der Zeile auf.
howto_title: 'Anleitung:'
index:
get_my_comment_requests: Meine Kommentaranfragen
all: "Alle Kommentaranfragen"
no_question: "Der Autor hat keine Frage zu dieser Anfrage gestellt."
mark_as_solved: "Diese Frage als beantwortet markieren"
show_all: "Alle Anfragen anzeigen"
show_solved: "Nur gelöste Anfragen anzeigen"
show_unsolved: "Nur ungelöste Anfragen anzeigen"
solved: "Diese Frage wurde erfolgreich beantwortet"
comment_exercise: "Ich möchte die Aufgabenstellung kommentieren"
sessions:
create:
failure: Fehlerhafte E-Mail oder Passwort.
@ -467,3 +528,18 @@ de:
previous_label: '&#8592; Vorherige Seite'
file_template:
no_template_label: "Leere Datei"
user_exercise_feedback:
difficulty_easy: "Die Aufgabe war zu einfach"
difficulty_some_what_easy: "Die Aufgabe war etwas zu einfach"
difficulty_ok: "Die Aufgabe war gut zu lösen"
difficulty_some_what_difficult: "Die Aufgabe war etwas zu schwer"
difficult_too_difficult: "Die Aufgabe war zu schwer"
difficulty: "Schwierigkeit der Aufgabe:"
description: "Ihre Punkte wurden übertragen. Wir würden uns freuen, wenn Sie uns hier Feedback zur Aufgabe geben.<br> Wenn sie das nicht möchten, können Sie das Fenster auch einfach schließen.<br><br>Bitte beschreiben Sie, was Ihnen an der Aufgabe gefallen hat und was nicht. Gab es Schwierigkeiten bei der Aufgabe? War die Aufgabe zu leicht oder zu schwer?<br>Wir freuen uns über jedes Feedback."
estimated_time_less_5: "weniger als 5 Minuten"
estimated_time_5_to_10: "zwischen 5 und 10 Minuten"
estimated_time_10_to_20: "zwischen 10 und 20 Minuten"
estimated_time_20_to_30: "zwischen 20 und 30 Minuten"
estimated_time_more_30: "mehr als 30 Minuten"
working_time: "Geschätze Bearbeitungszeit für diese Aufgabe:"

View File

@ -48,6 +48,7 @@ en:
exercise:
description: Description
embedding_parameters: LTI Embedding Parameters
tags: Tags
execution_environment: Execution Environment
execution_environment_id: Execution Environment
files: Files
@ -55,10 +56,16 @@ en:
instructions: Instructions
maximum_score: Maximum Score
public: Public
selection: Selected
title: Title
user: Author
allow_auto_completion: "Allow auto completion"
allow_file_creation: "Allow file creation"
difficulty: Difficulty
worktime: "Expected worktime in minutes"
proxy_exercise:
title: Title
files_count: Exercises Count
external_user:
consumer: Consumer
email: Email
@ -89,6 +96,8 @@ en:
message: Message
name: Name
regular_expression: Regular Expression
intervention:
name: Name
internal_user:
activated: Activated
consumer: Consumer
@ -105,6 +114,7 @@ en:
username: Username
requested_at: Request Date
question: "Question"
close: Close window
submission:
cause: Cause
code: Code
@ -112,6 +122,10 @@ en:
files: Files
score: Score
user: Author
tag:
name: Name
usage: Used
difficulty: Share on the Exercise
file_template:
name: "Name"
file_type: "File Type"
@ -132,6 +146,9 @@ en:
exercise:
one: Exercise
other: Exercises
proxy_exercise:
one: Proxy Exercise
other: Proxy Exercises
external_user:
one: External User
other: External Users
@ -150,9 +167,15 @@ en:
internal_user:
one: Internal User
other: Internal Users
request_for_comment:
one: Request for Comments
other: Requests for Comments
submission:
one: Submission
other: Submissions
user_exercise_feedback:
one: Feedback
other: Feedback
errors:
messages:
together: 'has to be set along with %{attribute}'
@ -276,11 +299,18 @@ en:
dialogtitle: Comment on this line
others: Other comments on this line
addyours: Add your comment
addComment: Comment this
addCommentExercise: Comment this exercise
addComment: You can give feedback to this exercise. Keep in mind that this should refer to the exercise and not to the solution of the participant. Did you find this exercise particulary easy or difficult? Was the description sufficient?
addCommentButton: Comment this
removeAllOnLine: Remove my comments on this line
listing: Listing the newest comment requests
request: "Request Comments"
question: "Please shortly describe your problem or the program part you would like to get feedback for."
rfc_intervention:
text: "It looks like you may struggle with this exercise. If you like we can help you out!"
break_intervention:
title: "Break"
text: "We recognized that you are already working quite a while on this exercise. We would like to encourage you to take a break and come back later."
index:
clone: Duplicate
implement: Implement
@ -311,6 +341,9 @@ en:
tests: Unit Test Results
time_difference: 'Working Time until here*'
addendum: '* Deltas longer than 30 minutes are ignored.'
proxy_exercises:
index:
clone: Duplicate
external_users:
statistics:
title: External User Statistics
@ -348,6 +381,8 @@ en:
success: You successfully changed your password.
show:
link: Profile
search:
search_in_forum: "Problems? Search here in forum"
locales:
de: German
en: English
@ -364,7 +399,15 @@ en:
Hallo %{receiver_displayname}, <br>
<br>
es gibt einen neuen Kommentar von %{commenting_user_displayname} zu Ihrer Kommentaranfrage auf CodeOcean. <br>
Sie finden ihn hier: %{link} <br>
<br>
%{commenting_user_displayname} schreibt: %{comment_text}<br>
<br>
Sie finden ihre Kommentaranfrage hier: %{link_to_comment} <br>
<br>
Falls Sie beim Klick auf diesen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.<br>
<br>
Eine Übersicht Ihrer Kommentaranfragen gibt es hier: %{link_my_comments} <br>
Alle Kommentaranfragen aller Benutzer finden Sie hier: %{link_all_comments} <br>
<br>
Diese Mail wurde automatisch von CodeOcean verschickt.<br>
<br>
@ -373,7 +416,15 @@ en:
Dear %{receiver_displayname}, <br>
<br>
you received a new comment from %{commenting_user_displayname} to your request for comments on CodeOcean. <br>
You can find it here: %{link} <br>
<br>
%{commenting_user_displayname} wrote: %{comment_text} <br>
<br>
You can find your request for comments here: %{link_to_comment} <br>
<br>
If you receive an error that you are not authorized to perform this action when clicking the link, please log-in through any course exercise beforehand and click the link again. <br>
<br>
An overview of all your comments can be accessed here: %{link_my_comments} <br>
All comments of all participants are available here: %{link_all_comments} <br>
<br>
This mail was automatically sent by CodeOcean. <br>
subject: 'You received a new comment on CodeOcean from %{commenting_user_displayname}.'
@ -381,13 +432,23 @@ en:
body: 'Please visit %{link} if you want to reset your password.'
subject: Password reset instructions
request_for_comments:
click_here: Click on this sidebar to comment!
comments: Comments
howto: |
To leave comments to a specific code line, click on the respective line number. <br>
Enter your comment in the popup and save it by clicking "Comment this". <br>
Your comment will show up next to the line number as a speech bubble symbol.
howto_title: 'How to comment:'
index:
all: All Requests for Comments
get_my_comment_requests: My Requests for Comments
no_question: "The author did not enter a question for this request."
mark_as_solved: "Mark this question as answered"
show_all: "All requests"
show_solved: "Solved requests"
show_unsolved: "Unvsolved requests"
solved: "This question has been answered"
comment_exercise: "I would like to give feedback for this exercise"
sessions:
create:
failure: Invalid email or password.
@ -488,4 +549,17 @@ en:
previous_label: '&#8592; Previous Page'
file_template:
no_template_label: "Empty File"
user_exercise_feedback:
difficulty_easy: "the exercise was too easy"
difficulty_some_what_easy: "the exercise was somewhat easy"
difficulty_ok: "the difficulty of the exercise was just right"
difficulty_some_what_difficult: "the exercise was somewhat difficult"
difficult_too_difficult: "the exercise was too difficult"
difficulty: "Difficulty of the exercise:"
description: "Your points have been submitted. We kindly ask you for feedback for this exercise. <br> If you do not want to give feedback you can simply close this window.<br><br>Please describe what you liked on this exercise and what you did not. Was the exercise easy to understand or did you have problems understanding? How was the difficulty of the exercise to you?<br>We are happy about any feedback."
estimated_time_less_5: "less than 5 minutes"
estimated_time_5_to_10: "between 5 and 10 minutes"
estimated_time_10_to_20: "between 10 and 20 minutes"
estimated_time_20_to_30: "between 20 and 30 minutes"
estimated_time_more_30: "more than 30 minutes"
working_time: "Estimated time working on this exercise:"

View File

@ -10,6 +10,7 @@ Rails.application.routes.draw do
resources :request_for_comments do
member do
get :mark_as_solved
post :create_comment_exercise
end
end
resources :comments, except: [:destroy] do
@ -60,12 +61,46 @@ Rails.application.routes.draw do
member do
post :clone
get :implement
get :working_times
post :intervention
post :search
get :statistics
get :reload
post :submit
end
end
resources :proxy_exercises do
member do
post :clone
get :reload
post :submit
end
end
resources :tags do
member do
post :clone
get :reload
post :submit
end
end
resources :user_exercise_feedbacks do
member do
get :reload
post :submit
end
end
resources :interventions do
member do
post :clone
get :reload
post :submit
end
end
resources :external_users, only: [:index, :show], concerns: :statistics do
resources :exercises, concerns: :statistics
end

View File

@ -0,0 +1,14 @@
class CreateExerciseCollections < ActiveRecord::Migration
def change
create_table :exercise_collections do |t|
t.string :name
t.timestamps
end
create_table :exercise_collections_exercises, id: false do |t|
t.belongs_to :exercise_collection, index: true
t.belongs_to :exercise, index: true
end
end
end

View File

@ -0,0 +1,23 @@
class CreateProxyExercises < ActiveRecord::Migration
def change
create_table :proxy_exercises do |t|
t.string :title
t.string :description
t.string :token
t.timestamps
end
create_table :exercises_proxy_exercises, id: false do |t|
t.belongs_to :proxy_exercise, index: true
t.belongs_to :exercise, index: true
t.timestamps
end
create_table :user_proxy_exercise_exercises do |t|
t.belongs_to :user, polymorphic: true, index: true
t.belongs_to :proxy_exercise, index: true
t.belongs_to :exercise, index: true
t.timestamps
end
end
end

View File

@ -0,0 +1,23 @@
class CreateInterventions < ActiveRecord::Migration
def change
create_table :user_exercise_interventions do |t|
t.belongs_to :user, polymorphic: true
t.belongs_to :exercise
t.belongs_to :intervention
t.integer :accumulated_worktime_s
t.text :reason
t.timestamps
end
create_table :interventions do |t|
t.string :name
t.text :markup
t.timestamps
end
Intervention.createDefaultInterventions
end
end

View File

@ -0,0 +1,19 @@
class AddTags < ActiveRecord::Migration
def change
add_column :exercises, :expected_worktime_seconds, :integer, default: 60
add_column :exercises, :expected_difficulty, :integer, default: 1
create_table :tags do |t|
t.string :name, null: false
t.timestamps
end
create_table :exercise_tags do |t|
t.belongs_to :exercise
t.belongs_to :tag
t.integer :factor, default: 1
end
end
end

View File

@ -0,0 +1,11 @@
class AddUserFeedback < ActiveRecord::Migration
def change
create_table :user_exercise_feedbacks do |t|
t.belongs_to :exercise, null: false
t.belongs_to :user, polymorphic: true, null: false
t.integer :difficulty
t.integer :working_time_seconds
t.string :feedback_text
end
end
end

View File

@ -0,0 +1,10 @@
class AddSearch < ActiveRecord::Migration
def change
create_table :searches do |t|
t.belongs_to :exercise, null: false
t.belongs_to :user, polymorphic: true, null: false
t.string :search
t.timestamps
end
end
end

View File

@ -0,0 +1,7 @@
class AddReasonToUserProxyExerciseExercise < ActiveRecord::Migration
def change
change_table :user_proxy_exercise_exercises do |t|
t.string :reason
end
end
end

View File

@ -0,0 +1,6 @@
class AddIndexToSubmissions < ActiveRecord::Migration
def change
add_index :submissions, :exercise_id
add_index :submissions, :user_id
end
end

View File

@ -0,0 +1,6 @@
class SetDefaultForRequestForCommentSolved < ActiveRecord::Migration
def change
change_column_default :request_for_comments, :solved, false
RequestForComment.where(solved: nil).update_all(solved: false)
end
end

View File

@ -0,0 +1,5 @@
class ImproveUserFeedback < ActiveRecord::Migration
def change
add_column :user_exercise_feedbacks, :user_estimated_worktime, :integer
end
end

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161214144837) do
ActiveRecord::Schema.define(version: 20170403162848) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -76,22 +76,54 @@ ActiveRecord::Schema.define(version: 20161214144837) do
t.boolean "network_enabled"
end
create_table "exercise_collections", force: :cascade do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "exercise_collections_exercises", id: false, force: :cascade do |t|
t.integer "exercise_collection_id"
t.integer "exercise_id"
end
add_index "exercise_collections_exercises", ["exercise_collection_id"], name: "index_exercise_collections_exercises_on_exercise_collection_id", using: :btree
add_index "exercise_collections_exercises", ["exercise_id"], name: "index_exercise_collections_exercises_on_exercise_id", using: :btree
create_table "exercise_tags", force: :cascade do |t|
t.integer "exercise_id"
t.integer "tag_id"
t.integer "factor", default: 0
end
create_table "exercises", force: :cascade do |t|
t.text "description"
t.integer "execution_environment_id"
t.string "title", limit: 255
t.string "title", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
t.integer "user_id"
t.text "instructions"
t.boolean "public"
t.string "user_type", limit: 255
t.string "token", limit: 255
t.string "user_type", limit: 255
t.string "token", limit: 255
t.boolean "hide_file_tree"
t.boolean "allow_file_creation"
t.boolean "allow_auto_completion", default: false
t.boolean "allow_auto_completion", default: false
t.integer "expected_worktime_seconds", default: 0
t.integer "expected_difficulty", default: 1
end
create_table "exercises_proxy_exercises", id: false, force: :cascade do |t|
t.integer "proxy_exercise_id"
t.integer "exercise_id"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "exercises_proxy_exercises", ["exercise_id"], name: "index_exercises_proxy_exercises_on_exercise_id", using: :btree
add_index "exercises_proxy_exercises", ["proxy_exercise_id"], name: "index_exercises_proxy_exercises_on_proxy_exercise_id", using: :btree
create_table "external_users", force: :cascade do |t|
t.integer "consumer_id"
t.string "email", limit: 255
@ -182,11 +214,26 @@ ActiveRecord::Schema.define(version: 20161214144837) do
add_index "internal_users", ["remember_me_token"], name: "index_internal_users_on_remember_me_token", using: :btree
add_index "internal_users", ["reset_password_token"], name: "index_internal_users_on_reset_password_token", using: :btree
create_table "interventions", force: :cascade do |t|
t.string "name"
t.text "markup"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "lti_parameters", force: :cascade do |t|
t.string "external_user_id"
t.integer "external_users_id"
t.integer "consumers_id"
t.integer "exercises_id"
t.jsonb "lti_parameters", default: {}, null: false
t.jsonb "lti_parameters", default: {}, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "proxy_exercises", force: :cascade do |t|
t.string "title"
t.string "description"
t.string "token"
t.datetime "created_at"
t.datetime "updated_at"
end
@ -200,17 +247,26 @@ ActiveRecord::Schema.define(version: 20161214144837) do
end
create_table "request_for_comments", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "exercise_id", null: false
t.integer "file_id", null: false
t.integer "user_id", null: false
t.integer "exercise_id", null: false
t.integer "file_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.string "user_type", limit: 255
t.text "question"
t.boolean "solved"
t.boolean "solved", default: false
t.integer "submission_id"
end
create_table "searches", force: :cascade do |t|
t.integer "exercise_id", null: false
t.integer "user_id", null: false
t.string "user_type", null: false
t.string "search"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "submissions", force: :cascade do |t|
t.integer "exercise_id"
t.float "score"
@ -221,6 +277,15 @@ ActiveRecord::Schema.define(version: 20161214144837) do
t.string "user_type", limit: 255
end
add_index "submissions", ["exercise_id"], name: "index_submissions_on_exercise_id", using: :btree
add_index "submissions", ["user_id"], name: "index_submissions_on_user_id", using: :btree
create_table "tags", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "testruns", force: :cascade do |t|
t.boolean "passed"
t.text "output"
@ -230,4 +295,36 @@ ActiveRecord::Schema.define(version: 20161214144837) do
t.datetime "updated_at"
end
create_table "user_exercise_feedbacks", force: :cascade do |t|
t.integer "exercise_id", null: false
t.integer "user_id", null: false
t.string "user_type", null: false
t.integer "difficulty"
t.integer "working_time_seconds"
t.string "feedback_text"
end
create_table "user_exercise_interventions", force: :cascade do |t|
t.integer "user_id"
t.string "user_type"
t.integer "exercise_id"
t.integer "intervention_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "user_proxy_exercise_exercises", force: :cascade do |t|
t.integer "user_id"
t.string "user_type"
t.integer "proxy_exercise_id"
t.integer "exercise_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "reason"
end
add_index "user_proxy_exercise_exercises", ["exercise_id"], name: "index_user_proxy_exercise_exercises_on_exercise_id", using: :btree
add_index "user_proxy_exercise_exercises", ["proxy_exercise_id"], name: "index_user_proxy_exercise_exercises_on_proxy_exercise_id", using: :btree
add_index "user_proxy_exercise_exercises", ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user_type_and_user_id", using: :btree
end

0
deleteme.txt Normal file
View File

View File

@ -1,3 +1,26 @@
.flash {
display: none;
}
.fixed_error_messages {
position: fixed;
z-index: 1000;
top: 20px;
left: 0;
width: 100%;
padding-left: 10%;
padding-right: 10%;
padding-top: 0;
}
.clickthrough {
pointer-events: none;
/* fixes for IE */
/*
background:white;
opacity:0;
filter:Alpha(opacity=0);
*/
}

View File

@ -255,6 +255,12 @@ class DockerClient
if(@tubesock)
@tubesock.send_data JSON.dump({'cmd' => 'timeout'})
end
if(@socket)
@socket.send('#timeout')
#sleep one more second to ensure that the message reaches the submissions_controller.
sleep(1)
@socket.close
end
kill_container(container)
end
#ensure
@ -274,6 +280,7 @@ class DockerClient
Rails.logger.debug('exiting container ' + container.to_s)
# exit the timeout thread if it is still alive
exit_thread_if_alive
@socket.close
# if we use pooling and recylce the containers, put it back. otherwise, destroy it.
(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container)
end

View File

@ -2,7 +2,7 @@ class JunitAdapter < TestingFrameworkAdapter
COUNT_REGEXP = /Tests run: (\d+)/
FAILURES_REGEXP = /Failures: (\d+)/
SUCCESS_REGEXP = /OK \((\d+) test[s]?\)/
ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:\s(.*)|org\.junit\.ComparisonFailure:\s(.*)/
ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:\s(.*)|org\.junit\.ComparisonFailure:\s(.*)/
def self.framework_name
'JUnit'

View File

@ -0,0 +1,40 @@
class UserGroupSeparator
# seperates user into 20% no intervention, 20% break intervention, 60% rfc intervention
def self.getInterventionGroup(user)
lastDigitId = user.id % 10
if lastDigitId < 2 # 0,1
:no_intervention
elsif lastDigitId < 4 # 2,3
:break_intervention
else # 4,5,6,7,8,9
:rfc_intervention
end
end
# seperates user into 20% dummy assignment, 20% random assignemnt, 60% recommended assignment
def self.getProxyExerciseGroup(user)
lastDigitCreatedAt = user.created_at.to_i % 10
if lastDigitCreatedAt < 2 # 0,1
:dummy_assigment
elsif lastDigitCreatedAt < 4 # 2,3
:random_assigment
else # 4,5,6,7,8,9
:recommended_assignment
end
end
def self.getGroupWeek2Testing(user)
groupById = user.id % 4
if groupById == 0
:group_a
elsif groupById == 1
:group_b
elsif groupById == 2
:group_c
else # 3
:group_d
end
end
end

View File

@ -10,7 +10,7 @@ class Xikolo::Client
end
def self.user_profile_url(user_id)
return url + 'users/' + user_id
return url + 'v2/users/' + user_id
end
def self.post_request(url, params)
@ -38,11 +38,11 @@ class Xikolo::Client
end
def self.accept
'application/vnd.xikolo.v1, application/json'
'application/vnd.xikolo.v1, application/vnd.api+json, application/json'
end
def self.token
'Token token="'+Rails.application.secrets.openhpi_api_token+'"'
'Token token='+Rails.application.secrets.openhpi_api_token#+'"'
end
private

View File

@ -4,12 +4,10 @@ class Xikolo::UserClient
# return default values if user is not found or if there is a server issue:
if user
if user['display_name'].present?
name = user['display_name']
else
name = user['first_name']
end
return {display_name: name, user_visual: user['user_visual'], language: user['language']}
name = user.dig('data', 'attributes', 'name') || "User " + user_id
user_visual = user.dig('data', 'attributes', 'avatar_url') || ActionController::Base.helpers.image_path('default.png')
language = user.dig('data', 'attributes', 'language') || "DE"
return {display_name: name, user_visual: user_visual, language: language}
else
return {display_name: "User " + user_id, user_visual: ActionController::Base.helpers.image_path('default.png'), language: "DE"}
end

View File

@ -165,6 +165,7 @@ describe Lti do
it 'stores data in the session' do
controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user))
controller.instance_variable_set(:@exercise, FactoryGirl.create(:fibonacci))
expect(controller.session).to receive(:[]=).with(:consumer_id, anything)
expect(controller.session).to receive(:[]=).with(:external_user_id, anything)
controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters)
@ -172,6 +173,8 @@ describe Lti do
it 'it creates an LtiParameter Object' do
before_count = LtiParameter.count
controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user))
controller.instance_variable_set(:@exercise, FactoryGirl.create(:fibonacci))
controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters)
expect(LtiParameter.count).to eq(before_count + 1)
end

View File

@ -28,6 +28,7 @@ describe SessionsController do
describe 'POST #create_through_lti' do
let(:exercise) { FactoryGirl.create(:dummy) }
let(:exercise2) { FactoryGirl.create(:dummy) }
let(:nonce) { SecureRandom.hex }
before(:each) { I18n.locale = I18n.default_locale }
@ -129,6 +130,23 @@ describe SessionsController do
request
expect(controller).to redirect_to(implement_exercise_path(exercise.id))
end
it 'redirects to recommended exercise if requested token of proxy exercise' do
FactoryGirl.create(:proxy_exercise, exercises: [exercise])
post :create_through_lti, custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id
expect(controller).to redirect_to(implement_exercise_path(exercise.id))
end
it 'recommends only exercises who are 1 degree more complicated than what user has seen' do
# dummy user has no exercises finished, therefore his highest difficulty is 0
FactoryGirl.create(:proxy_exercise, exercises: [exercise, exercise2])
exercise.expected_difficulty = 3
exercise.save
exercise2.expected_difficulty = 1
exercise2.save
post :create_through_lti, custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id
expect(controller).to redirect_to(implement_exercise_path(exercise2.id))
end
end
end

View File

@ -0,0 +1,7 @@
FactoryGirl.define do
factory :proxy_exercise, class: ProxyExercise do
token 'dummytoken'
title 'Dummy'
end
end