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(); $('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){ resizeParentOfAceEditor: function (element){
// calculate needed size: window height - position of top of button-bar - 60 for bar itself and margins // 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; var windowHeight = window.innerHeight - $('#editor-buttons').offset().top - 60;
@ -316,10 +324,14 @@ configureEditors: function () {
var button = $('#requestComments'); var button = $('#requestComments');
button.prop('disabled', true); button.prop('disabled', true);
button.on('click', function () { button.on('click', function () {
$('#rfc_intervention_text').hide()
$('#comment-modal').modal('show'); $('#comment-modal').modal('show');
}); });
$('#askForCommentsButton').on('click', this.requestComments.bind(this)); $('#askForCommentsButton').on('click', this.requestComments.bind(this));
$('#closeAskForCommentsButton').on('click', function(){
$('#comment-modal').modal('hide');
});
setTimeout(function () { setTimeout(function () {
button.prop('disabled', false); 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(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(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(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(', ')); 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); 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-panel').toggleClass('description-panel');
$('#description-symbol').toggleClass('fa-chevron-down'); $('#description-symbol').toggleClass('fa-chevron-down');
$('#description-symbol').toggleClass('fa-chevron-right'); $('#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() { initializeEverything: function() {
this.initializeRegexes(); this.initializeRegexes();
@ -575,10 +683,14 @@ configureEditors: function () {
this.initializeDescriptionToggle(); this.initializeDescriptionToggle();
this.initializeSideBarTooltips(); this.initializeSideBarTooltips();
this.initializeTooltips(); this.initializeTooltips();
this.initializeInterventionTimer();
this.initializeSearchButton();
this.initPrompt(); this.initPrompt();
this.renderScore(); this.renderScore();
this.showFirstFile(); this.showFirstFile();
$(window).on("beforeunload", this.unloadAutoSave.bind(this)); $(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.pathname = url;
sockURL.protocol = '<%= DockerClient.config['ws_client_protocol'] %>'; sockURL.protocol = '<%= DockerClient.config['ws_client_protocol'] %>';
// strip anchor if it is in the url
sockURL.hash = ''
return sockURL.toString(); return sockURL.toString();
}, },

View File

@ -228,7 +228,8 @@ $(function() {
} }
if ($.isController('exercises')) { 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(); enableBatchUpdate();
} else if ($('.edit_exercise, .new_exercise').isPresent()) { } else if ($('.edit_exercise, .new_exercise').isPresent()) {
execution_environments = $('form').data('execution-environments'); execution_environments = $('form').data('execution-environments');

View File

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

View File

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

View File

@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base
MEMBER_ACTIONS = [:destroy, :edit, :show, :update] MEMBER_ACTIONS = [:destroy, :edit, :show, :update]
after_action :verify_authorized, except: [:help, :welcome] after_action :verify_authorized, except: [:help, :welcome]
before_action :set_locale before_action :set_locale, :allow_iframe_requests
protect_from_forgery(with: :exception) protect_from_forgery(with: :exception)
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
@ -29,4 +29,8 @@ class ApplicationController < ActionController::Base
def welcome def welcome
end end
def allow_iframe_requests
response.headers.delete('X-Frame-Options')
end
end end

View File

@ -49,7 +49,7 @@ class CommentsController < ApplicationController
@comment = Comment.new(comment_params_without_request_id) @comment = Comment.new(comment_params_without_request_id)
if comment_params[: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 end
respond_to do |format| respond_to do |format|

View File

@ -74,7 +74,12 @@ module Lti
private :require_valid_consumer_key private :require_valid_consumer_key
def require_valid_exercise_token def require_valid_exercise_token
@exercise = Exercise.find_by(token: params[:custom_token]) proxy_exercise = ProxyExercise.find_by(token: params[:custom_token])
unless proxy_exercise.nil?
@exercise = proxy_exercise.get_matching_exercise(@current_user)
else
@exercise = Exercise.find_by(token: params[:custom_token])
end
refuse_lti_launch(message: t('sessions.oauth.invalid_exercise_token')) unless @exercise refuse_lti_launch(message: t('sessions.oauth.invalid_exercise_token')) unless @exercise
end end
private :require_valid_exercise_token private :require_valid_exercise_token
@ -129,19 +134,16 @@ module Lti
private :set_current_user private :set_current_user
def store_lti_session_data(options = {}) def store_lti_session_data(options = {})
exercise = Exercise.where(token: options[:parameters][:custom_token]).first
exercise_id = exercise.id unless exercise.nil?
current_user = ExternalUser.find_or_create_by(consumer_id: options[:consumer].id, external_id: options[:parameters][:user_id].to_s)
lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id, lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id,
external_users_id: current_user.id, external_users_id: @current_user.id,
exercises_id: exercise_id) exercises_id: @exercise.id)
lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).to_json lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).to_json
lti_parameters.save! lti_parameters.save!
@lti_parameters = lti_parameters
session[:consumer_id] = options[:consumer].id session[:consumer_id] = options[:consumer].id
session[:external_user_id] = current_user.id session[:external_user_id] = @current_user.id
end end
private :store_lti_session_data private :store_lti_session_data

View File

@ -25,7 +25,7 @@ module SubmissionScoring
def feedback_message(file, score) def feedback_message(file, score)
set_locale 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 end
def score_submission(submission) def score_submission(submission)

View File

@ -40,7 +40,7 @@ class ExecutionEnvironmentsController < ApplicationController
FROM FROM
(SELECT user_id, (SELECT user_id,
exercise_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 FROM
(SELECT user_id, (SELECT user_id,
exercise_id, exercise_id,

View File

@ -6,9 +6,10 @@ class ExercisesController < ApplicationController
before_action :handle_file_uploads, only: [:create, :update] before_action :handle_file_uploads, only: [:create, :update]
before_action :set_execution_environments, only: [:create, :edit, :new, :update] before_action :set_execution_environments, only: [:create, :edit, :new, :update]
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit, :reload] before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload]
before_action :set_external_user, only: [:statistics] before_action :set_external_user, only: [:statistics]
before_action :set_file_types, only: [:create, :edit, :new, :update] before_action :set_file_types, only: [:create, :edit, :new, :update]
before_action :set_course_token, only: [:implement]
skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml] skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml]
skip_after_action :verify_authorized, only: [:import_proforma_xml] skip_after_action :verify_authorized, only: [:import_proforma_xml]
@ -19,6 +20,15 @@ class ExercisesController < ApplicationController
end end
private :authorize! private :authorize!
def max_intervention_count
3
end
def java_course_token
"702cbd2a-c84c-4b37-923a-692d7d1532d0"
end
def batch_update def batch_update
@exercises = Exercise.all @exercises = Exercise.all
authorize! authorize!
@ -54,6 +64,20 @@ class ExercisesController < ApplicationController
def create def create
@exercise = Exercise.new(exercise_params) @exercise = Exercise.new(exercise_params)
collect_set_and_unset_exercise_tags
myparam = exercise_params
checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
for et in checked_exercise_tags
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
et.exercise = @exercise
end
myparam[:exercise_tags] = checked_exercise_tags
myparam.delete :tag_ids
removed_exercise_tags.map {|et| et.destroy}
authorize! authorize!
create_and_respond(object: @exercise) create_and_respond(object: @exercise)
end end
@ -63,6 +87,7 @@ class ExercisesController < ApplicationController
end end
def edit def edit
collect_set_and_unset_exercise_tags
end end
def import_proforma_xml def import_proforma_xml
@ -118,7 +143,8 @@ class ExercisesController < ApplicationController
private :user_by_code_harbor_token private :user_by_code_harbor_token
def exercise_params def exercise_params
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name) params[:exercise][:expected_worktime_seconds] = params[:exercise][:expected_worktime_minutes].to_i * 60
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, :expected_worktime_seconds, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name)
end end
private :exercise_params private :exercise_params
@ -139,6 +165,22 @@ class ExercisesController < ApplicationController
def implement def implement
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists? redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
user_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 @submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension) @files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension)
@paths = collect_paths(@files) @paths = collect_paths(@files)
@ -150,6 +192,59 @@ class ExercisesController < ApplicationController
end end
end end
def set_course_token
lti_parameters = LtiParameter.find_by(external_users_id: current_user.id,
exercises_id: @exercise.id)
if lti_parameters
lti_json = lti_parameters.lti_parameters["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 def index
@search = policy_scope(Exercise).search(params[:q]) @search = policy_scope(Exercise).search(params[:q])
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page]) @exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page])
@ -174,6 +269,8 @@ class ExercisesController < ApplicationController
def new def new
@exercise = Exercise.new @exercise = Exercise.new
collect_set_and_unset_exercise_tags
authorize! authorize!
end end
@ -201,6 +298,16 @@ class ExercisesController < ApplicationController
end end
private :set_file_types private :set_file_types
def collect_set_and_unset_exercise_tags
@search = policy_scope(Tag).search(params[:q])
@tags = @search.result.order(:name)
checked_exercise_tags = @exercise.exercise_tags
checked_tags = checked_exercise_tags.collect{|e| e.tag}.to_set
unchecked_tags = Tag.all.to_set.subtract checked_tags
@exercise_tags = checked_exercise_tags + unchecked_tags.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag)}
end
private :collect_set_and_unset_exercise_tags
def show def show
end end
@ -252,7 +359,20 @@ class ExercisesController < ApplicationController
private :transmit_lti_score private :transmit_lti_score
def update def update
update_and_respond(object: @exercise, params: exercise_params) collect_set_and_unset_exercise_tags
myparam = exercise_params
checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
for et in checked_exercise_tags
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
et.exercise = @exercise
end
myparam[:exercise_tags] = checked_exercise_tags
myparam.delete :tag_ids
removed_exercise_tags.map {|et| et.destroy}
update_and_respond(object: @exercise, params: myparam)
end end
def redirect_after_submit def redirect_after_submit
@ -260,8 +380,12 @@ class ExercisesController < ApplicationController
if @submission.normalized_score == 1.0 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, # 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.) # 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 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. # 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[:notice] = I18n.t('exercises.submit.full_score_redirect_to_own_rfc')
flash.keep(:notice) flash.keep(:notice)
@ -273,7 +397,7 @@ class ExercisesController < ApplicationController
return return
# else: show open rfc for same exercise if available # 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. # 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[:notice] = I18n.t('exercises.submit.full_score_redirect_to_rfc')
flash.keep(:notice) flash.keep(:notice)
@ -285,8 +409,25 @@ class ExercisesController < ApplicationController
return return
end end
end end
else
# redirect to feedback page if score is less than 100 percent
redirect_to_user_feedback
return
end end
redirect_to_lti_return_path redirect_to_lti_return_path
end 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 end

View File

@ -27,7 +27,7 @@ class ExternalUsersController < ApplicationController
score, score,
id, id,
CASE CASE
WHEN working_time >= '0:30:00' THEN '0' WHEN working_time >= '0:05:00' THEN '0'
ELSE working_time ELSE working_time
END AS working_time_new END AS working_time_new
FROM 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
# GET /request_for_comments.json # GET /request_for_comments.json
def index 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! authorize!
end end
def get_my_comment_requests 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' render 'index'
end end
@ -32,6 +34,10 @@ class RequestForCommentsController < ApplicationController
end end
end end
def submit
end
# GET /request_for_comments/1 # GET /request_for_comments/1
# GET /request_for_comments/1.json # GET /request_for_comments/1.json
def show def show
@ -63,6 +69,20 @@ class RequestForCommentsController < ApplicationController
authorize! authorize!
end 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
# DELETE /request_for_comments/1.json # DELETE /request_for_comments/1.json
def destroy def destroy
@ -74,6 +94,10 @@ class RequestForCommentsController < ApplicationController
authorize! authorize!
end end
def comment_params
params.permit(:exercise_id, :feedback_text).merge(user_id: current_user.id, user_type: current_user.class.name)
end
private private
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
def set_request_for_comment 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. # 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) 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
end end

View File

