Skip to content

Commit 2c9e4d4

Browse files
committed
1320: refactor convert proforma to task service to use persistence for file-consistency
1 parent 9c9206d commit 2c9e4d4

File tree

4 files changed

+123
-47
lines changed

4 files changed

+123
-47
lines changed

app/services/proforma_service/convert_proforma_task_to_task.rb

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,19 @@ def initialize(proforma_task:, user:, task: nil)
77
@proforma_task = proforma_task
88
@user = user
99
@task = task || Task.new
10-
@all_files = @task.all_files
1110
@file_xml_ids = []
1211
end
1312

1413
def execute
15-
import_task
14+
ActiveRecord::Base.transaction do
15+
import_task
16+
end
1617
@task
1718
end
1819

1920
private
2021

2122
def import_task
22-
upsert_files @proforma_task, @task
2323
@task.assign_attributes(
2424
user:,
2525
title: @proforma_task.title,
@@ -33,12 +33,51 @@ def import_task
3333

3434
submission_restrictions: @proforma_task.submission_restrictions,
3535
external_resources: @proforma_task.external_resources,
36-
grading_hints: @proforma_task.grading_hints,
37-
38-
tests:,
39-
model_solutions:
36+
grading_hints: @proforma_task.grading_hints
4037
)
41-
delete_removed_files
38+
Pundit.authorize @user, @task, :create?
39+
40+
@task.save!
41+
42+
manage_objects
43+
end
44+
45+
def manage_objects
46+
# delete files that do not exist in imported task
47+
unreferenced_files.each(&:destroy)
48+
@task.reload
49+
# Move only relocated files to the task, avoiding updates to the updated_at of untouched files.
50+
move_relocated_files_to_task
51+
set_tests
52+
set_model_solutions
53+
54+
@task.reload
55+
upsert_files @proforma_task, @task
56+
delete_removed_objects
57+
@task.save!
58+
end
59+
60+
def move_relocated_files_to_task
61+
@task.files = @task.all_files(cached: false).filter do |file|
62+
# Move files to the task if they belong directly to it or if the parent's xml_id differs from the current parent's xml_id.
63+
parent_id(file.xml_id) == 'task' || (file.fileable.respond_to?(:xml_id) && file.fileable.xml_id) != parent_id(file.xml_id)
64+
end
65+
end
66+
67+
def parent_id(xml_id)
68+
[@proforma_task, *@proforma_task.model_solutions, *@proforma_task.tests].find do |object|
69+
object.files.any? {|file| file.id == xml_id }
70+
end.then {|object| object.respond_to?(:id) ? object.id : 'task' }
71+
end
72+
73+
def all_proforma_files
74+
[@proforma_task.files + @proforma_task.tests.map(&:files) + @proforma_task.model_solutions.map(&:files)].flatten
75+
end
76+
77+
def unreferenced_files
78+
@task.all_files.reject do |f|
79+
all_proforma_files.map {|pf| pf.id.to_s }.include?(f.xml_id)
80+
end
4281
end
4382

4483
def parent_uuid
@@ -62,7 +101,7 @@ def upsert_files(collection, fileable)
62101
end
63102

64103
def upsert_file_from_proforma_file(proforma_task_file, fileable)
65-
task_file = ch_record_for(@all_files, proforma_task_file.id) || TaskFile.new
104+
task_file = (@file_xml_ids.exclude?(proforma_task_file.id) && ch_record_for(@task.all_files, proforma_task_file.id)) || TaskFile.new
66105
task_file.assign_attributes(file_attributes(proforma_task_file, fileable))
67106
attach_file_content(proforma_task_file, task_file)
68107
task_file
@@ -105,9 +144,10 @@ def file_attributes(proforma_task_file, fileable)
105144
}
106145
end
107146

108-
def tests
109-
@proforma_task.tests.map do |test|
110-
ch_test = ch_record_for(@task.tests, test.id) || Test.new(xml_id: test.id)
147+
def set_tests
148+
@proforma_task.tests.each do |test|
149+
ch_test = find_or_initialize_ch_test test
150+
ch_test.task = @task
111151
upsert_files test, ch_test
112152
ch_test.assign_attributes(
113153
title: test.title,
@@ -117,12 +157,12 @@ def tests
117157
meta_data: test.meta_data,
118158
configuration: test.configuration
119159
)
120-
ch_test
160+
ch_test.save!
121161
end
122162
end
123163

124-
def object_files(object)
125-
object.files.map {|file| files.delete(file.id) }
164+
def find_or_initialize_ch_test(test)
165+
ch_record_for(@task.tests, test.id) || Test.new(xml_id: test.id)
126166
end
127167

128168
def programming_language # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
@@ -136,24 +176,36 @@ def programming_language # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticCo
136176
end
137177
end
138178

139-
def model_solutions
140-
@proforma_task.model_solutions.map do |model_solution|
179+
def set_model_solutions
180+
@proforma_task.model_solutions.each do |model_solution|
141181
ch_model_solution = ch_record_for(@task.model_solutions, model_solution.id) || ModelSolution.new(xml_id: model_solution.id)
182+
ch_model_solution.task = @task
142183
upsert_files model_solution, ch_model_solution
143184
ch_model_solution.assign_attributes(
144185
description: model_solution.description,
145186
internal_description: model_solution.internal_description
146187
)
147-
ch_model_solution
188+
ch_model_solution.save!
148189
end
149190
end
150191

