Merge pull request #939 from openHPI/refactor_proforma_import_export

Refactor Proforma Import/Export
This commit is contained in:
Sebastian Serth
2022-10-26 17:58:48 +02:00
committed by GitHub
20 changed files with 315 additions and 120 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')