Browse Source

Refactor the form parsing and navigation logic into OOP domain objects (#106)

* OOP form

* Add ADR

* PR commits
pull/107/head
Daniel Baark 3 years ago committed by GitHub
parent
commit
ba51e66a56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 47
      app/controllers/case_logs_controller.rb
  2. 6
      app/controllers/soft_validations_controller.rb
  3. 61
      app/helpers/check_answers_helper.rb
  4. 8
      app/helpers/conditional_questions_helper.rb
  5. 10
      app/helpers/question_attribute_helper.rb
  6. 38
      app/helpers/tasklist_helper.rb
  7. 4
      app/models/case_log.rb
  8. 160
      app/models/form.rb
  9. 30
      app/models/form/page.rb
  10. 80
      app/models/form/question.rb
  11. 10
      app/models/form/section.rb
  12. 65
      app/models/form/subsection.rb
  13. 2
      app/validations/household_validations.rb
  14. 13
      app/views/case_logs/_tasklist.html.erb
  15. 2
      app/views/case_logs/edit.html.erb
  16. 6
      app/views/form/_check_answers_table.html.erb
  17. 10
      app/views/form/_checkbox_question.html.erb
  18. 6
      app/views/form/_date_question.html.erb
  19. 10
      app/views/form/_numeric_question.html.erb
  20. 12
      app/views/form/_radio_question.html.erb
  21. 11
      app/views/form/_select_question.html.erb
  22. 6
      app/views/form/_text_question.html.erb
  23. 6
      app/views/form/_validation_override_question.html.erb
  24. 8
      app/views/form/check_answers.html.erb
  25. 16
      app/views/form/page.html.erb
  26. 10
      config/routes.rb
  27. 10
      docs/adr/adr-011-form-oop-refactor.md
  28. 39
      spec/controllers/case_logs_controller_spec.rb
  29. 2
      spec/features/case_log_spec.rb
  30. 30
      spec/fixtures/forms/test_form.json
  31. 185
      spec/helpers/check_answers_helper_spec.rb
  32. 12
      spec/helpers/conditional_questions_helper_spec.rb
  33. 22
      spec/helpers/question_attribute_helper_spec.rb
  34. 30
      spec/helpers/tasklist_helper_spec.rb
  35. 66
      spec/models/form/page_spec.rb
  36. 140
      spec/models/form/question_spec.rb
  37. 21
      spec/models/form/section_spec.rb
  38. 72
      spec/models/form/subsection_spec.rb
  39. 2
      spec/models/form_handler_spec.rb
  40. 64
      spec/models/form_spec.rb

47
app/controllers/case_logs_controller.rb

@ -57,14 +57,14 @@ class CaseLogsController < ApplicationController
def submit_form
form = FormHandler.instance.get_form("2021_2022")
@case_log = CaseLog.find(params[:id])
@case_log.page = params[:case_log][:page]
responses_for_page = responses_for_page(@case_log.page)
@case_log.page_id = params[:case_log][:page]
page = form.get_page(@case_log.page_id)
responses_for_page = responses_for_page(page)
if @case_log.update(responses_for_page) && @case_log.has_no_unresolved_soft_errors?
redirect_path = form.next_page_redirect_path(@case_log.page, @case_log)
redirect_path = form.next_page_redirect_path(page, @case_log)
redirect_to(send(redirect_path, @case_log))
else
page_info = form.all_pages[@case_log.page]
render "form/page", locals: { form: form, page_key: @case_log.page, page_info: page_info }, status: :unprocessable_entity
render "form/page", locals: { form: form, page: page }, status: :unprocessable_entity
end
end
@ -84,15 +84,15 @@ class CaseLogsController < ApplicationController
form = FormHandler.instance.get_form("2021_2022")
@case_log = CaseLog.find(params[:case_log_id])
current_url = request.env["PATH_INFO"]
subsection = current_url.split("/")[-2]
subsection = form.get_subsection(current_url.split("/")[-2])
render "form/check_answers", locals: { subsection: subsection, form: form }
end
form = FormHandler.instance.get_form("2021_2022")
form.all_pages.map do |page_key, page_info|
define_method(page_key) do |_errors = {}|
form.pages.map do |page|
define_method(page.id) do |_errors = {}|
@case_log = CaseLog.find(params[:case_log_id])
render "form/page", locals: { form: form, page_key: page_key, page_info: page_info }
render "form/page", locals: { form: form, page: page }
end
end
@ -101,29 +101,28 @@ private
API_ACTIONS = %w[create show update destroy].freeze
def responses_for_page(page)
form = FormHandler.instance.get_form("2021_2022")
form.expected_responses_for_page(page).each_with_object({}) do |(question_key, question_info), result|
question_params = params["case_log"][question_key]
if question_info["type"] == "date"
day = params["case_log"]["#{question_key}(3i)"]
month = params["case_log"]["#{question_key}(2i)"]
year = params["case_log"]["#{question_key}(1i)"]
page.expected_responses.each_with_object({}) do |question, result|
question_params = params["case_log"][question.id]
if question.type == "date"
day = params["case_log"]["#{question.id}(3i)"]
month = params["case_log"]["#{question.id}(2i)"]
year = params["case_log"]["#{question.id}(1i)"]
next unless [day, month, year].any?(&:present?)
result[question_key] = if day.to_i.between?(1, 31) && month.to_i.between?(1, 12) && year.to_i.between?(2000, 2200)
Date.new(year.to_i, month.to_i, day.to_i)
else
Date.new(0, 1, 1)
end
result[question.id] = if day.to_i.between?(1, 31) && month.to_i.between?(1, 12) && year.to_i.between?(2000, 2200)
Date.new(year.to_i, month.to_i, day.to_i)
else
Date.new(0, 1, 1)
end
end
next unless question_params
if %w[checkbox validation_override].include?(question_info["type"])
question_info["answer_options"].keys.reject { |x| x.match(/divider/) }.each do |option|
if %w[checkbox validation_override].include?(question.type)
question.answer_options.keys.reject { |x| x.match(/divider/) }.each do |option|
result[option] = question_params.include?(option) ? 1 : 0
end
else
result[question_key] = question_params
result[question.id] = question_params
end
result
end

6
app/controllers/soft_validations_controller.rb

@ -1,9 +1,9 @@
class SoftValidationsController < ApplicationController
def show
@case_log = CaseLog.find(params[:case_log_id])
page_key = request.env["PATH_INFO"].split("/")[-2]
page_id = request.env["PATH_INFO"].split("/")[-2]
form = FormHandler.instance.get_form("2021_2022")
page = form.all_pages[page_key]
page = form.get_page(page_id)
if page_requires_soft_validation_override?(page)
errors = @case_log.soft_errors.values.first
render json: { show: true, label: errors.message, hint: errors.hint_text }
@ -15,6 +15,6 @@ class SoftValidationsController < ApplicationController
private
def page_requires_soft_validation_override?(page)
@case_log.soft_errors.present? && @case_log.soft_errors.keys.first == page["soft_validations"]&.keys&.first
@case_log.soft_errors.present? && @case_log.soft_errors.keys.first == page.soft_validations&.first&.id
end
end

61
app/helpers/check_answers_helper.rb

@ -1,57 +1,20 @@
module CheckAnswersHelper
def total_answered_questions(subsection, case_log, form)
total_questions(subsection, case_log, form).keys.count do |question_key|
case_log[question_key].present?
end
end
def total_number_of_questions(subsection, case_log, form)
total_questions(subsection, case_log, form).count
end
def total_questions(subsection, case_log, form)
questions = form.questions_for_subsection(subsection)
form.filter_conditional_questions(questions, case_log)
end
def get_next_page_name(form, page_name, case_log)
page = form.all_pages[page_name]
if page.key?("conditional_route_to")
page["conditional_route_to"].each do |conditional_page_name, condition|
unless condition.any? { |field, value| case_log[field].blank? || !value.include?(case_log[field]) }
return conditional_page_name
end
end
def display_answered_questions_summary(subsection, case_log)
total = subsection.applicable_questions_count(case_log)
answered = subsection.answered_questions_count(case_log)
if total == answered
'<p class="govuk-body govuk-!-margin-bottom-7">You answered all the questions</p>'.html_safe
else
"<p class=\"govuk-body govuk-!-margin-bottom-7\">You answered #{answered} of #{total} questions</p>
#{create_next_missing_question_link(subsection, case_log)}".html_safe
end
form.next_page(page_name)
end
def create_update_answer_link(question_title, question_info, case_log, form)
page = form.page_for_question(question_title)
link_name = if question_info["type"] == "checkbox"
question_info["answer_options"].keys.any? { |key| case_log[key] == "Yes" } ? "Change" : "Answer"
else
case_log[question_title].blank? ? "Answer" : "Change"
end
link_to(link_name, "/case_logs/#{case_log.id}/#{page}", class: "govuk-link").html_safe
end
private
def create_next_missing_question_link(case_log_id, subsection, case_log, form)
pages_to_fill_in = []
form.pages_for_subsection(subsection).each do |page_title, page_info|
page_info["questions"].any? { |question| case_log[question].blank? }
pages_to_fill_in << page_title
end
url = "/case_logs/#{case_log_id}/#{pages_to_fill_in.first}"
def create_next_missing_question_link(subsection, case_log)
pages_to_fill_in = subsection.unanswered_questions(case_log).map(&:page)
url = "/case_logs/#{case_log.id}/#{pages_to_fill_in.first.id}"
link_to("Answer the missing questions", url, class: "govuk-link").html_safe
end
def display_answered_questions_summary(subsection, case_log, form)
if total_answered_questions(subsection, case_log, form) == total_number_of_questions(subsection, case_log, form)
'<p class="govuk-body govuk-!-margin-bottom-7">You answered all the questions</p>'.html_safe
else
"<p class=\"govuk-body govuk-!-margin-bottom-7\">You answered #{total_answered_questions(subsection, case_log, form)} of #{total_number_of_questions(subsection, case_log, form)} questions</p>
#{create_next_missing_question_link(case_log['id'], subsection, case_log, form)}".html_safe
end
end
end

8
app/helpers/conditional_questions_helper.rb

@ -1,11 +1,9 @@
module ConditionalQuestionsHelper
def conditional_questions_for_page(page)
page["questions"].values.map { |question|
question["conditional_for"]
}.compact.map(&:keys).flatten
page.questions.map(&:conditional_for).compact.map(&:keys).flatten
end
def display_question_key_div(page_info, question_key)
"style='display:none;'".html_safe if conditional_questions_for_page(page_info).include?(question_key)
def display_question_key_div(page, question)
"style='display:none;'".html_safe if conditional_questions_for_page(page).include?(question.id)
end
end

10
app/helpers/question_attribute_helper.rb

@ -10,23 +10,23 @@ module QuestionAttributeHelper
private
def numeric_question_html_attributes(question)
return {} if question["fields-to-add"].blank? || question["result-field"].blank?
return {} if question.fields_to_add.blank? || question.result_field.blank?
{
"data-controller": "numeric-question",
"data-action": "numeric-question#calculateFields",
"data-target": "case-log-#{question['result-field'].to_s.dasherize}-field",
"data-calculated": question["fields-to-add"].to_json,
"data-target": "case-log-#{question.result_field.to_s.dasherize}-field",
"data-calculated": question.fields_to_add.to_json,
}
end
def conditional_html_attributes(question)
return {} if question["conditional_for"].blank?
return {} if question.conditional_for.blank?
{
"data-controller": "conditional-question",
"data-action": "conditional-question#displayConditional",
"data-info": question["conditional_for"].to_json,
"data-info": question.conditional_for.to_json,
}
end
end

38
app/helpers/tasklist_helper.rb

@ -14,40 +14,30 @@ module TasklistHelper
}.freeze
def get_next_incomplete_section(form, case_log)
subsections = form.all_subsections.keys
subsections.find { |subsection| is_incomplete?(subsection, case_log, form) }
form.subsections.find { |subsection| subsection.is_incomplete?(case_log) }
end
def get_subsections_count(form, case_log, status = :all)
subsections = form.all_subsections.keys
return subsections.count if status == :all
return form.subsections.count if status == :all
subsections.count { |subsection| form.subsection_status(subsection, case_log) == status }
form.subsections.count { |subsection| subsection.status(case_log) == status }
end
def get_first_page_or_check_answers(subsection, case_log, form)
path = if is_started?(subsection, case_log, form)
"case_log_#{subsection}_check_answers_path"
def first_page_or_check_answers(subsection, case_log)
path = if subsection.is_started?(case_log)
"case_log_#{subsection.id}_check_answers_path"
else
"case_log_#{form.first_page_for_subsection(subsection)}_path"
"case_log_#{subsection.pages.first.id}_path"
end
send(path, case_log)
end
def subsection_link(subsection_key, subsection_value, status, form, case_log)
next_page_path = status != :cannot_start_yet ? get_first_page_or_check_answers(subsection_key, case_log, form) : "#"
link_to(subsection_value["label"], next_page_path, class: "task-name govuk-link")
end
private
def is_incomplete?(subsection, case_log, form)
status = form.subsection_status(subsection, case_log)
%i[not_started in_progress].include?(status)
end
def is_started?(subsection, case_log, form)
status = form.subsection_status(subsection, case_log)
%i[in_progress completed].include?(status)
def subsection_link(subsection, case_log)
next_page_path = if subsection.status(case_log) != :cannot_start_yet
first_page_or_check_answers(subsection, case_log)
else
"#"
end
link_to(subsection.label, next_page_path, class: "task-name govuk-link")
end
end

