Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proforma: reuse existing files on import, if possible #2538

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 60 additions & 35 deletions app/services/proforma_service/convert_exercise_to_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def create_task

def proglang
regex = %r{^openhpi/co_execenv_(?<language>[^:]*):(?<version>[^-]*)(?>-.*)?$}
match = regex.match @exercise.execution_environment.docker_image
match = regex.match @exercise&.execution_environment&.docker_image
match ? {name: match[:language], version: match[:version]} : nil
end

Expand All @@ -77,62 +77,84 @@ def uuid
end

def model_solutions
@exercise.files.filter {|file| file.role == 'reference_implementation' }.map do |file|
@exercise.files.filter(&:reference_implementation?).group_by {|file| xml_id_from_file(file).first }.map do |xml_id, files|
ProformaXML::ModelSolution.new(
id: "ms-#{file.id}",
files: model_solution_file(file)
id: xml_id,
files: files.map {|file| model_solution_file(file) }
)
end
end

def model_solution_file(file)
[
task_file(file).tap do |ms_file|
ms_file.used_by_grader = false
ms_file.usage_by_lms = 'display'
end,
]
task_file(file).tap do |ms_file|
ms_file.used_by_grader = false
ms_file.usage_by_lms = 'display'
end
end

def tests
@exercise.files.filter(&:teacher_defined_assessment?).map do |file|
@exercise.files.filter(&:teacher_defined_assessment?).group_by {|file| xml_id_from_file(file).first }.map do |xml_id, files|
MrSerth marked this conversation as resolved.
Show resolved Hide resolved
ProformaXML::Test.new(
id: file.id,
title: file.name,
files: test_file(file),
meta_data: test_meta_data(file)
id: xml_id,
title: files.first.name,
files: files.map {|file| test_file(file) },
meta_data: test_meta_data(files)
)
end
end

def test_meta_data(file)
def xml_id_from_file(file)
xml_id_path = file.xml_id_path || []
return xml_id_path if xml_id_path&.any?

type = if file.teacher_defined_assessment?
'test'
elsif file.reference_implementation?
'ms'
else
'file'
end

xml_id_path << "co-#{type}-#{file.id}" unless type == 'file'
xml_id_path << file.id.to_s

xml_id_path
end

def test_meta_data(files)
{
'@@order' => %w[test-meta-data],
'test-meta-data' => {
'@@order' => %w[CodeOcean:feedback-message CodeOcean:weight CodeOcean:hidden-feedback],
'@@order' => %w[CodeOcean:test-file],
'@xmlns' => {'CodeOcean' => 'codeocean.openhpi.de'},
'CodeOcean:feedback-message' => {
'@@order' => %w[$1],
'$1' => file.feedback_message,
},
'CodeOcean:weight' => {
'@@order' => %w[$1],
'$1' => file.weight,
},
'CodeOcean:hidden-feedback' => {
'@@order' => %w[$1],
'$1' => file.hidden_feedback,
},
'CodeOcean:test-file' => files.map do |file|
{
'@@order' => %w[CodeOcean:feedback-message CodeOcean:weight CodeOcean:hidden-feedback],
'@xmlns' => {'CodeOcean' => 'codeocean.openhpi.de'},
'@id' => xml_id_from_file(file).last,
'@name' => file.name,
'CodeOcean:feedback-message' => {
'@@order' => %w[$1],
'$1' => file.feedback_message,
},
'CodeOcean:weight' => {
'@@order' => %w[$1],
'$1' => file.weight,
},
'CodeOcean:hidden-feedback' => {
'@@order' => %w[$1],
'$1' => file.hidden_feedback,
},
}
end,
},
}
end

def test_file(file)
[
task_file(file).tap do |t_file|
t_file.used_by_grader = true
end,
]
task_file(file).tap do |t_file|
t_file.used_by_grader = true
end
end

def exercise_files
Expand Down Expand Up @@ -168,8 +190,11 @@ def task_files
end

def task_file(file)
file.update(xml_id_path: xml_id_from_file(file)) if file.xml_id_path.blank?

xml_id = xml_id_from_file(file).last
task_file = ProformaXML::TaskFile.new(
id: file.id,
id: xml_id,
filename: filename(file),
usage_by_lms: file.read_only ? 'display' : 'edit',
visible: file.hidden ? 'no' : 'yes'
Expand Down
40 changes: 25 additions & 15 deletions app/services/proforma_service/convert_task_to_exercise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,44 +60,54 @@ def string_to_bool(str)
end

def files
model_solution_files + test_files + task_files.values.tap {|array| array.each {|file| file.role ||= 'regular_file' } }
model_solution_files + test_files + task_files
end

def test_files
@task.tests.map do |test_object|
task_files.delete(test_object.files.first.id).tap do |file|
file.weight = extract_meta_data(test_object.meta_data&.dig('test-meta-data'), 'weight').presence || 1.0
file.feedback_message = extract_meta_data(test_object.meta_data&.dig('test-meta-data'), 'feedback-message').presence || 'Feedback'
file.hidden_feedback = extract_meta_data(test_object.meta_data&.dig('test-meta-data'), 'hidden-feedback').presence || false
file.role ||= 'teacher_defined_test'
@task.tests.flat_map do |test|
test.files.map do |task_file|
codeocean_file_from_task_file(task_file, test).tap do |file|
file.weight = extract_meta_data(test.meta_data&.dig('test-meta-data'), 'test-file', task_file.id, 'weight').presence || 1.0
file.feedback_message = extract_meta_data(test.meta_data&.dig('test-meta-data'), 'test-file', task_file.id, 'feedback-message').presence || 'Feedback'
file.hidden_feedback = extract_meta_data(test.meta_data&.dig('test-meta-data'), 'test-file', task_file.id, 'hidden-feedback').presence || false
file.role = 'teacher_defined_test' unless file.teacher_defined_assessment?
end
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'
@task.model_solutions.flat_map do |model_solution|
model_solution.files.map do |task_file|
codeocean_file_from_task_file(task_file, model_solution).tap do |file|
file.role ||= 'reference_implementation'
file.feedback_message = nil
end
end
end
end

def task_files
@task_files ||= @task.all_files.reject {|file| file.id == 'ms-placeholder-file' }.to_h do |task_file|
[task_file.id, codeocean_file_from_task_file(task_file)]
@task.files.reject {|file| file.id == 'ms-placeholder-file' }.map do |task_file|
codeocean_file_from_task_file(task_file).tap do |file|
file.role ||= 'regular_file'
file.feedback_message = nil
end
end
end

def codeocean_file_from_task_file(file)
def codeocean_file_from_task_file(file, parent_object = nil)
extension = File.extname(file.filename)
codeocean_file = CodeOcean::File.new(
codeocean_file = @exercise.files.where('array_length(xml_id_path, 1) IS NOT NULL AND xml_id_path[array_length(xml_id_path, 1)] = ?', file.id).first_or_initialize
codeocean_file.assign_attributes(
context: @exercise,
file_type: file_type(extension),
hidden: file.visible != 'yes', # hides 'delayed' and 'no'
name: File.basename(file.filename, '.*'),
read_only: file.usage_by_lms != 'edit',
role: extract_meta_data(@task.meta_data&.dig('meta-data'), 'files', "CO-#{file.id}", 'role'),
path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename)
path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename),
xml_id_path: (parent_object.nil? ? [file.id] : [parent_object.id, file.id])
)
if file.binary
codeocean_file.native_file = FileIO.new(file.content.dup.force_encoding('UTF-8'), File.basename(file.filename))
Expand Down
8 changes: 1 addition & 7 deletions app/services/proforma_service/import.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ def execute
import_result = importer.perform
@task = import_result

exercise = base_exercise
exercise_files = exercise&.files&.to_a

exercise = ConvertTaskToExercise.call(task: @task, user: @user, exercise:)
exercise_files&.each(&:destroy) # feels suboptimal

exercise
ConvertTaskToExercise.call(task: @task, user: @user, exercise: base_exercise)
else
import_multi
end
Expand Down
7 changes: 7 additions & 0 deletions db/migrate/20241007204319_add_xml_path_to_files.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddXmlPathToFiles < ActiveRecord::Migration[7.1]
def change
add_column :files, :xml_id_path, :string, array: true, default: [], null: true
end
end
3 changes: 2 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading