Skip to content

Commit b46820b

Browse files
Auto-creating project when lesson created (#342)
Co-authored-by: Dan Halson <[email protected]>
1 parent 3f07206 commit b46820b

File tree

14 files changed

+149
-45
lines changed

14 files changed

+149
-45
lines changed

Gemfile.lock

+1
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ GEM
498498
PLATFORMS
499499
aarch64-linux
500500
arm64-darwin-22
501+
arm64-darwin-23
501502
x86_64-linux
502503

503504
DEPENDENCIES

app/controllers/api/lessons_controller.rb

+10-9
Original file line numberDiff line numberDiff line change
@@ -72,24 +72,25 @@ def verify_school_class_belongs_to_school
7272
end
7373

7474
def lesson_params
75-
if school_owner?
76-
# A school owner must specify who the lesson user is.
77-
base_params
78-
else
79-
# A school teacher may only create lessons they own.
80-
base_params.merge(user_id: current_user.id)
81-
end
75+
base_params.merge(user_id: current_user.id)
8276
end
8377

8478
def base_params
8579
params.fetch(:lesson, {}).permit(
8680
:school_id,
8781
:school_class_id,
88-
:user_id,
8982
:name,
9083
:description,
9184
:visibility,
92-
:due_date
85+
:due_date,
86+
{
87+
project_attributes: [
88+
:name,
89+
:project_type,
90+
:locale,
91+
{ components: %i[id name extension content index default] }
92+
]
93+
}
9394
)
9495
end
9596

app/models/lesson.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ class Lesson < ApplicationRecord
55
belongs_to :school_class, optional: true
66
belongs_to :parent, optional: true, class_name: :Lesson, foreign_key: :copied_from_id, inverse_of: :copies
77
has_many :copies, dependent: :nullify, class_name: :Lesson, foreign_key: :copied_from_id, inverse_of: :parent
8-
has_many :projects, dependent: :nullify
8+
has_one :project, dependent: :nullify
9+
accepts_nested_attributes_for :project
910

1011
before_validation :assign_school_from_school_class
1112
before_destroy -> { throw :abort }

app/views/api/lessons/show.json.jbuilder

+8
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,12 @@ json.call(
1818
:updated_at
1919
)
2020

21+
if lesson.project
22+
json.project(
23+
lesson.project,
24+
:identifier,
25+
:project_type
26+
)
27+
end
28+
2129
json.user_name(user&.name)

lib/concepts/lesson/operations/create.rb

+18-3
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,30 @@ class Create
55
class << self
66
def call(lesson_params:)
77
response = OperationResponse.new
8-
response[:lesson] = Lesson.new(lesson_params)
8+
response[:lesson] = build_lesson(lesson_params)
99
response[:lesson].save!
1010
response
1111
rescue StandardError => e
1212
Sentry.capture_exception(e)
13-
errors = response[:lesson].errors.full_messages.join(',')
14-
response[:error] = "Error creating lesson: #{errors}"
13+
if response[:lesson].nil?
14+
response[:error] = "Error creating lesson #{e}"
15+
else
16+
errors = response[:lesson].errors.full_messages.join(',')
17+
response[:error] = "Error creating lesson: #{errors}"
18+
end
1519
response
1620
end
21+
22+
private
23+
24+
def build_lesson(lesson_hash)
25+
new_lesson = Lesson.new(lesson_hash.except(:project_attributes))
26+
project_params = lesson_hash[:project_attributes].merge({ user_id: lesson_hash[:user_id],
27+
school_id: lesson_hash[:school_id],
28+
lesson_id: new_lesson.id })
29+
new_lesson.project = Project.new(project_params)
30+
new_lesson
31+
end
1732
end
1833
end
1934
end

lib/concepts/project/operations/create.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def call(project_hash:)
1010
response
1111
rescue StandardError => e
1212
Sentry.capture_exception(e)
13-
response[:error] = 'Error creating project'
13+
response[:error] = "Error creating project: #{e}"
1414
response
1515
end
1616

lib/tasks/classroom_management.rake

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ namespace :classroom_management do
2727
exit
2828
end
2929

30-
Project.where(school_id:).destroy_all
30+
lesson_id = Lesson.where(school_id:).pluck(:id)
31+
Project.where(lesson_id:).destroy_all
3132
# The `before_destroy` prevents us using destroy, but as we've removed projects already we can safely delete
3233
Lesson.where(school_id:).delete_all
3334
SchoolClass.where(school_id:).destroy_all

spec/concepts/lesson/create_spec.rb

+75-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@
66
let(:teacher_id) { SecureRandom.uuid }
77

88
let(:lesson_params) do
9-
{ name: 'Test Lesson', user_id: teacher_id }
9+
{
10+
name: 'Test Lesson',
11+
user_id: teacher_id,
12+
project_attributes: {
13+
name: 'Hello world project',
14+
project_type: 'python',
15+
components: [
16+
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
17+
]
18+
}
19+
}
1020
end
1121

1222
it 'returns a successful operation response' do
@@ -28,13 +38,71 @@
2838
expect(response[:lesson].name).to eq('Test Lesson')
2939
end
3040

41+
it 'creates a project for the lesson' do
42+
expect { described_class.call(lesson_params:) }.to change(Project, :count).by(1)
43+
end
44+
45+
it 'associates the project to the lesson' do
46+
response = described_class.call(lesson_params:)
47+
expect(response[:lesson].project).to be_a(Project)
48+
end
49+
3150
it 'assigns the user_id' do
3251
response = described_class.call(lesson_params:)
3352
expect(response[:lesson].user_id).to eq(teacher_id)
3453
end
3554

36-
context 'when creation fails' do
37-
let(:lesson_params) { {} }
55+
context 'when lesson creation fails' do
56+
let(:lesson_params) do
57+
{
58+
project_attributes: {
59+
name: 'Hello world project',
60+
project_type: 'python',
61+
components: [
62+
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
63+
]
64+
}
65+
}
66+
end
67+
68+
before do
69+
allow(Sentry).to receive(:capture_exception)
70+
end
71+
72+
it 'does not create a lesson' do
73+
expect { described_class.call(lesson_params:) }.not_to change(Lesson, :count)
74+
end
75+
76+
it 'does not create a project' do
77+
expect { described_class.call(lesson_params:) }.not_to change(Project, :count)
78+
end
79+
80+
it 'returns a failed operation response' do
81+
response = described_class.call(lesson_params:)
82+
expect(response.failure?).to be(true)
83+
end
84+
85+
it 'returns the error message in the operation response' do
86+
response = described_class.call(lesson_params:)
87+
expect(response[:error]).to match(/Error creating lesson/)
88+
end
89+
90+
it 'sent the exception to Sentry' do
91+
described_class.call(lesson_params:)
92+
expect(Sentry).to have_received(:capture_exception).with(kind_of(StandardError))
93+
end
94+
end
95+
96+
context 'when project creation fails' do
97+
let(:lesson_params) do
98+
{
99+
name: 'Test Lesson',
100+
user_id: teacher_id,
101+
project_attributes: {
102+
invalid_attribute: 'blah blah blah'
103+
}
104+
}
105+
end
38106

39107
before do
40108
allow(Sentry).to receive(:capture_exception)
@@ -44,6 +112,10 @@
44112
expect { described_class.call(lesson_params:) }.not_to change(Lesson, :count)
45113
end
46114

115+
it 'does not create a project' do
116+
expect { described_class.call(lesson_params:) }.not_to change(Project, :count)
117+
end
118+
47119
it 'returns a failed operation response' do
48120
response = described_class.call(lesson_params:)
49121
expect(response.failure?).to be(true)

spec/concepts/project/create_spec.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
end
5555

5656
it 'returns error message' do
57-
expect(create_project[:error]).to eq('Error creating project')
57+
expect(create_project[:error]).to eq('Error creating project: Some error')
5858
end
5959

6060
it 'sent the exception to Sentry' do

spec/factories/lesson.rb

+1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@
66
sequence(:name) { |n| "Lesson #{n}" }
77
description { 'Description' }
88
visibility { 'private' }
9+
project { create(:project) }
910
end
1011
end

spec/features/lesson/creating_a_lesson_spec.rb

+22-15
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@
1616
let(:params) do
1717
{
1818
lesson: {
19-
name: 'Test Lesson'
19+
name: 'Test Lesson',
20+
project_attributes: {
21+
name: 'Hello world project',
22+
project_type: 'python',
23+
components: [
24+
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
25+
]
26+
}
2027
}
2128
}
2229
end
@@ -59,7 +66,13 @@
5966
lesson: {
6067
name: 'Test Lesson',
6168
school_id: school.id,
62-
user_id: teacher.id
69+
project_attributes: {
70+
name: 'Hello world project',
71+
project_type: 'python',
72+
components: [
73+
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
74+
]
75+
}
6376
}
6477
}
6578
end
@@ -76,13 +89,6 @@
7689
expect(response).to have_http_status(:created)
7790
end
7891

