Skip to content

Commit

Permalink
Merge pull request #2742 from DFE-Digital/CAPT-1633_CAPT-1634-pt1
Browse files Browse the repository at this point in the history
CAPT-1633 Form objects for reminders | CAPT-1634 Encapsulate pre/post form rendering/submission logic per-journey (Part 1)
  • Loading branch information
your authored May 20, 2024
2 parents c2bc946 + 5ba8e23 commit fa16937
Show file tree
Hide file tree
Showing 25 changed files with 739 additions and 253 deletions.
167 changes: 167 additions & 0 deletions app/controllers/concerns/form_submittable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
module FormSubmittable
extend ActiveSupport::Concern

#
# This concern provides a way of handling form submissions for a generic slug sequence.
#
# The `new`, `show`, `create`, and `update` actions can be overridden only if necessary,
# but it's discouraged as you could easily break the rendering cycle and callback chain.
# If you need to override actions, you are probably not dealing with a form-based page sequence.
#
# The average use case will most likely only require to override slug-specific callbacks
# that are used to execute custom logic before, after, or around certain actions.
# In some cases, you may not need to define any callbacks at all.
#
# Default behaviour summary for each action:
#
# controller#new: `redirect_to_first_slug`
# controller#show: `before_show` -> `render_template_for_current_slug`
# controller#update: `before_update` -> `handle_form_submission` ->
# -> `form#save` succeded? ->
# -> `execute_callback_if_exists(:after_form_save_success)` OR `redirect_to_next_slug`
# -> `form#save failed?` ->
# -> `execute_callback_if_exists(:after_form_save_failure)` OR `render_template_for_current_slug
# controller#create: same as controller#update
#
# When including this concern make sure that these methods are accessible in the controller:
#
# - `slugs`, `current_slug`, `next_slug` (normally from `PageSequence`, or overridden)
# - `journey` (from `PartOfJourneyConcern`)
# - `current_data_object`, required to load the form object
# - any slug-specific callbacks (via a separate mixin)
#
# Important: `current_slug` is used to generate callbacks and assumed safe to use, i.e. derived
# from constrained, validated or sanitised user input.
#
# Important: If there are other callbacks in the controller, you should be including this concern
# **after** all the callbacks are defined. Placing this at the top is most likely a bad idea.
# In most cases, form submission callbacks are meant to kick in last.
#
# See the implementation of `_set_slug_specific_callbacks` below for more details.

included do
before_action :_set_slug_specific_callbacks, only: [:show, :update, :create]
before_action :before_show, only: :show
before_action :before_update, only: [:update, :create]
before_action :load_form_if_exists, only: [:show, :update, :create]
around_action :handle_form_submission, only: [:update, :create]

def current_data_object
# This is the instance of the main resource handled by the form object,
# and should always be defined in the controller.
nil
end

def new
redirect_to_first_slug
end

def show
render_template_for_current_slug
end

def create
# Note: if implemented, this action will be yielded at the end of `handle_form_submission`
end

def update
# Note: if implemented, this action will be yielded at the end of `handle_form_submission`
end

private

#
# Slug-specific callbacks are generated and executed around `show`, `update`, `create` actions,
# For example, for the "personal-details" slug, the following callbacks are available:
#
# `personal_details_before_show`
# `personal_details_before_update`
# `personal_details_after_form_save_success` (*)
# `personal_details_after_form_save_failure` (*)
#
# Ensure that the callbacks are implemented only where really needed.
# Consider organizing the callback methods in one mixin per journey and controller.
#
# (*) If you need to define these callbacks, the default rendering behaviour will not be
# followed, so you'll have to explicitly define what to do next (render/redirect_to).

def _set_slug_specific_callbacks
%i[before_show before_update after_form_save_success after_form_save_failure].each do |callback_name|
self.class.send(:define_method, callback_name) do
execute_callback_if_exists(callback_name)
end
end
end

def redirect_to_slug(slug)
raise NoMethodError, "End of sequence: you must define #{current_slug.underscore}_after_form_save_success" unless next_slug
raise NoMethodError, "Missing path helper for resource: \"#{path_helper_resource}\"; try overriding it with #path_helper_resource" unless respond_to?(:"#{path_helper_resource}_path")

redirect_to send(:"#{path_helper_resource}_path", current_journey_routing_name, slug)
end

def redirect_to_next_slug
redirect_to_slug(next_slug)
end

def redirect_to_first_slug
redirect_to_slug(first_slug)
end

def path_helper_resource
controller_name.singularize
end

def render_template_for_current_slug
render current_template
end

