Apply automatic rubocop fixes

This commit is contained in:
Sebastian Serth
2021-05-14 10:51:44 +02:00
parent fe4000916c
commit 6cbecb5b39
440 changed files with 2705 additions and 1853 deletions

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__)
require File.expand_path('config/application', __dir__)
Rails.application.load_tasks

6
Vagrantfile vendored
View File

@ -10,9 +10,9 @@ Vagrant.configure(2) do |config|
v.cpus = 4
end
config.vm.network 'forwarded_port',
host_ip: ENV['LISTEN_ADDRESS'] || '127.0.0.1',
host: 7000,
guest: 7000
host_ip: ENV['LISTEN_ADDRESS'] || '127.0.0.1',
host: 7000,
guest: 7000
config.vm.synced_folder '.', '/home/vagrant/codeocean'
config.vm.synced_folder '../dockercontainerpool', '/home/vagrant/dockercontainerpool'
config.vm.provision 'shell', path: 'provision/provision.vagrant.sh', privileged: false

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module ApplicationCable
class Channel < ActionCable::Channel::Base
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
@ -20,11 +22,7 @@ module ApplicationCable
def find_verified_user
# Finding the current_user is similar to the code used in application_controller.rb#current_user
current_user = ExternalUser.find_by(id: session[:external_user_id]) || InternalUser.find_by(id: session[:user_id])
if current_user
current_user
else
reject_unauthorized_connection
end
current_user || reject_unauthorized_connection
end
end
end

View File

@ -1,5 +1,6 @@
class LaExercisesChannel < ApplicationCable::Channel
# frozen_string_literal: true
class LaExercisesChannel < ApplicationCable::Channel
def subscribed
stream_from specific_channel
end
@ -9,6 +10,7 @@ class LaExercisesChannel < ApplicationCable::Channel
end
private
def specific_channel
reject unless StudyGroupPolicy.new(current_user, StudyGroup.find_by(id: params[:study_group_id])).stream_la?
"la_exercises_#{params[:exercise_id]}_channel_study_group_#{params[:study_group_id]}"

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Admin
class DashboardController < ApplicationController
include DashboardHelper

View File

@ -14,7 +14,8 @@ 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])
::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
end
@ -59,7 +60,7 @@ class ApplicationController < ActionController::Base
respond_to do |format|
format.html do
# Prevent redirect loop
if request.url == request.referrer
if request.url == request.referer
redirect_to :root, alert: message
else
redirect_back fallback_location: :root, allow_other_host: false, alert: message

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module CodeOcean
class FilesController < ApplicationController
include CommonBehavior
@ -25,9 +27,10 @@ module CodeOcean
if @object.save
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)
respond_with_valid_object(format, notice: t('shared.object_created', model: @object.class.model_name.human),
path: path, status: :created)
else
filename = (@object.path || '') + '/' + (@object.name || '') + (@object.file_type.try(:file_extension) || '')
filename = "#{@object.path || ''}/#{@object.name || ''}#{@object.file_type.try(:file_extension) || ''}"
format.html { redirect_to(options[:path]); flash[:danger] = t('files.error.filename', name: filename) }
format.json { render(json: @object.errors, status: :unprocessable_entity) }
end
@ -41,7 +44,10 @@ module CodeOcean
end
def file_params
params[:code_ocean_file].permit(file_attributes).merge(context_type: 'Submission', role: 'user_defined_file') if params[:code_ocean_file].present?
if params[:code_ocean_file].present?
params[:code_ocean_file].permit(file_attributes).merge(context_type: 'Submission',
role: 'user_defined_file')
end
end
private :file_params
end

View File

@ -7,7 +7,8 @@ class CodeharborLinksController < ApplicationController
def new
base_url = CodeOcean::Config.new(:code_ocean).read[:codeharbor][:url] || ''
@codeharbor_link = CodeharborLink.new(push_url: base_url + '/import_exercise', check_uuid_url: base_url + '/import_uuid_check')
@codeharbor_link = CodeharborLink.new(push_url: "#{base_url}/import_exercise",
check_uuid_url: "#{base_url}/import_uuid_check")
authorize!
end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
class CommentsController < ApplicationController
before_action :set_comment, only: [:show, :edit, :update, :destroy]
before_action :set_comment, only: %i[show edit update destroy]
# to disable authorization check: comment the line below back in
# skip_after_action :verify_authorized
# skip_after_action :verify_authorized
def authorize!
authorize(@comment || @comments)
@ -12,16 +14,16 @@ class CommentsController < ApplicationController
# GET /comments.json
def index
file = CodeOcean::File.find(params[:file_id])
#there might be no submission yet, so dont use find
# there might be no submission yet, so dont use find
submission = Submission.find_by(id: file.context_id)
if submission
@comments = Comment.where(file_id: params[:file_id])
@comments.map{|comment|
@comments.map do |comment|
comment.username = comment.user.displayname
comment.date = comment.created_at.strftime('%d.%m.%Y %k:%M')
comment.updated = (comment.created_at != comment.updated_at)
comment.editable = comment.user == current_user
}
end
else
@comments = []
end
@ -81,9 +83,10 @@ class CommentsController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through.
def comment_params
#params.require(:comment).permit(:user_id, :file_id, :row, :column, :text)
# params.require(:comment).permit(:user_id, :file_id, :row, :column, :text)
# fuer production mode, damit böse menschen keine falsche user_id uebergeben:
params.require(:comment).permit(:file_id, :row, :column, :text, :request_id).merge(user_id: current_user.id, user_type: current_user.class.name)
params.require(:comment).permit(:file_id, :row, :column, :text, :request_id).merge(user_id: current_user.id,
user_type: current_user.class.name)
end
def send_mail_to_author(comment, request_for_comment)
@ -96,15 +99,14 @@ class CommentsController < ApplicationController
request_for_comment.commenters.each do |commenter|
already_sent_mail = false
subscriptions = Subscription.where(
:request_for_comment_id => request_for_comment.id,
:user_id => commenter.id, :user_type => commenter.class.name,
:deleted => false)
request_for_comment_id: request_for_comment.id,
user_id: commenter.id, user_type: commenter.class.name,
deleted: false
)
subscriptions.each do |subscription|
if (subscription.subscription_type == 'author' and current_user == request_for_comment.user) or subscription.subscription_type == 'all'
unless subscription.user == current_user or already_sent_mail
UserMailer.got_new_comment_for_subscription(comment, subscription, current_user).deliver_now
already_sent_mail = true
end
if (((subscription.subscription_type == 'author') && (current_user == request_for_comment.user)) || (subscription.subscription_type == 'all')) && !((subscription.user == current_user) || already_sent_mail)
UserMailer.got_new_comment_for_subscription(comment, subscription, current_user).deliver_now
already_sent_mail = true
end
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module CommonBehavior
def create_and_respond(options = {})
@object = options[:object]
@ -5,7 +7,8 @@ module CommonBehavior
if @object.save
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)
respond_with_valid_object(format, notice: t('shared.object_created', model: @object.class.model_name.human),
path: path, status: :created)
else
respond_with_invalid_object(format, template: :new)
end
@ -40,7 +43,8 @@ module CommonBehavior
respond_to do |format|
if @object.update(options[:params])
path = options[:path] || @object
respond_with_valid_object(format, notice: t('shared.object_updated', model: @object.class.model_name.human), path: path, status: :ok)
respond_with_valid_object(format, notice: t('shared.object_updated', model: @object.class.model_name.human),
path: path, status: :ok)
else
respond_with_invalid_object(format, template: :edit)
end

View File

@ -15,7 +15,8 @@ module FileParameters
private :reject_illegal_file_attributes
def file_attributes
%w[content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight file_template_id]
%w[content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight
file_template_id]
end
private :file_attributes
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'oauth/request_proxy/rack_request'
module Lti
@ -6,7 +8,7 @@ module Lti
MAXIMUM_SCORE = 1
MAXIMUM_SESSION_AGE = 60.minutes
SESSION_PARAMETERS = %w(launch_presentation_return_url lis_outcome_service_url lis_result_sourcedid)
SESSION_PARAMETERS = %w[launch_presentation_return_url lis_outcome_service_url lis_result_sourcedid].freeze
def build_tool_provider(options = {})
if options[:consumer] && options[:parameters]
@ -20,7 +22,7 @@ module Lti
# 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)
if (exercise_id.nil?)
if exercise_id.nil?
session.delete(:external_user_id)
session.delete(:study_group_id)
session.delete(:embed_options)
@ -48,27 +50,23 @@ module Lti
def external_user_name(provider)
# save person_name_full if supplied. this is the display_name, if it is set.
# else only save the firstname, we don't want lastnames (family names)
if provider.lis_person_name_full
provider.lis_person_name_full
else
provider.lis_person_name_given
end
provider.lis_person_name_full || provider.lis_person_name_given
end
private :external_user_name
def external_user_role(provider)
result = 'learner'
unless provider.roles.blank?
if provider.roles.present?
provider.roles.each do |role|
case role.downcase
when 'administrator'
# We don't want anyone to get admin privileges through LTI
result = 'teacher' if result == 'learner'
when 'instructor'
result = 'teacher' if result == 'learner'
else # 'learner'
next
when 'administrator'
# We don't want anyone to get admin privileges through LTI
result = 'teacher' if result == 'learner'
when 'instructor'
result = 'teacher' if result == 'learner'
else # 'learner'
next
end
end
end
@ -141,7 +139,9 @@ module Lti
def send_score(submission)
::NewRelic::Agent.add_custom_attributes({score: submission.normalized_score, session: session})
fail(Error, "Score #{submission.normalized_score} must be between 0 and #{MAXIMUM_SCORE}!") unless (0..MAXIMUM_SCORE).include?(submission.normalized_score)
unless (0..MAXIMUM_SCORE).cover?(submission.normalized_score)
raise Error.new("Score #{submission.normalized_score} must be between 0 and #{MAXIMUM_SCORE}!")
end
if submission.user.consumer
lti_parameter = LtiParameter.where(consumers_id: submission.user.consumer.id,
@ -155,12 +155,12 @@ module Lti
{status: 'error'}
elsif provider.outcome_service?
Sentry.set_extras({
provider: provider.inspect,
provider: provider.inspect,
score: submission.normalized_score,
lti_parameter: lti_parameter.inspect,
session: session.to_hash,
exercise_id: submission.exercise_id
})
exercise_id: submission.exercise_id,
})
normalized_lit_score = submission.normalized_score
if submission.before_deadline?
# Keep the full score
@ -170,11 +170,10 @@ module Lti
elsif submission.after_late_deadline?
# Reduce score by 100%
normalized_lit_score *= 0.0
else # no deadline
# Keep the full score
end
response = provider.post_replace_result!(normalized_lit_score)
{code: response.response_code, message: response.post_response.body, status: response.code_major, score_sent: normalized_lit_score}
{code: response.response_code, message: response.post_response.body, status: response.code_major,
score_sent: normalized_lit_score}
else
{status: 'unsupported'}
end
@ -186,22 +185,21 @@ module Lti
@current_user = ExternalUser.find_or_create_by(consumer_id: @consumer.id, external_id: @provider.user_id)
external_role = external_user_role(@provider)
internal_role = @current_user.role
desired_role = internal_role != 'admin' ? external_role : internal_role
desired_role = internal_role == 'admin' ? internal_role : external_role
# Update user with new information but change the role only if he is no admin user
@current_user.update(email: external_user_email(@provider), name: external_user_name(@provider), role: desired_role)
end
private :set_current_user
def set_study_group_membership
group = if not context_id?
StudyGroup.find_or_create_by(external_id: @provider.resource_link_id, consumer: @consumer)
else
group = if context_id?
# Ensure to find the group independent of the name and set it only once.
StudyGroup.find_or_create_by(external_id: @provider.context_id, consumer: @consumer) do |new_group|
new_group.name = @provider.context_title
end
else
StudyGroup.find_or_create_by(external_id: @provider.resource_link_id, consumer: @consumer)
end
group.external_users << @current_user unless group.external_users.include? @current_user
group.save

View File

@ -1,8 +1,13 @@
# frozen_string_literal: true
module RemoteEvaluationParameters
include FileParameters
def remote_evaluation_params
remote_evaluation_params = params[:remote_evaluation].permit(:validation_token, files_attributes: file_attributes) if params[:remote_evaluation].present?
if params[:remote_evaluation].present?
remote_evaluation_params = params[:remote_evaluation].permit(:validation_token,
files_attributes: file_attributes)
end
end
private :remote_evaluation_params
end
end

View File

@ -4,7 +4,12 @@ module SubmissionParameters
include FileParameters
def submission_params
submission_params = params[:submission].present? ? params[:submission].permit(:cause, :exercise_id, files_attributes: file_attributes) : {}
submission_params = if params[:submission].present?
params[:submission].permit(:cause, :exercise_id,
files_attributes: file_attributes)
else
{}
end
submission_params = merge_user(submission_params)
files_attributes = submission_params[:files_attributes]
exercise = Exercise.find_by(id: submission_params[:exercise_id])

View File

@ -12,8 +12,8 @@ module SubmissionScoring
output = execute_test_file(file, submission)
assessment = assessor.assess(output)
passed = ((assessment[:passed] == assessment[:count]) and (assessment[:score]).positive?)
testrun_output = passed ? nil : 'status: ' + output[:status].to_s + "\n stdout: " + output[:stdout].to_s + "\n stderr: " + output[:stderr].to_s
unless testrun_output.blank?
testrun_output = passed ? nil : "status: #{output[:status]}\n stdout: #{output[:stdout]}\n stderr: #{output[:stderr]}"
if testrun_output.present?
submission.exercise.execution_environment.error_templates.each do |template|
pattern = Regexp.new(template.signature).freeze
StructuredError.create_from_template(template, testrun_output, submission) if pattern.match(testrun_output)
@ -50,7 +50,8 @@ module SubmissionScoring
private :collect_test_results
def execute_test_file(file, submission)
DockerClient.new(execution_environment: file.context.execution_environment).execute_test_command(submission, file.name_with_extension)
DockerClient.new(execution_environment: file.context.execution_environment).execute_test_command(submission,
file.name_with_extension)
end
private :execute_test_file
@ -69,17 +70,21 @@ module SubmissionScoring
def score_submission(submission)
outputs = collect_test_results(submission)
score = 0.0
unless outputs.nil? || outputs.empty?
if outputs.present?
outputs.each do |output|
score += output[:score] * output[:weight] unless output.nil?
output[:stderr] += "\n\n#{t('exercises.editor.timeout', permitted_execution_time: submission.exercise.execution_environment.permitted_execution_time.to_s)}" if output.present? && output[:status] == :timeout
if output.present? && output[:status] == :timeout
output[:stderr] += "\n\n#{t('exercises.editor.timeout',
permitted_execution_time: submission.exercise.execution_environment.permitted_execution_time.to_s)}"
end
end
end
submission.update(score: score)
if submission.normalized_score == 1.0
Thread.new do
RequestForComment.where(exercise_id: submission.exercise_id, user_id: submission.user_id, user_type: submission.user_type).each do |rfc|
RequestForComment.where(exercise_id: submission.exercise_id, user_id: submission.user_id,
user_type: submission.user_type).each do |rfc|
rfc.full_score_reached = true
rfc.save
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ConsumersController < ApplicationController
include CommonBehavior
@ -18,8 +20,7 @@ class ConsumersController < ApplicationController
destroy_and_respond(object: @consumer)
end
def edit
end
def edit; end
def consumer_params
params[:consumer].permit(:name, :oauth_key, :oauth_secret) if params[:consumer].present?
@ -42,8 +43,7 @@ class ConsumersController < ApplicationController
end
private :set_consumer
def show
end
def show; end
def update
update_and_respond(object: @consumer, params: consumer_params)

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class ErrorTemplateAttributesController < ApplicationController
before_action :set_error_template_attribute, only: [:show, :edit, :update, :destroy]
before_action :set_error_template_attribute, only: %i[show edit update destroy]
def authorize!
authorize(@error_template_attributes || @error_template_attribute)
@ -9,7 +11,8 @@ class ErrorTemplateAttributesController < ApplicationController
# GET /error_template_attributes
# GET /error_template_attributes.json
def index
@error_template_attributes = ErrorTemplateAttribute.all.order('important DESC', :key, :id).paginate(page: params[:page])
@error_template_attributes = ErrorTemplateAttribute.all.order('important DESC', :key,
:id).paginate(page: params[:page])
authorize!
end
@ -38,7 +41,9 @@ class ErrorTemplateAttributesController < ApplicationController
respond_to do |format|
if @error_template_attribute.save
format.html { redirect_to @error_template_attribute, notice: 'Error template attribute was successfully created.' }
format.html do
redirect_to @error_template_attribute, notice: 'Error template attribute was successfully created.'
end
format.json { render :show, status: :created, location: @error_template_attribute }
else
format.html { render :new }
@ -53,7 +58,9 @@ class ErrorTemplateAttributesController < ApplicationController
authorize!
respond_to do |format|
if @error_template_attribute.update(error_template_attribute_params)
format.html { redirect_to @error_template_attribute, notice: 'Error template attribute was successfully updated.' }
format.html do
redirect_to @error_template_attribute, notice: 'Error template attribute was successfully updated.'
end
format.json { render :show, status: :ok, location: @error_template_attribute }
else
format.html { render :edit }
@ -68,19 +75,25 @@ class ErrorTemplateAttributesController < ApplicationController
authorize!
@error_template_attribute.destroy
respond_to do |format|
format.html { redirect_to error_template_attributes_url, notice: 'Error template attribute was successfully destroyed.' }
format.html do
redirect_to error_template_attributes_url, notice: 'Error template attribute was successfully destroyed.'
end
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_error_template_attribute
@error_template_attribute = ErrorTemplateAttribute.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def error_template_attribute_params
params[:error_template_attribute].permit(:key, :description, :regex, :important) if params[:error_template_attribute].present?
# Use callbacks to share common setup or constraints between actions.
def set_error_template_attribute
@error_template_attribute = ErrorTemplateAttribute.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def error_template_attribute_params
if params[:error_template_attribute].present?
params[:error_template_attribute].permit(:key, :description, :regex,
:important)
end
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class ErrorTemplatesController < ApplicationController
before_action :set_error_template, only: [:show, :edit, :update, :destroy, :add_attribute, :remove_attribute]
before_action :set_error_template, only: %i[show edit update destroy add_attribute remove_attribute]
def authorize!
authorize(@error_templates || @error_template)
@ -92,13 +94,17 @@ class ErrorTemplatesController < ApplicationController
end
private
# Use callbacks to share common setup or constraints between actions.
def set_error_template
@error_template = ErrorTemplate.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def error_template_params
params[:error_template].permit(:name, :execution_environment_id, :signature, :description, :hint) if params[:error_template].present?
# Use callbacks to share common setup or constraints between actions.
def set_error_template
@error_template = ErrorTemplate.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def error_template_params
if params[:error_template].present?
params[:error_template].permit(:name, :execution_environment_id, :signature, :description,
:hint)
end
end
end

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true
class ExecutionEnvironmentsController < ApplicationController
include CommonBehavior
before_action :set_docker_images, only: [:create, :edit, :new, :update]
before_action :set_execution_environment, only: MEMBER_ACTIONS + [:execute_command, :shell, :statistics]
before_action :set_testing_framework_adapters, only: [:create, :edit, :new, :update]
before_action :set_docker_images, only: %i[create edit new update]
before_action :set_execution_environment, only: MEMBER_ACTIONS + %i[execute_command shell statistics]
before_action :set_testing_framework_adapters, only: %i[create edit new update]
def authorize!
authorize(@execution_environment || @execution_environments)
@ -20,17 +22,15 @@ class ExecutionEnvironmentsController < ApplicationController
destroy_and_respond(object: @execution_environment)
end
def edit
end
def edit; end
def execute_command
@docker_client = DockerClient.new(execution_environment: @execution_environment)
render(json: @docker_client.execute_arbitrary_command(params[:command]))
end
def working_time_query
"""
"
SELECT exercise_id, avg(working_time) as average_time, stddev_samp(extract('epoch' from working_time)) * interval '1 second' as stddev_time
FROM
(
@ -52,11 +52,11 @@ class ExecutionEnvironmentsController < ApplicationController
GROUP BY exercise_id, user_id, id) AS foo) AS bar
GROUP BY user_id, exercise_id
) AS baz GROUP BY exercise_id;
"""
"
end
def user_query
"""
"
SELECT
id AS exercise_id,
COUNT(DISTINCT user_id) AS users,
@ -79,7 +79,7 @@ class ExecutionEnvironmentsController < ApplicationController
GROUP BY e.id,
s.user_id) AS inner_query
GROUP BY id;
"""
"
end
def statistics
@ -87,21 +87,25 @@ class ExecutionEnvironmentsController < ApplicationController
user_statistics = {}
ApplicationRecord.connection.execute(working_time_query).each do |tuple|
working_time_statistics[tuple["exercise_id"].to_i] = tuple
working_time_statistics[tuple['exercise_id'].to_i] = tuple
end
ApplicationRecord.connection.execute(user_query).each do |tuple|
user_statistics[tuple["exercise_id"].to_i] = tuple
user_statistics[tuple['exercise_id'].to_i] = tuple
end
render locals: {
working_time_statistics: working_time_statistics,
user_statistics: user_statistics
user_statistics: user_statistics,
}
end
def execution_environment_params
params[:execution_environment].permit(:docker_image, :exposed_ports, :editor_mode, :file_extension, :file_type_id, :help, :indent_size, :memory_limit, :name, :network_enabled, :permitted_execution_time, :pool_size, :run_command, :test_command, :testing_framework).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:execution_environment].present?
if params[:execution_environment].present?
params[:execution_environment].permit(:docker_image, :exposed_ports, :editor_mode, :file_extension, :file_type_id, :help, :indent_size, :memory_limit, :name, :network_enabled, :permitted_execution_time, :pool_size, :run_command, :test_command, :testing_framework).merge(
user_id: current_user.id, user_type: current_user.class.name
)
end
end
private :execution_environment_params
@ -118,10 +122,10 @@ class ExecutionEnvironmentsController < ApplicationController
def set_docker_images
DockerClient.check_availability!
@docker_images = DockerClient.image_tags.sort
rescue DockerClient::Error => error
rescue DockerClient::Error => e
@docker_images = []
flash[:warning] = error.message
Sentry.capture_exception(error)
flash[:warning] = e.message
Sentry.capture_exception(e)
end
private :set_docker_images
@ -139,8 +143,7 @@ class ExecutionEnvironmentsController < ApplicationController
end
private :set_testing_framework_adapters
def shell
end
def shell; end
def show
if @execution_environment.testing_framework?