79-
it 'sets the lesson user to the specified user for school-owner users' do
80-
post('/api/lessons', headers:, params:)
81-
data = JSON.parse(response.body, symbolize_names: true)
82-
83-
expect(data[:user_id]).to eq(teacher.id)
84-
end
85-
8692
it 'sets the lesson user to the current user for school-teacher users' do
8793
authenticated_in_hydra_as(teacher)
8894
new_params = { lesson: params[:lesson].merge(user_id: 'ignored') }
@@ -122,16 +128,17 @@
122128
name: 'Test Lesson',
123129
school_id: school.id,
124130
school_class_id: school_class.id,
125-
user_id: teacher.id
131+
project_attributes: {
132+
name: 'Hello world project',
133+
project_type: 'python',
134+
components: [
135+
{ name: 'main.py', extension: 'py', content: 'print("Hello, world!")' }
136+
]
137+
}
126138
}
127139
}
128140
end
129141

130-
it 'responds 201 Created' do
131-
post('/api/lessons', headers:, params:)
132-
expect(response).to have_http_status(:created)
133-
end
134-
135142
it 'responds 201 Created when the user is the school-teacher for the class' do
136143
authenticated_in_hydra_as(teacher)
137144
school_class.update!(teacher_id: teacher.id)

spec/features/lesson/updating_a_lesson_spec.rb