151192
def ch_record_for(collection, xml_id)
152193
collection.select {|record| record.xml_id == xml_id }&.first
153194
end
154195

155-
def delete_removed_files
156-
@all_files.reject {|file| @file_xml_ids.include? file.xml_id }.each(&:destroy)
196+
def delete_removed_objects
197+
delete_removed_tests
198+
delete_removed_model_solutions
199+
end
200+
201+
def delete_removed_tests
202+
remaining_test_ids = @proforma_task.tests.map {|t| t.id.to_s }
203+
@task.tests.reject {|test| remaining_test_ids.include? test.xml_id }.each(&:destroy)
204+
end
205+
206+
def delete_removed_model_solutions
207+
remaining_model_solution_ids = @proforma_task.model_solutions.map {|ms| ms.id.to_s }
208+
@task.model_solutions.reject {|model_solution| remaining_model_solution_ids.include? model_solution.xml_id }.each(&:destroy)
157209
end
158210
end
159211
end

app/services/proforma_service/import_task.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@ def initialize(proforma_task:, user:)
99
end
1010

1111
def execute
12-
task = ConvertProformaTaskToTask.call(proforma_task: @proforma_task, user: @user, task: base_task)
13-
Pundit.authorize @user, task, :create?
14-
task.save!
15-
task
12+
ConvertProformaTaskToTask.call(proforma_task: @proforma_task, user: @user, task: base_task)
1613
end
1714

1815
private

spec/services/proforma_service/convert_proforma_task_to_task_spec.rb

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
end
2525

2626
describe '#execute' do
27-
subject(:convert_to_task_service) { described_class.call(proforma_task:, user:, task:) }
27+
subject(:convert_to_task_service) { described_class.call(proforma_task:, user:, task:).reload }
2828

2929
let(:proforma_task) do
3030
ProformaXML::Task.new(
@@ -237,8 +237,9 @@
237237
end
238238

239239
it 'creates files with correct attributes' do
240-
expect(convert_to_task_service.tests[1].files[0]).to have_attributes(convert_to_task_service.tests[0].files[0].attributes.merge('xml_id' => 'id-2'))
241-
expect(convert_to_task_service.tests[2].files[0]).to have_attributes(convert_to_task_service.tests[0].files[0].attributes.merge('xml_id' => 'id-3'))
240+
attribute_exceptions = %w[id created_at fileable_id updated_at]
241+
expect(convert_to_task_service.tests[1].files[0]).to have_attributes(convert_to_task_service.tests[0].files[0].attributes.except(*attribute_exceptions).merge('xml_id' => 'id-2'))
242+
expect(convert_to_task_service.tests[2].files[0]).to have_attributes(convert_to_task_service.tests[0].files[0].attributes.except(*attribute_exceptions).merge('xml_id' => 'id-3'))
242243
end
243244

244245
it 'creates separate copies of referenced file' do
@@ -321,10 +322,12 @@
321322
internal_description: 'internal_description',
322323
test_type: 'test_type',
323324
files: test_files,
324-
meta_data: test_meta_data
325+
meta_data: test_meta_data,
326+
configuration:
325327
)
326328
end
327329

330+
let(:configuration) {}
328331
let(:test_meta_data) {}
329332
let(:test_files) { [test_file] }
330333
let(:test_file) do
@@ -402,15 +405,15 @@
402405
end
403406

404407
context 'when test has custom configuration' do
405-
let(:test) { build(:test, :with_unittest) }
408+
let(:configuration) { attributes_for(:test, :with_unittest)[:configuration] }
406409

407410
it 'creates a test with the supplied test configuration' do
408411
expect(convert_to_task_service.tests.first).to have_attributes(configuration: test.configuration)
409412
end
410413
end
411414

412415
context 'when test has multiple custom configuration' do
413-
let(:test) { build(:test, :with_multiple_custom_configurations) }
416+
let(:configuration) { attributes_for(:test, :with_multiple_custom_configurations)[:configuration] }
414417

415418
it 'creates a test with the supplied test configurations' do
416419
expect(convert_to_task_service.tests.first).to have_attributes(configuration: test.configuration)
@@ -420,7 +423,11 @@
420423
context 'when proforma_task has multiple tests' do
421424
let(:tests) { [test, test2] }
422425
let(:test2) do
423-
ProformaXML::Test.new(files: test_files2)
426+
ProformaXML::Test.new(
427+
id: 'test_file_id',
428+
title: 'wild-title',
429+
files: test_files2
430+
)
424431
end
425432
let(:test_files2) { [test_file2] }
426433
let(:test_file2) do
@@ -473,6 +480,7 @@
473480
let(:task) do
474481
create(
475482
:task,
483+
user:,
476484
title: 'task-title',
477485
description: 'task-description',
478486
internal_description: 'task-internal_description'
@@ -579,7 +587,7 @@
579587

580588
context 'when proforma_task has been exported from task' do
581589
let(:proforma_task) { ProformaService::ConvertTaskToProformaTask.call(task:) }
582-
let!(:task) { create(:task, files: task_files, tests:, model_solutions:, title: 'title') }
590+
let!(:task) { create(:task, user:, files: task_files, tests:, model_solutions:, title: 'title') }
583591
let(:task_files) { build_list(:task_file, 1, :exportable, internal_description: 'original task file') }
584592
let(:tests) { build_list(:test, 1, files: test_files) }
585593
let(:test_files) { build_list(:task_file, 1, :exportable, internal_description: 'original test file') }
@@ -605,51 +613,67 @@
605613
)
606614
end
607615

616+
it 'does not update updated_at of unchanged files' do
617+
task_file_updated_at = task_files.first.updated_at
618+
test_file_updated_at = test_files.first.updated_at
619+
ms_file_update_at = model_solution_files.first.updated_at
620+
621+
expect(convert_to_task_service).to have_attributes(
622+
files: have(1).item.and(include(have_attributes(
623+
updated_at: task_file_updated_at
624+
))),
625+
model_solutions: have(1).item.and(include(have_attributes(
626+
files: have(1).item.and(include(have_attributes(updated_at: ms_file_update_at)))
627+
))),
628+
tests: have(1).item.and(include(have_attributes(
629+
id: tests.first.id,
630+
files: have(1).item.and(include(have_attributes(updated_at: test_file_updated_at)))
631+
)))
632+
)
633+
end
634+
608635
context 'when files have been deleted' do
609636
context 'when task files have been deleted' do
610-
before { task.files = [] }
637+
before { proforma_task.files = [] }
611638

612639
it 'imports task files correctly' do
613640
expect(convert_to_task_service.files).to be_empty
614641
end
615642

616643
it 'saves the task correctly' do
617-
convert_to_task_service.save
618-
task.reload
619-
expect(task.files).to be_empty
644+
convert_to_task_service
645+
expect(task.reload.files).to be_empty
620646
end
621647
end
622648

623649
context 'when test files have been deleted' do
624-
before { task.tests.first.files = [] }
650+
before { proforma_task.tests.first.files = [] }
625651

626652
it 'imports test files correctly' do
627653
expect(convert_to_task_service.tests.first.files).to be_empty
628654
end
629655

630656
it 'saves the task correctly' do
631-
convert_to_task_service.save
632-
task.reload
657+
convert_to_task_service
633658
expect(task.tests.first.files).to be_empty
634659
end
635660
end
636661

637662
context 'when model solution files have been deleted' do
638-
before { task.model_solutions.first.files = [] }
663+
before { proforma_task.model_solutions.first.files = [] }
639664

640665
it 'imports model solution files correctly' do
641666
expect(convert_to_task_service.model_solutions.first.files).to be_empty
642667
end
643668

644669
it 'saves the task correctly' do
645-
convert_to_task_service.save
646-
task.reload
670+
convert_to_task_service
647671
expect(task.model_solutions.first.files).to be_empty
648672
end
649673
end
650674
end
651675

652-
context 'when files have been move around' do
676+
context 'when files have been moved around' do
653677
before do
654678
task_file = proforma_task.files.first
655679
test_file = proforma_task.tests.first.files.first
@@ -691,15 +715,15 @@
691715
files: have(1).item.and(include(have_attributes(id: task_files.first.id)))
692716
))),
693717
tests: have(1).item.and(include(have_attributes(
694-
id: 987_654_325,
718+
xml_id: '987654325',
695719
files: have(1).item.and(include(have_attributes(id: model_solution_files.first.id)))
696720
)))
697721
)
698722
end
699723

700724
context 'when imported task is persisted' do
701725
before do
702-
convert_to_task_service.save
726+
convert_to_task_service.save!
703727
task.reload
704728
end
705729

@@ -735,7 +759,7 @@
735759
files: have(1).item.and(include(have_attributes(id: task_files.first.id)))
736760
))),
737761
tests: have(1).item.and(include(have_attributes(
738-
id: 987_654_325,
762+
xml_id: '987654325',
739763
files: have(1).item.and(include(have_attributes(id: model_solution_files.first.id)))
740764
)))
741765
)

spec/services/proforma_service/import_spec.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
before do
5454
zip_file.write(exporter)
5555
zip_file.rewind
56+
task.reload
5657
end
5758

5859
it { is_expected.to be_an_equal_task_as task }
@@ -75,7 +76,7 @@
7576
it { is_expected.to be_valid }
7677

7778
it 'sets the correct user as owner of the task' do
78-
expect(imported_task.user).to be user
79+
expect(imported_task.user).to eq user
7980
end
8081

8182
it 'sets the uuid' do
@@ -199,6 +200,8 @@
199200
user:)
200201
end
201202

203+
before { task2.reload }
204+
202205
it 'imports the tasks from zip containing multiple zips' do
203206
expect(imported_task).to all be_an(Task)
204207
end

0 commit comments

Comments
 (0)