4
app/models/case_log.rb

@ -11,7 +11,7 @@ class CaseLogValidator < ActiveModel::Validator
# If we've come from the form UI we only want to validate the specific fields
# that have just been submitted. If we're submitting a log via API or Bulk Upload
# we want to validate all data fields.
page_to_validate = record.page
page_to_validate = record.page_id
if page_to_validate
public_send("validate_#{page_to_validate}", record) if respond_to?("validate_#{page_to_validate}")
else
@ -44,7 +44,7 @@ class CaseLog < ApplicationRecord
validates_with CaseLogValidator
before_save :update_status!
attr_accessor :page
attr_accessor :page_id
enum status: { "not_started" => 0, "in_progress" => 1, "completed" => 2 }

160
app/models/form.rb

@ -1,75 +1,34 @@
class Form
attr_reader :form_definition
attr_reader :form_definition, :sections, :subsections, :pages, :questions
def initialize(form_path)
raise "No form definition file exists for given year".freeze unless File.exist?(form_path)
@form_definition = JSON.parse(File.open(form_path).read)
@sections = form_definition["sections"].map { |id, s| Form::Section.new(id, s, self) }
@subsections = sections.flat_map(&:subsections)
@pages = subsections.flat_map(&:pages)
@questions = pages.flat_map(&:questions)
end
# Returns a hash with sections as keys
def all_sections
@all_sections ||= @form_definition["sections"]
def get_subsection(id)
subsections.find { |s| s.id == id }
end
# Returns a hash with subsections as keys
def all_subsections
@all_subsections ||= all_sections.map { |_section_key, section_value|
section_value["subsections"]
}.reduce(:merge)
end
# Returns a hash with pages as keys
def all_pages
@all_pages ||= all_subsections.map { |_subsection_key, subsection_value|
subsection_value["pages"]
}.reduce(:merge)
end
# Returns a hash with the pages of a subsection as keys
def pages_for_subsection(subsection)
all_subsections[subsection]["pages"]
end
# Returns a hash with the questions as keys
def questions_for_page(page)
all_pages[page]["questions"]
end
# Returns a hash with the questions as keys
def questions_for_subsection(subsection)
pages_for_subsection(subsection).map { |title, _value| questions_for_page(title) }.reduce(:merge)
end
# Returns a hash with soft validation questions as keys
def soft_validations_for_page(page)
all_pages[page]["soft_validations"]
end
def expected_responses_for_page(page)
questions_for_page(page).merge(soft_validations_for_page(page) || {})
end
def first_page_for_subsection(subsection)
pages_for_subsection(subsection).keys.first
def get_page(id)
pages.find { |p| p.id == id }
end
def subsection_for_page(page)
all_subsections.find { |_subsection_key, subsection_value|
subsection_value["pages"].key?(page)
}.first
end
def page_for_question(question)
all_pages.find { |_page_key, page_value| page_value["questions"].key?(question) }.first
subsections.find { |s| s.pages.find { |p| p.id == page.id } }
end
def next_page(page, case_log)
subsection = subsection_for_page(page)
page_idx = pages_for_subsection(subsection).keys.index(page)
nxt_page = pages_for_subsection(subsection).keys[page_idx + 1]
page_ids = subsection_for_page(page).pages.map(&:id)
page_index = page_ids.index(page.id)
nxt_page = get_page(page_ids[page_index + 1])
return :check_answers if nxt_page.nil?
return nxt_page if page_routed_to?(nxt_page, case_log)
return nxt_page.id if nxt_page.routed_to?(case_log)
next_page(nxt_page, case_log)
end
@ -77,95 +36,16 @@ class Form
def next_page_redirect_path(page, case_log)
nxt_page = next_page(page, case_log)
if nxt_page == :check_answers
subsection = subsection_for_page(page)
"case_log_#{subsection}_check_answers_path"
"case_log_#{subsection_for_page(page).id}_check_answers_path"
else
"case_log_#{nxt_page}_path"
end
end
def all_questions
@all_questions ||= all_pages.map { |_page_key, page_value|
page_value["questions"]
}.reduce(:merge)
end
def filter_conditional_questions(questions, case_log)
applicable_questions = questions
questions.each do |k, question|
unless page_routed_to?(page_for_question(k), case_log)
applicable_questions = applicable_questions.reject { |z| z == k }
end
question.fetch("conditional_for", []).each do |conditional_question_key, condition|
if condition_not_met(case_log, k, question, condition)
applicable_questions = applicable_questions.reject { |z| z == conditional_question_key }
end
end
end
applicable_questions
end
def page_routed_to?(page, case_log)
return true unless (conditions = page_dependencies(page))
conditions.all? do |question, value|
case_log[question].present? && case_log[question] == value
end
end
def page_dependencies(page)
all_pages[page]["depends_on"]
end
def subsection_dependencies_met?(subsection_name, case_log)
conditions = all_subsections[subsection_name]["depends_on"]
return true unless conditions
conditions.all? do |subsection, status|
subsection_status(subsection, case_log) == status.to_sym
end
end
def subsection_status(subsection_name, case_log)
unless subsection_dependencies_met?(subsection_name, case_log)
return :cannot_start_yet
end
questions = questions_for_subsection(subsection_name)
applicable_questions = filter_conditional_questions(questions, case_log).keys
return :not_started if applicable_questions.all? { |question| case_log[question].blank? }
return :completed if applicable_questions.all? { |question| case_log[question].present? }
:in_progress
end
def condition_not_met(case_log, question_key, question, condition)
case question["type"]
when "numeric"
operator = condition[/[<>=]+/].to_sym
operand = condition[/\d+/].to_i
case_log[question_key].blank? || !case_log[question_key].send(operator, operand)
when "text"
case_log[question_key].blank? || !condition.include?(case_log[question_key])
when "radio"
case_log[question_key].blank? || !condition.include?(case_log[question_key])
when "select"
case_log[question_key].blank? || !condition.include?(case_log[question_key])
else
raise "Not implemented yet"
end
end
def get_answer_label(case_log, question_title)
question = all_questions[question_title]
if question["type"] == "checkbox"
answer = []
question["answer_options"].each { |key, value| case_log[key] == "Yes" ? answer << value : nil }
return answer.join(", ")
end
case_log[question_title]
def conditional_question_conditions
conditions = questions.map { |q| Hash(q.id => q.conditional_for) if q.conditional_for.present? }.compact
conditions.map { |c|
c.map { |k, v| v.keys.map { |key| Hash(from: k, to: key, cond: v[key]) } }
}.flatten
end
end

