Apply automatic rubocop fixes
This commit is contained in:
4
Rakefile
4
Rakefile
@ -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
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ApplicationCable
|
module ApplicationCable
|
||||||
class Channel < ActionCable::Channel::Base
|
class Channel < ActionCable::Channel::Base
|
||||||
end
|
end
|
||||||
|
@ -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
|
||||||
|
@ -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]}"
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class DashboardController < ApplicationController
|
class DashboardController < ApplicationController
|
||||||
include DashboardHelper
|
include DashboardHelper
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
@ -16,12 +18,12 @@ class CommentsController < ApplicationController
|
|||||||
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
|
||||||
@ -83,7 +85,8 @@ class CommentsController < ApplicationController
|
|||||||
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,12 +99,12 @@ 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
|
||||||
@ -109,4 +112,3 @@ class CommentsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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])
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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?
|
||||||
|
@ -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?
|
||||||
|
params[:exercise_collection].permit(:name,
|
||||||
|
:use_anomaly_detection, :user_id, :user_type, exercise_ids: []).merge(user_type: InternalUser.name)
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
sanitized_params[:exercise_ids] = sanitized_params[:exercise_ids].reject {|v| v.nil? or v == '' }
|
sanitized_params[:exercise_ids] = sanitized_params[:exercise_ids].reject {|v| v.nil? or v == '' }
|
||||||
sanitized_params.tap {|p| p[:exercise_collection_items] = p[:exercise_ids].map.with_index {|_id, index| ExerciseCollectionItem.find_or_create_by(exercise_id: _id, exercise_collection_id: @exercise_collection.id, position: index)}; p.delete(:exercise_ids)}
|
sanitized_params.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
|
||||||
|
@ -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)
|
||||||
@ -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]
|
||||||
@ -338,7 +351,8 @@ class ExercisesController < ApplicationController
|
|||||||
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'
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ExternalUsersController < ApplicationController
|
class ExternalUsersController < ApplicationController
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
|
||||||
@ -18,7 +20,7 @@ class ExternalUsersController < ApplicationController
|
|||||||
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,9 +77,10 @@ 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] }
|
||||||
|
|
||||||
@ -85,5 +88,4 @@ class ExternalUsersController < ApplicationController
|
|||||||
format.json { render(json: statistics) }
|
format.json { render(json: statistics) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -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])
|
||||||
|
@ -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)
|
||||||
|
@ -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,7 +27,7 @@ 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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
@ -68,13 +74,11 @@ class ProxyExercisesController < ApplicationController
|
|||||||
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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
@ -143,7 +144,9 @@ 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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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|
|
||||||
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,16 +37,17 @@ 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], :user_type => current_user.class.name, :row => annotation[1][:row], :column => annotation[1][:column], :text => annotation[1][:text])
|
comment = Comment.new(user_id: annotation[1][:user_id], file_id: annotation[1][:file_id],
|
||||||
|
user_type: current_user.class.name, row: annotation[1][:row], column: annotation[1][:column], text: annotation[1][:text])
|
||||||
source_file = CodeOcean::File.find(annotation[1][:file_id])
|
source_file = CodeOcean::File.find(annotation[1][:file_id])
|
||||||
|
|
||||||
# retrieve target file
|
# retrieve target file
|
||||||
@ -59,7 +63,6 @@ class SubmissionsController < ApplicationController
|
|||||||
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,20 +270,20 @@ 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')
|
||||||
@ -284,14 +291,14 @@ class SubmissionsController < ApplicationController
|
|||||||
@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
|
||||||
@ -303,17 +310,17 @@ class SubmissionsController < ApplicationController
|
|||||||
# 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,
|
||||||
@ -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)
|
||||||
@ -372,7 +379,8 @@ 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 = errors.to_a.uniq(&:hint)
|
||||||
errors.each do |error|
|
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
|
||||||
@ -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
|
||||||
|
@ -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') }
|
||||||
@ -40,12 +40,11 @@ class SubscriptionsController < ApplicationController
|
|||||||
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
|
|
||||||
|
|
||||||
def set_subscription
|
def set_subscription
|
||||||
@subscription = Subscription.find(params[:id])
|
@subscription = Subscription.find(params[:id])
|
||||||
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TipsController < ApplicationController
|
class TipsController < ApplicationController
|
||||||
include CommonBehavior
|
include CommonBehavior
|
||||||
|
|
||||||
@ -19,11 +21,10 @@ 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)
|
||||||
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
module DashboardHelper
|
module DashboardHelper
|
||||||
def dashboard_data
|
def dashboard_data
|
||||||
|
@ -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
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ErrorTemplateAttributesHelper
|
module ErrorTemplateAttributesHelper
|
||||||
end
|
end
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ErrorTemplatesHelper
|
module ErrorTemplatesHelper
|
||||||
end
|
end
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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?
|
||||||
|
@ -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,10 +119,10 @@ 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
|
||||||
|
|
||||||
@ -130,40 +132,44 @@ module StatisticsHelper
|
|||||||
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
|
||||||
|
|
||||||
@ -174,7 +180,7 @@ module StatisticsHelper
|
|||||||
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,15 +196,15 @@ 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
|
||||||
|
|
||||||
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Context
|
module Context
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Creation
|
module Creation
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
@ -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|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Consumer < ApplicationRecord
|
class Consumer < ApplicationRecord
|
||||||
has_many :users
|
has_many :users
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,12 +358,12 @@ 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
|
end
|
||||||
if result.count > 0
|
if result.count.positive?
|
||||||
begin
|
|
||||||
quantiles.each_with_index.map {|_q, i| ActiveSupport::Duration.parse(result[i]['unnest']).to_f }
|
quantiles.each_with_index.map {|_q, i| ActiveSupport::Duration.parse(result[i]['unnest']).to_f }
|
||||||
end
|
|
||||||
else
|
else
|
||||||
quantiles.map {|_q| 0 }
|
quantiles.map {|_q| 0 }
|
||||||
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
|
||||||
@ -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?
|
||||||
|
@ -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
|
||||||
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ProxyExercise < ApplicationRecord
|
class ProxyExercise < ApplicationRecord
|
||||||
include Creation
|
include Creation
|
||||||
include DefaultValues
|
include DefaultValues
|
||||||
@ -41,8 +43,7 @@ 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
|
||||||
@ -50,36 +51,36 @@ class ProxyExercise < ApplicationRecord
|
|||||||
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
|
||||||
@ -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,9 +207,9 @@ 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}")
|
||||||
@ -211,8 +217,12 @@ class ProxyExercise < ApplicationRecord
|
|||||||
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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,7 +7,7 @@ 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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
@ -62,7 +74,7 @@ class Submission < ApplicationRecord
|
|||||||
|
|
||||||
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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
|
@ -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
|
||||||
|
@ -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
|
@ -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
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class DashboardPolicy < AdminOnlyPolicy
|
class DashboardPolicy < AdminOnlyPolicy
|
||||||
def dump_docker?
|
def dump_docker?
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ApplicationPolicy
|
class ApplicationPolicy
|
||||||
def admin?
|
def admin?
|
||||||
@user.admin?
|
@user.admin?
|
||||||
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class CodeharborLinkPolicy < ApplicationPolicy
|
class CodeharborLinkPolicy < ApplicationPolicy
|
||||||
def index?
|
def index?
|
||||||
false
|
false
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
class ConsumerPolicy < AdminOnlyPolicy
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ConsumerPolicy < AdminOnlyPolicy
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
class ErrorTemplateAttributePolicy < AdminOnlyPolicy
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ErrorTemplateAttributePolicy < AdminOnlyPolicy
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ErrorTemplatePolicy < AdminOnlyPolicy
|
class ErrorTemplatePolicy < AdminOnlyPolicy
|
||||||
def add_attribute?
|
def add_attribute?
|
||||||
admin?
|
admin?
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
class EventPolicy < AdminOnlyPolicy
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EventPolicy < AdminOnlyPolicy
|
||||||
def create?
|
def create?
|
||||||
everyone
|
everyone
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
class ExerciseCollectionPolicy < AdminOnlyPolicy
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ExerciseCollectionPolicy < AdminOnlyPolicy
|
||||||
def statistics?
|
def statistics?
|
||||||
admin?
|
admin?
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ExternalUserPolicy < AdminOnlyPolicy
|
class ExternalUserPolicy < AdminOnlyPolicy
|
||||||
def index?
|
def index?
|
||||||
admin? || teacher?
|
admin? || teacher?
|
||||||
|
@ -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
Reference in New Issue
Block a user