Skip to content

Commit 28c2261

Browse files
Add bulk lesson creation via lesson_projects
Expand the LessonsController#create to accept a lesson_projects array in the request body and create multiple lessons in one request. Add bulk_create_params to the LessonsController, permitting origin_identifier on each entry so the caller can correlate responses with the original projects. Add Lesson::CreateBulk to create individual Lessons and echo origin_identifier on each result.
1 parent 18cfb66 commit 28c2261

5 files changed

Lines changed: 345 additions & 11 deletions

File tree

app/controllers/api/lessons_controller.rb

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,20 @@ def show
3030
end
3131

3232
def create
33-
result = Lesson::Create.call(lesson_params: create_params)
34-
35-
if result.success?
36-
@lesson_with_user = result[:lesson].with_user
37-
render :show, formats: [:json], status: :created
33+
if params[:lesson_projects].present?
34+
@results = Lesson::CreateBulk.call(
35+
lessons_params: params[:lesson_projects].map { |entry| bulk_create_params(entry) }
36+
)
37+
@user = current_user
38+
render :create_bulk, formats: [:json], status: :created
3839
else
39-
render json: { error: result[:error] }, status: :unprocessable_content
40+
result = Lesson::Create.call(lesson_params: create_params)
41+
if result.success?
42+
@lesson_with_user = result[:lesson].with_user
43+
render :show, formats: [:json], status: :created
44+
else
45+
render json: { error: result[:error] }, status: :unprocessable_content
46+
end
4047
end
4148
end
4249

@@ -78,18 +85,50 @@ def filtered_lessons_scope
7885
end
7986

8087
def verify_school_class_belongs_to_school
81-
return if create_params[:school_class_id].blank?
82-
return if school&.classes&.pluck(:id)&.include?(create_params[:school_class_id])
88+
if params[:lesson_projects].present?
89+
params[:lesson_projects].each { |lesson_params| verify_lesson_school_class!(lesson_params) }
90+
else
91+
verify_lesson_school_class!(create_params)
92+
end
93+
end
94+
95+
def verify_lesson_school_class!(lesson_params)
96+
school_class_id = lesson_params[:school_class_id]
97+
return if school_class_id.blank?
98+
99+
school = School.find_by(id: lesson_params[:school_id])
100+
return if school&.classes&.exists?(id: school_class_id)
83101

84102
raise ParameterError, 'school_class_id does not correspond to school_id'
85103
end
86104

87105
def verify_can_create_scratch_projects
88-
return unless scratch_project? && !school.scratch_enabled?
106+
if params[:lesson_projects].present?
107+
params[:lesson_projects].each do |lesson_params|
108+
verify_lesson_scratch!(lesson_params)
109+
return if performed?
110+
end
111+
else
112+
verify_lesson_scratch!(create_params)
113+
end
114+
end
115+
116+
def verify_lesson_scratch!(lesson_params)
117+
return unless scratch_project?(lesson_params)
118+
return if scratch_enabled_for_school?(lesson_params[:school_id])
89119

90120
render json: { error: 'Forbidden' }, status: :forbidden
91121
end
92122

123+
def scratch_enabled_for_school?(school_id)
124+
return false if school_id.blank?
125+
126+
@scratch_enabled_by_school_id ||= {}
127+
return @scratch_enabled_by_school_id[school_id] if @scratch_enabled_by_school_id.key?(school_id)
128+
129+
@scratch_enabled_by_school_id[school_id] = School.find_by(id: school_id)&.scratch_enabled?
130+
end
131+
93132
def user_remixes(lessons)
94133
lessons.map { |lesson| user_remix(lesson) }
95134
end
@@ -104,8 +143,8 @@ def user_remix(lesson)
104143
)
105144
end
106145

107-
def scratch_project?
108-
create_params.dig(:project_attributes, :project_type) == Project::Types::CODE_EDITOR_SCRATCH
146+
def scratch_project?(lesson_params)
147+
lesson_params.dig(:project_attributes, :project_type) == Project::Types::CODE_EDITOR_SCRATCH
109148
end
110149

111150
def update_params
@@ -118,6 +157,27 @@ def update_params
118157
)
119158
end
120159

160+
def bulk_create_params(lesson_project)
161+
lesson_project.permit(
162+
:school_id,
163+
:school_class_id,
164+
:name,
165+
:description,
166+
:visibility,
167+
:due_date,
168+
:origin_identifier,
169+
{
170+
project_attributes: [
171+
:name,
172+
:project_type,
173+
:locale,
174+
{ components: %i[id name extension content index default] },
175+
{ scratch_component: {} }
176+
]
177+
}
178+
).merge(user_id: current_user.id)
179+
end
180+
121181
def create_params
122182
params.fetch(:lesson, {}).permit(
123183
:school_id,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
json.array!(@results) do |result|
4+
if result.success?
5+
json.partial! 'lesson', lesson: result[:lesson], user: @user
6+
else
7+
json.error result[:error]
8+
end
9+
10+
json.origin_identifier result[:origin_identifier] if result[:origin_identifier].present?
11+
end
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
class Lesson
4+
class CreateBulk
5+
class << self
6+
def call(lessons_params:)
7+
lessons_params.map { |lesson| create_one(lesson) }
8+
end
9+
10+
private
11+
12+
def create_one(lesson_params)
13+
origin_identifier = lesson_params[:origin_identifier]
14+
Lesson::Create.call(lesson_params: lesson_params.except(:origin_identifier)).tap do |result|
15+
result[:origin_identifier] = origin_identifier if origin_identifier.present?
16+
end
17+
end
18+
end
19+
end
20+
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe Lesson::CreateBulk, type: :unit do
6+
let(:school) { create(:school) }
7+
let(:teacher) { create(:teacher, school:) }
8+
9+
let(:lessons_params) do
10+
[
11+
{
12+
name: 'Test Lesson',
13+
user_id: teacher.id,
14+
school_id: school.id,
15+
origin_identifier: 'test-lesson-identifier-one',
16+
project_attributes: {
17+
name: 'Hello world project',
18+
project_type: Project::Types::PYTHON,
19+
components: [
20+
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
21+
]
22+
}
23+
},
24+
{
25+
name: 'Test Lesson 2',
26+
user_id: teacher.id,
27+
school_id: school.id,
28+
origin_identifier: 'test-lesson-identifier-two',
29+
project_attributes: {
30+
name: 'Hello world project',
31+
project_type: Project::Types::PYTHON,
32+
components: [
33+
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
34+
]
35+
}
36+
}
37+
]
38+
end
39+
40+
context 'with a teacher' do
41+
let(:result) { described_class.call(lessons_params:) }
42+
43+
before do
44+
allow(User).to receive(:from_userinfo).with(ids: teacher.id).and_return([teacher])
45+
end
46+
47+
it 'returns a successful operation response for the first lesson' do
48+
expect(result.first.success?).to be(true)
49+
end
50+
51+
it 'returns a successful operation response for the second lesson' do
52+
expect(result.second.success?).to be(true)
53+
end
54+
55+
it 'creates multiple lessons' do
56+
expect { described_class.call(lessons_params:) }.to change(Lesson, :count).by(2)
57+
end
58+
59+
it 'does not pass origin_identifier to lesson creation' do
60+
received_params = []
61+
allow(Lesson::Create).to receive(:call).and_wrap_original do |method, lesson_params:|
62+
received_params << lesson_params
63+
method.call(lesson_params:)
64+
end
65+
66+
described_class.call(lessons_params:)
67+
68+
expect(received_params).to all(satisfy { |params| !params.key?(:origin_identifier) })
69+
end
70+
71+
it 'appends the origin_identifier to the first created lesson' do
72+
expect(result.first[:origin_identifier]).to eq('test-lesson-identifier-one')
73+
end
74+
75+
it 'appends the origin_identifier to the second created lesson' do
76+
expect(result.second[:origin_identifier]).to eq('test-lesson-identifier-two')
77+
end
78+
end
79+
end

0 commit comments

Comments
 (0)