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 'nokogiri'
gem 'pagedown-bootstrap-rails' gem 'pagedown-bootstrap-rails'
gem 'pg' gem 'pg'
gem 'proforma', github: 'openHPI/proforma', branch: 'v0.5.2' gem 'proforma', github: 'openHPI/proforma', tag: 'v0.7.1'
gem 'prometheus_exporter' gem 'prometheus_exporter'
gem 'pry-byebug' gem 'pry-byebug'
gem 'puma' gem 'puma'

View File

@ -1,13 +1,13 @@
GIT GIT
remote: https://github.com/openHPI/proforma.git remote: https://github.com/openHPI/proforma.git
revision: 243853e66034bc2afbb9c9661475d9718d007304 revision: cf61517a5cd765afb9d0d19ea1c692e18e3131d7
branch: v0.5.2 tag: v0.7.1
specs: specs:
proforma (0.5.2) proforma (0.7.1)
activemodel (>= 5.2.3, < 7.2.0) activemodel (>= 5.2.3, < 8.0.0)
activesupport (>= 5.2.3, < 7.2.0) activesupport (>= 5.2.3, < 8.0.0)
nokogiri (~> 1.13) nokogiri (>= 1.10.2, < 2.0.0)
rubyzip (~> 2.3) rubyzip (>= 1.2.2, < 3.0.0)
GIT GIT
remote: https://github.com/teamcapybara/capybara.git remote: https://github.com/teamcapybara/capybara.git

View File

@ -7,7 +7,7 @@ class CodeharborLinksController < ApplicationController
def new def new
base_url = CodeOcean::Config.new(:code_ocean).read[:codeharbor][:url] || '' 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") check_uuid_url: "#{base_url}/import_uuid_check")
authorize! authorize!
end end

View File

