Merge pull request #939 from openHPI/refactor_proforma_import_export
Refactor Proforma Import/Export
This commit is contained in:
2
Gemfile
2
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'
|
||||
|
14
Gemfile.lock
14
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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}"} }
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user