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, # 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. # 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 Rails.application.load_tasks

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,8 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::InvalidAuthenticityToken, with: :render_csrf_error rescue_from ActionController::InvalidAuthenticityToken, with: :render_csrf_error
def current_user 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 @current_user ||= ExternalUser.find_by(id: session[:external_user_id]) || login_from_session || login_from_other_sources || nil
end end
@ -59,7 +60,7 @@ class ApplicationController < ActionController::Base
respond_to do |format| respond_to do |format|
format.html do format.html do
# Prevent redirect loop # Prevent redirect loop
if request.url == request.referrer if request.url == request.referer
redirect_to :root, alert: message redirect_to :root, alert: message
else else
redirect_back fallback_location: :root, allow_other_host: false, alert: message redirect_back fallback_location: :root, allow_other_host: false, alert: message

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module CodeOcean module CodeOcean
class FilesController < ApplicationController class FilesController < ApplicationController
include CommonBehavior include CommonBehavior
@ -25,9 +27,10 @@ module CodeOcean
if @object.save if @object.save
yield if block_given? yield if block_given?
path = options[:path].try(:call) || @object 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 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.html { redirect_to(options[:path]); flash[:danger] = t('files.error.filename', name: filename) }
format.json { render(json: @object.errors, status: :unprocessable_entity) } format.json { render(json: @object.errors, status: :unprocessable_entity) }
end end
@ -41,7 +44,10 @@ module CodeOcean
end end
def file_params 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 end
private :file_params private :file_params
end end

View File

@ -7,7 +7,8 @@ class CodeharborLinksController < ApplicationController
def new def new
base_url = CodeOcean::Config.new(:code_ocean).read[:codeharbor][:url] || '' 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! authorize!
end end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class CommentsController < ApplicationController 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 # to disable authorization check: comment the line below back in
# skip_after_action :verify_authorized # skip_after_action :verify_authorized
@ -12,16 +14,16 @@ class CommentsController < ApplicationController
# GET /comments.json # GET /comments.json
def index def index
file = CodeOcean::File.find(params[:file_id]) 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) submission = Submission.find_by(id: file.context_id)
if submission if submission
@comments = Comment.where(file_id: params[:file_id]) @comments = Comment.where(file_id: params[:file_id])
@comments.map{|comment| @comments.map do |comment|
comment.username = comment.user.displayname comment.username = comment.user.displayname
comment.date = comment.created_at.strftime('%d.%m.%Y %k:%M') comment.date = comment.created_at.strftime('%d.%m.%Y %k:%M')
comment.updated = (comment.created_at != comment.updated_at) comment.updated = (comment.created_at != comment.updated_at)
comment.editable = comment.user == current_user comment.editable = comment.user == current_user
} end
else else
@comments = [] @comments = []
end end
@ -81,9 +83,10 @@ class CommentsController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through. # Never trust parameters from the scary internet, only allow the white list through.
def comment_params 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: # 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 end
def send_mail_to_author(comment, request_for_comment) def send_mail_to_author(comment, request_for_comment)
@ -96,17 +99,16 @@ class CommentsController < ApplicationController
request_for_comment.commenters.each do |commenter| request_for_comment.commenters.each do |commenter|
already_sent_mail = false already_sent_mail = false
subscriptions = Subscription.where( subscriptions = Subscription.where(
:request_for_comment_id => request_for_comment.id, request_for_comment_id: request_for_comment.id,
:user_id => commenter.id, :user_type => commenter.class.name, user_id: commenter.id, user_type: commenter.class.name,
:deleted => false) deleted: false
)
subscriptions.each do |subscription| subscriptions.each do |subscription|
if (subscription.subscription_type == 'author' and current_user == request_for_comment.user) or subscription.subscription_type == 'all' if (((subscription.subscription_type == 'author') && (current_user == request_for_comment.user)) || (subscription.subscription_type == 'all')) && !((subscription.user == current_user) || already_sent_mail)
unless subscription.user == current_user or already_sent_mail
UserMailer.got_new_comment_for_subscription(comment, subscription, current_user).deliver_now UserMailer.got_new_comment_for_subscription(comment, subscription, current_user).deliver_now
already_sent_mail = true already_sent_mail = true
end end
end end
end end
end end
end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module CommonBehavior module CommonBehavior
def create_and_respond(options = {}) def create_and_respond(options = {})
@object = options[:object] @object = options[:object]
@ -5,7 +7,8 @@ module CommonBehavior
if @object.save if @object.save
yield if block_given? yield if block_given?
path = options[:path].try(:call) || @object 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 else
respond_with_invalid_object(format, template: :new) respond_with_invalid_object(format, template: :new)
end end
@ -40,7 +43,8 @@ module CommonBehavior
respond_to do |format| respond_to do |format|
if @object.update(options[:params]) if @object.update(options[:params])
path = options[:path] || @object 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 else
respond_with_invalid_object(format, template: :edit) respond_with_invalid_object(format, template: :edit)
end end

View File

@ -15,7 +15,8 @@ module FileParameters
private :reject_illegal_file_attributes private :reject_illegal_file_attributes
def 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 end
private :file_attributes private :file_attributes
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'oauth/request_proxy/rack_request' require 'oauth/request_proxy/rack_request'
module Lti module Lti
@ -6,7 +8,7 @@ module Lti
MAXIMUM_SCORE = 1 MAXIMUM_SCORE = 1
MAXIMUM_SESSION_AGE = 60.minutes 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 = {}) def build_tool_provider(options = {})
if options[:consumer] && options[:parameters] 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. # exercise_id.exists? ==> the user has submitted the results of an exercise to the consumer.
# Only the lti_parameters are deleted. # Only the lti_parameters are deleted.
def clear_lti_session_data(exercise_id = nil, user_id = nil) def clear_lti_session_data(exercise_id = nil, user_id = nil)
if (exercise_id.nil?) if exercise_id.nil?
session.delete(:external_user_id) session.delete(:external_user_id)
session.delete(:study_group_id) session.delete(:study_group_id)
session.delete(:embed_options) session.delete(:embed_options)
@ -48,18 +50,14 @@ module Lti
def external_user_name(provider) def external_user_name(provider)
# save person_name_full if supplied. this is the display_name, if it is set. # 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) # else only save the firstname, we don't want lastnames (family names)
if provider.lis_person_name_full provider.lis_person_name_full || provider.lis_person_name_given
provider.lis_person_name_full
else
provider.lis_person_name_given
end
end end
private :external_user_name private :external_user_name
def external_user_role(provider) def external_user_role(provider)
result = 'learner' result = 'learner'
unless provider.roles.blank? if provider.roles.present?
provider.roles.each do |role| provider.roles.each do |role|
case role.downcase case role.downcase
when 'administrator' when 'administrator'
@ -141,7 +139,9 @@ module Lti
def send_score(submission) def send_score(submission)
::NewRelic::Agent.add_custom_attributes({score: submission.normalized_score, session: session}) ::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 if submission.user.consumer
lti_parameter = LtiParameter.where(consumers_id: submission.user.consumer.id, lti_parameter = LtiParameter.where(consumers_id: submission.user.consumer.id,
@ -159,7 +159,7 @@ module Lti
score: submission.normalized_score, score: submission.normalized_score,
lti_parameter: lti_parameter.inspect, lti_parameter: lti_parameter.inspect,
session: session.to_hash, session: session.to_hash,
exercise_id: submission.exercise_id exercise_id: submission.exercise_id,
}) })
normalized_lit_score = submission.normalized_score normalized_lit_score = submission.normalized_score
if submission.before_deadline? if submission.before_deadline?
@ -170,11 +170,10 @@ module Lti
elsif submission.after_late_deadline? elsif submission.after_late_deadline?
# Reduce score by 100% # Reduce score by 100%
normalized_lit_score *= 0.0 normalized_lit_score *= 0.0
else # no deadline
# Keep the full score
end end
response = provider.post_replace_result!(normalized_lit_score) 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 else
{status: 'unsupported'} {status: 'unsupported'}
end end
@ -186,22 +185,21 @@ module Lti
@current_user = ExternalUser.find_or_create_by(consumer_id: @consumer.id, external_id: @provider.user_id) @current_user = ExternalUser.find_or_create_by(consumer_id: @consumer.id, external_id: @provider.user_id)
external_role = external_user_role(@provider) external_role = external_user_role(@provider)
internal_role = @current_user.role 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 # 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) @current_user.update(email: external_user_email(@provider), name: external_user_name(@provider), role: desired_role)
end end
private :set_current_user private :set_current_user
def set_study_group_membership def set_study_group_membership
group = if not context_id? group = if context_id?
StudyGroup.find_or_create_by(external_id: @provider.resource_link_id, consumer: @consumer)
else
# Ensure to find the group independent of the name and set it only once. # 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| StudyGroup.find_or_create_by(external_id: @provider.context_id, consumer: @consumer) do |new_group|
new_group.name = @provider.context_title new_group.name = @provider.context_title
end end
else
StudyGroup.find_or_create_by(external_id: @provider.resource_link_id, consumer: @consumer)
end end
group.external_users << @current_user unless group.external_users.include? @current_user group.external_users << @current_user unless group.external_users.include? @current_user
group.save group.save

View File

@ -1,8 +1,13 @@
# frozen_string_literal: true
module RemoteEvaluationParameters module RemoteEvaluationParameters
include FileParameters include FileParameters
def remote_evaluation_params 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 end
private :remote_evaluation_params private :remote_evaluation_params
end end

View File

@ -4,7 +4,12 @@ module SubmissionParameters
include FileParameters include FileParameters
def submission_params 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) submission_params = merge_user(submission_params)
files_attributes = submission_params[:files_attributes] files_attributes = submission_params[:files_attributes]
exercise = Exercise.find_by(id: submission_params[:exercise_id]) exercise = Exercise.find_by(id: submission_params[:exercise_id])

View File

