Merge branch 'master' into client-routesv2
This commit is contained in:
@ -167,6 +167,14 @@ configureEditors: function () {
|
||||
$('button i.fa-spin').hide();
|
||||
},
|
||||
|
||||
|
||||
resizeAceEditors: function (){
|
||||
$('.editor').each(function (index, element) {
|
||||
this.resizeParentOfAceEditor(element);
|
||||
}.bind(this));
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
},
|
||||
|
||||
resizeParentOfAceEditor: function (element){
|
||||
// calculate needed size: window height - position of top of button-bar - 60 for bar itself and margins
|
||||
var windowHeight = window.innerHeight - $('#editor-buttons').offset().top - 60;
|
||||
@ -316,10 +324,14 @@ configureEditors: function () {
|
||||
var button = $('#requestComments');
|
||||
button.prop('disabled', true);
|
||||
button.on('click', function () {
|
||||
$('#rfc_intervention_text').hide()
|
||||
$('#comment-modal').modal('show');
|
||||
});
|
||||
|
||||
$('#askForCommentsButton').on('click', this.requestComments.bind(this));
|
||||
$('#closeAskForCommentsButton').on('click', function(){
|
||||
$('#comment-modal').modal('hide');
|
||||
});
|
||||
|
||||
setTimeout(function () {
|
||||
button.prop('disabled', false);
|
||||
@ -363,7 +375,7 @@ configureEditors: function () {
|
||||
panel.find('.row .col-sm-9').eq(0).find('.number').eq(1).text(result.count);
|
||||
panel.find('.row .col-sm-9').eq(1).find('.number').eq(0).text(parseFloat((result.score * result.weight).toFixed(2)));
|
||||
panel.find('.row .col-sm-9').eq(1).find('.number').eq(1).text(result.weight);
|
||||
panel.find('.row .col-sm-9').eq(2).text(result.message);
|
||||
panel.find('.row .col-sm-9').eq(2).html(result.message);
|
||||
if (result.error_messages) panel.find('.row .col-sm-9').eq(3).text(result.error_messages.join(', '));
|
||||
panel.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index);
|
||||
},
|
||||
@ -559,6 +571,102 @@ configureEditors: function () {
|
||||
$('#description-panel').toggleClass('description-panel');
|
||||
$('#description-symbol').toggleClass('fa-chevron-down');
|
||||
$('#description-symbol').toggleClass('fa-chevron-right');
|
||||
// resize ace editors
|
||||
this.resizeAceEditors();
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* interventions
|
||||
* */
|
||||
initializeInterventionTimer: function() {
|
||||
|
||||
if ($('#editor').data('rfc-interventions') == true || $('#editor').data('break-interventions') == true) { // split in break or rfc intervention
|
||||
window.onblur = function() { window.blurred = true; };
|
||||
window.onfocus = function() { window.blurred = false; };
|
||||
|
||||
var delta = 100; // time in ms to wait for window event before time gets stopped
|
||||
var tid;
|
||||
$.ajax({
|
||||
data: {
|
||||
exercise_id: $('#editor').data('exercise-id'),
|
||||
user_id: $('#editor').data('user-id')
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'GET',
|
||||
// get working times for this exercise
|
||||
url: $('#editor').data('working-times-url'),
|
||||
success: function (data) {
|
||||
var percentile75 = data['working_time_75_percentile'];
|
||||
var accumulatedWorkTimeUser = data['working_time_accumulated'];
|
||||
|
||||
var minTimeIntervention = 10 * 60 * 1000;
|
||||
|
||||
if ((accumulatedWorkTimeUser - percentile75) > 0) {
|
||||
// working time is already over 75 percentile
|
||||
var timeUntilIntervention = minTimeIntervention;
|
||||
} else {
|
||||
// working time is less than 75 percentile
|
||||
// ensure we give user at least minTimeIntervention before we bother the user
|
||||
var timeUntilIntervention = Math.max(percentile75 - accumulatedWorkTimeUser, minTimeIntervention);
|
||||
}
|
||||
|
||||
tid = setInterval(function() {
|
||||
if ( window.blurred ) { return; }
|
||||
timeUntilIntervention -= delta;
|
||||
if ( timeUntilIntervention <= 0 ) {
|
||||
clearInterval(tid);
|
||||
// timeUntilIntervention passed
|
||||
if ($('#editor').data('break-interventions')) {
|
||||
$('#break-intervention-modal').modal('show');
|
||||
$.ajax({
|
||||
data: {
|
||||
intervention_type: 'BreakIntervention'
|
||||
},
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
url: $('#editor').data('intervention-save-url')
|
||||
});
|
||||
} else if ($('#editor').data('rfc-interventions')){
|
||||
var button = $('#requestComments');
|
||||
// only show intervention if user did not requested for a comment already
|
||||
if (!button.prop('disabled')) {
|
||||
$('#rfc_intervention_text').show();
|
||||
$('#comment-modal').modal('show');
|
||||
$.ajax({
|
||||
data: {
|
||||
intervention_type: 'QuestionIntervention'
|
||||
},
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
url: $('#editor').data('intervention-save-url')
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}, delta);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
initializeSearchButton: function(){
|
||||
$('#btn-search-col').button().click(function(){
|
||||
var search = $('#search-input-text').val();
|
||||
var course_token = $('#editor').data('course_token')
|
||||
var save_search_url = $('#editor').data('search-save-url')
|
||||
window.open("https://open.hpi.de/courses/" + course_token + "/pinboard?query=" + search, '_blank');
|
||||
// save search
|
||||
$.ajax({
|
||||
data: {
|
||||
search_text: search
|
||||
},
|
||||
dataType: 'json',
|
||||
type: 'POST',
|
||||
url: save_search_url});
|
||||
})
|
||||
|
||||
$('#sidebar-search-collapsed').on('click',this.handleSideBarToggle.bind(this));
|
||||
},
|
||||
|
||||
|
||||
@ -575,10 +683,14 @@ configureEditors: function () {
|
||||
this.initializeDescriptionToggle();
|
||||
this.initializeSideBarTooltips();
|
||||
this.initializeTooltips();
|
||||
this.initializeInterventionTimer();
|
||||
this.initializeSearchButton();
|
||||
this.initPrompt();
|
||||
this.renderScore();
|
||||
this.showFirstFile();
|
||||
|
||||
$(window).on("beforeunload", this.unloadAutoSave.bind(this));
|
||||
// create autosave when the editor is opened the first time
|
||||
this.autosave();
|
||||
}
|
||||
};
|
@ -6,6 +6,9 @@ CodeOceanEditorWebsocket = {
|
||||
sockURL.pathname = url;
|
||||
sockURL.protocol = '<%= DockerClient.config['ws_client_protocol'] %>';
|
||||
|
||||
// strip anchor if it is in the url
|
||||
sockURL.hash = ''
|
||||
|
||||
return sockURL.toString();
|
||||
},
|
||||
|
||||
|
@ -228,7 +228,8 @@ $(function() {
|
||||
}
|
||||
|
||||
if ($.isController('exercises')) {
|
||||
if ($('table').isPresent()) {
|
||||
// ignore tags table since it is in the dom before other tables
|
||||
if ($('table:not(#tags-table)').isPresent()) {
|
||||
enableBatchUpdate();
|
||||
} else if ($('.edit_exercise, .new_exercise').isPresent()) {
|
||||
execution_environments = $('form').data('execution-environments');
|
||||
|
@ -5,6 +5,7 @@ h1 {
|
||||
|
||||
.lead {
|
||||
font-size: 16px;
|
||||
color: rgba(70, 70, 70, 1);
|
||||
}
|
||||
|
||||
i.fa {
|
||||
|
@ -1,4 +1,5 @@
|
||||
#commentitor {
|
||||
margin-top: 2rem;
|
||||
height: 600px;
|
||||
background-color:#f9f9f9
|
||||
}
|
@ -5,7 +5,7 @@ class ApplicationController < ActionController::Base
|
||||
MEMBER_ACTIONS = [:destroy, :edit, :show, :update]
|
||||
|
||||
after_action :verify_authorized, except: [:help, :welcome]
|
||||
before_action :set_locale
|
||||
before_action :set_locale, :allow_iframe_requests
|
||||
protect_from_forgery(with: :exception)
|
||||
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
|
||||
|
||||
@ -29,4 +29,8 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
def welcome
|
||||
end
|
||||
|
||||
def allow_iframe_requests
|
||||
response.headers.delete('X-Frame-Options')
|
||||
end
|
||||
end
|
||||
|
@ -49,7 +49,7 @@ class CommentsController < ApplicationController
|
||||
@comment = Comment.new(comment_params_without_request_id)
|
||||
|
||||
if comment_params[:request_id]
|
||||
UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user)
|
||||
UserMailer.got_new_comment(@comment, RequestForComment.find(comment_params[:request_id]), current_user).deliver_now
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
|
@ -74,7 +74,12 @@ module Lti
|
||||
private :require_valid_consumer_key
|
||||
|
||||
def require_valid_exercise_token
|
||||
proxy_exercise = ProxyExercise.find_by(token: params[:custom_token])
|
||||
unless proxy_exercise.nil?
|
||||
@exercise = proxy_exercise.get_matching_exercise(@current_user)
|
||||
else
|
||||
@exercise = Exercise.find_by(token: params[:custom_token])
|
||||
end
|
||||
refuse_lti_launch(message: t('sessions.oauth.invalid_exercise_token')) unless @exercise
|
||||
end
|
||||
private :require_valid_exercise_token
|
||||
@ -129,19 +134,16 @@ module Lti
|
||||
private :set_current_user
|
||||
|
||||
def store_lti_session_data(options = {})
|
||||
exercise = Exercise.where(token: options[:parameters][:custom_token]).first
|
||||
exercise_id = exercise.id unless exercise.nil?
|
||||
|
||||
current_user = ExternalUser.find_or_create_by(consumer_id: options[:consumer].id, external_id: options[:parameters][:user_id].to_s)
|
||||
lti_parameters = LtiParameter.find_or_create_by(consumers_id: options[:consumer].id,
|
||||
external_users_id: current_user.id,
|
||||
exercises_id: exercise_id)
|
||||
external_users_id: @current_user.id,
|
||||
exercises_id: @exercise.id)
|
||||
|
||||
lti_parameters.lti_parameters = options[:parameters].slice(*SESSION_PARAMETERS).to_json
|
||||
lti_parameters.save!
|
||||
@lti_parameters = lti_parameters
|
||||
|
||||
session[:consumer_id] = options[:consumer].id
|
||||
session[:external_user_id] = current_user.id
|
||||
session[:external_user_id] = @current_user.id
|
||||
end
|
||||
private :store_lti_session_data
|
||||
|
||||
|
@ -25,7 +25,7 @@ module SubmissionScoring
|
||||
|
||||
def feedback_message(file, score)
|
||||
set_locale
|
||||
score == Assessor::MAXIMUM_SCORE ? I18n.t('exercises.implement.default_feedback') : file.feedback_message
|
||||
score == Assessor::MAXIMUM_SCORE ? I18n.t('exercises.implement.default_feedback') : render_markdown(file.feedback_message)
|
||||
end
|
||||
|
||||
def score_submission(submission)
|
||||
|
@ -40,7 +40,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
exercise_id,
|
||||
CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
exercise_id,
|
||||
|
@ -6,9 +6,10 @@ class ExercisesController < ApplicationController
|
||||
|
||||
before_action :handle_file_uploads, only: [:create, :update]
|
||||
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
|
||||
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit, :reload]
|
||||
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :working_times, :intervention, :search, :run, :statistics, :submit, :reload]
|
||||
before_action :set_external_user, only: [:statistics]
|
||||
before_action :set_file_types, only: [:create, :edit, :new, :update]
|
||||
before_action :set_course_token, only: [:implement]
|
||||
|
||||
skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml]
|
||||
skip_after_action :verify_authorized, only: [:import_proforma_xml]
|
||||
@ -19,6 +20,15 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def max_intervention_count
|
||||
3
|
||||
end
|
||||
|
||||
|
||||
def java_course_token
|
||||
"702cbd2a-c84c-4b37-923a-692d7d1532d0"
|
||||
end
|
||||
|
||||
def batch_update
|
||||
@exercises = Exercise.all
|
||||
authorize!
|
||||
@ -54,6 +64,20 @@ class ExercisesController < ApplicationController
|
||||
|
||||
def create
|
||||
@exercise = Exercise.new(exercise_params)
|
||||
collect_set_and_unset_exercise_tags
|
||||
myparam = exercise_params
|
||||
checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
|
||||
for et in checked_exercise_tags
|
||||
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
|
||||
et.exercise = @exercise
|
||||
end
|
||||
|
||||
myparam[:exercise_tags] = checked_exercise_tags
|
||||
myparam.delete :tag_ids
|
||||
removed_exercise_tags.map {|et| et.destroy}
|
||||
|
||||
authorize!
|
||||
create_and_respond(object: @exercise)
|
||||
end
|
||||
@ -63,6 +87,7 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
|
||||
def edit
|
||||
collect_set_and_unset_exercise_tags
|
||||
end
|
||||
|
||||
def import_proforma_xml
|
||||
@ -118,7 +143,8 @@ class ExercisesController < ApplicationController
|
||||
private :user_by_code_harbor_token
|
||||
|
||||
def exercise_params
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
params[:exercise][:expected_worktime_seconds] = params[:exercise][:expected_worktime_minutes].to_i * 60
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, :expected_worktime_seconds, files_attributes: file_attributes, :tag_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
private :exercise_params
|
||||
|
||||
@ -139,6 +165,22 @@ class ExercisesController < ApplicationController
|
||||
|
||||
def implement
|
||||
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
||||
user_solved_exercise = @exercise.has_user_solved(current_user)
|
||||
user_got_enough_interventions = UserExerciseIntervention.where(user: current_user).where("created_at >= ?", Time.zone.now.beginning_of_day).count >= max_intervention_count
|
||||
is_java_course = @course_token && @course_token.eql?(java_course_token)
|
||||
|
||||
user_intervention_group = UserGroupSeparator.getInterventionGroup(current_user)
|
||||
|
||||
case user_intervention_group
|
||||
when :no_intervention
|
||||
when :break_intervention
|
||||
@show_break_interventions = (user_solved_exercise || !is_java_course || user_got_enough_interventions) ? "false" : "true"
|
||||
when :rfc_intervention
|
||||
@show_rfc_interventions = (user_solved_exercise || !is_java_course || user_got_enough_interventions) ? "false" : "true"
|
||||
end
|
||||
|
||||
@search = Search.new
|
||||
@search.exercise = @exercise
|
||||
@submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
|
||||
@files = (@submission ? @submission.collect_files : @exercise.files).select(&:visible).sort_by(&:name_with_extension)
|
||||
@paths = collect_paths(@files)
|
||||
@ -150,6 +192,59 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def set_course_token
|
||||
lti_parameters = LtiParameter.find_by(external_users_id: current_user.id,
|
||||
exercises_id: @exercise.id)
|
||||
if lti_parameters
|
||||
lti_json = lti_parameters.lti_parameters["launch_presentation_return_url"]
|
||||
|
||||
@course_token =
|
||||
unless lti_json.nil?
|
||||
if match = lti_json.match(/^.*courses\/([a-z0-9\-]+)\/sections/)
|
||||
match.captures.first
|
||||
else
|
||||
java_course_token
|
||||
end
|
||||
else
|
||||
""
|
||||
end
|
||||
else
|
||||
# no consumer, therefore implementation with internal user
|
||||
@course_token = java_course_token
|
||||
end
|
||||
end
|
||||
private :set_course_token
|
||||
|
||||
def working_times
|
||||
working_time_accumulated = @exercise.accumulated_working_time_for_only(current_user)
|
||||
working_time_75_percentile = @exercise.get_quantiles([0.75]).first
|
||||
render(json: {working_time_75_percentile: working_time_75_percentile, working_time_accumulated: working_time_accumulated})
|
||||
end
|
||||
|
||||
def intervention
|
||||
intervention = Intervention.find_by_name(params[:intervention_type])
|
||||
unless intervention.nil?
|
||||
uei = UserExerciseIntervention.new(
|
||||
user: current_user, exercise: @exercise, intervention: intervention,
|
||||
accumulated_worktime_s: @exercise.accumulated_working_time_for_only(current_user))
|
||||
uei.save
|
||||
render(json: {success: 'true'})
|
||||
else
|
||||
render(json: {success: 'false', error: "undefined intervention #{params[:intervention_type]}"})
|
||||
end
|
||||
end
|
||||
|
||||
def search
|
||||
search_text = params[:search_text]
|
||||
search = Search.new(user: current_user, exercise: @exercise, search: search_text)
|
||||
|
||||
begin search.save
|
||||
render(json: {success: 'true'})
|
||||
rescue
|
||||
render(json: {success: 'false', error: "could not save search: #{$!}"})
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
@search = policy_scope(Exercise).search(params[:q])
|
||||
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page])
|
||||
@ -174,6 +269,8 @@ class ExercisesController < ApplicationController
|
||||
|
||||
def new
|
||||
@exercise = Exercise.new
|
||||
collect_set_and_unset_exercise_tags
|
||||
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -201,6 +298,16 @@ class ExercisesController < ApplicationController
|
||||
end
|
||||
private :set_file_types
|
||||
|
||||
def collect_set_and_unset_exercise_tags
|
||||
@search = policy_scope(Tag).search(params[:q])
|
||||
@tags = @search.result.order(:name)
|
||||
checked_exercise_tags = @exercise.exercise_tags
|
||||
checked_tags = checked_exercise_tags.collect{|e| e.tag}.to_set
|
||||
unchecked_tags = Tag.all.to_set.subtract checked_tags
|
||||
@exercise_tags = checked_exercise_tags + unchecked_tags.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag)}
|
||||
end
|
||||
private :collect_set_and_unset_exercise_tags
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
@ -252,7 +359,20 @@ class ExercisesController < ApplicationController
|
||||
private :transmit_lti_score
|
||||
|
||||
def update
|
||||
update_and_respond(object: @exercise, params: exercise_params)
|
||||
collect_set_and_unset_exercise_tags
|
||||
myparam = exercise_params
|
||||
checked_exercise_tags = @exercise_tags.select { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
removed_exercise_tags = @exercise_tags.reject { | et | myparam[:tag_ids].include? et.tag.id.to_s }
|
||||
|
||||
for et in checked_exercise_tags
|
||||
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
|
||||
et.exercise = @exercise
|
||||
end
|
||||
|
||||
myparam[:exercise_tags] = checked_exercise_tags
|
||||
myparam.delete :tag_ids
|
||||
removed_exercise_tags.map {|et| et.destroy}
|
||||
update_and_respond(object: @exercise, params: myparam)
|
||||
end
|
||||
|
||||
def redirect_after_submit
|
||||
@ -260,8 +380,12 @@ class ExercisesController < ApplicationController
|
||||
if @submission.normalized_score == 1.0
|
||||
# if user is external and has an own rfc, redirect to it and message him to clean up and accept the answer. (we need to check that the user is external,
|
||||
# otherwise an internal user could be shown a false rfc here, since current_user.id is polymorphic, but only makes sense for external users when used with rfcs.)
|
||||
# redirect 10 percent pseudorandomly to the feedback page
|
||||
if current_user.respond_to? :external_id
|
||||
if rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise, user_id: current_user.id).first
|
||||
if ((current_user.id + @submission.exercise.created_at.to_i) % 10 == 1)
|
||||
redirect_to_user_feedback
|
||||
return
|
||||
elsif rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise, user_id: current_user.id).first
|
||||
# set a message that informs the user that his own RFC should be closed.
|
||||
flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_own_rfc')
|
||||
flash.keep(:notice)
|
||||
@ -273,7 +397,7 @@ class ExercisesController < ApplicationController
|
||||
return
|
||||
|
||||
# else: show open rfc for same exercise if available
|
||||
elsif rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise).where.not(question: nil).order("RANDOM()").first
|
||||
elsif rfc = RequestForComment.unsolved.where(exercise_id: @submission.exercise).where.not(question: nil).order("RANDOM()").find { | rfc_element |(rfc_element.comments_count < 5) }
|
||||
# set a message that informs the user that his score was perfect and help in RFC is greatly appreciated.
|
||||
flash[:notice] = I18n.t('exercises.submit.full_score_redirect_to_rfc')
|
||||
flash.keep(:notice)
|
||||
@ -285,8 +409,25 @@ class ExercisesController < ApplicationController
|
||||
return
|
||||
end
|
||||
end
|
||||
else
|
||||
# redirect to feedback page if score is less than 100 percent
|
||||
redirect_to_user_feedback
|
||||
return
|
||||
end
|
||||
redirect_to_lti_return_path
|
||||
end
|
||||
|
||||
def redirect_to_user_feedback
|
||||
url = if UserExerciseFeedback.find_by(exercise: @exercise, user: current_user)
|
||||
edit_user_exercise_feedback_path(user_exercise_feedback: {exercise_id: @exercise.id})
|
||||
else
|
||||
new_user_exercise_feedback_path(user_exercise_feedback: {exercise_id: @exercise.id})
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(url) }
|
||||
format.json { render(json: {redirect: url}) }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -27,7 +27,7 @@ class ExternalUsersController < ApplicationController
|
||||
score,
|
||||
id,
|
||||
CASE
|
||||
WHEN working_time >= '0:30:00' THEN '0'
|
||||
WHEN working_time >= '0:05:00' THEN '0'
|
||||
ELSE working_time
|
||||
END AS working_time_new
|
||||
FROM
|
||||
|
55
app/controllers/interventions_controller.rb
Normal file
55
app/controllers/interventions_controller.rb
Normal file
@ -0,0 +1,55 @@
|
||||
class InterventionsController < ApplicationController
|
||||
include CommonBehavior
|
||||
|
||||
before_action :set_intervention, only: MEMBER_ACTIONS
|
||||
|
||||
def authorize!
|
||||
authorize(@intervention || @interventions)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
#@intervention = Intervention.new(intervention_params)
|
||||
#authorize!
|
||||
#create_and_respond(object: @intervention)
|
||||
end
|
||||
|
||||
def destroy
|
||||
destroy_and_respond(object: @intervention)
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def intervention_params
|
||||
params[:intervention].permit(:name)
|
||||
end
|
||||
private :intervention_params
|
||||
|
||||
def index
|
||||
@interventions = Intervention.all.paginate(page: params[:page])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def new
|
||||
#@intervention = Intervention.new
|
||||
#authorize!
|
||||
end
|
||||
|
||||
def set_intervention
|
||||
@intervention = Intervention.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_intervention
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
update_and_respond(object: @intervention, params: intervention_params)
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
end
|
80
app/controllers/proxy_exercises_controller.rb
Normal file
80
app/controllers/proxy_exercises_controller.rb
Normal file
@ -0,0 +1,80 @@
|
||||
class ProxyExercisesController < ApplicationController
|
||||
include CommonBehavior
|
||||
|
||||
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :reload]
|
||||
|
||||
def authorize!
|
||||
authorize(@proxy_exercise || @proxy_exercises)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def clone
|
||||
proxy_exercise = @proxy_exercise.duplicate(token: nil, exercises: @proxy_exercise.exercises)
|
||||
proxy_exercise.send(:generate_token)
|
||||
if proxy_exercise.save
|
||||
redirect_to(proxy_exercise, notice: t('shared.object_cloned', model: ProxyExercise.model_name.human))
|
||||
else
|
||||
flash[:danger] = t('shared.message_failure')
|
||||
redirect_to(@proxy_exercise)
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
myparams = proxy_exercise_params
|
||||
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject { |c| c.empty? })
|
||||
@proxy_exercise = ProxyExercise.new(myparams)
|
||||
authorize!
|
||||
|
||||
create_and_respond(object: @proxy_exercise)
|
||||
end
|
||||
|
||||
def destroy
|
||||
destroy_and_respond(object: @proxy_exercise)
|
||||
end
|
||||
|
||||
def edit
|
||||
@search = policy_scope(Exercise).search(params[:q])
|
||||
@exercises = @search.result.order(:title)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def proxy_exercise_params
|
||||
params[:proxy_exercise].permit(:description, :title, :exercise_ids => [])
|
||||
end
|
||||
private :proxy_exercise_params
|
||||
|
||||
def index
|
||||
@search = policy_scope(ProxyExercise).search(params[:q])
|
||||
@proxy_exercises = @search.result.order(:title).paginate(page: params[:page])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def new
|
||||
@proxy_exercise = ProxyExercise.new
|
||||
@search = policy_scope(Exercise).search(params[:q])
|
||||
@exercises = @search.result.order(:title)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def set_exercise
|
||||
@proxy_exercise = ProxyExercise.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_exercise
|
||||
|
||||
def show
|
||||
@search = @proxy_exercise.exercises.search
|
||||
@exercises = @proxy_exercise.exercises.search.result.order(:title) #@search.result.order(:title)
|
||||
end
|
||||
|
||||
#we might want to think about auth here
|
||||
def reload
|
||||
end
|
||||
|
||||
def update
|
||||
myparams = proxy_exercise_params
|
||||
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject { |c| c.blank? })
|
||||
update_and_respond(object: @proxy_exercise, params: myparams)
|
||||
end
|
||||
|
||||
end
|
@ -11,12 +11,14 @@ class RequestForCommentsController < ApplicationController
|
||||
# GET /request_for_comments
|
||||
# GET /request_for_comments.json
|
||||
def index
|
||||
@request_for_comments = RequestForComment.last_per_user(2).order('created_at DESC').paginate(page: params[:page])
|
||||
@search = RequestForComment.last_per_user(2).search(params[:q])
|
||||
@request_for_comments = @search.result.order('created_at DESC').paginate(page: params[:page])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def get_my_comment_requests
|
||||
@request_for_comments = RequestForComment.where(user_id: current_user.id).order('created_at DESC').paginate(page: params[:page])
|
||||
@search = RequestForComment.where(user_id: current_user.id).order('created_at DESC').search(params[:q])
|
||||
@request_for_comments = @search.result.paginate(page: params[:page])
|
||||
render 'index'
|
||||
end
|
||||
|
||||
@ -32,6 +34,10 @@ class RequestForCommentsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def submit
|
||||
|
||||
end
|
||||
|
||||
# GET /request_for_comments/1
|
||||
# GET /request_for_comments/1.json
|
||||
def show
|
||||
@ -63,6 +69,20 @@ class RequestForCommentsController < ApplicationController
|
||||
authorize!
|
||||
end
|
||||
|
||||
def create_comment_exercise
|
||||
old = UserExerciseFeedback.find_by(exercise_id: params[:exercise_id], user_id: current_user.id, user_type: current_user.class.name)
|
||||
if old
|
||||
old.delete
|
||||
end
|
||||
uef = UserExerciseFeedback.new(comment_params)
|
||||
|
||||
if uef.save
|
||||
render(json: {success: "true"})
|
||||
else
|
||||
render(json: {success: "false"})
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /request_for_comments/1
|
||||
# DELETE /request_for_comments/1.json
|
||||
def destroy
|
||||
@ -74,6 +94,10 @@ class RequestForCommentsController < ApplicationController
|
||||
authorize!
|
||||
end
|
||||
|
||||
def comment_params
|
||||
params.permit(:exercise_id, :feedback_text).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_request_for_comment
|
||||
@ -85,4 +109,5 @@ class RequestForCommentsController < ApplicationController
|
||||
# we are using the current_user.id here, since internal users are not able to create comments. The external_user.id is a primary key and does not require the consumer_id to be unique.
|
||||
params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -1,7 +1,7 @@
|
||||
class SessionsController < ApplicationController
|
||||
include Lti
|
||||
|
||||
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :require_valid_exercise_token].each do |method_name|
|
||||
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :set_current_user, :require_valid_exercise_token].each do |method_name|
|
||||
before_action(method_name, only: :create_through_lti)
|
||||
end
|
||||
|
||||
@ -18,7 +18,6 @@ class SessionsController < ApplicationController
|
||||
end
|
||||
|
||||
def create_through_lti
|
||||
set_current_user
|
||||
store_lti_session_data(consumer: @consumer, parameters: params)
|
||||
store_nonce(params[:oauth_nonce])
|
||||
redirect_to(implement_exercise_path(@exercise),
|
||||
|
@ -13,6 +13,10 @@ class SubmissionsController < ApplicationController
|
||||
before_action :set_mime_type, only: [:download_file, :render_file]
|
||||
skip_before_action :verify_authenticity_token, only: [:download_file, :render_file]
|
||||
|
||||
def max_message_buffer_size
|
||||
500
|
||||
end
|
||||
|
||||
def authorize!
|
||||
authorize(@submission || @submissions)
|
||||
end
|
||||
@ -156,7 +160,7 @@ class SubmissionsController < ApplicationController
|
||||
tubesock.onmessage do |data|
|
||||
Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data)
|
||||
# Check whether the client send a JSON command and kill container
|
||||
# if the command is 'client_exit', send it to docker otherwise.
|
||||
# if the command is 'client_kill', send it to docker otherwise.
|
||||
begin
|
||||
parsed = JSON.parse(data)
|
||||
if parsed['cmd'] == 'client_kill'
|
||||
@ -183,21 +187,31 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def kill_socket(tubesock)
|
||||
# save the output of this "run" as a "testrun" (scoring runs are saved in submission_scoring.rb)
|
||||
save_run_output
|
||||
|
||||
# Hijacked connection needs to be notified correctly
|
||||
tubesock.send_data JSON.dump({'cmd' => 'exit'})
|
||||
tubesock.close
|
||||
end
|
||||
|
||||
def handle_message(message, tubesock, container)
|
||||
@message_buffer ||= ""
|
||||
# Handle special commands first
|
||||
if (/^exit/.match(message))
|
||||
kill_socket(tubesock)
|
||||
if (/^#exit/.match(message))
|
||||
# Just call exit_container on the docker_client.
|
||||
# Do not call kill_socket for the websocket to the client here.
|
||||
# @docker_client.exit_container closes the socket to the container,
|
||||
# kill_socket is called in the "on close handler" of the websocket to the container
|
||||
@docker_client.exit_container(container)
|
||||
elsif /^#timeout/.match(message)
|
||||
@message_buffer = 'timeout: ' + @message_buffer # add information that this run timed out to the buffer
|
||||
else
|
||||
# Filter out information about run_command, test_command, user or working directory
|
||||
run_command = @submission.execution_environment.run_command % command_substitutions(params[:filename])
|
||||
test_command = @submission.execution_environment.test_command % command_substitutions(params[:filename])
|
||||
if !(/root|workspace|#{run_command}|#{test_command}/.match(message))
|
||||
@message_buffer += message if @message_buffer.size <= max_message_buffer_size
|
||||
parse_message(message, 'stdout', tubesock)
|
||||
end
|
||||
end
|
||||
@ -245,6 +259,13 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def save_run_output
|
||||
if !@message_buffer.blank?
|
||||
@message_buffer = @message_buffer[(0..max_message_buffer_size-1)] # trim the string to max_message_buffer_size chars
|
||||
Testrun.create(file: @file, submission: @submission, output: @message_buffer)
|
||||
end
|
||||
end
|
||||
|
||||
def score
|
||||
hijack do |tubesock|
|
||||
Thread.new { EventMachine.run } unless EventMachine.reactor_running? && EventMachine.reactor_thread.alive?
|
||||
|
55
app/controllers/tags_controller.rb
Normal file
55
app/controllers/tags_controller.rb
Normal file
@ -0,0 +1,55 @@
|
||||
class TagsController < ApplicationController
|
||||
include CommonBehavior
|
||||
|
||||
before_action :set_tag, only: MEMBER_ACTIONS
|
||||
|
||||
def authorize!
|
||||
authorize(@tag || @tags)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@tag = Tag.new(tag_params)
|
||||
authorize!
|
||||
create_and_respond(object: @tag)
|
||||
end
|
||||
|
||||
def destroy
|
||||
destroy_and_respond(object: @tag)
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def tag_params
|
||||
params[:tag].permit(:name)
|
||||
end
|
||||
private :tag_params
|
||||
|
||||
def index
|
||||
@tags = Tag.all.paginate(page: params[:page])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def new
|
||||
@tag = Tag.new
|
||||
authorize!
|
||||
end
|
||||
|
||||
def set_tag
|
||||
@tag = Tag.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_tag
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
update_and_respond(object: @tag, params: tag_params)
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
end
|
115
app/controllers/user_exercise_feedbacks_controller.rb
Normal file
115
app/controllers/user_exercise_feedbacks_controller.rb
Normal file
@ -0,0 +1,115 @@
|
||||
class UserExerciseFeedbacksController < ApplicationController
|
||||
include CommonBehavior
|
||||
|
||||
before_action :set_user_exercise_feedback, only: [:edit, :update]
|
||||
|
||||
def comment_presets
|
||||
[[0,t('user_exercise_feedback.difficulty_easy')],
|
||||
[1,t('user_exercise_feedback.difficulty_some_what_easy')],
|
||||
[2,t('user_exercise_feedback.difficulty_ok')],
|
||||
[3,t('user_exercise_feedback.difficulty_some_what_difficult')],
|
||||
[4,t('user_exercise_feedback.difficult_too_difficult')]]
|
||||
end
|
||||
|
||||
def time_presets
|
||||
[[0,t('user_exercise_feedback.estimated_time_less_5')],
|
||||
[1,t('user_exercise_feedback.estimated_time_5_to_10')],
|
||||
[2,t('user_exercise_feedback.estimated_time_10_to_20')],
|
||||
[3,t('user_exercise_feedback.estimated_time_20_to_30')],
|
||||
[4,t('user_exercise_feedback.estimated_time_more_30')]]
|
||||
end
|
||||
|
||||
def authorize!
|
||||
authorize(@uef)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@exercise = Exercise.find(uef_params[:exercise_id])
|
||||
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first
|
||||
submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first rescue nil
|
||||
|
||||
if @exercise
|
||||
@uef = UserExerciseFeedback.new(uef_params)
|
||||
if validate_inputs(uef_params)
|
||||
authorize!
|
||||
path =
|
||||
if rfc && submission && submission.normalized_score == 1.0
|
||||
request_for_comment_path(rfc)
|
||||
else
|
||||
implement_exercise_path(@exercise)
|
||||
end
|
||||
create_and_respond(object: @uef, path: proc{path})
|
||||
else
|
||||
flash[:danger] = t('shared.message_failure')
|
||||
redirect_to(:back, id: uef_params[:exercise_id])
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def destroy
|
||||
destroy_and_respond(object: @tag)
|
||||
end
|
||||
|
||||
def edit
|
||||
@texts = comment_presets.to_a
|
||||
@times = time_presets.to_a
|
||||
authorize!
|
||||
end
|
||||
|
||||
def uef_params
|
||||
params[:user_exercise_feedback].permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
private :uef_params
|
||||
|
||||
def new
|
||||
@texts = comment_presets.to_a
|
||||
@times = time_presets.to_a
|
||||
@uef = UserExerciseFeedback.new
|
||||
@exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def update
|
||||
submission = current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first rescue nil
|
||||
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first
|
||||
authorize!
|
||||
if @exercise && validate_inputs(uef_params)
|
||||
path =
|
||||
if rfc && submission && submission.normalized_score == 1.0
|
||||
request_for_comment_path(rfc)
|
||||
else
|
||||
implement_exercise_path(@exercise)
|
||||
end
|
||||
update_and_respond(object: @uef, params: uef_params, path: path)
|
||||
else
|
||||
flash[:danger] = t('shared.message_failure')
|
||||
redirect_to(:back, id: uef_params[:exercise_id])
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def set_user_exercise_feedback
|
||||
@exercise = Exercise.find(params[:user_exercise_feedback][:exercise_id])
|
||||
@uef = UserExerciseFeedback.find_by(exercise_id: params[:user_exercise_feedback][:exercise_id], user: current_user)
|
||||
end
|
||||
|
||||
def validate_inputs(uef_params)
|
||||
begin
|
||||
if uef_params[:difficulty].to_i < 0 || uef_params[:difficulty].to_i >= comment_presets.size
|
||||
return false
|
||||
elsif uef_params[:user_estimated_worktime].to_i < 0 || uef_params[:user_estimated_worktime].to_i >= time_presets.size
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
rescue
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
end
|
@ -18,6 +18,6 @@ class UserMailer < ActionMailer::Base
|
||||
@commenting_user_displayname = commenting_user.displayname
|
||||
@comment_text = comment.text
|
||||
@rfc_link = request_for_comment_url(request_for_comment)
|
||||
mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email).deliver
|
||||
mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email)
|
||||
end
|
||||
end
|
||||
|
@ -8,6 +8,11 @@ module User
|
||||
has_many :exercises, as: :user
|
||||
has_many :file_types, as: :user
|
||||
has_many :submissions, as: :user
|
||||
has_many :user_proxy_exercise_exercises, as: :user
|
||||
has_many :user_exercise_interventions, as: :user
|
||||
has_many :interventions, through: :user_exercise_interventions
|
||||
accepts_nested_attributes_for :user_proxy_exercise_exercises
|
||||
|
||||
|
||||
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
|
||||
end
|
||||
@ -21,6 +26,6 @@ module User
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
displayname
|
||||
end
|
||||
end
|
||||
|
@ -12,6 +12,15 @@ class Exercise < ActiveRecord::Base
|
||||
belongs_to :execution_environment
|
||||
has_many :submissions
|
||||
|
||||
has_and_belongs_to_many :proxy_exercises
|
||||
has_many :user_proxy_exercise_exercises
|
||||
has_and_belongs_to_many :exercise_collections
|
||||
has_many :user_exercise_interventions
|
||||
has_many :interventions, through: :user_exercise_interventions
|
||||
has_many :exercise_tags
|
||||
has_many :tags, through: :exercise_tags
|
||||
accepts_nested_attributes_for :exercise_tags
|
||||
|
||||
has_many :external_users, source: :user, source_type: ExternalUser, through: :submissions
|
||||
has_many :internal_users, source: :user, source_type: InternalUser, through: :submissions
|
||||
alias_method :users, :external_users
|
||||
@ -48,17 +57,21 @@ class Exercise < ActiveRecord::Base
|
||||
return user_count == 0 ? 0 : submissions.count() / user_count.to_f()
|
||||
end
|
||||
|
||||
def time_maximum_score(user)
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC, created_at ASC").first.created_at rescue Time.zone.at(0)
|
||||
end
|
||||
|
||||
def user_working_time_query
|
||||
"""
|
||||
SELECT user_id,
|
||||
sum(working_time_new) AS working_time
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
CASE WHEN working_time >= '0:05:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
id,
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id=#{id}) AS foo) AS bar
|
||||
@ -66,6 +79,123 @@ class Exercise < ActiveRecord::Base
|
||||
"""
|
||||
end
|
||||
|
||||
def get_quantiles(quantiles)
|
||||
quantiles_str = "[" + quantiles.join(",") + "]"
|
||||
result = self.class.connection.execute("""
|
||||
WITH working_time AS
|
||||
(
|
||||
SELECT user_id,
|
||||
id,
|
||||
exercise_id,
|
||||
Max(score) AS max_score,
|
||||
(created_at - Lag(created_at) OVER (partition BY user_id, exercise_id ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id = #{id}
|
||||
AND user_type = 'ExternalUser'
|
||||
GROUP BY user_id,
|
||||
id,
|
||||
exercise_id), max_points AS
|
||||
(
|
||||
SELECT context_id AS ex_id,
|
||||
Sum(weight) AS max_points
|
||||
FROM files
|
||||
WHERE context_type = 'Exercise'
|
||||
AND context_id = #{id}
|
||||
AND role = 'teacher_defined_test'
|
||||
GROUP BY context_id),
|
||||
-- filter for rows containing max points
|
||||
time_max_score AS
|
||||
(
|
||||
SELECT *
|
||||
FROM working_time W1,
|
||||
max_points MS
|
||||
WHERE w1.exercise_id = ex_id
|
||||
AND w1.max_score = ms.max_points),
|
||||
-- find row containing the first time max points
|
||||
first_time_max_score AS
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time,
|
||||
rn
|
||||
FROM (
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time,
|
||||
Row_number() OVER(partition BY user_id, exercise_id ORDER BY id ASC) AS rn
|
||||
FROM time_max_score) T
|
||||
WHERE rn = 1), times_until_max_points AS
|
||||
(
|
||||
SELECT w.id,
|
||||
w.user_id,
|
||||
w.exercise_id,
|
||||
w.max_score,
|
||||
w.working_time,
|
||||
m.id AS reachedmax_at
|
||||
FROM working_time W,
|
||||
first_time_max_score M
|
||||
WHERE w.user_id = m.user_id
|
||||
AND w.exercise_id = m.exercise_id
|
||||
AND w.id <= m.id),
|
||||
-- if user never makes it to max points, take all times
|
||||
all_working_times_until_max AS (
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time
|
||||
FROM times_until_max_points)
|
||||
UNION ALL
|
||||
(
|
||||
SELECT id,
|
||||
user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
working_time
|
||||
FROM working_time W1
|
||||
WHERE NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM first_time_max_score F
|
||||
WHERE f.user_id = w1.user_id
|
||||
AND f.exercise_id = w1.exercise_id))), filtered_times_until_max AS
|
||||
(
|
||||
SELECT user_id,
|
||||
exercise_id,
|
||||
max_score,
|
||||
CASE
|
||||
WHEN working_time >= '0:05:00' THEN '0'
|
||||
ELSE working_time
|
||||
END AS working_time_new
|
||||
FROM all_working_times_until_max ), result AS
|
||||
(
|
||||
SELECT e.external_id AS external_user_id,
|
||||
f.user_id,
|
||||
exercise_id,
|
||||
Max(max_score) AS max_score,
|
||||
Sum(working_time_new) AS working_time
|
||||
FROM filtered_times_until_max f,
|
||||
external_users e
|
||||
WHERE f.user_id = e.id
|
||||
GROUP BY e.external_id,
|
||||
f.user_id,
|
||||
exercise_id )
|
||||
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
|
||||
FROM result
|
||||
""")
|
||||
if result.count > 0
|
||||
quantiles.each_with_index.map{|q,i| Time.parse(result[i]["unnest"]).seconds_since_midnight}
|
||||
else
|
||||
quantiles.map{|q| 0}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def retrieve_working_time_statistics
|
||||
@working_time_statistics = {}
|
||||
self.class.connection.execute(user_working_time_query).each do |tuple|
|
||||
@ -88,23 +218,64 @@ class Exercise < ActiveRecord::Base
|
||||
@working_time_statistics[user_id]["working_time"]
|
||||
end
|
||||
|
||||
def average_working_time_for_only(user_id)
|
||||
self.class.connection.execute("""
|
||||
SELECT sum(working_time_new) AS working_time
|
||||
FROM
|
||||
(SELECT CASE WHEN working_time >= '0:30:00' THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT id,
|
||||
(created_at - lag(created_at) over (PARTITION BY user_id
|
||||
def accumulated_working_time_for_only(user)
|
||||
user_type = user.external_user? ? "ExternalUser" : "InternalUser"
|
||||
Time.parse(self.class.connection.execute("""
|
||||
WITH WORKING_TIME AS
|
||||
(SELECT user_id,
|
||||
id,
|
||||
exercise_id,
|
||||
max(score) AS max_score,
|
||||
(created_at - lag(created_at) OVER (PARTITION BY user_id, exercise_id
|
||||
ORDER BY created_at)) AS working_time
|
||||
FROM submissions
|
||||
WHERE exercise_id=#{id} and user_id=#{user_id}) AS foo) AS bar
|
||||
""").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
|
||||
|
||||
def duplicate(attributes = {})
|
||||
exercise = dup
|
||||
exercise.attributes = attributes
|
||||
exercise_tags.each { |et| exercise.exercise_tags << et.dup }
|
||||
files.each { |file| exercise.files << file.dup }
|
||||
exercise
|
||||
end
|
||||
@ -162,9 +333,17 @@ class Exercise < ActiveRecord::Base
|
||||
end
|
||||
private :generate_token
|
||||
|
||||
def maximum_score
|
||||
def maximum_score(user = nil)
|
||||
if user
|
||||
submissions.where(user: user).where("cause IN ('submit','assess')").where("score IS NOT NULL").order("score DESC").first.score || 0 rescue 0
|
||||
else
|
||||
files.teacher_defined_tests.sum(:weight)
|
||||
end
|
||||
end
|
||||
|
||||
def has_user_solved(user)
|
||||
return maximum_score(user).to_i == maximum_score.to_i
|
||||
end
|
||||
|
||||
def set_default_values
|
||||
set_default_values_if_present(public: false)
|
||||
|
5
app/models/exercise_collection.rb
Normal file
5
app/models/exercise_collection.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class ExerciseCollection < ActiveRecord::Base
|
||||
|
||||
has_and_belongs_to_many :exercises
|
||||
|
||||
end
|
13
app/models/exercise_tag.rb
Normal file
13
app/models/exercise_tag.rb
Normal file
@ -0,0 +1,13 @@
|
||||
class ExerciseTag < ActiveRecord::Base
|
||||
|
||||
belongs_to :tag
|
||||
belongs_to :exercise
|
||||
|
||||
before_save :destroy_if_empty_exercise_or_tag
|
||||
|
||||
private
|
||||
def destroy_if_empty_exercise_or_tag
|
||||
destroy if exercise_id.blank? || tag_id.blank?
|
||||
end
|
||||
|
||||
end
|
@ -5,8 +5,8 @@ class ExternalUser < ActiveRecord::Base
|
||||
validates :external_id, presence: true
|
||||
|
||||
def displayname
|
||||
result = name
|
||||
if(consumer.name == 'openHPI')
|
||||
result = "User " + id.to_s
|
||||
if(!consumer.nil? && consumer.name == 'openHPI')
|
||||
result = Rails.cache.fetch("#{cache_key}/displayname", expires_in: 12.hours) do
|
||||
Xikolo::UserClient.get(external_id.to_s)[:display_name]
|
||||
end
|
||||
|
16
app/models/intervention.rb
Normal file
16
app/models/intervention.rb
Normal file
@ -0,0 +1,16 @@
|
||||
class Intervention < ActiveRecord::Base
|
||||
|
||||
has_many :user_exercise_interventions
|
||||
has_many :users, through: :user_exercise_interventions, source_type: "ExternalUser"
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def self.createDefaultInterventions
|
||||
%w(BreakIntervention QuestionIntervention).each do |name|
|
||||
Intervention.find_or_create_by(name: name)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
258
app/models/proxy_exercise.rb
Normal file
258
app/models/proxy_exercise.rb
Normal file
@ -0,0 +1,258 @@
|
||||
class ProxyExercise < ActiveRecord::Base
|
||||
|
||||
after_initialize :generate_token
|
||||
after_initialize :set_reason
|
||||
|
||||
has_and_belongs_to_many :exercises
|
||||
has_many :user_proxy_exercise_exercises
|
||||
|
||||
def count_files
|
||||
exercises.count
|
||||
end
|
||||
|
||||
def set_reason
|
||||
@reason = {}
|
||||
end
|
||||
|
||||
def generate_token
|
||||
self.token ||= SecureRandom.hex(4)
|
||||
end
|
||||
private :generate_token
|
||||
|
||||
def duplicate(attributes = {})
|
||||
proxy_exercise = dup
|
||||
proxy_exercise.attributes = attributes
|
||||
proxy_exercise
|
||||
end
|
||||
|
||||
def to_s
|
||||
title
|
||||
end
|
||||
|
||||
def get_matching_exercise(user)
|
||||
assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first
|
||||
recommended_exercise =
|
||||
if (assigned_user_proxy_exercise)
|
||||
Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" )
|
||||
assigned_user_proxy_exercise.exercise
|
||||
else
|
||||
matching_exercise =
|
||||
if (token.eql? "47f4c736")
|
||||
Rails.logger.debug("Proxy exercise with token 47f4c736, split user in groups..")
|
||||
group = UserGroupSeparator.getGroupWeek2Testing(user)
|
||||
Rails.logger.debug("user assigned to group #{group}")
|
||||
case group
|
||||
when :group_a
|
||||
exercises.where(id: 348).first
|
||||
when :group_b
|
||||
exercises.where(id: 349).first
|
||||
when :group_c
|
||||
exercises.where(id: 350).first
|
||||
when :group_d
|
||||
exercises.where(id: 351).first
|
||||
end
|
||||
else
|
||||
Rails.logger.debug("find new matching exercise for user #{user.id}" )
|
||||
begin
|
||||
find_matching_exercise(user)
|
||||
rescue #fallback
|
||||
Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" )
|
||||
@reason[:reason] = "fallback because of error"
|
||||
@reason[:error] = "#{$!}"
|
||||
exercises.shuffle.first
|
||||
end
|
||||
end
|
||||
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
|
||||
matching_exercise
|
||||
|
||||
|
||||
end
|
||||
recommended_exercise
|
||||
end
|
||||
|
||||
def find_matching_exercise(user)
|
||||
user_group = UserGroupSeparator.getProxyExerciseGroup(user)
|
||||
case user_group
|
||||
when :dummy_assigment
|
||||
rec_ex = select_easiest_exercise(exercises)
|
||||
@reason[:reason] = "dummy group"
|
||||
Rails.logger.debug("assigned user to dummy group, and gave him exercise: #{rec_ex.title}")
|
||||
rec_ex
|
||||
when :random_assigment
|
||||
@reason[:reason] = "random group"
|
||||
ex = exercises.where("expected_difficulty > 1").shuffle.first
|
||||
Rails.logger.debug("assigned user to random group, and gave him exercise: #{ex.title}")
|
||||
ex
|
||||
when :recommended_assignment
|
||||
exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq.compact
|
||||
tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten
|
||||
Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}")
|
||||
|
||||
# find exercises
|
||||
potential_recommended_exercises = []
|
||||
exercises.where("expected_difficulty > 1").each do |ex|
|
||||
## find exercises which have only tags the user has already seen
|
||||
if (ex.tags - tags_user_has_seen).empty?
|
||||
potential_recommended_exercises << ex
|
||||
end
|
||||
end
|
||||
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}")
|
||||
# if all exercises contain tags which the user has never seen, recommend easiest exercise
|
||||
if potential_recommended_exercises.empty?
|
||||
Rails.logger.debug("matched easiest exercise in pool")
|
||||
@reason[:reason] = "easiest exercise in pool. empty potential exercises"
|
||||
select_easiest_exercise(exercises)
|
||||
else
|
||||
recommended_exercise = select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
|
||||
recommended_exercise
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
private :find_matching_exercise
|
||||
|
||||
def select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
|
||||
topic_knowledge_user_and_max = get_user_knowledge_and_max_knowledge(user, exercises_user_has_accessed)
|
||||
Rails.logger.debug("topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}")
|
||||
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.size}: #{potential_recommended_exercises.map{|p| p.id}}")
|
||||
topic_knowledge_user = topic_knowledge_user_and_max[:user_topic_knowledge]
|
||||
topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge]
|
||||
current_users_knowledge_lack = {}
|
||||
topic_knowledge_max.keys.each do |tag|
|
||||
current_users_knowledge_lack[tag] = topic_knowledge_user[tag] / topic_knowledge_max[tag]
|
||||
end
|
||||
|
||||
relative_knowledge_improvement = {}
|
||||
potential_recommended_exercises.each do |potex|
|
||||
tags = potex.tags
|
||||
relative_knowledge_improvement[potex] = 0.0
|
||||
Rails.logger.debug("review potential exercise #{potex.id}")
|
||||
tags.each do |tag|
|
||||
tag_ratio = potex.exercise_tags.where(tag: tag).first.factor.to_f / potex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f
|
||||
max_topic_knowledge_ratio = potex.expected_difficulty * tag_ratio
|
||||
old_relative_loss_tag = topic_knowledge_user[tag] / topic_knowledge_max[tag]
|
||||
new_relative_loss_tag = topic_knowledge_user[tag] / (topic_knowledge_max[tag] + max_topic_knowledge_ratio)
|
||||
Rails.logger.debug("tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, tag_ratio #{tag_ratio}")
|
||||
relative_knowledge_improvement[potex] += old_relative_loss_tag - new_relative_loss_tag
|
||||
end
|
||||
end
|
||||
highest_difficulty_user_has_accessed = exercises_user_has_accessed.map{|e| e.expected_difficulty}.sort.last || 0
|
||||
best_matching_exercise = find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed)
|
||||
@reason[:reason] = "best matching exercise"
|
||||
@reason[:highest_difficulty_user_has_accessed] = highest_difficulty_user_has_accessed
|
||||
@reason[:current_users_knowledge_lack] = current_users_knowledge_lack
|
||||
@reason[:relative_knowledge_improvement] = relative_knowledge_improvement
|
||||
|
||||
Rails.logger.debug("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}.to_s)
|
||||
Rails.logger.debug("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}")
|
||||
best_matching_exercise
|
||||
end
|
||||
private :select_best_matching_exercise
|
||||
|
||||
def find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed)
|
||||
Rails.logger.debug("select most appropiate exercise for user. his highest difficulty was #{highest_difficulty_user_has_accessed}")
|
||||
sorted_exercises = relative_knowledge_improvement.sort_by{|k,v| v}.reverse
|
||||
|
||||
sorted_exercises.each do |ex,diff|
|
||||
Rails.logger.debug("review exercise #{ex.id} diff: #{ex.expected_difficulty}")
|
||||
if (ex.expected_difficulty - highest_difficulty_user_has_accessed) <= 1
|
||||
Rails.logger.debug("matched exercise #{ex.id}")
|
||||
return ex
|
||||
else
|
||||
Rails.logger.debug("exercise #{ex.id} is too difficult")
|
||||
end
|
||||
end
|
||||
easiest_exercise = sorted_exercises.min_by{|k,v| v}.first
|
||||
Rails.logger.debug("no match, select easiest exercise as fallback #{easiest_exercise.id}")
|
||||
easiest_exercise
|
||||
end
|
||||
private :find_best_exercise
|
||||
|
||||
# [score][quantile]
|
||||
def scoring_matrix
|
||||
[
|
||||
[0 ,0 ,0 ,0 ,0 ],
|
||||
[0.2,0.2,0.2,0.2,0.1],
|
||||
[0.5,0.5,0.4,0.4,0.3],
|
||||
[0.6,0.6,0.5,0.5,0.4],
|
||||
[1 ,1 ,0.9,0.8,0.7],
|
||||
]
|
||||
end
|
||||
|
||||
def scoring_matrix_quantiles
|
||||
[0.2,0.4,0.6,0.8]
|
||||
end
|
||||
private :scoring_matrix_quantiles
|
||||
|
||||
def score(user, ex)
|
||||
max_score = ex.maximum_score.to_f
|
||||
if max_score <= 0
|
||||
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: score: 0" )
|
||||
return 0.0
|
||||
end
|
||||
points_ratio = ex.maximum_score(user) / max_score
|
||||
if points_ratio == 0.0
|
||||
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: points_ratio=#{points_ratio} score: 0" )
|
||||
return 0.0
|
||||
end
|
||||
points_ratio_index = ((scoring_matrix.size - 1) * points_ratio).to_i
|
||||
working_time_user = ex.accumulated_working_time_for_only(user)
|
||||
quantiles_working_time = ex.get_quantiles(scoring_matrix_quantiles)
|
||||
quantile_index = quantiles_working_time.size
|
||||
quantiles_working_time.each_with_index do |quantile_time, i|
|
||||
if working_time_user <= quantile_time
|
||||
quantile_index = i
|
||||
break
|
||||
end
|
||||
end
|
||||
Rails.logger.debug(
|
||||
"scoring user #{user.id} exercise #{ex.id}: worktime #{working_time_user}, points: #{points_ratio}" \
|
||||
"(index #{points_ratio_index}) quantiles #{quantiles_working_time} placed into quantile index #{quantile_index} " \
|
||||
"score: #{scoring_matrix[points_ratio_index][quantile_index]}")
|
||||
scoring_matrix[points_ratio_index][quantile_index]
|
||||
end
|
||||
private :score
|
||||
|
||||
def get_user_knowledge_and_max_knowledge(user, exercises)
|
||||
# initialize knowledge for each tag with 0
|
||||
all_used_tags_with_count = {}
|
||||
exercises.each do |ex|
|
||||
ex.tags.each do |t|
|
||||
all_used_tags_with_count[t] ||= 0
|
||||
all_used_tags_with_count[t] += 1
|
||||
end
|
||||
end
|
||||
tags_counter = all_used_tags_with_count.keys.map{|tag| [tag,0]}.to_h
|
||||
topic_knowledge_loss_user = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h
|
||||
topic_knowledge_max = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h
|
||||
exercises_sorted = exercises.sort_by { |ex| ex.time_maximum_score(user)}
|
||||
exercises_sorted.each do |ex|
|
||||
Rails.logger.debug("exercise: #{ex.id}: #{ex}")
|
||||
user_score_factor = score(user, ex)
|
||||
ex.tags.each do |t|
|
||||
tags_counter[t] += 1
|
||||
tag_diminishing_return_factor = tag_diminishing_return_function(tags_counter[t], all_used_tags_with_count[t])
|
||||
tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f
|
||||
Rails.logger.debug("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum += et.factor }}")
|
||||
Rails.logger.debug("tag #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}")
|
||||
Rails.logger.debug("tag_ratio #{tag_ratio}")
|
||||
topic_knowledge_ratio = ex.expected_difficulty * tag_ratio
|
||||
Rails.logger.debug("topic_knowledge_ratio #{topic_knowledge_ratio}")
|
||||
topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio * tag_diminishing_return_factor
|
||||
topic_knowledge_max[t] += topic_knowledge_ratio * tag_diminishing_return_factor
|
||||
end
|
||||
end
|
||||
{user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max}
|
||||
end
|
||||
private :get_user_knowledge_and_max_knowledge
|
||||
|
||||
def tag_diminishing_return_function(count_tag, total_count_tag)
|
||||
total_count_tag += 1 # bonus exercise comes on top
|
||||
return 1/(1+(Math::E**(-3/(0.5*total_count_tag)*(count_tag-0.5*total_count_tag))))
|
||||
end
|
||||
|
||||
def select_easiest_exercise(exercises)
|
||||
exercises.order(:expected_difficulty).first
|
||||
end
|
||||
|
||||
end
|
4
app/models/search.rb
Normal file
4
app/models/search.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class Search < ActiveRecord::Base
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :exercise
|
||||
end
|
22
app/models/tag.rb
Normal file
22
app/models/tag.rb
Normal file
@ -0,0 +1,22 @@
|
||||
class Tag < ActiveRecord::Base
|
||||
|
||||
has_many :exercise_tags
|
||||
has_many :exercises, through: :exercise_tags
|
||||
|
||||
validates_uniqueness_of :name
|
||||
|
||||
def destroy
|
||||
if (can_be_destroyed?)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def can_be_destroyed?
|
||||
!exercises.any?
|
||||
end
|
||||
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
end
|
11
app/models/user_exercise_feedback.rb
Normal file
11
app/models/user_exercise_feedback.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class UserExerciseFeedback < ActiveRecord::Base
|
||||
include Creation
|
||||
|
||||
belongs_to :exercise
|
||||
|
||||
validates :user_id, uniqueness: { scope: [:exercise_id, :user_type] }
|
||||
|
||||
def to_s
|
||||
"User Exercise Feedback"
|
||||
end
|
||||
end
|
11
app/models/user_exercise_intervention.rb
Normal file
11
app/models/user_exercise_intervention.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class UserExerciseIntervention < ActiveRecord::Base
|
||||
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :intervention
|
||||
belongs_to :exercise
|
||||
|
||||
validates :user, presence: true
|
||||
validates :exercise, presence: true
|
||||
validates :intervention, presence: true
|
||||
|
||||
end
|
14
app/models/user_proxy_exercise_exercise.rb
Normal file
14
app/models/user_proxy_exercise_exercise.rb
Normal file
@ -0,0 +1,14 @@
|
||||
class UserProxyExerciseExercise < ActiveRecord::Base
|
||||
|
||||
belongs_to :user, polymorphic: true
|
||||
belongs_to :exercise
|
||||
belongs_to :proxy_exercise
|
||||
|
||||
validates :user_id, presence: true
|
||||
validates :user_type, presence: true
|
||||
validates :exercise_id, presence: true
|
||||
validates :proxy_exercise_id, presence: true
|
||||
|
||||
validates :user_id, uniqueness: { scope: [:proxy_exercise_id, :user_type] }
|
||||
|
||||
end
|
@ -16,7 +16,7 @@ class ExercisePolicy < AdminOrAuthorPolicy
|
||||
define_method(action) { admin? || author?}
|
||||
end
|
||||
|
||||
[:implement?, :submit?, :reload?].each do |action|
|
||||
[:implement?, :working_times?, :intervention?, :search?, :submit?, :reload?].each do |action|
|
||||
define_method(action) { everyone }
|
||||
end
|
||||
|
||||
|
34
app/policies/intervention_policy.rb
Normal file
34
app/policies/intervention_policy.rb
Normal file
@ -0,0 +1,34 @@
|
||||
class InterventionPolicy < AdminOrAuthorPolicy
|
||||
def author?
|
||||
@user == @record.author
|
||||
end
|
||||
private :author?
|
||||
|
||||
def batch_update?
|
||||
admin?
|
||||
end
|
||||
|
||||
def show?
|
||||
@user.internal_user?
|
||||
end
|
||||
|
||||
[:clone?, :destroy?, :edit?, :update?].each do |action|
|
||||
define_method(action) { admin? || author?}
|
||||
end
|
||||
|
||||
[:reload?].each do |action|
|
||||
define_method(action) { everyone }
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
if @user.admin?
|
||||
@scope.all
|
||||
elsif @user.internal_user?
|
||||
@scope.where('user_id = ? OR public = TRUE', @user.id)
|
||||
else
|
||||
@scope.none
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
34
app/policies/proxy_exercise_policy.rb
Normal file
34
app/policies/proxy_exercise_policy.rb
Normal file
@ -0,0 +1,34 @@
|
||||
class ProxyExercisePolicy < AdminOrAuthorPolicy
|
||||
def author?
|
||||
@user == @record.author
|
||||
end
|
||||
private :author?
|
||||
|
||||
def batch_update?
|
||||
admin?
|
||||
end
|
||||
|
||||
def show?
|
||||
@user.internal_user?
|
||||
end
|
||||
|
||||
[:clone?, :destroy?, :edit?, :update?].each do |action|
|
||||
define_method(action) { admin? || author?}
|
||||
end
|
||||
|
||||
[:reload?].each do |action|
|
||||
define_method(action) { everyone }
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
if @user.admin?
|
||||
@scope.all
|
||||
elsif @user.internal_user?
|
||||
@scope.where('user_id = ? OR public = TRUE', @user.id)
|
||||
else
|
||||
@scope.none
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -8,6 +8,10 @@ class RequestForCommentPolicy < ApplicationPolicy
|
||||
everyone
|
||||
end
|
||||
|
||||
def search?
|
||||
everyone
|
||||
end
|
||||
|
||||
def show?
|
||||
everyone
|
||||
end
|
||||
@ -27,4 +31,8 @@ class RequestForCommentPolicy < ApplicationPolicy
|
||||
def index?
|
||||
everyone
|
||||
end
|
||||
|
||||
def create_comment_exercise?
|
||||
everyone
|
||||
end
|
||||
end
|
||||
|
34
app/policies/search_policy.rb
Normal file
34
app/policies/search_policy.rb
Normal file
@ -0,0 +1,34 @@
|
||||
class SearchPolicy < AdminOrAuthorPolicy
|
||||
def author?
|
||||
@user == @record.author
|
||||
end
|
||||
private :author?
|
||||
|
||||
def batch_update?
|
||||
admin?
|
||||
end
|
||||
|
||||
def show?
|
||||
@user.internal_user?
|
||||
end
|
||||
|
||||
[:clone?, :destroy?, :edit?, :update?].each do |action|
|
||||
define_method(action) { admin? || author?}
|
||||
end
|
||||
|
||||
[:reload?].each do |action|
|
||||
define_method(action) { everyone }
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
if @user.admin?
|
||||
@scope.all
|
||||
elsif @user.internal_user?
|
||||
@scope.where('user_id = ? OR public = TRUE', @user.id)
|
||||
else
|
||||
@scope.none
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
34
app/policies/tag_policy.rb
Normal file
34
app/policies/tag_policy.rb
Normal file
@ -0,0 +1,34 @@
|
||||
class TagPolicy < AdminOrAuthorPolicy
|
||||
def author?
|
||||
@user == @record.author
|
||||
end
|
||||
private :author?
|
||||
|
||||
def batch_update?
|
||||
admin?
|
||||
end
|
||||
|
||||
def show?
|
||||
@user.internal_user?
|
||||
end
|
||||
|
||||
[:clone?, :destroy?, :edit?, :update?].each do |action|
|
||||
define_method(action) { admin? || author?}
|
||||
end
|
||||
|
||||
[:reload?].each do |action|
|
||||
define_method(action) { everyone }
|
||||
end
|
||||
|
||||
class Scope < Scope
|
||||
def resolve
|
||||
if @user.admin?
|
||||
@scope.all
|
||||
elsif @user.internal_user?
|
||||
@scope.where('user_id = ? OR public = TRUE', @user.id)
|
||||
else
|
||||
@scope.none
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
19
app/policies/user_exercise_feedback_policy.rb
Normal file
19
app/policies/user_exercise_feedback_policy.rb
Normal file
@ -0,0 +1,19 @@
|
||||
class UserExerciseFeedbackPolicy < ApplicationPolicy
|
||||
def author?
|
||||
@user == @record.author
|
||||
end
|
||||
private :author?
|
||||
|
||||
def create?
|
||||
everyone
|
||||
end
|
||||
|
||||
def new?
|
||||
everyone
|
||||
end
|
||||
|
||||
[:show? ,:destroy?, :edit?, :update?].each do |action|
|
||||
define_method(action) { admin? || author?}
|
||||
end
|
||||
|
||||
end
|
@ -1,3 +1,3 @@
|
||||
#flash data-message-failure=t('shared.message_failure')
|
||||
#flash.fixed_error_messages.clickthrough data-message-failure=t('shared.message_failure')
|
||||
- %w[alert danger info notice success warning].each do |severity|
|
||||
p.alert.flash class="alert-#{{'alert' => 'warning', 'notice' => 'success'}.fetch(severity, severity)}" id="flash-#{severity}" = flash[severity]
|
||||
|
@ -5,5 +5,5 @@ textarea.form-control(style='resize:none;')
|
||||
h5 =t('exercises.implement.comment.others')
|
||||
pre#otherCommentsTextfield
|
||||
p = ''
|
||||
button#addCommentButton.btn.btn-block.btn-primary(type='button') =t('exercises.implement.comment.addComment')
|
||||
button#addCommentButton.btn.btn-block.btn-primary(type='button') =t('exercises.implement.comment.addCommentButton')
|
||||
button#removeAllButton.btn.btn-block.btn-warning(type='button') =t('exercises.implement.comment.removeAllOnLine')
|
@ -1,7 +1,9 @@
|
||||
- external_user_external_id = @current_user.respond_to?(:external_id) ? @current_user.external_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
|
||||
- external_user_id = @current_user.respond_to?(:external_id) ? @current_user.id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
|
||||
- consumer_id = @current_user.respond_to?(:external_id) ? @current_user.consumer_id : '' #'tests' #(@current_user.uuid.present? ? @current_user.uuid : '')
|
||||
#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id
|
||||
- show_break_interventions = @show_break_interventions || "false"
|
||||
- show_rfc_interventions = @show_rfc_interventions || "false"
|
||||
#editor.row data-exercise-id=exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-errors-url=execution_environment_errors_path(exercise.execution_environment) data-submissions-url=submissions_path data-user-id=@current_user.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path data-intervention-save-url=intervention_exercise_path data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-course_token=@course_token data-search-save-url=search_exercise_path
|
||||
div id="sidebar" class=(@exercise.hide_file_tree ? 'sidebar-col-collapsed' : 'sidebar-col') = render('editor_file_tree', exercise: @exercise, files: @files)
|
||||
div id='output_sidebar' class='output-col-collapsed' = render('exercises/editor_output', external_user_id: external_user_id, consumer_id: consumer_id )
|
||||
div id='frames' class='editor-col'
|
||||
@ -22,3 +24,4 @@
|
||||
|
||||
|
||||
= render('shared/modal', id: 'comment-modal', title: t('exercises.implement.comment.request'), template: 'exercises/_request_comment_dialogcontent')
|
||||
= render('shared/modal', id: 'break-intervention-modal', title: t('exercises.implement.break_intervention.title'), template: 'interventions/_break_intervention_modal')
|
@ -6,6 +6,8 @@ div id='sidebar-collapsed' class=(@exercise.hide_file_tree ? '' : 'hidden')
|
||||
|
||||
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-download', id: 'download-collapsed', label:'', title: t('exercises.editor.download'))
|
||||
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise), :'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-history', id: 'start-over-collapsed', label:'', title: t('exercises.editor.start_over'))
|
||||
- if !@course_token.blank?
|
||||
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', data: {:'data-toggle' => 'tooltip', :'data-placement' => 'right'}, icon: 'fa fa-search', id: 'sidebar-search-collapsed', label: '', title: t('search.search_in_forum'))
|
||||
|
||||
div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '')
|
||||
= render('editor_button', classes: 'btn-block btn-primary btn-sm', icon: 'fa fa-minus-square', id: 'sidebar-collapse', label: t('exercises.editor.collapse_action_sidebar'))
|
||||
@ -24,5 +26,13 @@ div id='sidebar-uncollapsed' class=(@exercise.hide_file_tree ? 'hidden' : '')
|
||||
= render('editor_button', classes: 'btn-block btn-primary btn-sm enforce-top-margin', icon: 'fa fa-download', id: 'download', label: t('exercises.editor.download'))
|
||||
= render('editor_button', classes: 'btn-block btn-primary btn-sm', data: {:'data-message-confirm' => t('exercises.editor.confirm_start_over'), :'data-url' => reload_exercise_path(@exercise)}, icon: 'fa fa-history', id: 'start-over', label: t('exercises.editor.start_over'))
|
||||
|
||||
- if !@course_token.blank?
|
||||
.input-group.enforce-top-margin
|
||||
.enforce-right-margin
|
||||
= text_field_tag 'search-input-text', nil, placeholder: t('search.search_in_forum'), class: 'form-control'
|
||||
.input-group-btn
|
||||
= button_tag(class: 'btn btn-primary', id: 'btn-search-col') do
|
||||
i.fa.fa-search
|
||||
|
||||
- if @exercise.allow_file_creation?
|
||||
= render('shared/modal', id: 'modal-file', template: 'code_ocean/files/_form', title: t('exercises.editor.create_file'))
|
@ -32,6 +32,25 @@
|
||||
label
|
||||
= f.check_box(:allow_auto_completion)
|
||||
= t('activerecord.attributes.exercise.allow_auto_completion')
|
||||
.form-group
|
||||
= f.label(t('activerecord.attributes.exercise.difficulty'))
|
||||
= f.number_field :expected_difficulty, in: 1..10, step: 1
|
||||
.form-group
|
||||
= f.label(t('activerecord.attributes.exercise.worktime'))
|
||||
= f.number_field "expected_worktime_minutes", value: @exercise.expected_worktime_seconds / 60, in: 1..1000, step: 1
|
||||
h2 Tags
|
||||
.table-responsive
|
||||
table.table#tags-table
|
||||
thead
|
||||
tr
|
||||
th = t('activerecord.attributes.exercise.selection')
|
||||
th = sort_link(@search, :title, t('activerecord.attributes.tag.name'))
|
||||
th = t('activerecord.attributes.tag.difficulty')
|
||||
= collection_check_boxes :exercise, :tag_ids, @exercise_tags, :tag_id, :id do |b|
|
||||
tr
|
||||
td = b.check_box
|
||||
td = b.object.tag.name
|
||||
td = number_field "tag_factors[#{b.object.tag.id}]", :factor, :value => b.object.factor, in: 1..10, step: 1
|
||||
h2 = t('activerecord.attributes.exercise.files')
|
||||
ul#files.list-unstyled.panel-group
|
||||
= f.fields_for :files do |files_form|
|
||||
|
@ -1,6 +1,11 @@
|
||||
h5#rfc_intervention_text style='display: none;' = t('exercises.implement.rfc_intervention.text')
|
||||
h5 = t('exercises.implement.comment.question')
|
||||
|
||||
|
||||
textarea.form-control#question(style='resize:none;')
|
||||
p = ''
|
||||
/ data-cause='requestComments' is not used here right now, we pass the button #requestComments (not askForCommentsButton) as initiator of the action.
|
||||
/ But if we use this button, it will work since the correct cause is supplied
|
||||
div
|
||||
button#askForCommentsButton.btn.btn-block.btn-primary(type='button' data-cause='requestComments' data-message-success=t('exercises.editor.request_for_comments_sent')) =t('exercises.implement.comment.request')
|
||||
button#closeAskForCommentsButton.btn.btn-block.btn-warning(type='button') =t('activerecord.attributes.request_for_comments.close')
|
||||
|
@ -1,6 +1,8 @@
|
||||
h1 = "#{@exercise} (external user #{@external_user})"
|
||||
- submissions = Submission.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id).order("created_at")
|
||||
- current_submission = submissions.first
|
||||
- submissions_and_interventions = (submissions + UserExerciseIntervention.where("user_id = ? AND exercise_id = ?", @external_user.id, @exercise.id)).sort_by { |a| a.created_at }
|
||||
|
||||
- if current_submission
|
||||
- initial_files = current_submission.files.to_a
|
||||
|
||||
@ -41,20 +43,26 @@ h1 = "#{@exercise} (external user #{@external_user})"
|
||||
- ['.time', '.cause', '.score', '.tests', '.time_difference'].each do |title|
|
||||
th.header = t(title)
|
||||
tbody
|
||||
- deltas = submissions.map.with_index {|item, index| delta = item.created_at - submissions[index - 1].created_at if index > 0; if delta == nil or delta > 30*60 then 0 else delta end}
|
||||
- submissions.each_with_index do |submission, index|
|
||||
tr data-id=submission.id
|
||||
td.clickable = submission.created_at.strftime("%F %T")
|
||||
td = submission.cause
|
||||
td = submission.score
|
||||
- deltas = submissions.map.with_index {|item, index| delta = item.created_at - submissions[index - 1].created_at if index > 0; if delta == nil or delta > 10*60 then 0 else delta end}
|
||||
- submissions_and_interventions.each_with_index do |submission_or_intervention, index|
|
||||
tr data-id=submission_or_intervention.id
|
||||
td.clickable = submission_or_intervention.created_at.strftime("%F %T")
|
||||
- if submission_or_intervention.is_a?(Submission)
|
||||
td = submission_or_intervention.cause
|
||||
td = submission_or_intervention.score
|
||||
td
|
||||
-submission.testruns.each do |run|
|
||||
-submission_or_intervention.testruns.each do |run|
|
||||
- if run.passed
|
||||
.unit-test-result.positive-result title=run.output
|
||||
- else
|
||||
.unit-test-result.negative-result title=run.output
|
||||
td = Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0
|
||||
-working_times_until.push((Time.at(deltas[1..index].inject(:+)).utc.strftime("%H:%M:%S") if index > 0))
|
||||
- elsif submission_or_intervention.is_a? UserExerciseIntervention
|
||||
td = submission_or_intervention.intervention.name
|
||||
td =
|
||||
td =
|
||||
td =
|
||||
p = t('.addendum')
|
||||
.hidden#wtimes data-working_times=ActiveSupport::JSON.encode(working_times_until);
|
||||
div#progress_chart.col-lg-12
|
||||
|
@ -22,3 +22,4 @@
|
||||
#questions-column
|
||||
#questions-holder data-url="#{qa_url}/qa/index/#{@exercise.id}/#{@user_id}"
|
||||
= qa_js_tag
|
||||
|
||||
|
@ -16,6 +16,9 @@ h1 = Exercise.model_name.human(count: 2)
|
||||
th = sort_link(@search, :execution_environment_id, t('activerecord.attributes.exercise.execution_environment'))
|
||||
th = t('.test_files')
|
||||
th = t('activerecord.attributes.exercise.maximum_score')
|
||||
th = t('activerecord.attributes.exercise.tags')
|
||||
th = t('activerecord.attributes.exercise.difficulty')
|
||||
th = t('activerecord.attributes.exercise.worktime')
|
||||
th
|
||||
= t('activerecord.attributes.exercise.public')
|
||||
- if policy(Exercise).batch_update?
|
||||
@ -29,6 +32,9 @@ h1 = Exercise.model_name.human(count: 2)
|
||||
td = link_to_if(exercise.execution_environment && policy(exercise.execution_environment).show?, exercise.execution_environment, exercise.execution_environment)
|
||||
td = exercise.files.teacher_defined_tests.count
|
||||
td = exercise.maximum_score
|
||||
td = exercise.exercise_tags.count
|
||||
td = exercise.expected_difficulty
|
||||
td = (exercise.expected_worktime_seconds / 60).ceil
|
||||
td.public data-value=exercise.public? = symbol_for(exercise.public?)
|
||||
td = link_to(t('shared.edit'), edit_exercise_path(exercise)) if policy(exercise).edit?
|
||||
td = link_to(t('.implement'), implement_exercise_path(exercise)) if policy(exercise).implement?
|
||||
|
@ -19,6 +19,9 @@ h1
|
||||
= row(label: 'exercise.allow_auto_completion', value: @exercise.allow_auto_completion?)
|
||||
= row(label: 'exercise.embedding_parameters') do
|
||||
= content_tag(:input, nil, class: 'form-control', readonly: true, value: embedding_parameters(@exercise))
|
||||
= row(label: 'exercise.difficulty', value: @exercise.expected_difficulty)
|
||||
= row(label: 'exercise.worktime', value: "#{@exercise.expected_worktime_seconds/60} min")
|
||||
= row(label: 'exercise.tags', value: @exercise.exercise_tags.map{|et| "#{et.tag.name} (#{et.factor})"}.sort.join(", "))
|
||||
|
||||
h2 = t('activerecord.attributes.exercise.files')
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
h5 = t('exercises.implement.break_intervention.text')
|
6
app/views/interventions/_form.html.slim
Normal file
6
app/views/interventions/_form.html.slim
Normal file
@ -0,0 +1,6 @@
|
||||
= form_for(@intervention) do |f|
|
||||
= render('shared/form_errors', object: @intervention)
|
||||
.form-group
|
||||
= f.label(:name)
|
||||
= f.text_field(:name, class: 'form-control', required: true)
|
||||
.actions = render('shared/submit_button', f: f, object: @intervention)
|
14
app/views/interventions/index.html.slim
Normal file
14
app/views/interventions/index.html.slim
Normal file
@ -0,0 +1,14 @@
|
||||
h1 = Intervention.model_name.human(count: 2)
|
||||
|
||||
.table-responsive
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th = t('activerecord.attributes.intervention.name')
|
||||
tbody
|
||||
- @interventions.each do |intervention|
|
||||
tr
|
||||
td = intervention.name
|
||||
td = link_to(t('shared.show'), intervention)
|
||||
|
||||
= render('shared/pagination', collection: @interventions)
|
4
app/views/interventions/show.html.slim
Normal file
4
app/views/interventions/show.html.slim
Normal file
@ -0,0 +1,4 @@
|
||||
h1
|
||||
= @intervention.name
|
||||
|
||||
= row(label: 'intervention.name', value: @intervention.name)
|
24
app/views/proxy_exercises/_form.html.slim
Normal file
24
app/views/proxy_exercises/_form.html.slim
Normal file
@ -0,0 +1,24 @@
|
||||
= form_for(@proxy_exercise, multipart: true) do |f|
|
||||
= render('shared/form_errors', object: @proxy_exercise)
|
||||
.form-group
|
||||
= f.label(:title)
|
||||
= f.text_field(:title, class: 'form-control', required: true)
|
||||
.form-group
|
||||
= f.label(:description)
|
||||
= f.pagedown_editor :description
|
||||
|
||||
h3 Exercises
|
||||
.table-responsive
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th = t('activerecord.attributes.exercise.selection')
|
||||
th = sort_link(@search, :title, t('activerecord.attributes.submission.exercise'))
|
||||
th = sort_link(@search, :created_at, t('shared.created_at'))
|
||||
= collection_check_boxes :proxy_exercise, :exercise_ids, @exercises, :id, :title do |b|
|
||||
tr
|
||||
td = b.check_box
|
||||
td = link_to(b.object, b.object)
|
||||
td = l(b.object.created_at, format: :short)
|
||||
|
||||
.actions = render('shared/submit_button', f: f, object: @proxy_exercise)
|
3
app/views/proxy_exercises/edit.html.slim
Normal file
3
app/views/proxy_exercises/edit.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = t('activerecord.models.proxy_exercise.one', model: ProxyExercise.model_name.human)+ ": " + @proxy_exercise.title
|
||||
|
||||
= render('form')
|
35
app/views/proxy_exercises/index.html.slim
Normal file
35
app/views/proxy_exercises/index.html.slim
Normal file
@ -0,0 +1,35 @@
|
||||
h1 = ProxyExercise.model_name.human(count: 2)
|
||||
|
||||
= render(layout: 'shared/form_filters') do |f|
|
||||
.form-group
|
||||
= f.label(:title_cont, t('activerecord.attributes.proxy_exercise.title'), class: 'sr-only')
|
||||
= f.search_field(:title_cont, class: 'form-control', placeholder: t('activerecord.attributes.proxy_exercise.title'))
|
||||
|
||||
.table-responsive
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th = sort_link(@search, :title, t('activerecord.attributes.proxy_exercise.title'))
|
||||
th = "Token"
|
||||
th = t('activerecord.attributes.proxy_exercise.files_count')
|
||||
th colspan=6 = t('shared.actions')
|
||||
tbody
|
||||
- @proxy_exercises.each do |proxy_exercise|
|
||||
tr data-id=proxy_exercise.id
|
||||
td = link_to(proxy_exercise.title,proxy_exercise)
|
||||
td = proxy_exercise.token
|
||||
td = proxy_exercise.count_files
|
||||
td = link_to(t('shared.edit'), edit_proxy_exercise_path(proxy_exercise)) if policy(proxy_exercise).edit?
|
||||
|
||||
td
|
||||
.btn-group
|
||||
button.btn.btn-primary-outline.btn-xs.dropdown-toggle data-toggle="dropdown" type="button" = t('shared.actions_button')
|
||||
span.caret
|
||||
span.sr-only Toggle Dropdown
|
||||
ul.dropdown-menu.pull-right role="menu"
|
||||
li = link_to(t('shared.show'), proxy_exercise) if policy(proxy_exercise).show?
|
||||
li = link_to(t('shared.destroy'), proxy_exercise, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if policy(proxy_exercise).destroy?
|
||||
li = link_to(t('.clone'), clone_proxy_exercise_path(proxy_exercise), data: {confirm: t('shared.confirm_destroy')}, method: :post) if policy(proxy_exercise).clone?
|
||||
|
||||
= render('shared/pagination', collection: @proxy_exercises)
|
||||
p = render('shared/new_button', model: ProxyExercise)
|
3
app/views/proxy_exercises/new.html.slim
Normal file
3
app/views/proxy_exercises/new.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = t('shared.new_model', model: ProxyExercise.model_name.human)
|
||||
|
||||
= render('form')
|
3
app/views/proxy_exercises/reload.json.jbuilder
Normal file
3
app/views/proxy_exercises/reload.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
json.set! :files do
|
||||
json.array! @exercise.files.visible, :content, :id
|
||||
end
|
23
app/views/proxy_exercises/show.html.slim
Normal file
23
app/views/proxy_exercises/show.html.slim
Normal file
@ -0,0 +1,23 @@
|
||||
- content_for :head do
|
||||
= javascript_include_tag('http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/highlight.min.js')
|
||||
= stylesheet_link_tag('http://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.4/styles/default.min.css')
|
||||
|
||||
h1
|
||||
= @proxy_exercise.title
|
||||
- if policy(@proxy_exercise).edit?
|
||||
= render('shared/edit_button', object: @proxy_exercise)
|
||||
|
||||
= row(label: 'exercise.title', value: @proxy_exercise.title)
|
||||
= row(label: 'proxy_exercise.files_count', value: @exercises.count)
|
||||
= row(label: 'exercise.description', value: @proxy_exercise.description)
|
||||
h3 Exercises
|
||||
.table-responsive
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th = sort_link(@search, :title, t('activerecord.attributes.submission.exercise'))
|
||||
th = sort_link(@search, :created_at, t('shared.created_at'))
|
||||
- @proxy_exercise.exercises.each do |exercise|
|
||||
tr
|
||||
td = link_to(exercise.title, exercise)
|
||||
td = l(exercise.created_at, format: :short)
|
@ -1,12 +1,20 @@
|
||||
h1 = RequestForComment.model_name.human(count: 2)
|
||||
|
||||
= render(layout: 'shared/form_filters') do |f|
|
||||
.form-group
|
||||
= f.label(:exercise_title_cont, t('activerecord.attributes.request_for_comments.exercise'), class: 'sr-only')
|
||||
= f.search_field(:exercise_title_cont, class: 'form-control', placeholder: t('activerecord.attributes.request_for_comments.exercise'))
|
||||
.form-group
|
||||
= f.label(:title_cont, t('request_for_comments.solved'), class: 'sr-only')
|
||||
= f.select(:solved_not_eq, [[t('request_for_comments.show_all'), 2], [t('request_for_comments.show_unsolved'), 1], [t('request_for_comments.show_solved'), 0]])
|
||||
|
||||
.table-responsive
|
||||
table.table.sortable
|
||||
thead
|
||||
tr
|
||||
th
|
||||
i class="fa fa-lightbulb-o" aria-hidden="true" title = t('request_for_comments.solved') align="right"
|
||||
th = t('activerecord.attributes.request_for_comments.exercise')
|
||||
th = sort_link(@search, :title, t('activerecord.attributes.request_for_comments.exercise'))
|
||||
th = t('activerecord.attributes.request_for_comments.question')
|
||||
th
|
||||
i class="fa fa-comment" aria-hidden="true" title = t('request_for_comments.comments') align="center"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="list-group">
|
||||
<h4 id ="exercise_caption" class="list-group-item-heading" data-rfc-id = "<%= @request_for_comment.id %>" ><%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %></h4>
|
||||
<h4 id ="exercise_caption" class="list-group-item-heading" data-exercise-id="<%=@request_for_comment.exercise.id%>" data-comment-exercise-url="<%=create_comment_exercise_request_for_comment_path%>" data-rfc-id = "<%= @request_for_comment.id %>" ><%= link_to(@request_for_comment.exercise.title, [:implement, @request_for_comment.exercise]) %></h4>
|
||||
<p class="list-group-item-text">
|
||||
<%
|
||||
user = @request_for_comment.user
|
||||
@ -20,14 +20,17 @@
|
||||
<u><%= t('activerecord.attributes.request_for_comments.question')%>:</u> <%= t('request_for_comments.no_question') %>
|
||||
<% end %>
|
||||
</h5>
|
||||
|
||||
<% if (policy(@request_for_comment).mark_as_solved? and not @request_for_comment.solved?) %>
|
||||
<button class="btn btn-default" id="mark-as-solved-button"><%= t('request_for_comments.mark_as_solved') %></button>
|
||||
<button class="btn btn-primary" id="mark-as-solved-button"><%= t('request_for_comments.mark_as_solved') %></button>
|
||||
<% elsif (@request_for_comment.solved?) %>
|
||||
<button type="button" class="btn btn-success"><%= t('request_for_comments.solved') %></button>
|
||||
<% else %>
|
||||
|
||||
<% end %>
|
||||
|
||||
|
||||
|
||||
<% if @current_user.admin? && user.is_a?(ExternalUser) %>
|
||||
<br>
|
||||
<br>
|
||||
@ -41,14 +44,18 @@
|
||||
</ul>
|
||||
</h5>
|
||||
<% end %>
|
||||
<h5>
|
||||
<u><%= t('request_for_comments.howto_title') %></u><br> <%= render_markdown(t('request_for_comments.howto')) %>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<!--
|
||||
do not put a carriage return in the line below. it will be present in the presentation of the source code, otherwise.
|
||||
also, all settings from the rails model needed for the editor configuration in the JavaScript are attached to the editor as data attributes here.
|
||||
-->
|
||||
<% submission.files.each do |file| %>
|
||||
<%= (file.path or "") + "/" + file.name + file.file_type.file_extension %>
|
||||
<%= (file.path or "") + "/" + file.name + file.file_type.file_extension %><br>
|
||||
<i class="fa fa-arrow-down" aria-hidden="true"></i> <%= t('request_for_comments.click_here') %>
|
||||
<div id='commentitor' class='editor' data-read-only='true' data-file-id='<%=file.id%>' data-mode='<%=file.file_type.editor_mode%>'><%= file.content %>
|
||||
</div>
|
||||
<% end %>
|
||||
@ -58,6 +65,8 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
<script type="text/javascript">
|
||||
|
||||
var solvedButton = $('#mark-as-solved-button');
|
||||
var commentOnExerciseButton = $('#comment-exercise-button');
|
||||
var addCommentExerciseButton = $('#addCommentExerciseButton')
|
||||
|
||||
solvedButton.on('click', function(event){
|
||||
var jqrequest = $.ajax({
|
||||
@ -107,7 +116,14 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
jqrequest.done(function(response){
|
||||
$.each(response, function(index, comment) {
|
||||
comment.className = "code-ocean_comment";
|
||||
comment.text = comment.username + ": " + comment.text
|
||||
|
||||
// if we have tabs or carriage returns in the comment, just add the name and leave it as it is. otherwise: format!
|
||||
if(comment.text.includes("\n") || comment.text.includes("\t")){
|
||||
comment.text = comment.username + ": " + comment.text;
|
||||
} else {
|
||||
comment.text = comment.username + ": " + stringDivider(comment.text, 80, "\n\t\t");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
session.setAnnotations(response);
|
||||
@ -161,6 +177,27 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
function createCommentOnExercise(file_id, row, editor, commenttext){
|
||||
var jqxhr = $.ajax({
|
||||
data: {
|
||||
comment: {
|
||||
file_id: file_id,
|
||||
row: row,
|
||||
column: 0,
|
||||
text: commenttext,
|
||||
request_id: $('h4#exercise_caption').data('rfc-id')
|
||||
}
|
||||
},
|
||||
dataType: 'json',
|
||||
method: 'POST',
|
||||
url: "/comments"
|
||||
});
|
||||
jqxhr.done(function(response){
|
||||
setAnnotations(editor, file_id);
|
||||
});
|
||||
jqxhr.fail(ajaxError);
|
||||
}
|
||||
|
||||
function handleSidebarClick(e) {
|
||||
var target = e.domEvent.target;
|
||||
var editor = e.editor;
|
||||
@ -193,6 +230,7 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
|
||||
if (commenttext !== "") {
|
||||
createComment(file_id, row, editor, commenttext);
|
||||
commentModal.find('textarea').val('') ;
|
||||
commentModal.modal('hide');
|
||||
}
|
||||
});
|
||||
@ -213,4 +251,20 @@ also, all settings from the rails model needed for the editor configuration in t
|
||||
text: message.length > 0 ? message : $('#flash').data('message-failure')
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function stringDivider(str, width, spaceReplacer) {
|
||||
if (str.length>width) {
|
||||
var p=width
|
||||
for (;p>0 && str[p]!=' ';p--) {
|
||||
}
|
||||
if (p>0) {
|
||||
var left = str.substring(0, p);
|
||||
var right = str.substring(p+1);
|
||||
return left + spaceReplacer + stringDivider(right, width, spaceReplacer);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
0
app/views/searches/destroy.html.erb
Normal file
0
app/views/searches/destroy.html.erb
Normal file
@ -5,6 +5,6 @@
|
||||
= row(label: 'file.hidden', value: file.hidden)
|
||||
= row(label: 'file.read_only', value: file.read_only)
|
||||
- if file.teacher_defined_test?
|
||||
= row(label: 'file.feedback_message', value: file.feedback_message)
|
||||
= row(label: 'file.feedback_message', value: render_markdown(file.feedback_message))
|
||||
= row(label: 'file.weight', value: file.weight)
|
||||
= row(label: 'file.content', value: file.native_file? ? link_to(file.native_file.file.filename, file.native_file.url) : code_tag(file.content))
|
||||
|
6
app/views/tags/_form.html.slim
Normal file
6
app/views/tags/_form.html.slim
Normal file
@ -0,0 +1,6 @@
|
||||
= form_for(@tag) do |f|
|
||||
= render('shared/form_errors', object: @tag)
|
||||
.form-group
|
||||
= f.label(:name)
|
||||
= f.text_field(:name, class: 'form-control', required: true)
|
||||
.actions = render('shared/submit_button', f: f, object: @tag)
|
3
app/views/tags/edit.html.slim
Normal file
3
app/views/tags/edit.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = @tag.name
|
||||
|
||||
= render('form')
|
19
app/views/tags/index.html.slim
Normal file
19
app/views/tags/index.html.slim
Normal file
@ -0,0 +1,19 @@
|
||||
h1 = Tag.model_name.human(count: 2)
|
||||
|
||||
.table-responsive
|
||||
table.table
|
||||
thead
|
||||
tr
|
||||
th = t('activerecord.attributes.hint.name')
|
||||
/th = t('activerecord.attributes.hint.locale')
|
||||
/th colspan=3 = t('shared.actions')
|
||||
tbody
|
||||
- @tags.each do |tag|
|
||||
tr
|
||||
td = tag.name
|
||||
td = link_to(t('shared.show'), tag)
|
||||
td = link_to(t('shared.edit'), edit_tag_path(tag))
|
||||
td = link_to(t('shared.destroy'), tag, data: {confirm: t('shared.confirm_destroy')}, method: :delete) if tag.can_be_destroyed?
|
||||
|
||||
= render('shared/pagination', collection: @tags)
|
||||
p = render('shared/new_button', model: Tag, path: new_tag_path)
|
3
app/views/tags/new.html.slim
Normal file
3
app/views/tags/new.html.slim
Normal file
@ -0,0 +1,3 @@
|
||||
h1 = t('shared.new_model', model: Tag.model_name.human)
|
||||
|
||||
= render('form')
|
6
app/views/tags/show.html.slim
Normal file
6
app/views/tags/show.html.slim
Normal file
@ -0,0 +1,6 @@
|
||||
h1
|
||||
= @tag.name
|
||||
= render('shared/edit_button', object: @tag)
|
||||
|
||||
= row(label: 'tag.name', value: @tag.name)
|
||||
= row(label: 'tag.usage', value: @tag.exercises.count)
|
23
app/views/user_exercise_feedbacks/_form.html.slim
Normal file
23
app/views/user_exercise_feedbacks/_form.html.slim
Normal file
@ -0,0 +1,23 @@
|
||||
= form_for(@uef) do |f|
|
||||
div
|
||||
span.badge.pull-right.score
|
||||
|
||||
h1 id="exercise-headline"
|
||||
= t('activerecord.models.user_exercise_feedback.one') + " "
|
||||
= link_to(@exercise.title, [:implement, @exercise])
|
||||
= render('shared/form_errors', object: @uef)
|
||||
h4
|
||||
== t('user_exercise_feedback.description')
|
||||
#description-panel.lead.description-panel
|
||||
u = t('activerecord.attributes.exercise.description')
|
||||
= render_markdown(@exercise.description)
|
||||
.form-group
|
||||
= f.text_area(:feedback_text, class: 'form-control', required: true, :rows => "10")
|
||||
h4 = t('user_exercise_feedback.difficulty')
|
||||
= f.collection_radio_buttons :difficulty, @texts, :first, :last, html_options={class: "radio-inline"} do |b|
|
||||
= b.label(:class => 'radio') { b.radio_button + b.text }
|
||||
h4 = t('user_exercise_feedback.working_time')
|
||||
= f.collection_radio_buttons :user_estimated_worktime, @times, :first, :last, html_options={class: "radio-inline"} do |b|
|
||||
= b.label(:class => 'radio') { b.radio_button + b.text }
|
||||
= f.hidden_field(:exercise_id, :value => @exercise.id)
|
||||
.actions = render('shared/submit_button', f: f, object: @uef)
|
1
app/views/user_exercise_feedbacks/edit.html.slim
Normal file
1
app/views/user_exercise_feedbacks/edit.html.slim
Normal file
@ -0,0 +1 @@
|
||||
= render('form')
|
1
app/views/user_exercise_feedbacks/new.html.slim
Normal file
1
app/views/user_exercise_feedbacks/new.html.slim
Normal file
@ -0,0 +1 @@
|
||||
= render('form')
|
@ -1 +1 @@
|
||||
== t('mailers.user_mailer.got_new_comment.body', receiver_displayname: @receiver_displayname, link: link_to(@rfc_link, @rfc_link), commenting_user_displayname: @commenting_user_displayname, comment_text: @comment_text)
|
||||
== t('mailers.user_mailer.got_new_comment.body', receiver_displayname: @receiver_displayname, link_to_comment: link_to(@rfc_link, @rfc_link), commenting_user_displayname: @commenting_user_displayname, comment_text: @comment_text, link_my_comments: link_to(t('request_for_comments.index.get_my_comment_requests'), my_request_for_comments_url), link_all_comments: link_to(t('request_for_comments.index.all'), request_for_comments_url) )
|
||||
|
@ -28,7 +28,7 @@ module CodeOcean
|
||||
config.eager_load_paths << Rails.root.join('lib')
|
||||
config.assets.precompile += %w( markdown-buttons.png )
|
||||
|
||||
config.active_record.schema_format = :sql
|
||||
#config.active_record.schema_format = :sql
|
||||
|
||||
case (RUBY_ENGINE)
|
||||
when 'ruby'
|
||||
|
@ -4,7 +4,7 @@ set :default_env, 'PATH' => '/usr/java/jdk1.8.0_40/bin:$PATH'
|
||||
set :deploy_to, '/var/www/app'
|
||||
set :keep_releases, 3
|
||||
set :linked_dirs, %w(log public/uploads tmp/cache tmp/files tmp/pids tmp/sockets)
|
||||
set :linked_files, %w(config/action_mailer.yml config/code_ocean.yml config/database.yml config/newrelic.yml config/secrets.yml config/sendmail.yml config/smtp.yml)
|
||||
set :linked_files, %w(config/action_mailer.yml config/docker.yml.erb config/code_ocean.yml config/database.yml config/newrelic.yml config/secrets.yml config/sendmail.yml config/smtp.yml)
|
||||
set :log_level, :info
|
||||
set :puma_threads, [0, 16]
|
||||
set :repo_url, 'git@github.com:openHPI/codeocean.git'
|
||||
|
@ -32,7 +32,7 @@ production:
|
||||
timeout: 60
|
||||
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
|
||||
ws_host: ws://localhost:4243 #url to connect rails server to docker host
|
||||
ws_client_protocol: wss:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
|
||||
ws_client_protocol: 'wss:' #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
|
||||
|
||||
staging:
|
||||
<<: *default
|
||||
@ -46,7 +46,7 @@ staging:
|
||||
timeout: 60
|
||||
workspace_root: <%= Rails.root.join('tmp', 'files', Rails.env) %>
|
||||
ws_host: ws://localhost:4243 #url to connect rails server to docker host
|
||||
ws_client_protocol: wss:// #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
|
||||
ws_client_protocol: 'wss:' #set the websocket protocol to be used by the client to connect to the rails server (ws on development, wss on production)
|
||||
|
||||
test:
|
||||
<<: *default
|
||||
|
@ -27,6 +27,7 @@ de:
|
||||
exercise:
|
||||
description: Beschreibung
|
||||
embedding_parameters: Parameter für LTI-Einbettung
|
||||
tags: Tags
|
||||
execution_environment: Ausführungsumgebung
|
||||
execution_environment_id: Ausführungsumgebung
|
||||
files: Dateien
|
||||
@ -34,10 +35,16 @@ de:
|
||||
instructions: Anweisungen
|
||||
maximum_score: Erreichbare Punktzahl
|
||||
public: Öffentlich
|
||||
selection: Ausgewählt
|
||||
title: Titel
|
||||
user: Autor
|
||||
allow_auto_completion: "Autovervollständigung aktivieren"
|
||||
allow_file_creation: "Dateierstellung erlauben"
|
||||
difficulty: Schwierigkeitsgrad
|
||||
worktime: "vermutete Arbeitszeit in Minuten"
|
||||
proxy_exercise:
|
||||
title: Title
|
||||
files_count: Anzahl der Aufgaben
|
||||
external_user:
|
||||
consumer: Konsument
|
||||
email: E-Mail
|
||||
@ -68,6 +75,8 @@ de:
|
||||
message: Nachricht
|
||||
name: Name
|
||||
regular_expression: Regulärer Ausdruck
|
||||
intervention:
|
||||
name: Name
|
||||
internal_user:
|
||||
activated: Aktiviert
|
||||
consumer: Konsument
|
||||
@ -84,6 +93,7 @@ de:
|
||||
username: Benutzername
|
||||
requested_at: Angefragezeitpunkt
|
||||
question: "Frage"
|
||||
close: "Fenster schließen"
|
||||
submission:
|
||||
cause: Anlass
|
||||
code: Code
|
||||
@ -91,6 +101,10 @@ de:
|
||||
files: Dateien
|
||||
score: Punktzahl
|
||||
user: Autor
|
||||
tag:
|
||||
name: Name
|
||||
usage: Verwendet
|
||||
difficulty: Anteil an der Aufgabe
|
||||
file_template:
|
||||
name: "Name"
|
||||
file_type: "Dateityp"
|
||||
@ -111,6 +125,9 @@ de:
|
||||
exercise:
|
||||
one: Aufgabe
|
||||
other: Aufgaben
|
||||
proxy_exercise:
|
||||
one: Proxy Aufgabe
|
||||
other: Proxy Aufgaben
|
||||
external_user:
|
||||
one: Externer Nutzer
|
||||
other: Externe Nutzer
|
||||
@ -129,9 +146,15 @@ de:
|
||||
internal_user:
|
||||
one: Interner Nutzer
|
||||
other: Interne Nutzer
|
||||
request_for_comment:
|
||||
one: Kommentaranfrage
|
||||
other: Kommentaranfragen
|
||||
submission:
|
||||
one: Abgabe
|
||||
other: Abgaben
|
||||
user_exercise_feedback:
|
||||
one: Feedback
|
||||
other: Feedback
|
||||
errors:
|
||||
messages:
|
||||
together: 'muss zusammen mit %{attribute} definiert werden'
|
||||
@ -254,12 +277,19 @@ de:
|
||||
line: Zeile
|
||||
dialogtitle: Kommentar hinzufügen
|
||||
others: Andere Kommentare auf dieser Zeile
|
||||
addCommentExercise: Aufgabe kommentieren
|
||||
addyours: Fügen Sie Ihren Kommentar hinzu
|
||||
addComment: Kommentieren
|
||||
addComment: Hier haben Sie die Möglichkeit Ihr Feedback zu dieser Aufgabe zu geben. Dies bezieht sich ausdrücklich NICHT auf die hier sichtbare Lösung des Teilnehmers, sondern nur auf die Aufgabe. Fanden Sie die Aufgabe zu leicht oder schwer? War die Beschreibung der Aufgabe verständlich und vollständig?
|
||||
addCommentButton: Kommentar abschicken
|
||||
removeAllOnLine: Meine Kommentare auf dieser Zeile löschen
|
||||
listing: Die neuesten Kommentaranfragen
|
||||
request: "Kommentaranfrage stellen"
|
||||
question: "Bitte beschreiben Sie kurz ihre Problem oder nennen Sie den Programmteil, zu dem sie Feedback wünschen."
|
||||
question: "Bitte beschreiben Sie kurz ihre Probleme oder nennen Sie den Programmteil, zu dem Sie Feedback wünschen."
|
||||
rfc_intervention:
|
||||
text: "Es scheint so als würden Sie Probleme mit der Aufgabe haben. Wenn Sie möchten, können wir Ihnen helfen!"
|
||||
break_intervention:
|
||||
title: "Pause"
|
||||
text: "Uns ist aufgefallen, dass du schon lange an dieser Aufgabe arbeitest. Möchtest du vielleicht später weiter machen um erstmal auf neue Gedanken zu kommen?"
|
||||
index:
|
||||
clone: Duplizieren
|
||||
implement: Implementieren
|
||||
@ -290,6 +320,9 @@ de:
|
||||
tests: Unit Tests
|
||||
time_difference: 'Arbeitszeit bis hier*'
|
||||
addendum: '* Differenzen von mehr als 30 Minuten werden ignoriert.'
|
||||
proxy_exercises:
|
||||
index:
|
||||
clone: Duplizieren
|
||||
external_users:
|
||||
statistics:
|
||||
title: Statistiken für Externe Benutzer
|
||||
@ -327,6 +360,8 @@ de:
|
||||
success: Sie haben Ihr Passwort erfolgreich geändert.
|
||||
show:
|
||||
link: Profil
|
||||
search:
|
||||
search_in_forum: "Probleme? Suche hier im Forum"
|
||||
locales:
|
||||
de: Deutsch
|
||||
en: Englisch
|
||||
@ -343,7 +378,15 @@ de:
|
||||
Hallo %{receiver_displayname}, <br>
|
||||
<br>
|
||||
es gibt einen neuen Kommentar von %{commenting_user_displayname} zu Ihrer Kommentaranfrage auf CodeOcean. <br>
|
||||
Sie finden ihn hier: %{link} <br>
|
||||
<br>
|
||||
%{commenting_user_displayname} schreibt: %{comment_text}<br>
|
||||
<br>
|
||||
Sie finden ihre Kommentaranfrage hier: %{link_to_comment} <br>
|
||||
<br>
|
||||
Falls Sie beim Klick auf diesen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.<br>
|
||||
<br>
|
||||
Eine Übersicht Ihrer Kommentaranfragen gibt es hier: %{link_my_comments} <br>
|
||||
Alle Kommentaranfragen aller Benutzer finden Sie hier: %{link_all_comments} <br>
|
||||
<br>
|
||||
Diese Mail wurde automatisch von CodeOcean verschickt.<br>
|
||||
<br>
|
||||
@ -352,7 +395,15 @@ de:
|
||||
Dear %{receiver_displayname}, <br>
|
||||
<br>
|
||||
you received a new comment from %{commenting_user_displayname} to your request for comments on CodeOcean. <br>
|
||||
You can find it here: %{link} <br>
|
||||
<br>
|
||||
%{commenting_user_displayname} wrote: %{comment_text} <br>
|
||||
<br>
|
||||
You can find your request for comments here: %{link_to_comment} <br>
|
||||
<br>
|
||||
If you receive an error that you are not authorized to perform this action when clicking the link, please log-in through any course exercise beforehand and click the link again. <br>
|
||||
<br>
|
||||
An overview of all your comments can be accessed here: %{link_my_comments} <br>
|
||||
All comments of all participants are available here: %{link_all_comments} <br>
|
||||
<br>
|
||||
This mail was automatically sent by CodeOcean. <br>
|
||||
subject: Sie haben einen neuen Kommentar von %{commenting_user_displayname} auf CodeOcean erhalten.
|
||||
@ -360,13 +411,23 @@ de:
|
||||
body: 'Bitte besuchen Sie %{link}, sofern Sie Ihr Passwort zurücksetzen wollen.'
|
||||
subject: Anweisungen zum Zurücksetzen Ihres Passworts
|
||||
request_for_comments:
|
||||
click_here: Zum Kommentieren auf die Seitenleiste klicken!
|
||||
comments: Kommentare
|
||||
howto: |
|
||||
Um Kommentare zu einer Programmzeile hinzuzufügen, kann einfach auf die jeweilige Zeilennummer auf der linken Seite geklickt werden. <br>
|
||||
Es öffnet sich ein Textfeld, in dem der Kommentar eingetragen werden kann. <br>
|
||||
Mit "Kommentieren" wird der Kommentar dann gesichert und taucht als Sprechblase neben der Zeile auf.
|
||||
howto_title: 'Anleitung:'
|
||||
index:
|
||||
get_my_comment_requests: Meine Kommentaranfragen
|
||||
all: "Alle Kommentaranfragen"
|
||||
no_question: "Der Autor hat keine Frage zu dieser Anfrage gestellt."
|
||||
mark_as_solved: "Diese Frage als beantwortet markieren"
|
||||
show_all: "Alle Anfragen anzeigen"
|
||||
show_solved: "Nur gelöste Anfragen anzeigen"
|
||||
show_unsolved: "Nur ungelöste Anfragen anzeigen"
|
||||
solved: "Diese Frage wurde erfolgreich beantwortet"
|
||||
comment_exercise: "Ich möchte die Aufgabenstellung kommentieren"
|
||||
sessions:
|
||||
create:
|
||||
failure: Fehlerhafte E-Mail oder Passwort.
|
||||
@ -467,3 +528,18 @@ de:
|
||||
previous_label: '← Vorherige Seite'
|
||||
file_template:
|
||||
no_template_label: "Leere Datei"
|
||||
user_exercise_feedback:
|
||||
difficulty_easy: "Die Aufgabe war zu einfach"
|
||||
difficulty_some_what_easy: "Die Aufgabe war etwas zu einfach"
|
||||
difficulty_ok: "Die Aufgabe war gut zu lösen"
|
||||
difficulty_some_what_difficult: "Die Aufgabe war etwas zu schwer"
|
||||
difficult_too_difficult: "Die Aufgabe war zu schwer"
|
||||
difficulty: "Schwierigkeit der Aufgabe:"
|
||||
description: "Ihre Punkte wurden übertragen. Wir würden uns freuen, wenn Sie uns hier Feedback zur Aufgabe geben.<br> Wenn sie das nicht möchten, können Sie das Fenster auch einfach schließen.<br><br>Bitte beschreiben Sie, was Ihnen an der Aufgabe gefallen hat und was nicht. Gab es Schwierigkeiten bei der Aufgabe? War die Aufgabe zu leicht oder zu schwer?<br>Wir freuen uns über jedes Feedback."
|
||||
estimated_time_less_5: "weniger als 5 Minuten"
|
||||
estimated_time_5_to_10: "zwischen 5 und 10 Minuten"
|
||||
estimated_time_10_to_20: "zwischen 10 und 20 Minuten"
|
||||
estimated_time_20_to_30: "zwischen 20 und 30 Minuten"
|
||||
estimated_time_more_30: "mehr als 30 Minuten"
|
||||
working_time: "Geschätze Bearbeitungszeit für diese Aufgabe:"
|
||||
|
||||
|
@ -48,6 +48,7 @@ en:
|
||||
exercise:
|
||||
description: Description
|
||||
embedding_parameters: LTI Embedding Parameters
|
||||
tags: Tags
|
||||
execution_environment: Execution Environment
|
||||
execution_environment_id: Execution Environment
|
||||
files: Files
|
||||
@ -55,10 +56,16 @@ en:
|
||||
instructions: Instructions
|
||||
maximum_score: Maximum Score
|
||||
public: Public
|
||||
selection: Selected
|
||||
title: Title
|
||||
user: Author
|
||||
allow_auto_completion: "Allow auto completion"
|
||||
allow_file_creation: "Allow file creation"
|
||||
difficulty: Difficulty
|
||||
worktime: "Expected worktime in minutes"
|
||||
proxy_exercise:
|
||||
title: Title
|
||||
files_count: Exercises Count
|
||||
external_user:
|
||||
consumer: Consumer
|
||||
email: Email
|
||||
@ -89,6 +96,8 @@ en:
|
||||
message: Message
|
||||
name: Name
|
||||
regular_expression: Regular Expression
|
||||
intervention:
|
||||
name: Name
|
||||
internal_user:
|
||||
activated: Activated
|
||||
consumer: Consumer
|
||||
@ -105,6 +114,7 @@ en:
|
||||
username: Username
|
||||
requested_at: Request Date
|
||||
question: "Question"
|
||||
close: Close window
|
||||
submission:
|
||||
cause: Cause
|
||||
code: Code
|
||||
@ -112,6 +122,10 @@ en:
|
||||
files: Files
|
||||
score: Score
|
||||
user: Author
|
||||
tag:
|
||||
name: Name
|
||||
usage: Used
|
||||
difficulty: Share on the Exercise
|
||||
file_template:
|
||||
name: "Name"
|
||||
file_type: "File Type"
|
||||
@ -132,6 +146,9 @@ en:
|
||||
exercise:
|
||||
one: Exercise
|
||||
other: Exercises
|
||||
proxy_exercise:
|
||||
one: Proxy Exercise
|
||||
other: Proxy Exercises
|
||||
external_user:
|
||||
one: External User
|
||||
other: External Users
|
||||
@ -150,9 +167,15 @@ en:
|
||||
internal_user:
|
||||
one: Internal User
|
||||
other: Internal Users
|
||||
request_for_comment:
|
||||
one: Request for Comments
|
||||
other: Requests for Comments
|
||||
submission:
|
||||
one: Submission
|
||||
other: Submissions
|
||||
user_exercise_feedback:
|
||||
one: Feedback
|
||||
other: Feedback
|
||||
errors:
|
||||
messages:
|
||||
together: 'has to be set along with %{attribute}'
|
||||
@ -276,11 +299,18 @@ en:
|
||||
dialogtitle: Comment on this line
|
||||
others: Other comments on this line
|
||||
addyours: Add your comment
|
||||
addComment: Comment this
|
||||
addCommentExercise: Comment this exercise
|
||||
addComment: You can give feedback to this exercise. Keep in mind that this should refer to the exercise and not to the solution of the participant. Did you find this exercise particulary easy or difficult? Was the description sufficient?
|
||||
addCommentButton: Comment this
|
||||
removeAllOnLine: Remove my comments on this line
|
||||
listing: Listing the newest comment requests
|
||||
request: "Request Comments"
|
||||
question: "Please shortly describe your problem or the program part you would like to get feedback for."
|
||||
rfc_intervention:
|
||||
text: "It looks like you may struggle with this exercise. If you like we can help you out!"
|
||||
break_intervention:
|
||||
title: "Break"
|
||||
text: "We recognized that you are already working quite a while on this exercise. We would like to encourage you to take a break and come back later."
|
||||
index:
|
||||
clone: Duplicate
|
||||
implement: Implement
|
||||
@ -311,6 +341,9 @@ en:
|
||||
tests: Unit Test Results
|
||||
time_difference: 'Working Time until here*'
|
||||
addendum: '* Deltas longer than 30 minutes are ignored.'
|
||||
proxy_exercises:
|
||||
index:
|
||||
clone: Duplicate
|
||||
external_users:
|
||||
statistics:
|
||||
title: External User Statistics
|
||||
@ -348,6 +381,8 @@ en:
|
||||
success: You successfully changed your password.
|
||||
show:
|
||||
link: Profile
|
||||
search:
|
||||
search_in_forum: "Problems? Search here in forum"
|
||||
locales:
|
||||
de: German
|
||||
en: English
|
||||
@ -364,7 +399,15 @@ en:
|
||||
Hallo %{receiver_displayname}, <br>
|
||||
<br>
|
||||
es gibt einen neuen Kommentar von %{commenting_user_displayname} zu Ihrer Kommentaranfrage auf CodeOcean. <br>
|
||||
Sie finden ihn hier: %{link} <br>
|
||||
<br>
|
||||
%{commenting_user_displayname} schreibt: %{comment_text}<br>
|
||||
<br>
|
||||
Sie finden ihre Kommentaranfrage hier: %{link_to_comment} <br>
|
||||
<br>
|
||||
Falls Sie beim Klick auf diesen Link eine Fehlermeldung erhalten, dass Sie nicht berechtigt wären diese Aktion auszuführen, öffnen Sie bitte eine beliebige Programmieraufgabe aus einem Kurs heraus und klicken den Link danach noch einmal.<br>
|
||||
<br>
|
||||
Eine Übersicht Ihrer Kommentaranfragen gibt es hier: %{link_my_comments} <br>
|
||||
Alle Kommentaranfragen aller Benutzer finden Sie hier: %{link_all_comments} <br>
|
||||
<br>
|
||||
Diese Mail wurde automatisch von CodeOcean verschickt.<br>
|
||||
<br>
|
||||
@ -373,7 +416,15 @@ en:
|
||||
Dear %{receiver_displayname}, <br>
|
||||
<br>
|
||||
you received a new comment from %{commenting_user_displayname} to your request for comments on CodeOcean. <br>
|
||||
You can find it here: %{link} <br>
|
||||
<br>
|
||||
%{commenting_user_displayname} wrote: %{comment_text} <br>
|
||||
<br>
|
||||
You can find your request for comments here: %{link_to_comment} <br>
|
||||
<br>
|
||||
If you receive an error that you are not authorized to perform this action when clicking the link, please log-in through any course exercise beforehand and click the link again. <br>
|
||||
<br>
|
||||
An overview of all your comments can be accessed here: %{link_my_comments} <br>
|
||||
All comments of all participants are available here: %{link_all_comments} <br>
|
||||
<br>
|
||||
This mail was automatically sent by CodeOcean. <br>
|
||||
subject: 'You received a new comment on CodeOcean from %{commenting_user_displayname}.'
|
||||
@ -381,13 +432,23 @@ en:
|
||||
body: 'Please visit %{link} if you want to reset your password.'
|
||||
subject: Password reset instructions
|
||||
request_for_comments:
|
||||
click_here: Click on this sidebar to comment!
|
||||
comments: Comments
|
||||
howto: |
|
||||
To leave comments to a specific code line, click on the respective line number. <br>
|
||||
Enter your comment in the popup and save it by clicking "Comment this". <br>
|
||||
Your comment will show up next to the line number as a speech bubble symbol.
|
||||
howto_title: 'How to comment:'
|
||||
index:
|
||||
all: All Requests for Comments
|
||||
get_my_comment_requests: My Requests for Comments
|
||||
no_question: "The author did not enter a question for this request."
|
||||
mark_as_solved: "Mark this question as answered"
|
||||
show_all: "All requests"
|
||||
show_solved: "Solved requests"
|
||||
show_unsolved: "Unvsolved requests"
|
||||
solved: "This question has been answered"
|
||||
comment_exercise: "I would like to give feedback for this exercise"
|
||||
sessions:
|
||||
create:
|
||||
failure: Invalid email or password.
|
||||
@ -488,4 +549,17 @@ en:
|
||||
previous_label: '← Previous Page'
|
||||
file_template:
|
||||
no_template_label: "Empty File"
|
||||
|
||||
user_exercise_feedback:
|
||||
difficulty_easy: "the exercise was too easy"
|
||||
difficulty_some_what_easy: "the exercise was somewhat easy"
|
||||
difficulty_ok: "the difficulty of the exercise was just right"
|
||||
difficulty_some_what_difficult: "the exercise was somewhat difficult"
|
||||
difficult_too_difficult: "the exercise was too difficult"
|
||||
difficulty: "Difficulty of the exercise:"
|
||||
description: "Your points have been submitted. We kindly ask you for feedback for this exercise. <br> If you do not want to give feedback you can simply close this window.<br><br>Please describe what you liked on this exercise and what you did not. Was the exercise easy to understand or did you have problems understanding? How was the difficulty of the exercise to you?<br>We are happy about any feedback."
|
||||
estimated_time_less_5: "less than 5 minutes"
|
||||
estimated_time_5_to_10: "between 5 and 10 minutes"
|
||||
estimated_time_10_to_20: "between 10 and 20 minutes"
|
||||
estimated_time_20_to_30: "between 20 and 30 minutes"
|
||||
estimated_time_more_30: "more than 30 minutes"
|
||||
working_time: "Estimated time working on this exercise:"
|
||||
|
@ -10,6 +10,7 @@ Rails.application.routes.draw do
|
||||
resources :request_for_comments do
|
||||
member do
|
||||
get :mark_as_solved
|
||||
post :create_comment_exercise
|
||||
end
|
||||
end
|
||||
resources :comments, except: [:destroy] do
|
||||
@ -60,12 +61,46 @@ Rails.application.routes.draw do
|
||||
member do
|
||||
post :clone
|
||||
get :implement
|
||||
get :working_times
|
||||
post :intervention
|
||||
post :search
|
||||
get :statistics
|
||||
get :reload
|
||||
post :submit
|
||||
end
|
||||
end
|
||||
|
||||
resources :proxy_exercises do
|
||||
member do
|
||||
post :clone
|
||||
get :reload
|
||||
post :submit
|
||||
end
|
||||
end
|
||||
|
||||
resources :tags do
|
||||
member do
|
||||
post :clone
|
||||
get :reload
|
||||
post :submit
|
||||
end
|
||||
end
|
||||
|
||||
resources :user_exercise_feedbacks do
|
||||
member do
|
||||
get :reload
|
||||
post :submit
|
||||
end
|
||||
end
|
||||
|
||||
resources :interventions do
|
||||
member do
|
||||
post :clone
|
||||
get :reload
|
||||
post :submit
|
||||
end
|
||||
end
|
||||
|
||||
resources :external_users, only: [:index, :show], concerns: :statistics do
|
||||
resources :exercises, concerns: :statistics
|
||||
end
|
||||
|
14
db/migrate/20170205163247_create_exercise_collections.rb
Normal file
14
db/migrate/20170205163247_create_exercise_collections.rb
Normal file
@ -0,0 +1,14 @@
|
||||
class CreateExerciseCollections < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :exercise_collections do |t|
|
||||
t.string :name
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
create_table :exercise_collections_exercises, id: false do |t|
|
||||
t.belongs_to :exercise_collection, index: true
|
||||
t.belongs_to :exercise, index: true
|
||||
end
|
||||
|
||||
end
|
||||
end
|
23
db/migrate/20170205165450_create_proxy_exercises.rb
Normal file
23
db/migrate/20170205165450_create_proxy_exercises.rb
Normal file
@ -0,0 +1,23 @@
|
||||
class CreateProxyExercises < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :proxy_exercises do |t|
|
||||
t.string :title
|
||||
t.string :description
|
||||
t.string :token
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
create_table :exercises_proxy_exercises, id: false do |t|
|
||||
t.belongs_to :proxy_exercise, index: true
|
||||
t.belongs_to :exercise, index: true
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
create_table :user_proxy_exercise_exercises do |t|
|
||||
t.belongs_to :user, polymorphic: true, index: true
|
||||
t.belongs_to :proxy_exercise, index: true
|
||||
t.belongs_to :exercise, index: true
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
23
db/migrate/20170205210357_create_interventions.rb
Normal file
23
db/migrate/20170205210357_create_interventions.rb
Normal file
@ -0,0 +1,23 @@
|
||||
class CreateInterventions < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :user_exercise_interventions do |t|
|
||||
t.belongs_to :user, polymorphic: true
|
||||
t.belongs_to :exercise
|
||||
t.belongs_to :intervention
|
||||
t.integer :accumulated_worktime_s
|
||||
t.text :reason
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
create_table :interventions do |t|
|
||||
t.string :name
|
||||
t.text :markup
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
Intervention.createDefaultInterventions
|
||||
|
||||
end
|
||||
|
||||
|
||||
end
|
19
db/migrate/20170206141210_add_tags.rb
Normal file
19
db/migrate/20170206141210_add_tags.rb
Normal file
@ -0,0 +1,19 @@
|
||||
class AddTags < ActiveRecord::Migration
|
||||
|
||||
def change
|
||||
add_column :exercises, :expected_worktime_seconds, :integer, default: 60
|
||||
add_column :exercises, :expected_difficulty, :integer, default: 1
|
||||
|
||||
create_table :tags do |t|
|
||||
t.string :name, null: false
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
create_table :exercise_tags do |t|
|
||||
t.belongs_to :exercise
|
||||
t.belongs_to :tag
|
||||
t.integer :factor, default: 1
|
||||
end
|
||||
end
|
||||
|
||||
end
|
11
db/migrate/20170206152503_add_user_feedback.rb
Normal file
11
db/migrate/20170206152503_add_user_feedback.rb
Normal file
@ -0,0 +1,11 @@
|
||||
class AddUserFeedback < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :user_exercise_feedbacks do |t|
|
||||
t.belongs_to :exercise, null: false
|
||||
t.belongs_to :user, polymorphic: true, null: false
|
||||
t.integer :difficulty
|
||||
t.integer :working_time_seconds
|
||||
t.string :feedback_text
|
||||
end
|
||||
end
|
||||
end
|
10
db/migrate/20170228165741_add_search.rb
Normal file
10
db/migrate/20170228165741_add_search.rb
Normal file
@ -0,0 +1,10 @@
|
||||
class AddSearch < ActiveRecord::Migration
|
||||
def change
|
||||
create_table :searches do |t|
|
||||
t.belongs_to :exercise, null: false
|
||||
t.belongs_to :user, polymorphic: true, null: false
|
||||
t.string :search
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,7 @@
|
||||
class AddReasonToUserProxyExerciseExercise < ActiveRecord::Migration
|
||||
def change
|
||||
change_table :user_proxy_exercise_exercises do |t|
|
||||
t.string :reason
|
||||
end
|
||||
end
|
||||
end
|
6
db/migrate/20170323130756_add_index_to_submissions.rb
Normal file
6
db/migrate/20170323130756_add_index_to_submissions.rb
Normal file
@ -0,0 +1,6 @@
|
||||
class AddIndexToSubmissions < ActiveRecord::Migration
|
||||
def change
|
||||
add_index :submissions, :exercise_id
|
||||
add_index :submissions, :user_id
|
||||
end
|
||||
end
|
@ -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
|
5
db/migrate/20170411090543_improve_user_feedback.rb
Normal file
5
db/migrate/20170411090543_improve_user_feedback.rb
Normal file
@ -0,0 +1,5 @@
|
||||
class ImproveUserFeedback < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :user_exercise_feedbacks, :user_estimated_worktime, :integer
|
||||
end
|
||||
end
|
103
db/schema.rb
103
db/schema.rb
@ -11,7 +11,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20161214144837) do
|
||||
ActiveRecord::Schema.define(version: 20170403162848) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@ -76,6 +76,26 @@ ActiveRecord::Schema.define(version: 20161214144837) do
|
||||
t.boolean "network_enabled"
|
||||
end
|
||||
|
||||
create_table "exercise_collections", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
create_table "exercise_collections_exercises", id: false, force: :cascade do |t|
|
||||
t.integer "exercise_collection_id"
|
||||
t.integer "exercise_id"
|
||||
end
|
||||
|
||||
add_index "exercise_collections_exercises", ["exercise_collection_id"], name: "index_exercise_collections_exercises_on_exercise_collection_id", using: :btree
|
||||
add_index "exercise_collections_exercises", ["exercise_id"], name: "index_exercise_collections_exercises_on_exercise_id", using: :btree
|
||||
|
||||
create_table "exercise_tags", force: :cascade do |t|
|
||||
t.integer "exercise_id"
|
||||
t.integer "tag_id"
|
||||
t.integer "factor", default: 0
|
||||
end
|
||||
|
||||
create_table "exercises", force: :cascade do |t|
|
||||
t.text "description"
|
||||
t.integer "execution_environment_id"
|
||||
@ -90,8 +110,20 @@ ActiveRecord::Schema.define(version: 20161214144837) do
|
||||
t.boolean "hide_file_tree"
|
||||
t.boolean "allow_file_creation"
|
||||
t.boolean "allow_auto_completion", default: false
|
||||
t.integer "expected_worktime_seconds", default: 0
|
||||
t.integer "expected_difficulty", default: 1
|
||||
end
|
||||
|
||||
create_table "exercises_proxy_exercises", id: false, force: :cascade do |t|
|
||||
t.integer "proxy_exercise_id"
|
||||
t.integer "exercise_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
add_index "exercises_proxy_exercises", ["exercise_id"], name: "index_exercises_proxy_exercises_on_exercise_id", using: :btree
|
||||
add_index "exercises_proxy_exercises", ["proxy_exercise_id"], name: "index_exercises_proxy_exercises_on_proxy_exercise_id", using: :btree
|
||||
|
||||
create_table "external_users", force: :cascade do |t|
|
||||
t.integer "consumer_id"
|
||||
t.string "email", limit: 255
|
||||
@ -182,8 +214,15 @@ ActiveRecord::Schema.define(version: 20161214144837) do
|
||||
add_index "internal_users", ["remember_me_token"], name: "index_internal_users_on_remember_me_token", using: :btree
|
||||
add_index "internal_users", ["reset_password_token"], name: "index_internal_users_on_reset_password_token", using: :btree
|
||||
|
||||
create_table "interventions", force: :cascade do |t|
|
||||
t.string "name"
|
||||
t.text "markup"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
create_table "lti_parameters", force: :cascade do |t|
|
||||
t.string "external_user_id"
|
||||
t.integer "external_users_id"
|
||||
t.integer "consumers_id"
|
||||
t.integer "exercises_id"
|
||||
t.jsonb "lti_parameters", default: {}, null: false
|
||||
@ -191,6 +230,14 @@ ActiveRecord::Schema.define(version: 20161214144837) do
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
create_table "proxy_exercises", force: :cascade do |t|
|
||||
t.string "title"
|
||||
t.string "description"
|
||||
t.string "token"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
create_table "remote_evaluation_mappings", force: :cascade do |t|
|
||||
t.integer "user_id", null: false
|
||||
t.integer "exercise_id", null: false
|
||||
@ -207,10 +254,19 @@ ActiveRecord::Schema.define(version: 20161214144837) do
|
||||
t.datetime "updated_at"
|
||||
t.string "user_type", limit: 255
|
||||
t.text "question"
|
||||
t.boolean "solved"
|
||||
t.boolean "solved", default: false
|
||||
t.integer "submission_id"
|
||||
end
|
||||
|
||||
create_table "searches", force: :cascade do |t|
|
||||
t.integer "exercise_id", null: false
|
||||
t.integer "user_id", null: false
|
||||
t.string "user_type", null: false
|
||||
t.string "search"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
create_table "submissions", force: :cascade do |t|
|
||||
t.integer "exercise_id"
|
||||
t.float "score"
|
||||
@ -221,6 +277,15 @@ ActiveRecord::Schema.define(version: 20161214144837) do
|
||||
t.string "user_type", limit: 255
|
||||
end
|
||||
|
||||
add_index "submissions", ["exercise_id"], name: "index_submissions_on_exercise_id", using: :btree
|
||||
add_index "submissions", ["user_id"], name: "index_submissions_on_user_id", using: :btree
|
||||
|
||||
create_table "tags", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
create_table "testruns", force: :cascade do |t|
|
||||
t.boolean "passed"
|
||||
t.text "output"
|
||||
@ -230,4 +295,36 @@ ActiveRecord::Schema.define(version: 20161214144837) do
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
create_table "user_exercise_feedbacks", force: :cascade do |t|
|
||||
t.integer "exercise_id", null: false
|
||||
t.integer "user_id", null: false
|
||||
t.string "user_type", null: false
|
||||
t.integer "difficulty"
|
||||
t.integer "working_time_seconds"
|
||||
t.string "feedback_text"
|
||||
end
|
||||
|
||||
create_table "user_exercise_interventions", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.string "user_type"
|
||||
t.integer "exercise_id"
|
||||
t.integer "intervention_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
create_table "user_proxy_exercise_exercises", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.string "user_type"
|
||||
t.integer "proxy_exercise_id"
|
||||
t.integer "exercise_id"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.string "reason"
|
||||
end
|
||||
|
||||
add_index "user_proxy_exercise_exercises", ["exercise_id"], name: "index_user_proxy_exercise_exercises_on_exercise_id", using: :btree
|
||||
add_index "user_proxy_exercise_exercises", ["proxy_exercise_id"], name: "index_user_proxy_exercise_exercises_on_proxy_exercise_id", using: :btree
|
||||
add_index "user_proxy_exercise_exercises", ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user_type_and_user_id", using: :btree
|
||||
|
||||
end
|
||||
|
0
deleteme.txt
Normal file
0
deleteme.txt
Normal file
@ -1,3 +1,26 @@
|
||||
.flash {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fixed_error_messages {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding-left: 10%;
|
||||
padding-right: 10%;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.clickthrough {
|
||||
pointer-events: none;
|
||||
|
||||
/* fixes for IE */
|
||||
/*
|
||||
background:white;
|
||||
opacity:0;
|
||||
filter:Alpha(opacity=0);
|
||||
*/
|
||||
}
|
||||
|
||||
|
@ -255,6 +255,12 @@ class DockerClient
|
||||
if(@tubesock)
|
||||
@tubesock.send_data JSON.dump({'cmd' => 'timeout'})
|
||||
end
|
||||
if(@socket)
|
||||
@socket.send('#timeout')
|
||||
#sleep one more second to ensure that the message reaches the submissions_controller.
|
||||
sleep(1)
|
||||
@socket.close
|
||||
end
|
||||
kill_container(container)
|
||||
end
|
||||
#ensure
|
||||
@ -274,6 +280,7 @@ class DockerClient
|
||||
Rails.logger.debug('exiting container ' + container.to_s)
|
||||
# exit the timeout thread if it is still alive
|
||||
exit_thread_if_alive
|
||||
@socket.close
|
||||
# if we use pooling and recylce the containers, put it back. otherwise, destroy it.
|
||||
(DockerContainerPool.config[:active] && RECYCLE_CONTAINERS) ? self.class.return_container(container, @execution_environment) : self.class.destroy_container(container)
|
||||
end
|
||||
|
@ -2,7 +2,7 @@ class JunitAdapter < TestingFrameworkAdapter
|
||||
COUNT_REGEXP = /Tests run: (\d+)/
|
||||
FAILURES_REGEXP = /Failures: (\d+)/
|
||||
SUCCESS_REGEXP = /OK \((\d+) test[s]?\)/
|
||||
ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:\s(.*)|org\.junit\.ComparisonFailure:\s(.*)/
|
||||
ASSERTION_ERROR_REGEXP = /java\.lang\.AssertionError:\s(.*)|org\.junit\.ComparisonFailure:\s(.*)/
|
||||
|
||||
def self.framework_name
|
||||
'JUnit'
|
||||
|
40
lib/user_group_separator.rb
Normal file
40
lib/user_group_separator.rb
Normal 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
|
@ -10,7 +10,7 @@ class Xikolo::Client
|
||||
end
|
||||
|
||||
def self.user_profile_url(user_id)
|
||||
return url + 'users/' + user_id
|
||||
return url + 'v2/users/' + user_id
|
||||
end
|
||||
|
||||
def self.post_request(url, params)
|
||||
@ -38,11 +38,11 @@ class Xikolo::Client
|
||||
end
|
||||
|
||||
def self.accept
|
||||
'application/vnd.xikolo.v1, application/json'
|
||||
'application/vnd.xikolo.v1, application/vnd.api+json, application/json'
|
||||
end
|
||||
|
||||
def self.token
|
||||
'Token token="'+Rails.application.secrets.openhpi_api_token+'"'
|
||||
'Token token='+Rails.application.secrets.openhpi_api_token#+'"'
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -4,12 +4,10 @@ class Xikolo::UserClient
|
||||
|
||||
# return default values if user is not found or if there is a server issue:
|
||||
if user
|
||||
if user['display_name'].present?
|
||||
name = user['display_name']
|
||||
else
|
||||
name = user['first_name']
|
||||
end
|
||||
return {display_name: name, user_visual: user['user_visual'], language: user['language']}
|
||||
name = user.dig('data', 'attributes', 'name') || "User " + user_id
|
||||
user_visual = user.dig('data', 'attributes', 'avatar_url') || ActionController::Base.helpers.image_path('default.png')
|
||||
language = user.dig('data', 'attributes', 'language') || "DE"
|
||||
return {display_name: name, user_visual: user_visual, language: language}
|
||||
else
|
||||
return {display_name: "User " + user_id, user_visual: ActionController::Base.helpers.image_path('default.png'), language: "DE"}
|
||||
end
|
||||
|
@ -165,6 +165,7 @@ describe Lti do
|
||||
|
||||
it 'stores data in the session' do
|
||||
controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user))
|
||||
controller.instance_variable_set(:@exercise, FactoryGirl.create(:fibonacci))
|
||||
expect(controller.session).to receive(:[]=).with(:consumer_id, anything)
|
||||
expect(controller.session).to receive(:[]=).with(:external_user_id, anything)
|
||||
controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters)
|
||||
@ -172,6 +173,8 @@ describe Lti do
|
||||
|
||||
it 'it creates an LtiParameter Object' do
|
||||
before_count = LtiParameter.count
|
||||
controller.instance_variable_set(:@current_user, FactoryGirl.create(:external_user))
|
||||
controller.instance_variable_set(:@exercise, FactoryGirl.create(:fibonacci))
|
||||
controller.send(:store_lti_session_data, consumer: FactoryGirl.build(:consumer), parameters: parameters)
|
||||
expect(LtiParameter.count).to eq(before_count + 1)
|
||||
end
|
||||
|
@ -28,6 +28,7 @@ describe SessionsController do
|
||||
|
||||
describe 'POST #create_through_lti' do
|
||||
let(:exercise) { FactoryGirl.create(:dummy) }
|
||||
let(:exercise2) { FactoryGirl.create(:dummy) }
|
||||
let(:nonce) { SecureRandom.hex }
|
||||
before(:each) { I18n.locale = I18n.default_locale }
|
||||
|
||||
@ -129,6 +130,23 @@ describe SessionsController do
|
||||
request
|
||||
expect(controller).to redirect_to(implement_exercise_path(exercise.id))
|
||||
end
|
||||
|
||||
it 'redirects to recommended exercise if requested token of proxy exercise' do
|
||||
FactoryGirl.create(:proxy_exercise, exercises: [exercise])
|
||||
post :create_through_lti, custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id
|
||||
expect(controller).to redirect_to(implement_exercise_path(exercise.id))
|
||||
end
|
||||
|
||||
it 'recommends only exercises who are 1 degree more complicated than what user has seen' do
|
||||
# dummy user has no exercises finished, therefore his highest difficulty is 0
|
||||
FactoryGirl.create(:proxy_exercise, exercises: [exercise, exercise2])
|
||||
exercise.expected_difficulty = 3
|
||||
exercise.save
|
||||
exercise2.expected_difficulty = 1
|
||||
exercise2.save
|
||||
post :create_through_lti, custom_locale: locale, custom_token: ProxyExercise.first.token, oauth_consumer_key: consumer.oauth_key, oauth_nonce: nonce, oauth_signature: SecureRandom.hex, user_id: user.external_id
|
||||
expect(controller).to redirect_to(implement_exercise_path(exercise2.id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
7
spec/factories/proxy_exercise.rb
Normal file
7
spec/factories/proxy_exercise.rb
Normal file
@ -0,0 +1,7 @@
|
||||
FactoryGirl.define do
|
||||
factory :proxy_exercise, class: ProxyExercise do
|
||||
token 'dummytoken'
|
||||
title 'Dummy'
|
||||
end
|
||||
|
||||
end
|
Reference in New Issue
Block a user