View File

@ -1,15 +1,16 @@
# frozen_string_literal: true
class ExerciseCollectionsController < ApplicationController
include CommonBehavior
before_action :set_exercise_collection, only: [:show, :edit, :update, :destroy, :statistics]
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])
authorize!
end
def show
end
def show; end
def new
@exercise_collection = ExerciseCollection.new
@ -28,16 +29,14 @@ class ExerciseCollectionsController < ApplicationController
destroy_and_respond(object: @exercise_collection)
end
def edit
end
def edit; end
def update
authorize!
update_and_respond(object: @exercise_collection, params: exercise_collection_params)
end
def statistics
end
def statistics; end
private
@ -51,8 +50,18 @@ class ExerciseCollectionsController < ApplicationController
end
def exercise_collection_params
sanitized_params = params[:exercise_collection].present? ? params[:exercise_collection].permit(:name, :use_anomaly_detection, :user_id, :user_type, :exercise_ids => []).merge(user_type: InternalUser.name) : {}
sanitized_params[:exercise_ids] = sanitized_params[:exercise_ids].reject {|v| v.nil? or v == ''}
sanitized_params.tap {|p| p[:exercise_collection_items] = p[:exercise_ids].map.with_index {|_id, index| ExerciseCollectionItem.find_or_create_by(exercise_id: _id, exercise_collection_id: @exercise_collection.id, position: index)}; p.delete(:exercise_ids)}
sanitized_params = if params[:exercise_collection].present?
params[:exercise_collection].permit(:name,
:use_anomaly_detection, :user_id, :user_type, exercise_ids: []).merge(user_type: InternalUser.name)
else
{}
end
sanitized_params[:exercise_ids] = sanitized_params[:exercise_ids].reject {|v| v.nil? or v == '' }
sanitized_params.tap do |p|
p[:exercise_collection_items] = p[:exercise_ids].map.with_index do |_id, index|
ExerciseCollectionItem.find_or_create_by(exercise_id: _id, exercise_collection_id: @exercise_collection.id, position: index)
end
p.delete(:exercise_ids)
end
end
end

View File

@ -9,15 +9,19 @@ class ExercisesController < ApplicationController
before_action :handle_file_uploads, only: %i[create update]
before_action :set_execution_environments, only: %i[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_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]
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_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_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check export_external_confirm],
raise: false
def authorize!
authorize(@exercise || @exercises)
@ -72,8 +76,8 @@ class ExercisesController < ApplicationController
return if performed?
myparam = exercise_params.presence || {}
checked_exercise_tags = @exercise_tags.select { |et| myparam[:tag_ids].include? et.tag.id.to_s }
removed_exercise_tags = @exercise_tags.reject { |et| myparam[:tag_ids].include? et.tag.id.to_s }
checked_exercise_tags = @exercise_tags.select {|et| myparam[:tag_ids].include? et.tag.id.to_s }
removed_exercise_tags = @exercise_tags.reject {|et| myparam[:tag_ids].include? et.tag.id.to_s }
checked_exercise_tags.each do |et|
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
@ -106,7 +110,8 @@ class ExercisesController < ApplicationController
end
def export_external_check
codeharbor_check = ExerciseService::CheckExternal.call(uuid: @exercise.uuid, codeharbor_link: current_user.codeharbor_link)
codeharbor_check = ExerciseService::CheckExternal.call(uuid: @exercise.uuid,
codeharbor_link: current_user.codeharbor_link)
render json: {
message: codeharbor_check[:message],
actions: render_to_string(
@ -116,9 +121,9 @@ class ExercisesController < ApplicationController
exercise_found: codeharbor_check[:exercise_found],
update_right: codeharbor_check[:update_right],
error: codeharbor_check[:error],
exported: false
exported: false,
}
)
),
}, status: :ok
end
@ -133,14 +138,16 @@ class ExercisesController < ApplicationController
render json: {
status: 'success',
message: t('exercises.export_codeharbor.successfully_exported', id: @exercise.id, title: @exercise.title),
actions: render_to_string(partial: 'export_actions', locals: {exercise: @exercise, exported: true, error: error})
actions: render_to_string(partial: 'export_actions',
locals: {exercise: @exercise, exported: true, error: error}),
}
@exercise.save
else
render json: {
status: 'fail',
message: t('exercises.export_codeharbor.export_failed', id: @exercise.id, title: @exercise.title, error: error),
actions: render_to_string(partial: 'export_actions', locals: {exercise: @exercise, exported: true, error: error})
actions: render_to_string(partial: 'export_actions',
locals: {exercise: @exercise, exported: true, error: error}),
}
end
end
@ -177,7 +184,7 @@ class ExercisesController < ApplicationController
render json: t('exercises.import_codeharbor.import_errors.invalid'), status: :bad_request
rescue StandardError => e
Sentry.capture_exception(e)
render json: t('exercises.import_codeharbor.import_errors.internal_error'), status: 500
render json: t('exercises.import_codeharbor.import_errors.internal_error'), status: :internal_server_error
end
def user_from_api_key
@ -194,7 +201,11 @@ class ExercisesController < ApplicationController
private :user_by_codeharbor_token
def exercise_params
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :submission_deadline, :late_submission_deadline, :public, :unpublished, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, :tips, files_attributes: file_attributes, tag_ids: []).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:exercise].present?
if params[:exercise].present?
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :submission_deadline, :late_submission_deadline, :public, :unpublished, :hide_file_tree, :allow_file_creation, :allow_auto_completion, :title, :expected_difficulty, :tips, files_attributes: file_attributes, tag_ids: []).merge(
user_id: current_user.id, user_type: current_user.class.name
)
end
end
private :exercise_params
@ -261,8 +272,10 @@ class ExercisesController < ApplicationController
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished? && current_user.role != 'admin' && current_user.role != 'teacher' # TODO: TESTESTEST
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
user_solved_exercise = @exercise.has_user_solved(current_user)
count_interventions_today = UserExerciseIntervention.where(user: current_user).where('created_at >= ?', Time.zone.now.beginning_of_day).count
user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user, exercise: @exercise).size >= max_intervention_count_per_exercise
count_interventions_today = UserExerciseIntervention.where(user: current_user).where('created_at >= ?',
Time.zone.now.beginning_of_day).count
user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user,
exercise: @exercise).size >= max_intervention_count_per_exercise
(user_got_enough_interventions = count_interventions_today >= max_intervention_count_per_day) || user_got_intervention_in_exercise
if @embed_options[:disable_interventions]
@ -331,14 +344,15 @@ class ExercisesController < ApplicationController
end
# Return an array with top-level tips
@tips = nested_tips.values.select { |tip| tip.parent_exercise_tip_id.nil? }
@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})
render(json: {working_time_75_percentile: working_time_75_percentile,
working_time_accumulated: working_time_accumulated})
end
def intervention
@ -436,7 +450,9 @@ class ExercisesController < ApplicationController
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 { |tag| ExerciseTag.new(exercise: @exercise, tag: tag) }
@exercise_tags = checked_exercise_tags + unchecked_tags.collect do |tag|
ExerciseTag.new(exercise: @exercise, tag: tag)
end
end
private :collect_set_and_unset_exercise_tags
@ -453,8 +469,10 @@ class ExercisesController < ApplicationController
# 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)
@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?
@ -465,7 +483,8 @@ class ExercisesController < ApplicationController
@working_times_until.push((format_time_difference(@deltas[0..index].inject(:+)) if index.positive?))
end
else
final_submissions = Submission.where(user: @external_user, exercise_id: @exercise.id).in_study_group_of(current_user).final
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
@ -492,7 +511,7 @@ class ExercisesController < ApplicationController
user_statistics[tuple['user_id'].to_i] = tuple
end
render locals: {
user_statistics: user_statistics
user_statistics: user_statistics,
}
end
end
@ -508,7 +527,8 @@ class ExercisesController < ApplicationController
end
def transmit_lti_score
::NewRelic::Agent.add_custom_attributes({submission: @submission.id, normalized_score: @submission.normalized_score})
::NewRelic::Agent.add_custom_attributes({submission: @submission.id,
normalized_score: @submission.normalized_score})
response = send_score(@submission)
if response[:status] == 'success'
@ -533,8 +553,8 @@ class ExercisesController < ApplicationController
return if performed?
myparam = exercise_params
checked_exercise_tags = @exercise_tags.select { |et| myparam[:tag_ids].include? et.tag.id.to_s }
removed_exercise_tags = @exercise_tags.reject { |et| myparam[:tag_ids].include? et.tag.id.to_s }
checked_exercise_tags = @exercise_tags.select {|et| myparam[:tag_ids].include? et.tag.id.to_s }
removed_exercise_tags = @exercise_tags.reject {|et| myparam[:tag_ids].include? et.tag.id.to_s }
checked_exercise_tags.each do |et|
et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
@ -624,9 +644,9 @@ class ExercisesController < ApplicationController
authorize!
@study_group_id = params[:study_group_id]
@request_for_comments = RequestForComment
.where(exercise: @exercise).includes(:submission)
.where(submissions: {study_group_id: @study_group_id})
.order(created_at: :desc)
.where(exercise: @exercise).includes(:submission)
.where(submissions: {study_group_id: @study_group_id})
.order(created_at: :desc)
@graph_data = @exercise.get_working_times_for_study_group(@study_group_id)
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ExternalUsersController < ApplicationController
before_action :require_user!
@ -17,8 +19,8 @@ class ExternalUsersController < ApplicationController
authorize!
end
def working_time_query(tag=nil)
"""
def working_time_query(tag = nil)
"
SELECT user_id,
bar.exercise_id,
max(score) as maximum_score,
@ -43,16 +45,16 @@ class ExternalUsersController < ApplicationController
FROM submissions
WHERE user_id = #{@user.id}
AND user_type = 'ExternalUser'
#{!current_user.admin? ? "AND study_group_id IN (#{current_user.study_groups.pluck(:id).join(', ')}) AND cause = 'submit'" : ''}
#{current_user.admin? ? '' : "AND study_group_id IN (#{current_user.study_groups.pluck(:id).join(', ')}) AND cause = 'submit'"}
GROUP BY exercise_id,
user_id,
id
) AS foo
) AS bar
#{tag.nil? ? '' : ' JOIN exercise_tags et ON et.exercise_id = bar.exercise_id AND et.tag_id = ' + tag + ' '}
#{tag.nil? ? '' : " JOIN exercise_tags et ON et.exercise_id = bar.exercise_id AND et.tag_id = #{tag} "}
GROUP BY user_id,
bar.exercise_id;
"""
"
end
def statistics
@ -62,11 +64,11 @@ class ExternalUsersController < ApplicationController
statistics = {}
ApplicationRecord.connection.execute(working_time_query(params[:tag])).each do |tuple|
statistics[tuple["exercise_id"].to_i] = tuple
statistics[tuple['exercise_id'].to_i] = tuple
end
render locals: {
statistics: statistics
statistics: statistics,
}
end
@ -75,15 +77,15 @@ class ExternalUsersController < ApplicationController
authorize!
statistics = []
tags = ProxyExercise.new().get_user_knowledge_and_max_knowledge(@user, @user.participations.uniq.compact)
tags = ProxyExercise.new.get_user_knowledge_and_max_knowledge(@user, @user.participations.uniq.compact)
tags[:user_topic_knowledge].each_pair do |tag, value|
statistics.append({key: tag.name.to_s, value: (100.0 / tags[:max_topic_knowledge][tag] * value).round, id: tag.id})
statistics.append({key: tag.name.to_s, value: (100.0 / tags[:max_topic_knowledge][tag] * value).round,
id: tag.id})
end
statistics.sort_by! {|item| -item[:value]}
statistics.sort_by! {|item| -item[:value] }
respond_to do |format|
format.json { render(json: statistics) }
end
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class FileTemplatesController < ApplicationController
before_action :set_file_template, only: [:show, :edit, :update, :destroy]
before_action :set_file_template, only: %i[show edit update destroy]
def authorize!
authorize(@file_template || @file_templates)
@ -7,7 +9,7 @@ class FileTemplatesController < ApplicationController
private :authorize!
def by_file_type
@file_templates = FileTemplate.where(:file_type_id => params[:file_type_id])
@file_templates = FileTemplate.where(file_type_id: params[:file_type_id])
authorize!
respond_to do |format|
format.json { render :show, status: :ok, json: @file_templates.to_json }
@ -82,13 +84,14 @@ class FileTemplatesController < ApplicationController
end
private
# Use callbacks to share common setup or constraints between actions.
def set_file_template
@file_template = FileTemplate.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def file_template_params
params[:file_template].permit(:name, :file_type_id, :content) if params[:file_template].present?
end
# Use callbacks to share common setup or constraints between actions.
def set_file_template
@file_template = FileTemplate.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def file_template_params
params[:file_template].permit(:name, :file_type_id, :content) if params[:file_template].present?
end
end

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
class FileTypesController < ApplicationController
include CommonBehavior
before_action :set_editor_modes, only: [:create, :edit, :new, :update]
before_action :set_editor_modes, only: %i[create edit new update]
before_action :set_file_type, only: MEMBER_ACTIONS
def authorize!
@ -19,11 +21,14 @@ class FileTypesController < ApplicationController
destroy_and_respond(object: @file_type)
end
def edit
end
def edit; end
def file_type_params
params[:file_type].permit(:binary, :editor_mode, :executable, :file_extension, :name, :indent_size, :renderable).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:file_type].present?
if params[:file_type].present?
params[:file_type].permit(:binary, :editor_mode, :executable, :file_extension, :name, :indent_size, :renderable).merge(
user_id: current_user.id, user_type: current_user.class.name
)
end
end
private :file_type_params
@ -39,7 +44,7 @@ class FileTypesController < ApplicationController
def set_editor_modes
@editor_modes = Dir.glob('vendor/assets/javascripts/ace/mode-*.js').sort.map do |filename|
name = filename.gsub(/\w+\/|mode-|.js$/, '')
name = filename.gsub(%r{\w+/|mode-|.js$}, '')
[name, "ace/mode/#{name}"]
end
end
@ -51,8 +56,7 @@ class FileTypesController < ApplicationController
end
private :set_file_type
def show
end
def show; end
def update
update_and_respond(object: @file_type, params: file_type_params)

