Skip to content

Commit 3b2ff23

Browse files
authored
Scratch projects can be copied fix (#849)
## Status - Closes RaspberryPiFoundation/digital-editor-issues#1465 ## What's changed? The bug was that lesson copy already duplicated the lesson/project shell, but Scratch projects also need two Scratch-specific things: 1. scratch_component.content 2. project-owned scratch_assets Without those, the copied project exists, but Scratch cannot load the project/asset data correctly. https://github.com/user-attachments/assets/bb0aed59-69f7-4ba0-abfb-1d3ec06ad495
1 parent 18cfb66 commit 3b2ff23

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

lib/concepts/lesson/operations/create_copy.rb

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ class CreateCopy
55
class << self
66
def call(lesson:, lesson_params:)
77
response = OperationResponse.new
8-
response[:lesson] = build_copy(lesson, lesson_params)
9-
response[:lesson].save!
8+
9+
Lesson.transaction do
10+
response[:lesson] = build_copy(lesson, lesson_params)
11+
response[:lesson].save!
12+
copy_scratch_assets(lesson.project, response[:lesson].project)
13+
end
14+
1015
response
1116
rescue StandardError => e
1217
Sentry.capture_exception(e)
@@ -39,8 +44,30 @@ def build_project_copy(project, project_params)
3944
project_copy.components.build({ name: component.name, extension: component.extension, content: component.content })
4045
end
4146

47+
copy_scratch_component(project, project_copy)
48+
4249
project_copy
4350
end
51+
52+
def copy_scratch_component(project, project_copy)
53+
return unless project.scratch_project? && project.scratch_component
54+
55+
project_copy.build_scratch_component(content: project.scratch_component.content.deep_dup)
56+
end
57+
58+
def copy_scratch_assets(project, project_copy)
59+
return unless project.scratch_project?
60+
61+
project.scratch_assets.where(uploaded_user_id: project.user_id).find_each do |scratch_asset|
62+
next unless scratch_asset.file.attached?
63+
64+
scratch_asset_copy = project_copy.scratch_assets.create!(
65+
filename: scratch_asset.filename,
66+
uploaded_user_id: project_copy.user_id
67+
)
68+
scratch_asset_copy.file.attach(scratch_asset.file.blob)
69+
end
70+
end
4471
end
4572
end
4673
end

spec/concepts/lesson/create_copy_spec.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,78 @@
109109
expect(copied_component.content).to eq(original_component.content)
110110
end
111111

112+
context 'when the project is a Scratch project' do
113+
let(:copied_teacher_id) { SecureRandom.uuid }
114+
let(:lesson_params) { { user_id: copied_teacher_id } }
115+
let(:scratch_content) do
116+
{
117+
targets: [
118+
{
119+
isStage: true,
120+
costumes: [
121+
{
122+
assetId: 'teacher_asset',
123+
md5ext: 'teacher_asset.png',
124+
dataFormat: 'png'
125+
}
126+
]
127+
}
128+
],
129+
monitors: [],
130+
extensions: [],
131+
meta: {}
132+
}
133+
end
134+
135+
before do
136+
lesson.project.update!(project_type: Project::Types::CODE_EDITOR_SCRATCH, locale: nil)
137+
create(:scratch_component, project: lesson.project, content: scratch_content)
138+
end
139+
140+
it 'copies the Scratch component content' do
141+
response = described_class.call(lesson:, lesson_params:)
142+
copied_project = response[:lesson].reload.project
143+
144+
expect(copied_project.scratch_component.content.to_h)
145+
.to eq(scratch_content.deep_stringify_keys)
146+
end
147+
148+
it 'copies only teacher-owned Scratch assets to the new project owner' do
149+
create_scratch_asset(
150+
filename: 'teacher_asset.png',
151+
project: lesson.project,
152+
uploaded_user_id: teacher_id,
153+
body: 'teacher-body'
154+
)
155+
create_scratch_asset(
156+
filename: 'student_asset.png',
157+
project: lesson.project,
158+
uploaded_user_id: SecureRandom.uuid,
159+
body: 'student-body'
160+
)
161+
162+
response = described_class.call(lesson:, lesson_params:)
163+
copied_project = response[:lesson].reload.project
164+
copied_asset = ScratchAsset.find_by!(
165+
filename: 'teacher_asset.png',
166+
project: copied_project,
167+
uploaded_user_id: copied_teacher_id
168+
)
169+
170+
visible_asset = ScratchAsset.find_visible_to_project(
171+
project: copied_project,
172+
user: User.new(id: SecureRandom.uuid),
173+
filename: 'teacher_asset.png'
174+
)
175+
176+
expect(copied_asset.file.download).to eq('teacher-body')
177+
expect(visible_asset).to eq(copied_asset)
178+
expect(
179+
ScratchAsset.find_by(filename: 'student_asset.png', project: copied_project)
180+
).to be_nil
181+
end
182+
end
183+
112184
context 'when creating a copy fails' do
113185
let(:lesson_params) { { name: ' ' } }
114186

@@ -135,4 +207,10 @@
135207
expect(Sentry).to have_received(:capture_exception).with(kind_of(StandardError))
136208
end
137209
end
210+
211+
def create_scratch_asset(filename:, project:, uploaded_user_id:, body:)
212+
ScratchAsset.create!(filename:, project:, uploaded_user_id:).tap do |asset|
213+
asset.file.attach(io: StringIO.new(body), filename:, content_type: 'image/png')
214+
end
215+
end
138216
end

0 commit comments

Comments
 (0)