@ -12,8 +12,8 @@ module SubmissionScoring
output = execute_test_file(file, submission) output = execute_test_file(file, submission)
assessment = assessor.assess(output) assessment = assessor.assess(output)
passed = ((assessment[:passed] == assessment[:count]) and (assessment[:score]).positive?) 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 testrun_output = passed ? nil : "status: #{output[:status]}\n stdout: #{output[:stdout]}\n stderr: #{output[:stderr]}"
unless testrun_output.blank? if testrun_output.present?
submission.exercise.execution_environment.error_templates.each do |template| submission.exercise.execution_environment.error_templates.each do |template|
pattern = Regexp.new(template.signature).freeze pattern = Regexp.new(template.signature).freeze
StructuredError.create_from_template(template, testrun_output, submission) if pattern.match(testrun_output) StructuredError.create_from_template(template, testrun_output, submission) if pattern.match(testrun_output)
@ -50,7 +50,8 @@ module SubmissionScoring
private :collect_test_results private :collect_test_results
def execute_test_file(file, submission) 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 end
private :execute_test_file private :execute_test_file
@ -69,17 +70,21 @@ module SubmissionScoring
def score_submission(submission) def score_submission(submission)
outputs = collect_test_results(submission) outputs = collect_test_results(submission)
score = 0.0 score = 0.0
unless outputs.nil? || outputs.empty? if outputs.present?
outputs.each do |output| outputs.each do |output|
score += output[:score] * output[:weight] unless output.nil? 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
end end
submission.update(score: score) submission.update(score: score)
if submission.normalized_score == 1.0 if submission.normalized_score == 1.0
Thread.new do 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.full_score_reached = true
rfc.save rfc.save
end end

View File

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

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class ErrorTemplateAttributesController < ApplicationController 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! def authorize!
authorize(@error_template_attributes || @error_template_attribute) authorize(@error_template_attributes || @error_template_attribute)
@ -9,7 +11,8 @@ class ErrorTemplateAttributesController < ApplicationController
# GET /error_template_attributes # GET /error_template_attributes
# GET /error_template_attributes.json # GET /error_template_attributes.json
def index 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! authorize!
end end
@ -38,7 +41,9 @@ class ErrorTemplateAttributesController < ApplicationController
respond_to do |format| respond_to do |format|
if @error_template_attribute.save 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 } format.json { render :show, status: :created, location: @error_template_attribute }
else else
format.html { render :new } format.html { render :new }
@ -53,7 +58,9 @@ class ErrorTemplateAttributesController < ApplicationController
authorize! authorize!
respond_to do |format| respond_to do |format|
if @error_template_attribute.update(error_template_attribute_params) 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 } format.json { render :show, status: :ok, location: @error_template_attribute }
else else
format.html { render :edit } format.html { render :edit }
@ -68,12 +75,15 @@ class ErrorTemplateAttributesController < ApplicationController
authorize! authorize!
@error_template_attribute.destroy @error_template_attribute.destroy
respond_to do |format| 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 } format.json { head :no_content }
end end
end end
private private
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
def set_error_template_attribute def set_error_template_attribute
@error_template_attribute = ErrorTemplateAttribute.find(params[:id]) @error_template_attribute = ErrorTemplateAttribute.find(params[:id])
@ -81,6 +91,9 @@ class ErrorTemplateAttributesController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through. # Never trust parameters from the scary internet, only allow the white list through.
def error_template_attribute_params def error_template_attribute_params
params[:error_template_attribute].permit(:key, :description, :regex, :important) if params[:error_template_attribute].present? if params[:error_template_attribute].present?
params[:error_template_attribute].permit(:key, :description, :regex,
:important)
end
end end
end end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class ErrorTemplatesController < ApplicationController 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! def authorize!
authorize(@error_templates || @error_template) authorize(@error_templates || @error_template)
@ -92,6 +94,7 @@ class ErrorTemplatesController < ApplicationController
end end
private private
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
def set_error_template def set_error_template
@error_template = ErrorTemplate.find(params[:id]) @error_template = ErrorTemplate.find(params[:id])
@ -99,6 +102,9 @@ class ErrorTemplatesController < ApplicationController
# Never trust parameters from the scary internet, only allow the white list through. # Never trust parameters from the scary internet, only allow the white list through.
def error_template_params def error_template_params
params[:error_template].permit(:name, :execution_environment_id, :signature, :description, :hint) if params[:error_template].present? if params[:error_template].present?
params[:error_template].permit(:name, :execution_environment_id, :signature, :description,
:hint)
end
end end
end end

View File

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

View File

@ -1,15 +1,16 @@
# frozen_string_literal: true
class ExerciseCollectionsController < ApplicationController class ExerciseCollectionsController < ApplicationController
include CommonBehavior 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 def index
@exercise_collections = ExerciseCollection.all.paginate(:page => params[:page]) @exercise_collections = ExerciseCollection.all.paginate(page: params[:page])
authorize! authorize!
end end
def show def show; end
end
def new def new
@exercise_collection = ExerciseCollection.new @exercise_collection = ExerciseCollection.new
@ -28,16 +29,14 @@ class ExerciseCollectionsController < ApplicationController
destroy_and_respond(object: @exercise_collection) destroy_and_respond(object: @exercise_collection)
end end
def edit def edit; end
end
def update def update
authorize! authorize!
update_and_respond(object: @exercise_collection, params: exercise_collection_params) update_and_respond(object: @exercise_collection, params: exercise_collection_params)
end end
def statistics def statistics; end
end
private private
@ -51,8 +50,18 @@ class ExerciseCollectionsController < ApplicationController
end end
def exercise_collection_params 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 = if params[:exercise_collection].present?
sanitized_params[:exercise_ids] = sanitized_params[:exercise_ids].reject {|v| v.nil? or v == ''} params[:exercise_collection].permit(:name,
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)} :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
end end

View File