30
app/models/form/page.rb

@ -0,0 +1,30 @@
class Form::Page
attr_accessor :id, :header, :description, :questions, :soft_validations,
:depends_on, :subsection
def initialize(id, hsh, subsection)
@id = id
@header = hsh["header"]
@description = hsh["description"]
@questions = hsh["questions"].map { |q_id, q| Form::Question.new(q_id, q, self) }
@depends_on = hsh["depends_on"]
@soft_validations = hsh["soft_validations"]&.map { |v_id, s| Form::Question.new(v_id, s, self) }
@subsection = subsection
end
def expected_responses
questions + (soft_validations || [])
end
def has_soft_validations?
soft_validations.present?
end
def routed_to?(case_log)
return true unless depends_on
depends_on.all? do |question, value|
case_log[question].present? && case_log[question] == value
end
end
end

80
app/models/form/question.rb

@ -0,0 +1,80 @@
class Form::Question
attr_accessor :id, :header, :hint_text, :description, :questions,
:type, :min, :max, :step, :fields_to_add, :result_field,
:conditional_for, :readonly, :answer_options, :page, :check_answer_label
def initialize(id, hsh, page)
@id = id
@check_answer_label = hsh["check_answer_label"]
@header = hsh["header"]
@hint_text = hsh["hint_text"]
@type = hsh["type"]
@min = hsh["min"]
@max = hsh["max"]
@step = hsh["step"]
@fields_to_add = hsh["fields-to-add"]
@result_field = hsh["result-field"]
@readonly = hsh["readonly"]
@answer_options = hsh["answer_options"]
@conditional_for = hsh["conditional_for"]
@page = page
end
delegate :subsection, to: :page
delegate :form, to: :subsection
def answer_label(case_log)
return checkbox_answer_label(case_log) if type == "checkbox"
case_log[id].to_s
end
def read_only?
!!readonly
end
def conditional_on
@conditional_on ||= form.conditional_question_conditions.select do |condition|
condition[:to] == id
end
end
def enabled?(case_log)
return true if conditional_on.blank?
conditional_on.map { |condition| evaluate_condition(condition, case_log) }.all?
end
def update_answer_link_name(case_log)
if type == "checkbox"
answer_options.keys.any? { |key| case_log[key] == "Yes" } ? "Change" : "Answer"
else
case_log[id].blank? ? "Answer" : "Change"
end
end
private
def checkbox_answer_label(case_log)
answer = []
answer_options.each { |key, value| case_log[key] == "Yes" ? answer << value : nil }
answer.join(", ")
end
def evaluate_condition(condition, case_log)
case page.questions.find { |q| q.id == condition[:from] }.type
when "numeric"
operator = condition[:cond][/[<>=]+/].to_sym
operand = condition[:cond][/\d+/].to_i
case_log[condition[:from]].present? && case_log[condition[:from]].send(operator, operand)
when "text"
case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]])
when "radio"
case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]])
when "select"
case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]])
else
raise "Not implemented yet"
end
end
end

10
app/models/form/section.rb

@ -0,0 +1,10 @@
class Form::Section
attr_accessor :id, :label, :subsections, :form
def initialize(id, hsh, form)
@id = id
@label = hsh["label"]
@form = form
@subsections = hsh["subsections"].map { |s_id, s| Form::Subsection.new(s_id, s, self) }
end
end

65
app/models/form/subsection.rb

@ -0,0 +1,65 @@
class Form::Subsection
attr_accessor :id, :label, :section, :pages, :depends_on, :form
def initialize(id, hsh, section)
@id = id
@label = hsh["label"]
@depends_on = hsh["depends_on"]
@pages = hsh["pages"].map { |s_id, p| Form::Page.new(s_id, p, self) }
@section = section
end
delegate :form, to: :section
def questions
@questions ||= pages.flat_map(&:questions)
end
def enabled?(case_log)
return true unless depends_on
depends_on.all? do |subsection_id, dependent_status|
form.get_subsection(subsection_id).status(case_log) == dependent_status.to_sym
end
end
def status(case_log)
unless enabled?(case_log)
return :cannot_start_yet
end
qs = applicable_questions(case_log)
return :not_started if qs.all? { |question| case_log[question.id].blank? }
return :completed if qs.all? { |question| case_log[question.id].present? }
:in_progress
end
def is_incomplete?(case_log)
%i[not_started in_progress].include?(status(case_log))
end
def is_started?(case_log)
%i[in_progress completed].include?(status(case_log))
end
def applicable_questions_count(case_log)
applicable_questions(case_log).count
end
def answered_questions_count(case_log)
answered_questions(case_log).count
end
def applicable_questions(case_log)
questions.select { |q| q.page.routed_to?(case_log) && q.enabled?(case_log) }
end
def answered_questions(case_log)
applicable_questions(case_log).select { |question| case_log[question.id].present? }
end
def unanswered_questions(case_log)
applicable_questions(case_log) - answered_questions(case_log)
end
end