View File

@ -1,11 +1,12 @@
class FlowrController < ApplicationController
# frozen_string_literal: true
class FlowrController < ApplicationController
def insights
require_user!
# 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
.where(submissions: {user_id: current_user.id, user_type: current_user.class.name})
.order('testruns.created_at DESC').first
# Return if no submission was found
if submission.blank? || @embed_options[:disable_hints] || @embed_options[:hide_test_results]
@ -26,8 +27,8 @@ class FlowrController < ApplicationController
end
# once the programming language model becomes available, the language name can be added to the query to
# produce more relevant results
query = attributes.map{|att| att.value}.join(' ')
{ submission: submission, error: error, attributes: attributes, query: query }
query = attributes.map(&:value).join(' ')
{submission: submission, error: error, attributes: attributes, query: query}
end
# Always return JSON

View File

@ -93,7 +93,8 @@ class InternalUsersController < ApplicationController
private :require_reset_password_token
def require_token(type)
@user = InternalUser.send(:"load_from_#{type}_token", params[:token] || params[:internal_user].try(:[], :"#{type}_token"))
@user = InternalUser.send(:"load_from_#{type}_token",
params[:token] || params[:internal_user].try(:[], :"#{type}_token"))
render_not_authorized unless @user
end
private :require_token

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
class ProxyExercisesController < ApplicationController
include CommonBehavior
before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + [:clone, :reload]
before_action :set_exercise_and_authorize, only: MEMBER_ACTIONS + %i[clone reload]
def authorize!
authorize(@proxy_exercise || @proxy_exercises)
@ -9,10 +11,11 @@ class ProxyExercisesController < ApplicationController
private :authorize!
def clone
proxy_exercise = @proxy_exercise.duplicate(public: false, token: nil, exercises: @proxy_exercise.exercises, user: current_user)
proxy_exercise = @proxy_exercise.duplicate(public: false, token: nil, exercises: @proxy_exercise.exercises,
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, notice: t('shared.object_cloned', model: ProxyExercise.model_name.human))
else
flash[:danger] = t('shared.message_failure')
redirect_to(@proxy_exercise)
@ -21,7 +24,7 @@ class ProxyExercisesController < ApplicationController
def create
myparams = proxy_exercise_params
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject { |c| c.empty? })
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject(&:empty?))
@proxy_exercise = ProxyExercise.new(myparams)
authorize!
@ -39,7 +42,10 @@ class ProxyExercisesController < ApplicationController
end
def proxy_exercise_params
params[:proxy_exercise].permit(:description, :title, :public, :exercise_ids => []).merge(user_id: current_user.id, user_type: current_user.class.name) if params[:proxy_exercise].present?
if params[:proxy_exercise].present?
params[:proxy_exercise].permit(:description, :title, :public, exercise_ids: []).merge(user_id: current_user.id,
user_type: current_user.class.name)
end
end
private :proxy_exercise_params
@ -50,7 +56,7 @@ class ProxyExercisesController < ApplicationController
end
def new
@proxy_exercise = ProxyExercise.new
@proxy_exercise = ProxyExercise.new
@search = policy_scope(Exercise).ransack(params[:q])
@exercises = @search.result.order(:title)
authorize!
@ -64,17 +70,15 @@ class ProxyExercisesController < ApplicationController
def show
@search = @proxy_exercise.exercises.ransack
@exercises = @proxy_exercise.exercises.ransack.result.order(:title) #@search.result.order(:title)
@exercises = @proxy_exercise.exercises.ransack.result.order(:title) # @search.result.order(:title)
end
#we might want to think about auth here
def reload
end
# we might want to think about auth here
def reload; end
def update
myparams = proxy_exercise_params
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject { |c| c.blank? })
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].reject(&:blank?))
update_and_respond(object: @proxy_exercise, params: myparams)
end
end

View File

@ -26,7 +26,7 @@ class RemoteEvaluationController < ApplicationController
if @submission.present?
score_achieved_percentage = @submission.normalized_score
result = try_lti
result.merge!({score: score_achieved_percentage * 100}) unless result[:score]
result[:score] = score_achieved_percentage * 100 unless result[:score]
status = result[:status]
end
@ -38,7 +38,9 @@ class RemoteEvaluationController < ApplicationController
lti_response = send_score(@submission)
process_lti_response(lti_response)
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.", status: 410}
{
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.", status: 410
}
end
end
private :try_lti
@ -48,7 +50,8 @@ class RemoteEvaluationController < ApplicationController
# Score has been reduced due to the passed deadline
{message: I18n.t('exercises.submit.too_late'), status: 207, score: lti_response[:score_sent] * 100}
elsif lti_response[:status] == 'success'
{message: I18n.t('sessions.destroy_through_lti.success_with_outcome', consumer: @submission.user.consumer.name), status: 202}
{message: I18n.t('sessions.destroy_through_lti.success_with_outcome', consumer: @submission.user.consumer.name),
status: 202}
else
{message: I18n.t('exercises.submit.failure'), status: 424}
end
@ -77,7 +80,8 @@ class RemoteEvaluationController < ApplicationController
submission_params[:study_group_id] = remote_evaluation_mapping.study_group_id
submission_params[:cause] = cause
submission_params[:user_type] = remote_evaluation_mapping.user_type
submission_params[:files_attributes] = reject_illegal_file_attributes(remote_evaluation_mapping.exercise, files_attributes)
submission_params[:files_attributes] =
reject_illegal_file_attributes(remote_evaluation_mapping.exercise, files_attributes)
submission_params
end
private :build_submission_params

View File

@ -5,7 +5,8 @@ class RequestForCommentsController < ApplicationController
before_action :require_user!
before_action :set_request_for_comment, only: %i[show mark_as_solved set_thank_you_note]
before_action :set_study_group_grouping, only: %i[index get_my_comment_requests get_rfcs_with_my_comments get_rfcs_for_exercise]
before_action :set_study_group_grouping,
only: %i[index get_my_comment_requests get_rfcs_with_my_comments get_rfcs_for_exercise]
def authorize!
authorize(@request_for_comments || @request_for_comment)
@ -16,15 +17,15 @@ class RequestForCommentsController < ApplicationController
# GET /request_for_comments.json
def index
@search = RequestForComment
.last_per_user(2)
.with_last_activity
.ransack(params[:q])
.last_per_user(2)
.with_last_activity
.ransack(params[:q])
@request_for_comments = @search.result
.joins(:exercise)
.where(exercises: {unpublished: false})
.includes(submission: [:study_group])
.order('created_at DESC')
.paginate(page: params[:page], total_entries: @search.result.length)
.joins(:exercise)
.where(exercises: {unpublished: false})
.includes(submission: [:study_group])
.order('created_at DESC')
.paginate(page: params[:page], total_entries: @search.result.length)
authorize!
end
@ -32,12 +33,12 @@ class RequestForCommentsController < ApplicationController
# GET /my_request_for_comments
def get_my_comment_requests
@search = RequestForComment
.with_last_activity
.where(user: current_user)
.ransack(params[:q])
.with_last_activity
.where(user: current_user)
.ransack(params[:q])
@request_for_comments = @search.result
.order('created_at DESC')
.paginate(page: params[:page])
.order('created_at DESC')
.paginate(page: params[:page])
authorize!
render 'index'
end
@ -45,13 +46,13 @@ class RequestForCommentsController < ApplicationController
# GET /my_rfc_activity
def get_rfcs_with_my_comments
@search = RequestForComment
.with_last_activity
.joins(:comments) # we don't need to outer join here, because we know the user has commented on these
.where(comments: {user_id: current_user.id})
.ransack(params[:q])
.with_last_activity
.joins(:comments) # we don't need to outer join here, because we know the user has commented on these
.where(comments: {user_id: current_user.id})
.ransack(params[:q])
@request_for_comments = @search.result
.order('last_comment DESC')
.paginate(page: params[:page])
.order('last_comment DESC')
.paginate(page: params[:page])
authorize!
render 'index'
end
@ -60,13 +61,13 @@ class RequestForCommentsController < ApplicationController
def get_rfcs_for_exercise
exercise = Exercise.find(params[:exercise_id])
@search = RequestForComment
.with_last_activity
.where(exercise_id: exercise.id)
.ransack(params[:q])
.with_last_activity
.where(exercise_id: exercise.id)
.ransack(params[:q])
@request_for_comments = @search.result
.joins(:exercise)
.order('last_comment DESC')
.paginate(page: params[:page])
.joins(:exercise)
.order('last_comment DESC')
.paginate(page: params[:page])
# let the exercise decide, whether its rfcs should be visible
authorize(exercise)
render 'index'
@ -91,7 +92,7 @@ class RequestForCommentsController < ApplicationController
@request_for_comment.thank_you_note = params[:note]
commenters = @request_for_comment.commenters
commenters.each { |commenter| UserMailer.send_thank_you_note(@request_for_comment, commenter).deliver_now }
commenters.each {|commenter| UserMailer.send_thank_you_note(@request_for_comment, commenter).deliver_now }
respond_to do |format|
if @request_for_comment.save
@ -143,14 +144,16 @@ class RequestForCommentsController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through.
def request_for_comment_params
# The study_group_id might not be present in the session (e.g. for internal users), resulting in session[:study_group_id] = nil which is intended.
params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(user_id: current_user.id, user_type: current_user.class.name)
params.require(:request_for_comment).permit(:exercise_id, :file_id, :question, :requested_at, :solved, :submission_id).merge(
user_id: current_user.id, user_type: current_user.class.name
)
end
# The index page requires the grouping of the study groups
# The study groups are grouped by the current study group and other study groups of the user
def set_study_group_grouping
current_study_group = StudyGroup.find_by(id: session[:study_group_id])
my_study_groups = current_user.study_groups.reject { |group| group == current_study_group }
my_study_groups = current_user.study_groups.reject {|group| group == current_study_group }
@study_groups_grouping = [[t('request_for_comments.index.study_groups.current'), Array(current_study_group)],
[t('request_for_comments.index.study_groups.my'), my_study_groups]]
end

View File