+5-7
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
end
6969

7070
it 'responds 200 OK when assigning the lesson to a school class' do
71+
authenticated_in_hydra_as(teacher)
7172
school_class = create(:school_class, school:, teacher_id: teacher.id)
7273

7374
new_params = { lesson: params[:lesson].merge(school_class_id: school_class.id) }
@@ -105,6 +106,10 @@
105106
let(:school_class) { create(:school_class, teacher_id: teacher.id, school:) }
106107
let!(:lesson) { create(:lesson, school_class:, name: 'Test Lesson', visibility: 'students', user_id: teacher.id) }
107108

109+
before do
110+
authenticated_in_hydra_as(teacher)
111+
end
112+
108113
it 'responds 200 OK when the user is a school-owner' do
109114
put("/api/lessons/#{lesson.id}", headers:, params:)
110115
expect(response).to have_http_status(:ok)
@@ -122,12 +127,5 @@
122127
expect(response).to have_http_status(:unprocessable_entity)
123128
end
124129
# rubocop:enable RSpec/ExampleLength
125-
126-
it 'responds 422 Unprocessable Entity when trying to re-assign the lesson to a different user' do
127-
new_params = { lesson: params[:lesson].merge(user_id: owner.id) }
128-
put("/api/lessons/#{lesson.id}", headers:, params: new_params)
129-
130-
expect(response).to have_http_status(:unprocessable_entity)
131-
end
132130
end
133131
end

spec/lib/classroom_management_spec.rb

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
create(:teacher_role, user_id: creator_id, school:)
1717
create(:school_class, school_id: school.id, teacher_id: creator_id)
1818
create(:lesson, school_id: school.id, user_id: creator_id)
19-
create(:project, school_id: school.id, user_id: creator_id)
2019
end
2120

2221
# rubocop:disable RSpec/ExampleLength, RSpec/MultipleExpectations

spec/models/lesson_spec.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
expect(lesson.copies.size).to eq(2)
3434
end
3535

36-
it 'has many projects' do
36+
it 'has one project' do
3737
user_id = SecureRandom.uuid
38-
lesson = create(:lesson, user_id:, projects: [build(:project, user_id:)])
39-
expect(lesson.projects.size).to eq(1)
38+
lesson = create(:lesson, user_id:, project: build(:project, user_id:))
39+
expect(lesson.project).to be_a(Project)
4040
end
4141
end
4242

0 commit comments

Comments
 (0)