@ -1,7 +1,7 @@
class SessionsController < ApplicationController class SessionsController < ApplicationController
include Lti include Lti
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :require_valid_exercise_token].each do |method_name| [:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :set_current_user, :require_valid_exercise_token].each do |method_name|
before_action(method_name, only: :create_through_lti) before_action(method_name, only: :create_through_lti)
end end
@ -18,7 +18,6 @@ class SessionsController < ApplicationController
end end
def create_through_lti def create_through_lti
set_current_user
store_lti_session_data(consumer: @consumer, parameters: params) store_lti_session_data(consumer: @consumer, parameters: params)
store_nonce(params[:oauth_nonce]) store_nonce(params[:oauth_nonce])
redirect_to(implement_exercise_path(@exercise), redirect_to(implement_exercise_path(@exercise),

View File

@ -13,6 +13,10 @@ class SubmissionsController < ApplicationController
before_action :set_mime_type, only: [:download_file, :render_file] before_action :set_mime_type, only: [:download_file, :render_file]
skip_before_action :verify_authenticity_token, 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! def authorize!
authorize(@submission || @submissions) authorize(@submission || @submissions)
end end
@ -156,7 +160,7 @@ class SubmissionsController < ApplicationController
tubesock.onmessage do |data| tubesock.onmessage do |data|
Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data) Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data)
# Check whether the client send a JSON command and kill container # 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 begin
parsed = JSON.parse(data) parsed = JSON.parse(data)
if parsed['cmd'] == 'client_kill' if parsed['cmd'] == 'client_kill'
@ -183,21 +187,31 @@ class SubmissionsController < ApplicationController
end end
def kill_socket(tubesock) 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 # Hijacked connection needs to be notified correctly
tubesock.send_data JSON.dump({'cmd' => 'exit'}) tubesock.send_data JSON.dump({'cmd' => 'exit'})
tubesock.close tubesock.close
end end
def handle_message(message, tubesock, container) def handle_message(message, tubesock, container)
@message_buffer ||= ""
# Handle special commands first # Handle special commands first
if (/^exit/.match(message)) if (/^#exit/.match(message))
kill_socket(tubesock) # 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) @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 else
# Filter out information about run_command, test_command, user or working directory # Filter out information about run_command, test_command, user or working directory
run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename]) run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename])
test_command = @submission.execution_environment.test_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)) 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) parse_message(message, 'stdout', tubesock)
end end
end end
@ -245,6 +259,13 @@ class SubmissionsController < ApplicationController
end end
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 def score
hijack do |tubesock| hijack do |tubesock|
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive? 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 @commenting_user_displayname = commenting_user.displayname
@comment_text = comment.text @comment_text = comment.text
@rfc_link = request_for_comment_url(request_for_comment) @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
end end

View File

@ -8,6 +8,11 @@ module User
has_many :exercises, as: :user has_many :exercises, as: :user
has_many :file_types, as: :user has_many :file_types, as: :user
has_many :submissions, as: :user has_many :submissions, as: :user
has_many :user_proxy_exercise_exercises, as: :user
has_many :user_exercise_interventions, as: :user
has_many :interventions, through: :user_exercise_interventions
accepts_nested_attributes_for :user_proxy_exercise_exercises
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') } scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
end end
@ -21,6 +26,6 @@ module User
end end
def to_s def to_s
name displayname
end end
end end

View File

@ -12,6 +12,15 @@ class Exercise < ActiveRecord::Base
belongs_to :execution_environment belongs_to :execution_environment
has_many :submissions has_many :submissions
has_and_belongs_to_many :proxy_exercises
has_many :user_proxy_exercise_exercises
has_and_belongs_to_many :exercise_collections
has_many :user_exercise_interventions
has_many :interventions, through: :user_exercise_interventions
has_many :exercise_tags
has_many :tags, through: :exercise_tags
accepts_nested_attributes_for :exercise_tags
has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions
has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
alias_method :users, :external_users alias_method :users, :external_users
@ -48,17 +57,21 @@ class Exercise < ActiveRecord::Base
return user_count == 0 ? 0 : submissions.count() / user_count.to_f() return user_count == 0 ? 0 : submissions.count() / user_count.to_f()
end end
def time_maximum_score(user)
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC, created_at ASC").first.created_at rescue Time.zone.at(0)
end
def user_working_time_query def user_working_time_query
""" """
SELECT user_id, SELECT user_id,
sum(working_time_new) AS working_time sum(working_time_new) AS working_time
FROM FROM
(SELECT user_id, (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 FROM
(SELECT user_id, (SELECT user_id,
id, id,
(created_at - lag(created_at) over (PARTITION BY user_id (created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
ORDER BY created_at)) AS working_time ORDER BY created_at)) AS working_time
FROM submissions FROM submissions
WHERE exercise_id=#{id}) AS foo) AS bar WHERE exercise_id=#{id}) AS foo) AS bar
@ -66,6 +79,123 @@ class Exercise < ActiveRecord::Base
""" """
end 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 def retrieve_working_time_statistics
@working_time_statistics = {} @working_time_statistics = {}
self.class.connection.execute(user_working_time_query).each do |tuple| self.class.connection.execute(user_working_time_query).each do |tuple|
@ -88,23 +218,64 @@ class Exercise < ActiveRecord::Base
@working_time_statistics[user_id]["working_time"] @working_time_statistics[user_id]["working_time"]
end end
def average_working_time_for_only(user_id) def accumulated_working_time_for_only(user)
self.class.connection.execute(""" user_type = user.external_user? ? "ExternalUser" : "InternalUser"
SELECT sum(working_time_new) AS working_time Time.parse(self.class.connection.execute("""
FROM WITH WORKING_TIME AS
(SELECT CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new (SELECT user_id,
FROM id,
(SELECT id, exercise_id,
(created_at - lag(created_at) over (PARTITION BY user_id max(score) AS max_score,
ORDER BY created_at)) AS working_time (created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
FROM submissions ORDER BY created_at)) AS working_time
WHERE exercise_id=#{id} and user_id=#{user_id}) AS foo) AS bar FROM submissions
""").first["working_time"] 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 end
def duplicate(attributes = {}) def duplicate(attributes = {})
exercise = dup exercise = dup
exercise.attributes = attributes exercise.attributes = attributes
exercise_tags.each { |et| exercise.exercise_tags << et.dup }
files.each { |file| exercise.files << file.dup } files.each { |file| exercise.files << file.dup }
exercise exercise
end end
@ -162,8 +333,16 @@ class Exercise < ActiveRecord::Base
end end
private :generate_token private :generate_token
def maximum_score def maximum_score(user = nil)
files.teacher_defined_tests.sum(:weight) if user
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0
else
files.teacher_defined_tests.sum(:weight)
end
end
def has_user_solved(user)
return maximum_score(user).to_i == maximum_score.to_i
end end
def set_default_values 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 validates :external_id, presence: true
def displayname def displayname
result = name result = "User " + id.to_s
if(consumer.name == 'openHPI') if(!consumer.nil? && consumer.name == 'openHPI')
result = Rails.cache.fetch("#{cache_key}/displayname", expires_in: 12.hours) do result = Rails.cache.fetch("#{cache_key}/displayname", expires_in: 12.hours) do
Xikolo::UserClient.get(external_id.to_s)[:display_name] Xikolo::UserClient.get(external_id.to_s)[:display_name]
end 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?} define_method(action) { admin? || author?}
end end
[:implement?, :submit?, :reload?].each do |action| [:implement?, :working_times?, :intervention?, :search?, :submit?, :reload?].each do |action|
define_method(action) { everyone } define_method(action) { everyone }
end 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 everyone
end end
def search?
everyone
end
def show? def show?
everyone everyone
end end
@ -27,4 +31,8 @@ class RequestForCommentPolicy < ApplicationPolicy
def index? def index?
everyone everyone
end end
def create_comment_exercise?
everyone
end
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| - %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] 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') h5 =t('exercises.implement.comment.others')
pre#otherCommentsTextfield pre#otherCommentsTextfield
p = '' 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') 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_external_id = @current_user.respond_to?(:external_id) ? @current_user.external_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
- external_user_id = @current_user.respond_to?(:external_id) ? @current_user.id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - external_user_id = @current_user.respond_to?(:external_id) ? @current_user.id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
- consumer_id = @current_user.respond_to?(:external_id) ? @current_user.consumer_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '') - consumer_id = @current_user.respond_to?(:external_id) ? @current_user.consumer_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id - show_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="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files)
div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id ) div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id )
div id='frames' class='editor-col' div id='frames' class='editor-col'
@ -22,3 +24,4 @@
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent') = render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')
= render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal')

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-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over')) = render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over'))
- if !@course_token.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' : '') div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '')
= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar')) = render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar'))
@ -24,5 +26,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 enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download'))
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over')) = render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over'))
- if !@course_token.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? - if @exercise.allow_file_creation?
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file')) = render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))

View File

@ -32,6 +32,25 @@
label label
= f.check_box(:allow_auto_completion) = f.check_box(:allow_auto_completion)
= t('activerecord.attributes.exercise.allow_auto_completion') = t('activerecord.attributes.exercise.allow_auto_completion')
.form-group
= f.label(t('activerecord.attributes.exercise.difficulty'))
= f.number_field :expected_difficulty, in: 1..10, step: 1
.form-group
= f.label(t('activerecord.attributes.exercise.worktime'))
= f.number_field "expected_worktime_minutes", value: @exercise.expected_worktime_seconds / 60, in: 1..1000, step: 1
h2 Tags
.table-responsive
table.table#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') h2 = t('activerecord.attributes.exercise.files')
ul#files.list-unstyled.panel-group ul#files.list-unstyled.panel-group
= f.fields_for :files do |files_form| = f.fields_for :files do |files_form|

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') h5 = t('exercises.implement.comment.question')
textarea.form-control#question(style='resize:none;') textarea.form-control#question(style='resize:none;')
p = '' p = ''
/ data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action. / data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action.
/ But if we use this button, it will work since the correct cause is supplied / 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})" h1 = "#{@exercise} (external user #{@external_user})"
- submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id).order("created_at") - submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id).order("created_at")
- current_submission = submissions.first - 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 - if current_submission
- initial_files = current_submission.files.to_a - 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| - ['.time', '.cause', '.score', '.tests', '.time_difference'].each do |title|
th.header = t(title) th.header = t(title)
tbody 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} - 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.each_with_index do |submission, index| - submissions_and_interventions.each_with_index do |submission_or_intervention, index|
tr data-id=submission.id tr data-id=submission_or_intervention.id
td.clickable = submission.created_at.strftime("%F %T") td.clickable = submission_or_intervention.created_at.strftime("%F %T")
td = submission.cause - if submission_or_intervention.is_a?(Submission)
td = submission.score td = submission_or_intervention.cause
td td = submission_or_intervention.score
-submission.testruns.each do |run| td
- if run.passed -submission_or_intervention.testruns.each do |run|
.unit-test-result.positive-result title=run.output - if run.passed
- else .unit-test-result.positive-result title=run.output
.unit-test-result.negative-result title=run.output - else
td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0 .unit-test-result.negative-result title=run.output
-working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0)) 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') p = t('.addendum')
.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until); .hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until);
div#progress_chart.col-lg-12 div#progress_chart.col-lg-12

View File

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

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 = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment'))
th = t('.test_files') th = t('.test_files')
th = t('activerecord.attributes.exercise.maximum_score') th = t('activerecord.attributes.exercise.maximum_score')
th = t('activerecord.attributes.exercise.tags')
th = t('activerecord.attributes.exercise.difficulty')
th = t('activerecord.attributes.exercise.worktime')
th th
= t('activerecord.attributes.exercise.public') = t('activerecord.attributes.exercise.public')
- if policy(Exercise).batch_update? - if policy(Exercise).batch_update?
@ -29,6 +32,9 @@ h1 = Exercise.model_name.human(count: 2)
td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment) td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
td = exercise.files.teacher_defined_tests.count td = exercise.files.teacher_defined_tests.count
td = exercise.maximum_score td = exercise.maximum_score
td = exercise.exercise_tags.count
td = exercise.expected_difficulty
td = (exercise.expected_worktime_seconds / 60).ceil
td.public data-value=exercise.public? = symbol_for(exercise.public?) td.public data-value=exercise.public? = symbol_for(exercise.public?)
td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit? td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit?
td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement? td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement?

View File

@ -19,6 +19,9 @@ h1
= row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?) = row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?)
= row(label: 'exercise.embedding_parameters') do = row(label: 'exercise.embedding_parameters') do
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise)) = content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))
= row(label: 'exercise.difficulty', value: @exercise.expected_difficulty)
= row(label: 'exercise.worktime', value: "#{@exercise.expected_worktime_seconds/60} min")
= row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", "))
h2 = t('activerecord.attributes.exercise.files') h2 = t('activerecord.attributes.exercise.files')

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) 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-responsive
table.table.sortable table.table.sortable
thead thead
tr tr
th th
i class="fa fa-lightbulb-o" aria-hidden="true" title = t('request_for_comments.solved') align="right" 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 = t('activerecord.attributes.request_for_comments.question')
th th
i class="fa fa-comment" aria-hidden="true" title = t('request_for_comments.comments') align="center" 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"> <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"> <p class="list-group-item-text">
<% <%
user = @request_for_comment.user user = @request_for_comment.user
@ -20,14 +20,17 @@
<u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> <%= t('request_for_comments.no_question') %> <u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> <%= t('request_for_comments.no_question') %>
<% end %> <% end %>
</h5> </h5>
<% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %> <% 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?) %> <% 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 %> <% else %>
<% end %> <% end %>
<% if @current_user.admin? && user.is_a?(ExternalUser) %> <% if @current_user.admin? && user.is_a?(ExternalUser) %>
<br> <br>
<br> <br>
@ -41,14 +44,18 @@
</ul> </ul>
</h5> </h5>
<% end %> <% end %>
<h5>
<u><%= t('request_for_comments.howto_title') %></u><br> <%= render_markdown(t('request_for_comments.howto')) %>
</h5>
</div> </div>
<hr>
<!-- <!--
do not put a carriage return in the line below. it will be present in the presentation of the source code, otherwise. 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. 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| %> <% 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 id='commentitor' class='editor' data-read-only='true' data-file-id='<%=file.id%>' data-mode='<%=file.file_type.editor_mode%>'><%= file.content %>
</div> </div>
<% end %> <% end %>
@ -58,6 +65,8 @@ also, all settings from the rails model needed for the editor configuration in t
<script type="text/javascript"> <script type="text/javascript">
var solvedButton = $('#mark-as-solved-button'); var solvedButton = $('#mark-as-solved-button');
var commentOnExerciseButton = $('#comment-exercise-button');
var addCommentExerciseButton = $('#addCommentExerciseButton')
solvedButton.on('click', function(event){ solvedButton.on('click', function(event){
var jqrequest = $.ajax({ 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){ jqrequest.done(function(response){
$.each(response, function(index, comment) { $.each(response, function(index, comment) {
comment.className = "code-ocean_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); session.setAnnotations(response);
@ -161,6 +177,27 @@ also, all settings from the rails model needed for the editor configuration in t
jqxhr.fail(ajaxError); 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) { function handleSidebarClick(e) {
var target = e.domEvent.target; var target = e.domEvent.target;
var editor = e.editor; var editor = e.editor;
@ -193,6 +230,7 @@ also, all settings from the rails model needed for the editor configuration in t
if (commenttext !== "") { if (commenttext !== "") {
createComment(file_id, row, editor, commenttext); createComment(file_id, row, editor, commenttext);
commentModal.find('textarea').val('') ;
commentModal.modal('hide'); 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') 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> </script>

View File

View File

@ -5,6 +5,6 @@
= row(label: 'file.hidden', value: file.hidden) = row(label: 'file.hidden', value: file.hidden)
= row(label: 'file.read_only', value: file.read_only) = row(label: 'file.read_only', value: file.read_only)
- if file.teacher_defined_test? - 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.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)) = 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.eager_load_paths << Rails.root.join('lib')
config.assets.precompile += %w( markdown-buttons.png ) config.assets.precompile += %w( markdown-buttons.png )
config.active_record.schema_format = :sql #config.active_record.schema_format = :sql
case (RUBY_ENGINE) case (RUBY_ENGINE)
when 'ruby' 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 :deploy_to, '/var/www/app'
set :keep_releases, 3 set :keep_releases, 3
set :linked_dirs, %w(log public/uploads tmp/cache tmp/files tmp/pids tmp/sockets) 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 :log_level, :info
set :puma_threads, [0, 16] set :puma_threads, [0, 16]
set :repo_url, 'git@github.com:openHPI/codeocean.git' set :repo_url, 'git@github.com:openHPI/codeocean.git'

View File

@ -32,7 +32,7 @@ production:
timeout: 60 timeout: 60
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %> workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
ws_host: ws://localhost:4243 #url to connect rails server to docker host 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: staging:
<<: *default <<: *default
@ -46,7 +46,7 @@ staging:
timeout: 60 timeout: 60
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %> workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
ws_host: ws://localhost:4243 #url to connect rails server to docker host 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: test:
<<: *default <<: *default

View File

@ -27,6 +27,7 @@ de:
exercise: exercise:
description: Beschreibung description: Beschreibung
embedding_parameters: Parameter für LTI-Einbettung embedding_parameters: Parameter für LTI-Einbettung
tags: Tags
execution_environment: Ausführungsumgebung execution_environment: Ausführungsumgebung
execution_environment_id: Ausführungsumgebung execution_environment_id: Ausführungsumgebung
files: Dateien files: Dateien
@ -34,10 +35,16 @@ de:
instructions: Anweisungen instructions: Anweisungen
maximum_score: Erreichbare Punktzahl maximum_score: Erreichbare Punktzahl
public: Öffentlich public: Öffentlich
selection: Ausgewählt
title: Titel title: Titel
user: Autor user: Autor
allow_auto_completion: "Autovervollständigung aktivieren" allow_auto_completion: "Autovervollständigung aktivieren"
allow_file_creation: "Dateierstellung erlauben" allow_file_creation: "Dateierstellung erlauben"
difficulty: Schwierigkeitsgrad
worktime: "vermutete Arbeitszeit in Minuten"
proxy_exercise:
title: Title
files_count: Anzahl der Aufgaben
external_user: external_user:
consumer: Konsument consumer: Konsument
email: E-Mail email: E-Mail
@ -68,6 +75,8 @@ de:
message: Nachricht message: Nachricht
name: Name name: Name
regular_expression: Regulärer Ausdruck regular_expression: Regulärer Ausdruck
intervention:
name: Name
internal_user: internal_user:
activated: Aktiviert activated: Aktiviert
consumer: Konsument consumer: Konsument
@ -84,6 +93,7 @@ de:
username: Benutzername username: Benutzername
requested_at: Angefragezeitpunkt requested_at: Angefragezeitpunkt
question: "Frage" question: "Frage"
close: "Fenster schließen"
submission: submission:
cause: Anlass cause: Anlass
code: Code code: Code
@ -91,6 +101,10 @@ de:
files: Dateien files: Dateien
score: Punktzahl score: Punktzahl
user: Autor user: Autor
tag:
name: Name
usage: Verwendet
difficulty: Anteil an der Aufgabe
file_template: file_template:
name: "Name" name: "Name"
file_type: "Dateityp" file_type: "Dateityp"
@ -111,6 +125,9 @@ de:
exercise: exercise:
one: Aufgabe one: Aufgabe
other: Aufgaben other: Aufgaben
proxy_exercise:
one: Proxy Aufgabe
other: Proxy Aufgaben
external_user: external_user:
one: Externer Nutzer one: Externer Nutzer
other: Externe Nutzer other: Externe Nutzer
@ -129,9 +146,15 @@ de:
internal_user: internal_user:
one: Interner Nutzer one: Interner Nutzer
other: Interne Nutzer other: Interne Nutzer
request_for_comment:
one: Kommentaranfrage
other: Kommentaranfragen
submission: submission:
one: Abgabe one: Abgabe
other: Abgaben other: Abgaben
user_exercise_feedback:
one: Feedback
other: Feedback
errors: errors:
messages: messages:
together: 'muss zusammen mit %{attribute} definiert werden' together: 'muss zusammen mit %{attribute} definiert werden'
@ -254,12 +277,19 @@ de:
line: Zeile line: Zeile
dialogtitle: Kommentar hinzufügen dialogtitle: Kommentar hinzufügen
others: Andere Kommentare auf dieser Zeile others: Andere Kommentare auf dieser Zeile
addCommentExercise: Aufgabe kommentieren
addyours: Fügen Sie Ihren Kommentar hinzu 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 removeAllOnLine: Meine Kommentare auf dieser Zeile löschen
listing: Die neuesten Kommentaranfragen listing: Die neuesten Kommentaranfragen
request: "Kommentaranfrage stellen" request: "Kommentaranfrage stellen"
question: "Bitte beschreiben Sie kurz ihre Problem oder nennen Sie den Programmteil, zu dem sie Feedback wünschen." question: "Bitte beschreiben Sie kurz ihre Probleme oder nennen Sie den Programmteil, zu dem Sie Feedback wünschen."
rfc_intervention:
text: "Es scheint so als würden Sie Probleme mit der Aufgabe haben. Wenn Sie möchten, können wir Ihnen helfen!"
break_intervention:
title: "Pause"
text: "Uns ist aufgefallen, dass du schon lange an dieser Aufgabe arbeitest. Möchtest du vielleicht später weiter machen um erstmal auf neue Gedanken zu kommen?"
index: index:
clone: Duplizieren clone: Duplizieren
implement: Implementieren implement: Implementieren
@ -290,6 +320,9 @@ de:
tests: Unit Tests tests: Unit Tests
time_difference: 'Arbeitszeit bis hier*' time_difference: 'Arbeitszeit bis hier*'
addendum: '* Differenzen von mehr als 30 Minuten werden ignoriert.' addendum: '* Differenzen von mehr als 30 Minuten werden ignoriert.'
proxy_exercises:
index:
clone: Duplizieren
external_users: external_users:
statistics: statistics:
title: Statistiken für Externe Benutzer title: Statistiken für Externe Benutzer
@ -327,6 +360,8 @@ de:
success: Sie haben Ihr Passwort erfolgreich geändert. success: Sie haben Ihr Passwort erfolgreich geändert.
show: show:
link: Profil link: Profil
search:
search_in_forum: "Probleme? Suche hier im Forum"
locales: locales:
de: Deutsch de: Deutsch
en: Englisch en: Englisch
@ -343,7 +378,15 @@ de:
Hallo %{receiver_displayname}, <br> Hallo %{receiver_displayname}, <br>
<br> <br>
es gibt einen neuen Kommentar von %{commenting_user_displayname} zu Ihrer Kommentaranfrage auf CodeOcean. <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> <br>
Diese Mail wurde automatisch von CodeOcean verschickt.<br> Diese Mail wurde automatisch von CodeOcean verschickt.<br>
<br> <br>
@ -352,7 +395,15 @@ de:
Dear %{receiver_displayname}, <br> Dear %{receiver_displayname}, <br>
<br> <br>
you received a new comment from %{commenting_user_displayname} to your request for comments on CodeOcean. <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> <br>
This mail was automatically sent by CodeOcean. <br> This mail was automatically sent by CodeOcean. <br>
subject: Sie haben einen neuen Kommentar von %{commenting_user_displayname} auf CodeOcean erhalten. 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.' body: 'Bitte besuchen Sie %{link}, sofern Sie Ihr Passwort zurücksetzen wollen.'
subject: Anweisungen zum Zurücksetzen Ihres Passworts subject: Anweisungen zum Zurücksetzen Ihres Passworts
request_for_comments: request_for_comments:
click_here: Zum Kommentieren auf die Seitenleiste klicken!
comments: Kommentare 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: index:
get_my_comment_requests: Meine Kommentaranfragen get_my_comment_requests: Meine Kommentaranfragen
all: "Alle Kommentaranfragen" all: "Alle Kommentaranfragen"
no_question: "Der Autor hat keine Frage zu dieser Anfrage gestellt." no_question: "Der Autor hat keine Frage zu dieser Anfrage gestellt."
mark_as_solved: "Diese Frage als beantwortet markieren" 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" solved: "Diese Frage wurde erfolgreich beantwortet"
comment_exercise: "Ich möchte die Aufgabenstellung kommentieren"
sessions: sessions:
create: create:
failure: Fehlerhafte E-Mail oder Passwort. failure: Fehlerhafte E-Mail oder Passwort.
@ -467,3 +528,18 @@ de:
previous_label: '&#8592; Vorherige Seite' previous_label: '&#8592; Vorherige Seite'
file_template: file_template:
no_template_label: "Leere Datei" 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: exercise:
description: Description description: Description
embedding_parameters: LTI Embedding Parameters embedding_parameters: LTI Embedding Parameters
tags: Tags
execution_environment: Execution Environment execution_environment: Execution Environment
execution_environment_id: Execution Environment execution_environment_id: Execution Environment
files: Files files: Files
@ -55,10 +56,16 @@ en:
instructions: Instructions instructions: Instructions
maximum_score: Maximum Score maximum_score: Maximum Score
public: Public public: Public
selection: Selected
title: Title title: Title
user: Author user: Author
allow_auto_completion: "Allow auto completion" allow_auto_completion: "Allow auto completion"
allow_file_creation: "Allow file creation" allow_file_creation: "Allow file creation"
difficulty: Difficulty
worktime: "Expected worktime in minutes"
proxy_exercise:
title: Title
files_count: Exercises Count
external_user: external_user:
consumer: Consumer consumer: Consumer
email: Email email: Email
@ -89,6 +96,8 @@ en:
message: Message message: Message
name: Name name: Name
regular_expression: Regular Expression regular_expression: Regular Expression
intervention:
name: Name
internal_user: internal_user:
activated: Activated activated: Activated
consumer: Consumer consumer: Consumer
@ -105,6 +114,7 @@ en:
username: Username username: Username
requested_at: Request Date requested_at: Request Date
question: "Question" question: "Question"
close: Close window
submission: submission:
cause: Cause cause: Cause
code: Code code: Code
@ -112,6 +122,10 @@ en:
files: Files files: Files
score: Score score: Score
user: Author user: Author
tag:
name: Name
usage: Used
difficulty: Share on the Exercise
file_template: file_template:
name: "Name" name: "Name"
file_type: "File Type" file_type: "File Type"
@ -132,6 +146,9 @@ en:
exercise: exercise:
one: Exercise one: Exercise
other: Exercises other: Exercises
proxy_exercise:
one: Proxy Exercise
other: Proxy Exercises
external_user: external_user:
one: External User one: External User
other: External Users other: External Users
@ -150,9 +167,15 @@ en:
internal_user: internal_user:
one: Internal User one: Internal User
other: Internal Users other: Internal Users
request_for_comment:
one: Request for Comments
other: Requests for Comments
submission: submission:
one: Submission one: Submission
other: Submissions other: Submissions
user_exercise_feedback:
one: Feedback
other: Feedback
errors: errors:
messages: messages:
together: 'has to be set along with %{attribute}' together: 'has to be set along with %{attribute}'
@ -276,11 +299,18 @@ en:
dialogtitle: Comment on this line dialogtitle: Comment on this line
others: Other comments on this line others: Other comments on this line
addyours: Add your comment 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 removeAllOnLine: Remove my comments on this line
listing: Listing the newest comment requests listing: Listing the newest comment requests
request: "Request Comments" request: "Request Comments"
question: "Please shortly describe your problem or the program part you would like to get feedback for." question: "Please shortly describe your problem or the program part you would like to get feedback for."
rfc_intervention:
text: "It looks like you may struggle with this exercise. If you like we can help you out!"
break_intervention:
title: "Break"
text: "We recognized that you are already working quite a while on this exercise. We would like to encourage you to take a break and come back later."
index: index:
clone: Duplicate clone: Duplicate
implement: Implement implement: Implement
@ -311,6 +341,9 @@ en:
tests: Unit Test Results tests: Unit Test Results
time_difference: 'Working Time until here*' time_difference: 'Working Time until here*'
addendum: '* Deltas longer than 30 minutes are ignored.' addendum: '* Deltas longer than 30 minutes are ignored.'
proxy_exercises:
index:
clone: Duplicate
external_users: external_users:
statistics: statistics:
title: External User Statistics title: External User Statistics
@ -348,6 +381,8 @@ en:
success: You successfully changed your password. success: You successfully changed your password.
show: show:
link: Profile link: Profile
search:
search_in_forum: "Problems? Search here in forum"
locales: locales:
de: German de: German
en: English en: English
@ -364,7 +399,15 @@ en:
Hallo %{receiver_displayname}, <br> Hallo %{receiver_displayname}, <br>
<br> <br>
es gibt einen neuen Kommentar von %{commenting_user_displayname} zu Ihrer Kommentaranfrage auf CodeOcean. <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> <br>
Diese Mail wurde automatisch von CodeOcean verschickt.<br> Diese Mail wurde automatisch von CodeOcean verschickt.<br>
<br> <br>
@ -373,7 +416,15 @@ en:
Dear %{receiver_displayname}, <br> Dear %{receiver_displayname}, <br>
<br> <br>
you received a new comment from %{commenting_user_displayname} to your request for comments on CodeOcean. <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> <br>
This mail was automatically sent by CodeOcean. <br> This mail was automatically sent by CodeOcean. <br>
subject: 'You received a new comment on CodeOcean from %{commenting_user_displayname}.' 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.' body: 'Please visit %{link} if you want to reset your password.'
subject: Password reset instructions subject: Password reset instructions
request_for_comments: request_for_comments:
click_here: Click on this sidebar to comment!
comments: Comments 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: index:
all: All Requests for Comments all: All Requests for Comments
get_my_comment_requests: My Requests for Comments get_my_comment_requests: My Requests for Comments
no_question: "The author did not enter a question for this request." no_question: "The author did not enter a question for this request."
mark_as_solved: "Mark this question as answered" 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" solved: "This question has been answered"
comment_exercise: "I would like to give feedback for this exercise"
sessions: sessions:
create: create:
failure: Invalid email or password. failure: Invalid email or password.
@ -488,4 +549,17 @@ en:
previous_label: '&#8592; Previous Page' previous_label: '&#8592; Previous Page'
file_template: file_template:
no_template_label: "Empty File" 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 resources :request_for_comments do
member do member do
get :mark_as_solved get :mark_as_solved
post :create_comment_exercise
end end
end end
resources :comments, except: [:destroy] do resources :comments, except: [:destroy] do
@ -60,12 +61,46 @@ Rails.application.routes.draw do
member do member do
post :clone post :clone
get :implement get :implement
get :working_times
post :intervention
post :search
get :statistics get :statistics
get :reload get :reload
post :submit post :submit
end end
end end
resources :proxy_exercises do
member do
post :clone
get :reload
post :submit
end
end
resources :tags do
member do
post :clone
get :reload
post :submit
end
end
resources :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 :external_users, only: [:index, :show], concerns: :statistics do
resources :exercises, concerns: :statistics resources :exercises, concerns: :statistics
end 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. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -76,22 +76,54 @@ ActiveRecord::Schema.define(version: 20161214144837) do
t.boolean "network_enabled" t.boolean "network_enabled"
end 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| create_table "exercises", force: :cascade do |t|
t.text "description" t.text "description"
t.integer "execution_environment_id" t.integer "execution_environment_id"
t.string "title", limit: 255 t.string "title", limit: 255
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.integer "user_id" t.integer "user_id"
t.text "instructions" t.text "instructions"
t.boolean "public" t.boolean "public"
t.string "user_type", limit: 255 t.string "user_type", limit: 255
t.string "token", limit: 255 t.string "token", limit: 255
t.boolean "hide_file_tree" t.boolean "hide_file_tree"
t.boolean "allow_file_creation" 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 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| create_table "external_users", force: :cascade do |t|
t.integer "consumer_id" t.integer "consumer_id"
t.string "email", limit: 255 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", ["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 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| create_table "lti_parameters", force: :cascade do |t|
t.string "external_user_id" t.integer "external_users_id"
t.integer "consumers_id" t.integer "consumers_id"
t.integer "exercises_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 "created_at"
t.datetime "updated_at" t.datetime "updated_at"
end end
@ -200,17 +247,26 @@ ActiveRecord::Schema.define(version: 20161214144837) do
end end
create_table "request_for_comments", force: :cascade do |t| create_table "request_for_comments", force: :cascade do |t|
t.integer "user_id", null: false t.integer "user_id", null: false
t.integer "exercise_id", null: false t.integer "exercise_id", null: false
t.integer "file_id", null: false t.integer "file_id", null: false
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "user_type", limit: 255 t.string "user_type", limit: 255
t.text "question" t.text "question"
t.boolean "solved" t.boolean "solved", default: false
t.integer "submission_id" t.integer "submission_id"
end 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| create_table "submissions", force: :cascade do |t|
t.integer "exercise_id" t.integer "exercise_id"
t.float "score" t.float "score"
@ -221,6 +277,15 @@ ActiveRecord::Schema.define(version: 20161214144837) do
t.string "user_type", limit: 255 t.string "user_type", limit: 255
end 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| create_table "testruns", force: :cascade do |t|
t.boolean "passed" t.boolean "passed"
t.text "output" t.text "output"
@ -230,4 +295,36 @@ ActiveRecord::Schema.define(version: 20161214144837) do
t.datetime "updated_at" t.datetime "updated_at"
end 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 end

0
deleteme.txt Normal file
View File

View File

@ -1,3 +1,26 @@
.flash { .flash {
display: none; 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) if(@tubesock)
@tubesock.send_data JSON.dump({'cmd' => 'timeout'}) @tubesock.send_data JSON.dump({'cmd' => 'timeout'})
end 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) kill_container(container)
end end
#ensure #ensure
@ -274,6 +280,7 @@ class DockerClient
Rails.logger.debug('exiting container ' + container.to_s) Rails.logger.debug('exiting container ' + container.to_s)
# exit the timeout thread if it is still alive # exit the timeout thread if it is still alive
exit_thread_if_alive exit_thread_if_alive
@socket.close
# if we use pooling and recylce the containers, put it back. otherwise, destroy it. # 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) (DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container)
end end

View File

@ -2,7 +2,7 @@ class JunitAdapter < TestingFrameworkAdapter
COUNT_REGEXP = /Tests run: (\d+)/ COUNT_REGEXP = /Tests run: (\d+)/
FAILURES_REGEXP = /Failures: (\d+)/ FAILURES_REGEXP = /Failures: (\d+)/
SUCCESS_REGEXP = /OK \((\d+) test[s]?\)/ 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 def self.framework_name
'JUnit' '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 end
def self.user_profile_url(user_id) def self.user_profile_url(user_id)
return url + 'users/' + user_id return url + 'v2/users/' + user_id
end end
def self.post_request(url, params) def self.post_request(url, params)
@ -38,11 +38,11 @@ class Xikolo::Client
end end
def self.accept def self.accept
'application/vnd.xikolo.v1, application/json' 'application/vnd.xikolo.v1, application/vnd.api+json, application/json'
end end
def self.token def self.token
'Token token="'+Rails.application.secrets.openhpi_api_token+'"' 'Token token='+Rails.application.secrets.openhpi_api_token#+'"'
end end
private 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: # return default values if user is not found or if there is a server issue:
if user if user
if user['display_name'].present? name = user.dig('data', 'attributes', 'name') || "User " + user_id
name = user['display_name'] user_visual = user.dig('data', 'attributes', 'avatar_url') || ActionController::Base.helpers.image_path('default.png')
else language = user.dig('data', 'attributes', 'language') || "DE"
name = user['first_name'] return {display_name: name, user_visual: user_visual, language: language}
end
return {display_name: name, user_visual: user['user_visual'], language: user['language']}
else else
return {display_name: "User " + user_id, user_visual: ActionController::Base.helpers.image_path('default.png'), language: "DE"} return {display_name: "User " + user_id, user_visual: ActionController::Base.helpers.image_path('default.png'), language: "DE"}
end end

View File

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

View File

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

View File

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