@ -1,7 +1,10 @@
# frozen_string_literal: true
class SessionsController < ApplicationController
include Lti
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_unique_oauth_nonce, :set_current_user, :require_valid_exercise_token, :set_study_group_membership, :set_embedding_options].each do |method_name|
%i[require_oauth_parameters require_valid_consumer_key require_valid_oauth_signature require_unique_oauth_nonce
set_current_user require_valid_exercise_token set_study_group_membership set_embedding_options].each do |method_name|
before_action(method_name, only: :create_through_lti)
end
@ -24,8 +27,8 @@ class SessionsController < ApplicationController
redirect_to(params[:custom_redirect_target])
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",
consumer: @consumer))
notice: t("sessions.create_through_lti.session_#{lti_outcome_service?(@exercise.id, @current_user.id) ? 'with' : 'without'}_outcome",
consumer: @consumer))
end
end

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
class StatisticsController < ApplicationController
include StatisticsHelper
before_action :authorize!, only: [:show, :graphs, :user_activity, :user_activity_history, :rfc_activity,
:rfc_activity_history]
before_action :authorize!, only: %i[show graphs user_activity user_activity_history rfc_activity
rfc_activity_history]
def policy_class
StatisticsPolicy
@ -15,8 +17,7 @@ class StatisticsController < ApplicationController
end
end
def graphs
end
def graphs; end
def user_activity
respond_to do |format|
@ -27,7 +28,7 @@ class StatisticsController < ApplicationController
def user_activity_history
respond_to do |format|
format.html { render('activity_history', locals: {resource: :user}) }
format.json { render_ranged_data :ranged_user_data}
format.json { render_ranged_data :ranged_user_data }
end
end
@ -46,14 +47,21 @@ class StatisticsController < ApplicationController
def render_ranged_data(data_source)
interval = params[:interval].to_s.empty? ? 'year' : params[:interval]
from = DateTime.strptime(params[:from], '%Y-%m-%d') rescue DateTime.new(0)
to = DateTime.strptime(params[:to], '%Y-%m-%d') rescue DateTime.now
render(json: self.send(data_source, interval, from, to))
from = begin
DateTime.strptime(params[:from], '%Y-%m-%d')
rescue StandardError
DateTime.new(0)
end
to = begin
DateTime.strptime(params[:to], '%Y-%m-%d')
rescue StandardError
DateTime.now
end
render(json: send(data_source, interval, from, to))
end
def authorize!
authorize self
end
private :authorize!
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class StudyGroupsController < ApplicationController
include CommonBehavior
@ -20,7 +22,8 @@ class StudyGroupsController < ApplicationController
def update
myparams = study_group_params
myparams[:external_users] = StudyGroupMembership.find(myparams[:study_group_membership_ids].reject(&:empty?)).map(&:user)
myparams[:external_users] =
StudyGroupMembership.find(myparams[:study_group_membership_ids].reject(&:empty?)).map(&:user)
myparams.delete(:study_group_membership_ids)
update_and_respond(object: @study_group, params: myparams)
end
@ -30,7 +33,7 @@ class StudyGroupsController < ApplicationController
end
def study_group_params
params[:study_group].permit(:id, :name, :study_group_membership_ids => []) if params[:study_group].present?
params[:study_group].permit(:id, :name, study_group_membership_ids: []) if params[:study_group].present?
end
private :study_group_params
@ -44,5 +47,4 @@ class StudyGroupsController < ApplicationController
authorize(@study_groups || @study_group)
end
private :authorize!
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class SubmissionsController < ApplicationController
include ActionController::Live
include CommonBehavior
@ -6,15 +8,16 @@ class SubmissionsController < ApplicationController
include SubmissionScoring
include Tubesock::Hijack
before_action :set_submission, only: [:download, :download_file, :render_file, :run, :score, :extract_errors, :show, :statistics, :stop, :test]
before_action :set_docker_client, only: [:run, :test]
before_action :set_files, only: [:download, :download_file, :render_file, :show, :run]
before_action :set_file, only: [:download_file, :render_file, :run]
before_action :set_mime_type, only: [:download_file, :render_file]
skip_before_action :verify_authenticity_token, only: [:download_file, :render_file]
before_action :set_submission,
only: %i[download download_file render_file run score extract_errors show statistics stop test]
before_action :set_docker_client, only: %i[run test]
before_action :set_files, only: %i[download download_file render_file show run]
before_action :set_file, only: %i[download_file render_file run]
before_action :set_mime_type, only: %i[download_file render_file]
skip_before_action :verify_authenticity_token, only: %i[download_file render_file]
def max_run_output_buffer_size
if(@submission.cause == 'requestComments')
if @submission.cause == 'requestComments'
5000
else
500
@ -34,30 +37,30 @@ class SubmissionsController < ApplicationController
end
def command_substitutions(filename)
{class_name: File.basename(filename, File.extname(filename)).camelize, filename: filename, module_name: File.basename(filename, File.extname(filename)).underscore}
{class_name: File.basename(filename, File.extname(filename)).camelize, filename: filename,
module_name: File.basename(filename, File.extname(filename)).underscore}
end
private :command_substitutions
def copy_comments
# copy each annotation and set the target_file.id
unless(params[:annotations_arr].nil?)
params[:annotations_arr].each do | annotation |
#comment = Comment.new(annotation[1].permit(:user_id, :file_id, :user_type, :row, :column, :text, :created_at, :updated_at))
comment = Comment.new(:user_id => annotation[1][:user_id], :file_id => annotation[1][:file_id], :user_type => current_user.class.name, :row => annotation[1][:row], :column => annotation[1][:column], :text => annotation[1][:text])
source_file = CodeOcean::File.find(annotation[1][:file_id])
params[:annotations_arr]&.each do |annotation|
# comment = Comment.new(annotation[1].permit(:user_id, :file_id, :user_type, :row, :column, :text, :created_at, :updated_at))
comment = Comment.new(user_id: annotation[1][:user_id], file_id: annotation[1][:file_id],
user_type: current_user.class.name, row: annotation[1][:row], column: annotation[1][:column], text: annotation[1][:text])
source_file = CodeOcean::File.find(annotation[1][:file_id])
# retrieve target file
target_file = @submission.files.detect do |file|
# file_id has to be that of a the former iteration OR of the initial file (if this is the first run)
file.file_id == source_file.file_id || file.file_id == source_file.id #seems to be needed here: (check this): || file.file_id == source_file.id ; yes this is needed, for comments on templates as well as comments on files added by users.
end
#save to assign an id
target_file.save!
comment.file_id = target_file.id
comment.save!
# retrieve target file
target_file = @submission.files.detect do |file|
# file_id has to be that of a the former iteration OR of the initial file (if this is the first run)
file.file_id == source_file.file_id || file.file_id == source_file.id # seems to be needed here: (check this): || file.file_id == source_file.id ; yes this is needed, for comments on templates as well as comments on files added by users.
end
# save to assign an id
target_file.save!
comment.file_id = target_file.id
comment.save!
end
end
@ -75,30 +78,35 @@ class SubmissionsController < ApplicationController
require 'zip'
stringio = Zip::OutputStream.write_buffer do |zio|
@files.each do |file|
zio.put_next_entry(file.path.to_s == '' ? file.name_with_extension : File.join(file.path, file.name_with_extension))
zio.write(file.content.present? ? file.content : file.native_file.read)
zio.put_next_entry(if file.path.to_s == ''
file.name_with_extension
else
File.join(file.path,
file.name_with_extension)
end)
zio.write(file.content.presence || file.native_file.read)
end
# zip exercise description
zio.put_next_entry(t('activerecord.models.exercise.one') + '.txt')
zio.write(@submission.exercise.title + "\r\n======================\r\n")
zio.put_next_entry("#{t('activerecord.models.exercise.one')}.txt")
zio.write("#{@submission.exercise.title}\r\n======================\r\n")
zio.write(@submission.exercise.description)
# zip .co file
zio.put_next_entry(".co")
zio.write(File.read id_file)
zio.put_next_entry('.co')
zio.write(File.read(id_file))
File.delete(id_file) if File.exist?(id_file)
# zip client scripts
scripts_path = 'app/assets/remote_scripts'
Dir.foreach(scripts_path) do |file|
next if file == '.' or file == '..'
zio.put_next_entry(File.join('.scripts', File.basename(file)))
zio.write(File.read File.join(scripts_path, file))
end
next if (file == '.') || (file == '..')
zio.put_next_entry(File.join('.scripts', File.basename(file)))
zio.write(File.read(File.join(scripts_path, file)))
end
end
send_data(stringio.string, filename: @submission.exercise.title.tr(" ", "_") + ".zip")
send_data(stringio.string, filename: "#{@submission.exercise.title.tr(' ', '_')}.zip")
end
def download_file
@ -128,7 +136,7 @@ class SubmissionsController < ApplicationController
end
def run
# TODO reimplement SSEs with websocket commands
# TODO: reimplement SSEs with websocket commands
# with_server_sent_events do |server_sent_event|
# output = @docker_client.execute_run_command(@submission, sanitize_filename)
@ -155,56 +163,54 @@ class SubmissionsController < ApplicationController
end
end
# socket is the socket into the container, tubesock is the socket to the client
# give the docker_client the tubesock object, so that it can send messages (timeout)
@docker_client.tubesock = tubesock
container_request_time = Time.now
container_request_time = Time.zone.now
result = @docker_client.execute_run_command(@submission, sanitize_filename)
tubesock.send_data JSON.dump({'cmd' => 'status', 'status' => result[:status]})
@waiting_for_container_time = Time.now - container_request_time
@waiting_for_container_time = Time.zone.now - container_request_time
if result[:status] == :container_running
socket = result[:socket]
command = result[:command]
socket.on :message do |event|
Rails.logger.info( Time.now.getutc.to_s + ": Docker sending: " + event.data)
Rails.logger.info("#{Time.zone.now.getutc}: Docker sending: #{event.data}")
handle_message(event.data, tubesock, result[:container])
end
socket.on :close do |event|
socket.on :close do |_event|
kill_socket(tubesock)
end
tubesock.onmessage do |data|
Rails.logger.info(Time.now.getutc.to_s + ": Client sending: " + data)
Rails.logger.info("#{Time.zone.now.getutc}: Client sending: #{data}")
# Check whether the client send a JSON command and kill container
# if the command is 'client_kill', send it to docker otherwise.
begin
parsed = JSON.parse(data) unless data == "\n"
if parsed.class == Hash && parsed['cmd'] == 'client_kill'
Rails.logger.debug("Client exited container.")
if parsed.instance_of?(Hash) && parsed['cmd'] == 'client_kill'
Rails.logger.debug('Client exited container.')
@docker_client.kill_container(result[:container])
else
socket.send data
Rails.logger.debug('Sent the received client data to docker:' + data)
Rails.logger.debug("Sent the received client data to docker:#{data}")
end
rescue JSON::ParserError => error
rescue JSON::ParserError => e
socket.send data
Rails.logger.debug('Rescued parsing error, sent the received client data to docker:' + data)
Rails.logger.debug("Rescued parsing error, sent the received client data to docker:#{data}")
Sentry.set_extras(data: data)
end
end
# Send command after all listeners are attached.
# Newline required to flush
@execution_request_time = Time.now
socket.send command + "\n"
Rails.logger.info('Sent command: ' + command.to_s)
@execution_request_time = Time.zone.now
socket.send "#{command}\n"
Rails.logger.info("Sent command: #{command}")
else
kill_socket(tubesock)
end
@ -212,7 +218,7 @@ class SubmissionsController < ApplicationController
end
def kill_socket(tubesock)
@container_execution_time = Time.now - @execution_request_time unless @execution_request_time.blank?
@container_execution_time = Time.zone.now - @execution_request_time if @execution_request_time.present?
# search for errors and save them as StructuredError (for scoring runs see submission_scoring.rb)
errors = extract_errors
send_hints(tubesock, errors)
@ -225,7 +231,7 @@ class SubmissionsController < ApplicationController
if @run_output.blank? || @run_output&.strip == '{"cmd":"exit"}' || @run_output&.strip == 'timeout:'
@raw_output ||= ''
@run_output ||= ''
parse_message t('exercises.implement.no_output', timestamp: l(Time.now, format: :short)), 'stdout', tubesock
parse_message t('exercises.implement.no_output', timestamp: l(Time.zone.now, format: :short)), 'stdout', tubesock
end
# Hijacked connection needs to be notified correctly
@ -237,25 +243,26 @@ class SubmissionsController < ApplicationController
@raw_output ||= ''
@run_output ||= ''
# Handle special commands first
if /^#exit|{"cmd": "exit"}/.match(message)
# Just call exit_container on the docker_client.
# Do not call kill_socket for the websocket to the client here.
# @docker_client.exit_container closes the socket to the container,
# kill_socket is called in the "on close handler" of the websocket to the container
@docker_client.exit_container(container)
elsif /^#timeout/.match(message)
@run_output = 'timeout: ' + @run_output # add information that this run timed out to the buffer
else
# Filter out information about run_command, test_command, user or working directory
run_command = @submission.execution_environment.run_command % command_substitutions(sanitize_filename)
test_command = @submission.execution_environment.test_command % command_substitutions(sanitize_filename)
if test_command.blank?
# If no test command is set, use the run_command for the RegEx below. Otherwise, no output will be displayed!
test_command = run_command
end
unless /root@|:\/workspace|#{run_command}|#{test_command}|bash: cmd:canvasevent: command not found/.match(message)
parse_message(message, 'stdout', tubesock, container)
end
case message
when /^#exit|{"cmd": "exit"}/
# Just call exit_container on the docker_client.
# Do not call kill_socket for the websocket to the client here.
# @docker_client.exit_container closes the socket to the container,
# kill_socket is called in the "on close handler" of the websocket to the container
@docker_client.exit_container(container)
when /^#timeout/
@run_output = "timeout: #{@run_output}" # add information that this run timed out to the buffer
else
# Filter out information about run_command, test_command, user or working directory
run_command = @submission.execution_environment.run_command % command_substitutions(sanitize_filename)
test_command = @submission.execution_environment.test_command % command_substitutions(sanitize_filename)
if test_command.blank?
# If no test command is set, use the run_command for the RegEx below. Otherwise, no output will be displayed!
test_command = run_command
end
unless %r{root@|:/workspace|#{run_command}|#{test_command}|bash: cmd:canvasevent: command not found}.match?(message)
parse_message(message, 'stdout', tubesock, container)
end
end
end
@ -263,58 +270,58 @@ class SubmissionsController < ApplicationController
parsed = ''
begin
parsed = JSON.parse(message)
if parsed.class == Hash and parsed.key?('cmd')
if parsed.instance_of?(Hash) && parsed.key?('cmd')
socket.send_data message
Rails.logger.info('parse_message sent: ' + message)
Rails.logger.info("parse_message sent: #{message}")
@docker_client.exit_container(container) if container && parsed['cmd'] == 'exit'
else
parsed = {'cmd'=>'write','stream'=>output_stream,'data'=>message}
parsed = {'cmd' => 'write', 'stream' => output_stream, 'data' => message}
socket.send_data JSON.dump(parsed)
Rails.logger.info('parse_message sent: ' + JSON.dump(parsed))
Rails.logger.info("parse_message sent: #{JSON.dump(parsed)}")
end
rescue JSON::ParserError => e
# Check wether the message contains multiple lines, if true try to parse each line
if recursive and message.include? "\n"
for part in message.split("\n")
self.parse_message(part,output_stream,socket, container, false)
if recursive && message.include?("\n")
message.split("\n").each do |part|
parse_message(part, output_stream, socket, container, false)
end
elsif message.include?('<img') || message.start_with?('{"cmd') || message.include?('"turtlebatch"')
#Rails.logger.info('img foung')
# Rails.logger.info('img foung')
@buffering = true
@buffer = ''
@buffer += message
#Rails.logger.info('Starting to buffer')
elsif @buffering and message.include?('/>')
# Rails.logger.info('Starting to buffer')
elsif @buffering && message.include?('/>')
@buffer += message
parsed = {'cmd'=>'write','stream'=>output_stream,'data'=>@buffer}
parsed = {'cmd' => 'write', 'stream' => output_stream, 'data' => @buffer}
socket.send_data JSON.dump(parsed)
#socket.send_data @buffer
# socket.send_data @buffer
@buffering = false
#Rails.logger.info('Sent complete buffer')
elsif @buffering and message.end_with?("}\r")
# Rails.logger.info('Sent complete buffer')
elsif @buffering && message.end_with?("}\r")
@buffer += message
socket.send_data @buffer
@buffering = false
#Rails.logger.info('Sent complete buffer')
# Rails.logger.info('Sent complete buffer')
elsif @buffering
@buffer += message
#Rails.logger.info('Appending to buffer')
# Rails.logger.info('Appending to buffer')
else
#Rails.logger.info('else')
parsed = {'cmd'=>'write','stream'=>output_stream,'data'=>message}
# Rails.logger.info('else')
parsed = {'cmd' => 'write', 'stream' => output_stream, 'data' => message}
socket.send_data JSON.dump(parsed)
Rails.logger.info('parse_message sent: ' + JSON.dump(parsed))
Rails.logger.info("parse_message sent: #{JSON.dump(parsed)}")
end
ensure
@raw_output += parsed['data'].to_s if parsed.class == Hash and parsed.key? 'data'
@raw_output += parsed['data'].to_s if parsed.instance_of?(Hash) && parsed.key?('data')
# save the data that was send to the run_output if there is enough space left. this will be persisted as a testrun with cause "run"
@run_output += JSON.dump(parsed).to_s if @run_output.size <= max_run_output_buffer_size
end
end
def save_run_output
unless @run_output.blank?
@run_output = @run_output[(0..max_run_output_buffer_size-1)] # trim the string to max_message_buffer_size chars
if @run_output.present?
@run_output = @run_output[(0..max_run_output_buffer_size - 1)] # trim the string to max_message_buffer_size chars
Testrun.create(
file: @file,
cause: 'run',
@ -328,7 +335,7 @@ class SubmissionsController < ApplicationController
def extract_errors
results = []
unless @raw_output.blank?
if @raw_output.present?
@submission.exercise.execution_environment.error_templates.each do |template|
pattern = Regexp.new(template.signature).freeze
if pattern.match(@raw_output)
@ -361,7 +368,7 @@ class SubmissionsController < ApplicationController
tubesock.send_data JSON.dump(score_submission(@submission))
# To enable hints when scoring a submission, uncomment the next line:
#send_hints(tubesock, StructuredError.where(submission: @submission))
# send_hints(tubesock, StructuredError.where(submission: @submission))
tubesock.send_data JSON.dump({'cmd' => 'exit'})
ensure
@ -372,8 +379,9 @@ class SubmissionsController < ApplicationController
def send_hints(tubesock, errors)
return if @embed_options[:disable_hints]
errors = errors.to_a.uniq { |e| e.hint}
errors.each do | error |
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})
end
end
@ -384,7 +392,7 @@ class SubmissionsController < ApplicationController
private :set_docker_client
def set_file
@file = @files.detect { |file| file.name_with_extension == sanitize_filename }
@file = @files.detect {|file| file.name_with_extension == sanitize_filename }
head :not_found unless @file
end
private :set_file
@ -406,11 +414,9 @@ class SubmissionsController < ApplicationController
end
private :set_submission
def show
end
def show; end
def statistics
end
def statistics; end
def test
hijack do |tubesock|
@ -436,10 +442,10 @@ class SubmissionsController < ApplicationController
server_sent_event.write(nil, event: 'start')
yield(server_sent_event) if block_given?
server_sent_event.write({code: 200}, event: 'close')
rescue => exception
Sentry.capture_exception(exception)
logger.error(exception.message)
logger.error(exception.backtrace.join("\n"))
rescue StandardError => e
Sentry.capture_exception(e)
logger.error(e.message)
logger.error(e.backtrace.join("\n"))
server_sent_event.write({code: 500}, event: 'close')
ensure
server_sent_event.close
@ -457,16 +463,16 @@ class SubmissionsController < ApplicationController
)
# create .co file
path = "tmp/" + user.id.to_s + ".co"
path = "tmp/#{user.id}.co"
# parse validation token
content = "#{remote_evaluation_mapping.validation_token}\n"
# parse remote request url
content += "#{request.base_url}/evaluate\n"
@submission.files.each do |file|
file_path = file.path.to_s == '' ? file.name_with_extension : File.join(file.path, file.name_with_extension)
content += "#{file_path}=#{file.file_id.to_s}\n"
content += "#{file_path}=#{file.file_id}\n"
end
File.open(path, "w+") do |f|
File.open(path, 'w+') do |f|
f.write(content)
end
path

View File

@ -1,5 +1,6 @@
class SubscriptionsController < ApplicationController
# frozen_string_literal: true
class SubscriptionsController < ApplicationController
def authorize!
authorize(@subscription || @subscriptions)
end
@ -21,28 +22,26 @@ class SubscriptionsController < ApplicationController
# DELETE /subscriptions/1
# DELETE /subscriptions/1.json
def destroy
begin
@subscription = Subscription.find(params[:id])
rescue
skip_authorization
@subscription = Subscription.find(params[:id])
rescue StandardError
skip_authorization
respond_to do |format|
format.html { redirect_to request_for_comments_url, alert: t('subscriptions.subscription_not_existent') }
format.json { render json: {message: t('subscriptions.subscription_not_existent')}, status: :not_found }
end
else
authorize!
rfc = @subscription.try(:request_for_comment)
@subscription.deleted = true
if @subscription.save
respond_to do |format|
format.html { redirect_to request_for_comments_url, alert: t('subscriptions.subscription_not_existent') }
format.json { render json: {message: t('subscriptions.subscription_not_existent')}, status: :not_found }
format.html { redirect_to request_for_comment_url(rfc), notice: t('subscriptions.successfully_unsubscribed') }
format.json { render json: {message: t('subscriptions.successfully_unsubscribed')}, status: :ok }
end
else
authorize!
rfc = @subscription.try(:request_for_comment)
@subscription.deleted = true
if @subscription.save
respond_to do |format|
format.html { redirect_to request_for_comment_url(rfc), notice: t('subscriptions.successfully_unsubscribed') }
format.json { render json: {message: t('subscriptions.successfully_unsubscribed')}, status: :ok}
end
else
respond_to do |format|
format.html { redirect_to request_for_comment_url(rfc), :flash => { :danger => t('shared.message_failure') } }
format.json { render json: {message: t('shared.message_failure')}, status: :internal_server_error}
end
respond_to do |format|
format.html { redirect_to request_for_comment_url(rfc), flash: {danger: t('shared.message_failure')} }
format.json { render json: {message: t('shared.message_failure')}, status: :internal_server_error }
end
end
end
@ -56,7 +55,10 @@ class SubscriptionsController < ApplicationController
def subscription_params
current_user_id = current_user.try(:id)
current_user_class_name = current_user.try(:class).try(:name)
params[:subscription].permit(:request_for_comment_id, :subscription_type).merge(user_id: current_user_id, user_type: current_user_class_name, deleted: false) if params[:subscription].present?
if params[:subscription].present?
params[:subscription].permit(:request_for_comment_id, :subscription_type).merge(user_id: current_user_id,
user_type: current_user_class_name, deleted: false)
end
end
private :subscription_params
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class TagsController < ApplicationController
include CommonBehavior
@ -18,8 +20,7 @@ class TagsController < ApplicationController
destroy_and_respond(object: @tag)
end
def edit
end
def edit; end
def tag_params
params[:tag].permit(:name) if params[:tag].present?
@ -42,8 +43,7 @@ class TagsController < ApplicationController
end
private :set_tag
def show
end
def show; end
def update
update_and_respond(object: @tag, params: tag_params)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class TipsController < ApplicationController
include CommonBehavior
@ -19,15 +21,14 @@ class TipsController < ApplicationController
destroy_and_respond(object: @tip)
end
def edit
end
def edit; end
def tip_params
return unless params[:tip].present?
return if params[:tip].blank?
params[:tip]
.permit(:title, :description, :example, :file_type_id)
.each { |_key, value| value.strip! unless value.is_a?(Array) }
.each {|_key, value| value.strip! unless value.is_a?(Array) }
.merge(user_id: current_user.id, user_type: current_user.class.name)
end
private :tip_params
@ -48,8 +49,7 @@ class TipsController < ApplicationController
end
private :set_tip
def show
end
def show; end
def update
update_and_respond(object: @tip, params: tip_params)

View File

@ -28,10 +28,10 @@ class UserExerciseFeedbacksController < ApplicationController
@exercise = Exercise.find(uef_params[:exercise_id])
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first
submission = begin
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
rescue StandardError
nil
end
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
rescue StandardError
nil
end
if @exercise
@uef = UserExerciseFeedback.find_or_initialize_by(user: current_user, exercise: @exercise)
@ -74,10 +74,10 @@ class UserExerciseFeedbacksController < ApplicationController
def update
submission = begin
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
rescue StandardError
nil
end
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
rescue StandardError
nil
end
rfc = RequestForComment.unsolved.where(exercise_id: @exercise.id, user_id: current_user.id).first
authorize!
if @exercise && validate_inputs(uef_params)
@ -115,7 +115,7 @@ class UserExerciseFeedbacksController < ApplicationController
end
def uef_params
return unless params[:user_exercise_feedback].present?
return if params[:user_exercise_feedback].blank?
exercise_id = if params[:user_exercise_feedback].nil?
params[:exercise_id]
@ -126,8 +126,8 @@ class UserExerciseFeedbacksController < ApplicationController
user_id = current_user.id
user_type = current_user.class.name
latest_submission = Submission
.where(user_id: user_id, user_type: user_type, exercise_id: exercise_id)
.order(created_at: :desc).first
.where(user_id: user_id, user_type: user_type, exercise_id: exercise_id)
.order(created_at: :desc).first
params[:user_exercise_feedback]
.permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime)
@ -140,10 +140,8 @@ class UserExerciseFeedbacksController < ApplicationController
def validate_inputs(uef_params)
if uef_params[:difficulty].to_i.negative? || uef_params[:difficulty].to_i >= comment_presets.size
false
elsif uef_params[:user_estimated_worktime].to_i.negative? || uef_params[:user_estimated_worktime].to_i >= time_presets.size
false
else
true
!(uef_params[:user_estimated_worktime].to_i.negative? || uef_params[:user_estimated_worktime].to_i >= time_presets.size)
end
rescue StandardError
false

