Skip to content

Commit 21602cc

Browse files
Allow class members create route to take teachers as well as students (#516)
## What's Changed? - Updated class member `create` and `create_batch` routes to accept both students and teachers - `ClassMembers::Create` operation now deals with both students and teachers closes #464 --------- Co-authored-by: Jamie Benstead <[email protected]>
1 parent 3caee3d commit 21602cc

File tree

6 files changed

+245
-91
lines changed

6 files changed

+245
-91
lines changed

app/controllers/api/class_members_controller.rb

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,16 @@ def index
2020
end
2121

2222
def create
23-
student_ids = [class_member_params[:student_id]]
24-
students = SchoolStudent::List.call(school: @school, token: current_user.token, student_ids:)
25-
result = ClassMember::Create.call(school_class: @school_class, students: students[:school_students])
23+
user_ids = [class_member_params[:user_id]]
24+
user_type = class_member_params[:type]
25+
if user_type == 'teacher'
26+
teachers = SchoolTeacher::List.call(school: @school, teacher_ids: user_ids)
27+
students = { school_students: [] }
28+
else
29+
teachers = { school_teachers: [] }
30+
students = SchoolStudent::List.call(school: @school, token: current_user.token, student_ids: user_ids)
31+
end
32+
result = ClassMember::Create.call(school_class: @school_class, students: students[:school_students], teachers: teachers[:school_teachers])
2633

2734
if result.success?
2835
@class_member = result[:class_members].first
@@ -33,8 +40,16 @@ def create
3340
end
3441

3542
def create_batch
36-
students = SchoolStudent::List.call(school: @school, token: current_user.token, student_ids: create_batch_params)
37-
result = ClassMember::Create.call(school_class: @school_class, students: students[:school_students])
43+
# Teacher objects needs to be the compliment of student objects so that every user creation is attempted and validated.
44+
student_objects = create_batch_params.select { |user| user[:type] == 'student' }
45+
teacher_objects = create_batch_params.select { |user| student_objects.pluck(:user_id).exclude?(user[:user_id]) }
46+
student_ids = student_objects.pluck(:user_id)
47+
teacher_ids = teacher_objects.pluck(:user_id)
48+
49+
students = list_students(@school, current_user.token, student_ids)
50+
teachers = list_teachers(@school, teacher_ids)
51+
52+
result = ClassMember::Create.call(school_class: @school_class, students: students[:school_students], teachers: teachers[:school_teachers])
3853

3954
if result.success?
4055
@class_members = result[:class_members]
@@ -57,11 +72,33 @@ def destroy
5772
private
5873

5974
def class_member_params
60-
params.require(:class_member).permit(:student_id)
75+
params.require(:class_member).permit(:user_id, :type)
6176
end
6277

6378
def create_batch_params
64-
params.permit(student_ids: [])
79+
class_members = params.require(:class_members)
80+
81+
class_members.map do |class_member|
82+
next if class_member.blank?
83+
84+
class_member.permit(:user_id, :type).to_h.with_indifferent_access
85+
end
86+
end
87+
88+
def list_students(school, _token, student_ids)
89+
if student_ids.present?
90+
SchoolStudent::List.call(school:, token: current_user.token, student_ids:)
91+
else
92+
{ school_students: [] }
93+
end
94+
end
95+
96+
def list_teachers(school, teacher_ids)
97+
if teacher_ids.present?
98+
SchoolTeacher::List.call(school:, teacher_ids:)
99+
else
100+
{ school_teachers: [] }
101+
end
65102
end
66103
end
67104
end

app/views/api/class_members/_class_member.json.jbuilder

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ json.call(
44
class_member,
55
:id,
66
:school_class_id,
7-
:student_id,
87
:created_at,
98
:updated_at
109
)
1110

12-
if class_member.student.present?
11+
if class_member.respond_to?(:student)
12+
json.student_id(class_member.student_id)
1313
json.set! :student do
1414
json.partial! '/api/school_students/school_student', student: class_member.student
1515
end
16+
elsif class_member.respond_to?(:teacher)
17+
json.teacher_id(class_member.teacher_id)
18+
json.set! :teacher do
19+
json.partial! '/api/school_teachers/school_teacher', teacher: class_member.teacher
20+
end
1621
end

lib/concepts/class_member/operations/create.rb

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
module ClassMember
44
class Create
55
class << self
6-
def call(school_class:, students:)
6+
def call(school_class:, students: [], teachers: [])
77
response = OperationResponse.new
88
response[:class_members] = []
99
response[:errors] = {}
10-
raise ArgumentError, 'No valid students provided' if students.blank?
10+
raise ArgumentError, 'No valid school members provided' if students.blank? && teachers.blank?
1111

12-
create_class_members(school_class:, students:, response:)
12+
create_class_teachers(school_class:, teachers:, response:)
13+
create_class_students(school_class:, students:, response:)
1314
response
1415
rescue StandardError => e
1516
Sentry.capture_exception(e)
@@ -19,21 +20,39 @@ def call(school_class:, students:)
1920

2021
private
2122

22-
def create_class_members(school_class:, students:, response:)
23+
def create_class_teachers(school_class:, teachers:, response:)
24+
teachers.each do |teacher|
25+
class_teacher = school_class.teachers.build({ teacher_id: teacher.id })
26+
class_teacher.teacher = teacher
27+
class_teacher.save!
28+
response[:class_members] << class_teacher
29+
rescue StandardError => e
30+
handle_class_teacher_error(e, class_teacher, teacher, response)
31+
response
32+
end
33+
end
34+
35+
def create_class_students(school_class:, students:, response:)
2336
students.each do |student|
2437
class_student = school_class.students.build({ student_id: student.id })
2538
class_student.student = student
2639
class_student.save!
2740
response[:class_members] << class_student
2841
rescue StandardError => e
29-
handle_class_member_error(e, class_student, student, response)
42+
handle_class_student_error(e, class_student, student, response)
3043
response
3144
end
3245
end
3346

34-
def handle_class_member_error(exception, class_member, student, response)
47+
def handle_class_teacher_error(exception, class_teacher, teacher, response)
48+
Sentry.capture_exception(exception)
49+
errors = class_teacher.errors.full_messages.join(',')
50+
response[:errors][teacher.id] = "Error creating class member for teacher_id #{teacher.id}: #{errors}"
51+
end
52+
53+
def handle_class_student_error(exception, class_student, student, response)
3554
Sentry.capture_exception(exception)
36-
errors = class_member.errors.full_messages.join(',')
55+
errors = class_student.errors.full_messages.join(',')
3756
response[:errors][student.id] = "Error creating class member for student_id #{student.id}: #{errors}"
3857
end
3958
end

spec/concepts/class_member/create_spec.rb

Lines changed: 67 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,38 @@
77
let(:school) { create(:school) }
88
let(:students) { create_list(:student, 3, school:) }
99
let(:teacher) { create(:teacher, school:) }
10+
let(:teachers) { [create(:teacher, school:), create(:teacher, school:)] }
1011

1112
let(:student_ids) { students.map(&:id) }
1213

1314
it 'returns a successful operation response' do
14-
response = described_class.call(school_class:, students:)
15+
response = described_class.call(school_class:, students:, teachers:)
1516
expect(response.success?).to be(true)
1617
end
1718

1819
it 'creates class students' do
19-
expect { described_class.call(school_class:, students:) }.to change(ClassStudent, :count).by(3)
20+
expect { described_class.call(school_class:, students:, teachers:) }.to change(ClassStudent, :count).by(3)
21+
end
22+
23+
it 'creates class teachers' do
24+
expect { described_class.call(school_class:, students:, teachers:) }.to change(ClassTeacher, :count).by(2)
2025
end
2126

2227
it 'returns a class members JSON array' do
23-
response = described_class.call(school_class:, students:)
24-
expect(response[:class_members].size).to eq(3)
28+
response = described_class.call(school_class:, students:, teachers:)
29+
expect(response[:class_members].size).to eq(5)
2530
end
2631

2732
it 'returns class students in the operation response' do
28-
response = described_class.call(school_class:, students:)
29-
expect(response[:class_members]).to all(be_a(ClassStudent))
33+
response = described_class.call(school_class:, students:, teachers:)
34+
class_students_count = response[:class_members].count { |member| member.is_a?(ClassStudent) }
35+
expect(class_students_count).to eq(3)
36+
end
37+
38+
it 'returns class teachers in the operation response' do
39+
response = described_class.call(school_class:, students:, teachers:)
40+
class_teachers_count = response[:class_members].count { |member| member.is_a?(ClassTeacher) }
41+
expect(class_teachers_count).to eq(2)
3042
end
3143

3244
it 'assigns the school_class' do
@@ -35,41 +47,53 @@
3547
end
3648

3749
it 'assigns the student_id' do
38-
response = described_class.call(school_class:, students:)
39-
expect(response[:class_members].map(&:student_id)).to match_array(student_ids)
50+
response = described_class.call(school_class:, students:, teachers:)
51+
response_students = response[:class_members].select { |member| member.is_a?(ClassStudent) }
52+
expect(response_students.map(&:student_id)).to match_array(student_ids)
53+
end
54+
55+
it 'assigns the teacher_id' do
56+
teacher_ids = teachers.map(&:id)
57+
response = described_class.call(school_class:, students:, teachers:)
58+
response_teachers = response[:class_members].select { |member| member.is_a?(ClassTeacher) }
59+
expect(response_teachers.map(&:teacher_id)).to match_array(teacher_ids)
4060
end
4161

4262
context 'when creations fail' do
4363
before do
4464
allow(Sentry).to receive(:capture_exception)
4565
end
4666

47-
context 'with malformed students' do
67+
context 'with malformed members' do
4868
let(:students) { nil }
69+
let(:teachers) { nil }
70+
71+
it 'does not create a class student' do
72+
expect { described_class.call(school_class:, students:, teachers:) }.not_to change(ClassStudent, :count)
73+
end
4974

50-
it 'does not create a class member' do
51-
expect { described_class.call(school_class:, students:) }.not_to change(ClassStudent, :count)
75+
it 'does not create a class teacher' do
76+
expect { described_class.call(school_class:, students:, teachers:) }.not_to change(ClassTeacher, :count)
5277
end
5378

5479
it 'returns a failed operation response' do
55-
response = described_class.call(school_class:, students:)
80+
response = described_class.call(school_class:, students:, teachers:)
5681
expect(response.failure?).to be(true)
5782
end
5883

5984
it 'returns the error message in the operation response' do
60-
response = described_class.call(school_class:, students:)
61-
expect(response[:error]).to match(/No valid students provided/)
85+
response = described_class.call(school_class:, students:, teachers:)
86+
expect(response[:error]).to match(/No valid school members provided/)
6287
end
6388

6489
it 'sent the exception to Sentry' do
65-
described_class.call(school_class:, students:)
90+
described_class.call(school_class:, students:, teachers:)
6691
expect(Sentry).to have_received(:capture_exception).with(kind_of(StandardError))
6792
end
6893
end
6994

7095
context 'with a student from a different school' do
71-
let(:different_school) { create(:school) }
72-
let(:different_school_student) { create(:student, school: different_school) }
96+
let(:different_school_student) { create(:student, school: create(:school)) }
7397

7498
context 'with non existent students' do
7599
let(:students) { [different_school_student] }
@@ -103,37 +127,52 @@
103127
let(:new_students) { students + [different_school_student] }
104128

105129
it 'returns a successful operation response' do
106-
response = described_class.call(school_class:, students: new_students)
130+
response = described_class.call(school_class:, students: new_students, teachers:)
107131
expect(response.success?).to be(true)
108132
end
109133

110134
it 'returns a class members JSON array' do
111-
response = described_class.call(school_class:, students: new_students)
112-
expect(response[:class_members].size).to eq(3)
135+
response = described_class.call(school_class:, students: new_students, teachers:)
136+
expect(response[:class_members].size).to eq(5)
113137
end
114138

115-
it 'returns class members in the operation response' do
116-
response = described_class.call(school_class:, students: new_students)
117-
expect(response[:class_members]).to all(be_a(ClassStudent))
139+
it 'returns class students in the operation response' do
140+
response = described_class.call(school_class:, students: new_students, teachers:)
141+
class_students_count = response[:class_members].count { |member| member.is_a?(ClassStudent) }
142+
expect(class_students_count).to eq(3)
143+
end
144+
145+
it 'returns class teachers in the operation response' do
146+
response = described_class.call(school_class:, students: new_students, teachers:)
147+
class_teachers_count = response[:class_members].count { |member| member.is_a?(ClassTeacher) }
148+
expect(class_teachers_count).to eq(2)
118149
end
119150

120151
it 'assigns the school_class' do
121-
response = described_class.call(school_class:, students: new_students)
152+
response = described_class.call(school_class:, students: new_students, teachers:)
122153
expect(response[:class_members]).to all(have_attributes(school_class:))
123154
end
124155

125156
it 'assigns the successful students' do
126-
response = described_class.call(school_class:, students: new_students)
127-
expect(response[:class_members].map(&:student_id)).to match_array(student_ids)
157+
response = described_class.call(school_class:, students: new_students, teachers:)
158+
response_students = response[:class_members].select { |member| member.is_a?(ClassStudent) }
159+
expect(response_students.map(&:student_id)).to match_array(student_ids)
160+
end
161+
162+
it 'assigns the successful teachers' do
163+
teacher_ids = teachers.map(&:id)
164+
response = described_class.call(school_class:, students: new_students, teachers:)
165+
response_teachers = response[:class_members].select { |member| member.is_a?(ClassTeacher) }
166+
expect(response_teachers.map(&:teacher_id)).to match_array(teacher_ids)
128167
end
129168

130169
it 'returns the error messages in the operation response' do
131-
response = described_class.call(school_class:, students: new_students)
170+
response = described_class.call(school_class:, students: new_students, teachers:)
132171
expect(response[:errors][different_school_student.id]).to eq("Error creating class member for student_id #{different_school_student.id}: Student '#{different_school_student.id}' does not have the 'school-student' role for organisation '#{school.id}'")
133172
end
134173

135174
it 'sent the exception to Sentry' do
136-
described_class.call(school_class:, students: new_students)
175+
described_class.call(school_class:, students: new_students, teachers:)
137176
expect(Sentry).to have_received(:capture_exception).with(kind_of(StandardError))
138177
end
139178
end

0 commit comments

Comments
 (0)