merge master

This commit is contained in:
Karol
2022-08-20 22:20:52 +02:00
291 changed files with 5413 additions and 9429 deletions

View File

@ -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

View File

@ -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

View File

@ -44,7 +44,6 @@ class CodeharborLinksController < ApplicationController
def set_codeharbor_link
@codeharbor_link = CodeharborLink.find(params[:id])
@codeharbor_link.user = current_user
authorize!
end

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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