View File

@ -1,15 +1,17 @@
# frozen_string_literal: true
module ActionCableHelper
def trigger_rfc_action_cable
Thread.new do
# Context: RfC
if submission.study_group_id.present?
ActionCable.server.broadcast(
"la_exercises_#{exercise_id}_channel_study_group_#{submission.study_group_id}",
type: :rfc,
id: id,
html: ApplicationController.render(partial: 'request_for_comments/list_entry',
locals: {request_for_comment: self}))
"la_exercises_#{exercise_id}_channel_study_group_#{submission.study_group_id}",
type: :rfc,
id: id,
html: ApplicationController.render(partial: 'request_for_comments/list_entry',
locals: {request_for_comment: self})
)
end
ensure
ActiveRecord::Base.connection_pool.release_connection
@ -26,9 +28,10 @@ module ActionCableHelper
# Context: Submission
if study_group_id.present?
ActionCable.server.broadcast(
"la_exercises_#{exercise_id}_channel_study_group_#{study_group_id}",
type: :working_times,
working_time_data: exercise.get_working_times_for_study_group(study_group_id, user))
"la_exercises_#{exercise_id}_channel_study_group_#{study_group_id}",
type: :working_times,
working_time_data: exercise.get_working_times_for_study_group(study_group_id, user)
)
end
ensure
ActiveRecord::Base.connection_pool.release_connection

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Admin
module DashboardHelper
def dashboard_data

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module ApplicationHelper
APPLICATION_NAME = 'CodeOcean'
@ -7,8 +9,8 @@ module ApplicationHelper
def code_tag(code, language = nil)
if code.present?
content_tag(:pre) do
content_tag(:code, code, class: "language-#{language}")
tag.pre do
tag.code(code, class: "language-#{language}")
end
else
empty
@ -16,12 +18,12 @@ module ApplicationHelper
end
def empty
content_tag(:i, nil, class: 'empty fa fa-minus')
tag.i(nil, class: 'empty fa fa-minus')
end
def label_column(label)
content_tag(:div, class: 'col-sm-3') do
content_tag(:strong) do
tag.div(class: 'col-sm-3') do
tag.strong do
I18n.translation_present?("activerecord.attributes.#{label}") ? t("activerecord.attributes.#{label}") : t(label)
end
end
@ -29,12 +31,13 @@ module ApplicationHelper
private :label_column
def no
content_tag(:i, nil, class: 'fa fa-times')
tag.i(nil, class: 'fa fa-times')
end
def progress_bar(value)
content_tag(:div, class: value ? 'progress' : 'disabled progress') do
content_tag(:div, value ? "#{value}%" : '', :'aria-valuemax' => 100, :'aria-valuemin' => 0, :'aria-valuenow' => value, class: 'progress-bar progress-bar-striped', role: 'progressbar', style: "width: #{[value || 0, 100].min}%;")
tag.div(class: value ? 'progress' : 'disabled progress') do
tag.div(value ? "#{value}%" : '', 'aria-valuemax': 100, 'aria-valuemin': 0,
'aria-valuenow': value, class: 'progress-bar progress-bar-striped', role: 'progressbar', style: "width: #{[value || 0, 100].min}%;")
end
end
@ -43,7 +46,7 @@ module ApplicationHelper
end
def row(options = {}, &block)
content_tag(:div, class: 'attribute-row row') do
tag.div(class: 'attribute-row row') do
label_column(options[:label]) + value_column(options[:value], &block)
end
end
@ -61,13 +64,13 @@ module ApplicationHelper
end
def value_column(value)
content_tag(:div, class: 'col-sm-9') do
tag.div(class: 'col-sm-9') do
block_given? ? yield : symbol_for(value)
end
end
private :value_column
def yes
content_tag(:i, nil, class: 'fa fa-check')
tag.i(nil, class: 'fa fa-check')
end
end

View File

@ -1,2 +1,4 @@
# frozen_string_literal: true
module ErrorTemplateAttributesHelper
end

View File

@ -1,2 +1,4 @@
# frozen_string_literal: true
module ErrorTemplatesHelper
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module ExerciseHelper
include LtiHelper
@ -6,7 +8,7 @@ module ExerciseHelper
end
def qa_js_tag
javascript_include_tag qa_url + "/assets/qa_api.js"
javascript_include_tag "#{qa_url}/assets/qa_api.js"
end
def qa_url
@ -15,8 +17,6 @@ module ExerciseHelper
if enabled
config.read[:code_pilot][:url]
else
return nil
end
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'oauth/request_proxy/action_controller_request' # Rails 5 changed `Rack::Request` to `ActionDispatch::Request`
module LtiHelper

View File

@ -16,20 +16,19 @@ class PagedownFormBuilder < ActionView::Helpers::FormBuilder
private
def wmd_button_bar
@template.content_tag :div, nil, id: "wmd-button-bar-#{base_id}"
@template.tag.div(nil, id: "wmd-button-bar-#{base_id}")
end
def wmd_textarea
@template.text_area @object_name, @attribute_name,
**@input_html_options,
class: 'form-control wmd-input',
id: "wmd-input-#{base_id}"
**@input_html_options,
class: 'form-control wmd-input',
id: "wmd-input-#{base_id}"
end
def wmd_preview
@template.content_tag :div, nil,
class: 'wmd-preview',
id: "wmd-preview-#{base_id}"
@template.tag.div(nil, class: 'wmd-preview',
id: "wmd-preview-#{base_id}")
end
def show_wmd_preview?

View File

@ -1,226 +1,231 @@
module StatisticsHelper
# frozen_string_literal: true
module StatisticsHelper
WORKING_TIME_DELTA_IN_SECONDS = 5.minutes
WORKING_TIME_DELTA_IN_SQL_INTERVAL = "'0:05:00'" # yes, a string with quotes
def statistics_data
[
{
key: 'users',
name: t('statistics.sections.users'),
entries: user_statistics
},
{
key: 'exercises',
name: t('statistics.sections.exercises'),
entries: exercise_statistics
},
{
key: 'request_for_comments',
name: t('statistics.sections.request_for_comments'),
entries: rfc_statistics
}
{
key: 'users',
name: t('statistics.sections.users'),
entries: user_statistics,
},
{
key: 'exercises',
name: t('statistics.sections.exercises'),
entries: exercise_statistics,
},
{
key: 'request_for_comments',
name: t('statistics.sections.request_for_comments'),
entries: rfc_statistics,
},
]
end
def user_statistics
[
{
key: 'internal_users',
name: t('activerecord.models.internal_user.other'),
data: InternalUser.count,
url: internal_users_path
},
{
key: 'external_users',
name: t('activerecord.models.external_user.other'),
data: ExternalUser.count,
url: external_users_path
},
{
key: 'currently_active',
name: t('statistics.entries.users.currently_active'),
data: ExternalUser.joins(:submissions)
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
.distinct('external_users.id').count,
url: 'statistics/graphs'
}
{
key: 'internal_users',
name: t('activerecord.models.internal_user.other'),
data: InternalUser.count,
url: internal_users_path,
},
{
key: 'external_users',
name: t('activerecord.models.external_user.other'),
data: ExternalUser.count,
url: external_users_path,
},
{
key: 'currently_active',
name: t('statistics.entries.users.currently_active'),
data: ExternalUser.joins(:submissions)
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
.distinct('external_users.id').count,
url: 'statistics/graphs',
},
]
end
def exercise_statistics
[
{
key: 'exercises',
name: t('activerecord.models.exercise.other'),
data: Exercise.count,
url: exercises_path
},
{
key: 'average_submissions',
name: t('statistics.entries.exercises.average_number_of_submissions'),
data: (Submission.count.to_f / Exercise.count).round(2)
},
{
key: 'submissions_per_minute',
name: t('statistics.entries.exercises.submissions_per_minute'),
data: (Submission.where('created_at >= ?', DateTime.now - 1.hours).count.to_f / 60).round(2),
unit: '/min',
url: statistics_graphs_path
},
{
key: 'autosaves_per_minute',
name: t('statistics.entries.exercises.autosaves_per_minute'),
data: (Submission.where('created_at >= ?', DateTime.now - 1.hours).where(cause: 'autosave').count.to_f / 60).round(2),
unit: '/min'
},
{
key: 'container_requests_per_minute',
name: t('statistics.entries.exercises.container_requests_per_minute'),
data: (Testrun.where('created_at >= ?', DateTime.now - 1.hours).count.to_f / 60).round(2),
unit: '/min'
},
{
key: 'execution_environments',
name: t('activerecord.models.execution_environment.other'),
data: ExecutionEnvironment.count,
url: execution_environments_path
},
{
key: 'exercise_collections',
name: t('activerecord.models.exercise_collection.other'),
data: ExerciseCollection.count,
url: exercise_collections_path
}
{
key: 'exercises',
name: t('activerecord.models.exercise.other'),
data: Exercise.count,
url: exercises_path,
},
{
key: 'average_submissions',
name: t('statistics.entries.exercises.average_number_of_submissions'),
data: (Submission.count.to_f / Exercise.count).round(2),
},
{
key: 'submissions_per_minute',
name: t('statistics.entries.exercises.submissions_per_minute'),
data: (Submission.where('created_at >= ?', DateTime.now - 1.hour).count.to_f / 60).round(2),
unit: '/min',
url: statistics_graphs_path,
},
{
key: 'autosaves_per_minute',
name: t('statistics.entries.exercises.autosaves_per_minute'),
data: (Submission.where('created_at >= ?',
DateTime.now - 1.hour).where(cause: 'autosave').count.to_f / 60).round(2),
unit: '/min',
},
{
key: 'container_requests_per_minute',
name: t('statistics.entries.exercises.container_requests_per_minute'),
data: (Testrun.where('created_at >= ?', DateTime.now - 1.hour).count.to_f / 60).round(2),
unit: '/min',
},
{
key: 'execution_environments',
name: t('activerecord.models.execution_environment.other'),
data: ExecutionEnvironment.count,
url: execution_environments_path,
},
{
key: 'exercise_collections',
name: t('activerecord.models.exercise_collection.other'),
data: ExerciseCollection.count,
url: exercise_collections_path,
},
]
end
def rfc_statistics
rfc_activity_data + [
{
key: 'comments',
name: t('activerecord.models.comment.other'),
data: Comment.count
}
{
key: 'comments',
name: t('activerecord.models.comment.other'),
data: Comment.count,
},
]
end
def user_activity_live_data
[
{
key: 'active_in_last_hour',
name: t('statistics.entries.users.currently_active'),
data: ExternalUser.joins(:submissions)
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
.distinct('external_users.id').count,
},
{
key: 'submissions_per_minute',
name: t('statistics.entries.exercises.submissions_per_minute'),
data: (Submission.where('created_at >= ?', DateTime.now - 1.hours).count.to_f / 60).round(2),
unit: '/min',
axis: 'right'
}
{
key: 'active_in_last_hour',
name: t('statistics.entries.users.currently_active'),
data: ExternalUser.joins(:submissions)
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
.distinct('external_users.id').count,
},
{
key: 'submissions_per_minute',
name: t('statistics.entries.exercises.submissions_per_minute'),
data: (Submission.where('created_at >= ?', DateTime.now - 1.hour).count.to_f / 60).round(2),
unit: '/min',
axis: 'right',
},
]
end
def rfc_activity_data(from=DateTime.new(0), to=DateTime.now)
def rfc_activity_data(from = DateTime.new(0), to = DateTime.now)
[
{
key: 'rfcs',
name: t('activerecord.models.request_for_comment.other'),
data: RequestForComment.in_range(from, to).count,
url: request_for_comments_path
},
{
key: 'percent_solved',
name: t('statistics.entries.request_for_comments.percent_solved'),
data: (100.0 / RequestForComment.in_range(from, to).count * RequestForComment.in_range(from, to).where(solved: true).count).round(1),
unit: '%',
axis: 'right',
url: statistics_graphs_path
},
{
key: 'percent_soft_solved',
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
data: (100.0 / RequestForComment.in_range(from, to).count * RequestForComment.in_range(from, to).unsolved.where(full_score_reached: true).count).round(1),
unit: '%',
axis: 'right',
url: statistics_graphs_path
},
{
key: 'percent_unsolved',
name: t('statistics.entries.request_for_comments.percent_unsolved'),
data: (100.0 / RequestForComment.in_range(from, to).count * RequestForComment.in_range(from, to).unsolved.count).round(1),
unit: '%',
axis: 'right',
url: statistics_graphs_path
},
{
key: 'rfcs_with_comments',
name: t('statistics.entries.request_for_comments.with_comments'),
data: RequestForComment.in_range(from, to).joins('join "submissions" s on s.id = request_for_comments.submission_id
{
key: 'rfcs',
name: t('activerecord.models.request_for_comment.other'),
data: RequestForComment.in_range(from, to).count,
url: request_for_comments_path,
},
{
key: 'percent_solved',
name: t('statistics.entries.request_for_comments.percent_solved'),
data: (100.0 / RequestForComment.in_range(from,
to).count * RequestForComment.in_range(from, to).where(solved: true).count).round(1),
unit: '%',
axis: 'right',
url: statistics_graphs_path,
},
{
key: 'percent_soft_solved',
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
data: (100.0 / RequestForComment.in_range(from,
to).count * RequestForComment.in_range(from, to).unsolved.where(full_score_reached: true).count).round(1),
unit: '%',
axis: 'right',
url: statistics_graphs_path,
},
{
key: 'percent_unsolved',
name: t('statistics.entries.request_for_comments.percent_unsolved'),
data: (100.0 / RequestForComment.in_range(from,
to).count * RequestForComment.in_range(from, to).unsolved.count).round(1),
unit: '%',
axis: 'right',
url: statistics_graphs_path,
},
{
key: 'rfcs_with_comments',
name: t('statistics.entries.request_for_comments.with_comments'),
data: RequestForComment.in_range(from,
to).joins('join "submissions" s on s.id = request_for_comments.submission_id
join "files" f on f.context_id = s.id and f.context_type = \'Submission\'
join "comments" c on c.file_id = f.id').group('request_for_comments.id').count.size,
url: statistics_graphs_path
}
url: statistics_graphs_path,
},
]
end
def ranged_rfc_data(interval='year', from=DateTime.new(0), to=DateTime.now)
def ranged_rfc_data(interval = 'year', from = DateTime.new(0), to = DateTime.now)
[
{
key: 'rfcs',
name: t('activerecord.models.request_for_comment.other'),
data: RequestForComment.in_range(from, to)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key')
},
{
key: 'rfcs_solved',
name: t('statistics.entries.request_for_comments.percent_solved'),
data: RequestForComment.in_range(from, to)
.where(solved: true)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key')
},
{
key: 'rfcs_soft_solved',
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
data: RequestForComment.in_range(from, to).unsolved
.where(full_score_reached: true)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key')
},
{
key: 'rfcs_unsolved',
name: t('statistics.entries.request_for_comments.percent_unsolved'),
data: RequestForComment.in_range(from, to).unsolved
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key')
}
{
key: 'rfcs',
name: t('activerecord.models.request_for_comment.other'),
data: RequestForComment.in_range(from, to)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key'),
},
{
key: 'rfcs_solved',
name: t('statistics.entries.request_for_comments.percent_solved'),
data: RequestForComment.in_range(from, to)
.where(solved: true)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key'),
},
{
key: 'rfcs_soft_solved',
name: t('statistics.entries.request_for_comments.percent_soft_solved'),
data: RequestForComment.in_range(from, to).unsolved
.where(full_score_reached: true)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key'),
},
{
key: 'rfcs_unsolved',
name: t('statistics.entries.request_for_comments.percent_unsolved'),
data: RequestForComment.in_range(from, to).unsolved
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key'),
},
]
end
def ranged_user_data(interval='year', from=DateTime.new(0), to=DateTime.now)
def ranged_user_data(interval = 'year', from = DateTime.new(0), to = DateTime.now)
[
{
key: 'active',
name: t('statistics.entries.users.active'),
data: ExternalUser.joins(:submissions)
.where(submissions: {created_at: from..to})
.select("date_trunc('#{interval}', submissions.created_at) AS \"key\", count(distinct external_users.id) AS \"value\"")
.group('key').order('key')
},
{
key: 'submissions',
name: t('statistics.entries.exercises.submissions'),
data: Submission.where(created_at: from..to)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key'),
axis: 'right'
}
{
key: 'active',
name: t('statistics.entries.users.active'),
data: ExternalUser.joins(:submissions)
.where(submissions: {created_at: from..to})
.select("date_trunc('#{interval}', submissions.created_at) AS \"key\", count(distinct external_users.id) AS \"value\"")
.group('key').order('key'),
},
{
key: 'submissions',
name: t('statistics.entries.exercises.submissions'),
data: Submission.where(created_at: from..to)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key'),
axis: 'right',
},
]
end
end

View File

@ -1,5 +1,6 @@
module TimeHelper
# frozen_string_literal: true
module TimeHelper
# convert timestamps ('12:34:56.789') to seconds
def time_to_f(timestamp)
unless timestamp.nil?
@ -11,7 +12,6 @@ module TimeHelper
# given a delta in seconds, return a "Hours:Minutes:Seconds" representation
def format_time_difference(delta)
Time.at(delta).utc.strftime("%H:%M:%S")
Time.at(delta).utc.strftime('%H:%M:%S')
end
end

View File

@ -1,8 +1,9 @@
class UserMailer < ActionMailer::Base
# frozen_string_literal: true
class UserMailer < ApplicationMailer
def mail(*args)
# used to prevent the delivery to pseudonymous users without a valid email address
super unless args.first[:to].blank?
super if args.first[:to].present?
end
def activation_needed_email(user)
@ -10,8 +11,7 @@ class UserMailer < ActionMailer::Base
mail(subject: t('mailers.user_mailer.activation_needed.subject'), to: user.email)
end
def activation_success_email(*)
end
def activation_success_email(*); end
def reset_password_email(user)
@reset_password_url = reset_password_internal_user_url(user, token: user.reset_password_token)
@ -19,12 +19,15 @@ class UserMailer < ActionMailer::Base
end
def got_new_comment(comment, request_for_comment, commenting_user)
# todo: check whether we can take the last known locale of the receiver?
# TODO: check whether we can take the last known locale of the receiver?
@receiver_displayname = request_for_comment.user.displayname
@commenting_user_displayname = commenting_user.displayname
@comment_text = comment.text
@rfc_link = request_for_comment_url(request_for_comment)
mail(subject: t('mailers.user_mailer.got_new_comment.subject', commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email)
mail(
subject: t('mailers.user_mailer.got_new_comment.subject',
commenting_user_displayname: @commenting_user_displayname), to: request_for_comment.user.email
)
end
def got_new_comment_for_subscription(comment, subscription, from_user)
@ -33,7 +36,10 @@ class UserMailer < ActionMailer::Base
@comment_text = comment.text
@rfc_link = request_for_comment_url(subscription.request_for_comment)
@unsubscribe_link = unsubscribe_subscription_url(subscription)
mail(subject: t('mailers.user_mailer.got_new_comment_for_subscription.subject', author_displayname: @author_displayname), to: subscription.user.email)
mail(
subject: t('mailers.user_mailer.got_new_comment_for_subscription.subject',
author_displayname: @author_displayname), to: subscription.user.email
)
end
def send_thank_you_note(request_for_comments, receiver)
@ -43,7 +49,7 @@ class UserMailer < ActionMailer::Base
@rfc_link = request_for_comment_url(request_for_comments)
mail(subject: t('mailers.user_mailer.send_thank_you_note.subject', author: @author), to: receiver.email)
end
def exercise_anomaly_detected(exercise_collection, anomalies)
@user = exercise_collection.user
@receiver_displayname = exercise_collection.user.displayname

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AnomalyNotification < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :exercise

View File

@ -1,16 +1,15 @@
require File.expand_path('../../../uploaders/file_uploader', __FILE__)
require File.expand_path('../../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
# frozen_string_literal: true
require File.expand_path('../../uploaders/file_uploader', __dir__)
require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __dir__)
module CodeOcean
class FileNameValidator < ActiveModel::Validator
def validate(record)
existing_files = File.where(name: record.name, path: record.path, file_type_id: record.file_type_id,
context_id: record.context_id, context_type: record.context_type).to_a
unless existing_files.empty?
if (not record.context.is_a?(Exercise)) || (record.context.new_record?)
record.errors[:base] << 'Duplicate'
end
if !existing_files.empty? && (!record.context.is_a?(Exercise) || record.context.new_record?)
record.errors[:base] << 'Duplicate'
end
end
end
@ -19,7 +18,8 @@ module CodeOcean
include DefaultValues
DEFAULT_WEIGHT = 1.0
ROLES = %w[regular_file main_file reference_implementation executable_file teacher_defined_test user_defined_file user_defined_test teacher_defined_linter].freeze
ROLES = %w[regular_file main_file reference_implementation executable_file teacher_defined_test user_defined_file
user_defined_test teacher_defined_linter].freeze
TEACHER_DEFINED_ROLES = ROLES - %w[user_defined_file]
after_initialize :set_default_values
@ -29,13 +29,13 @@ module CodeOcean
belongs_to :context, polymorphic: true
belongs_to :file, class_name: 'CodeOcean::File', optional: true # This is only required for submissions and is validated below
alias_method :ancestor, :file
alias ancestor file
belongs_to :file_type
has_many :files, class_name: 'CodeOcean::File'
has_many :testruns
has_many :comments
alias_method :descendants, :files
alias descendants files
mount_uploader :native_file, FileUploader
@ -61,7 +61,7 @@ module CodeOcean
validates :weight, absence: true, unless: :teacher_defined_assessment?
validates :file, presence: true if :context.is_a?(Submission)
validates_with FileNameValidator, fields: [:name, :path, :file_type_id]
validates_with FileNameValidator, fields: %i[name path file_type_id]
ROLES.each do |role|
define_method("#{role}?") { self.role == role }
@ -94,7 +94,12 @@ module CodeOcean
end
def hash_content
self.hashed_content = Digest::MD5.new.hexdigest(file_type.try(:binary?) ? ::File.new(native_file.file.path, 'r').read : content)
self.hashed_content = Digest::MD5.new.hexdigest(if file_type.try(:binary?)
::File.new(native_file.file.path,
'r').read
else
content
end)
end
private :hash_content
@ -108,7 +113,7 @@ module CodeOcean
end
def set_ancestor_values
[:feedback_message, :file_type_id, :hidden, :name, :path, :read_only, :role, :weight].each do |attribute|
%i[feedback_message file_type_id hidden name path read_only role weight].each do |attribute|
send(:"#{attribute}=", ancestor.send(attribute))
end
end

View File

@ -5,9 +5,7 @@ class CodeharborLink < ApplicationRecord
validates :check_uuid_url, presence: true
validates :api_key, presence: true
belongs_to :user, foreign_key: :user_id, class_name: 'InternalUser'
belongs_to :user, class_name: 'InternalUser'
def to_s
id.to_s
end
delegate :to_s, to: :id
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Context
extend ActiveSupport::Concern

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Creation
extend ActiveSupport::Concern

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module DefaultValues
def set_default_values_if_present(options = {})
options.each do |attribute, value|

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class Consumer < ApplicationRecord
has_many :users

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ErrorTemplate < ApplicationRecord
belongs_to :execution_environment
has_and_belongs_to_many :error_template_attributes

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ErrorTemplateAttribute < ApplicationRecord
has_and_belongs_to_many :error_template

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class Event < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :exercise

View File

@ -1,4 +1,6 @@
require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
# frozen_string_literal: true
require File.expand_path('../../lib/active_model/validations/boolean_presence_validator', __dir__)
class ExecutionEnvironment < ApplicationRecord
include Creation
@ -17,7 +19,8 @@ class ExecutionEnvironment < ApplicationRecord
validate :valid_test_setup?
validate :working_docker_image?, if: :validate_docker_image?
validates :docker_image, presence: true
validates :memory_limit, numericality: {greater_than_or_equal_to: DockerClient::MINIMUM_MEMORY_LIMIT, only_integer: true}, presence: true
validates :memory_limit,
numericality: {greater_than_or_equal_to: DockerClient::MINIMUM_MEMORY_LIMIT, only_integer: true}, presence: true
validates :network_enabled, boolean_presence: true
validates :name, presence: true
validates :permitted_execution_time, numericality: {only_integer: true}, presence: true
@ -35,7 +38,9 @@ class ExecutionEnvironment < ApplicationRecord
def valid_test_setup?
if test_command? ^ testing_framework?
errors.add(:test_command, I18n.t('activerecord.errors.messages.together', attribute: I18n.t('activerecord.attributes.execution_environment.testing_framework')))
errors.add(:test_command,
I18n.t('activerecord.errors.messages.together',
attribute: I18n.t('activerecord.attributes.execution_environment.testing_framework')))
end
end
private :valid_test_setup?
@ -46,11 +51,11 @@ class ExecutionEnvironment < ApplicationRecord
private :validate_docker_image?
def working_docker_image?
DockerClient.pull(docker_image) unless DockerClient.find_image_by_tag(docker_image).blank?
DockerClient.pull(docker_image) if DockerClient.find_image_by_tag(docker_image).present?
output = DockerClient.new(execution_environment: self).execute_arbitrary_command(VALIDATION_COMMAND)
errors.add(:docker_image, "error: #{output[:stderr]}") if output[:stderr].present?
rescue DockerClient::Error => error
errors.add(:docker_image, "error: #{error}")
rescue DockerClient::Error => e
errors.add(:docker_image, "error: #{e}")
end
private :working_docker_image?
end

View File

@ -41,7 +41,7 @@ class Exercise < ApplicationRecord
validates :unpublished, boolean_presence: true
validates :title, presence: true
validates :token, presence: true, uniqueness: true
validates_uniqueness_of :uuid, if: -> { uuid.present? }
validates :uuid, uniqueness: {if: -> { uuid.present? }}
@working_time_statistics = nil
attr_reader :working_time_statistics
@ -57,10 +57,10 @@ class Exercise < ApplicationRecord
end
def finishers_percentage
if users.distinct.count != 0
(100.0 / users.distinct.count * finishers.count).round(2)
else
if users.distinct.count.zero?
0
else
(100.0 / users.distinct.count * finishers.count).round(2)
end
end
@ -73,11 +73,11 @@ class Exercise < ApplicationRecord
def average_number_of_submissions
user_count = internal_users.distinct.count + external_users.distinct.count
user_count == 0 ? 0 : submissions.count / user_count.to_f
user_count.zero? ? 0 : submissions.count / user_count.to_f
end
def time_maximum_score(user)
submissions.where(user: user).where("cause IN ('submit','assess')").where('score IS NOT NULL').order('score DESC, created_at ASC').first.created_at
submissions.where(user: user).where("cause IN ('submit','assess')").where.not(score: nil).order('score DESC, created_at ASC').first.created_at
rescue StandardError
Time.zone.at(0)
end
@ -107,7 +107,7 @@ class Exercise < ApplicationRecord
end
def study_group_working_time_query(exercise_id, study_group_id, additional_filter)
''"
"
WITH working_time_between_submissions AS (
SELECT submissions.user_id,
submissions.user_type,
@ -200,7 +200,7 @@ class Exercise < ApplicationRecord
FROM working_times_with_index
JOIN internal_users ON user_type = 'InternalUser' AND user_id = internal_users.id
ORDER BY index, score ASC;
"''
"
end
def get_working_times_for_study_group(study_group_id, user = nil)
@ -217,7 +217,8 @@ class Exercise < ApplicationRecord
results = ActiveRecord::Base.transaction do
self.class.connection.execute("SET LOCAL intervalstyle = 'postgres'")
self.class.connection.execute(study_group_working_time_query(id, study_group_id, additional_filter)).each do |tuple|
self.class.connection.execute(study_group_working_time_query(id, study_group_id,
additional_filter)).each do |tuple|
bucket = if maximum_score > 0.0 && tuple['score'] <= maximum_score
(tuple['score'] / maximum_score * max_bucket).round
else
@ -230,11 +231,12 @@ class Exercise < ApplicationRecord
user_progress[bucket][tuple['index']] = tuple['working_time_per_score']
additional_user_data[bucket][tuple['index']] = {start_time: tuple['start_time'], score: tuple['score']}
additional_user_data[max_bucket + 1][tuple['index']] = {id: tuple['user_id'], type: tuple['user_type'], name: tuple['name']}
additional_user_data[max_bucket + 1][tuple['index']] =
{id: tuple['user_id'], type: tuple['user_type'], name: tuple['name']}
end
end
if results.ntuples > 0
if results.ntuples.positive?
first_index = results[0]['index']
last_index = results[results.ntuples - 1]['index']
buckets = last_index - first_index
@ -247,9 +249,9 @@ class Exercise < ApplicationRecord
end
def get_quantiles(quantiles)
quantiles_str = '[' + quantiles.join(',') + ']'
quantiles_str = "[#{quantiles.join(',')}]"
result = ActiveRecord::Base.transaction do
self.class.connection.execute(''"
self.class.connection.execute("
SET LOCAL intervalstyle = 'iso_8601';
WITH working_time AS
(
@ -356,14 +358,14 @@ class Exercise < ApplicationRecord
exercise_id )
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
FROM result
"'')
")
end
if result.count > 0
begin
quantiles.each_with_index.map { |_q, i| ActiveSupport::Duration.parse(result[i]['unnest']).to_f }
end
if result.count.positive?
quantiles.each_with_index.map {|_q, i| ActiveSupport::Duration.parse(result[i]['unnest']).to_f }
else
quantiles.map { |_q| 0 }
quantiles.map {|_q| 0 }
end
end
@ -380,11 +382,11 @@ class Exercise < ApplicationRecord
def average_working_time
ActiveRecord::Base.transaction do
self.class.connection.execute("SET LOCAL intervalstyle = 'postgres'")
self.class.connection.execute(''"
self.class.connection.execute("
SELECT avg(working_time) as average_time
FROM
(#{user_working_time_query}) AS baz;
"'').first['average_time']
").first['average_time']
end
end
@ -397,7 +399,7 @@ class Exercise < ApplicationRecord
user_type = user.external_user? ? 'ExternalUser' : 'InternalUser'
begin
result = ActiveRecord::Base.transaction do
self.class.connection.execute(''"
self.class.connection.execute("
SET LOCAL intervalstyle = 'iso_8601';
WITH WORKING_TIME AS
(SELECT user_id,
@ -447,7 +449,7 @@ class Exercise < ApplicationRecord
SELECT e.external_id AS external_user_id, f.user_id, exercise_id, MAX(max_score) AS max_score, sum(working_time_new) AS working_time
FROM FILTERED_TIMES_UNTIL_MAX f, EXTERNAL_USERS e
WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id
"'')
")
end
ActiveSupport::Duration.parse(result.first['working_time']).to_f
rescue StandardError
@ -458,8 +460,8 @@ class Exercise < ApplicationRecord
def duplicate(attributes = {})
exercise = dup
exercise.attributes = attributes
exercise_tags.each { |et| exercise.exercise_tags << et.dup }
files.each { |file| exercise.files << file.dup }
exercise_tags.each {|et| exercise.exercise_tags << et.dup }
files.each {|file| exercise.files << file.dup }
exercise
end
@ -490,7 +492,7 @@ class Exercise < ApplicationRecord
self.attributes = {
title: task_node.xpath('p:meta-data/p:title/text()')[0].content,
description: description,
instructions: description
instructions: description,
}
task_node.xpath('p:files/p:file').all? do |file|
file_name_split = file.xpath('@filename').first.value.split('.')
@ -498,16 +500,16 @@ class Exercise < ApplicationRecord
role = determine_file_role_from_proforma_file(task_node, file)
feedback_message_nodes = task_node.xpath('p:tests/p:test/p:test-configuration/c:feedback-message/text()')
files.build({
name: file_name_split.first,
name: file_name_split.first,
content: file.xpath('text()').first.content,
read_only: false,
hidden: file_class == 'internal',
role: role,
feedback_message: role == 'teacher_defined_test' ? feedback_message_nodes.first.content : nil,
file_type: FileType.where(
file_type: FileType.find_by(
file_extension: ".#{file_name_split.second}"
).take
})
),
})
end
self.execution_environment_id = 1
end
@ -521,7 +523,7 @@ class Exercise < ApplicationRecord
if user
# FIXME: where(user: user) will not work here!
begin
submissions.where(user: user).where("cause IN ('submit','assess')").where('score IS NOT NULL').order('score DESC').first.score || 0
submissions.where(user: user).where("cause IN ('submit','assess')").where.not(score: nil).order('score DESC').first.score || 0
rescue StandardError
0
end
@ -539,7 +541,8 @@ class Exercise < ApplicationRecord
end
def finishers
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score, cause: %w[submit assess remoteSubmit remoteAssess]}).distinct
ExternalUser.joins(:submissions).where(submissions: {exercise_id: id, score: maximum_score,
cause: %w[submit assess remoteSubmit remoteAssess]}).distinct
end
def set_default_values
@ -552,18 +555,25 @@ class Exercise < ApplicationRecord
end
def valid_main_file?
errors.add(:files, I18n.t('activerecord.errors.models.exercise.at_most_one_main_file')) if files.main_files.count > 1
if files.main_files.count > 1
errors.add(:files,
I18n.t('activerecord.errors.models.exercise.at_most_one_main_file'))
end
end
private :valid_main_file?
def valid_submission_deadlines?
return unless submission_deadline.present? || late_submission_deadline.present?
errors.add(:late_submission_deadline, I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_alone')) if late_submission_deadline.present? && submission_deadline.blank?
if late_submission_deadline.present? && submission_deadline.blank?
errors.add(:late_submission_deadline,
I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_alone'))
end
if submission_deadline.present? && late_submission_deadline.present? &&
late_submission_deadline < submission_deadline
errors.add(:late_submission_deadline, I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_before_submission_deadline'))
errors.add(:late_submission_deadline,
I18n.t('activerecord.errors.models.exercise.late_submission_deadline_not_before_submission_deadline'))
end
end
private :valid_submission_deadlines?

View File

@ -1,15 +1,19 @@
# frozen_string_literal: true
class ExerciseCollection < ApplicationRecord
include TimeHelper
has_many :exercise_collection_items, dependent: :delete_all
alias_method :items, :exercise_collection_items
alias items exercise_collection_items
has_many :exercises, through: :exercise_collection_items, inverse_of: :exercise_collections
belongs_to :user, polymorphic: true
def collection_statistics
statistics = {}
exercise_collection_items.each do |item|
statistics[item.position] = {exercise_id: item.exercise.id, exercise_title: item.exercise.title, working_time: time_to_f(item.exercise.average_working_time)}
statistics[item.position] =
{exercise_id: item.exercise.id, exercise_title: item.exercise.title,
working_time: time_to_f(item.exercise.average_working_time)}
end
statistics
end
@ -18,8 +22,8 @@ class ExerciseCollection < ApplicationRecord
if exercises.empty?
0
else
values = collection_statistics.values.reject { |o| o[:working_time].nil?}
sum = values.reduce(0) {|sum, item| sum + item[:working_time]}
values = collection_statistics.values.reject {|o| o[:working_time].nil? }
sum = values.reduce(0) {|sum, item| sum + item[:working_time] }
sum / values.size
end
end
@ -27,5 +31,4 @@ class ExerciseCollection < ApplicationRecord
def to_s
"#{I18n.t('activerecord.models.exercise_collection.one')}: #{name} (#{id})"
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ExerciseCollectionItem < ApplicationRecord
belongs_to :exercise_collection
belongs_to :exercise

View File

@ -1,13 +1,14 @@
class ExerciseTag < ApplicationRecord
# frozen_string_literal: true
class ExerciseTag < ApplicationRecord
belongs_to :tag
belongs_to :exercise
before_save :destroy_if_empty_exercise_or_tag
private
def destroy_if_empty_exercise_or_tag
destroy if exercise_id.blank? || tag_id.blank?
end
end
def destroy_if_empty_exercise_or_tag
destroy if exercise_id.blank? || tag_id.blank?
end
end

View File

@ -13,6 +13,12 @@ class ExerciseTip < ApplicationRecord
def tip_chain?
# Ensure each referenced parent exercise tip is set for this exercise
errors.add :parent_exercise_tip, I18n.t('activerecord.errors.messages.together', attribute: I18n.t('activerecord.attributes.exercise_tip.tip')) unless ExerciseTip.exists?(exercise: exercise, id: parent_exercise_tip)
unless ExerciseTip.exists?(
exercise: exercise, id: parent_exercise_tip
)
errors.add :parent_exercise_tip,
I18n.t('activerecord.errors.messages.together',
attribute: I18n.t('activerecord.attributes.exercise_tip.tip'))
end
end
end

View File

@ -1,13 +1,10 @@
class ExternalUser < User
# frozen_string_literal: true
class ExternalUser < User
validates :consumer_id, presence: true
validates :external_id, presence: true
def displayname
if name.blank?
"User " + id.to_s
else
name
end
name.presence || "User #{id}"
end
end

View File

@ -1,10 +1,9 @@
# frozen_string_literal: true
class FileTemplate < ApplicationRecord
belongs_to :file_type
def to_s
name
end
end

View File

@ -1,12 +1,14 @@
require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
# frozen_string_literal: true
require File.expand_path('../../lib/active_model/validations/boolean_presence_validator', __dir__)
class FileType < ApplicationRecord
include Creation
include DefaultValues
AUDIO_FILE_EXTENSIONS = %w(.aac .flac .m4a .mp3 .ogg .wav .wma)
IMAGE_FILE_EXTENSIONS = %w(.bmp .gif .jpeg .jpg .png)
VIDEO_FILE_EXTENSIONS = %w(.avi .flv .mkv .mp4 .m4v .ogv .webm)
AUDIO_FILE_EXTENSIONS = %w[.aac .flac .m4a .mp3 .ogg .wav .wma].freeze
IMAGE_FILE_EXTENSIONS = %w[.bmp .gif .jpeg .jpg .png].freeze
VIDEO_FILE_EXTENSIONS = %w[.avi .flv .mkv .mp4 .m4v .ogv .webm].freeze
after_initialize :set_default_values
@ -21,7 +23,7 @@ class FileType < ApplicationRecord
validates :name, presence: true
validates :renderable, boolean_presence: true
[:audio, :image, :video].each do |type|
%i[audio image video].each do |type|
define_method("#{type}?") do
self.class.const_get("#{type.upcase}_FILE_EXTENSIONS").include?(file_extension)
end

View File

@ -1,5 +1,6 @@
class InternalUser < User
# frozen_string_literal: true
class InternalUser < User
authenticates_with_sorcery!
validates :email, presence: true, uniqueness: true
@ -22,5 +23,4 @@ class InternalUser < User
def displayname
name
end
end

View File

@ -1,5 +1,6 @@
class Intervention < ApplicationRecord
# frozen_string_literal: true
class Intervention < ApplicationRecord
has_many :user_exercise_interventions
has_many :users, through: :user_exercise_interventions, source_type: 'ExternalUser'
@ -8,9 +9,8 @@ class Intervention < ApplicationRecord
end
def self.createDefaultInterventions
%w(BreakIntervention QuestionIntervention).each do |name|
%w[BreakIntervention QuestionIntervention].each do |name|
Intervention.find_or_create_by(name: name)
end
end
end
end

View File

@ -1,9 +1,11 @@
class LtiParameter < ApplicationRecord
belongs_to :consumer, foreign_key: "consumers_id"
belongs_to :exercise, foreign_key: "exercises_id"
belongs_to :external_user, foreign_key: "external_users_id"
# frozen_string_literal: true
scope :lis_outcome_service_url?, -> {
class LtiParameter < ApplicationRecord
belongs_to :consumer, foreign_key: 'consumers_id'
belongs_to :exercise, foreign_key: 'exercises_id'
belongs_to :external_user, foreign_key: 'external_users_id'
scope :lis_outcome_service_url?, lambda {
where("lti_parameters.lti_parameters ? 'lis_outcome_service_url'")
}
end
end

View File

@ -1,236 +1,245 @@
# frozen_string_literal: true
class ProxyExercise < ApplicationRecord
include Creation
include DefaultValues
include Creation
include DefaultValues
after_initialize :generate_token
after_initialize :set_reason
after_initialize :set_default_values
after_initialize :generate_token
after_initialize :set_reason
after_initialize :set_default_values
has_and_belongs_to_many :exercises
has_many :user_proxy_exercise_exercises
has_and_belongs_to_many :exercises
has_many :user_proxy_exercise_exercises
validates :public, boolean_presence: true
validates :public, boolean_presence: true
def count_files
exercises.count
end
def count_files
exercises.count
end
def set_reason
@reason = {}
end
def set_reason
@reason = {}
end
def generate_token
self.token ||= SecureRandom.hex(4)
end
private :generate_token
def generate_token
self.token ||= SecureRandom.hex(4)
end
private :generate_token
def set_default_values
set_default_values_if_present(public: false)
end
private :set_default_values
def set_default_values
set_default_values_if_present(public: false)
end
private :set_default_values
def duplicate(attributes = {})
proxy_exercise = dup
proxy_exercise.attributes = attributes
proxy_exercise
end
def duplicate(attributes = {})
proxy_exercise = dup
proxy_exercise.attributes = attributes
proxy_exercise
end
def to_s
title
end
def to_s
title
end
def get_matching_exercise(user)
assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first
recommended_exercise =
if (assigned_user_proxy_exercise)
Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" )
assigned_user_proxy_exercise.exercise
else
Rails.logger.debug("find new matching exercise for user #{user.id}" )
matching_exercise =
begin
find_matching_exercise(user)
rescue => e #fallback
Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" )
@reason[:reason] = "fallback because of error"
@reason[:error] = "#{$!}:\n\t#{e.backtrace.join("\n\t")}"
exercises.where("expected_difficulty > 1").shuffle.first # difficulty should be > 1 to prevent dummy exercise from being chosen.
end
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
matching_exercise
def get_matching_exercise(user)
assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first
if assigned_user_proxy_exercise
Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}")
assigned_user_proxy_exercise.exercise
else
Rails.logger.debug("find new matching exercise for user #{user.id}")
matching_exercise =
begin
find_matching_exercise(user)
rescue StandardError => e # fallback
Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$ERROR_INFO}")
@reason[:reason] = 'fallback because of error'
@reason[:error] = "#{$ERROR_INFO}:\n\t#{e.backtrace.join("\n\t")}"
exercises.where('expected_difficulty > 1').sample # difficulty should be > 1 to prevent dummy exercise from being chosen.
end
recommended_exercise
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user,
exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
matching_exercise
end
end
def find_matching_exercise(user)
exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq.compact
tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten
Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}")
def find_matching_exercise(user)
exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map(&:exercise).uniq.compact
tags_user_has_seen = exercises_user_has_accessed.map(&:tags).uniq.flatten
Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map(&:id).join(',')}")
# find exercises
potential_recommended_exercises = []
exercises.where("expected_difficulty >= 1").each do |ex|
## find exercises which have only tags the user has already seen
if (ex.tags - tags_user_has_seen).empty?
potential_recommended_exercises << ex
end
end
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}")
# if all exercises contain tags which the user has never seen, recommend easiest exercise
if potential_recommended_exercises.empty?
Rails.logger.debug("matched easiest exercise in pool")
@reason[:reason] = "easiest exercise in pool. empty potential exercises"
select_easiest_exercise(exercises)
else
select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
end
end
private :find_matching_exercise
def select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
topic_knowledge_user_and_max = get_user_knowledge_and_max_knowledge(user, exercises_user_has_accessed)
Rails.logger.debug("topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}")
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.size}: #{potential_recommended_exercises.map{|p| p.id}}")
topic_knowledge_user = topic_knowledge_user_and_max[:user_topic_knowledge]
topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge]
current_users_knowledge_lack = {}
topic_knowledge_max.keys.each do |tag|
current_users_knowledge_lack[tag] = topic_knowledge_user[tag] / topic_knowledge_max[tag]
# find exercises
potential_recommended_exercises = []
exercises.where('expected_difficulty >= 1').find_each do |ex|
## find exercises which have only tags the user has already seen
if (ex.tags - tags_user_has_seen).empty?
potential_recommended_exercises << ex
end
end
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map(&:id)}")
# if all exercises contain tags which the user has never seen, recommend easiest exercise
if potential_recommended_exercises.empty?
Rails.logger.debug('matched easiest exercise in pool')
@reason[:reason] = 'easiest exercise in pool. empty potential exercises'
select_easiest_exercise(exercises)
else
select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
end
end
private :find_matching_exercise
relative_knowledge_improvement = {}
potential_recommended_exercises.each do |potex|
tags = potex.tags
relative_knowledge_improvement[potex] = 0.0
Rails.logger.debug("review potential exercise #{potex.id}")
tags.each do |tag|
tag_ratio = potex.exercise_tags.where(tag: tag).first.factor.to_f / potex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f
max_topic_knowledge_ratio = potex.expected_difficulty * tag_ratio
old_relative_loss_tag = topic_knowledge_user[tag] / topic_knowledge_max[tag]
new_relative_loss_tag = topic_knowledge_user[tag] / (topic_knowledge_max[tag] + max_topic_knowledge_ratio)
Rails.logger.debug("tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, tag_ratio #{tag_ratio}")
relative_knowledge_improvement[potex] += old_relative_loss_tag - new_relative_loss_tag
end
def select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
topic_knowledge_user_and_max = get_user_knowledge_and_max_knowledge(user, exercises_user_has_accessed)
Rails.logger.debug("topic_knowledge_user_and_max: #{topic_knowledge_user_and_max}")
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.size}: #{potential_recommended_exercises.map(&:id)}")
topic_knowledge_user = topic_knowledge_user_and_max[:user_topic_knowledge]
topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge]
current_users_knowledge_lack = {}
topic_knowledge_max.each_key do |tag|
current_users_knowledge_lack[tag] = topic_knowledge_user[tag] / topic_knowledge_max[tag]
end
relative_knowledge_improvement = {}
potential_recommended_exercises.each do |potex|
tags = potex.tags
relative_knowledge_improvement[potex] = 0.0
Rails.logger.debug("review potential exercise #{potex.id}")
tags.each do |tag|
tag_ratio = potex.exercise_tags.where(tag: tag).first.factor.to_f / potex.exercise_tags.inject(0) do |sum, et|
sum += et.factor
end
max_topic_knowledge_ratio = potex.expected_difficulty * tag_ratio
old_relative_loss_tag = topic_knowledge_user[tag] / topic_knowledge_max[tag]
new_relative_loss_tag = topic_knowledge_user[tag] / (topic_knowledge_max[tag] + max_topic_knowledge_ratio)
Rails.logger.debug("tag #{tag} old_relative_loss_tag #{old_relative_loss_tag}, new_relative_loss_tag #{new_relative_loss_tag}, tag_ratio #{tag_ratio}")
relative_knowledge_improvement[potex] += old_relative_loss_tag - new_relative_loss_tag
end
highest_difficulty_user_has_accessed = exercises_user_has_accessed.map{|e| e.expected_difficulty}.sort.last || 0
best_matching_exercise = find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed)
@reason[:reason] = "best matching exercise"
@reason[:highest_difficulty_user_has_accessed] = highest_difficulty_user_has_accessed
@reason[:current_users_knowledge_lack] = current_users_knowledge_lack
@reason[:relative_knowledge_improvement] = relative_knowledge_improvement
Rails.logger.debug("current users knowledge loss: " + current_users_knowledge_lack.map{|k,v| "#{k} => #{v}"}.to_s)
Rails.logger.debug("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}")
best_matching_exercise
end
private :select_best_matching_exercise
def find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed)
Rails.logger.debug("select most appropiate exercise for user. his highest difficulty was #{highest_difficulty_user_has_accessed}")
sorted_exercises = relative_knowledge_improvement.sort_by{|k,v| v}.reverse
highest_difficulty_user_has_accessed = exercises_user_has_accessed.map(&:expected_difficulty).max || 0
best_matching_exercise = find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed)
@reason[:reason] = 'best matching exercise'
@reason[:highest_difficulty_user_has_accessed] = highest_difficulty_user_has_accessed
@reason[:current_users_knowledge_lack] = current_users_knowledge_lack
@reason[:relative_knowledge_improvement] = relative_knowledge_improvement
sorted_exercises.each do |ex,diff|
Rails.logger.debug("review exercise #{ex.id} diff: #{ex.expected_difficulty}")
if (ex.expected_difficulty - highest_difficulty_user_has_accessed) <= 1
Rails.logger.debug("matched exercise #{ex.id}")
return ex
else
Rails.logger.debug("exercise #{ex.id} is too difficult")
end
Rails.logger.debug('current users knowledge loss: ' + current_users_knowledge_lack.map do |k, v|
"#{k} => #{v}"
end.to_s)
Rails.logger.debug("relative improvements #{relative_knowledge_improvement.map {|k, v| "#{k.id}:#{v}" }}")
best_matching_exercise
end
private :select_best_matching_exercise
def find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed)
Rails.logger.debug("select most appropiate exercise for user. his highest difficulty was #{highest_difficulty_user_has_accessed}")
sorted_exercises = relative_knowledge_improvement.sort_by {|_k, v| v }.reverse
sorted_exercises.each do |ex, _diff|
Rails.logger.debug("review exercise #{ex.id} diff: #{ex.expected_difficulty}")
if (ex.expected_difficulty - highest_difficulty_user_has_accessed) <= 1
Rails.logger.debug("matched exercise #{ex.id}")
return ex
else
Rails.logger.debug("exercise #{ex.id} is too difficult")
end
easiest_exercise = sorted_exercises.min_by{|k,v| v}.first
Rails.logger.debug("no match, select easiest exercise as fallback #{easiest_exercise.id}")
easiest_exercise
end
private :find_best_exercise
easiest_exercise = sorted_exercises.min_by {|_k, v| v }.first
Rails.logger.debug("no match, select easiest exercise as fallback #{easiest_exercise.id}")
easiest_exercise
end
private :find_best_exercise
# [score][quantile]
def scoring_matrix
[
[0 ,0 ,0 ,0 ,0 ],
[0.2,0.2,0.2,0.2,0.1],
[0.5,0.5,0.4,0.4,0.3],
[0.6,0.6,0.5,0.5,0.4],
[1 ,1 ,0.9,0.8,0.7],
]
# [score][quantile]
def scoring_matrix
[
[0, 0, 0, 0, 0],
[0.2, 0.2, 0.2, 0.2, 0.1],
[0.5, 0.5, 0.4, 0.4, 0.3],
[0.6, 0.6, 0.5, 0.5, 0.4],
[1, 1, 0.9, 0.8, 0.7],
]
end
def scoring_matrix_quantiles
[0.2, 0.4, 0.6, 0.8]
end
private :scoring_matrix_quantiles
def score(user, ex)
max_score = ex.maximum_score.to_f
if max_score <= 0
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: score: 0")
return 0.0
end
def scoring_matrix_quantiles
[0.2,0.4,0.6,0.8]
points_ratio = ex.maximum_score(user) / max_score
if points_ratio == 0.0
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: points_ratio=#{points_ratio} score: 0")
return 0.0
elsif points_ratio > 1.0
points_ratio = 1.0 # The score of the exercise was adjusted and is now lower than it was
end
private :scoring_matrix_quantiles
def score(user, ex)
max_score = ex.maximum_score.to_f
if max_score <= 0
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: score: 0" )
return 0.0
points_ratio_index = ((scoring_matrix.size - 1) * points_ratio).to_i
working_time_user = ex.accumulated_working_time_for_only(user)
quantiles_working_time = ex.get_quantiles(scoring_matrix_quantiles)
quantile_index = quantiles_working_time.size
quantiles_working_time.each_with_index do |quantile_time, i|
if working_time_user <= quantile_time
quantile_index = i
break
end
points_ratio = ex.maximum_score(user) / max_score
if points_ratio == 0.0
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: points_ratio=#{points_ratio} score: 0" )
return 0.0
elsif points_ratio > 1.0
points_ratio = 1.0 # The score of the exercise was adjusted and is now lower than it was
end
points_ratio_index = ((scoring_matrix.size - 1) * points_ratio).to_i
working_time_user = ex.accumulated_working_time_for_only(user)
quantiles_working_time = ex.get_quantiles(scoring_matrix_quantiles)
quantile_index = quantiles_working_time.size
quantiles_working_time.each_with_index do |quantile_time, i|
if working_time_user <= quantile_time
quantile_index = i
break
end
end
Rails.logger.debug(
"scoring user #{user.id} exercise #{ex.id}: worktime #{working_time_user}, points: #{points_ratio}" \
"(index #{points_ratio_index}) quantiles #{quantiles_working_time} placed into quantile index #{quantile_index} " \
"score: #{scoring_matrix[points_ratio_index][quantile_index]}")
scoring_matrix[points_ratio_index][quantile_index]
end
private :score
Rails.logger.debug(
"scoring user #{user.id} exercise #{ex.id}: worktime #{working_time_user}, points: #{points_ratio}" \
"(index #{points_ratio_index}) quantiles #{quantiles_working_time} placed into quantile index #{quantile_index} " \
"score: #{scoring_matrix[points_ratio_index][quantile_index]}"
)
scoring_matrix[points_ratio_index][quantile_index]
end
private :score
def get_user_knowledge_and_max_knowledge(user, exercises)
# initialize knowledge for each tag with 0
all_used_tags_with_count = {}
exercises.each do |ex|
ex.tags.each do |t|
all_used_tags_with_count[t] ||= 0
all_used_tags_with_count[t] += 1
end
def get_user_knowledge_and_max_knowledge(user, exercises)
# initialize knowledge for each tag with 0
all_used_tags_with_count = {}
exercises.each do |ex|
ex.tags.each do |t|
all_used_tags_with_count[t] ||= 0
all_used_tags_with_count[t] += 1
end
tags_counter = all_used_tags_with_count.keys.map{|tag| [tag,0]}.to_h
topic_knowledge_loss_user = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h
topic_knowledge_max = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h
exercises_sorted = exercises.sort_by { |ex| ex.time_maximum_score(user)}
exercises_sorted.each do |ex|
Rails.logger.debug("exercise: #{ex.id}: #{ex}")
user_score_factor = score(user, ex)
ex.tags.each do |t|
tags_counter[t] += 1
tag_diminishing_return_factor = tag_diminishing_return_function(tags_counter[t], all_used_tags_with_count[t])
tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum + et.factor }.to_f
Rails.logger.debug("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum + et.factor }}")
Rails.logger.debug("tag #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}")
Rails.logger.debug("tag_ratio #{tag_ratio}")
topic_knowledge_ratio = ex.expected_difficulty * tag_ratio
Rails.logger.debug("topic_knowledge_ratio #{topic_knowledge_ratio}")
topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio * tag_diminishing_return_factor
topic_knowledge_max[t] += topic_knowledge_ratio * tag_diminishing_return_factor
end
end
tags_counter = all_used_tags_with_count.keys.index_with {|_tag| 0 }
topic_knowledge_loss_user = all_used_tags_with_count.keys.index_with {|_t| 0 }
topic_knowledge_max = all_used_tags_with_count.keys.index_with {|_t| 0 }
exercises_sorted = exercises.sort_by {|ex| ex.time_maximum_score(user) }
exercises_sorted.each do |ex|
Rails.logger.debug("exercise: #{ex.id}: #{ex}")
user_score_factor = score(user, ex)
ex.tags.each do |t|
tags_counter[t] += 1
tag_diminishing_return_factor = tag_diminishing_return_function(tags_counter[t], all_used_tags_with_count[t])
tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0) do |sum, et|
sum + et.factor
end
Rails.logger.debug("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0) do |sum, et|
sum + et.factor
end }")
Rails.logger.debug("tag #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}")
Rails.logger.debug("tag_ratio #{tag_ratio}")
topic_knowledge_ratio = ex.expected_difficulty * tag_ratio
Rails.logger.debug("topic_knowledge_ratio #{topic_knowledge_ratio}")
topic_knowledge_loss_user[t] += (1 - user_score_factor) * topic_knowledge_ratio * tag_diminishing_return_factor
topic_knowledge_max[t] += topic_knowledge_ratio * tag_diminishing_return_factor
end
{user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max}
end
{user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max}
end
def tag_diminishing_return_function(count_tag, total_count_tag)
total_count_tag += 1 # bonus exercise comes on top
1 / (1 + (Math::E**(-3 / (0.5 * total_count_tag) * (count_tag - 0.5 * total_count_tag))))
end
def select_easiest_exercise(exercises)
exercises.order(:expected_difficulty).first
end
def tag_diminishing_return_function(count_tag, total_count_tag)
total_count_tag += 1 # bonus exercise comes on top
1 / (1 + (Math::E**(-3 / (0.5 * total_count_tag) * (count_tag - 0.5 * total_count_tag))))
end
def select_easiest_exercise(exercises)
exercises.order(:expected_difficulty).first
end
end

