diff --git a/Gemfile b/Gemfile index 5f1dc5ee..2607ffbe 100644 --- a/Gemfile +++ b/Gemfile @@ -29,7 +29,7 @@ gem 'net-smtp', require: false gem 'nokogiri' gem 'pagedown-bootstrap-rails' gem 'pg' -gem 'proforma', github: 'openHPI/proforma', branch: 'v0.5.2' +gem 'proforma', github: 'openHPI/proforma', tag: 'v0.7.1' gem 'prometheus_exporter' gem 'pry-byebug' gem 'puma' diff --git a/Gemfile.lock b/Gemfile.lock index 92824b75..98c3498e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,13 @@ GIT remote: https://github.com/openHPI/proforma.git - revision: 243853e66034bc2afbb9c9661475d9718d007304 - branch: v0.5.2 + revision: cf61517a5cd765afb9d0d19ea1c692e18e3131d7 + tag: v0.7.1 specs: - proforma (0.5.2) - activemodel (>= 5.2.3, < 7.2.0) - activesupport (>= 5.2.3, < 7.2.0) - nokogiri (~> 1.13) - rubyzip (~> 2.3) + proforma (0.7.1) + activemodel (>= 5.2.3, < 8.0.0) + activesupport (>= 5.2.3, < 8.0.0) + nokogiri (>= 1.10.2, < 2.0.0) + rubyzip (>= 1.2.2, < 3.0.0) GIT remote: https://github.com/teamcapybara/capybara.git diff --git a/app/controllers/codeharbor_links_controller.rb b/app/controllers/codeharbor_links_controller.rb index c98b02b1..0daeb4ea 100644 --- a/app/controllers/codeharbor_links_controller.rb +++ b/app/controllers/codeharbor_links_controller.rb @@ -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 diff --git a/app/controllers/exercises_controller.rb b/app/controllers/exercises_controller.rb index 48595918..d6001bf1 100644 --- a/app/controllers/exercises_controller.rb +++ b/app/controllers/exercises_controller.rb @@ -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 diff --git a/app/services/exercise_service/check_external.rb b/app/services/exercise_service/check_external.rb index 79836a44..04392d4c 100644 --- a/app/services/exercise_service/check_external.rb +++ b/app/services/exercise_service/check_external.rb @@ -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 diff --git a/app/services/proforma_service/convert_exercise_to_task.rb b/app/services/proforma_service/convert_exercise_to_task.rb index 686932ac..0acb062a 100644 --- a/app/services/proforma_service/convert_exercise_to_task.rb +++ b/app/services/proforma_service/convert_exercise_to_task.rb @@ -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_(?[^:]*):(?[^-]*)(?>-.*)?$} + 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 diff --git a/app/services/proforma_service/convert_task_to_exercise.rb b/app/services/proforma_service/convert_task_to_exercise.rb index a147c7cd..c32e5321 100644 --- a/app/services/proforma_service/convert_task_to_exercise.rb +++ b/app/services/proforma_service/convert_task_to_exercise.rb @@ -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 diff --git a/app/services/proforma_service/export_task.rb b/app/services/proforma_service/export_task.rb index ac5ed627..ca97f1c7 100644 --- a/app/services/proforma_service/export_task.rb +++ b/app/services/proforma_service/export_task.rb @@ -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 diff --git a/app/services/proforma_service/import.rb b/app/services/proforma_service/import.rb index 2aa6b6e2..2fa24383 100644 --- a/app/services/proforma_service/import.rb +++ b/app/services/proforma_service/import.rb @@ -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 diff --git a/app/views/exercises/_export_actions.html.slim b/app/views/exercises/_export_actions.html.slim index c545172d..73993cb9 100644 --- a/app/views/exercises/_export_actions.html.slim +++ b/app/views/exercises/_export_actions.html.slim @@ -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') diff --git a/config/locales/de.yml b/config/locales/de.yml index 532960d4..1e8305f3 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -402,9 +402,9 @@ de: close: Schließen abort: Abbrechen check: - no_exercise: Auf CodeHarbor wurde keine entsprechende Aufgabe gefunden. Mit dem Export der Aufgabe wird eine neue auf CodeHarbor angelegt, die mit dieser verbunden ist. Anschließend können Veränderungen an der Aufgabe von beiden Systemen aus jeweils in das andere Übertragen werden. - exercise_found: Auf CodeHarbor wurde eine entsprechende Aufgabe gefunden. Mit dem Export der Aufgabe werden alle Veränderungen, die auf Codeocean vorgenommen wurden, exportiert und die Aufgabe auf CodeHarbor überschrieben. - exercise_found_no_right: Auf CodeHarbor wurde eine entsprechende Aufgabe gefunden, Sie haben aber keine Rechte sie zu bearbeiten. Bitte wenden Sie sich an einen Admin, wenn Sie denken, dass Sie die Rechte dazu besitzen sollten. + no_task: Auf CodeHarbor wurde keine entsprechende Aufgabe gefunden. Mit dem Export der Aufgabe wird eine neue auf CodeHarbor angelegt, die mit dieser verbunden ist. Anschließend können Veränderungen an der Aufgabe von beiden Systemen aus jeweils in das andere Übertragen werden. + task_found: Auf CodeHarbor wurde eine entsprechende Aufgabe gefunden. Mit dem Export der Aufgabe werden alle Veränderungen, die auf CodeOcean vorgenommen wurden, exportiert und die Aufgabe auf CodeHarbor überschrieben. + task_found_no_right: Auf CodeHarbor wurde eine entsprechende Aufgabe gefunden, Sie haben aber keine Rechte sie zu bearbeiten. Bitte wenden Sie sich an einen Admin, wenn Sie denken, dass Sie die Rechte dazu besitzen sollten. file_form: hints: feedback_message: Diese Nachricht wird als Hinweis zu fehlschlagenden Tests angezeigt. diff --git a/config/locales/en.yml b/config/locales/en.yml index ffd330c7..8deb9f84 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -402,9 +402,9 @@ en: close: Close abort: Abort check: - no_exercise: No corresponding exercise found on CodeHarbor. Pushing this exercise will create a new exercise on CodeHarbor, which will be linked to this one on Codeocean. Any changes to either one can be pushed to the respective other platform. - exercise_found: 'A corresponding exercise has been found on CodeHarbor. You can export the exercise to transfer all changes made on Codeocean to CodeHarbor. Careful: This will overwrite all potential changes made on CodeHarbor.' - exercise_found_no_right: A corresponding exercise has been found on CodeHarbor, but you don't have the rights to edit it. Please contact an Admin if you think you should be able to edit the exercise on CodeHarbor. + no_task: No corresponding task found on CodeHarbor. Pushing this exercise will create a new task on CodeHarbor, which will be linked to this one on CodeOcean. Any changes to either one can be pushed to the respective other platform. + task_found: 'A corresponding task has been found on CodeHarbor. You can export the exercise to transfer all changes made on CodeOcean to CodeHarbor. Careful: This will overwrite all potential changes made on CodeHarbor.' + task_found_no_right: A corresponding task has been found on CodeHarbor, but you don't have the rights to edit it. Please contact an Admin if you think you should be able to edit the task on CodeHarbor. file_form: hints: feedback_message: This message is used as a hint for failing tests. diff --git a/config/routes.rb b/config/routes.rb index 30a9bd9b..81781f70 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -73,7 +73,7 @@ Rails.application.routes.draw do post :sync_all_to_runner_management, on: :collection end - post '/import_exercise' => 'exercises#import_exercise' + post '/import_task' => 'exercises#import_task' post '/import_uuid_check' => 'exercises#import_uuid_check' resources :exercises do diff --git a/spec/controllers/exercises_controller_spec.rb b/spec/controllers/exercises_controller_spec.rb index dab8b754..b6c38477 100644 --- a/spec/controllers/exercises_controller_spec.rb +++ b/spec/controllers/exercises_controller_spec.rb @@ -403,7 +403,7 @@ describe ExercisesController do let(:post_request) { post :export_external_check, params: {id: exercise.id} } let!(:codeharbor_link) { create(:codeharbor_link, user: user) } - let(:external_check_hash) { {message: message, exercise_found: true, update_right: update_right, error: error} } + let(:external_check_hash) { {message: message, uuid_found: true, update_right: update_right, error: error} } let(:message) { 'message' } let(:update_right) { true } let(:error) { nil } @@ -452,7 +452,7 @@ describe ExercisesController do end end - describe '#export_external_confirm' do + describe 'POST #export_external_confirm' do render_views let!(:codeharbor_link) { create(:codeharbor_link, user: user) } @@ -489,7 +489,7 @@ describe ExercisesController do end end - describe '#import_uuid_check' do + describe 'POST #import_uuid_check' do let(:exercise) { create(:dummy, uuid: SecureRandom.uuid) } let!(:codeharbor_link) { create(:codeharbor_link, user: user) } let(:uuid) { exercise.reload.uuid } @@ -502,7 +502,7 @@ describe ExercisesController do post_request expect(response).to have_http_status(:success) - expect(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be true + expect(JSON.parse(response.body).symbolize_keys[:uuid_found]).to be true expect(JSON.parse(response.body).symbolize_keys[:update_right]).to be true end @@ -522,7 +522,7 @@ describe ExercisesController do post_request expect(response).to have_http_status(:success) - expect(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be true + expect(JSON.parse(response.body).symbolize_keys[:uuid_found]).to be true expect(JSON.parse(response.body).symbolize_keys[:update_right]).to be false end end @@ -534,15 +534,15 @@ describe ExercisesController do post_request expect(response).to have_http_status(:success) - expect(JSON.parse(response.body).symbolize_keys[:exercise_found]).to be false + expect(JSON.parse(response.body).symbolize_keys[:uuid_found]).to be false end end end - describe 'POST #import_exercise' do + describe 'POST #import_task' do let(:codeharbor_link) { create(:codeharbor_link, user: user) } let!(:imported_exercise) { create(:fibonacci) } - let(:post_request) { post :import_exercise, body: zip_file_content } + let(:post_request) { post :import_task, body: zip_file_content } let(:zip_file_content) { 'zipped task xml' } let(:headers) { {'Authorization' => "Bearer #{codeharbor_link.api_key}"} } diff --git a/spec/services/exercise_service/check_external_spec.rb b/spec/services/exercise_service/check_external_spec.rb index a8c74c4d..4244e85b 100644 --- a/spec/services/exercise_service/check_external_spec.rb +++ b/spec/services/exercise_service/check_external_spec.rb @@ -42,25 +42,25 @@ describe ExerciseService::CheckExternal do end context 'when response contains a JSON with expected keys' do - let(:response) { {exercise_found: true, update_right: true}.to_json } + let(:response) { {uuid_found: true, update_right: true}.to_json } it 'returns the correct hash' do - expect(check_external_service).to eql(error: false, message: I18n.t('exercises.export_codeharbor.check.exercise_found'), exercise_found: true, update_right: true) + expect(check_external_service).to eql(error: false, message: I18n.t('exercises.export_codeharbor.check.task_found'), uuid_found: true, update_right: true) end - context 'with exercise_found: false and no update_right' do - let(:response) { {exercise_found: false}.to_json } + context 'with uuid_found: false and no update_right' do + let(:response) { {uuid_found: false}.to_json } it 'returns the correct hash' do - expect(check_external_service).to eql(error: false, message: I18n.t('exercises.export_codeharbor.check.no_exercise'), exercise_found: false) + expect(check_external_service).to eql(error: false, message: I18n.t('exercises.export_codeharbor.check.no_task'), uuid_found: false) end end - context 'with exercise_found: true and update_right: false' do - let(:response) { {exercise_found: true, update_right: false}.to_json } + context 'with uuid_found: true and update_right: false' do + let(:response) { {uuid_found: true, update_right: false}.to_json } it 'returns the correct hash' do - expect(check_external_service).to eql(error: false, message: I18n.t('exercises.export_codeharbor.check.exercise_found_no_right'), exercise_found: true, update_right: false) + expect(check_external_service).to eql(error: false, message: I18n.t('exercises.export_codeharbor.check.task_found_no_right'), uuid_found: true, update_right: false) end end end 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 b367dda9..bf9f5b40 100644 --- a/spec/services/proforma_service/convert_exercise_to_task_spec.rb +++ b/spec/services/proforma_service/convert_exercise_to_task_spec.rb @@ -19,31 +19,44 @@ RSpec.describe ProformaService::ConvertExerciseToTask do let(:convert_to_task) { described_class.new(exercise: exercise) } let(:exercise) do create(:dummy, + execution_environment: execution_environment, instructions: 'instruction', uuid: SecureRandom.uuid, files: files + tests) end let(:files) { [] } let(:tests) { [] } + let(:execution_environment) { create(:java) } it 'creates a task with all basic attributes' do expect(task).to have_attributes( title: exercise.title, description: exercise.description, - internal_description: exercise.instructions, - # proglang: { - # name: exercise.execution_environment.language, - # version: exercise.execution_environment.version - # }, uuid: exercise.uuid, language: described_class::DEFAULT_LANGUAGE, - # parent_uuid: exercise.clone_relations.first&.origin&.uuid, + meta_data: { + CodeOcean: { + allow_auto_completion: exercise.allow_auto_completion, + allow_file_creation: exercise.allow_file_creation, + execution_environment_id: exercise.execution_environment_id, + expected_difficulty: exercise.expected_difficulty, + hide_file_tree: exercise.hide_file_tree, + public: exercise.public, + files: {}, + }, + }, files: [], tests: [], model_solutions: [] ) end + context 'when exercise has execution_environment with correct docker-image name' do + it 'creates a task with the correct proglang attribute' do + expect(task).to have_attributes(proglang: {name: 'java', version: '8'}) + end + end + context 'when exercise has a mainfile' do let(:files) { [file] } let(:file) { build(:file) } @@ -57,7 +70,15 @@ RSpec.describe ProformaService::ConvertExerciseToTask do usage_by_lms: 'edit', visible: 'yes', binary: false, - internal_description: 'main_file' + internal_description: nil + ) + end + + it 'adds the file\'s role to the file hash in task-meta_data' do + expect(task).to have_attributes( + meta_data: { + CodeOcean: a_hash_including(files: {"CO-#{file.id}" => {role: 'main_file'}}), + } ) end end @@ -77,7 +98,15 @@ RSpec.describe ProformaService::ConvertExerciseToTask do usage_by_lms: 'display', visible: 'no', binary: false, - internal_description: 'regular_file' + internal_description: nil + ) + end + + it 'adds the file\'s role to the file hash in task-meta_data' do + expect(task).to have_attributes( + meta_data: { + CodeOcean: a_hash_including(files: {"CO-#{file.id}" => {role: 'regular_file'}}), + } ) end @@ -134,7 +163,7 @@ RSpec.describe ProformaService::ConvertExerciseToTask do usage_by_lms: 'display', visible: 'yes', binary: false, - internal_description: 'reference_implementation' + internal_description: nil ) end end @@ -161,7 +190,12 @@ RSpec.describe ProformaService::ConvertExerciseToTask do id: test_file.id, title: test_file.name, files: have(1).item, - meta_data: {'feedback-message' => test_file.feedback_message} + meta_data: { + CodeOcean: { + 'feedback-message': 'feedback_message', + weight: test_file.weight, + }, + } ) end @@ -173,7 +207,7 @@ RSpec.describe ProformaService::ConvertExerciseToTask do used_by_grader: true, visible: 'no', binary: false, - internal_description: 'teacher_defined_test' + internal_description: nil ) 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 02ae9224..f8a06cbc 100644 --- a/spec/services/proforma_service/convert_task_to_exercise_spec.rb +++ b/spec/services/proforma_service/convert_task_to_exercise_spec.rb @@ -3,8 +3,6 @@ require 'rails_helper' describe ProformaService::ConvertTaskToExercise do - # TODO: Add teacher_defined_linter for tests - describe '.new' do subject(:convert_to_exercise_service) { described_class.new(task: task, user: user, exercise: exercise) } @@ -34,35 +32,94 @@ describe ProformaService::ConvertTaskToExercise do Proforma::Task.new( title: 'title', description: 'description', - internal_description: 'internal_description', - proglang: {name: 'proglang-name', version: 'proglang-version'}, + proglang: {name: 'python', version: '3.4'}, uuid: 'uuid', parent_uuid: 'parent_uuid', language: 'language', + meta_data: meta_data, model_solutions: model_solutions, files: files, tests: tests ) end let(:user) { create(:teacher) } + let(:files) { [] } let(:tests) { [] } let(:model_solutions) { [] } let(:exercise) { nil } + let(:meta_data) { {} } + let(:public) { 'true' } + let(:hide_file_tree) { 'true' } + let(:allow_file_creation) { 'true' } + let(:allow_auto_completion) { 'true' } + let(:expected_difficulty) { 7 } + let!(:execution_environment) { create(:java) } + it 'creates an exercise with the correct attributes' do expect(convert_to_exercise_service).to have_attributes( title: 'title', description: 'description', - instructions: 'internal_description', - execution_environment: be_blank, uuid: be_blank, unpublished: true, user: user, - files: be_empty + files: be_empty, + public: false, + hide_file_tree: false, + allow_file_creation: false, + allow_auto_completion: false, + expected_difficulty: 1, + execution_environment_id: nil ) end + it { is_expected.to be_valid } + + context 'when meta_data is set' do + let(:meta_data) do + { + CodeOcean: { + public: public, + hide_file_tree: hide_file_tree, + allow_file_creation: allow_file_creation, + allow_auto_completion: allow_auto_completion, + expected_difficulty: expected_difficulty, + execution_environment_id: execution_environment&.id, + files: files_meta_data, + }, + } + end + let(:files_meta_data) { {} } + + it 'creates an exercise with the correct attributes' do + expect(convert_to_exercise_service).to have_attributes( + title: 'title', + description: 'description', + uuid: be_blank, + unpublished: true, + user: user, + files: be_empty, + public: true, + hide_file_tree: true, + allow_file_creation: true, + allow_auto_completion: true, + expected_difficulty: 7, + execution_environment_id: execution_environment.id + ) + end + end + + context 'when execution environment is not set in meta_data' do + let(:execution_environment) { nil } + + before { create(:python) } + + it 'sets the execution_environment based on proglang name and value' do + expect(convert_to_exercise_service).to have_attributes(execution_environment: have_attributes(name: 'Python 3.4')) + end + end + context 'when task has a file' do let(:files) { [file] } let(:file) do @@ -74,7 +131,6 @@ describe ProformaService::ConvertTaskToExercise do visible: 'yes', usage_by_lms: usage_by_lms, binary: binary, - internal_description: 'regular_file', mimetype: mimetype ) end @@ -101,6 +157,21 @@ describe ProformaService::ConvertTaskToExercise do expect { convert_to_exercise_service.save! }.to change(Exercise, :count).by(1) end + context 'when file is a main_file' do + let(:meta_data) do + { + CodeOcean: { + files: files_meta_data, + }, + } + end + let(:files_meta_data) { {"CO-#{file.id}".to_sym => {role: 'main_file'}} } + + it 'creates an exercise with a file that has the correct attributes' do + expect(convert_to_exercise_service.files.first).to have_attributes(role: 'main_file') + end + end + context 'when path is folder/' do let(:path) { 'folder/' } @@ -150,7 +221,7 @@ describe ProformaService::ConvertTaskToExercise do end end - context 'when file has an unkown file_type' do + context 'when file has an unknown file_type' do let(:filename) { 'unknown_file_type.asdf' } it 'creates a new Exercise on save' do @@ -180,8 +251,7 @@ describe ProformaService::ConvertTaskToExercise do used_by_grader: 'used_by_grader', visible: 'yes', usage_by_lms: 'display', - binary: false, - internal_description: 'reference_implementation' + binary: false ) end @@ -226,13 +296,14 @@ describe ProformaService::ConvertTaskToExercise do id: 'test-id', title: 'title', description: 'description', - internal_description: 'internal_description', test_type: 'test_type', files: test_files, meta_data: { - 'feedback-message' => 'feedback-message', - 'testing-framework' => 'testing-framework', - 'testing-framework-version' => 'testing-framework-version', + CodeOcean: { + 'feedback-message': 'feedback-message', + 'testing-framework': 'testing-framework', + 'testing-framework-version': 'testing-framework-version', + }, } ) end @@ -267,15 +338,32 @@ describe ProformaService::ConvertTaskToExercise do ) end + context 'when test file is a teacher_defined_linter' do + let(:meta_data) do + { + CodeOcean: { + files: files_meta_data, + }, + } + end + let(:files_meta_data) { {"CO-#{test_file.id}".to_sym => {role: 'teacher_defined_linter'}} } + + it 'creates an exercise with a test' do + expect(convert_to_exercise_service.files.select {|file| file.role == 'teacher_defined_linter' }).to have(1).item + end + end + context 'when task has multiple tests' do let(:tests) { [test, test2] } let(:test2) do Proforma::Test.new( files: test_files2, meta_data: { - 'feedback-message' => 'feedback-message', - 'testing-framework' => 'testing-framework', - 'testing-framework-version' => 'testing-framework-version', + CodeOcean: { + 'feedback-message': 'feedback-message', + 'testing-framework': 'testing-framework', + 'testing-framework-version': 'testing-framework-version', + }, } ) end @@ -304,8 +392,7 @@ describe ProformaService::ConvertTaskToExercise do create( :files, title: 'exercise-title', - description: 'exercise-description', - instructions: 'exercise-instruction' + description: 'exercise-description' ) end @@ -315,9 +402,8 @@ describe ProformaService::ConvertTaskToExercise do convert_to_exercise_service.save expect(exercise.reload).to have_attributes( id: exercise.id, - title: task.title, - description: task.description, - instructions: task.internal_description, + title: exercise.title, + description: exercise.description, execution_environment: exercise.execution_environment, uuid: exercise.uuid, user: exercise.user, @@ -339,8 +425,7 @@ describe ProformaService::ConvertTaskToExercise do used_by_grader: 'used_by_grader', visible: 'yes', usage_by_lms: 'display', - binary: false, - internal_description: 'regular_file' + binary: false ) end let(:tests) { [test] } @@ -349,13 +434,14 @@ describe ProformaService::ConvertTaskToExercise do id: 'test-id', title: 'title', description: 'description', - internal_description: 'regular_file', test_type: 'test_type', files: test_files, meta_data: { - 'feedback-message' => 'feedback-message', - 'testing-framework' => 'testing-framework', - 'testing-framework-version' => 'testing-framework-version', + CodeOcean: { + 'feedback-message': 'feedback-message', + 'testing-framework': 'testing-framework', + 'testing-framework-version': 'testing-framework-version', + }, } ) end diff --git a/spec/services/proforma_service/export_task_spec.rb b/spec/services/proforma_service/export_task_spec.rb index ed4b5fdf..211bd4f4 100644 --- a/spec/services/proforma_service/export_task_spec.rb +++ b/spec/services/proforma_service/export_task_spec.rb @@ -30,7 +30,7 @@ describe ProformaService::ExportTask do before do allow(ProformaService::ConvertExerciseToTask).to receive(:call).with(exercise: exercise).and_return(task) - allow(Proforma::Exporter).to receive(:new).with(task).and_return(exporter) + allow(Proforma::Exporter).to receive(:new).with(task: task, custom_namespaces: [{prefix: 'CodeOcean', uri: 'codeocean.openhpi.de'}]).and_return(exporter) end it do diff --git a/spec/services/proforma_service/import_spec.rb b/spec/services/proforma_service/import_spec.rb index 1f27d731..359c3500 100644 --- a/spec/services/proforma_service/import_spec.rb +++ b/spec/services/proforma_service/import_spec.rb @@ -29,6 +29,10 @@ describe ProformaService::Import do instructions: 'instruction', execution_environment: execution_environment, files: files + tests, + hide_file_tree: true, + allow_file_creation: false, + allow_auto_completion: true, + expected_difficulty: 7, uuid: uuid, user: user) end diff --git a/spec/support/expectations/equal_exercise.rb b/spec/support/expectations/equal_exercise.rb index 5cdaece9..289941f5 100644 --- a/spec/support/expectations/equal_exercise.rb +++ b/spec/support/expectations/equal_exercise.rb @@ -31,7 +31,11 @@ RSpec::Matchers.define :be_an_equal_exercise_as do |exercise| return true if object == other # for [] return false if object.length != other.length - object.to_a.product(other.to_a).map {|k, v| equal?(k, v) }.any? + object.map do |element| + other.map {|other_element| equal?(element, other_element) }.any? + end.all? && other.map do |element| + object.map {|other_element| equal?(element, other_element) }.any? + end.all? end def attributes_and_associations(object)