diff --git a/app/services/proforma_service/convert_exercise_to_task.rb b/app/services/proforma_service/convert_exercise_to_task.rb index 3eba296f6..913bba8f4 100644 --- a/app/services/proforma_service/convert_exercise_to_task.rb +++ b/app/services/proforma_service/convert_exercise_to_task.rb @@ -77,62 +77,68 @@ def uuid end def model_solutions - @exercise.files.filter {|file| file.role == 'reference_implementation' }.map do |file| + @exercise.files.filter {|file| file.role == 'reference_implementation' }.group_by {|file| file.xml_id_path&.split('/')&.first || "co-ms-#{file.id}" }.map do |xml_id, files| ProformaXML::ModelSolution.new( - id: "ms-#{file.id}", - files: model_solution_file(file) + id: xml_id || "ms-#{files.first.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| file.xml_id_path&.split('/')&.first || "co-test-#{file.id}" }.map do |xml_id, files| ProformaXML::Test.new( - id: file.id, - title: file.name, - files: test_file(file), - meta_data: test_meta_data(file) + id: xml_id || files.first.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 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.to_h do |file| + [ + "CodeOcean:#{file.xml_id_path&.split('/')&.last || file.id}", { + '@@order' => %w[CodeOcean:feedback-message CodeOcean:weight CodeOcean:hidden-feedback], + '@xmlns' => {'CodeOcean' => 'codeocean.openhpi.de'}, + '@id' => file.xml_id_path&.split('/')&.last || file.id, + '@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 @@ -169,7 +175,7 @@ def task_files def task_file(file) task_file = ProformaXML::TaskFile.new( - id: file.id, + id: file.xml_id_path&.split('/')&.last || file.id, filename: filename(file), usage_by_lms: file.read_only ? 'display' : 'edit', visible: file.hidden ? 'no' : 'yes' diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index 6eda6e4ce..2f03d6b45 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -60,44 +60,56 @@ 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' + 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' + end end + # @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)] + # 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 = CodeOcean::File.where(context: @exercise).where('xml_id_path LIKE ?', "%#{file.id}").first_or_initialize(context: @exercise) + 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? ? '' : "#{parent_object.id}/") + file.id.to_s ) if file.binary codeocean_file.native_file = FileIO.new(file.content.dup.force_encoding('UTF-8'), File.basename(file.filename)) diff --git a/db/migrate/20240903204319_add_xml_path_to_files.rb b/db/migrate/20240903204319_add_xml_path_to_files.rb new file mode 100644 index 000000000..680624c15 --- /dev/null +++ b/db/migrate/20240903204319_add_xml_path_to_files.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddXmlPathToFiles < ActiveRecord::Migration[7.1] + def change + add_column :files, :xml_id_path, :string, null: true, default: nil + end +end diff --git a/db/schema.rb b/db/schema.rb index 01938a6da..74d1e242c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,15 +10,15 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_12_08_194632) do +ActiveRecord::Schema[7.1].define(version: 2024_09_03_204319) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" enable_extension "pgcrypto" enable_extension "plpgsql" create_table "anomaly_notifications", id: :serial, force: :cascade do |t| - t.integer "contributor_id", null: false t.string "contributor_type", null: false + t.integer "contributor_id", null: false t.integer "exercise_id", null: false t.integer "exercise_collection_id", null: false t.jsonb "reason" @@ -46,10 +46,10 @@ t.string "api_key" t.datetime "created_at" t.datetime "updated_at" + t.string "user_type" t.integer "user_id" t.string "push_url" t.string "check_uuid_url" - t.string "user_type" t.index ["user_type", "user_id"], name: "index_codeharbor_links_on_user_type_and_user_id" end @@ -137,8 +137,8 @@ create_table "events", id: :serial, force: :cascade do |t| t.string "category" t.string "data" - t.integer "user_id" t.string "user_type" + t.integer "user_id" t.integer "exercise_id" t.integer "file_id" t.datetime "created_at", null: false @@ -207,8 +207,8 @@ t.datetime "created_at" t.datetime "updated_at" t.boolean "use_anomaly_detection", default: false - t.integer "user_id" t.string "user_type" + t.integer "user_id" t.index ["user_type", "user_id"], name: "index_exercise_collections_on_user_type_and_user_id" end @@ -298,8 +298,8 @@ create_table "files", id: :serial, force: :cascade do |t| t.text "content" - t.integer "context_id" t.string "context_type" + t.integer "context_id" t.integer "file_id" t.integer "file_type_id" t.boolean "hidden" @@ -315,6 +315,7 @@ t.string "path" t.integer "file_template_id" t.boolean "hidden_feedback", default: false, null: false + t.string "xml_id_path" t.index ["context_id", "context_type"], name: "index_files_on_context_id_and_context_type" end @@ -492,8 +493,8 @@ create_table "searches", id: :serial, force: :cascade do |t| t.integer "exercise_id", null: false - t.integer "user_id", null: false t.string "user_type", null: false + t.integer "user_id", null: false t.string "search" t.datetime "created_at" t.datetime "updated_at" @@ -523,7 +524,7 @@ t.bigint "user_id" t.integer "role", limit: 2, default: 0, null: false, comment: "Used as enum in Rails" t.index ["study_group_id"], name: "index_study_group_memberships_on_study_group_id" - t.index ["user_type", "user_id"], name: "index_study_group_memberships_on_user" + t.index ["user_type", "user_id"], name: "index_study_group_memberships_on_user_type_and_user_id" end create_table "study_groups", force: :cascade do |t| @@ -551,8 +552,8 @@ end create_table "subscriptions", id: :serial, force: :cascade do |t| - t.integer "user_id" t.string "user_type" + t.integer "user_id" t.integer "request_for_comment_id" t.string "subscription_type" t.datetime "created_at", null: false @@ -619,13 +620,13 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["file_type_id"], name: "index_tips_on_file_type_id" - t.index ["user_type", "user_id"], name: "index_tips_on_user" + t.index ["user_type", "user_id"], name: "index_tips_on_user_type_and_user_id" end create_table "user_exercise_feedbacks", id: :serial, force: :cascade do |t| t.integer "exercise_id", null: false - t.integer "user_id", null: false t.string "user_type", null: false + t.integer "user_id", null: false t.integer "difficulty" t.integer "working_time_seconds" t.string "feedback_text" @@ -638,8 +639,8 @@ end create_table "user_exercise_interventions", id: :serial, force: :cascade do |t| - t.integer "contributor_id", null: false t.string "contributor_type", null: false + t.integer "contributor_id", null: false t.integer "exercise_id", null: false t.integer "intervention_id", null: false t.integer "accumulated_worktime_s" @@ -652,8 +653,8 @@ end create_table "user_proxy_exercise_exercises", id: :serial, force: :cascade do |t| - t.integer "user_id" t.string "user_type" + t.integer "user_id" t.integer "proxy_exercise_id" t.integer "exercise_id" t.datetime "created_at" @@ -661,7 +662,7 @@ t.string "reason" t.index ["exercise_id"], name: "index_user_proxy_exercise_exercises_on_exercise_id" t.index ["proxy_exercise_id"], name: "index_user_proxy_exercise_exercises_on_proxy_exercise_id" - t.index ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user" + t.index ["user_type", "user_id"], name: "index_user_proxy_exercise_exercises_on_user_type_and_user_id" end add_foreign_key "anomaly_notifications", "exercise_collections" diff --git a/spec/services/proforma_service/convert_exercise_to_task_spec.rb b/spec/services/proforma_service/convert_exercise_to_task_spec.rb index 73e68dc03..034feba55 100644 --- a/spec/services/proforma_service/convert_exercise_to_task_spec.rb +++ b/spec/services/proforma_service/convert_exercise_to_task_spec.rb @@ -185,7 +185,7 @@ it 'creates a model-solution with one file' do expect(task.model_solutions.first).to have_attributes( - id: "ms-#{file.id}", + id: "co-ms-#{file.id}", files: have(1).item ) end @@ -210,6 +210,18 @@ it 'creates a task with two model-solutions' do expect(task.model_solutions).to have(2).items end + + context 'when reference_implementations belong to the same proforma model_solution' do + let(:files) { build_list(:file, 2, role: 'reference_implementation') {|file, i| file.xml_id_path = "proforma_ms/xml_id_#{i}" } } + + it 'creates a task with one model-solution' do + expect(task.model_solutions).to have(1).items + end + + it 'creates a task with a model-solution with two files' do + expect(task.model_solutions.first.files).to have(2).items + end + end end context 'when exercise has a test' do @@ -223,14 +235,19 @@ it 'creates a test with one file' do expect(task.tests.first).to have_attributes( - id: test_file.id, + id: "co-test-#{test_file.id}", title: test_file.name, files: have(1).item, meta_data: a_hash_including( 'test-meta-data' => a_hash_including( - 'CodeOcean:feedback-message' => {'$1' => 'feedback_message', '@@order' => ['$1']}, - 'CodeOcean:weight' => {'$1' => test_file.weight, '@@order' => ['$1']}, - 'CodeOcean:hidden-feedback' => {'$1' => test_file.hidden_feedback, '@@order' => ['$1']} + 'CodeOcean:test-file' => a_hash_including( + "CodeOcean:#{test_file.id}" => a_hash_including( + '@name' => test_file.name, + 'CodeOcean:feedback-message' => {'$1' => 'feedback_message', '@@order' => ['$1']}, + 'CodeOcean:weight' => {'$1' => test_file.weight, '@@order' => ['$1']}, + 'CodeOcean:hidden-feedback' => {'$1' => test_file.hidden_feedback, '@@order' => ['$1']} + ) + ) ) ) ) @@ -263,6 +280,36 @@ it 'creates a task with two tests' do expect(task.tests).to have(2).items end + + context 'when test_files belong to the same proforma test' do + let(:tests) { build_list(:test_file, 2) {|file, i| file.xml_id_path = "proforma_test/xml_id_#{i}" } } + + it 'creates a test with two file' do + expect(task.tests.first).to have_attributes( + id: 'proforma_test', + title: tests.first.name, + files: have(2).item, + meta_data: a_hash_including( + 'test-meta-data' => a_hash_including( + 'CodeOcean:test-file' => a_hash_including( + 'CodeOcean:xml_id_0' => a_hash_including( + '@name' => tests.first.name, + 'CodeOcean:feedback-message' => {'$1' => 'feedback_message', '@@order' => ['$1']}, + 'CodeOcean:weight' => {'$1' => 1, '@@order' => ['$1']}, + 'CodeOcean:hidden-feedback' => {'$1' => tests.first.hidden_feedback, '@@order' => ['$1']} + ), + 'CodeOcean:xml_id_1' => a_hash_including( + '@name' => tests.last.name, + 'CodeOcean:feedback-message' => {'$1' => 'feedback_message', '@@order' => ['$1']}, + 'CodeOcean:weight' => {'$1' => 1, '@@order' => ['$1']}, + 'CodeOcean:hidden-feedback' => {'$1' => tests.last.hidden_feedback, '@@order' => ['$1']} + ) + ) + ) + ) + ) + end + end end end end diff --git a/spec/services/proforma_service/convert_task_to_exercise_spec.rb b/spec/services/proforma_service/convert_task_to_exercise_spec.rb index 12438d9b3..6ac2ced83 100644 --- a/spec/services/proforma_service/convert_task_to_exercise_spec.rb +++ b/spec/services/proforma_service/convert_task_to_exercise_spec.rb @@ -361,10 +361,18 @@ files: test_files, meta_data: { 'test-meta-data' => { - '@@order' => %w[CodeOcean:feedback-message CodeOcean:weight CodeOcean:hidden-feedback], - 'CodeOcean:feedback-message' => {'$1' => 'feedback-message', '@@order' => ['$1']}, - 'CodeOcean:weight' => {'$1' => '0.7', '@@order' => ['$1']}, - 'CodeOcean:hidden-feedback' => {'$1' => 'true', '@@order' => ['$1']}, + '@@order' => %w[CodeOcean:test-file], + '@xmlns' => {'CodeOcean' => 'codeocean.openhpi.de'}, + 'CodeOcean:test-file' => { + 'CodeOcean:test_file_id' => + { + '@@order' => %w[CodeOcean:feedback-message CodeOcean:weight CodeOcean:hidden-feedback], + '@name' => 'test_file_name', + 'CodeOcean:feedback-message' => {'$1' => 'feedback-message', '@@order' => ['$1']}, + 'CodeOcean:weight' => {'$1' => '0.7', '@@order' => ['$1']}, + 'CodeOcean:hidden-feedback' => {'$1' => 'true', '@@order' => ['$1']}, + }, + }, }, } )