View File

@ -18,7 +18,7 @@ class RequestForComment < ApplicationRecord
scope :unsolved, -> { where(solved: [false, nil]) }
scope :in_range, ->(from, to) { where(created_at: from..to) }
scope :with_comments, -> { select { |rfc| rfc.comments.any? } }
scope :with_comments, -> { select {|rfc| rfc.comments.any? } }
# after_save :trigger_rfc_action_cable
@ -44,7 +44,7 @@ class RequestForComment < ApplicationRecord
end
def comments_count
submission.files.map { |file| file.comments.size }.sum
submission.files.sum {|file| file.comments.size }
end
def commenters

View File

@ -1,4 +1,6 @@
# frozen_string_literal: true
class Search < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :exercise
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class StructuredError < ApplicationRecord
belongs_to :error_template
belongs_to :submission
@ -5,8 +7,8 @@ class StructuredError < ApplicationRecord
has_many :structured_error_attributes
def self.create_from_template(template, message_buffer, submission)
instance = self.create(error_template: template, submission: submission)
template.error_template_attributes.each do | attribute |
instance = create(error_template: template, submission: submission)
template.error_template_attributes.each do |attribute|
StructuredErrorAttribute.create_from_template(attribute, instance, message_buffer)
end
instance
@ -14,7 +16,7 @@ class StructuredError < ApplicationRecord
def hint
content = error_template.hint
structured_error_attributes.each do | attribute |
structured_error_attributes.each do |attribute|
content.sub! "{{#{attribute.error_template_attribute.key}}}", attribute.value if attribute.match
end
content

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class StructuredErrorAttribute < ApplicationRecord
belongs_to :structured_error
belongs_to :error_template_attribute
@ -5,11 +7,10 @@ class StructuredErrorAttribute < ApplicationRecord
def self.create_from_template(attribute, structured_error, message_buffer)
value = nil
result = message_buffer.match(attribute.regex)
if result != nil
if result.captures.size > 0
value = result.captures[0]
end
if !result.nil? && result.captures.size.positive?
value = result.captures[0]
end
self.create(structured_error: structured_error, error_template_attribute: attribute, value: value, match: result != nil)
create(structured_error: structured_error, error_template_attribute: attribute, value: value,
match: !result.nil?)
end
end

