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

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

View File

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

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

@ -4,11 +4,11 @@
= 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')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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