From 973d3755a33af0451c300c9841cc966f53a6c6b3 Mon Sep 17 00:00:00 2001 From: abcampo-iry <261805581+abcampo-iry@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:22:55 +0200 Subject: [PATCH] create copy for scratch assets missing --- lib/concepts/lesson/operations/create_copy.rb | 31 +++++++- spec/concepts/lesson/create_copy_spec.rb | 78 +++++++++++++++++++ 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/lib/concepts/lesson/operations/create_copy.rb b/lib/concepts/lesson/operations/create_copy.rb index 4d09d70ee..72a8f0aad 100644 --- a/lib/concepts/lesson/operations/create_copy.rb +++ b/lib/concepts/lesson/operations/create_copy.rb @@ -5,8 +5,13 @@ class CreateCopy class << self def call(lesson:, lesson_params:) response = OperationResponse.new - response[:lesson] = build_copy(lesson, lesson_params) - response[:lesson].save! + + Lesson.transaction do + response[:lesson] = build_copy(lesson, lesson_params) + response[:lesson].save! + copy_scratch_assets(lesson.project, response[:lesson].project) + end + response rescue StandardError => e Sentry.capture_exception(e) @@ -39,8 +44,30 @@ def build_project_copy(project, project_params) project_copy.components.build({ name: component.name, extension: component.extension, content: component.content }) end + copy_scratch_component(project, project_copy) + project_copy end + + def copy_scratch_component(project, project_copy) + return unless project.scratch_project? && project.scratch_component + + project_copy.build_scratch_component(content: project.scratch_component.content.deep_dup) + end + + def copy_scratch_assets(project, project_copy) + return unless project.scratch_project? + + project.scratch_assets.where(uploaded_user_id: project.user_id).find_each do |scratch_asset| + next unless scratch_asset.file.attached? + + scratch_asset_copy = project_copy.scratch_assets.create!( + filename: scratch_asset.filename, + uploaded_user_id: project_copy.user_id + ) + scratch_asset_copy.file.attach(scratch_asset.file.blob) + end + end end end end diff --git a/spec/concepts/lesson/create_copy_spec.rb b/spec/concepts/lesson/create_copy_spec.rb index 23c51d991..e0bbaa95c 100644 --- a/spec/concepts/lesson/create_copy_spec.rb +++ b/spec/concepts/lesson/create_copy_spec.rb @@ -109,6 +109,78 @@ expect(copied_component.content).to eq(original_component.content) end + context 'when the project is a Scratch project' do + let(:copied_teacher_id) { SecureRandom.uuid } + let(:lesson_params) { { user_id: copied_teacher_id } } + let(:scratch_content) do + { + targets: [ + { + isStage: true, + costumes: [ + { + assetId: 'teacher_asset', + md5ext: 'teacher_asset.png', + dataFormat: 'png' + } + ] + } + ], + monitors: [], + extensions: [], + meta: {} + } + end + + before do + lesson.project.update!(project_type: Project::Types::CODE_EDITOR_SCRATCH, locale: nil) + create(:scratch_component, project: lesson.project, content: scratch_content) + end + + it 'copies the Scratch component content' do + response = described_class.call(lesson:, lesson_params:) + copied_project = response[:lesson].reload.project + + expect(copied_project.scratch_component.content.to_h) + .to eq(scratch_content.deep_stringify_keys) + end + + it 'copies only teacher-owned Scratch assets to the new project owner' do + create_scratch_asset( + filename: 'teacher_asset.png', + project: lesson.project, + uploaded_user_id: teacher_id, + body: 'teacher-body' + ) + create_scratch_asset( + filename: 'student_asset.png', + project: lesson.project, + uploaded_user_id: SecureRandom.uuid, + body: 'student-body' + ) + + response = described_class.call(lesson:, lesson_params:) + copied_project = response[:lesson].reload.project + copied_asset = ScratchAsset.find_by!( + filename: 'teacher_asset.png', + project: copied_project, + uploaded_user_id: copied_teacher_id + ) + + visible_asset = ScratchAsset.find_visible_to_project( + project: copied_project, + user: User.new(id: SecureRandom.uuid), + filename: 'teacher_asset.png' + ) + + expect(copied_asset.file.download).to eq('teacher-body') + expect(visible_asset).to eq(copied_asset) + expect( + ScratchAsset.find_by(filename: 'student_asset.png', project: copied_project) + ).to be_nil + end + end + context 'when creating a copy fails' do let(:lesson_params) { { name: ' ' } } @@ -135,4 +207,10 @@ expect(Sentry).to have_received(:capture_exception).with(kind_of(StandardError)) end end + + def create_scratch_asset(filename:, project:, uploaded_user_id:, body:) + ScratchAsset.create!(filename:, project:, uploaded_user_id:).tap do |asset| + asset.file.attach(io: StringIO.new(body), filename:, content_type: 'image/png') + end + end end