View File

@ -4,5 +4,5 @@ class StudyGroupMembership < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :study_group
validates_uniqueness_of :user_id, :scope => [:user_type, :study_group_id]
validates :user_id, uniqueness: {scope: %i[user_type study_group_id]}
end

View File

@ -1,9 +1,12 @@
# frozen_string_literal: true
class Submission < ApplicationRecord
include Context
include Creation
include ActionCableHelper
CAUSES = %w(assess download file render run save submit test autosave requestComments remoteAssess remoteSubmit)
CAUSES = %w[assess download file render run save submit test autosave requestComments remoteAssess
remoteSubmit].freeze
FILENAME_URL_PLACEHOLDER = '{filename}'
MAX_COMMENTS_ON_RECOMMENDED_RFC = 5
OLDEST_RFC_TO_SHOW = 6.months
@ -15,17 +18,27 @@ class Submission < ApplicationRecord
has_many :structured_errors
has_many :comments, through: :files
belongs_to :external_users, -> { where(submissions: {user_type: 'ExternalUser'}).includes(:submissions) }, foreign_key: :user_id, class_name: 'ExternalUser', optional: true
belongs_to :internal_users, -> { where(submissions: {user_type: 'InternalUser'}).includes(:submissions) }, foreign_key: :user_id, class_name: 'InternalUser', optional: true
belongs_to :external_users, lambda {
where(submissions: {user_type: 'ExternalUser'}).includes(:submissions)
}, foreign_key: :user_id, class_name: 'ExternalUser', optional: true
belongs_to :internal_users, lambda {
where(submissions: {user_type: 'InternalUser'}).includes(:submissions)
}, foreign_key: :user_id, class_name: 'InternalUser', optional: true
delegate :execution_environment, to: :exercise
scope :final, -> { where(cause: %w[submit remoteSubmit]) }
scope :intermediate, -> { where.not(cause: 'submit') }
scope :before_deadline, -> { joins(:exercise).where('submissions.updated_at <= exercises.submission_deadline OR exercises.submission_deadline IS NULL') }
scope :within_grace_period, -> { joins(:exercise).where('(submissions.updated_at > exercises.submission_deadline) AND (submissions.updated_at <= exercises.late_submission_deadline OR exercises.late_submission_deadline IS NULL)') }
scope :after_late_deadline, -> { joins(:exercise).where('submissions.updated_at > exercises.late_submission_deadline') }
scope :before_deadline, lambda {
joins(:exercise).where('submissions.updated_at <= exercises.submission_deadline OR exercises.submission_deadline IS NULL')
}
scope :within_grace_period, lambda {
joins(:exercise).where('(submissions.updated_at > exercises.submission_deadline) AND (submissions.updated_at <= exercises.late_submission_deadline OR exercises.late_submission_deadline IS NULL)')
}
scope :after_late_deadline, lambda {
joins(:exercise).where('submissions.updated_at > exercises.late_submission_deadline')
}
scope :latest, -> { order(updated_at: :desc).first }
@ -36,7 +49,6 @@ class Submission < ApplicationRecord
# after_save :trigger_working_times_action_cable
def build_files_hash(files, attribute)
files.map(&attribute.to_proc).zip(files).to_h
end
@ -57,12 +69,12 @@ class Submission < ApplicationRecord
# expects the full file path incl. file extension
# Caution: There must be no unnecessary path prefix included.
# Use `file.ext` rather than `./file.ext`
collect_files.detect { |file| file.filepath == file_path }
collect_files.detect {|file| file.filepath == file_path }
end
def normalized_score
::NewRelic::Agent.add_custom_attributes({unnormalized_score: score})
if !score.nil? && !exercise.maximum_score.nil? && (exercise.maximum_score > 0)
if !score.nil? && !exercise.maximum_score.nil? && exercise.maximum_score.positive?
score / exercise.maximum_score
else
0
@ -119,6 +131,8 @@ class Submission < ApplicationRecord
end
def unsolved_rfc
RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).where(created_at: OLDEST_RFC_TO_SHOW.ago..Time.current).order("RANDOM()").find { |rfc_element| ((rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) && (!rfc_element.question.empty?)) }
RequestForComment.unsolved.where(exercise_id: exercise).where.not(question: nil).where(created_at: OLDEST_RFC_TO_SHOW.ago..Time.current).order('RANDOM()').find do |rfc_element|
((rfc_element.comments_count < MAX_COMMENTS_ON_RECOMMENDED_RFC) && !rfc_element.question.empty?)
end
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class Subscription < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :request_for_comment

View File

@ -1,22 +1,22 @@
class Tag < ApplicationRecord
# frozen_string_literal: true
class Tag < ApplicationRecord
has_many :exercise_tags
has_many :exercises, through: :exercise_tags
validates_uniqueness_of :name
validates :name, uniqueness: true
def destroy
if (can_be_destroyed?)
if can_be_destroyed?
super
end
end
def can_be_destroyed?
!exercises.any?
exercises.none?
end
def to_s
name
end
end
end

View File

@ -1,4 +1,6 @@
# frozen_string_literal: true
class Testrun < ApplicationRecord
belongs_to :file, class_name: 'CodeOcean::File', optional: true
belongs_to :submission
belongs_to :file, class_name: 'CodeOcean::File', optional: true
belongs_to :submission
end

View File

@ -6,11 +6,16 @@ class Tip < ApplicationRecord
has_many :exercise_tips
has_many :exercises, through: :exercise_tips
belongs_to :file_type, optional: true
validates_presence_of :file_type, if: :example?
validates :file_type, presence: {if: :example?}
validate :content?
def content?
errors.add :description, I18n.t('activerecord.errors.messages.at_least', attribute: I18n.t('activerecord.attributes.tip.example')) unless [description?, example?].include?(true)
unless [
description?, example?
].include?(true)
errors.add :description,
I18n.t('activerecord.errors.messages.at_least', attribute: I18n.t('activerecord.attributes.tip.example'))
end
end
def to_s

View File

@ -3,7 +3,7 @@
class User < ApplicationRecord
self.abstract_class = true
ROLES = %w(admin teacher learner)
ROLES = %w[admin teacher learner].freeze
belongs_to :consumer
has_many :study_group_memberships, as: :user
@ -19,10 +19,11 @@ class User < ApplicationRecord
has_one :codeharbor_link, dependent: :destroy
accepts_nested_attributes_for :user_proxy_exercise_exercises
scope :with_submissions, -> { where('id IN (SELECT user_id FROM submissions)') }
scope :in_study_group_of, ->(user) { joins(:study_group_memberships).where(study_group_memberships: {study_group_id: user.study_groups}) unless user.admin? }
scope :in_study_group_of, lambda {|user|
joins(:study_group_memberships).where(study_group_memberships: {study_group_id: user.study_groups}) unless user.admin?
}
ROLES.each do |role|
define_method("#{role}?") { try(:role) == role }

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class UserExerciseFeedback < ApplicationRecord
include Creation
@ -5,17 +7,17 @@ class UserExerciseFeedback < ApplicationRecord
belongs_to :submission, optional: true
has_one :execution_environment, through: :exercise
validates :user_id, uniqueness: { scope: [:exercise_id, :user_type] }
validates :user_id, uniqueness: {scope: %i[exercise_id user_type]}
scope :intermediate, -> { where.not(normalized_score: 1.00) }
scope :final, -> { where(normalized_score: 1.00) }
def to_s
"User Exercise Feedback"
'User Exercise Feedback'
end
def anomaly_notification
AnomalyNotification.where({exercise_id: exercise.id, user_id: user_id, user_type: user_type})
.where("created_at < ?", created_at).order("created_at DESC").to_a.first
.where('created_at < ?', created_at).order('created_at DESC').to_a.first
end
end

View File

@ -1,5 +1,6 @@
class UserExerciseIntervention < ApplicationRecord
# frozen_string_literal: true
class UserExerciseIntervention < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :intervention
belongs_to :exercise
@ -7,5 +8,4 @@ class UserExerciseIntervention < ApplicationRecord
validates :user, presence: true
validates :exercise, presence: true
validates :intervention, presence: true
end
end

View File

@ -1,5 +1,6 @@
class UserProxyExerciseExercise < ApplicationRecord
# frozen_string_literal: true
class UserProxyExerciseExercise < ApplicationRecord
belongs_to :user, polymorphic: true
belongs_to :exercise
belongs_to :proxy_exercise
@ -9,6 +10,5 @@ class UserProxyExerciseExercise < ApplicationRecord
validates :exercise_id, presence: true
validates :proxy_exercise_id, presence: true
validates :user_id, uniqueness: { scope: [:proxy_exercise_id, :user_type] }
end
validates :user_id, uniqueness: {scope: %i[proxy_exercise_id user_type]}
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Admin
class DashboardPolicy < AdminOnlyPolicy
def dump_docker?

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class AdminOnlyPolicy < ApplicationPolicy
[:create?, :destroy?, :edit?, :index?, :new?, :show?, :update?].each do |action|
%i[create? destroy? edit? index? new? show? update?].each do |action|
define_method(action) { admin? }
end
end

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true
class AdminOrAuthorPolicy < ApplicationPolicy
[:create?, :index?, :new?].each do |action|
%i[create? index? new?].each do |action|
define_method(action) { admin? || teacher? }
end
[:destroy?, :edit?, :show?, :update?].each do |action|
%i[destroy? edit? show? update?].each do |action|
define_method(action) { admin? || author? }
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ApplicationPolicy
def admin?
@user.admin?
@ -43,7 +45,7 @@ class ApplicationPolicy
return false
end
@user.study_groups.any?{|i| study_groups.include?(i) }
@user.study_groups.any? {|i| study_groups.include?(i) }
end
private :everyone_in_study_group
@ -59,7 +61,7 @@ class ApplicationPolicy
end
def require_user!
fail Pundit::NotAuthorizedError unless @user
raise Pundit::NotAuthorizedError unless @user
end
private :require_user!
@ -71,7 +73,7 @@ class ApplicationPolicy
end
def require_user!
fail Pundit::NotAuthorizedError unless @user
raise Pundit::NotAuthorizedError unless @user
end
private :require_user!
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module CodeOcean
class FilePolicy < AdminOrAuthorPolicy
def author?
@ -15,7 +17,7 @@ module CodeOcean
def create?
if @record.context.is_a?(Exercise)
admin? || author?
elsif @record.context.is_a?(Submission) and @record.context.exercise.allow_file_creation
elsif @record.context.is_a?(Submission) && @record.context.exercise.allow_file_creation
author?
else
no_one

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class CodeharborLinkPolicy < ApplicationPolicy
def index?
false

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class CommentPolicy < ApplicationPolicy
def create?
everyone
@ -7,7 +9,7 @@ class CommentPolicy < ApplicationPolicy
everyone
end
[:new?, :destroy?, :update?, :edit?].each do |action|
%i[new? destroy? update? edit?].each do |action|
define_method(action) { admin? || author? }
end

View File

@ -1,3 +1,4 @@
class ConsumerPolicy < AdminOnlyPolicy
# frozen_string_literal: true
class ConsumerPolicy < AdminOnlyPolicy
end

View File

@ -1,3 +1,4 @@
class ErrorTemplateAttributePolicy < AdminOnlyPolicy
# frozen_string_literal: true
class ErrorTemplateAttributePolicy < AdminOnlyPolicy
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ErrorTemplatePolicy < AdminOnlyPolicy
def add_attribute?
admin?

View File

@ -1,7 +1,7 @@
class EventPolicy < AdminOnlyPolicy
# frozen_string_literal: true
class EventPolicy < AdminOnlyPolicy
def create?
everyone
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class ExecutionEnvironmentPolicy < AdminOnlyPolicy
[:execute_command?, :shell?, :statistics?, :show?].each do |action|
%i[execute_command? shell? statistics? show?].each do |action|
define_method(action) { admin? || author? }
end

View File

@ -1,7 +1,7 @@
class ExerciseCollectionPolicy < AdminOnlyPolicy
# frozen_string_literal: true
class ExerciseCollectionPolicy < AdminOnlyPolicy
def statistics?
admin?
end
end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ExternalUserPolicy < AdminOnlyPolicy
def index?
admin? || teacher?

Some files were not shown because too many files have changed in this diff Show More