Skip to content

Commit 6a222df

Browse files
authored
School project statuses (#607)
## Status closes RaspberryPiFoundation/digital-editor-issues#985 closes RaspberryPiFoundation/digital-editor-issues#986 closes RaspberryPiFoundation/digital-editor-issues#988 closes RaspberryPiFoundation/digital-editor-issues#995 ## Points for consideration: - Use of state machine ## What's changed? - Added state machine to handle and log school project status changes
1 parent d2ebba4 commit 6a222df

26 files changed

+1041
-10
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ gem 'rack-cors'
3737
gem 'rails', '~> 7.1'
3838
gem 'scout_apm'
3939
gem 'sentry-rails'
40+
gem 'statesman'
4041

4142
group :development, :test do
4243
gem 'awesome_print'

Gemfile.lock

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ GEM
522522
actionpack (>= 6.1)
523523
activesupport (>= 6.1)
524524
sprockets (>= 3.0.0)
525+
statesman (13.1.0)
525526
stringio (3.1.7)
526527
thor (1.4.0)
527528
tilt (2.6.1)
@@ -617,6 +618,7 @@ DEPENDENCIES
617618
sentry-rails
618619
shoulda-matchers (~> 5.0)
619620
simplecov
621+
statesman
620622
webdrivers
621623
webmock
622624

app/controllers/api/school_projects_controller.rb

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,63 @@ class SchoolProjectsController < ApiController
55
before_action :authorize_user
66
load_and_authorize_resource :project
77

8+
def show_status
9+
authorize! :show_status, school_project
10+
render :show_status, formats: [:json], status: :ok
11+
end
12+
13+
def unsubmit
14+
authorize! :unsubmit, school_project
15+
result = SchoolProject::SetStatus.call(school_project:, status: :unsubmitted, user_id: current_user.id)
16+
if result.success?
17+
@school_project = result[:school_project]
18+
render :show_status, formats: [:json], status: :ok
19+
else
20+
render json: { error: result[:error] }, status: :unprocessable_entity
21+
end
22+
end
23+
24+
def submit
25+
authorize! :submit, school_project
26+
result = SchoolProject::SetStatus.call(school_project:, status: :submitted, user_id: current_user.id)
27+
if result.success?
28+
@school_project = result[:school_project]
29+
render :show_status, formats: [:json], status: :ok
30+
else
31+
render json: { error: result[:error] }, status: :unprocessable_entity
32+
end
33+
end
34+
35+
def return
36+
authorize! :return, school_project
37+
result = SchoolProject::SetStatus.call(school_project:, status: :returned, user_id: current_user.id)
38+
if result.success?
39+
@school_project = result[:school_project]
40+
render :show_status, formats: [:json], status: :ok
41+
else
42+
render json: { error: result[:error] }, status: :unprocessable_entity
43+
end
44+
end
45+
46+
def complete
47+
authorize! :complete, school_project
48+
result = SchoolProject::SetStatus.call(school_project:, status: :complete, user_id: current_user.id)
49+
if result.success?
50+
@school_project = result[:school_project]
51+
render :show_status, formats: [:json], status: :ok
52+
else
53+
render json: { error: result[:error] }, status: :unprocessable_entity
54+
end
55+
end
56+
857
def show_finished
9-
@school_project = Project.find_by!(identifier: params[:id]).school_project
10-
authorize! :show_finished, @school_project
58+
authorize! :show_finished, school_project
1159
render :finished, formats: [:json], status: :ok
1260
end
1361

1462
def set_finished
15-
project = Project.find_by!(identifier: params[:id])
16-
@school_project = project.school_project
17-
authorize! :set_finished, @school_project
18-
result = SchoolProject::SetFinished.call(school_project: @school_project, finished: params[:finished])
63+
authorize! :set_finished, school_project
64+
result = SchoolProject::SetFinished.call(school_project:, finished: params[:finished])
1965

2066
if result.success?
2167
@school_project = result[:school_project]
@@ -24,5 +70,19 @@ def set_finished
2470
render json: { error: result[:error] }, status: :unprocessable_entity
2571
end
2672
end
73+
74+
private
75+
76+
def project
77+
@project ||= Project.find_by!(identifier: params[:id])
78+
end
79+
80+
def school_project
81+
@school_project ||= project.school_project
82+
end
83+
84+
def school_project_params
85+
params.permit(:finished)
86+
end
2787
end
2888
end

app/models/ability.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ def define_school_teacher_abilities(user:, school:)
9696
)
9797
).pluck(:id)
9898
can(%i[read], Project, remixed_from_id: teacher_project_ids)
99+
can(%i[show_status unsubmit return complete], SchoolProject, project: { remixed_from_id: teacher_project_ids })
99100
can(%i[read create], Feedback, school_project: { project: { remixed_from_id: teacher_project_ids } })
100101
end
101102