def current_template
current_slug.underscore
end

def slugs
journey.slug_sequence::SLUGS
end

def first_slug
slugs.first.to_sym
end

def execute_callback_if_exists(callback_name)
callback_name = :"#{current_slug.underscore}_#{callback_name}"
if respond_to?(callback_name)
log_event(callback_name) { send(callback_name) }
return true
end
false
end

def handle_form_submission
log_event(__method__)

if @form.present?
if @form.save
return if execute_callback_if_exists(:after_form_save_success)
redirect_to_next_slug
else
return if execute_callback_if_exists(:after_form_save_failure)
render_template_for_current_slug
end
else
redirect_to_next_slug
end

yield
end

def log_event(callback_name)
logger.info "Executing callback ##{callback_name}"
yield if block_given?
end

def load_form_if_exists
@form ||= journey.form(claim: current_data_object, journey_session:, params:)
end
end
end
Original file line number Diff line number Diff line change
@@ -1,59 +1,21 @@
module Journeys
module AdditionalPaymentsForTeaching
class RemindersController < BasePublicController
helper_method :current_reminder
after_action :reminder_set_email, :clear_sessions, only: [:show]

def new
# Skip the OTP process if the current_claim already has email_verified
# - transfer the email_verified state to the reminder (done in #current_reminder)
# - jump straight to reminder set
if current_reminder.email_verified? && current_reminder.save
redirect_to reminder_path(current_journey_routing_name, "set")
return
end
include PartOfClaimJourney

render "reminders/#{first_template_in_sequence}"
end

def create
current_reminder.attributes = reminder_params

begin
one_time_password
rescue Notifications::Client::BadRequestError => e
if notify_email_error?(e.message)
render "reminders/#{first_template_in_sequence}"
return
else
raise
end
end
after_action :clear_sessions, only: :show
helper_method :current_reminder

if current_reminder.save(context: current_slug.to_sym)
session[:reminder_id] = current_reminder.to_param
redirect_to reminder_path(current_journey_routing_name, next_slug)
else
render "reminders/#{first_template_in_sequence}"
end
end
include FormSubmittable
include RemindersFormCallbacks

def show
render "reminders/#{current_template}"
end
private

def update
current_reminder.attributes = reminder_params
one_time_password
if current_reminder.save(context: current_slug.to_sym)
redirect_to reminder_path(current_journey_routing_name, next_slug)
else
show
end
# Wrapping `current_reminder` with an abstract method that is fed to the form object.
def current_data_object
current_reminder
end

private

def claim_from_session
return unless session.key?(:claim_id) || session.key?(:submitted_claim_id)

Expand All @@ -69,18 +31,6 @@ def slugs
journey.slug_sequence::REMINDER_SLUGS
end

def first_template_in_sequence
slugs.first.underscore
end

def current_template
current_slug.underscore
end

def next_template
next_slug.underscore
end

def next_slug
slugs[current_slug_index + 1]
end
Expand All @@ -93,6 +43,10 @@ def current_slug_index
slugs.index(params[:slug]) || 0
end

def current_template
"reminders/#{current_slug.underscore}"
end

def current_reminder
@current_reminder ||=
reminder_from_session ||
Expand Down Expand Up @@ -127,51 +81,12 @@ def next_academic_year
journey_configuration.current_academic_year + 1
end

def reminder_params
params.require(:reminder).permit(:full_name, :email_address, :one_time_password)
end

def one_time_password
case current_slug
when "personal-details"
if current_reminder.valid?(:"personal-details")
ReminderMailer.email_verification(current_reminder, otp.code).deliver_now
session[:sent_one_time_password_at] = Time.now
end
when "email-verification"
current_reminder.update(sent_one_time_password_at: session[:sent_one_time_password_at])
end
end

def otp
@otp ||= OneTimePassword::Generator.new
end

def reminder_set_email
return unless current_slug == "set" && current_reminder.email_verified?

ReminderMailer.reminder_set(current_reminder).deliver_now
end

def clear_sessions
return unless current_template == "set"
return unless current_slug == "set"

session.delete(:claim_id)
session.delete(:reminder_id)
end

def notify_email_error?(msg)
case msg
when "ValidationError: email_address is a required property"
current_reminder.add_invalid_email_error("Enter an email address in the correct format, like name@example.com")
true
when "BadRequestError: Can’t send to this recipient using a team-only API key"
current_reminder.add_invalid_email_error("Only authorised email addresses can be used when using a team-only API key")
true
else
false
end
end
end
end
end
Loading

0 comments on commit fa16937

Please sign in to comment.