
Tags can be added to exercises in the edit view. Tags can monitored under /tags. Added the concept of ProxyExercises which are a collection of Exercises. They can be found under /proxy_exercises Added Interventions as prework to show interventions later to the user. Added exercise/[:id]/working_time to return the working time of the user in this exercise and the average working time of all users in this exercise
340 lines
12 KiB
Ruby
340 lines
12 KiB
Ruby
class ExercisesController < ApplicationController
|
|
include CommonBehavior
|
|
include Lti
|
|
include SubmissionParameters
|
|
include SubmissionScoring
|
|
|
|
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, :working_times, :run, :statistics, :submit, :reload]
|
|
before_action :set_external_user, only: [:statistics]
|
|
before_action :set_file_types, only: [:create, :edit, :new, :update]
|
|
|
|
skip_before_filter :verify_authenticity_token, only: [:import_proforma_xml]
|
|
skip_after_action :verify_authorized, only: [:import_proforma_xml]
|
|
skip_after_action :verify_policy_scoped, only: [:import_proforma_xml]
|
|
|
|
def authorize!
|
|
authorize(@exercise || @exercises)
|
|
end
|
|
private :authorize!
|
|
|
|
def batch_update
|
|
@exercises = Exercise.all
|
|
authorize!
|
|
@exercises = params[:exercises].values.map do |exercise_params|
|
|
exercise = Exercise.find(exercise_params.delete(:id))
|
|
exercise.update(exercise_params)
|
|
exercise
|
|
end
|
|
render(json: {exercises: @exercises})
|
|
end
|
|
|
|
def clone
|
|
exercise = @exercise.duplicate(public: false, token: nil, user: current_user)
|
|
exercise.send(:generate_token)
|
|
if exercise.save
|
|
redirect_to(exercise, notice: t('shared.object_cloned', model: Exercise.model_name.human))
|
|
else
|
|
flash[:danger] = t('shared.message_failure')
|
|
redirect_to(@exercise)
|
|
end
|
|
end
|
|
|
|
def collect_paths(files)
|
|
unique_paths = files.map(&:path).reject(&:blank?).uniq
|
|
subpaths = unique_paths.map do |path|
|
|
(path.split('/').length + 1).times.map do |n|
|
|
path.split('/').shift(n).join('/')
|
|
end
|
|
end
|
|
subpaths.flatten.uniq
|
|
end
|
|
private :collect_paths
|
|
|
|
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
|
|
|
|
def destroy
|
|
destroy_and_respond(object: @exercise)
|
|
end
|
|
|
|
def edit
|
|
collect_set_and_unset_exercise_tags
|
|
end
|
|
|
|
def import_proforma_xml
|
|
begin
|
|
user = user_for_oauth2_request()
|
|
exercise = Exercise.new
|
|
request_body = request.body.read
|
|
exercise.from_proforma_xml(request_body)
|
|
exercise.user = user
|
|
saved = exercise.save
|
|
if saved
|
|
render :text => 'SUCCESS', :status => 200
|
|
else
|
|
logger.info(exercise.errors.full_messages)
|
|
render :text => 'Invalid exercise', :status => 400
|
|
end
|
|
rescue => error
|
|
if error.class == Hash
|
|
render :text => error.message, :status => error.status
|
|
else
|
|
raise error
|
|
render :text => '', :status => 500
|
|
end
|
|
end
|
|
end
|
|
|
|
def user_for_oauth2_request
|
|
authorizationHeader = request.headers['Authorization']
|
|
if authorizationHeader == nil
|
|
raise ({status: 401, message: 'No Authorization header'})
|
|
end
|
|
|
|
oauth2Token = authorizationHeader.split(' ')[1]
|
|
if oauth2Token == nil || oauth2Token.size == 0
|
|
raise ({status: 401, message: 'No token in Authorization header'})
|
|
end
|
|
|
|
user = user_by_code_harbor_token(oauth2Token)
|
|
if user == nil
|
|
raise ({status: 401, message: 'Unknown OAuth2 token'})
|
|
end
|
|
|
|
return user
|
|
end
|
|
private :user_for_oauth2_request
|
|
|
|
def user_by_code_harbor_token(oauth2Token)
|
|
link = CodeHarborLink.where(:oauth2token => oauth2Token)[0]
|
|
if link != nil
|
|
return link.user
|
|
end
|
|
end
|
|
private :user_by_code_harbor_token
|
|
|
|
def exercise_params
|
|
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
|
|
|
|
def handle_file_uploads
|
|
exercise_params[:files_attributes].try(:each) do |index, file_attributes|
|
|
if file_attributes[:content].respond_to?(:read)
|
|
file_params = params[:exercise][:files_attributes][index]
|
|
if FileType.find_by(id: file_attributes[:file_type_id]).try(:binary?)
|
|
file_params[:content] = nil
|
|
file_params[:native_file] = file_attributes[:content]
|
|
else
|
|
file_params[:content] = file_attributes[:content].read
|
|
end
|
|
end
|
|
end
|
|
end
|
|
private :handle_file_uploads
|
|
|
|
def implement
|
|
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
|
|
@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)
|
|
|
|
if current_user.respond_to? :external_id
|
|
@user_id = current_user.external_id
|
|
else
|
|
@user_id = current_user.id
|
|
end
|
|
end
|
|
|
|
def working_times
|
|
working_time_accumulated = Time.parse(@exercise.average_working_time_for_only(current_user.id) || "00:00:00").seconds_since_midnight
|
|
working_time_avg = Time.parse(@exercise.average_working_time || "00:00:00").seconds_since_midnight
|
|
render(json: {working_time_avg: working_time_avg, working_time_accumulated: working_time_accumulated})
|
|
end
|
|
|
|
def index
|
|
@search = policy_scope(Exercise).search(params[:q])
|
|
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page])
|
|
authorize!
|
|
end
|
|
|
|
def redirect_to_lti_return_path
|
|
lti_parameter = LtiParameter.where(consumers_id: session[:consumer_id],
|
|
external_users_id: @submission.user_id,
|
|
exercises_id: @submission.exercise_id).first
|
|
|
|
path = lti_return_path(consumer_id: session[:consumer_id],
|
|
submission_id: @submission.id,
|
|
url: consumer_return_url(build_tool_provider(consumer: Consumer.find_by(id: session[:consumer_id]),
|
|
parameters: lti_parameter.lti_parameters)))
|
|
respond_to do |format|
|
|
format.html { redirect_to(path) }
|
|
format.json { render(json: {redirect: path}) }
|
|
end
|
|
end
|
|
private :redirect_to_lti_return_path
|
|
|
|
def new
|
|
@exercise = Exercise.new
|
|
collect_set_and_unset_exercise_tags
|
|
|
|
authorize!
|
|
end
|
|
|
|
def set_execution_environments
|
|
@execution_environments = ExecutionEnvironment.all.order(:name)
|
|
end
|
|
private :set_execution_environments
|
|
|
|
def set_exercise
|
|
@exercise = Exercise.find(params[:id])
|
|
authorize!
|
|
end
|
|
private :set_exercise
|
|
|
|
def set_external_user
|
|
if params[:external_user_id]
|
|
@external_user = ExternalUser.find(params[:external_user_id])
|
|
authorize!
|
|
end
|
|
end
|
|
private :set_exercise
|
|
|
|
def set_file_types
|
|
@file_types = FileType.all.order(:name)
|
|
end
|
|
private :set_file_types
|
|
|
|
def collect_set_and_unset_exercise_tags
|
|
@search = policy_scope(Tag).search(params[:q])
|
|
@tags = @search.result.order(:name)
|
|
exercise_tags = @exercise.exercise_tags
|
|
tags_set = exercise_tags.collect{|e| e.tag}.to_set
|
|
tags_not_set = Tag.all.to_set.subtract tags_set
|
|
@exercise_tags = exercise_tags + tags_not_set.collect { |tag| ExerciseTag.new(exercise: @exercise, tag: tag)}
|
|
end
|
|
private :collect_set_and_unset_exercise_tags
|
|
|
|
def show
|
|
end
|
|
|
|
#we might want to think about auth here
|
|
def reload
|
|
end
|
|
|
|
def statistics
|
|
if(@external_user)
|
|
render 'exercises/external_users/statistics'
|
|
else
|
|
user_statistics = {}
|
|
query = "SELECT user_id, MAX(score) AS maximum_score, COUNT(id) AS runs
|
|
FROM submissions WHERE exercise_id = #{@exercise.id} GROUP BY
|
|
user_id;"
|
|
ActiveRecord::Base.connection.execute(query).each do |tuple|
|
|
user_statistics[tuple["user_id"].to_i] = tuple
|
|
end
|
|
render locals: {
|
|
user_statistics: user_statistics
|
|
}
|
|
end
|
|
end
|
|
|
|
def submit
|
|
@submission = Submission.create(submission_params)
|
|
score_submission(@submission)
|
|
current_user = ExternalUser.find(@submission.user_id)
|
|
if !current_user.nil? && lti_outcome_service?(@submission.exercise_id, current_user.id, current_user.consumer_id)
|
|
transmit_lti_score
|
|
else
|
|
redirect_after_submit
|
|
end
|
|
end
|
|
|
|
def transmit_lti_score
|
|
::NewRelic::Agent.add_custom_parameters({ submission: @submission.id, normalized_score: @submission.normalized_score })
|
|
response = send_score(@submission.exercise_id, @submission.normalized_score, @submission.user_id)
|
|
|
|
if response[:status] == 'success'
|
|
redirect_after_submit
|
|
else
|
|
respond_to do |format|
|
|
format.html { redirect_to(implement_exercise_path(@submission.exercise)) }
|
|
format.json { render(json: {message: I18n.t('exercises.submit.failure')}, status: 503) }
|
|
end
|
|
end
|
|
end
|
|
private :transmit_lti_score
|
|
|
|
def update
|
|
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
|
|
Rails.logger.debug('Redirecting user with score:s ' + @submission.normalized_score.to_s)
|
|
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.)
|
|
if current_user.respond_to? :external_id
|
|
if 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)
|
|
|
|
respond_to do |format|
|
|
format.html { redirect_to(rfc) }
|
|
format.json { render(json: {redirect: url_for(rfc)}) }
|
|
end
|
|
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
|
|
# 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)
|
|
|
|
respond_to do |format|
|
|
format.html { redirect_to(rfc) }
|
|
format.json { render(json: {redirect: url_for(rfc)}) }
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
redirect_to_lti_return_path
|
|
end
|
|
|
|
end
|