transferred Code Ocean from original repository to GitHub
This commit is contained in:
33
app/controllers/application_controller.rb
Normal file
33
app/controllers/application_controller.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include ApplicationHelper
|
||||
include Pundit
|
||||
|
||||
MEMBER_ACTIONS = [:destroy, :edit, :show, :update]
|
||||
|
||||
after_action :verify_authorized, except: [:help, :welcome]
|
||||
before_action :set_locale
|
||||
protect_from_forgery(with: :exception)
|
||||
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
|
||||
|
||||
def current_user
|
||||
@current_user ||= ExternalUser.find_by(id: session[:external_user_id]) || login_from_session || login_from_other_sources
|
||||
end
|
||||
|
||||
def help
|
||||
end
|
||||
|
||||
def render_not_authorized
|
||||
flash[:danger] = t('application.not_authorized')
|
||||
redirect_to(:root)
|
||||
end
|
||||
private :render_not_authorized
|
||||
|
||||
def set_locale
|
||||
session[:locale] = params[:locale] if params[:locale]
|
||||
I18n.locale = session[:locale] || I18n.default_locale
|
||||
end
|
||||
private :set_locale
|
||||
|
||||
def welcome
|
||||
end
|
||||
end
|
39
app/controllers/code_ocean/files_controller.rb
Normal file
39
app/controllers/code_ocean/files_controller.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
module CodeOcean
|
||||
class FilesController < ApplicationController
|
||||
include FileParameters
|
||||
|
||||
def authorize!
|
||||
authorize(@file)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@file = CodeOcean::File.new(file_params)
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @file.save
|
||||
format.html { redirect_to(implement_exercise_path(@file.context.exercise, tab: 2), notice: t('shared.object_created', model: File.model_name.human)) }
|
||||
format.json { render(:show, location: @file, status: :created) }
|
||||
else
|
||||
format.html { render(:new) }
|
||||
format.json { render(json: @file.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@file = CodeOcean::File.find(params[:id])
|
||||
authorize!
|
||||
@file.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(@file.context, notice: t('shared.object_destroyed', model: File.model_name.human)) }
|
||||
format.json { head(:no_content) }
|
||||
end
|
||||
end
|
||||
|
||||
def file_params
|
||||
params[:code_ocean_file].permit(file_attributes).merge(context_type: 'Submission', role: 'user_defined_file')
|
||||
end
|
||||
private :file_params
|
||||
end
|
||||
end
|
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
6
app/controllers/concerns/file_parameters.rb
Normal file
6
app/controllers/concerns/file_parameters.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
module FileParameters
|
||||
def file_attributes
|
||||
%w[content context_id feedback_message file_id file_type_id hidden id name native_file path read_only role weight]
|
||||
end
|
||||
private :file_attributes
|
||||
end
|
131
app/controllers/concerns/lti.rb
Normal file
131
app/controllers/concerns/lti.rb
Normal file
@@ -0,0 +1,131 @@
|
||||
require 'oauth/request_proxy/rack_request'
|
||||
|
||||
module Lti
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
MAXIMUM_SCORE = 1
|
||||
MAXIMUM_SESSION_AGE = 60.minutes
|
||||
SESSION_PARAMETERS = %w[launch_presentation_return_url lis_outcome_service_url lis_result_sourcedid]
|
||||
|
||||
def build_tool_provider(options = {})
|
||||
if options[:consumer] && options[:parameters]
|
||||
IMS::LTI::ToolProvider.new(options[:consumer].oauth_key, options[:consumer].oauth_secret, options[:parameters])
|
||||
end
|
||||
end
|
||||
private :build_tool_provider
|
||||
|
||||
def clear_lti_session_data
|
||||
session.delete(:consumer_id)
|
||||
session.delete(:external_user_id)
|
||||
session.delete(:lti_parameters)
|
||||
end
|
||||
private :clear_lti_session_data
|
||||
|
||||
def consumer_return_url(provider, options = {})
|
||||
consumer_return_url = provider.try(:launch_presentation_return_url) || params[:launch_presentation_return_url]
|
||||
consumer_return_url += "?#{options.to_query}" if consumer_return_url && options.present?
|
||||
consumer_return_url
|
||||
end
|
||||
|
||||
def external_user_email(provider)
|
||||
provider.lis_person_contact_email_primary
|
||||
end
|
||||
private :external_user_email
|
||||
|
||||
def external_user_name(provider)
|
||||
if provider.lis_person_name_full
|
||||
provider.lis_person_name_full
|
||||
elsif provider.lis_person_name_given && provider.lis_person_name_family
|
||||
"#{provider.lis_person_name_given} #{provider.lis_person_name_family}"
|
||||
else
|
||||
provider.lis_person_name_given || provider.lis_person_name_family
|
||||
end
|
||||
end
|
||||
private :external_user_name
|
||||
|
||||
def lti_outcome_service?
|
||||
session[:lti_parameters].try(:has_key?, 'lis_outcome_service_url')
|
||||
end
|
||||
private :lti_outcome_service?
|
||||
|
||||
def refuse_lti_launch(options = {})
|
||||
return_to_consumer(lti_errorlog: options[:message], lti_errormsg: t('sessions.oauth.failure'))
|
||||
end
|
||||
private :refuse_lti_launch
|
||||
|
||||
def require_oauth_parameters
|
||||
refuse_lti_launch(message: t('sessions.oauth.missing_parameters')) unless params[:oauth_consumer_key] && params[:oauth_signature]
|
||||
end
|
||||
private :require_oauth_parameters
|
||||
|
||||
def require_unique_oauth_nonce
|
||||
refuse_lti_launch(message: t('sessions.oauth.used_nonce')) if NonceStore.has?(params[:oauth_nonce])
|
||||
end
|
||||
private :require_unique_oauth_nonce
|
||||
|
||||
def require_valid_consumer_key
|
||||
@consumer = Consumer.find_by(oauth_key: params[:oauth_consumer_key])
|
||||
refuse_lti_launch(message: t('sessions.oauth.invalid_consumer')) unless @consumer
|
||||
end
|
||||
private :require_valid_consumer_key
|
||||
|
||||
def require_valid_exercise_token
|
||||
@exercise = Exercise.find_by(token: params[:custom_token])
|
||||
refuse_lti_launch(message: t('sessions.oauth.invalid_exercise_token')) unless @exercise
|
||||
end
|
||||
private :require_valid_exercise_token
|
||||
|
||||
def require_valid_oauth_signature
|
||||
@provider = build_tool_provider(consumer: @consumer, parameters: params)
|
||||
refuse_lti_launch(message: t('sessions.oauth.invalid_signature')) unless @provider.valid_request?(request)
|
||||
end
|
||||
private :require_valid_oauth_signature
|
||||
|
||||
def return_to_consumer(options = {})
|
||||
consumer_return_url = @provider.try(:launch_presentation_return_url) || params[:launch_presentation_return_url]
|
||||
if consumer_return_url
|
||||
consumer_return_url += "?#{options.to_query}" if options.present?
|
||||
redirect_to(consumer_return_url)
|
||||
else
|
||||
flash[:danger] = options[:lti_errormsg]
|
||||
flash[:info] = options[:lti_msg]
|
||||
redirect_to(:root)
|
||||
end
|
||||
end
|
||||
private :return_to_consumer
|
||||
|
||||
def send_score(score)
|
||||
raise Error.new("Score #{score} must be between 0 and #{MAXIMUM_SCORE}!") unless (0..MAXIMUM_SCORE).include?(score)
|
||||
provider = build_tool_provider(consumer: Consumer.find_by(id: session[:consumer_id]), parameters: session[:lti_parameters])
|
||||
if provider.nil?
|
||||
{status: 'error'}
|
||||
elsif provider.outcome_service?
|
||||
response = provider.post_replace_result!(score)
|
||||
{code: response.response_code, message: response.post_response.body, status: response.code_major}
|
||||
else
|
||||
{status: 'unsupported'}
|
||||
end
|
||||
end
|
||||
private :send_score
|
||||
|
||||
def set_current_user
|
||||
@current_user = ExternalUser.find_or_create_by(consumer_id: @consumer.id, external_id: @provider.user_id)
|
||||
@current_user.update(email: external_user_email(@provider), name: external_user_name(@provider))
|
||||
end
|
||||
private :set_current_user
|
||||
|
||||
def store_lti_session_data(options = {})
|
||||
session[:consumer_id] = options[:consumer].id
|
||||
session[:external_user_id] = @current_user.id
|
||||
session[:lti_parameters] = options[:parameters].slice(*SESSION_PARAMETERS)
|
||||
end
|
||||
private :store_lti_session_data
|
||||
|
||||
def store_nonce(nonce)
|
||||
NonceStore.add(nonce)
|
||||
end
|
||||
private :store_nonce
|
||||
|
||||
class Error < RuntimeError
|
||||
end
|
||||
end
|
20
app/controllers/concerns/submission_parameters.rb
Normal file
20
app/controllers/concerns/submission_parameters.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
module SubmissionParameters
|
||||
include FileParameters
|
||||
|
||||
def reject_illegal_file_attributes!(submission_params)
|
||||
if exercise = Exercise.find_by(id: submission_params[:exercise_id])
|
||||
submission_params[:files_attributes].try(:reject!) do |index, file_attributes|
|
||||
file = CodeOcean::File.find_by(id: file_attributes[:file_id])
|
||||
file.nil? || file.hidden || file.read_only
|
||||
end
|
||||
end
|
||||
end
|
||||
private :reject_illegal_file_attributes!
|
||||
|
||||
def submission_params
|
||||
submission_params = params[:submission].permit(:cause, :exercise_id, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
reject_illegal_file_attributes!(submission_params)
|
||||
submission_params
|
||||
end
|
||||
private :submission_params
|
||||
end
|
19
app/controllers/concerns/submission_scoring.rb
Normal file
19
app/controllers/concerns/submission_scoring.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module SubmissionScoring
|
||||
def execute_test_files(submission)
|
||||
submission.collect_files.select(&:teacher_defined_test?).map do |file|
|
||||
output = @docker_client.execute_test_command(submission, file.name_with_extension)
|
||||
output.merge!(@assessor.assess(output))
|
||||
output.merge!(filename: file.name_with_extension, message: output[:score] == Assessor::MAXIMUM_SCORE ? I18n.t('exercises.implement.default_feedback') : file.feedback_message, weight: file.weight)
|
||||
end
|
||||
end
|
||||
private :execute_test_files
|
||||
|
||||
def score_submission(submission)
|
||||
@assessor = Assessor.new(execution_environment: submission.execution_environment)
|
||||
@docker_client = DockerClient.new(execution_environment: submission.execution_environment, user: current_user)
|
||||
outputs = execute_test_files(submission)
|
||||
score = outputs.map { |output| output[:score] * output[:weight] }.reduce(:+)
|
||||
submission.update(score: score)
|
||||
outputs
|
||||
end
|
||||
end
|
69
app/controllers/consumers_controller.rb
Normal file
69
app/controllers/consumers_controller.rb
Normal file
@@ -0,0 +1,69 @@
|
||||
class ConsumersController < ApplicationController
|
||||
before_action :set_consumer, only: MEMBER_ACTIONS
|
||||
|
||||
def authorize!
|
||||
authorize(@consumer || @consumers)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@consumer = Consumer.new(consumer_params)
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @consumer.save
|
||||
format.html { redirect_to(@consumer, notice: t('shared.object_created', model: Consumer.model_name.human)) }
|
||||
format.json { render(:show, location: @consumer, status: :created) }
|
||||
else
|
||||
format.html { render(:new) }
|
||||
format.json { render(json: @consumer.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@consumer.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(consumers_url, notice: t('shared.object_destroyed', model: Consumer.model_name.human)) }
|
||||
format.json { head(:no_content) }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def consumer_params
|
||||
params[:consumer].permit(:name, :oauth_key, :oauth_secret)
|
||||
end
|
||||
private :consumer_params
|
||||
|
||||
def index
|
||||
@consumers = Consumer.all
|
||||
authorize!
|
||||
end
|
||||
|
||||
def new
|
||||
@consumer = Consumer.new(oauth_key: SecureRandom.hex, oauth_secret: SecureRandom.hex)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def set_consumer
|
||||
@consumer = Consumer.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_consumer
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @consumer.update(consumer_params)
|
||||
format.html { redirect_to(@consumer, notice: t('shared.object_updated', model: Consumer.model_name.human)) }
|
||||
format.json { render(:show, location: @consumer, status: :ok) }
|
||||
else
|
||||
format.html { render(:edit) }
|
||||
format.json { render(json: @consumer.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
43
app/controllers/errors_controller.rb
Normal file
43
app/controllers/errors_controller.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
class ErrorsController < ApplicationController
|
||||
before_action :set_execution_environment
|
||||
|
||||
def authorize!
|
||||
authorize(@error || Error.where(execution_environment_id: @execution_environment.id))
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@error = Error.new(error_params)
|
||||
authorize!
|
||||
hint = Whistleblower.new(execution_environment: @error.execution_environment).generate_hint(@error.message)
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
if hint
|
||||
render(json: {hint: hint})
|
||||
else
|
||||
render(nothing: true, status: @error.save ? :created : :unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def error_params
|
||||
params[:error].permit(:message).merge(execution_environment_id: @execution_environment.id)
|
||||
end
|
||||
private :error_params
|
||||
|
||||
def index
|
||||
authorize!
|
||||
@errors = Error.for_execution_environment(@execution_environment)
|
||||
end
|
||||
|
||||
def set_execution_environment
|
||||
@execution_environment = ExecutionEnvironment.find(params[:execution_environment_id])
|
||||
end
|
||||
private :set_execution_environment
|
||||
|
||||
def show
|
||||
@error = Error.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
end
|
98
app/controllers/execution_environments_controller.rb
Normal file
98
app/controllers/execution_environments_controller.rb
Normal file
@@ -0,0 +1,98 @@
|
||||
class ExecutionEnvironmentsController < ApplicationController
|
||||
before_action :set_docker_images, only: [:create, :edit, :new, :update]
|
||||
before_action :set_execution_environment, only: MEMBER_ACTIONS + [:execute_command, :shell]
|
||||
before_action :set_testing_framework_adapters, only: [:create, :edit, :new, :update]
|
||||
|
||||
def authorize!
|
||||
authorize(@execution_environment || @execution_environments)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@execution_environment = ExecutionEnvironment.new(execution_environment_params)
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @execution_environment.save
|
||||
format.html { redirect_to(@execution_environment, notice: t('shared.object_created', model: ExecutionEnvironment.model_name.human)) }
|
||||
format.json { render(:show, location: @execution_environment, status: :created) }
|
||||
else
|
||||
format.html { render(:new) }
|
||||
format.json { render(json: @execution_environment.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@execution_environment.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(execution_environments_url, notice: t('shared.object_destroyed', model: ExecutionEnvironment.model_name.human)) }
|
||||
format.json { head(:no_content) }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def execute_command
|
||||
@docker_client = DockerClient.new(execution_environment: @execution_environment, user: current_user)
|
||||
render(json: @docker_client.execute_command(params[:command]))
|
||||
end
|
||||
|
||||
def execution_environment_params
|
||||
params[:execution_environment].permit(:docker_image, :exposed_ports, :editor_mode, :file_extension, :help, :indent_size, :name, :permitted_execution_time, :run_command, :test_command, :testing_framework).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
private :execution_environment_params
|
||||
|
||||
def index
|
||||
@execution_environments = ExecutionEnvironment.all.order(:name)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def new
|
||||
@execution_environment = ExecutionEnvironment.new
|
||||
authorize!
|
||||
end
|
||||
|
||||
def set_docker_images
|
||||
@docker_images = DockerClient.image_tags.sort
|
||||
rescue DockerClient::Error => error
|
||||
@docker_images = []
|
||||
flash[:warning] = error.message
|
||||
end
|
||||
private :set_docker_images
|
||||
|
||||
def set_execution_environment
|
||||
@execution_environment = ExecutionEnvironment.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_execution_environment
|
||||
|
||||
def set_testing_framework_adapters
|
||||
Rails.application.eager_load!
|
||||
@testing_framework_adapters = TestingFrameworkAdapter.descendants.sort_by(&:framework_name).map do |klass|
|
||||
[klass.framework_name, klass.name]
|
||||
end
|
||||
end
|
||||
private :set_testing_framework_adapters
|
||||
|
||||
def shell
|
||||
end
|
||||
|
||||
def show
|
||||
if @execution_environment.testing_framework?
|
||||
@testing_framework_adapter = Kernel.const_get(@execution_environment.testing_framework)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @execution_environment.update(execution_environment_params)
|
||||
format.html { redirect_to(@execution_environment, notice: t('shared.object_updated', model: ExecutionEnvironment.model_name.human)) }
|
||||
format.json { render(:show, location: @execution_environment, status: :ok) }
|
||||
else
|
||||
format.html { render(:edit) }
|
||||
format.json { render(json: @execution_environment.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
152
app/controllers/exercises_controller.rb
Normal file
152
app/controllers/exercises_controller.rb
Normal file
@@ -0,0 +1,152 @@
|
||||
class ExercisesController < ApplicationController
|
||||
include Lti
|
||||
include SubmissionParameters
|
||||
include SubmissionScoring
|
||||
|
||||
before_action :handle_file_uploads, only: [:create, :update]
|
||||
before_action :set_execution_environments, only: [:create, :edit, :new, :update]
|
||||
before_action :set_exercise, only: MEMBER_ACTIONS + [:clone, :implement, :run, :statistics, :submit]
|
||||
before_action :set_file_types, only: [:create, :edit, :new, :update]
|
||||
|
||||
def authorize!
|
||||
authorize(@exercise || @exercises)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def clone
|
||||
exercise = @exercise.duplicate(public: false, user: current_user)
|
||||
if exercise.save
|
||||
redirect_to(exercise, notice: t('shared.object_cloned', model: Exercise.model_name.human))
|
||||
else
|
||||
flash[:danger] = t('shared.message_failure')
|
||||
redirect_to(exercises_path)
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
@exercise = Exercise.new(exercise_params)
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @exercise.save
|
||||
format.html { redirect_to(@exercise, notice: t('shared.object_created', model: Exercise.model_name.human)) }
|
||||
format.json { render(:show, location: @exercise, status: :created) }
|
||||
else
|
||||
format.html { render(:new) }
|
||||
format.json { render(json: @exercise.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@exercise.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(exercises_url, notice: t('shared.object_destroyed', model: Exercise.model_name.human)) }
|
||||
format.json { head(:no_content) }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def exercise_params
|
||||
params[:exercise].permit(:description, :execution_environment_id, :file_id, :instructions, :public, :title, files_attributes: file_attributes).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
private :exercise_params
|
||||
|
||||
def handle_file_uploads
|
||||
exercise_params[:files_attributes].try(:each) do |index, file_attributes|
|
||||
if file_attributes[:content].respond_to?(:read)
|
||||
file_params = params[:exercise][:files_attributes][index]
|
||||
if FileType.find_by(id: file_attributes[:file_type_id]).try(:binary?)
|
||||
file_params[:content] = nil
|
||||
file_params[:native_file] = file_attributes[:content]
|
||||
else
|
||||
file_params[:content] = file_attributes[:content].read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
private :handle_file_uploads
|
||||
|
||||
def implement
|
||||
if Submission.exists?(exercise_id: @exercise.id, user_id: current_user.id)
|
||||
@submission = Submission.where(exercise_id: @exercise.id, user_id: current_user.id).order('created_at DESC').first
|
||||
@files = @submission.collect_files.select(&:visible)
|
||||
else
|
||||
@files = @exercise.files.visible
|
||||
end
|
||||
@files = @files.sort_by(&:name_with_extension)
|
||||
end
|
||||
|
||||
def index
|
||||
@search = policy_scope(Exercise).search(params[:q])
|
||||
@exercises = @search.result.order(:title)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def redirect_to_lti_return_path
|
||||
path = lti_return_path(consumer_id: session[:consumer_id], submission_id: @submission.id, url: consumer_return_url(build_tool_provider(consumer: Consumer.find_by(id: session[:consumer_id]), parameters: session[:lti_parameters])))
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(path) }
|
||||
format.json { render(json: {redirect: path}) }
|
||||
end
|
||||
end
|
||||
private :redirect_to_lti_return_path
|
||||
|
||||
def new
|
||||
@exercise = Exercise.new
|
||||
authorize!
|
||||
end
|
||||
|
||||
def set_execution_environments
|
||||
@execution_environments = ExecutionEnvironment.all.order(:name)
|
||||
end
|
||||
private :set_execution_environments
|
||||
|
||||
def set_exercise
|
||||
@exercise = Exercise.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_exercise
|
||||
|
||||
def set_file_types
|
||||
@file_types = FileType.all.order(:name)
|
||||
end
|
||||
private :set_file_types
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def statistics
|
||||
end
|
||||
|
||||
def submit
|
||||
@submission = Submission.create(submission_params)
|
||||
score_submission(@submission)
|
||||
if lti_outcome_service?
|
||||
response = send_score(@submission.normalized_score)
|
||||
if response[:status] == 'success'
|
||||
redirect_to_lti_return_path
|
||||
else
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(implement_exercise_path(@submission.exercise)) }
|
||||
format.json { render(json: {message: I18n.t('exercises.submit.failure')}, status: 503) }
|
||||
end
|
||||
end
|
||||
else
|
||||
redirect_to_lti_return_path
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @exercise.update(exercise_params)
|
||||
format.html { redirect_to(@exercise, notice: t('shared.object_updated', model: Exercise.model_name.human)) }
|
||||
format.json { render(:show, location: @exercise, status: :ok) }
|
||||
else
|
||||
format.html { render(:edit) }
|
||||
format.json { render(json: @exercise.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
16
app/controllers/external_users_controller.rb
Normal file
16
app/controllers/external_users_controller.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
class ExternalUsersController < ApplicationController
|
||||
def authorize!
|
||||
authorize(@user || @users)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def index
|
||||
@users = ExternalUser.all
|
||||
authorize!
|
||||
end
|
||||
|
||||
def show
|
||||
@user = ExternalUser.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
end
|
78
app/controllers/file_types_controller.rb
Normal file
78
app/controllers/file_types_controller.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
class FileTypesController < ApplicationController
|
||||
before_action :set_editor_modes, only: [:create, :edit, :new, :update]
|
||||
before_action :set_file_type, only: MEMBER_ACTIONS
|
||||
|
||||
def authorize!
|
||||
authorize(@file_type || @file_types)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@file_type = FileType.new(file_type_params)
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @file_type.save
|
||||
format.html { redirect_to(@file_type, notice: t('shared.object_created', model: FileType.model_name.human)) }
|
||||
format.json { render(:show, location: @file_type, status: :created) }
|
||||
else
|
||||
format.html { render(:new) }
|
||||
format.json { render(json: @file_type.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@file_type.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(file_types_url, notice: t('shared.object_destroyed', model: FileType.model_name.human)) }
|
||||
format.json { head(:no_content) }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def file_type_params
|
||||
params[:file_type].permit(:binary, :editor_mode, :executable, :file_extension, :name, :indent_size, :renderable).merge(user_id: current_user.id, user_type: current_user.class.name)
|
||||
end
|
||||
private :file_type_params
|
||||
|
||||
def index
|
||||
@file_types = FileType.all.order(:name)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def new
|
||||
@file_type = FileType.new
|
||||
authorize!
|
||||
end
|
||||
|
||||
def set_editor_modes
|
||||
@editor_modes = Dir.glob('vendor/assets/javascripts/ace/mode-*.js').map do |filename|
|
||||
name = filename.gsub(/\w+\/|mode-|.js$/, '')
|
||||
[name, "ace/mode/#{name}"]
|
||||
end
|
||||
end
|
||||
private :set_editor_modes
|
||||
|
||||
def set_file_type
|
||||
@file_type = FileType.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_file_type
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @file_type.update(file_type_params)
|
||||
format.html { redirect_to(@file_type, notice: t('shared.object_updated', model: FileType.model_name.human)) }
|
||||
format.json { render(:show, location: @file_type, status: :ok) }
|
||||
else
|
||||
format.html { render(:edit) }
|
||||
format.json { render(json: @file_type.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
75
app/controllers/hints_controller.rb
Normal file
75
app/controllers/hints_controller.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
class HintsController < ApplicationController
|
||||
before_action :set_execution_environment
|
||||
before_action :set_hint, only: MEMBER_ACTIONS
|
||||
|
||||
def authorize!
|
||||
authorize(@hint || @hints)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@hint = Hint.new(hint_params)
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
if @hint.save
|
||||
format.html { redirect_to(execution_environment_hint_path(@execution_environment, @hint.id), notice: t('shared.object_created', model: Hint.model_name.human)) }
|
||||
format.json { render(:show, location: @hint, status: :created) }
|
||||
else
|
||||
format.html { render(:new) }
|
||||
format.json { render(json: @hint.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hint.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(execution_environment_hints_path(@execution_environment), notice: t('shared.object_destroyed', model: Hint.model_name.human)) }
|
||||
format.json { head(:no_content) }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def hint_params
|
||||
params[:hint].permit(:locale, :message, :name, :regular_expression).merge(execution_environment_id: @execution_environment.id)
|
||||
end
|
||||
private :hint_params
|
||||
|
||||
def index
|
||||
@hints = Hint.where(execution_environment_id: @execution_environment.id).order(:name)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def new
|
||||
@hint = Hint.new
|
||||
authorize!
|
||||
end
|
||||
|
||||
def set_execution_environment
|
||||
@execution_environment = ExecutionEnvironment.find(params[:execution_environment_id])
|
||||
end
|
||||
private :set_execution_environment
|
||||
|
||||
def set_hint
|
||||
@hint = Hint.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_hint
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @hint.update(hint_params)
|
||||
format.html { redirect_to(execution_environment_hint_path(params[:execution_environment_id], @hint.id), notice: t('shared.object_updated', model: Hint.model_name.human)) }
|
||||
format.json { render(:show, location: @hint, status: :ok) }
|
||||
else
|
||||
format.html { render(:edit) }
|
||||
format.json { render(json: @hint.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
131
app/controllers/internal_users_controller.rb
Normal file
131
app/controllers/internal_users_controller.rb
Normal file
@@ -0,0 +1,131 @@
|
||||
class InternalUsersController < ApplicationController
|
||||
before_action :require_activation_token, only: :activate
|
||||
before_action :require_reset_password_token, only: :reset_password
|
||||
before_action :set_user, only: MEMBER_ACTIONS
|
||||
skip_before_action :verify_authenticity_token, only: :activate
|
||||
skip_after_action :verify_authorized, only: [:activate, :forgot_password, :reset_password]
|
||||
|
||||
def activate
|
||||
if request.patch? || request.put?
|
||||
respond_to do |format|
|
||||
if @user.update(params[:internal_user].permit(:password, :password_confirmation))
|
||||
@user.activate!
|
||||
format.html { redirect_to(sign_in_path, notice: t('.success')) }
|
||||
format.json { render(nothing: true, status: :ok) }
|
||||
else
|
||||
format.html { render(:activate) }
|
||||
format.json { render(json: @user.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def authorize!
|
||||
authorize(@user || @users)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@user = InternalUser.new(internal_user_params)
|
||||
authorize!
|
||||
@user.send(:setup_activation)
|
||||
respond_to do |format|
|
||||
if @user.save
|
||||
@user.send(:send_activation_needed_email!)
|
||||
format.html { redirect_to(@user, notice: t('shared.object_created', model: InternalUser.model_name.human)) }
|
||||
format.json { render(:show, location: @user, status: :created) }
|
||||
else
|
||||
format.html { render(:new) }
|
||||
format.json { render(json: @user.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user.destroy
|
||||
respond_to do |format|
|
||||
format.html { redirect_to(internal_users_url, notice: t('shared.object_destroyed', model: InternalUser.model_name.human)) }
|
||||
format.json { head(:no_content) }
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def forgot_password
|
||||
if request.get? && current_user
|
||||
flash[:warning] = t('shared.already_signed_in')
|
||||
redirect_to(:root)
|
||||
elsif request.post?
|
||||
if params[:email].present?
|
||||
InternalUser.find_by(email: params[:email]).try(:deliver_reset_password_instructions!)
|
||||
flash[:notice] = t('.success')
|
||||
redirect_to(:root)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
@search = InternalUser.search(params[:q])
|
||||
@users = @search.result.order(:name)
|
||||
authorize!
|
||||
end
|
||||
|
||||
def internal_user_params
|
||||
params[:internal_user].permit(:consumer_id, :email, :name, :role)
|
||||
end
|
||||
private :internal_user_params
|
||||
|
||||
def new
|
||||
@user = InternalUser.new
|
||||
authorize!
|
||||
end
|
||||
|
||||
def require_activation_token
|
||||
@user = InternalUser.load_from_activation_token(params[:token] || params[:internal_user].try(:[], :activation_token))
|
||||
render_not_authorized unless @user
|
||||
end
|
||||
private :require_activation_token
|
||||
|
||||
def require_reset_password_token
|
||||
@user = InternalUser.load_from_reset_password_token(params[:token] || params[:internal_user].try(:[], :reset_password_token))
|
||||
render_not_authorized unless @user
|
||||
end
|
||||
private :require_reset_password_token
|
||||
|
||||
def reset_password
|
||||
if request.patch? || request.put?
|
||||
respond_to do |format|
|
||||
if @user.update(params[:internal_user].permit(:password, :password_confirmation))
|
||||
@user.change_password!(params[:internal_user][:password])
|
||||
format.html { redirect_to(sign_in_path, notice: t('.success')) }
|
||||
format.json { render(nothing: true, status: :ok) }
|
||||
else
|
||||
format.html { render(:reset_password) }
|
||||
format.json { render(json: @user.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_user
|
||||
@user = InternalUser.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_user
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @user.update(internal_user_params)
|
||||
format.html { redirect_to(@user, notice: t('shared.object_updated', model: InternalUser.model_name.human)) }
|
||||
format.json { render(:show, location: @user, status: :ok) }
|
||||
else
|
||||
format.html { render(:edit) }
|
||||
format.json { render(json: @user.errors, status: :unprocessable_entity) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
49
app/controllers/sessions_controller.rb
Normal file
49
app/controllers/sessions_controller.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
class SessionsController < ApplicationController
|
||||
include Lti
|
||||
|
||||
[:require_oauth_parameters, :require_valid_consumer_key, :require_valid_oauth_signature, :require_valid_exercise_token].each do |method_name|
|
||||
before_action(method_name, only: :create_through_lti)
|
||||
end
|
||||
|
||||
skip_after_action :verify_authorized
|
||||
skip_before_action :verify_authenticity_token, only: :create_through_lti
|
||||
|
||||
def create
|
||||
if user = login(params[:email], params[:password], params[:remember_me])
|
||||
redirect_back_or_to(:root, notice: t('.success'))
|
||||
else
|
||||
flash.now[:danger] = t('.failure')
|
||||
render(:new)
|
||||
end
|
||||
end
|
||||
|
||||
def create_through_lti
|
||||
set_current_user
|
||||
store_lti_session_data(consumer: @consumer, parameters: params)
|
||||
store_nonce(params[:oauth_nonce])
|
||||
flash[:notice] = I18n.t("sessions.create_through_lti.session_#{lti_outcome_service? ? 'with' : 'without'}_outcome", consumer: @consumer)
|
||||
redirect_to(implement_exercise_path(@exercise.id))
|
||||
end
|
||||
|
||||
def destroy
|
||||
if current_user.external?
|
||||
clear_lti_session_data
|
||||
else
|
||||
logout
|
||||
end
|
||||
redirect_to(:root, notice: t('.success'))
|
||||
end
|
||||
|
||||
def destroy_through_lti
|
||||
@consumer = Consumer.find_by(id: params[:consumer_id])
|
||||
@submission = Submission.find(params[:submission_id])
|
||||
clear_lti_session_data
|
||||
end
|
||||
|
||||
def new
|
||||
if current_user
|
||||
flash[:warning] = t('shared.already_signed_in')
|
||||
redirect_to(:root)
|
||||
end
|
||||
end
|
||||
end
|
145
app/controllers/submissions_controller.rb
Normal file
145
app/controllers/submissions_controller.rb
Normal file
@@ -0,0 +1,145 @@
|
||||
class SubmissionsController < ApplicationController
|
||||
include ActionController::Live
|
||||
include Lti
|
||||
include SubmissionParameters
|
||||
include SubmissionScoring
|
||||
|
||||
around_action :with_server_sent_events, only: :run
|
||||
before_action :set_submission, only: [:download_file, :render_file, :run, :score, :show, :statistics, :stop, :test]
|
||||
before_action :set_docker_client, only: [:run, :test]
|
||||
before_action :set_files, only: [:download_file, :render_file, :show]
|
||||
before_action :set_file, only: [:download_file, :render_file]
|
||||
before_action :set_mime_type, only: [:download_file, :render_file]
|
||||
skip_before_action :verify_authenticity_token, only: [:download_file, :render_file]
|
||||
|
||||
def authorize!
|
||||
authorize(@submission || @submissions)
|
||||
end
|
||||
private :authorize!
|
||||
|
||||
def create
|
||||
@submission = Submission.new(submission_params)
|
||||
authorize!
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
if @submission.save
|
||||
render(:show, location: @submission, status: :created)
|
||||
else
|
||||
render(nothing: true, status: :unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def download_file
|
||||
if @file.native_file?
|
||||
send_file(@file.native_file.path)
|
||||
else
|
||||
send_data(@file.content, filename: @file.name_with_extension)
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
@search = Submission.search(params[:q])
|
||||
@submissions = @search.result.paginate(page: params[:page])
|
||||
authorize!
|
||||
end
|
||||
|
||||
def render_file
|
||||
if @file.native_file?
|
||||
send_file(@file.native_file.path, disposition: 'inline')
|
||||
else
|
||||
render(text: @file.content)
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
container_id = nil
|
||||
stderr = ''
|
||||
output = @docker_client.execute_run_command(@submission, params[:filename]) do |stream, chunk|
|
||||
unless container_id
|
||||
container_id = @docker_client.container_id
|
||||
@server_sent_event.write({id: container_id, ports: @docker_client.assigned_ports}, event: 'info')
|
||||
end
|
||||
@server_sent_event.write({stream => chunk}, event: 'output')
|
||||
stderr += chunk if stream == :stderr
|
||||
end
|
||||
@server_sent_event.write(output, event: 'status')
|
||||
if stderr.present?
|
||||
if hint = Whistleblower.new(execution_environment: @submission.execution_environment).generate_hint(stderr)
|
||||
@server_sent_event.write(hint, event: 'hint')
|
||||
else
|
||||
store_error(stderr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def score
|
||||
render(json: score_submission(@submission))
|
||||
end
|
||||
|
||||
def set_docker_client
|
||||
@docker_client = DockerClient.new(execution_environment: @submission.execution_environment, user: current_user)
|
||||
end
|
||||
private :set_docker_client
|
||||
|
||||
def set_file
|
||||
@file = @files.detect { |file| file.name_with_extension == params[:filename] }
|
||||
render(nothing: true, status: 404) unless @file
|
||||
end
|
||||
private :set_file
|
||||
|
||||
def set_files
|
||||
@files = @submission.collect_files.select(&:visible)
|
||||
end
|
||||
private :set_files
|
||||
|
||||
def set_mime_type
|
||||
@mime_type = Mime::Type.lookup_by_extension(@file.file_type.file_extension.gsub(/^\./, ''))
|
||||
response.headers['Content-Type'] = @mime_type.to_s
|
||||
end
|
||||
private :set_mime_type
|
||||
|
||||
def set_submission
|
||||
@submission = Submission.find(params[:id])
|
||||
authorize!
|
||||
end
|
||||
private :set_submission
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def statistics
|
||||
end
|
||||
|
||||
def stop
|
||||
container = Docker::Container.get(params[:container_id])
|
||||
DockerClient.destroy_container(container)
|
||||
rescue Docker::Error::NotFoundError
|
||||
ensure
|
||||
render(nothing: true)
|
||||
end
|
||||
|
||||
def store_error(stderr)
|
||||
::Error.create(execution_environment_id: @submission.exercise.execution_environment_id, message: stderr)
|
||||
end
|
||||
private :store_error
|
||||
|
||||
def test
|
||||
output = @docker_client.execute_test_command(@submission, params[:filename])
|
||||
render(json: [output])
|
||||
end
|
||||
|
||||
def with_server_sent_events
|
||||
response.headers['Content-Type'] = 'text/event-stream'
|
||||
@server_sent_event = SSE.new(response.stream)
|
||||
@server_sent_event.write(nil, event: 'start')
|
||||
yield
|
||||
@server_sent_event.write({code: 200}, event: 'close')
|
||||
rescue
|
||||
@server_sent_event.write({code: 500}, event: 'close')
|
||||
ensure
|
||||
@server_sent_event.close
|
||||
end
|
||||
private :with_server_sent_events
|
||||
end
|
Reference in New Issue
Block a user