@ -9,15 +9,19 @@ class ExercisesController < ApplicationController
before_action :handle_file_uploads, only: %i[create update] before_action :handle_file_uploads, only: %i[create update]
before_action :set_execution_environments, only: %i[create edit new 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_external_user_and_authorize, only: [:statistics]
before_action :set_file_types, only: %i[create edit new update] before_action :set_file_types, only: %i[create edit new update]
before_action :set_course_token, only: [:implement] before_action :set_course_token, only: [:implement]
before_action :set_available_tips, only: %i[implement show new edit] 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_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! def authorize!
authorize(@exercise || @exercises) authorize(@exercise || @exercises)
@ -72,8 +76,8 @@ class ExercisesController < ApplicationController
return if performed? return if performed?
myparam = exercise_params.presence || {} myparam = exercise_params.presence || {}
checked_exercise_tags = @exercise_tags.select { |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 } removed_exercise_tags = @exercise_tags.reject {|et| myparam[:tag_ids].include? et.tag.id.to_s }
checked_exercise_tags.each do |et| checked_exercise_tags.each do |et|
et.factor = params[:tag_factors][et.tag_id.to_s][:factor] et.factor = params[:tag_factors][et.tag_id.to_s][:factor]
@ -106,7 +110,8 @@ class ExercisesController < ApplicationController
end end
def export_external_check 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: { render json: {
message: codeharbor_check[:message], message: codeharbor_check[:message],
actions: render_to_string( actions: render_to_string(
@ -116,9 +121,9 @@ class ExercisesController < ApplicationController
exercise_found: codeharbor_check[:exercise_found], exercise_found: codeharbor_check[:exercise_found],
update_right: codeharbor_check[:update_right], update_right: codeharbor_check[:update_right],
error: codeharbor_check[:error], error: codeharbor_check[:error],
exported: false exported: false,
} }
) ),
}, status: :ok }, status: :ok
end end
@ -133,14 +138,16 @@ class ExercisesController < ApplicationController
render json: { render json: {
status: 'success', status: 'success',
message: t('exercises.export_codeharbor.successfully_exported', id: @exercise.id, title: @exercise.title), 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 @exercise.save
else else
render json: { render json: {
status: 'fail', status: 'fail',
message: t('exercises.export_codeharbor.export_failed', id: @exercise.id, title: @exercise.title, error: error), 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
end end
@ -177,7 +184,7 @@ class ExercisesController < ApplicationController
render json: t('exercises.import_codeharbor.import_errors.invalid'), status: :bad_request render json: t('exercises.import_codeharbor.import_errors.invalid'), status: :bad_request
rescue StandardError => e rescue StandardError => e
Sentry.capture_exception(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 end
def user_from_api_key def user_from_api_key
@ -194,7 +201,11 @@ class ExercisesController < ApplicationController
private :user_by_codeharbor_token private :user_by_codeharbor_token
def exercise_params 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 end
private :exercise_params 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.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? redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
user_solved_exercise = @exercise.has_user_solved(current_user) 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 count_interventions_today = UserExerciseIntervention.where(user: current_user).where('created_at >= ?',
user_got_intervention_in_exercise = UserExerciseIntervention.where(user: current_user, exercise: @exercise).size >= max_intervention_count_per_exercise 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 (user_got_enough_interventions = count_interventions_today >= max_intervention_count_per_day) || user_got_intervention_in_exercise
if @embed_options[:disable_interventions] if @embed_options[:disable_interventions]
@ -331,14 +344,15 @@ class ExercisesController < ApplicationController
end end
# Return an array with top-level tips # 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 end
private :set_available_tips private :set_available_tips
def working_times def working_times
working_time_accumulated = @exercise.accumulated_working_time_for_only(current_user) working_time_accumulated = @exercise.accumulated_working_time_for_only(current_user)
working_time_75_percentile = @exercise.get_quantiles([0.75]).first 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 end
def intervention def intervention
@ -436,7 +450,9 @@ class ExercisesController < ApplicationController
checked_exercise_tags = @exercise.exercise_tags checked_exercise_tags = @exercise.exercise_tags
checked_tags = checked_exercise_tags.collect(&:tag).to_set checked_tags = checked_exercise_tags.collect(&:tag).to_set
unchecked_tags = Tag.all.to_set.subtract checked_tags 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 end
private :collect_set_and_unset_exercise_tags private :collect_set_and_unset_exercise_tags
@ -453,8 +469,10 @@ class ExercisesController < ApplicationController
# Render statistics page for one specific external user # Render statistics page for one specific external user
authorize(@external_user, :statistics?) authorize(@external_user, :statistics?)
if policy(@exercise).detailed_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') @submissions = Submission.where(user: @external_user,
interventions = UserExerciseIntervention.where('user_id = ? AND exercise_id = ?', @external_user.id, @exercise.id) 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) @all_events = (@submissions + interventions).sort_by(&:created_at)
@deltas = @all_events.map.with_index do |item, index| @deltas = @all_events.map.with_index do |item, index|
delta = item.created_at - @all_events[index - 1].created_at if index.positive? 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?)) @working_times_until.push((format_time_difference(@deltas[0..index].inject(:+)) if index.positive?))
end end
else 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 = [] @submissions = []
%i[before_deadline within_grace_period after_late_deadline].each do |filter| %i[before_deadline within_grace_period after_late_deadline].each do |filter|
relevant_submission = final_submissions.send(filter).latest relevant_submission = final_submissions.send(filter).latest
@ -492,7 +511,7 @@ class ExercisesController < ApplicationController
user_statistics[tuple['user_id'].to_i] = tuple user_statistics[tuple['user_id'].to_i] = tuple
end end
render locals: { render locals: {
user_statistics: user_statistics user_statistics: user_statistics,
} }
end end
end end
@ -508,7 +527,8 @@ class ExercisesController < ApplicationController
end end
def transmit_lti_score 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) response = send_score(@submission)
if response[:status] == 'success' if response[:status] == 'success'
@ -533,8 +553,8 @@ class ExercisesController < ApplicationController
return if performed? return if performed?
myparam = exercise_params myparam = exercise_params
checked_exercise_tags = @exercise_tags.select { |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 } removed_exercise_tags = @exercise_tags.reject {|et| myparam[:tag_ids].include? et.tag.id.to_s }
checked_exercise_tags.each do |et| checked_exercise_tags.each do |et|
et.factor = params[:tag_factors][et.tag_id.to_s][:factor] et.factor = params[:tag_factors][et.tag_id.to_s][:factor]

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ExternalUsersController < ApplicationController class ExternalUsersController < ApplicationController
before_action :require_user! before_action :require_user!
@ -17,8 +19,8 @@ class ExternalUsersController < ApplicationController
authorize! authorize!
end end
def working_time_query(tag=nil) def working_time_query(tag = nil)
""" "
SELECT user_id, SELECT user_id,
bar.exercise_id, bar.exercise_id,
max(score) as maximum_score, max(score) as maximum_score,
@ -43,16 +45,16 @@ class ExternalUsersController < ApplicationController
FROM submissions FROM submissions
WHERE user_id = #{@user.id} WHERE user_id = #{@user.id}
AND user_type = 'ExternalUser' 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, GROUP BY exercise_id,
user_id, user_id,
id id
) AS foo ) AS foo
) AS bar ) 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, GROUP BY user_id,
bar.exercise_id; bar.exercise_id;
""" "
end end
def statistics def statistics
@ -62,11 +64,11 @@ class ExternalUsersController < ApplicationController
statistics = {} statistics = {}
ApplicationRecord.connection.execute(working_time_query(params[:tag])).each do |tuple| 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 end
render locals: { render locals: {
statistics: statistics statistics: statistics,
} }
end end
@ -75,15 +77,15 @@ class ExternalUsersController < ApplicationController
authorize! authorize!
statistics = [] 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| 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 end
statistics.sort_by! {|item| -item[:value]} statistics.sort_by! {|item| -item[:value] }
respond_to do |format| respond_to do |format|
format.json { render(json: statistics) } format.json { render(json: statistics) }
end end
end end
end end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class FileTemplatesController < ApplicationController 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! def authorize!
authorize(@file_template || @file_templates) authorize(@file_template || @file_templates)
@ -7,7 +9,7 @@ class FileTemplatesController < ApplicationController
private :authorize! private :authorize!
def by_file_type 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! authorize!
respond_to do |format| respond_to do |format|
format.json { render :show, status: :ok, json: @file_templates.to_json } format.json { render :show, status: :ok, json: @file_templates.to_json }
@ -82,6 +84,7 @@ class FileTemplatesController < ApplicationController
end end
private private
# Use callbacks to share common setup or constraints between actions. # Use callbacks to share common setup or constraints between actions.
def set_file_template def set_file_template
@file_template = FileTemplate.find(params[:id]) @file_template = FileTemplate.find(params[:id])

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
class FileTypesController < ApplicationController class FileTypesController < ApplicationController
include CommonBehavior 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 before_action :set_file_type, only: MEMBER_ACTIONS
def authorize! def authorize!
@ -19,11 +21,14 @@ class FileTypesController < ApplicationController
destroy_and_respond(object: @file_type) destroy_and_respond(object: @file_type)
end end
def edit def edit; end
end
def file_type_params 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 end
private :file_type_params private :file_type_params
@ -39,7 +44,7 @@ class FileTypesController < ApplicationController
def set_editor_modes def set_editor_modes
@editor_modes = Dir.glob('vendor/assets/javascripts/ace/mode-*.js').sort.map do |filename| @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}"] [name, "ace/mode/#{name}"]
end end
end end
@ -51,8 +56,7 @@ class FileTypesController < ApplicationController
end end
private :set_file_type private :set_file_type
def show def show; end
end
def update def update
update_and_respond(object: @file_type, params: file_type_params) update_and_respond(object: @file_type, params: file_type_params)

View File

@ -1,5 +1,6 @@
class FlowrController < ApplicationController # frozen_string_literal: true
class FlowrController < ApplicationController
def insights def insights
require_user! require_user!
# get the latest submission for this user that also has a test run (i.e. structured_errors if applicable) # get the latest submission for this user that also has a test run (i.e. structured_errors if applicable)
@ -26,8 +27,8 @@ class FlowrController < ApplicationController
end end
# once the programming language model becomes available, the language name can be added to the query to # once the programming language model becomes available, the language name can be added to the query to
# produce more relevant results # produce more relevant results
query = attributes.map{|att| att.value}.join(' ') query = attributes.map(&:value).join(' ')
{ submission: submission, error: error, attributes: attributes, query: query } {submission: submission, error: error, attributes: attributes, query: query}
end end
# Always return JSON # Always return JSON

View File

@ -93,7 +93,8 @@ class InternalUsersController < ApplicationController
private :require_reset_password_token private :require_reset_password_token
def require_token(type) 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 render_not_authorized unless @user
end end
private :require_token private :require_token

View File

@ -1,7 +1,9 @@
# frozen_string_literal: true
class ProxyExercisesController < ApplicationController class ProxyExercisesController < ApplicationController
include CommonBehavior 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! def authorize!
authorize(@proxy_exercise || @proxy_exercises) authorize(@proxy_exercise || @proxy_exercises)
@ -9,7 +11,8 @@ class ProxyExercisesController < ApplicationController
private :authorize! private :authorize!
def clone 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) proxy_exercise.send(:generate_token)
if proxy_exercise.save 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))
@ -21,7 +24,7 @@ class ProxyExercisesController < ApplicationController
def create def create
myparams = proxy_exercise_params 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) @proxy_exercise = ProxyExercise.new(myparams)
authorize! authorize!
@ -39,7 +42,10 @@ class ProxyExercisesController < ApplicationController
end end
def proxy_exercise_params 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 end
private :proxy_exercise_params private :proxy_exercise_params
@ -64,17 +70,15 @@ class ProxyExercisesController < ApplicationController
def show def show
@search = @proxy_exercise.exercises.ransack @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 end
#we might want to think about auth here # we might want to think about auth here
def reload def reload; end
end
def update def update
myparams = proxy_exercise_params 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) update_and_respond(object: @proxy_exercise, params: myparams)
end end
end end

View File

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

View File

@ -5,7 +5,8 @@ class RequestForCommentsController < ApplicationController
before_action :require_user! before_action :require_user!
before_action :set_request_for_comment, only: %i[show mark_as_solved set_thank_you_note] before_action :set_request_for_comment, only: %i[show mark_as_solved set_thank_you_note]
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! def authorize!
authorize(@request_for_comments || @request_for_comment) authorize(@request_for_comments || @request_for_comment)
@ -91,7 +92,7 @@ class RequestForCommentsController < ApplicationController
@request_for_comment.thank_you_note = params[:note] @request_for_comment.thank_you_note = params[:note]
commenters = @request_for_comment.commenters 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| respond_to do |format|
if @request_for_comment.save 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. # Never trust parameters from the scary internet, only allow the white list through.
def request_for_comment_params 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. # 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 end
# The index page requires the grouping of the study groups # 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 # The study groups are grouped by the current study group and other study groups of the user
def set_study_group_grouping def set_study_group_grouping
current_study_group = StudyGroup.find_by(id: session[:study_group_id]) 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)], @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]] [t('request_for_comments.index.study_groups.my'), my_study_groups]]
end end

View File

@ -1,7 +1,10 @@
# frozen_string_literal: true
class SessionsController < ApplicationController class SessionsController < ApplicationController
include Lti 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) before_action(method_name, only: :create_through_lti)
end end

View File

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

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class StudyGroupsController < ApplicationController class StudyGroupsController < ApplicationController
include CommonBehavior include CommonBehavior
@ -20,7 +22,8 @@ class StudyGroupsController < ApplicationController
def update def update
myparams = study_group_params 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) myparams.delete(:study_group_membership_ids)
update_and_respond(object: @study_group, params: myparams) update_and_respond(object: @study_group, params: myparams)
end end
@ -30,7 +33,7 @@ class StudyGroupsController < ApplicationController
end end
def study_group_params 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 end
private :study_group_params private :study_group_params
@ -44,5 +47,4 @@ class StudyGroupsController < ApplicationController
authorize(@study_groups || @study_group) authorize(@study_groups || @study_group)
end end
private :authorize! private :authorize!
end end