2
app/validations/household_validations.rb

@ -66,7 +66,7 @@ module HouseholdValidations
return unless record.age1
if !record.age1.is_a?(Integer) || record.age1 < 16 || record.age1 > 120
record.errors.add "age1", "Tenant age must be an integer between 16 and 120"
record.errors.add :age1, "Tenant age must be an integer between 16 and 120"
end
end

13
app/views/case_logs/_tasklist.html.erb

@ -1,17 +1,16 @@
<ol class="app-task-list app-task-list--no-numbers">
<% @form.all_sections.map do |section_key, section_value| %>
<% @form.sections.map do |section| %>
<li>
<h2 class="app-task-list__section">
<span class="app-task-list__section-number">
<%= section_value["label"] %>
<%= section.label %>
</span>
</h2>
<ul class="app-task-list__items">
<% section_value["subsections"].map do |subsection_key, subsection_value| %>
<li class="app-task-list__item" id=<%= subsection_key %>>
<% questions_for_subsection = @form.questions_for_subsection(subsection_key) %>
<% subsection_status = @form.subsection_status(subsection_key, @case_log) %>
<%= subsection_link(subsection_key, subsection_value, subsection_status, @form, @case_log) %>
<% section.subsections.map do |subsection| %>
<li class="app-task-list__item" id=<%= subsection.id %>>
<% subsection_status = subsection.status(@case_log) %>
<%= subsection_link(subsection, @case_log) %>
<strong class="govuk-tag app-task-list__tag <%= TasklistHelper::STYLES[subsection_status] %>">
<%= TasklistHelper::STATUSES[subsection_status] %>
</strong>

2
app/views/case_logs/edit.html.erb

@ -8,7 +8,7 @@
<%= @case_log.status.to_s.humanize.downcase %></h2>
<p class="govuk-body govuk-!-margin-bottom-7">You've completed <%= get_subsections_count(@form, @case_log, :completed) %> of <%= get_subsections_count(@form, @case_log, :all) %> sections.</p>
<p class="govuk-body govuk-!-margin-bottom-7">
<% next_incomplete_section=get_next_incomplete_section(@form, @case_log) %>
<% next_incomplete_section = get_next_incomplete_section(@form, @case_log).id %>
<a class="govuk-link" href="#<%= next_incomplete_section %>"
data-controller="tasklist"
data-action="tasklist#addHighlight"

6
app/views/form/_check_answers_table.html.erb

@ -1,11 +1,11 @@
<div class="govuk-summary-list__row">
<dt class="govuk-summary-list__key">
<%= question_info["check_answer_label"].to_s.present? ? question_info["check_answer_label"].to_s : question_info["header"].to_s%>
<%= question.check_answer_label.to_s.present? ? question.check_answer_label.to_s : question.header.to_s %>
<dt>
<dd class="govuk-summary-list__value">
<%= form.get_answer_label(@case_log, question_title) %>
<%= question.answer_label(@case_log) %>
</dd>
<dd class="govuk-summary-list__actions">
<%= create_update_answer_link(question_title, question_info, @case_log, form) %>
<%= link_to(question.update_answer_link_name(@case_log), "/case_logs/#{@case_log.id}/#{question.page.id}", class: "govuk-link").html_safe %>
</dd>
</div>

10
app/views/form/_checkbox_question.html.erb

@ -1,12 +1,12 @@
<%= f.govuk_check_boxes_fieldset question_key,
legend: { text: question["header"].html_safe, size: "l" },
hint: { text: question["hint_text"] } do %>
<%= f.govuk_check_boxes_fieldset question.id.to_sym,
legend: { text: question.header.html_safe, size: "l" },
hint: { text: question.hint_text } do %>
<% question["answer_options"].map do |key, val| %>
<% question.answer_options.map do |key, val| %>
<% if key.starts_with?("divider") %>
<%= f.govuk_check_box_divider %>
<% else %>
<%= f.govuk_check_box question_key, key,
<%= f.govuk_check_box question.id, key,
label: { text: val },
checked: @case_log[key] == "Yes",
**stimulus_html_attributes(question)

6
app/views/form/_date_question.html.erb

@ -1,6 +1,6 @@
<%= f.govuk_date_field question_key,
hint: { text: question["hint_text"] },
legend: { text: question["header"].html_safe, size: "l"},
<%= f.govuk_date_field question.id.to_sym,
hint: { text: question.hint_text },
legend: { text: question.header.html_safe, size: "l"},
width: 20,
**stimulus_html_attributes(question)
%>

10
app/views/form/_numeric_question.html.erb

@ -1,7 +1,7 @@
<%= f.govuk_number_field question_key,
hint: { text: question["hint_text"] },
label: { text: question["header"].html_safe, size: "l"},
min: question["min"], max: question["max"], step: question["step"],
width: 20, :readonly => question["readonly"],
<%= f.govuk_number_field question.id.to_sym,
hint: { text: question.hint_text },
label: { text: question.header.html_safe, size: "l"},
min: question.min, max: question.max, step: question.step,
width: 20, :readonly => question.read_only?,
**stimulus_html_attributes(question)
%>

12
app/views/form/_radio_question.html.erb

@ -1,13 +1,13 @@
<%= f.govuk_radio_buttons_fieldset question_key,
legend: { text: question["header"].html_safe, size: "l" },
hint: { text: question["hint_text"] },
small: (question["answer_options"].size > 5) do %>
<%= f.govuk_radio_buttons_fieldset question.id.to_sym,
legend: { text: question.header.html_safe, size: "l" },
hint: { text: question.hint_text },
small: (question.answer_options.size > 5) do %>
<% question["answer_options"].map do |key, val| %>
<% question.answer_options.map do |key, val| %>
<% if key.starts_with?("divider") %>
<%= f.govuk_radio_divider %>
<% else %>
<%= f.govuk_radio_button question_key, val, label: { text: val }, **stimulus_html_attributes(question) %>
<%= f.govuk_radio_button question.id, val, label: { text: val }, **stimulus_html_attributes(question) %>
<% end %>
<% end %>
<% end %>

11
app/views/form/_select_question.html.erb

@ -1,11 +1,8 @@
<%= answers = question["answer_options"].map {|key, value| OpenStruct.new(id:key, name: value)}
f.govuk_collection_select question_key,
<%= answers = question.answer_options.map { |key, value| OpenStruct.new(id:key, name: value) }
f.govuk_collection_select question.id.to_sym,
answers,
:name,
:name,
label: { text: question["header"]},
hint: { text: question["hint_text"] }
label: { text: question.header},
hint: { text: question.hint_text }
%>

6
app/views/form/_text_question.html.erb

@ -1,6 +1,6 @@
<%= f.govuk_text_field question_key,
hint: { text: question["hint_text"] },
label: { text: question["header"].html_safe, size: "l"},
<%= f.govuk_text_field question.id.to_sym,
hint: { text: question.hint_text },
label: { text: question.header.html_safe, size: "l"},
width: 20,
**stimulus_html_attributes(question)
%>

6
app/views/form/_validation_override_question.html.erb

