Skip to content

Commit 4d3198e

Browse files
committed
basic functionality of reusing files based on xml_id.
Use xml_id_path to recreate proforma structure with tests and model_solutions containing multiple files
1 parent 472ef0b commit 4d3198e

File tree

8 files changed

+186
-79
lines changed

8 files changed

+186
-79
lines changed

app/services/proforma_service/convert_exercise_to_task.rb

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -77,62 +77,85 @@ def uuid
7777
end
7878

7979
def model_solutions
80-
@exercise.files.filter {|file| file.role == 'reference_implementation' }.map do |file|
80+
@exercise.files.filter {|file| file.role == 'reference_implementation' }.group_by {|file| file.xml_id_path&.split('/')&.first || "co-ms-#{file.id}" }.map do |xml_id, files|
8181
ProformaXML::ModelSolution.new(
82-
id: "ms-#{file.id}",
83-
files: model_solution_file(file)
82+
id: xml_id || "ms-#{files.first.id}",
83+
files: files.map {|file| model_solution_file(file) }
8484
)
8585
end
8686
end
8787

8888
def model_solution_file(file)
89-
[
90-
task_file(file).tap do |ms_file|
91-
ms_file.used_by_grader = false
92-
ms_file.usage_by_lms = 'display'
93-
end,
94-
]
89+
task_file(file).tap do |ms_file|
90+
ms_file.used_by_grader = false
91+
ms_file.usage_by_lms = 'display'
92+
end
9593
end
9694

9795
def tests
98-
@exercise.files.filter(&:teacher_defined_assessment?).map do |file|
96+
@exercise.files.filter(&:teacher_defined_assessment?).group_by {|file| xml_id_from_file(file).first }.map do |xml_id, files|
9997
ProformaXML::Test.new(
100-
id: file.id,
101-
title: file.name,
102-
files: test_file(file),
103-
meta_data: test_meta_data(file)
98+
id: xml_id || files.first.id,
99+
title: files.first.name,
100+
files: files.map {|file| test_file(file) },
101+
meta_data: test_meta_data(files)
104102
)
105103
end
106104
end
107105

108-
def test_meta_data(file)
106+
def xml_id_from_file(file)
107+
type = if file.teacher_defined_assessment?
108+
'test'
109+
elsif file.reference_implementation?
110+
'ms'
111+
else
112+
'file'
113+
end
114+
xml_id_path_parts = file.xml_id_path&.split('/')
115+
xml_id_path = []
116+
117+
xml_id_path << (xml_id_path_parts&.first || "co-#{type}-#{file.id}") unless type == 'file'
118+
xml_id_path << (xml_id_path_parts&.last || file.id)
119+
120+
xml_id_path
121+
end
122+
123+
def test_meta_data(files)
109124
{
110125
'@@order' => %w[test-meta-data],
111126
'test-meta-data' => {
112-
'@@order' => %w[CodeOcean:feedback-message CodeOcean:weight CodeOcean:hidden-feedback],
127+
'@@order' => %w[CodeOcean:test-file],
113128
'@xmlns' => {'CodeOcean' => 'codeocean.openhpi.de'},
114-
'CodeOcean:feedback-message' => {
115-
'@@order' => %w[$1],
116-
'$1' => file.feedback_message,
117-
},
118-
'CodeOcean:weight' => {
119-
'@@order' => %w[$1],
120-
'$1' => file.weight,
121-
},
122-
'CodeOcean:hidden-feedback' => {
123-
'@@order' => %w[$1],
124-
'$1' => file.hidden_feedback,
125-
},
129+
'CodeOcean:test-file' => files.to_h do |file|
130+
[
131+
"CodeOcean:#{xml_id_from_file(file).last}", {
132+
'@@order' => %w[CodeOcean:feedback-message CodeOcean:weight CodeOcean:hidden-feedback],
133+
'@xmlns' => {'CodeOcean' => 'codeocean.openhpi.de'},
134+
'@id' => xml_id_from_file(file).last,
135+
'@name' => file.name,
136+
'CodeOcean:feedback-message' => {
137+
'@@order' => %w[$1],
138+
'$1' => file.feedback_message,
139+
},
140+
'CodeOcean:weight' => {
141+
'@@order' => %w[$1],
142+
'$1' => file.weight,
143+
},
144+
'CodeOcean:hidden-feedback' => {
145+
'@@order' => %w[$1],
146+
'$1' => file.hidden_feedback,
147+
},
148+
}
149+
]
150+
end,
126151
},
127152
}
128153
end
129154

130155
def test_file(file)
131-
[
132-
task_file(file).tap do |t_file|
133-
t_file.used_by_grader = true
134-
end,
135-
]
156+
task_file(file).tap do |t_file|
157+
t_file.used_by_grader = true
158+
end
136159
end
137160

138161
def exercise_files
@@ -168,8 +191,11 @@ def task_files
168191
end
169192

170193
def task_file(file)
194+
file.update(xml_id_path: xml_id_from_file(file).join('/')) if file.xml_id_path.blank?
195+
196+
xml_id = xml_id_from_file(file).last
171197
task_file = ProformaXML::TaskFile.new(
172-
id: file.id,
198+
id: xml_id,
173199
filename: filename(file),
174200
usage_by_lms: file.read_only ? 'display' : 'edit',
175201
visible: file.hidden ? 'no' : 'yes'

app/services/proforma_service/convert_task_to_exercise.rb

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,44 +60,56 @@ def string_to_bool(str)
6060
end
6161

6262
def files
63-
model_solution_files + test_files + task_files.values.tap {|array| array.each {|file| file.role ||= 'regular_file' } }
63+
model_solution_files + test_files + task_files
6464
end
6565

6666
def test_files
67-
@task.tests.map do |test_object|
68-
task_files.delete(test_object.files.first.id).tap do |file|
69-
file.weight = extract_meta_data(test_object.meta_data&.dig('test-meta-data'), 'weight').presence || 1.0
70-
file.feedback_message = extract_meta_data(test_object.meta_data&.dig('test-meta-data'), 'feedback-message').presence || 'Feedback'
71-
file.hidden_feedback = extract_meta_data(test_object.meta_data&.dig('test-meta-data'), 'hidden-feedback').presence || false
72-
file.role ||= 'teacher_defined_test'
67+
@task.tests.flat_map do |test|
68+
test.files.map do |task_file|
69+
codeocean_file_from_task_file(task_file, test).tap do |file|
70+
file.weight = extract_meta_data(test.meta_data&.dig('test-meta-data'), 'test-file', task_file.id, 'weight').presence || 1.0
71+
file.feedback_message = extract_meta_data(test.meta_data&.dig('test-meta-data'), 'test-file', task_file.id, 'feedback-message').presence || 'Feedback'
72+
file.hidden_feedback = extract_meta_data(test.meta_data&.dig('test-meta-data'), 'test-file', task_file.id, 'hidden-feedback').presence || false
73+
file.role = 'teacher_defined_test' unless file.teacher_defined_assessment?
74+
end
7375
end
7476
end
7577
end
7678

7779
def model_solution_files
78-
@task.model_solutions.map do |model_solution_object|
79-
task_files.delete(model_solution_object.files.first.id).tap do |file|
80-
file.role ||= 'reference_implementation'
80+
@task.model_solutions.flat_map do |model_solution|
81+
model_solution.files.map do |task_file|
82+
codeocean_file_from_task_file(task_file, model_solution).tap do |file|
83+
file.role ||= 'reference_implementation'
84+
end
8185
end
8286
end
8387
end
8488

8589
def task_files
86-
@task_files ||= @task.all_files.reject {|file| file.id == 'ms-placeholder-file' }.to_h do |task_file|
87-
[task_file.id, codeocean_file_from_task_file(task_file)]
90+
@task.files.reject {|file| file.id == 'ms-placeholder-file' }.map do |task_file|
91+
codeocean_file_from_task_file(task_file).tap do |file|
92+
file.role ||= 'regular_file'
93+
end
8894
end
95+
# @task_files ||= @task.all_files.reject {|file| file.id == 'ms-placeholder-file' }.to_h do |task_file|
96+
# [task_file.id, codeocean_file_from_task_file(task_file)]
97+
# end
8998
end
9099

91-
def codeocean_file_from_task_file(file)
100+
def codeocean_file_from_task_file(file, parent_object = nil)
92101
extension = File.extname(file.filename)
93-
codeocean_file = CodeOcean::File.new(
102+
103+
codeocean_file = CodeOcean::File.where(context: @exercise).where('xml_id_path LIKE ?', "%#{file.id}").first_or_initialize(context: @exercise)
104+
codeocean_file.assign_attributes(
94105
context: @exercise,
95106
file_type: file_type(extension),
96107
hidden: file.visible != 'yes', # hides 'delayed' and 'no'
97108
name: File.basename(file.filename, '.*'),
98109
read_only: file.usage_by_lms != 'edit',
99110
role: extract_meta_data(@task.meta_data&.dig('meta-data'), 'files', "CO-#{file.id}", 'role'),
100-
path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename)
111+
path: File.dirname(file.filename).in?(['.', '']) ? nil : File.dirname(file.filename),
112+
xml_id_path: (parent_object.nil? ? '' : "#{parent_object.id}/") + file.id.to_s
101113
)
102114
if file.binary
103115
codeocean_file.native_file = FileIO.new(file.content.dup.force_encoding('UTF-8'), File.basename(file.filename))

app/services/proforma_service/import.rb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,7 @@ def execute
1414
import_result = importer.perform
1515
@task = import_result
1616

17-
exercise = base_exercise
18-
exercise_files = exercise&.files&.to_a
19-
20-
exercise = ConvertTaskToExercise.call(task: @task, user: @user, exercise:)
21-
exercise_files&.each(&:destroy) # feels suboptimal
22-
23-
exercise
17+
ConvertTaskToExercise.call(task: @task, user: @user, exercise: base_exercise)
2418
else
2519
import_multi
2620
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
class AddXmlPathToFiles < ActiveRecord::Migration[7.1]
4+
def change
5+
add_column :files, :xml_id_path, :string, null: true, default: nil
6+
end
7+
end

db/schema.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[7.1].define(version: 2023_12_08_194632) do
13+
ActiveRecord::Schema[7.1].define(version: 2024_09_03_204319) do
1414
# These are extensions that must be enabled in order to support this database
1515
enable_extension "pg_trgm"
1616
enable_extension "pgcrypto"
@@ -315,6 +315,7 @@
315315
t.string "path"
316316
t.integer "file_template_id"
317317
t.boolean "hidden_feedback", default: false, null: false
318+
t.string "xml_id_path"
318319
t.index ["context_id", "context_type"], name: "index_files_on_context_id_and_context_type"
319320
end
320321

spec/services/proforma_service/convert_exercise_to_task_spec.rb

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@
7979

8080
context 'when exercise has a mainfile' do
8181
let(:files) { [file] }
82-
let(:file) { build(:file) }
82+
let(:file) { create(:file) }
8383

8484
it 'creates a task-file with the correct attributes' do
8585
expect(task.files.first).to have_attributes(
86-
id: file.id,
86+
id: file.id.to_s,
8787
content: file.content,
8888
filename: file.name_with_extension,
8989
used_by_grader: true,
@@ -109,6 +109,18 @@
109109
)
110110
)
111111
end
112+
113+
it 'sets xml_id_path to default' do
114+
expect { convert_to_task.execute }.to change(file.reload, :xml_id_path).from(nil).to(file.id.to_s)
115+
end
116+
117+
context 'when xml_id_path is set for file' do
118+
let(:file) { create(:file, xml_id_path: 'foobar') }
119+
120+
it 'does not change xml_path_id' do
121+
expect { convert_to_task.execute }.not_to change(file.reload, :xml_id_path)
122+
end
123+
end
112124
end
113125

114126
context 'when exercise has a regular file' do
@@ -119,7 +131,7 @@
119131

120132
it 'creates a task-file with the correct attributes' do
121133
expect(task.files.first).to have_attributes(
122-
id: file.id,
134+
id: file.id.to_s,
123135
content: file.content,
124136
filename: file.name_with_extension,
125137
used_by_grader: true,
@@ -185,14 +197,14 @@
185197

186198
it 'creates a model-solution with one file' do
187199
expect(task.model_solutions.first).to have_attributes(
188-
id: "ms-#{file.id}",
200+
id: "co-ms-#{file.id}",
189201
files: have(1).item
190202
)
191203
end
192204

193205
it 'creates a model-solution with one file with correct attributes' do
194206
expect(task.model_solutions.first.files.first).to have_attributes(
195-
id: file.id,
207+
id: file.id.to_s,
196208
content: file.content,
197209
filename: file.name_with_extension,
198210
used_by_grader: false,
@@ -210,6 +222,18 @@
210222
it 'creates a task with two model-solutions' do
211223
expect(task.model_solutions).to have(2).items
212224
end
225+
226+
context 'when reference_implementations belong to the same proforma model_solution' do
227+
let(:files) { build_list(:file, 2, role: 'reference_implementation') {|file, i| file.xml_id_path = "proforma_ms/xml_id_#{i}" } }
228+
229+
it 'creates a task with one model-solution' do
230+
expect(task.model_solutions).to have(1).items
231+
end
232+
233+
it 'creates a task with a model-solution with two files' do
234+
expect(task.model_solutions.first.files).to have(2).items
235+
end
236+
end
213237
end
214238

215239
context 'when exercise has a test' do
@@ -223,22 +247,27 @@
223247

224248
it 'creates a test with one file' do
225249
expect(task.tests.first).to have_attributes(
226-
id: test_file.id,
250+
id: "co-test-#{test_file.id}",
227251
title: test_file.name,
228252
files: have(1).item,
229253
meta_data: a_hash_including(
230254
'test-meta-data' => a_hash_including(
231-
'CodeOcean:feedback-message' => {'$1' => 'feedback_message', '@@order' => ['$1']},
232-
'CodeOcean:weight' => {'$1' => test_file.weight, '@@order' => ['$1']},
233-
'CodeOcean:hidden-feedback' => {'$1' => test_file.hidden_feedback, '@@order' => ['$1']}
255+
'CodeOcean:test-file' => a_hash_including(
256+
"CodeOcean:#{test_file.id}" => a_hash_including(
257+
'@name' => test_file.name,
258+
'CodeOcean:feedback-message' => {'$1' => 'feedback_message', '@@order' => ['$1']},
259+
'CodeOcean:weight' => {'$1' => test_file.weight, '@@order' => ['$1']},
260+
'CodeOcean:hidden-feedback' => {'$1' => test_file.hidden_feedback, '@@order' => ['$1']}
261+
)
262+
)
234263
)
235264
)
236265
)
237266
end
238267

239268
it 'creates a test with one file with correct attributes' do
240269
expect(task.tests.first.files.first).to have_attributes(
241-
id: test_file.id,
270+
id: test_file.id.to_s,
242271
content: test_file.content,
243272
filename: test_file.name_with_extension,
244273
used_by_grader: true,
@@ -263,6 +292,36 @@
263292
it 'creates a task with two tests' do
264293
expect(task.tests).to have(2).items
265294
end
295+
296+
context 'when test_files belong to the same proforma test' do
297+
let(:tests) { build_list(:test_file, 2) {|file, i| file.xml_id_path = "proforma_test/xml_id_#{i}" } }
298+
299+
it 'creates a test with two file' do
300+
expect(task.tests.first).to have_attributes(
301+
id: 'proforma_test',
302+
title: tests.first.name,
303+
files: have(2).item,
304+
meta_data: a_hash_including(
305+
'test-meta-data' => a_hash_including(
306+
'CodeOcean:test-file' => a_hash_including(
307+
'CodeOcean:xml_id_0' => a_hash_including(
308+
'@name' => tests.first.name,
309+
'CodeOcean:feedback-message' => {'$1' => 'feedback_message', '@@order' => ['$1']},
310+
'CodeOcean:weight' => {'$1' => 1, '@@order' => ['$1']},
311+
'CodeOcean:hidden-feedback' => {'$1' => tests.first.hidden_feedback, '@@order' => ['$1']}
312+
),
313+
'CodeOcean:xml_id_1' => a_hash_including(
314+
'@name' => tests.last.name,
315+
'CodeOcean:feedback-message' => {'$1' => 'feedback_message', '@@order' => ['$1']},
316+
'CodeOcean:weight' => {'$1' => 1, '@@order' => ['$1']},
317+
'CodeOcean:hidden-feedback' => {'$1' => tests.last.hidden_feedback, '@@order' => ['$1']}
318+
)
319+
)
320+
)
321+
)
322+
)
323+
end
324+
end
266325
end
267326
end
268327
end

0 commit comments

Comments
 (0)