View File

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

View File

@ -1,5 +1,6 @@
class SubscriptionsController < ApplicationController # frozen_string_literal: true
class SubscriptionsController < ApplicationController
def authorize! def authorize!
authorize(@subscription || @subscriptions) authorize(@subscription || @subscriptions)
end end
@ -21,9 +22,8 @@ class SubscriptionsController < ApplicationController
# DELETE /subscriptions/1 # DELETE /subscriptions/1
# DELETE /subscriptions/1.json # DELETE /subscriptions/1.json
def destroy def destroy
begin
@subscription = Subscription.find(params[:id]) @subscription = Subscription.find(params[:id])
rescue rescue StandardError
skip_authorization skip_authorization
respond_to do |format| respond_to do |format|
format.html { redirect_to request_for_comments_url, alert: t('subscriptions.subscription_not_existent') } format.html { redirect_to request_for_comments_url, alert: t('subscriptions.subscription_not_existent') }
@ -36,13 +36,12 @@ class SubscriptionsController < ApplicationController
if @subscription.save if @subscription.save
respond_to do |format| respond_to do |format|
format.html { redirect_to request_for_comment_url(rfc), notice: t('subscriptions.successfully_unsubscribed') } 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} format.json { render json: {message: t('subscriptions.successfully_unsubscribed')}, status: :ok }
end end
else else
respond_to do |format| respond_to do |format|
format.html { redirect_to request_for_comment_url(rfc), :flash => { :danger => t('shared.message_failure') } } 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} format.json { render json: {message: t('shared.message_failure')}, status: :internal_server_error }
end
end end
end end
end end
@ -56,7 +55,10 @@ class SubscriptionsController < ApplicationController
def subscription_params def subscription_params
current_user_id = current_user.try(:id) current_user_id = current_user.try(:id)
current_user_class_name = current_user.try(:class).try(:name) 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 end
private :subscription_params private :subscription_params
end end

View File

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

View File

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

View File

@ -115,7 +115,7 @@ class UserExerciseFeedbacksController < ApplicationController
end end
def uef_params 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? exercise_id = if params[:user_exercise_feedback].nil?
params[:exercise_id] params[:exercise_id]
@ -140,10 +140,8 @@ class UserExerciseFeedbacksController < ApplicationController
def validate_inputs(uef_params) def validate_inputs(uef_params)
if uef_params[:difficulty].to_i.negative? || uef_params[:difficulty].to_i >= comment_presets.size if uef_params[:difficulty].to_i.negative? || uef_params[:difficulty].to_i >= comment_presets.size
false false
elsif uef_params[:user_estimated_worktime].to_i.negative? || uef_params[:user_estimated_worktime].to_i >= time_presets.size
false
else else
true !(uef_params[:user_estimated_worktime].to_i.negative? || uef_params[:user_estimated_worktime].to_i >= time_presets.size)
end end
rescue StandardError rescue StandardError
false false

View File

@ -1,4 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module ActionCableHelper module ActionCableHelper
def trigger_rfc_action_cable def trigger_rfc_action_cable
Thread.new do Thread.new do
@ -9,7 +10,8 @@ module ActionCableHelper
type: :rfc, type: :rfc,
id: id, id: id,
html: ApplicationController.render(partial: 'request_for_comments/list_entry', html: ApplicationController.render(partial: 'request_for_comments/list_entry',
locals: {request_for_comment: self})) locals: {request_for_comment: self})
)
end end
ensure ensure
ActiveRecord::Base.connection_pool.release_connection ActiveRecord::Base.connection_pool.release_connection
@ -28,7 +30,8 @@ module ActionCableHelper
ActionCable.server.broadcast( ActionCable.server.broadcast(
"la_exercises_#{exercise_id}_channel_study_group_#{study_group_id}", "la_exercises_#{exercise_id}_channel_study_group_#{study_group_id}",
type: :working_times, type: :working_times,
working_time_data: exercise.get_working_times_for_study_group(study_group_id, user)) working_time_data: exercise.get_working_times_for_study_group(study_group_id, user)
)
end end
ensure ensure
ActiveRecord::Base.connection_pool.release_connection ActiveRecord::Base.connection_pool.release_connection

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module ExerciseHelper module ExerciseHelper
include LtiHelper include LtiHelper
@ -6,7 +8,7 @@ module ExerciseHelper
end end
def qa_js_tag def qa_js_tag
javascript_include_tag qa_url + "/assets/qa_api.js" javascript_include_tag "#{qa_url}/assets/qa_api.js"
end end
def qa_url def qa_url
@ -15,8 +17,6 @@ module ExerciseHelper
if enabled if enabled
config.read[:code_pilot][:url] config.read[:code_pilot][:url]
else
return nil
end end
end 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` require 'oauth/request_proxy/action_controller_request' # Rails 5 changed `Rack::Request` to `ActionDispatch::Request`
module LtiHelper module LtiHelper

View File

@ -16,7 +16,7 @@ class PagedownFormBuilder < ActionView::Helpers::FormBuilder
private private
def wmd_button_bar 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 end
def wmd_textarea def wmd_textarea
@ -27,9 +27,8 @@ class PagedownFormBuilder < ActionView::Helpers::FormBuilder
end end
def wmd_preview def wmd_preview
@template.content_tag :div, nil, @template.tag.div(nil, class: 'wmd-preview',
class: 'wmd-preview', id: "wmd-preview-#{base_id}")
id: "wmd-preview-#{base_id}"
end end
def show_wmd_preview? def show_wmd_preview?

View File

@ -1,5 +1,6 @@
module StatisticsHelper # frozen_string_literal: true
module StatisticsHelper
WORKING_TIME_DELTA_IN_SECONDS = 5.minutes WORKING_TIME_DELTA_IN_SECONDS = 5.minutes
WORKING_TIME_DELTA_IN_SQL_INTERVAL = "'0:05:00'" # yes, a string with quotes WORKING_TIME_DELTA_IN_SQL_INTERVAL = "'0:05:00'" # yes, a string with quotes
@ -8,18 +9,18 @@ module StatisticsHelper
{ {
key: 'users', key: 'users',
name: t('statistics.sections.users'), name: t('statistics.sections.users'),
entries: user_statistics entries: user_statistics,
}, },
{ {
key: 'exercises', key: 'exercises',
name: t('statistics.sections.exercises'), name: t('statistics.sections.exercises'),
entries: exercise_statistics entries: exercise_statistics,
}, },
{ {
key: 'request_for_comments', key: 'request_for_comments',
name: t('statistics.sections.request_for_comments'), name: t('statistics.sections.request_for_comments'),
entries: rfc_statistics entries: rfc_statistics,
} },
] ]
end end
@ -29,13 +30,13 @@ module StatisticsHelper
key: 'internal_users', key: 'internal_users',
name: t('activerecord.models.internal_user.other'), name: t('activerecord.models.internal_user.other'),
data: InternalUser.count, data: InternalUser.count,
url: internal_users_path url: internal_users_path,
}, },
{ {
key: 'external_users', key: 'external_users',
name: t('activerecord.models.external_user.other'), name: t('activerecord.models.external_user.other'),
data: ExternalUser.count, data: ExternalUser.count,
url: external_users_path url: external_users_path,
}, },
{ {
key: 'currently_active', key: 'currently_active',
@ -43,8 +44,8 @@ module StatisticsHelper
data: ExternalUser.joins(:submissions) data: ExternalUser.joins(:submissions)
.where(['submissions.created_at >= ?', DateTime.now - 5.minutes]) .where(['submissions.created_at >= ?', DateTime.now - 5.minutes])
.distinct('external_users.id').count, .distinct('external_users.id').count,
url: 'statistics/graphs' url: 'statistics/graphs',
} },
] ]
end end
@ -54,44 +55,45 @@ module StatisticsHelper
key: 'exercises', key: 'exercises',
name: t('activerecord.models.exercise.other'), name: t('activerecord.models.exercise.other'),
data: Exercise.count, data: Exercise.count,
url: exercises_path url: exercises_path,
}, },
{ {
key: 'average_submissions', key: 'average_submissions',
name: t('statistics.entries.exercises.average_number_of_submissions'), name: t('statistics.entries.exercises.average_number_of_submissions'),
data: (Submission.count.to_f / Exercise.count).round(2) data: (Submission.count.to_f / Exercise.count).round(2),
}, },
{ {
key: 'submissions_per_minute', key: 'submissions_per_minute',
name: t('statistics.entries.exercises.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), data: (Submission.where('created_at >= ?', DateTime.now - 1.hour).count.to_f / 60).round(2),
unit: '/min', unit: '/min',
url: statistics_graphs_path url: statistics_graphs_path,
}, },
{ {
key: 'autosaves_per_minute', key: 'autosaves_per_minute',
name: t('statistics.entries.exercises.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), data: (Submission.where('created_at >= ?',
unit: '/min' DateTime.now - 1.hour).where(cause: 'autosave').count.to_f / 60).round(2),
unit: '/min',
}, },
{ {
key: 'container_requests_per_minute', key: 'container_requests_per_minute',
name: t('statistics.entries.exercises.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), data: (Testrun.where('created_at >= ?', DateTime.now - 1.hour).count.to_f / 60).round(2),
unit: '/min' unit: '/min',
}, },
{ {
key: 'execution_environments', key: 'execution_environments',
name: t('activerecord.models.execution_environment.other'), name: t('activerecord.models.execution_environment.other'),
data: ExecutionEnvironment.count, data: ExecutionEnvironment.count,
url: execution_environments_path url: execution_environments_path,
}, },
{ {
key: 'exercise_collections', key: 'exercise_collections',
name: t('activerecord.models.exercise_collection.other'), name: t('activerecord.models.exercise_collection.other'),
data: ExerciseCollection.count, data: ExerciseCollection.count,
url: exercise_collections_path url: exercise_collections_path,
} },
] ]
end end
@ -100,8 +102,8 @@ module StatisticsHelper
{ {
key: 'comments', key: 'comments',
name: t('activerecord.models.comment.other'), name: t('activerecord.models.comment.other'),
data: Comment.count data: Comment.count,
} },
] ]
end end
@ -117,64 +119,68 @@ module StatisticsHelper
{ {
key: 'submissions_per_minute', key: 'submissions_per_minute',
name: t('statistics.entries.exercises.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), data: (Submission.where('created_at >= ?', DateTime.now - 1.hour).count.to_f / 60).round(2),
unit: '/min', unit: '/min',
axis: 'right' axis: 'right',
} },
] ]
end 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', key: 'rfcs',
name: t('activerecord.models.request_for_comment.other'), name: t('activerecord.models.request_for_comment.other'),
data: RequestForComment.in_range(from, to).count, data: RequestForComment.in_range(from, to).count,
url: request_for_comments_path url: request_for_comments_path,
}, },
{ {
key: 'percent_solved', key: 'percent_solved',
name: t('statistics.entries.request_for_comments.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), data: (100.0 / RequestForComment.in_range(from,
to).count * RequestForComment.in_range(from, to).where(solved: true).count).round(1),
unit: '%', unit: '%',
axis: 'right', axis: 'right',
url: statistics_graphs_path url: statistics_graphs_path,
}, },
{ {
key: 'percent_soft_solved', key: 'percent_soft_solved',
name: t('statistics.entries.request_for_comments.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), data: (100.0 / RequestForComment.in_range(from,
to).count * RequestForComment.in_range(from, to).unsolved.where(full_score_reached: true).count).round(1),
unit: '%', unit: '%',
axis: 'right', axis: 'right',
url: statistics_graphs_path url: statistics_graphs_path,
}, },
{ {
key: 'percent_unsolved', key: 'percent_unsolved',
name: t('statistics.entries.request_for_comments.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), data: (100.0 / RequestForComment.in_range(from,
to).count * RequestForComment.in_range(from, to).unsolved.count).round(1),
unit: '%', unit: '%',
axis: 'right', axis: 'right',
url: statistics_graphs_path url: statistics_graphs_path,
}, },
{ {
key: 'rfcs_with_comments', key: 'rfcs_with_comments',
name: t('statistics.entries.request_for_comments.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 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 "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, 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 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', key: 'rfcs',
name: t('activerecord.models.request_for_comment.other'), name: t('activerecord.models.request_for_comment.other'),
data: RequestForComment.in_range(from, to) data: RequestForComment.in_range(from, to)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"") .select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key') .group('key').order('key'),
}, },
{ {
key: 'rfcs_solved', key: 'rfcs_solved',
@ -182,7 +188,7 @@ module StatisticsHelper
data: RequestForComment.in_range(from, to) data: RequestForComment.in_range(from, to)
.where(solved: true) .where(solved: true)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"") .select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key') .group('key').order('key'),
}, },
{ {
key: 'rfcs_soft_solved', key: 'rfcs_soft_solved',
@ -190,19 +196,19 @@ module StatisticsHelper
data: RequestForComment.in_range(from, to).unsolved data: RequestForComment.in_range(from, to).unsolved
.where(full_score_reached: true) .where(full_score_reached: true)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"") .select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key') .group('key').order('key'),
}, },
{ {
key: 'rfcs_unsolved', key: 'rfcs_unsolved',
name: t('statistics.entries.request_for_comments.percent_unsolved'), name: t('statistics.entries.request_for_comments.percent_unsolved'),
data: RequestForComment.in_range(from, to).unsolved data: RequestForComment.in_range(from, to).unsolved
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"") .select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key') .group('key').order('key'),
} },
] ]
end 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', key: 'active',
@ -210,7 +216,7 @@ module StatisticsHelper
data: ExternalUser.joins(:submissions) data: ExternalUser.joins(:submissions)
.where(submissions: {created_at: from..to}) .where(submissions: {created_at: from..to})
.select("date_trunc('#{interval}', submissions.created_at) AS \"key\", count(distinct external_users.id) AS \"value\"") .select("date_trunc('#{interval}', submissions.created_at) AS \"key\", count(distinct external_users.id) AS \"value\"")
.group('key').order('key') .group('key').order('key'),
}, },
{ {
key: 'submissions', key: 'submissions',
@ -218,9 +224,8 @@ module StatisticsHelper
data: Submission.where(created_at: from..to) data: Submission.where(created_at: from..to)
.select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"") .select("date_trunc('#{interval}', created_at) AS \"key\", count(id) AS \"value\"")
.group('key').order('key'), .group('key').order('key'),
axis: 'right' axis: 'right',
} },
] ]
end end
end end

View File

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

View File

@ -1,8 +1,9 @@
class UserMailer < ActionMailer::Base # frozen_string_literal: true
class UserMailer < ApplicationMailer
def mail(*args) def mail(*args)
# used to prevent the delivery to pseudonymous users without a valid email address # 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 end
def activation_needed_email(user) 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) mail(subject: t('mailers.user_mailer.activation_needed.subject'), to: user.email)
end end
def activation_success_email(*) def activation_success_email(*); end
end
def reset_password_email(user) def reset_password_email(user)
@reset_password_url = reset_password_internal_user_url(user, token: user.reset_password_token) @reset_password_url = reset_password_internal_user_url(user, token: user.reset_password_token)
@ -19,12 +19,15 @@ class UserMailer < ActionMailer::Base
end end
def got_new_comment(comment, request_for_comment, commenting_user) 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 @receiver_displayname = request_for_comment.user.displayname
@commenting_user_displayname = commenting_user.displayname @commenting_user_displayname = commenting_user.displayname
@comment_text = comment.text @comment_text = comment.text
@rfc_link = request_for_comment_url(request_for_comment) @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 end
def got_new_comment_for_subscription(comment, subscription, from_user) def got_new_comment_for_subscription(comment, subscription, from_user)
@ -33,7 +36,10 @@ class UserMailer < ActionMailer::Base
@comment_text = comment.text @comment_text = comment.text
@rfc_link = request_for_comment_url(subscription.request_for_comment) @rfc_link = request_for_comment_url(subscription.request_for_comment)
@unsubscribe_link = unsubscribe_subscription_url(subscription) @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 end
def send_thank_you_note(request_for_comments, receiver) def send_thank_you_note(request_for_comments, receiver)

View File

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

View File

@ -1,25 +1,25 @@
require File.expand_path('../../../uploaders/file_uploader', __FILE__) # frozen_string_literal: true
require File.expand_path('../../../../lib/active_model/validations/boolean_presence_validator', __FILE__)
require File.expand_path('../../uploaders/file_uploader', __dir__)
require File.expand_path('../../../lib/active_model/validations/boolean_presence_validator', __dir__)
module CodeOcean module CodeOcean
class FileNameValidator < ActiveModel::Validator class FileNameValidator < ActiveModel::Validator
def validate(record) def validate(record)
existing_files = File.where(name: record.name, path: record.path, file_type_id: record.file_type_id, 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 context_id: record.context_id, context_type: record.context_type).to_a
unless existing_files.empty? if !existing_files.empty? && (!record.context.is_a?(Exercise) || record.context.new_record?)
if (not record.context.is_a?(Exercise)) || (record.context.new_record?)
record.errors[:base] << 'Duplicate' record.errors[:base] << 'Duplicate'
end end
end end
end end
end
class File < ApplicationRecord class File < ApplicationRecord
include DefaultValues include DefaultValues
DEFAULT_WEIGHT = 1.0 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] TEACHER_DEFINED_ROLES = ROLES - %w[user_defined_file]
after_initialize :set_default_values after_initialize :set_default_values
@ -29,13 +29,13 @@ module CodeOcean
belongs_to :context, polymorphic: true belongs_to :context, polymorphic: true
belongs_to :file, class_name: 'CodeOcean::File', optional: true # This is only required for submissions and is validated below 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 belongs_to :file_type
has_many :files, class_name: 'CodeOcean::File' has_many :files, class_name: 'CodeOcean::File'
has_many :testruns has_many :testruns
has_many :comments has_many :comments
alias_method :descendants, :files alias descendants files
mount_uploader :native_file, FileUploader mount_uploader :native_file, FileUploader
@ -61,7 +61,7 @@ module CodeOcean
validates :weight, absence: true, unless: :teacher_defined_assessment? validates :weight, absence: true, unless: :teacher_defined_assessment?
validates :file, presence: true if :context.is_a?(Submission) 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| ROLES.each do |role|
define_method("#{role}?") { self.role == role } define_method("#{role}?") { self.role == role }
@ -94,7 +94,12 @@ module CodeOcean
end end
def hash_content 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 end
private :hash_content private :hash_content
@ -108,7 +113,7 @@ module CodeOcean
end end
def set_ancestor_values 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)) send(:"#{attribute}=", ancestor.send(attribute))
end end
end end

View File

@ -5,9 +5,7 @@ class CodeharborLink < ApplicationRecord
validates :check_uuid_url, presence: true validates :check_uuid_url, presence: true
validates :api_key, 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 delegate :to_s, to: :id
id.to_s
end
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class Event < ApplicationRecord class Event < ApplicationRecord
belongs_to :user, polymorphic: true belongs_to :user, polymorphic: true
belongs_to :exercise 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 class ExecutionEnvironment < ApplicationRecord
include Creation include Creation
@ -17,7 +19,8 @@ class ExecutionEnvironment < ApplicationRecord
validate :valid_test_setup? validate :valid_test_setup?
validate :working_docker_image?, if: :validate_docker_image? validate :working_docker_image?, if: :validate_docker_image?
validates :docker_image, presence: true 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 :network_enabled, boolean_presence: true
validates :name, presence: true validates :name, presence: true
validates :permitted_execution_time, numericality: {only_integer: true}, presence: true validates :permitted_execution_time, numericality: {only_integer: true}, presence: true
@ -35,7 +38,9 @@ class ExecutionEnvironment < ApplicationRecord
def valid_test_setup? def valid_test_setup?
if test_command? ^ testing_framework? 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
end end
private :valid_test_setup? private :valid_test_setup?
@ -46,11 +51,11 @@ class ExecutionEnvironment < ApplicationRecord
private :validate_docker_image? private :validate_docker_image?
def working_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) output = DockerClient.new(execution_environment: self).execute_arbitrary_command(VALIDATION_COMMAND)
errors.add(:docker_image, "error: #{output[:stderr]}") if output[:stderr].present? errors.add(:docker_image, "error: #{output[:stderr]}") if output[:stderr].present?
rescue DockerClient::Error => error rescue DockerClient::Error => e
errors.add(:docker_image, "error: #{error}") errors.add(:docker_image, "error: #{e}")
end end
private :working_docker_image? private :working_docker_image?
end end

View File

@ -41,7 +41,7 @@ class Exercise < ApplicationRecord
validates :unpublished, boolean_presence: true validates :unpublished, boolean_presence: true
validates :title, presence: true validates :title, presence: true
validates :token, presence: true, uniqueness: true validates :token, presence: true, uniqueness: true
validates_uniqueness_of :uuid, if: -> { uuid.present? } validates :uuid, uniqueness: {if: -> { uuid.present? }}
@working_time_statistics = nil @working_time_statistics = nil
attr_reader :working_time_statistics attr_reader :working_time_statistics
@ -57,10 +57,10 @@ class Exercise < ApplicationRecord
end end
def finishers_percentage def finishers_percentage
if users.distinct.count != 0 if users.distinct.count.zero?
(100.0 / users.distinct.count * finishers.count).round(2)
else
0 0
else
(100.0 / users.distinct.count * finishers.count).round(2)
end end
end end
@ -73,11 +73,11 @@ class Exercise < ApplicationRecord
def average_number_of_submissions def average_number_of_submissions
user_count = internal_users.distinct.count + external_users.distinct.count 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 end
def time_maximum_score(user) 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 rescue StandardError
Time.zone.at(0) Time.zone.at(0)
end end
@ -107,7 +107,7 @@ class Exercise < ApplicationRecord
end end
def study_group_working_time_query(exercise_id, study_group_id, additional_filter) def study_group_working_time_query(exercise_id, study_group_id, additional_filter)
''" "
WITH working_time_between_submissions AS ( WITH working_time_between_submissions AS (
SELECT submissions.user_id, SELECT submissions.user_id,
submissions.user_type, submissions.user_type,
@ -200,7 +200,7 @@ class Exercise < ApplicationRecord
FROM working_times_with_index FROM working_times_with_index
JOIN internal_users ON user_type = 'InternalUser' AND user_id = internal_users.id JOIN internal_users ON user_type = 'InternalUser' AND user_id = internal_users.id
ORDER BY index, score ASC; ORDER BY index, score ASC;
"'' "
end end
def get_working_times_for_study_group(study_group_id, user = nil) def get_working_times_for_study_group(study_group_id, user = nil)
@ -217,7 +217,8 @@ class Exercise < ApplicationRecord
results = ActiveRecord::Base.transaction do results = ActiveRecord::Base.transaction do
self.class.connection.execute("SET LOCAL intervalstyle = 'postgres'") 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 bucket = if maximum_score > 0.0 && tuple['score'] <= maximum_score
(tuple['score'] / maximum_score * max_bucket).round (tuple['score'] / maximum_score * max_bucket).round
else else
@ -230,11 +231,12 @@ class Exercise < ApplicationRecord
user_progress[bucket][tuple['index']] = tuple['working_time_per_score'] 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[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
end end
if results.ntuples > 0 if results.ntuples.positive?
first_index = results[0]['index'] first_index = results[0]['index']
last_index = results[results.ntuples - 1]['index'] last_index = results[results.ntuples - 1]['index']
buckets = last_index - first_index buckets = last_index - first_index
@ -247,9 +249,9 @@ class Exercise < ApplicationRecord
end end
def get_quantiles(quantiles) def get_quantiles(quantiles)
quantiles_str = '[' + quantiles.join(',') + ']' quantiles_str = "[#{quantiles.join(',')}]"
result = ActiveRecord::Base.transaction do result = ActiveRecord::Base.transaction do
self.class.connection.execute(''" self.class.connection.execute("
SET LOCAL intervalstyle = 'iso_8601'; SET LOCAL intervalstyle = 'iso_8601';
WITH working_time AS WITH working_time AS
( (
@ -356,14 +358,14 @@ class Exercise < ApplicationRecord
exercise_id ) exercise_id )
SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time)) SELECT unnest(percentile_cont(array#{quantiles_str}) within GROUP (ORDER BY working_time))
FROM result FROM result
"'') ")
end
if result.count > 0
begin
quantiles.each_with_index.map { |_q, i| ActiveSupport::Duration.parse(result[i]['unnest']).to_f }
end end
if result.count.positive?
quantiles.each_with_index.map {|_q, i| ActiveSupport::Duration.parse(result[i]['unnest']).to_f }
else else
quantiles.map { |_q| 0 } quantiles.map {|_q| 0 }
end end
end end
@ -380,11 +382,11 @@ class Exercise < ApplicationRecord
def average_working_time def average_working_time
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
self.class.connection.execute("SET LOCAL intervalstyle = 'postgres'") self.class.connection.execute("SET LOCAL intervalstyle = 'postgres'")
self.class.connection.execute(''" self.class.connection.execute("
SELECT avg(working_time) as average_time SELECT avg(working_time) as average_time
FROM FROM
(#{user_working_time_query}) AS baz; (#{user_working_time_query}) AS baz;
"'').first['average_time'] ").first['average_time']
end end
end end
@ -397,7 +399,7 @@ class Exercise < ApplicationRecord
user_type = user.external_user? ? 'ExternalUser' : 'InternalUser' user_type = user.external_user? ? 'ExternalUser' : 'InternalUser'
begin begin
result = ActiveRecord::Base.transaction do result = ActiveRecord::Base.transaction do
self.class.connection.execute(''" self.class.connection.execute("
SET LOCAL intervalstyle = 'iso_8601'; SET LOCAL intervalstyle = 'iso_8601';
WITH WORKING_TIME AS WITH WORKING_TIME AS
(SELECT user_id, (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 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 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 WHERE f.user_id = e.id GROUP BY e.external_id, f.user_id, exercise_id
"'') ")
end end
ActiveSupport::Duration.parse(result.first['working_time']).to_f ActiveSupport::Duration.parse(result.first['working_time']).to_f
rescue StandardError rescue StandardError
@ -458,8 +460,8 @@ class Exercise < ApplicationRecord
def duplicate(attributes = {}) def duplicate(attributes = {})
exercise = dup exercise = dup
exercise.attributes = attributes exercise.attributes = attributes
exercise_tags.each { |et| exercise.exercise_tags << et.dup } exercise_tags.each {|et| exercise.exercise_tags << et.dup }
files.each { |file| exercise.files << file.dup } files.each {|file| exercise.files << file.dup }
exercise exercise
end end
@ -490,7 +492,7 @@ class Exercise < ApplicationRecord
self.attributes = { self.attributes = {
title: task_node.xpath('p:meta-data/p:title/text()')[0].content, title: task_node.xpath('p:meta-data/p:title/text()')[0].content,
description: description, description: description,
instructions: description instructions: description,
} }
task_node.xpath('p:files/p:file').all? do |file| task_node.xpath('p:files/p:file').all? do |file|
file_name_split = file.xpath('@filename').first.value.split('.') file_name_split = file.xpath('@filename').first.value.split('.')
@ -504,9 +506,9 @@ class Exercise < ApplicationRecord
hidden: file_class == 'internal', hidden: file_class == 'internal',
role: role, role: role,
feedback_message: role == 'teacher_defined_test' ? feedback_message_nodes.first.content : nil, 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}" file_extension: ".#{file_name_split.second}"
).take ),
}) })
end end
self.execution_environment_id = 1 self.execution_environment_id = 1
@ -521,7 +523,7 @@ class Exercise < ApplicationRecord
if user if user
# FIXME: where(user: user) will not work here! # FIXME: where(user: user) will not work here!
begin 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 rescue StandardError
0 0
end end
@ -539,7 +541,8 @@ class Exercise < ApplicationRecord
end end
def finishers 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 end
def set_default_values def set_default_values
@ -552,18 +555,25 @@ class Exercise < ApplicationRecord
end end
def valid_main_file? 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 end
private :valid_main_file? private :valid_main_file?
def valid_submission_deadlines? def valid_submission_deadlines?
return unless submission_deadline.present? || late_submission_deadline.present? 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? && if submission_deadline.present? && late_submission_deadline.present? &&
late_submission_deadline < submission_deadline 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
end end
private :valid_submission_deadlines? private :valid_submission_deadlines?

View File

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

View File

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

View File

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

View File

@ -13,6 +13,12 @@ class ExerciseTip < ApplicationRecord
def tip_chain? def tip_chain?
# Ensure each referenced parent exercise tip is set for this exercise # 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
end end

View File

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

View File

@ -1,10 +1,9 @@
# frozen_string_literal: true
class FileTemplate < ApplicationRecord class FileTemplate < ApplicationRecord
belongs_to :file_type belongs_to :file_type
def to_s def to_s
name name
end end
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 class FileType < ApplicationRecord
include Creation include Creation
include DefaultValues include DefaultValues
AUDIO_FILE_EXTENSIONS = %w(.aac .flac .m4a .mp3 .ogg .wav .wma) AUDIO_FILE_EXTENSIONS = %w[.aac .flac .m4a .mp3 .ogg .wav .wma].freeze
IMAGE_FILE_EXTENSIONS = %w(.bmp .gif .jpeg .jpg .png) IMAGE_FILE_EXTENSIONS = %w[.bmp .gif .jpeg .jpg .png].freeze
VIDEO_FILE_EXTENSIONS = %w(.avi .flv .mkv .mp4 .m4v .ogv .webm) VIDEO_FILE_EXTENSIONS = %w[.avi .flv .mkv .mp4 .m4v .ogv .webm].freeze
after_initialize :set_default_values after_initialize :set_default_values
@ -21,7 +23,7 @@ class FileType < ApplicationRecord
validates :name, presence: true validates :name, presence: true
validates :renderable, boolean_presence: true validates :renderable, boolean_presence: true
[:audio, :image, :video].each do |type| %i[audio image video].each do |type|
define_method("#{type}?") do define_method("#{type}?") do
self.class.const_get("#{type.upcase}_FILE_EXTENSIONS").include?(file_extension) self.class.const_get("#{type.upcase}_FILE_EXTENSIONS").include?(file_extension)
end end

View File

@ -1,5 +1,6 @@
class InternalUser < User # frozen_string_literal: true
class InternalUser < User
authenticates_with_sorcery! authenticates_with_sorcery!
validates :email, presence: true, uniqueness: true validates :email, presence: true, uniqueness: true
@ -22,5 +23,4 @@ class InternalUser < User
def displayname def displayname
name name
end end
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 :user_exercise_interventions
has_many :users, through: :user_exercise_interventions, source_type: 'ExternalUser' has_many :users, through: :user_exercise_interventions, source_type: 'ExternalUser'
@ -8,9 +9,8 @@ class Intervention < ApplicationRecord
end end
def self.createDefaultInterventions def self.createDefaultInterventions
%w(BreakIntervention QuestionIntervention).each do |name| %w[BreakIntervention QuestionIntervention].each do |name|
Intervention.find_or_create_by(name: name) Intervention.find_or_create_by(name: name)
end end
end end
end end

View File

@ -1,9 +1,11 @@
class LtiParameter < ApplicationRecord # frozen_string_literal: true
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?, -> { 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'") where("lti_parameters.lti_parameters ? 'lis_outcome_service_url'")
} }
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class ProxyExercise < ApplicationRecord class ProxyExercise < ApplicationRecord
include Creation include Creation
include DefaultValues include DefaultValues
@ -41,45 +43,44 @@ class ProxyExercise < ApplicationRecord
def get_matching_exercise(user) def get_matching_exercise(user)
assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first assigned_user_proxy_exercise = user_proxy_exercise_exercises.where(user: user).first
recommended_exercise = if assigned_user_proxy_exercise
if (assigned_user_proxy_exercise) Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}")
Rails.logger.debug("retrieved assigned exercise for user #{user.id}: Exercise #{assigned_user_proxy_exercise.exercise}" )
assigned_user_proxy_exercise.exercise assigned_user_proxy_exercise.exercise
else else
Rails.logger.debug("find new matching exercise for user #{user.id}" ) Rails.logger.debug("find new matching exercise for user #{user.id}")
matching_exercise = matching_exercise =
begin begin
find_matching_exercise(user) find_matching_exercise(user)
rescue => e #fallback rescue StandardError => e # fallback
Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$!}" ) Rails.logger.error("finding matching exercise failed. Fall back to random exercise! Error: #{$ERROR_INFO}")
@reason[:reason] = "fallback because of error" @reason[:reason] = 'fallback because of error'
@reason[:error] = "#{$!}:\n\t#{e.backtrace.join("\n\t")}" @reason[:error] = "#{$ERROR_INFO}:\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. exercises.where('expected_difficulty > 1').sample # difficulty should be > 1 to prevent dummy exercise from being chosen.
end end
user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user, exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json) user.user_proxy_exercise_exercises << UserProxyExerciseExercise.create(user: user,
exercise: matching_exercise, proxy_exercise: self, reason: @reason.to_json)
matching_exercise matching_exercise
end end
recommended_exercise
end end
def find_matching_exercise(user) def find_matching_exercise(user)
exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map{|s| s.exercise}.uniq.compact exercises_user_has_accessed = user.submissions.where("cause IN ('submit','assess')").map(&:exercise).uniq.compact
tags_user_has_seen = exercises_user_has_accessed.map{|ex| ex.tags}.uniq.flatten tags_user_has_seen = exercises_user_has_accessed.map(&:tags).uniq.flatten
Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map{|e|e.id}.join(",")}") Rails.logger.debug("exercises_user_has_accessed #{exercises_user_has_accessed.map(&:id).join(',')}")
# find exercises # find exercises
potential_recommended_exercises = [] potential_recommended_exercises = []
exercises.where("expected_difficulty >= 1").each do |ex| exercises.where('expected_difficulty >= 1').find_each do |ex|
## find exercises which have only tags the user has already seen ## find exercises which have only tags the user has already seen
if (ex.tags - tags_user_has_seen).empty? if (ex.tags - tags_user_has_seen).empty?
potential_recommended_exercises << ex potential_recommended_exercises << ex
end end
end end
Rails.logger.debug("potential_recommended_exercises: #{potential_recommended_exercises.map{|e|e.id}}") 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 all exercises contain tags which the user has never seen, recommend easiest exercise
if potential_recommended_exercises.empty? if potential_recommended_exercises.empty?
Rails.logger.debug("matched easiest exercise in pool") Rails.logger.debug('matched easiest exercise in pool')
@reason[:reason] = "easiest exercise in pool. empty potential exercises" @reason[:reason] = 'easiest exercise in pool. empty potential exercises'
select_easiest_exercise(exercises) select_easiest_exercise(exercises)
else else
select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises) select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises)
@ -90,11 +91,11 @@ class ProxyExercise < ApplicationRecord
def select_best_matching_exercise(user, exercises_user_has_accessed, potential_recommended_exercises) 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) 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("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}}") 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_user = topic_knowledge_user_and_max[:user_topic_knowledge]
topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge] topic_knowledge_max = topic_knowledge_user_and_max[:max_topic_knowledge]
current_users_knowledge_lack = {} current_users_knowledge_lack = {}
topic_knowledge_max.keys.each do |tag| topic_knowledge_max.each_key do |tag|
current_users_knowledge_lack[tag] = topic_knowledge_user[tag] / topic_knowledge_max[tag] current_users_knowledge_lack[tag] = topic_knowledge_user[tag] / topic_knowledge_max[tag]
end end
@ -104,7 +105,9 @@ class ProxyExercise < ApplicationRecord
relative_knowledge_improvement[potex] = 0.0 relative_knowledge_improvement[potex] = 0.0
Rails.logger.debug("review potential exercise #{potex.id}") Rails.logger.debug("review potential exercise #{potex.id}")
tags.each do |tag| 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 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 max_topic_knowledge_ratio = potex.expected_difficulty * tag_ratio
old_relative_loss_tag = topic_knowledge_user[tag] / topic_knowledge_max[tag] 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) new_relative_loss_tag = topic_knowledge_user[tag] / (topic_knowledge_max[tag] + max_topic_knowledge_ratio)
@ -113,24 +116,26 @@ class ProxyExercise < ApplicationRecord
end end
end end
highest_difficulty_user_has_accessed = exercises_user_has_accessed.map{|e| e.expected_difficulty}.sort.last || 0 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) best_matching_exercise = find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed)
@reason[:reason] = "best matching exercise" @reason[:reason] = 'best matching exercise'
@reason[:highest_difficulty_user_has_accessed] = highest_difficulty_user_has_accessed @reason[:highest_difficulty_user_has_accessed] = highest_difficulty_user_has_accessed
@reason[:current_users_knowledge_lack] = current_users_knowledge_lack @reason[:current_users_knowledge_lack] = current_users_knowledge_lack
@reason[:relative_knowledge_improvement] = relative_knowledge_improvement @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('current users knowledge loss: ' + current_users_knowledge_lack.map do |k, v|
Rails.logger.debug("relative improvements #{relative_knowledge_improvement.map{|k,v| k.id.to_s + ':' + v.to_s}}") "#{k} => #{v}"
end.to_s)
Rails.logger.debug("relative improvements #{relative_knowledge_improvement.map {|k, v| "#{k.id}:#{v}" }}")
best_matching_exercise best_matching_exercise
end end
private :select_best_matching_exercise private :select_best_matching_exercise
def find_best_exercise(relative_knowledge_improvement, highest_difficulty_user_has_accessed) 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}") 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 = relative_knowledge_improvement.sort_by {|_k, v| v }.reverse
sorted_exercises.each do |ex,diff| sorted_exercises.each do |ex, _diff|
Rails.logger.debug("review exercise #{ex.id} diff: #{ex.expected_difficulty}") Rails.logger.debug("review exercise #{ex.id} diff: #{ex.expected_difficulty}")
if (ex.expected_difficulty - highest_difficulty_user_has_accessed) <= 1 if (ex.expected_difficulty - highest_difficulty_user_has_accessed) <= 1
Rails.logger.debug("matched exercise #{ex.id}") Rails.logger.debug("matched exercise #{ex.id}")
@ -139,7 +144,7 @@ class ProxyExercise < ApplicationRecord
Rails.logger.debug("exercise #{ex.id} is too difficult") Rails.logger.debug("exercise #{ex.id} is too difficult")
end end
end end
easiest_exercise = sorted_exercises.min_by{|k,v| v}.first easiest_exercise = sorted_exercises.min_by {|_k, v| v }.first
Rails.logger.debug("no match, select easiest exercise as fallback #{easiest_exercise.id}") Rails.logger.debug("no match, select easiest exercise as fallback #{easiest_exercise.id}")
easiest_exercise easiest_exercise
end end
@ -148,28 +153,28 @@ class ProxyExercise < ApplicationRecord
# [score][quantile] # [score][quantile]
def scoring_matrix def scoring_matrix
[ [
[0 ,0 ,0 ,0 ,0 ], [0, 0, 0, 0, 0],
[0.2,0.2,0.2,0.2,0.1], [0.2, 0.2, 0.2, 0.2, 0.1],
[0.5,0.5,0.4,0.4,0.3], [0.5, 0.5, 0.4, 0.4, 0.3],
[0.6,0.6,0.5,0.5,0.4], [0.6, 0.6, 0.5, 0.5, 0.4],
[1 ,1 ,0.9,0.8,0.7], [1, 1, 0.9, 0.8, 0.7],
] ]
end end
def scoring_matrix_quantiles def scoring_matrix_quantiles
[0.2,0.4,0.6,0.8] [0.2, 0.4, 0.6, 0.8]
end end
private :scoring_matrix_quantiles private :scoring_matrix_quantiles
def score(user, ex) def score(user, ex)
max_score = ex.maximum_score.to_f max_score = ex.maximum_score.to_f
if max_score <= 0 if max_score <= 0
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: score: 0" ) Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: score: 0")
return 0.0 return 0.0
end end
points_ratio = ex.maximum_score(user) / max_score points_ratio = ex.maximum_score(user) / max_score
if points_ratio == 0.0 if points_ratio == 0.0
Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: points_ratio=#{points_ratio} score: 0" ) Rails.logger.debug("scoring user #{user.id} for exercise #{ex.id}: points_ratio=#{points_ratio} score: 0")
return 0.0 return 0.0
elsif points_ratio > 1.0 elsif points_ratio > 1.0
points_ratio = 1.0 # The score of the exercise was adjusted and is now lower than it was points_ratio = 1.0 # The score of the exercise was adjusted and is now lower than it was
@ -187,7 +192,8 @@ class ProxyExercise < ApplicationRecord
Rails.logger.debug( Rails.logger.debug(
"scoring user #{user.id} exercise #{ex.id}: worktime #{working_time_user}, points: #{points_ratio}" \ "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} " \ "(index #{points_ratio_index}) quantiles #{quantiles_working_time} placed into quantile index #{quantile_index} " \
"score: #{scoring_matrix[points_ratio_index][quantile_index]}") "score: #{scoring_matrix[points_ratio_index][quantile_index]}"
)
scoring_matrix[points_ratio_index][quantile_index] scoring_matrix[points_ratio_index][quantile_index]
end end
private :score private :score
@ -201,18 +207,22 @@ class ProxyExercise < ApplicationRecord
all_used_tags_with_count[t] += 1 all_used_tags_with_count[t] += 1
end end
end end
tags_counter = all_used_tags_with_count.keys.map{|tag| [tag,0]}.to_h tags_counter = all_used_tags_with_count.keys.index_with {|_tag| 0 }
topic_knowledge_loss_user = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h topic_knowledge_loss_user = all_used_tags_with_count.keys.index_with {|_t| 0 }
topic_knowledge_max = all_used_tags_with_count.keys.map{|t| [t, 0]}.to_h 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 = exercises.sort_by {|ex| ex.time_maximum_score(user) }
exercises_sorted.each do |ex| exercises_sorted.each do |ex|
Rails.logger.debug("exercise: #{ex.id}: #{ex}") Rails.logger.debug("exercise: #{ex.id}: #{ex}")
user_score_factor = score(user, ex) user_score_factor = score(user, ex)
ex.tags.each do |t| ex.tags.each do |t|
tags_counter[t] += 1 tags_counter[t] += 1
tag_diminishing_return_factor = tag_diminishing_return_function(tags_counter[t], all_used_tags_with_count[t]) 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 tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0) do |sum, et|
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 }}") 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 #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}")
Rails.logger.debug("tag_ratio #{tag_ratio}") Rails.logger.debug("tag_ratio #{tag_ratio}")
topic_knowledge_ratio = ex.expected_difficulty * tag_ratio topic_knowledge_ratio = ex.expected_difficulty * tag_ratio
@ -232,5 +242,4 @@ class ProxyExercise < ApplicationRecord
def select_easiest_exercise(exercises) def select_easiest_exercise(exercises)
exercises.order(:expected_difficulty).first exercises.order(:expected_difficulty).first
end end
end end

View File

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

View File

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

View File

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

View File

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

View File

@ -4,5 +4,5 @@ class StudyGroupMembership < ApplicationRecord
belongs_to :user, polymorphic: true belongs_to :user, polymorphic: true
belongs_to :study_group 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 end

View File

@ -1,9 +1,12 @@
# frozen_string_literal: true
class Submission < ApplicationRecord class Submission < ApplicationRecord
include Context include Context
include Creation include Creation
include ActionCableHelper 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}' FILENAME_URL_PLACEHOLDER = '{filename}'
MAX_COMMENTS_ON_RECOMMENDED_RFC = 5 MAX_COMMENTS_ON_RECOMMENDED_RFC = 5
OLDEST_RFC_TO_SHOW = 6.months OLDEST_RFC_TO_SHOW = 6.months
@ -15,17 +18,27 @@ class Submission < ApplicationRecord
has_many :structured_errors has_many :structured_errors
has_many :comments, through: :files 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 :external_users, lambda {
belongs_to :internal_users, -> { where(submissions: {user_type: 'InternalUser'}).includes(:submissions) }, foreign_key: :user_id, class_name: 'InternalUser', optional: true 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 delegate :execution_environment, to: :exercise
scope :final, -> { where(cause: %w[submit remoteSubmit]) } scope :final, -> { where(cause: %w[submit remoteSubmit]) }
scope :intermediate, -> { where.not(cause: 'submit') } 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 :before_deadline, lambda {
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)') } joins(:exercise).where('submissions.updated_at <= exercises.submission_deadline OR exercises.submission_deadline IS NULL')
scope :after_late_deadline, -> { joins(:exercise).where('submissions.updated_at > exercises.late_submission_deadline') } }
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 } scope :latest, -> { order(updated_at: :desc).first }
@ -36,7 +49,6 @@ class Submission < ApplicationRecord
# after_save :trigger_working_times_action_cable # after_save :trigger_working_times_action_cable
def build_files_hash(files, attribute) def build_files_hash(files, attribute)
files.map(&attribute.to_proc).zip(files).to_h files.map(&attribute.to_proc).zip(files).to_h
end end
@ -57,12 +69,12 @@ class Submission < ApplicationRecord
# expects the full file path incl. file extension # expects the full file path incl. file extension
# Caution: There must be no unnecessary path prefix included. # Caution: There must be no unnecessary path prefix included.
# Use `file.ext` rather than `./file.ext` # Use `file.ext` rather than `./file.ext`
collect_files.detect { |file| file.filepath == file_path } collect_files.detect {|file| file.filepath == file_path }
end end
def normalized_score def normalized_score
::NewRelic::Agent.add_custom_attributes({unnormalized_score: 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 score / exercise.maximum_score
else else
0 0
@ -119,6 +131,8 @@ class Submission < ApplicationRecord
end end
def unsolved_rfc 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
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class Subscription < ApplicationRecord class Subscription < ApplicationRecord
belongs_to :user, polymorphic: true belongs_to :user, polymorphic: true
belongs_to :request_for_comment 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 :exercise_tags
has_many :exercises, through: :exercise_tags has_many :exercises, through: :exercise_tags
validates_uniqueness_of :name validates :name, uniqueness: true
def destroy def destroy
if (can_be_destroyed?) if can_be_destroyed?
super super
end end
end end
def can_be_destroyed? def can_be_destroyed?
!exercises.any? exercises.none?
end end
def to_s def to_s
name name
end end
end end

View File

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

View File

@ -6,11 +6,16 @@ class Tip < ApplicationRecord
has_many :exercise_tips has_many :exercise_tips
has_many :exercises, through: :exercise_tips has_many :exercises, through: :exercise_tips
belongs_to :file_type, optional: true belongs_to :file_type, optional: true
validates_presence_of :file_type, if: :example? validates :file_type, presence: {if: :example?}
validate :content? validate :content?
def 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 end
def to_s def to_s

View File

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

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class UserExerciseFeedback < ApplicationRecord class UserExerciseFeedback < ApplicationRecord
include Creation include Creation
@ -5,17 +7,17 @@ class UserExerciseFeedback < ApplicationRecord
belongs_to :submission, optional: true belongs_to :submission, optional: true
has_one :execution_environment, through: :exercise 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 :intermediate, -> { where.not(normalized_score: 1.00) }
scope :final, -> { where(normalized_score: 1.00) } scope :final, -> { where(normalized_score: 1.00) }
def to_s def to_s
"User Exercise Feedback" 'User Exercise Feedback'
end end
def anomaly_notification def anomaly_notification
AnomalyNotification.where({exercise_id: exercise.id, user_id: user_id, user_type: user_type}) 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
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 :user, polymorphic: true
belongs_to :intervention belongs_to :intervention
belongs_to :exercise belongs_to :exercise
@ -7,5 +8,4 @@ class UserExerciseIntervention < ApplicationRecord
validates :user, presence: true validates :user, presence: true
validates :exercise, presence: true validates :exercise, presence: true
validates :intervention, 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 :user, polymorphic: true
belongs_to :exercise belongs_to :exercise
belongs_to :proxy_exercise belongs_to :proxy_exercise
@ -9,6 +10,5 @@ class UserProxyExerciseExercise < ApplicationRecord
validates :exercise_id, presence: true validates :exercise_id, presence: true
validates :proxy_exercise_id, presence: true validates :proxy_exercise_id, presence: true
validates :user_id, uniqueness: { scope: [:proxy_exercise_id, :user_type] } validates :user_id, uniqueness: {scope: %i[proxy_exercise_id user_type]}
end end

View File

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

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class AdminOnlyPolicy < ApplicationPolicy 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? } define_method(action) { admin? }
end end
end end

View File

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

View File

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

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module CodeOcean module CodeOcean
class FilePolicy < AdminOrAuthorPolicy class FilePolicy < AdminOrAuthorPolicy
def author? def author?
@ -15,7 +17,7 @@ module CodeOcean
def create? def create?
if @record.context.is_a?(Exercise) if @record.context.is_a?(Exercise)
admin? || author? 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? author?
else else
no_one no_one

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
class ExecutionEnvironmentPolicy < AdminOnlyPolicy 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? } define_method(action) { admin? || author? }
end end

View File

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

View File

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

View File

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

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