Skip to content

Commit 385f5a1

Browse files
authored
Create models and controllers for lessons (#258)
2 parents c0e104d + 0148d94 commit 385f5a1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2507
-102
lines changed
+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# frozen_string_literal: true
2+
3+
module Api
4+
class LessonsController < ApiController
5+
before_action :authorize_user, except: %i[index show]
6+
before_action :verify_school_class_belongs_to_school, only: :create
7+
load_and_authorize_resource :lesson
8+
9+
def index
10+
scope = params[:include_archived] == 'true' ? Lesson : Lesson.unarchived
11+
@lessons_with_users = scope.accessible_by(current_ability).with_users
12+
render :index, formats: [:json], status: :ok
13+
end
14+
15+
def show
16+
@lesson_with_user = @lesson.with_user
17+
render :show, formats: [:json], status: :ok
18+
end
19+
20+
def create
21+
result = Lesson::Create.call(lesson_params:)
22+
23+
if result.success?
24+
@lesson_with_user = result[:lesson].with_user
25+
render :show, formats: [:json], status: :created
26+
else
27+
render json: { error: result[:error] }, status: :unprocessable_entity
28+
end
29+
end
30+
31+
def create_copy
32+
result = Lesson::CreateCopy.call(lesson: @lesson, lesson_params:)
33+
34+
if result.success?
35+
@lesson_with_user = result[:lesson].with_user
36+
render :show, formats: [:json], status: :created
37+
else
38+
render json: { error: result[:error] }, status: :unprocessable_entity
39+
end
40+
end
41+
42+
def update
43+
result = Lesson::Update.call(lesson: @lesson, lesson_params:)
44+
45+
if result.success?
46+
@lesson_with_user = result[:lesson].with_user
47+
render :show, formats: [:json], status: :ok
48+
else
49+
render json: { error: result[:error] }, status: :unprocessable_entity
50+
end
51+
end
52+
53+
def destroy
54+
operation = params[:undo] == 'true' ? Lesson::Unarchive : Lesson::Archive
55+
result = operation.call(lesson: @lesson)
56+
57+
if result.success?
58+
head :no_content
59+
else
60+
render json: { error: result[:error] }, status: :unprocessable_entity
61+
end
62+
end
63+
64+
private
65+
66+
def verify_school_class_belongs_to_school
67+
return if base_params[:school_class_id].blank?
68+
return if school&.classes&.pluck(:id)&.include?(base_params[:school_class_id])
69+
70+
raise ParameterError, 'school_class_id does not correspond to school_id'
71+
end
72+
73+
def lesson_params
74+
if school_owner?
75+
# A school owner must specify who the lesson user is.
76+
base_params
77+
else
78+
# A school teacher may only create lessons they own.
79+
base_params.merge(user_id: current_user.id)
80+
end
81+
end
82+
83+
def base_params
84+
params.fetch(:lesson, {}).permit(
85+
:school_id,
86+
:school_class_id,
87+
:user_id,
88+
:name,
89+
:description,
90+
:visibility,
91+
:due_date
92+
)
93+
end
94+
95+
def school_owner?
96+
school && current_user.school_owner?(organisation_id: school.id)
97+
end
98+
99+
def school
100+
@school ||= @lesson&.school || School.find_by(id: base_params[:school_id])
101+
end
102+
end
103+
end

app/controllers/api/projects_controller.rb

+35-10
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ class ProjectsController < ApiController
77
before_action :authorize_user, only: %i[create update index destroy]
88
before_action :load_project, only: %i[show update destroy]
99
before_action :load_projects, only: %i[index]
10-
after_action :pagination_link_header, only: [:index]
1110
load_and_authorize_resource
12-
skip_load_resource only: :create
11+
before_action :verify_lesson_belongs_to_school, only: :create
12+
after_action :pagination_link_header, only: %i[index]
1313

1414
def index
1515
@paginated_projects = @projects.page(params[:page])
@@ -21,25 +21,23 @@ def show
2121
end
2222

2323
def create
24-
project_hash = project_params.merge(user_id: current_user&.id)
25-
result = Project::Create.call(project_hash:)
24+
result = Project::Create.call(project_hash: project_params)
2625

2726
if result.success?
2827
@project = result[:project]
29-
render :show, formats: [:json]
28+
render :show, formats: [:json], status: :created
3029
else
31-
render json: { error: result[:error] }, status: :internal_server_error
30+
render json: { error: result[:error] }, status: :unprocessable_entity
3231
end
3332
end
3433

3534
def update
36-
update_hash = project_params.merge(user_id: current_user&.id)
37-
result = Project::Update.call(project: @project, update_hash:)
35+
result = Project::Update.call(project: @project, update_hash: project_params)
3836

3937
if result.success?
4038
render :show, formats: [:json]
4139
else
42-
render json: { error: result[:error] }, status: :bad_request
40+
render json: { error: result[:error] }, status: :unprocessable_entity
4341
end
4442
end
4543

@@ -50,6 +48,13 @@ def destroy
5048

5149
private
5250

51+
def verify_lesson_belongs_to_school
52+
return if base_params[:lesson_id].blank?
53+
return if school&.lessons&.pluck(:id)&.include?(base_params[:lesson_id])
54+
55+
raise ParameterError, 'lesson_id does not correspond to school_id'
56+
end
57+
5358
def load_project
5459
project_loader = ProjectLoader.new(params[:id], [params[:locale]])
5560
@project = project_loader.load
@@ -60,18 +65,38 @@ def load_projects
6065
end
6166

6267
def project_params
68+
if school_owner?
69+
# A school owner must specify who the project user is.
70+
base_params
71+
else
72+
# A school teacher may only create projects they own.
73+
base_params.merge(user_id: current_user&.id)
74+
end
75+
end
76+
77+
def base_params
6378
params.fetch(:project, {}).permit(
79+
:school_id,
80+
:lesson_id,
81+
:user_id,
6482
:identifier,
6583
:name,
6684
:project_type,
6785
:locale,
6886
{
69-
image_list: [],
7087
components: %i[id name extension content index default]
7188
}
7289
)
7390
end
7491

92+
def school_owner?
93+
school && current_user.school_owner?(organisation_id: school.id)
94+
end
95+
96+
def school
97+
@school ||= @project&.school || School.find_by(id: base_params[:school_id])
98+
end
99+
75100
def pagination_link_header
76101
pagination_links = []
77102
pagination_links << page_links(first_page, 'first')

app/controllers/api/school_classes_controller.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def destroy
5353

5454
def school_class_params
5555
if school_owner?
56-
# The school owner must specify who the class teacher is.
56+
# A school owner must specify who the class teacher is.
5757
params.require(:school_class).permit(:teacher_id, :name)
5858
else
5959
# A school teacher may only create classes they own.

app/controllers/api_controller.rb

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,36 @@
11
# frozen_string_literal: true
22

33
class ApiController < ActionController::API
4+
class ::ParameterError < StandardError; end
5+
46
include Identifiable
57

68
unless Rails.application.config.consider_all_requests_local
79
rescue_from ActionController::ParameterMissing, with: -> { bad_request }
810
rescue_from ActiveRecord::RecordNotFound, with: -> { not_found }
911
rescue_from CanCan::AccessDenied, with: -> { denied }
12+
rescue_from ParameterError, with: -> { unprocessable }
1013
end
1114

1215
private
1316

17+
def bad_request
18+
head :bad_request # 400 status
19+
end
20+
1421
def authorize_user
15-
head :unauthorized unless current_user
22+
head :unauthorized unless current_user # 401 status
1623
end
1724

18-
def bad_request
19-
head :bad_request
25+
def denied
26+
head :forbidden # 403 status
2027
end
2128

2229
def not_found
23-
head :not_found
30+
head :not_found # 404 status
2431
end
2532

26-
def denied
27-
head :forbidden
33+
def unprocessable
34+
head :unprocessable_entity # 422 status
2835
end
2936
end

app/models/ability.rb

+79-30
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,92 @@
33
class Ability
44
include CanCan::Ability
55

6-
# rubocop:disable Metrics/AbcSize, Layout/LineLength
6+
# rubocop:disable Metrics/AbcSize
77
def initialize(user)
8-
can :show, Project, user_id: nil
9-
can :show, Component, project: { user_id: nil }
8+
# Anyone can view projects not owner by a user or a school.
9+
can :show, Project, user_id: nil, school_id: nil
10+
can :show, Component, project: { user_id: nil, school_id: nil }
11+
12+
# Anyone can read publicly shared lessons.
13+
can :read, Lesson, visibility: 'public'
1014

1115
return unless user
1216

13-
can %i[read create update destroy], Project, user_id: user.id
14-
can %i[read create update destroy], Component, project: { user_id: user.id }
17+
# Any authenticated user can create projects not owned by a school.
18+
can :create, Project, user_id: user.id, school_id: nil
19+
can :create, Component, project: { user_id: user.id, school_id: nil }
20+
21+
# Any authenticated user can manage their own projects.
22+
can %i[read update destroy], Project, user_id: user.id
23+
can %i[read update destroy], Component, project: { user_id: user.id }
24+
25+
# Any authenticated user can create a school. They agree to become the school-owner.
26+
can :create, School
27+
28+
# Any authenticated user can create a lesson, to support a RPF library of public lessons.
29+
can :create, Lesson, school_id: nil, school_class_id: nil
30+
31+
# Any authenticated user can create a copy of a publicly shared lesson.
32+
can :create_copy, Lesson, visibility: 'public'
1533

16-
can %i[create], School # The user agrees to become a school-owner by creating a school.
34+
# Any authenticated user can manage their own lessons.
35+
can %i[read create_copy update destroy], Lesson, user_id: user.id
1736

1837
user.organisation_ids.each do |organisation_id|
19-
if user.school_owner?(organisation_id:)
20-
can(%i[read update destroy], School, id: organisation_id)
21-
can(%i[read create update destroy], SchoolClass, school: { id: organisation_id })
22-
can(%i[read create destroy], ClassMember, school_class: { school: { id: organisation_id } })
23-
can(%i[read create destroy], :school_owner)
24-
can(%i[read create destroy], :school_teacher)
25-
can(%i[read create create_batch update destroy], :school_student)
26-
end
27-
28-
if user.school_teacher?(organisation_id:)
29-
can(%i[read], School, id: organisation_id)
30-
can(%i[create], SchoolClass, school: { id: organisation_id })
31-
can(%i[read update destroy], SchoolClass, school: { id: organisation_id }, teacher_id: user.id)
32-
can(%i[read create destroy], ClassMember, school_class: { school: { id: organisation_id }, teacher_id: user.id })
33-
can(%i[read], :school_owner)
34-
can(%i[read], :school_teacher)
35-
can(%i[read create create_batch update], :school_student)
36-
end
37-
38-
if user.school_student?(organisation_id:)
39-
can(%i[read], School, id: organisation_id)
40-
can(%i[read], SchoolClass, school: { id: organisation_id }, members: { student_id: user.id })
41-
end
38+
define_school_owner_abilities(organisation_id:) if user.school_owner?(organisation_id:)
39+
define_school_teacher_abilities(user:, organisation_id:) if user.school_teacher?(organisation_id:)
40+
define_school_student_abilities(user:, organisation_id:) if user.school_student?(organisation_id:)
4241
end
4342
end
44-
# rubocop:enable Metrics/AbcSize, Layout/LineLength
43+
# rubocop:enable Metrics/AbcSize
44+
45+
private
46+
47+
def define_school_owner_abilities(organisation_id:)
48+
can(%i[read update destroy], School, id: organisation_id)
49+
can(%i[read create update destroy], SchoolClass, school: { id: organisation_id })
50+
can(%i[read create destroy], ClassMember, school_class: { school: { id: organisation_id } })
51+
can(%i[read create destroy], :school_owner)
52+
can(%i[read create destroy], :school_teacher)
53+
can(%i[read create create_batch update destroy], :school_student)
54+
can(%i[create create_copy], Lesson, school_id: organisation_id)
55+
can(%i[read update destroy], Lesson, school_id: organisation_id, visibility: %w[teachers students public])
56+
can(%i[create], Project, school_id: organisation_id)
57+
end
58+
59+
def define_school_teacher_abilities(user:, organisation_id:)
60+
can(%i[read], School, id: organisation_id)
61+
can(%i[create], SchoolClass, school: { id: organisation_id })
62+
can(%i[read update destroy], SchoolClass, school: { id: organisation_id }, teacher_id: user.id)
63+
can(%i[read create destroy], ClassMember, school_class: { school: { id: organisation_id }, teacher_id: user.id })
64+
can(%i[read], :school_owner)
65+
can(%i[read], :school_teacher)
66+
can(%i[read create create_batch update], :school_student)
67+
can(%i[create destroy], Lesson) { |lesson| school_teacher_can_manage_lesson?(user:, organisation_id:, lesson:) }
68+
can(%i[read create_copy], Lesson, school_id: organisation_id, visibility: %w[teachers students])
69+
can(%i[create], Project) { |project| school_teacher_can_manage_project?(user:, organisation_id:, project:) }
70+
end
71+
72+
# rubocop:disable Layout/LineLength
73+
def define_school_student_abilities(user:, organisation_id:)
74+
can(%i[read], School, id: organisation_id)
75+
can(%i[read], SchoolClass, school: { id: organisation_id }, members: { student_id: user.id })
76+
can(%i[read], Lesson, school_id: organisation_id, visibility: 'students', school_class: { members: { student_id: user.id } })
77+
can(%i[create], Project, school_id: organisation_id, user_id: user.id, lesson_id: nil)
78+
end
79+
# rubocop:enable Layout/LineLength
80+
81+
def school_teacher_can_manage_lesson?(user:, organisation_id:, lesson:)
82+
is_my_lesson = lesson.school_id == organisation_id && lesson.user_id == user.id
83+
is_my_class = lesson.school_class && lesson.school_class.teacher_id == user.id
84+
85+
is_my_lesson && (is_my_class || !lesson.school_class)
86+
end
87+
88+
def school_teacher_can_manage_project?(user:, organisation_id:, project:)
89+
is_my_project = project.school_id == organisation_id && project.user_id == user.id
90+
is_my_lesson = project.lesson && project.lesson.user_id == user.id
91+
92+
is_my_project && (is_my_lesson || !project.lesson)
93+
end
4594
end

app/models/class_member.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ def self.students
1616
end
1717

1818
def self.with_students
19-
users = students.index_by(&:id)
20-
all.map { |instance| [instance, users[instance.student_id]] }
19+
by_id = students.index_by(&:id)
20+
all.map { |instance| [instance, by_id[instance.student_id]] }
2121
end
2222

2323
def with_student

0 commit comments

Comments
 (0)