Skip to content

Instantly share code, notes, and snippets.

@bensheldon
Last active August 27, 2025 00:46
Show Gist options
  • Save bensheldon/bbab8298f1b8b940efe8cb4c0c18c618 to your computer and use it in GitHub Desktop.
Save bensheldon/bbab8298f1b8b940efe8cb4c0c18c618 to your computer and use it in GitHub Desktop.
base_
class BaseController < ApplicationController
include Flowable
self.form_flow = [
"AppliesController",
# Initial "can we help you" questions
"Apply::LocationsController",
"Apply::RecentlyAppliedsController",
"Apply::LanguagesController",
# Applicant and Household
"Apply::ApplicantsController",
"Apply::ExperiencesController",
"Apply::HouseholdsController",
"Apply::DisabilitiesController",
"Apply::StudentsController",
"Apply::CitizenshipsController",
"Apply::GrossIncomesController",
# Result as part of Gross Income question
"Apply::PhonesController",
# Income and Expense questions
"Apply::JobsController",
"Apply::OtherIncomesController",
"Apply::HousingExpensesController",
"Apply::UtilitiesController",
"Apply::MedicalExpensesController",
"Apply::CareExpensesController",
"Apply::ExpeditedServicesController",
# Identity Questions
"Apply::MembersController",
"Apply::AddressesController",
# Document Questions
"Apply::CoveringExpensesController",
"Apply::ReferralsController",
"Apply::DocumentsController",
# Submission Questions
"Apply::DisabilitySupportsController",
"Apply::LegalsController",
"Apply::SignaturesController",
"Apply::DonesController",
].freeze
self.form_flow_steps = [
"AppliesController", # Step 1
"Apply::PhonesController", # Step 2
"Apply::DocumentsController", # Step 3
"Apply::SignaturesController", # Step 4 (final step)
].freeze
end
class ExampleController < BaseController
flow_action { :edit }
skip_if { current_snap_app&.calculator&.expedited_service_asset_limit.nil? }
done_if { current_snap_app&.expedited_low_cash&.not_nil? }
def edit
@snap_app = current_snap_app
end
def update
@snap_app = current_snap_app
@snap_app.assign_attributes(form_params)
if @snap_app.save(context: :expedited_low_cash)
if @snap_app.expedited_reason.present?
redirect_to({ action: :reason }, status: :see_other)
else
redirect_to flow_next_path, status: :see_other
end
else
render :edit, status: :unprocessable_content
end
end
def reason
@snap_app = current_snap_app
# renders a static page that has a link with flow_next_path to move forward
end
private
def form_params
params.expect(
snap_app: [:expedited_low_cash]
)
end
end
# frozen_string_literal: true
module Flowable
extend ActiveSupport::Concern
ALWAYS_DONE = Module.new
InvalidDoneNavigationError = Class.new(StandardError)
mattr_accessor :verify_done_on_next_path, default: false
mattr_accessor :verify_not_done_on_next_controller, default: false
included do
cattr_accessor :form_flow, default: [] # list of controllers as Strings in the flow
cattr_accessor :form_flow_steps, default: [] # specific controllers that are landmarks for displaying progress
helper_method :flow_path,
:flow_next_path,
:flow_progress_step,
:flow_progress_step_percent,
:flow_progress_percent,
:flow_progress_percent?
end
class_methods do
def flow_action(value = nil, instance: nil, &block)
if value || block
@_flow_action = value.presence || block
else
flow_action_value(instance)
end
end
def flow_action_value(instance)
action = @_flow_action || :edit
action.respond_to?(:call) ? instance.instance_exec(&action) : action
end
def done_if(callable = nil, &block)
@_done_if = callable || block
end
def skip_if(callable = nil, &block)
@_skip_if = callable || block
end
def done_in_flow?(instance)
@_done_if ? instance.instance_exec(&@_done_if) : true
end
def skip_in_flow?(instance)
@_skip_if ? instance.instance_exec(&@_skip_if) : false
end
# When a controller is not in the flow, have it pretend to be another controller
# for the purposes of calculating progress
def flow_alias=(klass)
raise "alias #{klass} not in flow of #{form_flow}" unless klass.in?(form_flow)
@_flow_alias = klass
end
def flow_progress_step_indexes
form_flow_steps
.map { |step| form_flow.index { |value| value == step } }
.tap { |result| raise "Controller not in form flow steps" if result.any?(&:nil?) }
end
end
def flow_next_path
raise NotDoneNavigationError, "#{self.class} called `flow_next_path` but was not done" if Flowable.verify_done_on_next_path && !self.class.done_in_flow?(self)
next_controller_class = nil
next_index = flow_current_index!
loop do
next_index += 1
next_controller_const = Array(self.class.form_flow[next_index])
raise "At end of flow" if next_controller_const.empty?
next_controller_class = next_controller_const.first.constantize
next if next_controller_class.skip_in_flow?(self)
break
end
if Flowable.verify_not_done_on_next_controller
done = next_controller_class.done_in_flow?(self)
raise InvalidDoneNavigationError, "#{next_controller_class.name} is flow_next_path but already done" if done && done != ALWAYS_DONE
end
next_controller_action = next_controller_class.flow_action_value(self)
url_for(controller: File.join("/", next_controller_class.controller_path), action: next_controller_action, only_path: true)
end
# The current step based on `form_flow_steps`
def flow_progress_step(klass = self.class)
klass = klass.constantize if klass.is_a?(String)
current_controller_index = self.class.form_flow.index { |value| value == klass.name }
klass.flow_progress_step_indexes.index { |step_index| current_controller_index < step_index } || klass.flow_progress_step_indexes.size
end
# The progress between this step and the next step
def flow_progress_step_percent(klass = self.class, step:)
klass = klass.constantize if klass.is_a?(String)
current_step = flow_progress_step(klass)
if step < current_step
100.0
elsif step > current_step
0.0
else
index = index_in_flow(klass)
index_in_step = index - self.class.flow_progress_step_indexes[current_step - 1]
total_in_step = self.class.flow_progress_step_indexes[current_step] - self.class.flow_progress_step_indexes[current_step - 1]
(index_in_step.to_f / total_in_step) * 100
end
end
def flow_progress_percent?
index_in_flow.present?
end
def flow_progress_percent
(flow_current_index! + 1) / self.class.form_flow.size.to_f * 100
end
def index_in_flow(klass = self.class)
klass = klass.name if klass.is_a?(Class)
self.class.form_flow.index { |value| Array(value).first == klass }
end
def flow_current_index
index_in_flow(self.class)
end
def flow_path(klass, action = nil)
index_in_flow(klass) || raise("Controller not in flow")
klass = klass.constantize unless klass.is_a?(Class)
url_for(controller: File.join("/", klass.controller_path), action: action || klass.flow_action_value(self), only_path: true)
end
def flow_current_index!
index_in_flow || raise("Controller not in flow")
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment