Merge pull request #939 from openHPI/refactor_proforma_import_export
Refactor Proforma Import/Export
This commit is contained in:
@ -7,7 +7,7 @@ class CodeharborLinksController < ApplicationController
|
||||
|
||||
def new
|
||||
base_url = CodeOcean::Config.new(:code_ocean).read[:codeharbor][:url] || ''
|
||||
@codeharbor_link = CodeharborLink.new(push_url: "#{base_url}/import_exercise",
|
||||
@codeharbor_link = CodeharborLink.new(push_url: "#{base_url}/import_task",
|
||||
check_uuid_url: "#{base_url}/import_uuid_check")
|
||||
authorize!
|
||||
end
|
||||
|
@ -19,9 +19,9 @@ class ExercisesController < ApplicationController
|
||||
before_action :set_course_token, only: [:implement]
|
||||
before_action :set_available_tips, only: %i[implement show new edit]
|
||||
|
||||
skip_before_action :verify_authenticity_token, only: %i[import_exercise import_uuid_check]
|
||||
skip_after_action :verify_authorized, only: %i[import_exercise import_uuid_check]
|
||||
skip_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check], raise: false
|
||||
skip_before_action :verify_authenticity_token, only: %i[import_task import_uuid_check]
|
||||
skip_after_action :verify_authorized, only: %i[import_task import_uuid_check]
|
||||
skip_after_action :verify_policy_scoped, only: %i[import_task import_uuid_check], raise: false
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError, with: :not_authorized_for_exercise
|
||||
|
||||
@ -106,7 +106,7 @@ class ExercisesController < ApplicationController
|
||||
partial: 'export_actions',
|
||||
locals: {
|
||||
exercise: @exercise,
|
||||
exercise_found: codeharbor_check[:exercise_found],
|
||||
uuid_found: codeharbor_check[:uuid_found],
|
||||
update_right: codeharbor_check[:update_right],
|
||||
error: codeharbor_check[:error],
|
||||
exported: false,
|
||||
@ -148,13 +148,13 @@ class ExercisesController < ApplicationController
|
||||
uuid = params[:uuid]
|
||||
exercise = Exercise.find_by(uuid: uuid)
|
||||
|
||||
return render json: {exercise_found: false} if exercise.nil?
|
||||
return render json: {exercise_found: true, update_right: false} unless ExercisePolicy.new(user, exercise).update?
|
||||
return render json: {uuid_found: false} if exercise.nil?
|
||||
return render json: {uuid_found: true, update_right: false} unless ExercisePolicy.new(user, exercise).update?
|
||||
|
||||
render json: {exercise_found: true, update_right: true}
|
||||
render json: {uuid_found: true, update_right: true}
|
||||
end
|
||||
|
||||
def import_exercise
|
||||
def import_task
|
||||
tempfile = Tempfile.new('codeharbor_import.zip')
|
||||
tempfile.write request.body.read.force_encoding('UTF-8')
|
||||
tempfile.rewind
|
||||
|
@ -14,21 +14,20 @@ module ExerciseService
|
||||
req.headers['Authorization'] = "Bearer #{@codeharbor_link.api_key}"
|
||||
req.body = {uuid: @uuid}.to_json
|
||||
end
|
||||
response_hash = JSON.parse(response.body, symbolize_names: true).slice(:exercise_found, :update_right)
|
||||
response_hash = JSON.parse(response.body, symbolize_names: true).slice(:uuid_found, :update_right)
|
||||
|
||||
{error: false,
|
||||
message: message(response_hash[:exercise_found], response_hash[:update_right])}.merge(response_hash)
|
||||
{error: false, message: message(response_hash[:uuid_found], response_hash[:update_right])}.merge(response_hash)
|
||||
rescue Faraday::Error, JSON::ParserError
|
||||
{error: true, message: I18n.t('exercises.export_codeharbor.error')}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message(exercise_found, update_right)
|
||||
if exercise_found
|
||||
update_right ? I18n.t('exercises.export_codeharbor.check.exercise_found') : I18n.t('exercises.export_codeharbor.check.exercise_found_no_right')
|
||||
def message(task_found, update_right)
|
||||
if task_found
|
||||
update_right ? I18n.t('exercises.export_codeharbor.check.task_found') : I18n.t('exercises.export_codeharbor.check.task_found_no_right')
|
||||
else
|
||||
I18n.t('exercises.export_codeharbor.check.no_exercise')
|
||||
I18n.t('exercises.export_codeharbor.check.no_task')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -22,16 +22,34 @@ module ProformaService
|
||||
{
|
||||
title: @exercise.title,
|
||||
description: @exercise.description,
|
||||
internal_description: @exercise.instructions,
|
||||
internal_description: nil,
|
||||
proglang: proglang,
|
||||
files: task_files,
|
||||
tests: tests,
|
||||
uuid: uuid,
|
||||
language: DEFAULT_LANGUAGE,
|
||||
model_solutions: model_solutions,
|
||||
meta_data: {
|
||||
CodeOcean: {
|
||||
public: @exercise.public,
|
||||
hide_file_tree: @exercise.hide_file_tree,
|
||||
allow_file_creation: @exercise.allow_file_creation,
|
||||
allow_auto_completion: @exercise.allow_auto_completion,
|
||||
expected_difficulty: @exercise.expected_difficulty,
|
||||
execution_environment_id: @exercise.execution_environment_id,
|
||||
files: task_files_meta_data,
|
||||
},
|
||||
},
|
||||
}.compact
|
||||
)
|
||||
end
|
||||
|
||||
def proglang
|
||||
regex = %r{^openhpi/co_execenv_(?<language>[^:]*):(?<version>[^-]*)(?>-.*)?$}
|
||||
match = regex.match @exercise.execution_environment.docker_image
|
||||
match ? {name: match[:language], version: match[:version]} : nil
|
||||
end
|
||||
|
||||
def uuid
|
||||
@exercise.update(uuid: SecureRandom.uuid) if @exercise.uuid.nil?
|
||||
@exercise.uuid
|
||||
@ -56,35 +74,49 @@ module ProformaService
|
||||
end
|
||||
|
||||
def tests
|
||||
@exercise.files.filter do |file|
|
||||
file.role == 'teacher_defined_test' || file.role == 'teacher_defined_linter'
|
||||
end.map do |file|
|
||||
@exercise.files.filter(&:teacher_defined_assessment?).map do |file|
|
||||
Proforma::Test.new(
|
||||
id: file.id,
|
||||
title: file.name,
|
||||
files: test_file(file),
|
||||
meta_data: {
|
||||
'feedback-message' => file.feedback_message,
|
||||
}.compact
|
||||
meta_data: test_meta_data(file)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def test_meta_data(file)
|
||||
{
|
||||
CodeOcean: {
|
||||
'feedback-message': file.feedback_message,
|
||||
weight: file.weight,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
def test_file(file)
|
||||
[
|
||||
task_file(file).tap do |t_file|
|
||||
t_file.used_by_grader = true
|
||||
t_file.internal_description = 'teacher_defined_test'
|
||||
end,
|
||||
]
|
||||
end
|
||||
|
||||
def task_files
|
||||
@exercise.files
|
||||
.filter do |file|
|
||||
def exercise_files
|
||||
@exercise.files.filter do |file|
|
||||
!file.role.in? %w[reference_implementation teacher_defined_test
|
||||
teacher_defined_linter]
|
||||
end.map do |file|
|
||||
end
|
||||
end
|
||||
|
||||
def task_files_meta_data
|
||||
exercise_files.to_h do |file|
|
||||
# added CO- to id, otherwise the key would have CodeOcean as a prefix after export and import (cause unknown)
|
||||
["CO-#{file.id}", {role: file.role}]
|
||||
end
|
||||
end
|
||||
|
||||
def task_files
|
||||
exercise_files.map do |file|
|
||||
task_file(file)
|
||||
end
|
||||
end
|
||||
@ -94,8 +126,7 @@ module ProformaService
|
||||
id: file.id,
|
||||
filename: filename(file),
|
||||
usage_by_lms: file.read_only ? 'display' : 'edit',
|
||||
visible: file.hidden ? 'no' : 'yes',
|
||||
internal_description: file.role || 'regular_file'
|
||||
visible: file.hidden ? 'no' : 'yes'
|
||||
)
|
||||
add_content_to_task_file(file, task_file)
|
||||
task_file
|
||||
@ -103,8 +134,7 @@ module ProformaService
|
||||
|
||||
def filename(file)
|
||||
if file.path.present? && file.path != '.'
|
||||
::File.join(file.path,
|
||||
file.name_with_extension)
|
||||
::File.join(file.path, file.name_with_extension)
|
||||
else
|
||||
file.name_with_extension
|
||||
end
|
||||
|
@ -10,31 +10,67 @@ module ProformaService
|
||||
end
|
||||
|
||||
def execute
|
||||
import_exercise
|
||||
import_task
|
||||
@exercise
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def import_exercise
|
||||
def import_task
|
||||
@exercise.assign_attributes(
|
||||
user: @user,
|
||||
title: @task.title,
|
||||
description: @task.description,
|
||||
instructions: @task.internal_description,
|
||||
public: string_to_bool(@task.meta_data[:CodeOcean]&.dig(:public)) || false,
|
||||
hide_file_tree: string_to_bool(@task.meta_data[:CodeOcean]&.dig(:hide_file_tree)) || false,
|
||||
allow_file_creation: string_to_bool(@task.meta_data[:CodeOcean]&.dig(:allow_file_creation)) || false,
|
||||
allow_auto_completion: string_to_bool(@task.meta_data[:CodeOcean]&.dig(:allow_auto_completion)) || false,
|
||||
expected_difficulty: @task.meta_data[:CodeOcean]&.dig(:expected_difficulty) || 1,
|
||||
execution_environment_id: execution_environment_id,
|
||||
|
||||
files: files
|
||||
)
|
||||
end
|
||||
|
||||
def execution_environment_id
|
||||
from_meta_data = @task.meta_data[:CodeOcean]&.dig(:execution_environment_id)
|
||||
return from_meta_data if from_meta_data
|
||||
return nil unless @task.proglang
|
||||
|
||||
ex_envs_with_name_and_version = ExecutionEnvironment.where('docker_image ilike ?', "%#{@task.proglang[:name]}%#{@task.proglang[:version]}%")
|
||||
return ex_envs_with_name_and_version.first.id if ex_envs_with_name_and_version.any?
|
||||
|
||||
ex_envs_with_name = ExecutionEnvironment.where('docker_image like ?', "%#{@task.proglang[:name]}%")
|
||||
return ex_envs_with_name.first.id if ex_envs_with_name.any?
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def string_to_bool(str)
|
||||
return true if str == 'true'
|
||||
return false if str == 'false'
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def files
|
||||
test_files + task_files.values
|
||||
model_solution_files + test_files + task_files.values.tap {|array| array.each {|file| file.role ||= 'regular_file' } }
|
||||
end
|
||||
|
||||
def test_files
|
||||
@task.tests.map do |test_object|
|
||||
task_files.delete(test_object.files.first.id).tap do |file|
|
||||
file.weight = 1.0
|
||||
file.feedback_message = test_object.meta_data['feedback-message']
|
||||
file.weight = test_object.meta_data[:CodeOcean]&.dig(:weight) || 1.0
|
||||
file.feedback_message = test_object.meta_data[:CodeOcean]&.dig(:'feedback-message').presence || 'Feedback'
|
||||
file.role ||= 'teacher_defined_test'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def model_solution_files
|
||||
@task.model_solutions.map do |model_solution_object|
|
||||
task_files.delete(model_solution_object.files.first.id).tap do |file|
|
||||
file.role ||= 'reference_implementation'
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -53,7 +89,7 @@ module ProformaService
|
||||
hidden: file.visible == 'no',
|
||||
name: File.basename(file.filename, '.*'),
|
||||
read_only: file.usage_by_lms != 'edit',
|
||||
role: file.internal_description,
|
||||
role: @task.meta_data[:CodeOcean]&.dig(:files)&.dig("CO-#{file.id}".to_sym)&.dig(:role),
|
||||
path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename)
|
||||
)
|
||||
if file.binary
|
||||
|
@ -9,7 +9,8 @@ module ProformaService
|
||||
|
||||
def execute
|
||||
@task = ConvertExerciseToTask.call(exercise: @exercise)
|
||||
exporter = Proforma::Exporter.new(@task)
|
||||
namespaces = [{prefix: 'CodeOcean', uri: 'codeocean.openhpi.de'}]
|
||||
exporter = Proforma::Exporter.new(task: @task, custom_namespaces: namespaces)
|
||||
exporter.perform
|
||||
end
|
||||
end
|
||||
|
@ -10,8 +10,9 @@ module ProformaService
|
||||
|
||||
def execute
|
||||
if single_task?
|
||||
importer = Proforma::Importer.new(@zip)
|
||||
@task = importer.perform
|
||||
importer = Proforma::Importer.new(zip: @zip)
|
||||
import_result = importer.perform
|
||||
@task = import_result[:task]
|
||||
|
||||
exercise = base_exercise
|
||||
exercise_files = exercise&.files&.to_a
|
||||
|
@ -1,14 +1,14 @@
|
||||
- if error
|
||||
= button_tag type: 'button', class:'btn btn-primary float-end export-button export-retry-button', data: {exercise_id: exercise.id} do
|
||||
i.fa-solid.fa-arrows-rotate.confirm-icon
|
||||
= t('exercises.export_codeharbor.buttons.retry')
|
||||
= button_tag type: 'button', class:'btn btn-primary float-end export-button export-retry-button', data: {exercise_id: exercise.id} do
|
||||
i.fa-solid.fa-arrows-rotate.confirm-icon
|
||||
= t('exercises.export_codeharbor.buttons.retry')
|
||||
- else
|
||||
- unless exported
|
||||
- if !exercise_found || update_right
|
||||
- if !uuid_found || update_right
|
||||
= button_tag type: 'button', class:'btn btn-primary float-end export-action export-button', data: {exercise_id: exercise.id} do
|
||||
i.fa-solid.fa-check.confirm-icon
|
||||
= t('exercises.export_codeharbor.buttons.export')
|
||||
|
||||
= button_tag type: 'submit', class:'btn btn-secondary float-end export-button', data: {dismiss: 'modal'} do
|
||||
= button_tag type: 'submit', class:'btn btn-secondary float-end export-button', data: {bs_dismiss: 'modal'} do
|
||||
i.fa-solid.fa-xmark.abort-icon
|
||||
= exported ? t('exercises.export_codeharbor.buttons.close') : t('exercises.export_codeharbor.buttons.abort')
|
||||
|
Reference in New Issue
Block a user