@ -3,13 +3,13 @@
data-soft-validations-target="override"
style='display:none;'>
<%= f.govuk_check_boxes_fieldset page_info["soft_validations"]&.keys&.first,
<%= f.govuk_check_boxes_fieldset page.soft_validations&.first&.id.to_sym,
legend: { text: "soft-validations-placeholder-message", size: "l" },
hint: { text: "soft-validations-placeholder-hint-text" } do %>
<%= f.govuk_check_box page_info["soft_validations"]&.keys&.first, page_info["soft_validations"]&.keys&.first,
<%= f.govuk_check_box page.soft_validations&.first&.id, page.soft_validations&.first&.id,
label: { text: "Yes" },
checked: @case_log[page_info["soft_validations"]&.keys&.first] == "Yes"
checked: @case_log[page.soft_validations&.first&.id] == "Yes"
%>
<% end %>
</div>

8
app/views/form/check_answers.html.erb

@ -1,11 +1,11 @@
<%= turbo_frame_tag "case_log_form", target: "_top" do %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-three-quarters-from-desktop">
<h1 class="govuk-heading-l">Check the answers you gave for <%= subsection.humanize(capitalize: false) %></h1>
<%= display_answered_questions_summary(subsection, @case_log, form) %>
<h1 class="govuk-heading-l">Check the answers you gave for <%= subsection.id.humanize(capitalize: false) %></h1>
<%= display_answered_questions_summary(subsection, @case_log) %>
<dl class="govuk-summary-list govuk-!-margin-bottom-9">
<% total_questions(subsection, @case_log, form).each do |question_title, question_info| %>
<%= render partial: 'form/check_answers_table', locals: { question_title: question_title, question_info: question_info, case_log: @case_log, form: form } %>
<% subsection.applicable_questions(@case_log).each do |question| %>
<%= render partial: 'form/check_answers_table', locals: { question: question, case_log: @case_log } %>
<% end %>
</dl>
<%= form_with model: @case_log, method: "get", builder: GOVUKDesignSystemFormBuilder::FormBuilder do |f| %>

16
app/views/form/page.html.erb

@ -5,24 +5,24 @@
<%= turbo_frame_tag "case_log_form", target: "_top" do %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<% if page_info["header"].present? %>
<% if page.header.present? %>
<h1 class="govuk-heading-xl">
<%= page_info["header"] %>
<%= page.header %>
</h1>
<% end %>
<%= form_with model: @case_log, url: form_case_log_path(@case_log), method: "post", builder: GOVUKDesignSystemFormBuilder::FormBuilder do |f| %>
<%= f.govuk_error_summary %>
<% page_info["questions"].map do |question_key, question| %>
<div id=<%= question_key + "_div " %><%= display_question_key_div(page_info, question_key) %> >
<%= render partial: "form/#{question["type"]}_question", locals: { question_key: question_key.to_sym, question: question, f: f } %>
<% page.questions.map do |question| %>
<div id=<%= question.id + "_div " %><%= display_question_key_div(page, question) %> >
<%= render partial: "form/#{question.type}_question", locals: { question: question, f: f } %>
</div>
<% end %>
<% if page_info["soft_validations"]&.keys&.first %>
<%= render partial: "form/validation_override_question", locals: { f: f, page_key: page_key, page_info: page_info } %>
<% if page.has_soft_validations? %>
<%= render partial: "form/validation_override_question", locals: { f: f, page: page } %>
<% end %>
<%= f.hidden_field :page, value: page_key %>
<%= f.hidden_field :page, value: page.id %>
<%= f.govuk_submit "Save and continue" %>
<% end %>
</div>

10
config/routes.rb

@ -23,13 +23,13 @@ Rails.application.routes.draw do
post "/form", to: "case_logs#submit_form"
end
form.all_pages.keys.map do |page|
get page.to_s, to: "case_logs##{page}"
get "#{page}/soft_validations", to: "soft_validations#show" if form.soft_validations_for_page(page)
form.pages.map do |page|
get page.id.to_s, to: "case_logs##{page.id}"
get "#{page.id}/soft_validations", to: "soft_validations#show" if page.has_soft_validations?
end
form.all_subsections.keys.map do |subsection|
get "#{subsection}/check_answers", to: "case_logs#check_answers"
form.subsections.map do |subsection|
get "#{subsection.id}/check_answers", to: "case_logs#check_answers"
end
end
end

10
docs/adr/adr-011-form-oop-refactor.md

@ -0,0 +1,10 @@
### ADR - 011: Splitting the form parsing into objects
Initially a single "Form" class handled the parsing of the form definition JSON as well as a lot of the logic around what different sections meant. This works fine but led to a lot of places in code where we're passing around arguments to determine whether a page or section should or shouldn't do something rather than being able to ask it directly. Refactoring this into smaller form domain object classes has several benefits:
- It's easier to compare the form definition JSON to the code classes and reason about what fields can be passed and what effect they'll have
- It moves business logic out of the helpers and keeps them to just dealing with display logic
- It makes it easier to unit test form functionality, and group that into smaller chunks
- It allows for less passing of arguments. e.g. `page.routed_to?(case_log)` vs `form.was_page_routed_to?(page, case_log)`
This abstraction is likely still not the best (the form vs case log split) but this seems like an improvement that can be iterated on.

39
spec/controllers/case_logs_controller_spec.rb

@ -93,28 +93,29 @@ RSpec.describe CaseLogsController, type: :controller do
page: "accessibility_requirements" }
end
let(:questions_for_page) do
{ "accessibility_requirements" =>
{
"type" => "checkbox",
"answer_options" =>
{ "housingneeds_a" => "Fully wheelchair accessible housing",
"housingneeds_b" => "Wheelchair access to essential rooms",
"housingneeds_c" => "Level access housing",
"housingneeds_f" => "Other disability requirements",
"housingneeds_g" => "No disability requirements",
"divider_a" => true,
"housingneeds_h" => "Do not know",
"divider_b" => true,
"accessibility_requirements_prefer_not_to_say" => "Prefer not to say" },
},
"tenant_code" =>
{
"type" => "text",
} }
[
Form::Question.new(
"accessibility_requirements",
{
"type" => "checkbox",
"answer_options" =>
{ "housingneeds_a" => "Fully wheelchair accessible housing",
"housingneeds_b" => "Wheelchair access to essential rooms",
"housingneeds_c" => "Level access housing",
"housingneeds_f" => "Other disability requirements",
"housingneeds_g" => "No disability requirements",
"divider_a" => true,
"housingneeds_h" => "Do not know",
"divider_b" => true,
"accessibility_requirements_prefer_not_to_say" => "Prefer not to say" },
}, nil
),
Form::Question.new("tenant_code", { "type" => "text" }, nil),
]
end
it "updates both question fields" do
allow_any_instance_of(Form).to receive(:questions_for_page).and_return(questions_for_page)
allow_any_instance_of(Form::Page).to receive(:expected_responses).and_return(questions_for_page)
post :submit_form, params: { id: id, case_log: case_log_form_params }
case_log.reload

2
spec/features/case_log_spec.rb

@ -29,7 +29,7 @@ RSpec.describe "Form Features" do
click_button("Save and continue")
choose("case-log-benefits-all-field")
click_button("Save and continue")
choose("case-log-hb-housing-benefit-but-not-universal-credit-field")
choose("case-log-hb-prefer-not-to-say-field")
click_button("Save and continue")
end

30
spec/fixtures/forms/test_form.json vendored