@ -19,9 +19,9 @@ class ExercisesController < ApplicationController
before_action :set_course_token, only: [:implement] before_action :set_course_token, only: [:implement]
before_action :set_available_tips, only: %i[implement show new edit] 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_before_action :verify_authenticity_token, only: %i[import_task import_uuid_check]
skip_after_action :verify_authorized, only: %i[import_exercise import_uuid_check] skip_after_action :verify_authorized, only: %i[import_task import_uuid_check]
skip_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check], raise: false skip_after_action :verify_policy_scoped, only: %i[import_task import_uuid_check], raise: false
rescue_from Pundit::NotAuthorizedError, with: :not_authorized_for_exercise rescue_from Pundit::NotAuthorizedError, with: :not_authorized_for_exercise
@ -106,7 +106,7 @@ class ExercisesController < ApplicationController
partial: 'export_actions', partial: 'export_actions',
locals: { locals: {
exercise: @exercise, exercise: @exercise,
exercise_found: codeharbor_check[:exercise_found], uuid_found: codeharbor_check[:uuid_found],
update_right: codeharbor_check[:update_right], update_right: codeharbor_check[:update_right],
error: codeharbor_check[:error], error: codeharbor_check[:error],
exported: false, exported: false,
@ -148,13 +148,13 @@ class ExercisesController < ApplicationController
uuid = params[:uuid] uuid = params[:uuid]
exercise = Exercise.find_by(uuid: uuid) exercise = Exercise.find_by(uuid: uuid)
return render json: {exercise_found: false} if exercise.nil? return render json: {uuid_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: 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 end
def import_exercise def import_task
tempfile = Tempfile.new('codeharbor_import.zip') tempfile = Tempfile.new('codeharbor_import.zip')
tempfile.write request.body.read.force_encoding('UTF-8') tempfile.write request.body.read.force_encoding('UTF-8')
tempfile.rewind tempfile.rewind

View File

@ -14,21 +14,20 @@ module ExerciseService
req.headers['Authorization'] = "Bearer #{@codeharbor_link.api_key}" req.headers['Authorization'] = "Bearer #{@codeharbor_link.api_key}"
req.body = {uuid: @uuid}.to_json req.body = {uuid: @uuid}.to_json
end 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, {error: false, message: message(response_hash[:uuid_found], response_hash[:update_right])}.merge(response_hash)
message: message(response_hash[:exercise_found], response_hash[:update_right])}.merge(response_hash)
rescue Faraday::Error, JSON::ParserError rescue Faraday::Error, JSON::ParserError
{error: true, message: I18n.t('exercises.export_codeharbor.error')} {error: true, message: I18n.t('exercises.export_codeharbor.error')}
end end
private private
def message(exercise_found, update_right) def message(task_found, update_right)
if exercise_found if task_found
update_right ? I18n.t('exercises.export_codeharbor.check.exercise_found') : I18n.t('exercises.export_codeharbor.check.exercise_found_no_right') update_right ? I18n.t('exercises.export_codeharbor.check.task_found') : I18n.t('exercises.export_codeharbor.check.task_found_no_right')
else else
I18n.t('exercises.export_codeharbor.check.no_exercise') I18n.t('exercises.export_codeharbor.check.no_task')
end end
end end

View File

@ -22,16 +22,34 @@ module ProformaService
{ {
title: @exercise.title, title: @exercise.title,
description: @exercise.description, description: @exercise.description,
internal_description: @exercise.instructions, internal_description: nil,
proglang: proglang,
files: task_files, files: task_files,
tests: tests, tests: tests,
uuid: uuid, uuid: uuid,
language: DEFAULT_LANGUAGE, language: DEFAULT_LANGUAGE,
model_solutions: model_solutions, 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 }.compact
) )
end 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 def uuid
@exercise.update(uuid: SecureRandom.uuid) if @exercise.uuid.nil? @exercise.update(uuid: SecureRandom.uuid) if @exercise.uuid.nil?
@exercise.uuid @exercise.uuid
@ -56,35 +74,49 @@ module ProformaService
end end
def tests def tests
@exercise.files.filter do |file| @exercise.files.filter(&:teacher_defined_assessment?).map do |file|
file.role == 'teacher_defined_test' || file.role == 'teacher_defined_linter'
end.map do |file|
Proforma::Test.new( Proforma::Test.new(
id: file.id, id: file.id,
title: file.name, title: file.name,
files: test_file(file), files: test_file(file),
meta_data: { meta_data: test_meta_data(file)
'feedback-message' => file.feedback_message,
}.compact
) )
end end
end end
def test_meta_data(file)
{
CodeOcean: {
'feedback-message': file.feedback_message,
weight: file.weight,
},
}
end
def test_file(file) def test_file(file)
[ [
task_file(file).tap do |t_file| task_file(file).tap do |t_file|
t_file.used_by_grader = true t_file.used_by_grader = true
t_file.internal_description = 'teacher_defined_test'
end, end,
] ]
end end
def task_files def exercise_files
@exercise.files @exercise.files.filter do |file|
.filter do |file|
!file.role.in? %w[reference_implementation teacher_defined_test !file.role.in? %w[reference_implementation teacher_defined_test
teacher_defined_linter] 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) task_file(file)
end end
end end
@ -94,8 +126,7 @@ module ProformaService
id: file.id, id: file.id,
filename: filename(file), filename: filename(file),
usage_by_lms: file.read_only ? 'display' : 'edit', usage_by_lms: file.read_only ? 'display' : 'edit',
visible: file.hidden ? 'no' : 'yes', visible: file.hidden ? 'no' : 'yes'
internal_description: file.role || 'regular_file'
) )
add_content_to_task_file(file, task_file) add_content_to_task_file(file, task_file)
task_file task_file
@ -103,8 +134,7 @@ module ProformaService
def filename(file) def filename(file)
if file.path.present? && file.path != '.' if file.path.present? && file.path != '.'
::File.join(file.path, ::File.join(file.path, file.name_with_extension)
file.name_with_extension)
else else
file.name_with_extension file.name_with_extension
end end

View File