@@ -113,7 +114,7 @@ def define_school_student_abilities(user:, school:)
113114
can(%i[read create update], Project, school_id: school.id, user_id: user.id, lesson_id: nil, remixed_from_id: visible_lesson_project_ids)
114115
can(%i[read show_context], Project, lesson: { school_id: school.id, visibility: 'students', school_class: { students: { student_id: user.id } } })
115116
can(%i[read], Feedback, school_project: { project: { school_id: school.id, user_id: user.id, lesson_id: nil, remixed_from_id: visible_lesson_project_ids } })
116-
can(%i[show_finished set_finished], SchoolProject, project: { user_id: user.id, lesson_id: nil }, school_id: school.id)
117+
can(%i[show_finished set_finished show_status unsubmit submit], SchoolProject, project: { user_id: user.id, lesson_id: nil }, school_id: school.id)
117118
end
118119

119120
def define_experience_cs_admin_abilities(user)

app/models/school_project.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,43 @@ class SchoolProject < ApplicationRecord
44
belongs_to :school
55
belongs_to :project
66
has_many :feedback, dependent: :destroy
7+
has_many :school_project_transitions, autosave: false, dependent: :nullify
8+
9+
include Statesman::Adapters::ActiveRecordQueries[
10+
transition_class: ::SchoolProjectTransition,
11+
initial_state: :unsubmitted
12+
]
13+
14+
def status
15+
state_machine.current_state
16+
end
17+
18+
def transition_status_to!(new_status, user_id)
19+
state_machine.transition_to!(new_status, metadata: { changed_by: user_id })
20+
end
21+
22+
# Add convenience methods for each state
23+
def unsubmitted?
24+
state_machine.in_state?(:unsubmitted)
25+
end
26+
27+
def submitted?
28+
state_machine.in_state?(:submitted)
29+
end
30+
31+
def complete?
32+
state_machine.in_state?(:complete)
33+
end
34+
35+
def returned?
36+
state_machine.in_state?(:returned)
37+
end
38+
39+
delegate :can_transition_to?, :history, to: :state_machine
40+
41+
private
42+
43+
def state_machine
44+
@state_machine ||= SchoolProjectStateMachine.new(self, transition_class: SchoolProjectTransition)
45+
end
746
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
class SchoolProjectTransition < ApplicationRecord
4+
include Statesman::Adapters::ActiveRecordTransition
5+
6+
belongs_to :school_project, inverse_of: :school_project_transitions
7+
8+
after_destroy :update_most_recent, if: :most_recent?
9+
10+
private
11+
12+
def update_most_recent
13+
last_transition = school_project.school_project_transitions.order(:sort_key).last
14+
return if last_transition.blank?
15+
16+
last_transition.update!(most_recent: true)
17+
end
18+
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
class SchoolProjectStateMachine
4+
include Statesman::Machine
5+
6+
# Define all possible states
7+
state :unsubmitted, initial: true
8+
state :submitted
9+
state :returned
10+
state :complete
11+
12+
# Define transition rules
13+
transition from: :unsubmitted, to: %i[submitted complete]
14+
transition from: :submitted, to: %i[unsubmitted returned complete]
15+
transition from: :returned, to: %i[submitted complete]
16+
transition from: :complete, to: [:unsubmitted]
17+
end
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.call(
4+
@school_project,
5+
:id,
6+
:school_id,
7+
:project_id,
8+
:status
9+
)
10+
11+
json.identifier(@school_project.project.identifier)

config/initializers/statesman.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
3+
Statesman.configure do
4+
storage_adapter(Statesman::Adapters::ActiveRecord)
5+
end

config/routes.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
get :finished, on: :member, to: 'school_projects#show_finished'
3939
get :context, on: :member, to: 'projects#show_context'
4040
put :finished, on: :member, to: 'school_projects#set_finished'
41+
get :status, on: :member, to: 'school_projects#show_status'
42+
post :unsubmit, on: :member, to: 'school_projects#unsubmit'
43+
post :submit, on: :member, to: 'school_projects#submit'
44+
post :return, on: :member, to: 'school_projects#return'
45+
post :complete, on: :member, to: 'school_projects#complete'
4146
resource :remix, only: %i[show create], controller: 'projects/remixes'
4247
resources :remixes, only: %i[index], controller: 'projects/remixes'
4348
resource :images, only: %i[show create], controller: 'projects/images'

0 commit comments

Comments
 (0)