@ -303,13 +303,15 @@
"label": "Income and benefits",
"pages": {
"net_income": {
"header": "Test header",
"description": "Some extra text for the page",
"questions": {
"earnings": {
"check_answer_label": "Income",
"header": "What is the tenant’s /and partner’s combined income after tax?",
"type": "numeric",
"min": 0,
"step": "1"
"step": 1
},
"incfreq": {
"check_answer_label": "Income Frequency",
@ -354,6 +356,32 @@
"answer_options": {
"0": "Housing Benefit, but not Universal Credit",
"1": "Prefer not to say"
},
"conditional_for": {
"conditional_question": ["Housing Benefit, but not Universal Credit"]
}
},
"conditional_question": {
"check_answer_label": "Conditional Question",
"header": "Question to test page conditions",
"type": "radio",
"answer_options": {
"0": "Option A",
"1": "Option B"
}
}
}
},
"dependent_page": {
"depends_on": { "incfreq": "Weekly" },
"questions": {
"dependent_question": {
"check_answer_label": "Dependent Question",
"header": "Question to test page routing",
"type": "checkbox",
"answer_options": {
"0": "Option A",
"1": "Option B"
}
}
}

185
spec/helpers/check_answers_helper_spec.rb

@ -1,172 +1,29 @@
require "rails_helper"
RSpec.describe CheckAnswersHelper do
let(:case_log) { FactoryBot.create(:case_log) }
let(:case_log_with_met_numeric_condition) do
FactoryBot.create(
:case_log,
:in_progress,
other_hhmemb: 1,
relat2: "Partner",
)
end
let(:case_log_with_met_radio_condition) do
FactoryBot.create(:case_log, armedforces: "A current or former regular in the UK Armed Forces (exc. National Service)",
reservist: "No",
leftreg: "Yes")
end
let(:subsection) { "income_and_benefits" }
let(:subsection_with_numeric_conditionals) { "household_characteristics" }
let(:subsection_with_radio_conditionals) { "household_needs" }
let(:conditional_routing_subsection) { "conditional_question" }
let(:conditional_page_subsection) { "household_needs" }
form_handler = FormHandler.instance
let(:form) { form_handler.get_form("test_form") }
describe "Get answered questions total" do
it "returns 0 if no questions are answered" do
expect(total_answered_questions(subsection, case_log, form)).to equal(0)
end
it "returns 1 if 1 question gets answered" do
case_log["earnings"] = "123"
expect(total_answered_questions(subsection, case_log, form)).to equal(1)
end
it "ignores questions with unmet numeric conditions" do
case_log["tenant_code"] = "T1234"
expect(total_answered_questions(subsection_with_numeric_conditionals, case_log, form)).to equal(1)
end
it "includes conditional questions with met numeric conditions" do
expect(total_answered_questions(
subsection_with_numeric_conditionals,
case_log_with_met_numeric_condition,
form,
)).to equal(4)
end
it "ignores questions with unmet radio conditions" do
case_log["armedforces"] = "No"
expect(total_answered_questions(subsection_with_radio_conditionals, case_log, form)).to equal(1)
end
it "includes conditional questions with met radio conditions" do
case_log_with_met_radio_condition["reservist"] = "No"
case_log_with_met_radio_condition["illness"] = "No"
expect(total_answered_questions(
subsection_with_radio_conditionals,
case_log_with_met_radio_condition,
form,
)).to equal(4)
end
end
describe "Get total number of questions" do
it "returns the total number of questions for a subsection" do
expect(total_number_of_questions(subsection, case_log, form)).to eq(4)
end
it "ignores questions with unmet numeric conditions" do
expect(total_number_of_questions(subsection_with_numeric_conditionals, case_log, form)).to eq(4)
end
it "includes conditional questions with met numeric conditions" do
expect(total_number_of_questions(
subsection_with_numeric_conditionals,
case_log_with_met_numeric_condition,
form,
)).to eq(8)
end
it "ignores questions with unmet radio conditions" do
expect(total_number_of_questions(subsection_with_radio_conditionals, case_log, form)).to eq(4)
end
it "includes conditional questions with met radio conditions" do
expect(total_number_of_questions(
subsection_with_radio_conditionals,
case_log_with_met_radio_condition,
form,
)).to eq(6)
end
context "conditional questions with type that hasn't been implemented yet" do
let(:unimplemented_conditional) do
{ "previous_postcode" =>
{ "header" => "The actual question?",
"hint_text" => "",
"type" => "date",
"check_answer_label" => "Question Label",
"conditional_for" => { "question_2" => %w[12-12-2021] } } }
end
it "raises an error" do
allow_any_instance_of(Form).to receive(:questions_for_page).and_return(unimplemented_conditional)
expect { total_number_of_questions(subsection, case_log, form) }.to raise_error(RuntimeError, "Not implemented yet")
end
end
context "conditional routing" do
it "ignores not visited questions when no questions are answered" do
expect(total_number_of_questions(conditional_routing_subsection, case_log, form)).to eq(1)
end
it "counts correct questions when the conditional question is answered" do
case_log["preg_occ"] = "Yes"
expect(total_number_of_questions(conditional_routing_subsection, case_log, form)).to eq(2)
end
it "counts correct questions when the conditional question is answered" do
case_log["preg_occ"] = "No"
case_log["sex1"] = "Male"
expect(total_number_of_questions(conditional_routing_subsection, case_log, form)).to eq(3)
end
end
context "total questions" do
it "returns total questions" do
result = total_questions(subsection, case_log, form)
expected_keys = %w[earnings incfreq benefits hb]
expect(result.keys).to eq(expected_keys)
end
context "conditional questions on the same page" do
it "it filters out conditional questions that were not displayed" do
result = total_questions(conditional_page_subsection, case_log, form)
expected_keys = %w[armedforces illness accessibility_requirements condition_effects]
expect(result.keys).to eq(expected_keys)
end
it "it includes conditional questions that were displayed" do
case_log["armedforces"] = "A current or former regular in the UK Armed Forces (exc. National Service)"
result = total_questions(conditional_page_subsection, case_log, form)
expected_keys = %w[armedforces leftreg reservist illness accessibility_requirements condition_effects]
expect(result.keys).to eq(expected_keys)
end
end
context "conditional routing" do
it "it ignores skipped pages and the questions therein when conditional routing" do
result = total_questions(conditional_routing_subsection, case_log, form)
expected_keys = %w[preg_occ]
expect(result.keys).to match_array(expected_keys)
end
it "it includes conditional pages and questions that were displayed" do
case_log["preg_occ"] = "Yes"
case_log["sex1"] = "Female"
result = total_questions(conditional_routing_subsection, case_log, form)
expected_keys = %w[preg_occ cbl]
expect(result.keys).to match_array(expected_keys)
end
it "it includes conditional pages and questions that were displayed" do
case_log["preg_occ"] = "No"
result = total_questions(conditional_routing_subsection, case_log, form)
expected_keys = %w[preg_occ conditional_question_no_question]
expect(result.keys).to match_array(expected_keys)
end
let(:subsection) { form.get_subsection("household_characteristics") }
let(:case_log) { FactoryBot.build(:case_log, :in_progress) }
describe "display_answered_questions_summary" do
context "given a section that hasn't been completed yet" do
it "returns a link to the next unanswered question" do
expect(display_answered_questions_summary(subsection, case_log))
.to match(/You answered 2 of 4 questions/)
expect(display_answered_questions_summary(subsection, case_log))
.to match(/href/)
end
end
context "given a section that has been completed" do
it "returns that you have answered all the questions" do
case_log.sex1 = "F"
case_log.other_hhmemb = 0
expect(display_answered_questions_summary(subsection, case_log))
.to match(/You answered all the questions/)
expect(display_answered_questions_summary(subsection, case_log))
.not_to match(/href/)
end
end
end

12
spec/helpers/conditional_questions_helper_spec.rb

@ -3,8 +3,7 @@ require "rails_helper"
RSpec.describe ConditionalQuestionsHelper do
form_handler = FormHandler.instance
let(:form) { form_handler.get_form("test_form") }
let(:page_key) { "armed_forces" }
let(:page) { form.all_pages[page_key] }
let(:page) { form.get_page("armed_forces") }
describe "conditional questions for page" do
let(:conditional_pages) { %w[leftreg reservist] }
@ -15,15 +14,14 @@ RSpec.describe ConditionalQuestionsHelper do
end
describe "display question key div" do
let(:question_key) { "armed_forces" }
let(:conditional_question_key) { "reservist" }
let(:conditional_question) { page.questions.find { |q| q.id == "reservist" } }
it "returns a non visible div for conditional questions" do
expect(display_question_key_div(page, conditional_question_key)).to match("style='display:none;'")
expect(display_question_key_div(page, conditional_question)).to match("style='display:none;'")
end
it "returns a visible div for conditional questions" do
expect(display_question_key_div(page, question_key)).not_to match("style='display:none;'")
it "returns a visible div for questions" do
expect(display_question_key_div(page, page.questions.first)).not_to match("style='display:none;'")
end
end
end

22
spec/helpers/question_attribute_helper_spec.rb

@ -3,25 +3,27 @@ require "rails_helper"
RSpec.describe QuestionAttributeHelper do
form_handler = FormHandler.instance
let(:form) { form_handler.get_form("test_form") }
let(:questions) { form.questions_for_page("rent") }
let(:questions) { form.get_page("rent").questions }
describe "html attributes" do
it "returns empty hash if fields-to-add or result-field are empty " do
expect(stimulus_html_attributes(questions["tcharge"])).to eq({})
question = questions.find { |q| q.id == "tcharge" }
expect(stimulus_html_attributes(question)).to eq({})
end
it "returns html attributes if fields-to-add or result-field are not empty " do
expect(stimulus_html_attributes(questions["brent"])).to eq({
brent = questions.find { |q| q.id == "brent" }
expect(stimulus_html_attributes(brent)).to eq({
"data-controller": "numeric-question",
"data-action": "numeric-question#calculateFields",
"data-target": "case-log-#{questions['brent']['result-field'].to_s.dasherize}-field",
"data-calculated": questions["brent"]["fields-to-add"].to_json,
"data-target": "case-log-#{brent.result_field.to_s.dasherize}-field",
"data-calculated": brent.fields_to_add.to_json,
})
end
context "a question that requires multiple controllers" do
let(:question) do
{
Form::Question.new("brent", {
"check_answer_label" => "Basic Rent",
"header" => "What is the basic rent?",
"hint_text" => "Eligible for housing benefit or Universal Credit",
@ -33,15 +35,15 @@ RSpec.describe QuestionAttributeHelper do
"conditional_for" => {
"next_question": ">1",
},
}
}, nil)
end
let(:expected_attribs) do
{
"data-controller": "numeric-question conditional-question",
"data-action": "numeric-question#calculateFields conditional-question#displayConditional",
"data-target": "case-log-#{question['result-field'].to_s.dasherize}-field",
"data-calculated": question["fields-to-add"].to_json,
"data-info": question["conditional_for"].to_json,
"data-target": "case-log-#{question.result_field.to_s.dasherize}-field",
"data-calculated": question.fields_to_add.to_json,
"data-info": question.conditional_for.to_json,
}
end
it "correctly merges html attributes" do

30
spec/helpers/tasklist_helper_spec.rb

@ -8,12 +8,12 @@ RSpec.describe TasklistHelper do
describe "get next incomplete section" do
it "returns the first subsection name if it is not completed" do
expect(get_next_incomplete_section(form, case_log)).to eq("household_characteristics")
expect(get_next_incomplete_section(form, case_log).id).to eq("household_characteristics")
end
it "returns the first subsection name if it is partially completed" do
case_log["tenant_code"] = 123
expect(get_next_incomplete_section(form, case_log)).to eq("household_characteristics")
expect(get_next_incomplete_section(form, case_log).id).to eq("household_characteristics")
end
end
@ -40,12 +40,34 @@ RSpec.describe TasklistHelper do
end
describe "get_first_page_or_check_answers" do
let(:subsection) { form.get_subsection("household_characteristics") }
it "returns the check answers page path if the section has been started already" do
expect(get_first_page_or_check_answers("household_characteristics", case_log, form)).to match(/check_answers/)
expect(first_page_or_check_answers(subsection, case_log)).to match(/check_answers/)
end
it "returns the first question page path for the section if it has not been started yet" do
expect(get_first_page_or_check_answers("household_characteristics", empty_case_log, form)).to match(/tenant_code/)
expect(first_page_or_check_answers(subsection, empty_case_log)).to match(/tenant_code/)
end
end
describe "subsection link" do
let(:subsection) { form.get_subsection("household_characteristics") }
context "for a subsection that's enabled" do
it "returns the subsection link url" do
expect(subsection_link(subsection, case_log)).to match(/household_characteristics/)
end
end
context "for a subsection that cannot be started yet" do
before do
allow(subsection).to receive(:status).with(case_log).and_return(:cannot_start_yet)
end
it "returns a # link" do
expect(subsection_link(subsection, case_log)).to match(/#/)
end
end
end
end

66
spec/models/form/page_spec.rb

@ -0,0 +1,66 @@
require "rails_helper"
RSpec.describe Form::Page, type: :model do
let(:form) { FormHandler.instance.get_form("test_form") }
let(:section_id) { "rent_and_charges" }
let(:section_definition) { form.form_definition["sections"][section_id] }
let(:section) { Form::Section.new(section_id, section_definition, form) }
let(:subsection_id) { "income_and_benefits" }
let(:subsection_definition) { section_definition["subsections"][subsection_id] }
let(:subsection) { Form::Subsection.new(subsection_id, subsection_definition, section) }
let(:page_id) { "net_income" }
let(:page_definition) { subsection_definition["pages"][page_id] }
subject { Form::Page.new(page_id, page_definition, subsection) }
it "has an id" do
expect(subject.id).to eq(page_id)
end
it "has a header" do
expect(subject.header).to eq("Test header")
end
it "has a description" do
expect(subject.description).to eq("Some extra text for the page")
end
it "has questions" do
expected_questions = %w[earnings incfreq]
expect(subject.questions.map(&:id)).to eq(expected_questions)
end
it "has soft validations" do
expected_soft_validations = %w[override_net_income_validation]
expect(subject.soft_validations.map(&:id)).to eq(expected_soft_validations)
end
it "has a soft_validation helper" do
expect(subject.has_soft_validations?).to be true
end
it "has expected form responses" do
expected_responses = %w[earnings incfreq override_net_income_validation]
expect(subject.expected_responses.map(&:id)).to eq(expected_responses)
end
context "for a given case log" do
let(:case_log) { FactoryBot.build(:case_log, :in_progress) }
it "knows if it's been routed to" do
expect(subject.routed_to?(case_log)).to be true
end
context "given routing conditions" do
let(:page_id) { "dependent_page" }
it "evaluates not met conditions correctly" do
expect(subject.routed_to?(case_log)).to be false
end
it "evaluates not conditions correctly" do
case_log.incfreq = "Weekly"
expect(subject.routed_to?(case_log)).to be true
end
end
end
end

140
spec/models/form/question_spec.rb

@ -0,0 +1,140 @@
require "rails_helper"
RSpec.describe Form::Question, type: :model do
let(:form) { FormHandler.instance.get_form("test_form") }
let(:section_id) { "rent_and_charges" }
let(:section_definition) { form.form_definition["sections"][section_id] }
let(:section) { Form::Section.new(section_id, section_definition, form) }
let(:subsection_id) { "income_and_benefits" }
let(:subsection_definition) { section_definition["subsections"][subsection_id] }
let(:subsection) { Form::Subsection.new(subsection_id, subsection_definition, section) }
let(:page_id) { "net_income" }
let(:page_definition) { subsection_definition["pages"][page_id] }
let(:page) { Form::Page.new(page_id, page_definition, subsection) }
let(:question_id) { "earnings" }
let(:question_definition) { page_definition["questions"][question_id] }
subject { Form::Question.new(question_id, question_definition, page) }
it "has an id" do
expect(subject.id).to eq(question_id)
end
it "has a header" do
expect(subject.header).to eq("What is the tenant’s /and partner’s combined income after tax?")
end
it "has a check answers label" do
expect(subject.check_answer_label).to eq("Income")
end
it "has a question type" do
expect(subject.type).to eq("numeric")
end
it "belongs to a page" do
expect(subject.page).to eq(page)
end
it "belongs to a subsection" do
expect(subject.subsection).to eq(subsection)
end
it "has a read only helper" do
expect(subject.read_only?).to be false
end
context "when type is numeric" do
it "has a min value" do
expect(subject.min).to eq(0)
end
it "has a step value" do
expect(subject.step).to eq(1)
end
end
context "when type is radio" do
let(:question_id) { "incfreq" }
it "has answer options" do
expected_answer_options = { "0" => "Weekly", "1" => "Monthly", "2" => "Yearly" }
expect(subject.answer_options).to eq(expected_answer_options)
end
end
context "when type is checkbox" do
let(:page_id) { "dependent_page" }
let(:question_id) { "dependent_question" }
it "has answer options" do
expected_answer_options = { "0" => "Option A", "1" => "Option B" }
expect(subject.answer_options).to eq(expected_answer_options)
end
end
context "when the question is read only" do
let(:subsection_id) { "rent" }
let(:page_id) { "rent" }
let(:question_id) { "tcharge" }
it "has a read only helper" do
expect(subject.read_only?).to be true
end
context "when the answer is part of a sum" do
let(:question_id) { "pscharge" }
it "has a result_field" do
expect(subject.result_field).to eq("tcharge")
end
it "has fields to sum" do
expected_fields_to_sum = %w[brent scharge pscharge supcharg]
expect(subject.fields_to_add).to eq(expected_fields_to_sum)
end
end
end
context "for a given case log" do
let(:case_log) { FactoryBot.build(:case_log, :in_progress) }
it "has an answer label" do
case_log.earnings = 100
expect(subject.answer_label(case_log)).to eq("100")
end
it "has an update answer link text helper" do
expect(subject.update_answer_link_name(case_log)).to eq("Answer")
case_log[question_id] = 5
expect(subject.update_answer_link_name(case_log)).to eq("Change")
end
context "when type is checkbox" do
let(:section_id) { "household" }
let(:subsection_id) { "household_needs" }
let(:page_id) { "accessibility_requirements" }
let(:question_id) { "accessibility_requirements" }
it "has a joined answers label" do
case_log.housingneeds_a = 1
case_log.housingneeds_c = 1
expected_answer_label = "Fully wheelchair accessible housing, Level access housing"
expect(subject.answer_label(case_log)).to eq(expected_answer_label)
end
end
context "when a condition is present" do
let(:page_id) { "housing_benefit" }
let(:question_id) { "conditional_question" }
it "knows whether it is enabled or not for unmet conditions" do
expect(subject.enabled?(case_log)).to be false
end
it "knows whether it is enabled or not for met conditions" do
case_log.hb = "Housing Benefit, but not Universal Credit"
expect(subject.enabled?(case_log)).to be true
end
end
end
end

21
spec/models/form/section_spec.rb

@ -0,0 +1,21 @@
require "rails_helper"
RSpec.describe Form::Section, type: :model do
let(:form) { FormHandler.instance.get_form("test_form") }
let(:section_id) { "household" }
let(:section_definition) { form.form_definition["sections"][section_id] }
subject { Form::Section.new(section_id, section_definition, form) }
it "has an id" do
expect(subject.id).to eq(section_id)
end
it "has a label" do
expect(subject.label).to eq("About the household")
end
it "has subsections" do
expected_subsections = %w[household_characteristics household_needs]
expect(subject.subsections.map(&:id)).to eq(expected_subsections)
end
end

72
spec/models/form/subsection_spec.rb

@ -0,0 +1,72 @@
require "rails_helper"
RSpec.describe Form::Subsection, type: :model do
let(:form) { FormHandler.instance.get_form("test_form") }
let(:section_id) { "household" }
let(:section_definition) { form.form_definition["sections"][section_id] }
let(:section) { Form::Section.new(section_id, section_definition, form) }
let(:subsection_id) { "household_characteristics" }
let(:subsection_definition) { section_definition["subsections"][subsection_id] }
subject { Form::Subsection.new(subsection_id, subsection_definition, section) }
it "has an id" do
expect(subject.id).to eq(subsection_id)
end
it "has a label" do
expect(subject.label).to eq("Household characteristics")
end
it "has pages" do
expected_pages = %w[tenant_code person_1_age person_1_gender household_number_of_other_members]
expect(subject.pages.map(&:id)).to eq(expected_pages)
end
it "has questions" do
expected_questions = %w[tenant_code age1 sex1 other_hhmemb relat2 age2 sex2 ecstat2]
expect(subject.questions.map(&:id)).to eq(expected_questions)
end
context "for a given in progress case log" do
let(:case_log) { FactoryBot.build(:case_log, :in_progress) }
it "has a status" do
expect(subject.status(case_log)).to eq(:in_progress)
end
it "has status helpers" do
expect(subject.is_incomplete?(case_log)).to be(true)
expect(subject.is_started?(case_log)).to be(true)
end
it "has question helpers for the number of applicable questions" do
expected_questions = %w[tenant_code age1 sex1 other_hhmemb]
expect(subject.applicable_questions(case_log).map(&:id)).to eq(expected_questions)
expect(subject.applicable_questions_count(case_log)).to eq(4)
end
it "has question helpers for the number of answered questions" do
expected_questions = %w[tenant_code age1]
expect(subject.answered_questions(case_log).map(&:id)).to eq(expected_questions)
expect(subject.answered_questions_count(case_log)).to eq(2)
end
it "has a question helpers for the unanswered questions" do
expected_questions = %w[sex1 other_hhmemb]
expect(subject.unanswered_questions(case_log).map(&:id)).to eq(expected_questions)
end
end
context "for a given completed case log" do
let(:case_log) { FactoryBot.build(:case_log, :completed) }
it "has a status" do
expect(subject.status(case_log)).to eq(:completed)
end
it "has status helpers" do
expect(subject.is_incomplete?(case_log)).to be(false)
expect(subject.is_started?(case_log)).to be(true)
end
end
end

2
spec/models/form_handler_spec.rb

@ -15,7 +15,7 @@ RSpec.describe FormHandler do
form_handler = FormHandler.instance
form = form_handler.get_form("test_form")
expect(form).to be_a(Form)
expect(form.all_pages.count).to eq(23)
expect(form.pages.count).to eq(24)
end
end

64
spec/models/form_spec.rb

@ -8,72 +8,16 @@ RSpec.describe Form, type: :model do
let(:conditional_section_complete_case_log) { FactoryBot.build(:case_log, :conditional_section_complete) }
describe ".next_page" do
let(:previous_page) { "person_1_age" }
let(:previous_page) { form.get_page("person_1_age") }
it "returns the next page given the previous" do
expect(form.next_page(previous_page, case_log)).to eq("person_1_gender")
end
end
describe "get subsection status" do
let(:section) { "income_and_benefits" }
it "returns not started if none of the questions in the subsection are answered" do
status = form.subsection_status("income_and_benefits", case_log)
expect(status).to eq(:not_started)
end
it "returns cannot start yet if the subsection is declaration" do
status = form.subsection_status("declaration", case_log)
expect(status).to eq(:cannot_start_yet)
end
it "returns in progress if some of the questions have been answered" do
case_log["previous_postcode"] = "P0 5TT"
status = form.subsection_status("local_authority", case_log)
expect(status).to eq(:in_progress)
end
it "returns completed if all the questions in the subsection have been answered" do
case_log["earnings"] = "value"
case_log["incfreq"] = "Weekly"
case_log["benefits"] = "All"
case_log["hb"] = "Do not know"
status = form.subsection_status("income_and_benefits", case_log)
expect(status).to eq(:completed)
end
it "returns not started if the subsection is declaration and all the questions are completed" do
status = form.subsection_status("declaration", completed_case_log)
expect(status).to eq(:not_started)
end
it "sets the correct status for sections with conditional questions" do
status = form.subsection_status("household_characteristics", conditional_section_complete_case_log)
expect(status).to eq(:completed)
end
end
describe ".first_page_for_subsection" do
let(:subsection) { "household_characteristics" }
it "returns the first page given a subsection" do
expect(form.first_page_for_subsection(subsection)).to eq("tenant_code")
end
end
describe ".questions_for_subsection" do
let(:subsection) { "income_and_benefits" }
it "returns all questions for subsection" do
result = form.questions_for_subsection(subsection)
expect(result.length).to eq(4)
expect(result.keys).to eq(%w[earnings incfreq benefits hb])
end
end
describe "next_page_redirect_path" do
let(:previous_page) { "net_income" }
let(:last_previous_page) { "housing_benefit" }
let(:previous_conditional_page) { "conditional_question" }
let(:previous_page) { form.get_page("net_income") }
let(:last_previous_page) { form.get_page("housing_benefit") }
let(:previous_conditional_page) { form.get_page("conditional_question") }
it "returns a correct page path if there is no conditional routing" do
expect(form.next_page_redirect_path(previous_page, case_log)).to eq("case_log_net_income_uc_proportion_path")

Loading…
Cancel
Save