@ -10,31 +10,67 @@ module ProformaService
end end
def execute def execute
import_exercise import_task
@exercise @exercise
end end
private private
def import_exercise def import_task
@exercise.assign_attributes( @exercise.assign_attributes(
user: @user, user: @user,
title: @task.title, title: @task.title,
description: @task.description, 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 files: files
) )
end 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 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 end
def test_files def test_files
@task.tests.map do |test_object| @task.tests.map do |test_object|
task_files.delete(test_object.files.first.id).tap do |file| task_files.delete(test_object.files.first.id).tap do |file|
file.weight = 1.0 file.weight = test_object.meta_data[:CodeOcean]&.dig(:weight) || 1.0
file.feedback_message = test_object.meta_data['feedback-message'] 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 end
end end
@ -53,7 +89,7 @@ module ProformaService
hidden: file.visible == 'no', hidden: file.visible == 'no',
name: File.basename(file.filename, '.*'), name: File.basename(file.filename, '.*'),
read_only: file.usage_by_lms != 'edit', 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) path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename)
) )
if file.binary if file.binary

View File

@ -9,7 +9,8 @@ module ProformaService
def execute def execute
@task = ConvertExerciseToTask.call(exercise: @exercise) @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 exporter.perform
end end
end end

View File

@ -10,8 +10,9 @@ module ProformaService
def execute def execute
if single_task? if single_task?
importer = Proforma::Importer.new(@zip) importer = Proforma::Importer.new(zip: @zip)
@task = importer.perform import_result = importer.perform
@task = import_result[:task]
exercise = base_exercise exercise = base_exercise
exercise_files = exercise&.files&.to_a exercise_files = exercise&.files&.to_a

View File

@ -1,14 +1,14 @@
- if error - if error
= button_tag type: 'button', class:'btn btn-primary float-end export-button export-retry-button', data: {exercise_id: exercise.id} do = 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 i.fa-solid.fa-arrows-rotate.confirm-icon
= t('exercises.export_codeharbor.buttons.retry') = t('exercises.export_codeharbor.buttons.retry')
- else - else
- unless exported - 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 = 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 i.fa-solid.fa-check.confirm-icon
= t('exercises.export_codeharbor.buttons.export') = 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 i.fa-solid.fa-xmark.abort-icon
= exported ? t('exercises.export_codeharbor.buttons.close') : t('exercises.export_codeharbor.buttons.abort') = exported ? t('exercises.export_codeharbor.buttons.close') : t('exercises.export_codeharbor.buttons.abort')

View File

@ -402,9 +402,9 @@ de:
close: Schließen close: Schließen
abort: Abbrechen abort: Abbrechen
check: 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. 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.
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. 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.
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. 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: file_form:
hints: hints:
feedback_message: Diese Nachricht wird als Hinweis zu fehlschlagenden Tests angezeigt. feedback_message: Diese Nachricht wird als Hinweis zu fehlschlagenden Tests angezeigt.

View File

@ -402,9 +402,9 @@ en:
close: Close close: Close
abort: Abort abort: Abort
check: 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. 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.
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.' 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.'
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. 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: file_form:
hints: hints:
feedback_message: This message is used as a hint for failing tests. 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 post :sync_all_to_runner_management, on: :collection
end end
post '/import_exercise' => 'exercises#import_exercise' post '/import_task' => 'exercises#import_task'
post '/import_uuid_check' => 'exercises#import_uuid_check' post '/import_uuid_check' => 'exercises#import_uuid_check'
resources :exercises do resources :exercises do

View File

@ -403,7 +403,7 @@ describe ExercisesController do
let(:post_request) { post :export_external_check, params: {id: exercise.id} } let(:post_request) { post :export_external_check, params: {id: exercise.id} }
let!(:codeharbor_link) { create(:codeharbor_link, user: user) } 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(:message) { 'message' }
let(:update_right) { true } let(:update_right) { true }
let(:error) { nil } let(:error) { nil }
@ -452,7 +452,7 @@ describe ExercisesController do
end end
end end
describe '#export_external_confirm' do describe 'POST #export_external_confirm' do
render_views render_views
let!(:codeharbor_link) { create(:codeharbor_link, user: user) } let!(:codeharbor_link) { create(:codeharbor_link, user: user) }
@ -489,7 +489,7 @@ describe ExercisesController do
end end
end end
describe '#import_uuid_check' do describe 'POST #import_uuid_check' do
let(:exercise) { create(:dummy, uuid: SecureRandom.uuid) } let(:exercise) { create(:dummy, uuid: SecureRandom.uuid) }
let!(:codeharbor_link) { create(:codeharbor_link, user: user) } let!(:codeharbor_link) { create(:codeharbor_link, user: user) }
let(:uuid) { exercise.reload.uuid } let(:uuid) { exercise.reload.uuid }
@ -502,7 +502,7 @@ describe ExercisesController do
post_request post_request
expect(response).to have_http_status(:success) 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 expect(JSON.parse(response.body).symbolize_keys[:update_right]).to be true
end end
@ -522,7 +522,7 @@ describe ExercisesController do
post_request post_request
expect(response).to have_http_status(:success) 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 expect(JSON.parse(response.body).symbolize_keys[:update_right]).to be false
end end
end end
@ -534,15 +534,15 @@ describe ExercisesController do
post_request post_request
expect(response).to have_http_status(:success) 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 end
end end
describe 'POST #import_exercise' do describe 'POST #import_task' do
let(:codeharbor_link) { create(:codeharbor_link, user: user) } let(:codeharbor_link) { create(:codeharbor_link, user: user) }
let!(:imported_exercise) { create(:fibonacci) } 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(:zip_file_content) { 'zipped task xml' }
let(:headers) { {'Authorization' => "Bearer #{codeharbor_link.api_key}"} } let(:headers) { {'Authorization' => "Bearer #{codeharbor_link.api_key}"} }

View File

@ -42,25 +42,25 @@ describe ExerciseService::CheckExternal do
end end
context 'when response contains a JSON with expected keys' do 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 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 end
context 'with exercise_found: false and no update_right' do context 'with uuid_found: false and no update_right' do
let(:response) { {exercise_found: false}.to_json } let(:response) { {uuid_found: false}.to_json }
it 'returns the correct hash' do 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
end end
context 'with exercise_found: true and update_right: false' do context 'with uuid_found: true and update_right: false' do
let(:response) { {exercise_found: true, update_right: false}.to_json } let(:response) { {uuid_found: true, update_right: false}.to_json }
it 'returns the correct hash' do 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 end
end end

View File

@ -19,31 +19,44 @@ RSpec.describe ProformaService::ConvertExerciseToTask do
let(:convert_to_task) { described_class.new(exercise: exercise) } let(:convert_to_task) { described_class.new(exercise: exercise) }
let(:exercise) do let(:exercise) do
create(:dummy, create(:dummy,
execution_environment: execution_environment,
instructions: 'instruction', instructions: 'instruction',
uuid: SecureRandom.uuid, uuid: SecureRandom.uuid,
files: files + tests) files: files + tests)
end end
let(:files) { [] } let(:files) { [] }
let(:tests) { [] } let(:tests) { [] }
let(:execution_environment) { create(:java) }
it 'creates a task with all basic attributes' do it 'creates a task with all basic attributes' do
expect(task).to have_attributes( expect(task).to have_attributes(
title: exercise.title, title: exercise.title,
description: exercise.description, description: exercise.description,
internal_description: exercise.instructions,
# proglang: {
# name: exercise.execution_environment.language,
# version: exercise.execution_environment.version
# },
uuid: exercise.uuid, uuid: exercise.uuid,
language: described_class::DEFAULT_LANGUAGE, 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: [], files: [],
tests: [], tests: [],
model_solutions: [] model_solutions: []
) )
end 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 context 'when exercise has a mainfile' do
let(:files) { [file] } let(:files) { [file] }
let(:file) { build(:file) } let(:file) { build(:file) }
@ -57,7 +70,15 @@ RSpec.describe ProformaService::ConvertExerciseToTask do
usage_by_lms: 'edit', usage_by_lms: 'edit',
visible: 'yes', visible: 'yes',
binary: false, 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
end end
@ -77,7 +98,15 @@ RSpec.describe ProformaService::ConvertExerciseToTask do
usage_by_lms: 'display', usage_by_lms: 'display',
visible: 'no', visible: 'no',
binary: false, 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 end
@ -134,7 +163,7 @@ RSpec.describe ProformaService::ConvertExerciseToTask do
usage_by_lms: 'display', usage_by_lms: 'display',
visible: 'yes', visible: 'yes',
binary: false, binary: false,
internal_description: 'reference_implementation' internal_description: nil
) )
end end
end end
@ -161,7 +190,12 @@ RSpec.describe ProformaService::ConvertExerciseToTask do
id: test_file.id, id: test_file.id,
title: test_file.name, title: test_file.name,
files: have(1).item, files: have(1).item,
meta_data: {'feedback-message' => test_file.feedback_message} meta_data: {
CodeOcean: {
'feedback-message': 'feedback_message',
weight: test_file.weight,
},
}
) )
end end
@ -173,7 +207,7 @@ RSpec.describe ProformaService::ConvertExerciseToTask do
used_by_grader: true, used_by_grader: true,
visible: 'no', visible: 'no',
binary: false, binary: false,
internal_description: 'teacher_defined_test' internal_description: nil
) )
end end

View File

@ -3,8 +3,6 @@
require 'rails_helper' require 'rails_helper'
describe ProformaService::ConvertTaskToExercise do describe ProformaService::ConvertTaskToExercise do
# TODO: Add teacher_defined_linter for tests
describe '.new' do describe '.new' do
subject(:convert_to_exercise_service) { described_class.new(task: task, user: user, exercise: exercise) } 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( Proforma::Task.new(
title: 'title', title: 'title',
description: 'description', description: 'description',
internal_description: 'internal_description', proglang: {name: 'python', version: '3.4'},
proglang: {name: 'proglang-name', version: 'proglang-version'},
uuid: 'uuid', uuid: 'uuid',
parent_uuid: 'parent_uuid', parent_uuid: 'parent_uuid',
language: 'language', language: 'language',
meta_data: meta_data,
model_solutions: model_solutions, model_solutions: model_solutions,
files: files, files: files,
tests: tests tests: tests
) )
end end
let(:user) { create(:teacher) } let(:user) { create(:teacher) }
let(:files) { [] } let(:files) { [] }
let(:tests) { [] } let(:tests) { [] }
let(:model_solutions) { [] } let(:model_solutions) { [] }
let(:exercise) { nil } 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 it 'creates an exercise with the correct attributes' do
expect(convert_to_exercise_service).to have_attributes( expect(convert_to_exercise_service).to have_attributes(
title: 'title', title: 'title',
description: 'description', description: 'description',
instructions: 'internal_description',
execution_environment: be_blank,
uuid: be_blank, uuid: be_blank,
unpublished: true, unpublished: true,
user: user, 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 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 context 'when task has a file' do
let(:files) { [file] } let(:files) { [file] }
let(:file) do let(:file) do
@ -74,7 +131,6 @@ describe ProformaService::ConvertTaskToExercise do
visible: 'yes', visible: 'yes',
usage_by_lms: usage_by_lms, usage_by_lms: usage_by_lms,
binary: binary, binary: binary,
internal_description: 'regular_file',
mimetype: mimetype mimetype: mimetype
) )
end end
@ -101,6 +157,21 @@ describe ProformaService::ConvertTaskToExercise do
expect { convert_to_exercise_service.save! }.to change(Exercise, :count).by(1) expect { convert_to_exercise_service.save! }.to change(Exercise, :count).by(1)
end 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 context 'when path is folder/' do
let(:path) { 'folder/' } let(:path) { 'folder/' }
@ -150,7 +221,7 @@ describe ProformaService::ConvertTaskToExercise do
end end
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' } let(:filename) { 'unknown_file_type.asdf' }
it 'creates a new Exercise on save' do it 'creates a new Exercise on save' do
@ -180,8 +251,7 @@ describe ProformaService::ConvertTaskToExercise do
used_by_grader: 'used_by_grader', used_by_grader: 'used_by_grader',
visible: 'yes', visible: 'yes',
usage_by_lms: 'display', usage_by_lms: 'display',
binary: false, binary: false
internal_description: 'reference_implementation'
) )
end end
@ -226,13 +296,14 @@ describe ProformaService::ConvertTaskToExercise do
id: 'test-id', id: 'test-id',
title: 'title', title: 'title',
description: 'description', description: 'description',
internal_description: 'internal_description',
test_type: 'test_type', test_type: 'test_type',
files: test_files, files: test_files,
meta_data: { meta_data: {
'feedback-message' => 'feedback-message', CodeOcean: {
'testing-framework' => 'testing-framework', 'feedback-message': 'feedback-message',
'testing-framework-version' => 'testing-framework-version', 'testing-framework': 'testing-framework',
'testing-framework-version': 'testing-framework-version',
},
} }
) )
end end
@ -267,15 +338,32 @@ describe ProformaService::ConvertTaskToExercise do
) )
end 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 context 'when task has multiple tests' do
let(:tests) { [test, test2] } let(:tests) { [test, test2] }
let(:test2) do let(:test2) do
Proforma::Test.new( Proforma::Test.new(
files: test_files2, files: test_files2,
meta_data: { meta_data: {
'feedback-message' => 'feedback-message', CodeOcean: {
'testing-framework' => 'testing-framework', 'feedback-message': 'feedback-message',
'testing-framework-version' => 'testing-framework-version', 'testing-framework': 'testing-framework',
'testing-framework-version': 'testing-framework-version',
},
} }
) )
end end
@ -304,8 +392,7 @@ describe ProformaService::ConvertTaskToExercise do
create( create(
:files, :files,
title: 'exercise-title', title: 'exercise-title',
description: 'exercise-description', description: 'exercise-description'
instructions: 'exercise-instruction'
) )
end end
@ -315,9 +402,8 @@ describe ProformaService::ConvertTaskToExercise do
convert_to_exercise_service.save convert_to_exercise_service.save
expect(exercise.reload).to have_attributes( expect(exercise.reload).to have_attributes(
id: exercise.id, id: exercise.id,
title: task.title, title: exercise.title,
description: task.description, description: exercise.description,
instructions: task.internal_description,
execution_environment: exercise.execution_environment, execution_environment: exercise.execution_environment,
uuid: exercise.uuid, uuid: exercise.uuid,
user: exercise.user, user: exercise.user,
@ -339,8 +425,7 @@ describe ProformaService::ConvertTaskToExercise do
used_by_grader: 'used_by_grader', used_by_grader: 'used_by_grader',
visible: 'yes', visible: 'yes',
usage_by_lms: 'display', usage_by_lms: 'display',
binary: false, binary: false
internal_description: 'regular_file'
) )
end end
let(:tests) { [test] } let(:tests) { [test] }
@ -349,13 +434,14 @@ describe ProformaService::ConvertTaskToExercise do
id: 'test-id', id: 'test-id',
title: 'title', title: 'title',
description: 'description', description: 'description',
internal_description: 'regular_file',
test_type: 'test_type', test_type: 'test_type',
files: test_files, files: test_files,
meta_data: { meta_data: {
'feedback-message' => 'feedback-message', CodeOcean: {
'testing-framework' => 'testing-framework', 'feedback-message': 'feedback-message',
'testing-framework-version' => 'testing-framework-version', 'testing-framework': 'testing-framework',
'testing-framework-version': 'testing-framework-version',
},
} }
) )
end end

View File

@ -30,7 +30,7 @@ describe ProformaService::ExportTask do
before do before do
allow(ProformaService::ConvertExerciseToTask).to receive(:call).with(exercise: exercise).and_return(task) 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 end
it do it do

View File

@ -29,6 +29,10 @@ describe ProformaService::Import do
instructions: 'instruction', instructions: 'instruction',
execution_environment: execution_environment, execution_environment: execution_environment,
files: files + tests, files: files + tests,
hide_file_tree: true,
allow_file_creation: false,
allow_auto_completion: true,
expected_difficulty: 7,
uuid: uuid, uuid: uuid,
user: user) user: user)
end end

View File

@ -31,7 +31,11 @@ RSpec::Matchers.define :be_an_equal_exercise_as do |exercise|
return true if object == other # for [] return true if object == other # for []
return false if object.length != other.length 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 end
def attributes_and_associations(object) def attributes_and_associations(object)