merge master
This commit is contained in:
@ -2,7 +2,7 @@
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
include ApplicationHelper
|
||||
include Pundit
|
||||
include Pundit::Authorization
|
||||
|
||||
MEMBER_ACTIONS = %i[destroy edit show update].freeze
|
||||
|
||||
@ -15,9 +15,11 @@ class ApplicationController < ActionController::Base
|
||||
rescue_from ActionController::InvalidAuthenticityToken, with: :render_csrf_error
|
||||
|
||||
def current_user
|
||||
::NewRelic::Agent.add_custom_attributes(external_user_id: session[:external_user_id],
|
||||
session_user_id: session[:user_id])
|
||||
@current_user ||= ExternalUser.find_by(id: session[:external_user_id]) || login_from_session || login_from_other_sources || nil
|
||||
@current_user ||= ExternalUser.find_by(id: session[:external_user_id]) ||
|
||||
login_from_session ||
|
||||
login_from_other_sources ||
|
||||
login_from_authentication_token ||
|
||||
nil
|
||||
end
|
||||
|
||||
def require_user!
|
||||
@ -34,6 +36,13 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
end
|
||||
|
||||
def login_from_authentication_token
|
||||
token = AuthenticationToken.find_by(shared_secret: params[:token])
|
||||
return unless token
|
||||
|
||||
auto_login(token.user) if token.expire_at.future?
|
||||
end
|
||||
|
||||
def set_sentry_context
|
||||
return if current_user.blank?
|
||||
|
||||
@ -73,7 +82,7 @@ class ApplicationController < ActionController::Base
|
||||
private :render_error
|
||||
|
||||
def switch_locale(&action)
|
||||
session[:locale] = params[:custom_locale] || params[:locale] || session[:locale]
|
||||
session[:locale] = sanitize_locale(params[:custom_locale] || params[:locale] || session[:locale])
|
||||
locale = session[:locale] || I18n.default_locale
|
||||
Sentry.set_extras(locale: locale)
|
||||
I18n.with_locale(locale, &action)
|
||||
@ -98,4 +107,18 @@ class ApplicationController < ActionController::Base
|
||||
@embed_options
|
||||
end
|
||||
private :load_embed_options
|
||||
|
||||
# Sanitize given locale.
|
||||
#
|
||||
# Return `nil` if the locale is blank or not available.
|
||||
#
|
||||
def sanitize_locale(locale)
|
||||
return if locale.blank?
|
||||
|
||||
locale = locale.downcase.to_sym
|
||||
return unless I18n.available_locales.include?(locale)
|
||||
|
||||
locale
|
||||
end
|
||||
private :sanitize_locale
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ module CodeOcean
|
||||
yield if block_given?
|
||||
path = options[:path].try(:call) || @object
|
||||
respond_with_valid_object(format, notice: t('shared.object_created', model: @object.class.model_name.human),
|
||||
path: path, status: :created)
|
||||
path: path, status: :created)
|
||||
else
|
||||
filename = "#{@object.path || ''}/#{@object.name || ''}#{@object.file_type.try(:file_extension) || ''}"
|
||||
format.html do
|
||||
|
@ -44,7 +44,6 @@ class CodeharborLinksController < ApplicationController
|
||||
|
||||
def set_codeharbor_link
|
||||
@codeharbor_link = CodeharborLink.find(params[:id])
|
||||
@codeharbor_link.user = current_user
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -3,9 +3,6 @@
|
||||
class CommentsController < ApplicationController
|
||||
before_action :set_comment, only: %i[show update destroy]
|
||||
|
||||
# to disable authorization check: comment the line below back in
|
||||
# skip_after_action :verify_authorized
|
||||
|
||||
def authorize!
|
||||
authorize(@comment || @comments)
|
||||
end
|
||||
@ -55,7 +52,7 @@ class CommentsController < ApplicationController
|
||||
|
||||
# PATCH/PUT /comments/1.json
|
||||
def update
|
||||
if @comment.update(comment_params_without_request_id)
|
||||
if @comment.update(comment_params_for_update)
|
||||
render :show, status: :ok, location: @comment
|
||||
else
|
||||
render json: @comment.errors, status: :unprocessable_entity
|
||||
@ -77,6 +74,10 @@ class CommentsController < ApplicationController
|
||||
@comment = Comment.find(params[:id])
|
||||
end
|
||||
|
||||
def comment_params_for_update
|
||||
params.require(:comment).permit(:text)
|
||||
end
|
||||
|
||||
def comment_params_without_request_id
|
||||
comment_params.except :request_id
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ class CommunitySolutionsController < ApplicationController
|
||||
|
||||
# GET /community_solutions
|
||||
def index
|
||||
@community_solutions = CommunitySolution.all
|
||||
@community_solutions = CommunitySolution.all.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -85,7 +85,7 @@ class CommunitySolutionsController < ApplicationController
|
||||
private
|
||||
|
||||
def authorize!
|
||||
authorize(@community_solution)
|
||||
authorize(@community_solution || @community_solutions)
|
||||
end
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
|
@ -21,7 +21,7 @@ module Lti
|
||||
# exercise_id.nil? ==> the user has logged out. All session data is to be destroyed
|
||||
# exercise_id.exists? ==> the user has submitted the results of an exercise to the consumer.
|
||||
# Only the lti_parameters are deleted.
|
||||
def clear_lti_session_data(exercise_id = nil, user_id = nil)
|
||||
def clear_lti_session_data(exercise_id = nil, _user_id = nil)
|
||||
if exercise_id.nil?
|
||||
session.delete(:external_user_id)
|
||||
session.delete(:study_group_id)
|
||||
@ -29,8 +29,10 @@ module Lti
|
||||
session.delete(:lti_exercise_id)
|
||||
session.delete(:lti_parameters_id)
|
||||
end
|
||||
LtiParameter.where(external_users_id: user_id,
|
||||
exercises_id: exercise_id).destroy_all
|
||||
|
||||
# March 2022: We temporarily allow reusing the LTI credentials and don't remove them on purpose.
|
||||
# This allows users to jump between remote and web evaluation with the same behavior.
|
||||
# LtiParameter.where(external_users_id: user_id, exercises_id: exercise_id).destroy_all
|
||||
end
|
||||
|
||||
private :clear_lti_session_data
|
||||
@ -136,7 +138,6 @@ module Lti
|
||||
private :return_to_consumer
|
||||
|
||||
def send_score(submission)
|
||||
::NewRelic::Agent.add_custom_attributes({score: submission.normalized_score, session: session})
|
||||
unless (0..MAXIMUM_SCORE).cover?(submission.normalized_score)
|
||||
raise Error.new("Score #{submission.normalized_score} must be between 0 and #{MAXIMUM_SCORE}!")
|
||||
end
|
||||
|
@ -129,12 +129,7 @@ module RedirectBehavior
|
||||
lti_parameters_id: session[:lti_parameters_id]
|
||||
)
|
||||
|
||||
lti_parameter = LtiParameter.where(external_users_id: @submission.user_id,
|
||||
exercises_id: @submission.exercise_id).last
|
||||
|
||||
path = lti_return_path(submission_id: @submission.id,
|
||||
url: consumer_return_url(build_tool_provider(consumer: @submission.user.consumer,
|
||||
parameters: lti_parameter&.lti_parameters)))
|
||||
path = lti_return_path(submission_id: @submission.id)
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id)
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(path) }
|
||||
|
@ -28,7 +28,7 @@ class ConsumersController < ApplicationController
|
||||
private :consumer_params
|
||||
|
||||
def index
|
||||
@consumers = Consumer.paginate(page: params[:page])
|
||||
@consumers = Consumer.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -12,7 +12,7 @@ class ErrorTemplateAttributesController < ApplicationController
|
||||
# GET /error_template_attributes.json
|
||||
def index
|
||||
@error_template_attributes = ErrorTemplateAttribute.all.order('important DESC', :key,
|
||||
:id).paginate(page: params[:page])
|
||||
:id).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -42,7 +42,7 @@ class ErrorTemplateAttributesController < ApplicationController
|
||||
respond_to do |format|
|
||||
if @error_template_attribute.save
|
||||
format.html do
|
||||
redirect_to @error_template_attribute, notice: 'Error template attribute was successfully created.'
|
||||
redirect_to @error_template_attribute, notice: t('shared.object_created', model: @error_template_attribute.class.model_name.human)
|
||||
end
|
||||
format.json { render :show, status: :created, location: @error_template_attribute }
|
||||
else
|
||||
@ -59,7 +59,7 @@ class ErrorTemplateAttributesController < ApplicationController
|
||||
respond_to do |format|
|
||||
if @error_template_attribute.update(error_template_attribute_params)
|
||||
format.html do
|
||||
redirect_to @error_template_attribute, notice: 'Error template attribute was successfully updated.'
|
||||
redirect_to @error_template_attribute, notice: t('shared.object_updated', model: @error_template_attribute.class.model_name.human)
|
||||
end
|
||||
format.json { render :show, status: :ok, location: @error_template_attribute }
|
||||
else
|
||||
@ -76,7 +76,7 @@ class ErrorTemplateAttributesController < ApplicationController
|
||||
@error_template_attribute.destroy
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to error_template_attributes_url, notice: 'Error template attribute was successfully destroyed.'
|
||||
redirect_to error_template_attributes_url, notice: t('shared.object_destroyed', model: @error_template_attribute.class.model_name.human)
|
||||
end
|
||||
format.json { head :no_content }
|
||||
end
|
||||
|
@ -11,7 +11,7 @@ class ErrorTemplatesController < ApplicationController
|
||||
# GET /error_templates
|
||||
# GET /error_templates.json
|
||||
def index
|
||||
@error_templates = ErrorTemplate.all.order(:execution_environment_id, :name).paginate(page: params[:page])
|
||||
@error_templates = ErrorTemplate.all.order(:execution_environment_id, :name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -40,7 +40,7 @@ class ErrorTemplatesController < ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
if @error_template.save
|
||||
format.html { redirect_to @error_template, notice: 'Error template was successfully created.' }
|
||||
format.html { redirect_to @error_template, notice: t('shared.object_created', model: @error_template.class.model_name.human) }
|
||||
format.json { render :show, status: :created, location: @error_template }
|
||||
else
|
||||
format.html { render :new }
|
||||
@ -55,7 +55,7 @@ class ErrorTemplatesController < ApplicationController
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @error_template.update(error_template_params)
|
||||
format.html { redirect_to @error_template, notice: 'Error template was successfully updated.' }
|
||||
format.html { redirect_to @error_template, notice: t('shared.object_updated', model: @error_template.class.model_name.human) }
|
||||
format.json { render :show, status: :ok, location: @error_template }
|
||||
else
|
||||
format.html { render :edit }
|
||||
@ -70,14 +70,14 @@ class ErrorTemplatesController < ApplicationController
|
||||
authorize!
|
||||
@error_template.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to error_templates_url, notice: 'Error template was successfully destroyed.' }
|
||||
format.html { redirect_to error_templates_url, notice: t('shared.object_destroyed', model: @error_template.class.model_name.human) }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
||||
def add_attribute
|
||||
authorize!
|
||||
@error_template.error_template_attributes << ErrorTemplateAttribute.find(params['error_template_attribute_id'])
|
||||
@error_template.error_template_attributes << ErrorTemplateAttribute.find(params[:error_template_attribute_id])
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @error_template }
|
||||
format.json { head :no_content }
|
||||
@ -86,7 +86,7 @@ class ErrorTemplatesController < ApplicationController
|
||||
|
||||
def remove_attribute
|
||||
authorize!
|
||||
@error_template.error_template_attributes.delete(ErrorTemplateAttribute.find(params['error_template_attribute_id']))
|
||||
@error_template.error_template_attributes.delete(ErrorTemplateAttribute.find(params[:error_template_attribute_id]))
|
||||
respond_to do |format|
|
||||
format.html { redirect_to @error_template }
|
||||
format.json { head :no_content }
|
||||
|
@ -30,7 +30,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
def execute_command
|
||||
runner = Runner.for(current_user, @execution_environment)
|
||||
output = runner.execute_command(params[:command], raise_exception: false)
|
||||
render json: output
|
||||
render json: output.except(:messages)
|
||||
end
|
||||
|
||||
def working_time_query
|
||||
@ -44,7 +44,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
exercise_id,
|
||||
CASE WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0' ELSE working_time END AS working_time_new
|
||||
CASE WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0' ELSE working_time END AS working_time_new
|
||||
FROM
|
||||
(SELECT user_id,
|
||||
exercise_id,
|
||||
@ -121,7 +121,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
private :execution_environment_params
|
||||
|
||||
def index
|
||||
@execution_environments = ExecutionEnvironment.all.includes(:user).order(:name).paginate(page: params[:page])
|
||||
@execution_environments = ExecutionEnvironment.all.includes(:user).order(:name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -158,7 +158,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
|
||||
def show
|
||||
if @execution_environment.testing_framework?
|
||||
@testing_framework_adapter = Kernel.const_get(@execution_environment.testing_framework)
|
||||
@testing_framework_adapter = TestingFrameworkAdapter.descendants.find {|klass| klass.name == @execution_environment.testing_framework }
|
||||
end
|
||||
end
|
||||
|
||||
@ -172,8 +172,7 @@ class ExecutionEnvironmentsController < ApplicationController
|
||||
begin
|
||||
Runner.strategy_class.sync_environment(@execution_environment)
|
||||
rescue Runner::Error => e
|
||||
Rails.logger.debug { "Runner error while synchronizing execution environment with id #{@execution_environment.id}: #{e.message}" }
|
||||
Sentry.capture_exception(e)
|
||||
Rails.logger.warn { "Runner error while synchronizing execution environment with id #{@execution_environment.id}: #{e.message}" }
|
||||
redirect_to @execution_environment, alert: t('execution_environments.index.synchronize.failure', error: e.message)
|
||||
else
|
||||
redirect_to @execution_environment, notice: t('execution_environments.index.synchronize.success')
|
||||
|
@ -6,7 +6,7 @@ class ExerciseCollectionsController < ApplicationController
|
||||
before_action :set_exercise_collection, only: %i[show edit update destroy statistics]
|
||||
|
||||
def index
|
||||
@exercise_collections = ExerciseCollection.all.paginate(page: params[:page])
|
||||
@exercise_collections = ExerciseCollection.all.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -11,21 +11,21 @@ class ExercisesController < ApplicationController
|
||||
before_action :set_execution_environments, only: %i[index create edit new update]
|
||||
before_action :set_exercise_and_authorize,
|
||||
only: MEMBER_ACTIONS + %i[clone implement working_times intervention search run statistics submit reload feedback
|
||||
requests_for_comments study_group_dashboard export_external_check export_external_confirm]
|
||||
before_action :set_external_user_and_authorize, only: [:statistics]
|
||||
requests_for_comments study_group_dashboard export_external_check export_external_confirm
|
||||
external_user_statistics]
|
||||
before_action :set_external_user_and_authorize, only: [:external_user_statistics]
|
||||
before_action :set_file_types, only: %i[create edit new update]
|
||||
before_action :set_course_token, only: [:implement]
|
||||
before_action :set_available_tips, only: %i[implement show new edit]
|
||||
|
||||
skip_before_action :verify_authenticity_token,
|
||||
only: %i[import_exercise import_uuid_check export_external_confirm export_external_check]
|
||||
skip_after_action :verify_authorized, only: %i[import_exercise import_uuid_check export_external_confirm]
|
||||
skip_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check export_external_confirm],
|
||||
raise: false
|
||||
skip_before_action :verify_authenticity_token, only: %i[import_exercise import_uuid_check]
|
||||
skip_after_action :verify_authorized, only: %i[import_exercise import_uuid_check]
|
||||
skip_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check], raise: false
|
||||
|
||||
def authorize!
|
||||
authorize(@exercise || @exercises)
|
||||
end
|
||||
|
||||
private :authorize!
|
||||
|
||||
def max_intervention_count_per_day
|
||||
@ -51,7 +51,7 @@ raise: false
|
||||
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))
|
||||
redirect_to(exercise_path(exercise), notice: t('shared.object_cloned', model: Exercise.model_name.human))
|
||||
else
|
||||
flash[:danger] = t('shared.message_failure')
|
||||
redirect_to(@exercise)
|
||||
@ -67,6 +67,7 @@ raise: false
|
||||
end
|
||||
subpaths.flatten.uniq
|
||||
end
|
||||
|
||||
private :collect_paths
|
||||
|
||||
def create
|
||||
@ -103,7 +104,7 @@ raise: false
|
||||
|
||||
def feedback
|
||||
authorize!
|
||||
@feedbacks = @exercise.user_exercise_feedbacks.paginate(page: params[:page])
|
||||
@feedbacks = @exercise.user_exercise_feedbacks.paginate(page: params[:page], per_page: per_page_param)
|
||||
@submissions = @feedbacks.map do |feedback|
|
||||
feedback.exercise.final_submission(feedback.user)
|
||||
end
|
||||
@ -128,6 +129,7 @@ raise: false
|
||||
end
|
||||
|
||||
def export_external_confirm
|
||||
authorize!
|
||||
@exercise.uuid = SecureRandom.uuid if @exercise.uuid.nil?
|
||||
|
||||
error = ExerciseService::PushExternal.call(
|
||||
@ -176,7 +178,7 @@ raise: false
|
||||
ActiveRecord::Base.transaction do
|
||||
exercise = ::ProformaService::Import.call(zip: tempfile, user: user)
|
||||
exercise.save!
|
||||
return render json: {}, status: :created
|
||||
render json: {}, status: :created
|
||||
end
|
||||
rescue Proforma::ExerciseNotOwned
|
||||
render json: {}, status: :unauthorized
|
||||
@ -192,12 +194,14 @@ raise: false
|
||||
api_key = authorization_header&.split(' ')&.second
|
||||
user_by_codeharbor_token(api_key)
|
||||
end
|
||||
|
||||
private :user_from_api_key
|
||||
|
||||
def user_by_codeharbor_token(api_key)
|
||||
link = CodeharborLink.find_by(api_key: api_key)
|
||||
link&.user
|
||||
end
|
||||
|
||||
private :user_by_codeharbor_token
|
||||
|
||||
def exercise_params
|
||||
@ -225,6 +229,7 @@ raise: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private :exercise_params
|
||||
|
||||
def handle_file_uploads
|
||||
@ -241,6 +246,7 @@ raise: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private :handle_file_uploads
|
||||
|
||||
def handle_exercise_tips
|
||||
@ -258,6 +264,7 @@ raise: false
|
||||
redirect_to(edit_exercise_path(@exercise))
|
||||
end
|
||||
end
|
||||
|
||||
private :handle_exercise_tips
|
||||
|
||||
def update_exercise_tips(exercise_tips, parent_exercise_tip_id, rank)
|
||||
@ -283,6 +290,7 @@ raise: false
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
private :update_exercise_tips
|
||||
|
||||
def implement
|
||||
@ -348,6 +356,7 @@ raise: false
|
||||
@course_token = '702cbd2a-c84c-4b37-923a-692d7d1532d0'
|
||||
end
|
||||
end
|
||||
|
||||
private :set_course_token
|
||||
|
||||
def set_available_tips
|
||||
@ -374,13 +383,14 @@ raise: false
|
||||
# Return an array with top-level tips
|
||||
@tips = nested_tips.values.select {|tip| tip.parent_exercise_tip_id.nil? }
|
||||
end
|
||||
|
||||
private :set_available_tips
|
||||
|
||||
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})
|
||||
working_time_accumulated: working_time_accumulated})
|
||||
end
|
||||
|
||||
def intervention
|
||||
@ -401,8 +411,9 @@ working_time_accumulated: working_time_accumulated})
|
||||
search_text = params[:search_text]
|
||||
search = Search.new(user: current_user, exercise: @exercise, search: search_text)
|
||||
|
||||
begin search.save
|
||||
render(json: {success: 'true'})
|
||||
begin
|
||||
search.save
|
||||
render(json: {success: 'true'})
|
||||
rescue StandardError
|
||||
render(json: {success: 'false', error: "could not save search: #{$ERROR_INFO}"})
|
||||
end
|
||||
@ -410,7 +421,7 @@ working_time_accumulated: working_time_accumulated})
|
||||
|
||||
def index
|
||||
@search = policy_scope(Exercise).ransack(params[:q])
|
||||
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page])
|
||||
@exercises = @search.result.includes(:execution_environment, :user).order(:title).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -424,12 +435,14 @@ working_time_accumulated: working_time_accumulated})
|
||||
def set_execution_environments
|
||||
@execution_environments = ExecutionEnvironment.all.order(:name)
|
||||
end
|
||||
|
||||
private :set_execution_environments
|
||||
|
||||
def set_exercise_and_authorize
|
||||
@exercise = Exercise.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
|
||||
private :set_exercise_and_authorize
|
||||
|
||||
def set_external_user_and_authorize
|
||||
@ -438,23 +451,25 @@ working_time_accumulated: working_time_accumulated})
|
||||
authorize!
|
||||
end
|
||||
end
|
||||
|
||||
private :set_external_user_and_authorize
|
||||
|
||||
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).ransack(params[:q])
|
||||
@tags = @search.result.order(:name)
|
||||
@tags = policy_scope(Tag)
|
||||
checked_exercise_tags = @exercise.exercise_tags
|
||||
checked_tags = checked_exercise_tags.collect(&:tag).to_set
|
||||
unchecked_tags = Tag.all.to_set.subtract checked_tags
|
||||
@exercise_tags = checked_exercise_tags + unchecked_tags.collect do |tag|
|
||||
ExerciseTag.new(exercise: @exercise, tag: tag)
|
||||
end
|
||||
ExerciseTag.new(exercise: @exercise, tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
private :collect_set_and_unset_exercise_tags
|
||||
|
||||
def show
|
||||
@ -466,55 +481,63 @@ working_time_accumulated: working_time_accumulated})
|
||||
end
|
||||
|
||||
def statistics
|
||||
if @external_user
|
||||
# Render statistics page for one specific external user
|
||||
authorize(@external_user, :statistics?)
|
||||
if policy(@exercise).detailed_statistics?
|
||||
@submissions = Submission.where(user: @external_user,
|
||||
exercise_id: @exercise.id).in_study_group_of(current_user).order('created_at')
|
||||
interventions = UserExerciseIntervention.where('user_id = ? AND exercise_id = ?', @external_user.id,
|
||||
@exercise.id)
|
||||
@all_events = (@submissions + interventions).sort_by(&:created_at)
|
||||
@deltas = @all_events.map.with_index do |item, index|
|
||||
delta = item.created_at - @all_events[index - 1].created_at if index.positive?
|
||||
delta.nil? || (delta > StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS) ? 0 : delta
|
||||
end
|
||||
@working_times_until = []
|
||||
@all_events.each_with_index do |_, index|
|
||||
@working_times_until.push((format_time_difference(@deltas[0..index].sum) if index.positive?))
|
||||
end
|
||||
else
|
||||
final_submissions = Submission.where(user: @external_user,
|
||||
exercise_id: @exercise.id).in_study_group_of(current_user).final
|
||||
@submissions = []
|
||||
%i[before_deadline within_grace_period after_late_deadline].each do |filter|
|
||||
relevant_submission = final_submissions.send(filter).latest
|
||||
@submissions.push relevant_submission if relevant_submission.present?
|
||||
end
|
||||
@all_events = @submissions
|
||||
end
|
||||
render 'exercises/external_users/statistics'
|
||||
else
|
||||
# Show general statistic page for specific exercise
|
||||
user_statistics = {}
|
||||
additional_filter = if policy(@exercise).detailed_statistics?
|
||||
''
|
||||
elsif !policy(@exercise).detailed_statistics? && current_user.study_groups.count.positive?
|
||||
"AND study_group_id IN (#{current_user.study_groups.pluck(:id).join(', ')}) AND cause = 'submit'"
|
||||
else
|
||||
# e.g. internal user without any study groups, show no submissions
|
||||
'AND FALSE'
|
||||
end
|
||||
query = "SELECT user_id, MAX(score) AS maximum_score, COUNT(id) AS runs
|
||||
FROM submissions WHERE exercise_id = #{@exercise.id} #{additional_filter} GROUP BY
|
||||
user_id;"
|
||||
ApplicationRecord.connection.execute(query).each do |tuple|
|
||||
user_statistics[tuple['user_id'].to_i] = tuple
|
||||
end
|
||||
render locals: {
|
||||
user_statistics: user_statistics,
|
||||
}
|
||||
# Show general statistic page for specific exercise
|
||||
user_statistics = {'InternalUser' => {}, 'ExternalUser' => {}}
|
||||
|
||||
query = Submission.select('user_id, user_type, MAX(score) AS maximum_score, COUNT(id) AS runs')
|
||||
.where(exercise_id: @exercise.id)
|
||||
.group('user_id, user_type')
|
||||
|
||||
query = if policy(@exercise).detailed_statistics?
|
||||
query
|
||||
elsif !policy(@exercise).detailed_statistics? && current_user.study_groups.count.positive?
|
||||
query.where(study_groups: current_user.study_groups.pluck(:id), cause: 'submit')
|
||||
else
|
||||
# e.g. internal user without any study groups, show no submissions
|
||||
query.where('false')
|
||||
end
|
||||
|
||||
query.each do |tuple|
|
||||
user_statistics[tuple['user_type']][tuple['user_id'].to_i] = tuple
|
||||
end
|
||||
|
||||
render locals: {
|
||||
user_statistics: user_statistics,
|
||||
}
|
||||
end
|
||||
|
||||
def external_user_statistics
|
||||
# Render statistics page for one specific external user
|
||||
|
||||
if policy(@exercise).detailed_statistics?
|
||||
submissions = Submission.where(user: @external_user, exercise: @exercise)
|
||||
.in_study_group_of(current_user)
|
||||
.order('created_at')
|
||||
@show_autosaves = params[:show_autosaves] == 'true' || submissions.none? {|s| s.cause != 'autosave' }
|
||||
submissions = submissions.where.not(cause: 'autosave') unless @show_autosaves
|
||||
interventions = UserExerciseIntervention.where('user_id = ? AND exercise_id = ?', @external_user.id,
|
||||
@exercise.id)
|
||||
@all_events = (submissions + interventions).sort_by(&:created_at)
|
||||
@deltas = @all_events.map.with_index do |item, index|
|
||||
delta = item.created_at - @all_events[index - 1].created_at if index.positive?
|
||||
delta.nil? || (delta > StatisticsHelper::WORKING_TIME_DELTA_IN_SECONDS) ? 0 : delta
|
||||
end
|
||||
@working_times_until = []
|
||||
@all_events.each_with_index do |_, index|
|
||||
@working_times_until.push((format_time_difference(@deltas[0..index].sum) if index.positive?))
|
||||
end
|
||||
else
|
||||
final_submissions = Submission.where(user: @external_user,
|
||||
exercise_id: @exercise.id).in_study_group_of(current_user).final
|
||||
submissions = []
|
||||
%i[before_deadline within_grace_period after_late_deadline].each do |filter|
|
||||
relevant_submission = final_submissions.send(filter).latest
|
||||
submissions.push relevant_submission if relevant_submission.present?
|
||||
end
|
||||
@all_events = submissions
|
||||
end
|
||||
|
||||
render 'exercises/external_users/statistics'
|
||||
end
|
||||
|
||||
def submit
|
||||
@ -534,8 +557,6 @@ working_time_accumulated: working_time_accumulated})
|
||||
end
|
||||
|
||||
def transmit_lti_score
|
||||
::NewRelic::Agent.add_custom_attributes({submission: @submission.id,
|
||||
normalized_score: @submission.normalized_score})
|
||||
response = send_score(@submission)
|
||||
|
||||
if response[:status] == 'success'
|
||||
@ -552,6 +573,7 @@ working_time_accumulated: working_time_accumulated})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private :transmit_lti_score
|
||||
|
||||
def update
|
||||
|
@ -10,7 +10,7 @@ class ExternalUsersController < ApplicationController
|
||||
|
||||
def index
|
||||
@search = ExternalUser.ransack(params[:q])
|
||||
@users = @search.result.in_study_group_of(current_user).includes(:consumer).paginate(page: params[:page])
|
||||
@users = @search.result.in_study_group_of(current_user).includes(:consumer).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -32,7 +32,7 @@ class ExternalUsersController < ApplicationController
|
||||
score,
|
||||
id,
|
||||
CASE
|
||||
WHEN working_time >= #{StatisticsHelper::WORKING_TIME_DELTA_IN_SQL_INTERVAL} THEN '0'
|
||||
WHEN #{StatisticsHelper.working_time_larger_delta} THEN '0'
|
||||
ELSE working_time
|
||||
END AS working_time_new
|
||||
FROM
|
||||
|
@ -19,7 +19,7 @@ class FileTemplatesController < ApplicationController
|
||||
# GET /file_templates
|
||||
# GET /file_templates.json
|
||||
def index
|
||||
@file_templates = FileTemplate.all.order(:file_type_id).paginate(page: params[:page])
|
||||
@file_templates = FileTemplate.all.order(:file_type_id).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
@ -48,7 +48,7 @@ class FileTemplatesController < ApplicationController
|
||||
|
||||
respond_to do |format|
|
||||
if @file_template.save
|
||||
format.html { redirect_to @file_template, notice: 'File template was successfully created.' }
|
||||
format.html { redirect_to @file_template, notice: t('shared.object_created', model: @file_template.class.model_name.human) }
|
||||
format.json { render :show, status: :created, location: @file_template }
|
||||
else
|
||||
format.html { render :new }
|
||||
@ -63,7 +63,7 @@ class FileTemplatesController < ApplicationController
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @file_template.update(file_template_params)
|
||||
format.html { redirect_to @file_template, notice: 'File template was successfully updated.' }
|
||||
format.html { redirect_to @file_template, notice: t('shared.object_updated', model: @file_template.class.model_name.human) }
|
||||
format.json { render :show, status: :ok, location: @file_template }
|
||||
else
|
||||
format.html { render :edit }
|
||||
@ -78,7 +78,7 @@ class FileTemplatesController < ApplicationController
|
||||
authorize!
|
||||
@file_template.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to file_templates_url, notice: 'File template was successfully destroyed.' }
|
||||
format.html { redirect_to file_templates_url, notice: t('shared.object_destroyed', model: @file_template.class.model_name.human) }
|
||||
format.json { head :no_content }
|
||||
end
|
||||
end
|
||||
|
@ -33,7 +33,7 @@ class FileTypesController < ApplicationController
|
||||
private :file_type_params
|
||||
|
||||
def index
|
||||
@file_types = FileType.all.includes(:user).order(:name).paginate(page: params[:page])
|
||||
@file_types = FileType.all.includes(:user).order(:name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -6,7 +6,7 @@ class FlowrController < ApplicationController
|
||||
# get the latest submission for this user that also has a test run (i.e. structured_errors if applicable)
|
||||
submission = Submission.joins(:testruns)
|
||||
.where(submissions: {user_id: current_user.id, user_type: current_user.class.name})
|
||||
.order('testruns.created_at DESC').first
|
||||
.merge(Testrun.order(created_at: :desc)).first
|
||||
|
||||
# Return if no submission was found
|
||||
if submission.blank? || @embed_options[:disable_hints] || @embed_options[:hide_test_results]
|
||||
|
@ -6,7 +6,6 @@ class InternalUsersController < ApplicationController
|
||||
before_action :require_activation_token, only: :activate
|
||||
before_action :require_reset_password_token, only: :reset_password
|
||||
before_action :set_user, only: MEMBER_ACTIONS
|
||||
skip_before_action :verify_authenticity_token, only: :activate
|
||||
after_action :verify_authorized, except: %i[activate forgot_password reset_password]
|
||||
|
||||
def activate
|
||||
@ -33,9 +32,15 @@ class InternalUsersController < ApplicationController
|
||||
|
||||
def create
|
||||
@user = InternalUser.new(internal_user_params)
|
||||
@user.role = role_param if current_user.admin?
|
||||
authorize!
|
||||
@user.send(:setup_activation)
|
||||
create_and_respond(object: @user) { @user.send(:send_activation_needed_email!) }
|
||||
create_and_respond(object: @user) do
|
||||
@user.send(:send_activation_needed_email!)
|
||||
# The return value is used as a flash message. If this block does not
|
||||
# have any specific return value, a default success message is shown.
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def deliver_reset_password_instructions
|
||||
@ -63,15 +68,20 @@ class InternalUsersController < ApplicationController
|
||||
|
||||
def index
|
||||
@search = InternalUser.ransack(params[:q])
|
||||
@users = @search.result.includes(:consumer).order(:name).paginate(page: params[:page])
|
||||
@users = @search.result.includes(:consumer).order(:name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def internal_user_params
|
||||
params[:internal_user].permit(:consumer_id, :email, :name, :role) if params[:internal_user].present?
|
||||
params.require(:internal_user).permit(:consumer_id, :email, :name)
|
||||
end
|
||||
private :internal_user_params
|
||||
|
||||
def role_param
|
||||
params.require(:internal_user).permit(:role)[:role]
|
||||
end
|
||||
private :role_param
|
||||
|
||||
def new
|
||||
@user = InternalUser.new
|
||||
authorize!
|
||||
@ -129,6 +139,7 @@ class InternalUsersController < ApplicationController
|
||||
# the form by another user. Otherwise, the update might fail if an
|
||||
# activation_token or password_reset_token is present
|
||||
@user.validate_password = current_user == @user
|
||||
@user.role = role_param if current_user.admin?
|
||||
|
||||
update_and_respond(object: @user, params: internal_user_params)
|
||||
end
|
||||
|
@ -15,7 +15,7 @@ class ProxyExercisesController < ApplicationController
|
||||
user: current_user)
|
||||
proxy_exercise.send(:generate_token)
|
||||
if proxy_exercise.save
|
||||
redirect_to(proxy_exercise, notice: t('shared.object_cloned', model: ProxyExercise.model_name.human))
|
||||
redirect_to(proxy_exercise_path(proxy_exercise), notice: t('shared.object_cloned', model: ProxyExercise.model_name.human))
|
||||
else
|
||||
flash[:danger] = t('shared.message_failure')
|
||||
redirect_to(@proxy_exercise)
|
||||
@ -51,7 +51,7 @@ class ProxyExercisesController < ApplicationController
|
||||
|
||||
def index
|
||||
@search = policy_scope(ProxyExercise).ransack(params[:q])
|
||||
@proxy_exercises = @search.result.order(:title).paginate(page: params[:page])
|
||||
@proxy_exercises = @search.result.order(:title).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -39,8 +39,8 @@ class RemoteEvaluationController < ApplicationController
|
||||
else
|
||||
{
|
||||
message: "Your submission was successfully scored with #{@submission.normalized_score}%. " \
|
||||
'However, your score could not be sent to the e-Learning platform. Please reopen ' \
|
||||
'the exercise through the e-Learning platform and try again.',
|
||||
'However, your score could not be sent to the e-Learning platform. Please check ' \
|
||||
'the submission deadline, reopen the exercise through the e-Learning platform and try again.',
|
||||
status: 410,
|
||||
}
|
||||
end
|
||||
|
@ -1,8 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class RequestForCommentsController < ApplicationController
|
||||
include CommonBehavior
|
||||
before_action :require_user!
|
||||
before_action :set_request_for_comment, only: %i[show mark_as_solved set_thank_you_note]
|
||||
before_action :set_request_for_comment, only: %i[show mark_as_solved set_thank_you_note clear_question]
|
||||
before_action :set_study_group_grouping,
|
||||
only: %i[index my_comment_requests rfcs_with_my_comments rfcs_for_exercise]
|
||||
|
||||
@ -23,7 +24,7 @@ class RequestForCommentsController < ApplicationController
|
||||
.where(exercises: {unpublished: false})
|
||||
.includes(submission: [:study_group])
|
||||
.order('created_at DESC')
|
||||
.paginate(page: params[:page], total_entries: @search.result.length)
|
||||
.paginate(page: params[:page], per_page: per_page_param, total_entries: @search.result.length)
|
||||
|
||||
authorize!
|
||||
end
|
||||
@ -36,7 +37,7 @@ class RequestForCommentsController < ApplicationController
|
||||
.ransack(params[:q])
|
||||
@request_for_comments = @search.result
|
||||
.order('created_at DESC')
|
||||
.paginate(page: params[:page])
|
||||
.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
render 'index'
|
||||
end
|
||||
@ -50,7 +51,7 @@ class RequestForCommentsController < ApplicationController
|
||||
.ransack(params[:q])
|
||||
@request_for_comments = @search.result
|
||||
.order('last_comment DESC')
|
||||
.paginate(page: params[:page])
|
||||
.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
render 'index'
|
||||
end
|
||||
@ -65,7 +66,7 @@ class RequestForCommentsController < ApplicationController
|
||||
@request_for_comments = @search.result
|
||||
.joins(:exercise)
|
||||
.order('last_comment DESC')
|
||||
.paginate(page: params[:page])
|
||||
.paginate(page: params[:page], per_page: per_page_param)
|
||||
# let the exercise decide, whether its rfcs should be visible
|
||||
authorize(exercise)
|
||||
render 'index'
|
||||
@ -101,6 +102,12 @@ class RequestForCommentsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
# POST /request_for_comments/1/clear_question
|
||||
def clear_question
|
||||
authorize!
|
||||
update_and_respond(object: @request_for_comment, params: {question: nil})
|
||||
end
|
||||
|
||||
# GET /request_for_comments/1
|
||||
# GET /request_for_comments/1.json
|
||||
def show
|
||||
|
@ -24,7 +24,7 @@ class SessionsController < ApplicationController
|
||||
store_lti_session_data(consumer: @consumer, parameters: params)
|
||||
store_nonce(params[:oauth_nonce])
|
||||
if params[:custom_redirect_target]
|
||||
redirect_to(params[:custom_redirect_target])
|
||||
redirect_to(URI.parse(params[:custom_redirect_target].to_s).path)
|
||||
else
|
||||
redirect_to(implement_exercise_path(@exercise),
|
||||
notice: t("sessions.create_through_lti.session_#{lti_outcome_service?(@exercise.id, @current_user.id) ? 'with' : 'without'}_outcome",
|
||||
@ -43,6 +43,10 @@ class SessionsController < ApplicationController
|
||||
|
||||
def destroy_through_lti
|
||||
@submission = Submission.find(params[:submission_id])
|
||||
authorize(@submission, :show?)
|
||||
lti_parameter = LtiParameter.where(external_users_id: @submission.user_id, exercises_id: @submission.exercise_id).last
|
||||
@url = consumer_return_url(build_tool_provider(consumer: @submission.user.consumer, parameters: lti_parameter&.lti_parameters))
|
||||
|
||||
clear_lti_session_data(@submission.exercise_id, @submission.user_id)
|
||||
end
|
||||
|
||||
|
@ -7,7 +7,7 @@ class StudyGroupsController < ApplicationController
|
||||
|
||||
def index
|
||||
@search = policy_scope(StudyGroup).ransack(params[:q])
|
||||
@study_groups = @search.result.includes(:consumer).order(:name).paginate(page: params[:page])
|
||||
@study_groups = @search.result.includes(:consumer).order(:name).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -9,10 +9,10 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_submission, only: %i[download download_file render_file run score show statistics test]
|
||||
before_action :set_testrun, only: %i[run score test]
|
||||
before_action :set_files, only: %i[download show]
|
||||
before_action :set_files_and_specific_file, only: %i[download_file render_file run test]
|
||||
before_action :set_mime_type, only: %i[download_file render_file]
|
||||
skip_before_action :verify_authenticity_token, only: %i[download_file render_file]
|
||||
|
||||
def create
|
||||
@submission = Submission.new(submission_params)
|
||||
@ -27,8 +27,8 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
stringio = Zip::OutputStream.write_buffer do |zio|
|
||||
@files.each do |file|
|
||||
zio.put_next_entry(file.filepath)
|
||||
zio.write(file.content.presence || file.native_file.read)
|
||||
zio.put_next_entry(file.filepath.delete_prefix('/'))
|
||||
zio.write(file.read)
|
||||
end
|
||||
|
||||
# zip exercise description
|
||||
@ -39,7 +39,7 @@ class SubmissionsController < ApplicationController
|
||||
# zip .co file
|
||||
zio.put_next_entry('.co')
|
||||
zio.write(File.read(id_file))
|
||||
File.delete(id_file) if File.exist?(id_file)
|
||||
FileUtils.rm_rf(id_file)
|
||||
|
||||
# zip client scripts
|
||||
scripts_path = 'app/assets/remote_scripts'
|
||||
@ -56,22 +56,18 @@ class SubmissionsController < ApplicationController
|
||||
def download_file
|
||||
raise Pundit::NotAuthorizedError if @embed_options[:disable_download]
|
||||
|
||||
if @file.native_file?
|
||||
send_file(@file.native_file.path)
|
||||
else
|
||||
send_data(@file.content, filename: @file.name_with_extension)
|
||||
end
|
||||
send_data(@file.read, filename: @file.name_with_extension)
|
||||
end
|
||||
|
||||
def index
|
||||
@search = Submission.ransack(params[:q])
|
||||
@submissions = @search.result.includes(:exercise, :user).paginate(page: params[:page])
|
||||
@submissions = @search.result.includes(:exercise, :user).paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def render_file
|
||||
if @file.native_file?
|
||||
send_file(@file.native_file.path, disposition: 'inline')
|
||||
send_data(@file.read, filename: @file.name_with_extension, disposition: 'inline')
|
||||
else
|
||||
render(plain: @file.content)
|
||||
end
|
||||
@ -85,10 +81,14 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
hijack do |tubesock|
|
||||
client_socket = tubesock
|
||||
return kill_client_socket(client_socket) if @embed_options[:disable_run]
|
||||
|
||||
client_socket.onopen do |_event|
|
||||
kill_client_socket(client_socket) if @embed_options[:disable_run]
|
||||
end
|
||||
|
||||
client_socket.onclose do |_event|
|
||||
runner_socket&.close(:terminated_by_client)
|
||||
@testrun[:status] ||= :terminated_by_client
|
||||
end
|
||||
|
||||
client_socket.onmessage do |raw_event|
|
||||
@ -97,9 +97,17 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
# Otherwise, we expect to receive a JSON: Parsing.
|
||||
event = JSON.parse(raw_event).deep_symbolize_keys
|
||||
event[:cmd] = event[:cmd].to_sym
|
||||
event[:stream] = event[:stream].to_sym if event.key? :stream
|
||||
|
||||
case event[:cmd].to_sym
|
||||
# We could store the received event. However, it is also echoed by the container
|
||||
# and correctly identified as the original input. Therefore, we don't store
|
||||
# it here to prevent duplicated events.
|
||||
# @testrun[:messages].push(event)
|
||||
|
||||
case event[:cmd]
|
||||
when :client_kill
|
||||
@testrun[:status] = :terminated_by_client
|
||||
close_client_connection(client_socket)
|
||||
Rails.logger.debug('Client exited container.')
|
||||
when :result, :canvasevent, :exception
|
||||
@ -125,68 +133,88 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
@output = +''
|
||||
durations = @submission.run(@file) do |socket|
|
||||
@testrun[:output] = +''
|
||||
durations = @submission.run(@file) do |socket, starting_time|
|
||||
runner_socket = socket
|
||||
@testrun[:starting_time] = starting_time
|
||||
client_socket.send_data JSON.dump({cmd: :status, status: :container_running})
|
||||
|
||||
runner_socket.on :stdout do |data|
|
||||
json_data = prepare data, :stdout
|
||||
@output << json_data[0, max_output_buffer_size - @output.size]
|
||||
client_socket.send_data(json_data)
|
||||
message = retrieve_message_from_output data, :stdout
|
||||
@testrun[:output] << message[:data][0, max_output_buffer_size - @testrun[:output].size] if message[:data]
|
||||
send_and_store client_socket, message
|
||||
end
|
||||
|
||||
runner_socket.on :stderr do |data|
|
||||
json_data = prepare data, :stderr
|
||||
@output << json_data[0, max_output_buffer_size - @output.size]
|
||||
client_socket.send_data(json_data)
|
||||
message = retrieve_message_from_output data, :stderr
|
||||
@testrun[:output] << message[:data][0, max_output_buffer_size - @testrun[:output].size] if message[:data]
|
||||
send_and_store client_socket, message
|
||||
end
|
||||
|
||||
runner_socket.on :exit do |exit_code|
|
||||
@testrun[:exit_code] = exit_code
|
||||
exit_statement =
|
||||
if @output.empty? && exit_code.zero?
|
||||
if @testrun[:output].empty? && exit_code.zero?
|
||||
@testrun[:status] = :ok
|
||||
t('exercises.implement.no_output_exit_successful', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)
|
||||
elsif @output.empty?
|
||||
elsif @testrun[:output].empty?
|
||||
@testrun[:status] = :failed
|
||||
t('exercises.implement.no_output_exit_failure', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)
|
||||
elsif exit_code.zero?
|
||||
@testrun[:status] = :ok
|
||||
"\n#{t('exercises.implement.exit_successful', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)}"
|
||||
else
|
||||
@testrun[:status] = :failed
|
||||
"\n#{t('exercises.implement.exit_failure', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)}"
|
||||
end
|
||||
client_socket.send_data JSON.dump({cmd: :write, stream: :stdout, data: "#{exit_statement}\n"})
|
||||
send_and_store client_socket, {cmd: :write, stream: :stdout, data: "#{exit_statement}\n"}
|
||||
if exit_code == 137
|
||||
send_and_store client_socket, {cmd: :status, status: :out_of_memory}
|
||||
@testrun[:status] = :out_of_memory
|
||||
end
|
||||
|
||||
close_client_connection(client_socket)
|
||||
end
|
||||
end
|
||||
@container_execution_time = durations[:execution_duration]
|
||||
@waiting_for_container_time = durations[:waiting_duration]
|
||||
@testrun[:container_execution_time] = durations[:execution_duration]
|
||||
@testrun[:waiting_for_container_time] = durations[:waiting_duration]
|
||||
rescue Runner::Error::ExecutionTimeout => e
|
||||
client_socket.send_data JSON.dump({cmd: :status, status: :timeout})
|
||||
send_and_store client_socket, {cmd: :status, status: :timeout}
|
||||
close_client_connection(client_socket)
|
||||
Rails.logger.debug { "Running a submission timed out: #{e.message}" }
|
||||
@output = "timeout: #{@output}"
|
||||
@testrun[:status] ||= :timeout
|
||||
@testrun[:output] = "timeout: #{@testrun[:output]}"
|
||||
extract_durations(e)
|
||||
rescue Runner::Error => e
|
||||
client_socket.send_data JSON.dump({cmd: :status, status: :container_depleted})
|
||||
send_and_store client_socket, {cmd: :status, status: :container_depleted}
|
||||
close_client_connection(client_socket)
|
||||
@testrun[:status] ||= :container_depleted
|
||||
Rails.logger.debug { "Runner error while running a submission: #{e.message}" }
|
||||
extract_durations(e)
|
||||
ensure
|
||||
save_run_output
|
||||
save_testrun_output 'run'
|
||||
end
|
||||
|
||||
def score
|
||||
hijack do |tubesock|
|
||||
return if @embed_options[:disable_score]
|
||||
tubesock.onopen do |_event|
|
||||
switch_locale do
|
||||
kill_client_socket(tubesock) if @embed_options[:disable_score]
|
||||
|
||||
tubesock.send_data(JSON.dump(@submission.calculate_score))
|
||||
# To enable hints when scoring a submission, uncomment the next line:
|
||||
# send_hints(tubesock, StructuredError.where(submission: @submission))
|
||||
rescue Runner::Error => e
|
||||
tubesock.send_data JSON.dump({cmd: :status, status: :container_depleted})
|
||||
Rails.logger.debug { "Runner error while scoring submission #{@submission.id}: #{e.message}" }
|
||||
ensure
|
||||
kill_client_socket(tubesock)
|
||||
# The score is stored separately, we can forward it to the client immediately
|
||||
tubesock.send_data(JSON.dump(@submission.calculate_score))
|
||||
# To enable hints when scoring a submission, uncomment the next line:
|
||||
# send_hints(tubesock, StructuredError.where(submission: @submission))
|
||||
kill_client_socket(tubesock)
|
||||
rescue Runner::Error => e
|
||||
extract_durations(e)
|
||||
send_and_store tubesock, {cmd: :status, status: :container_depleted}
|
||||
kill_client_socket(tubesock)
|
||||
Rails.logger.debug { "Runner error while scoring submission #{@submission.id}: #{e.message}" }
|
||||
@testrun[:passed] = false
|
||||
save_testrun_output 'assess'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -196,14 +224,22 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
def test
|
||||
hijack do |tubesock|
|
||||
return kill_client_socket(tubesock) if @embed_options[:disable_run]
|
||||
tubesock.onopen do |_event|
|
||||
switch_locale do
|
||||
kill_client_socket(tubesock) if @embed_options[:disable_run]
|
||||
|
||||
tubesock.send_data(JSON.dump(@submission.test(@file)))
|
||||
rescue Runner::Error => e
|
||||
tubesock.send_data JSON.dump({cmd: :status, status: :container_depleted})
|
||||
Rails.logger.debug { "Runner error while testing submission #{@submission.id}: #{e.message}" }
|
||||
ensure
|
||||
kill_client_socket(tubesock)
|
||||
# The score is stored separately, we can forward it to the client immediately
|
||||
tubesock.send_data(JSON.dump(@submission.test(@file)))
|
||||
kill_client_socket(tubesock)
|
||||
rescue Runner::Error => e
|
||||
extract_durations(e)
|
||||
send_and_store tubesock, {cmd: :status, status: :container_depleted}
|
||||
kill_client_socket(tubesock)
|
||||
Rails.logger.debug { "Runner error while testing submission #{@submission.id}: #{e.message}" }
|
||||
@testrun[:passed] = false
|
||||
save_testrun_output 'assess'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -221,6 +257,7 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def kill_client_socket(client_socket)
|
||||
# We don't want to store this (arbitrary) exit command and redirect it ourselves
|
||||
client_socket.send_data JSON.dump({cmd: :exit})
|
||||
client_socket.close
|
||||
end
|
||||
@ -240,7 +277,7 @@ class SubmissionsController < ApplicationController
|
||||
# parse validation token
|
||||
content = "#{remote_evaluation_mapping.validation_token}\n"
|
||||
# parse remote request url
|
||||
content += "#{request.base_url}/evaluate\n"
|
||||
content += "#{evaluate_url}\n"
|
||||
@submission.files.each do |file|
|
||||
content += "#{file.filepath}=#{file.file_id}\n"
|
||||
end
|
||||
@ -249,21 +286,33 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
|
||||
def extract_durations(error)
|
||||
@container_execution_time = error.execution_duration
|
||||
@waiting_for_container_time = error.waiting_duration
|
||||
@testrun[:starting_time] = error.starting_time
|
||||
@testrun[:container_execution_time] = error.execution_duration
|
||||
@testrun[:waiting_for_container_time] = error.waiting_duration
|
||||
end
|
||||
|
||||
def extract_errors
|
||||
results = []
|
||||
if @output.present?
|
||||
if @testrun[:output].present?
|
||||
@submission.exercise.execution_environment.error_templates.each do |template|
|
||||
pattern = Regexp.new(template.signature).freeze
|
||||
results << StructuredError.create_from_template(template, @output, @submission) if pattern.match(@output)
|
||||
results << StructuredError.create_from_template(template, @testrun[:output], @submission) if pattern.match(@testrun[:output])
|
||||
end
|
||||
end
|
||||
results
|
||||
end
|
||||
|
||||
def send_and_store(client_socket, message)
|
||||
message[:timestamp] = if @testrun[:starting_time]
|
||||
ActiveSupport::Duration.build(Time.zone.now - @testrun[:starting_time])
|
||||
else
|
||||
0.seconds
|
||||
end
|
||||
@testrun[:messages].push message
|
||||
@testrun[:status] = message[:status] if message[:status]
|
||||
client_socket.send_data JSON.dump(message)
|
||||
end
|
||||
|
||||
def max_output_buffer_size
|
||||
if @submission.cause == 'requestComments'
|
||||
5000
|
||||
@ -272,28 +321,25 @@ class SubmissionsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def prepare(data, stream)
|
||||
if valid_command? data
|
||||
data
|
||||
else
|
||||
JSON.dump({cmd: :write, stream: stream, data: data})
|
||||
end
|
||||
end
|
||||
|
||||
def sanitize_filename
|
||||
params[:filename].gsub(/\.json$/, '')
|
||||
end
|
||||
|
||||
# save the output of this "run" as a "testrun" (scoring runs are saved in submission.rb)
|
||||
def save_run_output
|
||||
Testrun.create(
|
||||
def save_testrun_output(cause)
|
||||
testrun = Testrun.create!(
|
||||
file: @file,
|
||||
cause: 'run',
|
||||
passed: @testrun[:passed],
|
||||
cause: cause,
|
||||
submission: @submission,
|
||||
output: @output,
|
||||
container_execution_time: @container_execution_time,
|
||||
waiting_for_container_time: @waiting_for_container_time
|
||||
exit_code: @testrun[:exit_code], # might be nil, e.g., when the run did not finish
|
||||
status: @testrun[:status],
|
||||
output: @testrun[:output].presence, # TODO: Remove duplicated saving of the output after creating TestrunMessages
|
||||
container_execution_time: @testrun[:container_execution_time],
|
||||
waiting_for_container_time: @testrun[:waiting_for_container_time]
|
||||
)
|
||||
TestrunMessage.create_for(testrun, @testrun[:messages])
|
||||
TestrunExecutionEnvironment.create(testrun: testrun, execution_environment: @submission.used_execution_environment)
|
||||
end
|
||||
|
||||
def send_hints(tubesock, errors)
|
||||
@ -301,7 +347,7 @@ class SubmissionsController < ApplicationController
|
||||
|
||||
errors = errors.to_a.uniq(&:hint)
|
||||
errors.each do |error|
|
||||
tubesock.send_data JSON.dump({cmd: 'hint', hint: error.hint, description: error.error_template.description})
|
||||
send_and_store tubesock, {cmd: :hint, hint: error.hint, description: error.error_template.description}
|
||||
end
|
||||
end
|
||||
|
||||
@ -327,10 +373,26 @@ class SubmissionsController < ApplicationController
|
||||
authorize!
|
||||
end
|
||||
|
||||
def valid_command?(data)
|
||||
def set_testrun
|
||||
@testrun = {
|
||||
messages: [],
|
||||
exit_code: nil,
|
||||
status: nil,
|
||||
}
|
||||
end
|
||||
|
||||
def retrieve_message_from_output(data, stream)
|
||||
parsed = JSON.parse(data)
|
||||
parsed.instance_of?(Hash) && parsed.key?('cmd')
|
||||
if parsed.instance_of?(Hash) && parsed.key?('cmd')
|
||||
parsed.symbolize_keys!
|
||||
# Symbolize two values if present
|
||||
parsed[:cmd] = parsed[:cmd].to_sym
|
||||
parsed[:stream] = parsed[:stream].to_sym if parsed.key? :stream
|
||||
parsed
|
||||
else
|
||||
{cmd: :write, stream: stream, data: data}
|
||||
end
|
||||
rescue JSON::ParserError
|
||||
false
|
||||
{cmd: :write, stream: stream, data: data}
|
||||
end
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ class TagsController < ApplicationController
|
||||
private :tag_params
|
||||
|
||||
def index
|
||||
@tags = Tag.all.paginate(page: params[:page])
|
||||
@tags = Tag.all.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
@ -34,7 +34,7 @@ class TipsController < ApplicationController
|
||||
private :tip_params
|
||||
|
||||
def index
|
||||
@tips = Tip.all.paginate(page: params[:page])
|
||||
@tips = Tip.all.paginate(page: params[:page], per_page: per_page_param)
|
||||
authorize!
|
||||
end
|
||||
|
||||
|
Reference in New Issue
Block a user