Browse Source

Merge branch 'main' into your-account-page

pull/110/head
Matthew Phelan 4 years ago
parent
commit
e45b61a645
  1. 27
      app/admin/admin_users.rb
  2. 2
      app/admin/case_logs.rb
  3. 38
      app/constants/db_enums.rb
  4. 49
      app/controllers/case_logs_controller.rb
  5. 6
      app/controllers/soft_validations_controller.rb
  6. 61
      app/helpers/check_answers_helper.rb
  7. 8
      app/helpers/conditional_questions_helper.rb
  8. 10
      app/helpers/question_attribute_helper.rb
  9. 38
      app/helpers/tasklist_helper.rb
  10. 5
      app/models/admin_user.rb
  11. 2
      app/models/bulk_upload.rb
  12. 20
      app/models/case_log.rb
  13. 160
      app/models/form.rb
  14. 30
      app/models/form/page.rb
  15. 80
      app/models/form/question.rb
  16. 10
      app/models/form/section.rb
  17. 65
      app/models/form/subsection.rb
  18. 10
      app/validations/household_validations.rb
  19. 2
      app/validations/soft_validations.rb
  20. 13
      app/views/case_logs/_tasklist.html.erb
  21. 4
      app/views/case_logs/edit.html.erb
  22. 2
      app/views/case_logs/index.html.erb
  23. 6
      app/views/form/_check_answers_table.html.erb
  24. 10
      app/views/form/_checkbox_question.html.erb
  25. 6
      app/views/form/_date_question.html.erb
  26. 10
      app/views/form/_numeric_question.html.erb
  27. 12
      app/views/form/_radio_question.html.erb
  28. 11
      app/views/form/_select_question.html.erb
  29. 6
      app/views/form/_text_question.html.erb
  30. 6
      app/views/form/_validation_override_question.html.erb
  31. 8
      app/views/form/check_answers.html.erb
  32. 21
      app/views/form/page.html.erb
  33. 281
      config/forms/2021_2022.json
  34. 4
      config/initializers/active_admin.rb
  35. 13
      config/routes.rb
  36. 10
      db/migrate/20211119104835_add_property_info_fields.rb
  37. 18
      db/migrate/20211119120910_add_admin_users.rb
  38. 19
      db/migrate/20211124152204_change_recently_let_as_to_enum.rb
  39. 9
      db/migrate/20211124174732_remove_old_fields.rb
  40. 15
      db/schema.rb
  41. 1
      db/seeds.rb
  42. 9
      docs/adr/adr-010-admin-users-vs-users.md
  43. 10
      docs/adr/adr-011-form-oop-refactor.md
  44. 2
      docs/api/DLUHC-CORE-Data.v1.json
  45. 30
      spec/controllers/admin/admin_users_controller_spec.rb
  46. 29
      spec/controllers/admin/case_logs_controller_spec.rb
  47. 41
      spec/controllers/admin/dashboard_controller_spec.rb
  48. 39
      spec/controllers/case_logs_controller_spec.rb
  49. 8
      spec/factories/admin_user.rb
  50. 9
      spec/factories/case_log.rb
  51. 1
      spec/factories/user.rb
  52. 45
      spec/features/case_log_spec.rb
  53. 17
      spec/fixtures/complete_case_log.json
  54. 179
      spec/fixtures/forms/test_aboutthislog.json
  55. 30
      spec/fixtures/forms/test_form.json
  56. 185
      spec/helpers/check_answers_helper_spec.rb
  57. 12
      spec/helpers/conditional_questions_helper_spec.rb
  58. 22
      spec/helpers/question_attribute_helper_spec.rb
  59. 30
      spec/helpers/tasklist_helper_spec.rb
  60. 2
      spec/lib/tasks/form_definition_validator_spec.rb
  61. 101
      spec/models/case_log_spec.rb
  62. 66
      spec/models/form/page_spec.rb
  63. 140
      spec/models/form/question_spec.rb
  64. 21
      spec/models/form/section_spec.rb
  65. 72
      spec/models/form/subsection_spec.rb
  66. 2
      spec/models/form_handler_spec.rb
  67. 64
      spec/models/form_spec.rb
  68. 8
      spec/support/controller_macros.rb

27
app/admin/admin_users.rb

@ -0,0 +1,27 @@
ActiveAdmin.register AdminUser do
permit_params :email, :password, :password_confirmation
index do
selectable_column
id_column
column :email
column :current_sign_in_at
column :sign_in_count
column :created_at
actions
end
filter :email
filter :current_sign_in_at
filter :sign_in_count
filter :created_at
form do |f|
f.inputs do
f.input :email
f.input :password
f.input :password_confirmation
end
f.actions
end
end

2
app/admin/case_logs.rb

@ -2,7 +2,7 @@ ActiveAdmin.register CaseLog do
# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
permit_params do
permitted = %i[status tenant_code age1 sex1 tenant_ethnic_group tenant_nationality previous_housing_situation armedforces ecstat1 other_hhmemb relat2 age2 sex2 ecstat2 relat3 age3 sex3 ecstat3 relat4 age4 sex4 ecstat4 relat5 age5 sex5 ecstat5 relat6 age6 sex6 ecstat6 relat7 age7 person_7_gender ecstat7 relat8 age8 sex8 ecstat8 homelessness reason benefit_cap_spare_room_subsidy armed_forces_active armed_forces_injured medical_conditions pregnancy accessibility_requirements condition_effects tenancy_code tenancy_start_date starter_tenancy fixed_term_tenancy tenancy_type letting_type letting_provider la previous_postcode property_relet property_vacancy_reason property_reference property_unit_type property_building_type property_number_of_bedrooms property_void_date majorrepairs mrcdate property_number_of_times_relet property_wheelchair_accessible net_income net_income_frequency net_income_uc_proportion hb rent_frequency basic_rent service_charge personal_service_charge support_charge total_charge tshortfall time_lived_in_la time_on_la_waiting_list prevloc property_postcode reasonable_preference reasonable_preference_reason cbl_letting chr_letting cap_letting hbrentshortfall other_reason accessibility_requirements_fully_wheelchair_accessible_housing accessibility_requirements_wheelchair_access_to_essential_rooms accessibility_requirements_level_access_housing accessibility_requirements_other_disability_requirements accessibility_requirements_no_disability_requirements accessibility_requirements_do_not_know accessibility_requirements_prefer_not_to_say condition_effects_vision condition_effects_hearing condition_effects_mobility condition_effects_dexterity condition_effects_stamina condition_effects_learning condition_effects_memory condition_effects_mental_health condition_effects_social_or_behavioral condition_effects_other condition_effects_prefer_not_to_say reasonable_preference_reason_homeless reasonable_preference_reason_unsatisfactory_housing reasonable_preference_reason_medical_grounds reasonable_preference_reason_avoid_hardship reasonable_preference_reason_do_not_know other_tenancy_type override_net_income_validation net_income_known]
permitted = %i[status tenant_code age1 sex1 tenant_ethnic_group tenant_nationality previous_housing_situation armedforces ecstat1 other_hhmemb relat2 age2 sex2 ecstat2 relat3 age3 sex3 ecstat3 relat4 age4 sex4 ecstat4 relat5 age5 sex5 ecstat5 relat6 age6 sex6 ecstat6 relat7 age7 person_7_gender ecstat7 relat8 age8 sex8 ecstat8 homelessness reason benefit_cap_spare_room_subsidy armed_forces_active armed_forces_injured medical_conditions pregnancy accessibility_requirements condition_effects tenancy_code tenancy_start_date starter_tenancy fixed_term_tenancy tenancy_type letting_type letting_provider la previous_postcode property_relet property_vacancy_reason property_reference property_unit_type property_building_type property_number_of_bedrooms property_void_date majorrepairs mrcdate property_wheelchair_accessible net_income net_income_frequency net_income_uc_proportion hb rent_frequency basic_rent service_charge personal_service_charge support_charge total_charge tshortfall time_lived_in_la time_on_la_waiting_list prevloc property_postcode reasonable_preference reasonable_preference_reason cbl_letting chr_letting cap_letting hbrentshortfall other_reason accessibility_requirements_fully_wheelchair_accessible_housing accessibility_requirements_wheelchair_access_to_essential_rooms accessibility_requirements_level_access_housing accessibility_requirements_other_disability_requirements accessibility_requirements_no_disability_requirements accessibility_requirements_do_not_know accessibility_requirements_prefer_not_to_say condition_effects_vision condition_effects_hearing condition_effects_mobility condition_effects_dexterity condition_effects_stamina condition_effects_learning condition_effects_memory condition_effects_mental_health condition_effects_social_or_behavioral condition_effects_other condition_effects_prefer_not_to_say reasonable_preference_reason_homeless reasonable_preference_reason_unsatisfactory_housing reasonable_preference_reason_medical_grounds reasonable_preference_reason_avoid_hardship reasonable_preference_reason_do_not_know other_tenancy_type override_net_income_validation net_income_known]
permitted
end

38
app/constants/db_enums.rb

@ -10,6 +10,22 @@ module DbEnums
}
end
def self.unitletas
{
"Social rent basis" => 1,
"Affordable rent basis" => 2,
"Intermediate rent basis" => 4,
"Do not know" => 3,
}
end
def self.builtype
{
"Purpose built" => 1,
"Conversion" => 2,
}
end
def self.ecstat
{
"Part-time - Less than 30 hours" => 2,
@ -186,17 +202,17 @@ module DbEnums
"First let of newbuild property" => 15,
"First let of conversion/rehabilitation/acquired property" => 16,
"First let of leased property" => 17,
"Relet - tenant evicted due to arrears" => 10,
"Relet - tenant evicted due to ASB or other reason" => 11,
"Relet - tenant died (no succession)" => 5,
"Relet - tenant moved to other social housing provider" => 12,
"Relet - tenant abandoned property" => 6,
"Relet - tenant moved to private sector or other accommodation" => 8,
"Relet - to tenant who occupied same property as temporary accommodation" => 9,
"Relet – internal transfer (excluding renewals of a fixed-term tenancy)" => 13,
"Relet – renewal of fixed-term tenancy" => 14,
"Relet – tenant moved to care home" => 18,
"Relet – tenant involved in a succession downsize" => 19,
"Tenant evicted due to arrears" => 10,
"Tenant evicted due to ASB or other reason" => 11,
"Tenant died (no succession)" => 5,
"Tenant moved to other social housing provider" => 12,
"Tenant abandoned property" => 6,
"Tenant moved to private sector or other accommodation" => 8,
"Relet to tenant who occupied same property as temporary accommodation" => 9,
"Internal transfer (excluding renewals of a fixed-term tenancy)" => 13,
"Renewal of fixed-term tenancy" => 14,
"Tenant moved to care home" => 18,
"Tenant involved in a succession downsize" => 19,
}
end

49
app/controllers/case_logs_controller.rb

@ -57,14 +57,15 @@ 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
subsection = form.subsection_for_page(page)
render "form/page", locals: { form: form, page: page, subsection: subsection.label }, status: :unprocessable_entity
end
end
@ -84,15 +85,16 @@ 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 }
subsection = form.subsection_for_page(page)
render "form/page", locals: { form: form, page: page, subsection: subsection.label }
end
end
@ -101,29 +103,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

5
app/models/admin_user.rb

@ -0,0 +1,5 @@
class AdminUser < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :recoverable, :rememberable, :validatable
end

2
app/models/bulk_upload.rb

@ -159,7 +159,7 @@ class BulkUpload
# property_reference: row[100],
beds: row[101],
unittype_gn: row[102],
property_building_type: row[103],
builtype: row[103],
wchair: row[104],
property_relet: row[105],
rsnvac: row[106],

20
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 }
@ -110,8 +110,14 @@ class CaseLog < ApplicationRecord
enum hbrentshortfall: DbEnums.polar_with_unknown, _suffix: true
enum property_relet: DbEnums.polar, _suffix: true
enum armedforces: DbEnums.armed_forces, _suffix: true
enum first_time_property_let_as_social_housing: DbEnums.polar, _suffix: true
enum unitletas: DbEnums.unitletas, _suffix: true
enum builtype: DbEnums.builtype, _suffix: true
AUTOGENERATED_FIELDS = %w[id status created_at updated_at discarded_at].freeze
OPTIONAL_FIELDS = %w[do_you_know_the_postcode
do_you_know_the_local_authority
first_time_property_let_as_social_housing].freeze
def self.editable_fields
attribute_names - AUTOGENERATED_FIELDS
@ -221,7 +227,7 @@ private
end
def mandatory_fields
required = attributes.except(*AUTOGENERATED_FIELDS)
required = attributes.except(*(AUTOGENERATED_FIELDS + OPTIONAL_FIELDS))
dynamically_not_required = []
@ -233,6 +239,14 @@ private
dynamically_not_required << "incfreq"
end
if sale_or_letting == "Letting"
dynamically_not_required << "sale_completion_date"
end
if la.present?
dynamically_not_required << "why_dont_you_know_la"
end
if tenancy == "Secure (including flexible)"
dynamically_not_required << "tenancylength"
end

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 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 conditional_on
@conditional_on ||= form.conditional_question_conditions.select do |condition|
condition[:to] == id
end
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

10
app/validations/household_validations.rb

@ -5,12 +5,12 @@ module HouseholdValidations
if record.homeless == "No" && record.reasonpref == "Yes"
record.errors.add :reasonpref, "Can not be Yes if Not Homeless immediately prior to this letting has been selected"
elsif record.reasonpref == "Yes"
if !record.rp_homeless && !record.rp_insan_unsat && !record.rp_medwel && !record.rp_hardship && !record.rp_dontknow
record.errors.add :reasonable_preference_reason, "If reasonable preference is Yes, a reason must be given"
if [record.rp_homeless, record.rp_insan_unsat, record.rp_medwel, record.rp_hardship, record.rp_dontknow].none? { |a| a == "Yes" }
record.errors.add :reasonable_preference_reason, 'If reasonable preference is "Yes", a reason must be given'
end
elsif record.reasonpref == "No"
if record.rp_homeless || record.rp_insan_unsat || record.rp_medwel || record.rp_hardship || record.rp_dontknow
record.errors.add :reasonable_preference_reason, "If reasonable preference is No, no reasons should be given"
if [record.rp_homeless, record.rp_insan_unsat, record.rp_medwel, record.rp_hardship, record.rp_dontknow].any? { |a| a == "Yes" }
record.errors.add :reasonable_preference_reason, 'If reasonable preference is "No", no reasons should be given'
end
end
end
@ -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

2
app/validations/soft_validations.rb

@ -40,6 +40,6 @@ private
def net_income_in_soft_min_range?
return unless weekly_net_income && ecstat1
weekly_net_income.between?(applicable_income_range.soft_min, applicable_income_range.hard_min)
weekly_net_income.between?(applicable_income_range.hard_min, applicable_income_range.soft_min)
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>

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

@ -1,14 +1,14 @@
<%= turbo_frame_tag "case_log_form", target: "_top" do %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<h1 class="govuk-heading-xl">Tasklist for log
<h1 class="govuk-heading-l">Tasklist for log
<%= @case_log.id %></h1>
<h2 class="govuk-heading-s govuk-!-margin-bottom-2">This submission is
<%= @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"

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

@ -1,6 +1,6 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">
<h1 class="govuk-heading-xl">Your logs</h1>
<h1 class="govuk-heading-l">Your logs</h1>
</div>
<div class="govuk-grid-column-two-thirds-from-desktop">

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: page_header.nil? ? "l" : "m", tag: page_header.nil? ? "h2" : "h1" },
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: page_header.nil? ? "l" : "m", tag: page_header.nil? ? "h2" : "h1" },
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: page_header.nil? ? "l" : "m", tag: page_header.nil? ? "h2" : "h1" },
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: page_header.nil? ? "l" : "m", tag: page_header.nil? ? "h2" : "h1" },
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, size: page_header.nil? ? "l" : "m", tag: page_header.nil? ? "h2" : "h1" },
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: page_header.nil? ? "l" : "m", tag: page_header.nil? ? "h2" : "h1" },
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| %>

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

@ -5,24 +5,27 @@
<%= 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? %>
<h1 class="govuk-heading-xl">
<%= page_info["header"] %>
<span class="govuk-caption-l">
<%= subsection %>
</span>
<% if page.header.present? %>
<h1 class="govuk-heading-l">
<%= 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, page_header: page.header, 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>

281
config/forms/2021_2022.json

@ -87,8 +87,8 @@
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Yes",
"1": "No"
"0": "No",
"1": "Yes"
}
}
},
@ -1107,13 +1107,66 @@
"label": "Property information",
"depends_on": { "about_this_log": "completed" },
"pages": {
"property_location": {
"property_reference": {
"header": "",
"description": "",
"questions": {
"propcode": {
"check_answer_label": "What’s the property reference?",
"header": "What's the property reference?",
"hint_text": "",
"type": "text"
}
}
},
"property_postcode": {
"header": "",
"description": "",
"questions": {
"do_you_know_the_postcode": {
"check_answer_label": "Do you know the property postcode?",
"header": "Do you know the postcode?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "No",
"1": "Yes"
},
"conditional_for": {
"postcode": ["Yes"]
}
},
"postcode": {
"check_answer_label": "",
"header": "",
"hint_text": "",
"type": "text"
}
}
},
"do_you_know_the_local_authority": {
"header": "",
"description": "",
"questions": {
"do_you_know_the_local_authority": {
"check_answer_label": "Do you know what local authority the property is located in?",
"header": "Do you know what local authority the property is located in?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "No",
"1": "Yes"
}
}
}
},
"select_local_authority": {
"header": "",
"description": "",
"questions": {
"la": {
"check_answer_label": "Property Location",
"header": "Property location",
"check_answer_label": "Local Authority",
"header": "Select a local authority",
"hint_text": "",
"type": "radio",
"answer_options": {
@ -1432,37 +1485,59 @@
"312": "York"
}
}
}
},
"depends_on": { "do_you_know_the_local_authority": "Yes" }
},
"property_postcode": {
"why_dont_you_know_la": {
"header": "",
"description": "",
"questions": {
"property_postcode": {
"check_answer_label": "What was the previous postcode?",
"header": "What is the property's postcode?",
"why_dont_you_know_la": {
"check_answer_label": "Reason for not knowing local authority",
"header": "Give a reason why you don't know the postcode or local authority",
"hint_text": "",
"type": "text"
}
}
},
"depends_on": { "do_you_know_the_local_authority": "No" }
},
"property_relet": {
"first_time_property_let_as_social_housing": {
"header": "",
"description": "",
"questions": {
"property_relet": {
"first_time_property_let_as_social_housing": {
"check_answer_label": "Which type was the property most recently let as?",
"header": "Is this property a relet?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Yes",
"1": "No"
"0": "No",
"1": "Yes"
}
}
}
},
"depends_on": { "tenant_same_property_renewal": "No"}
},
"property_vacancy_reason": {
"unitletas": {
"header": "",
"description": "",
"questions": {
"unitletas": {
"check_answer_label": "Type property most recently let as",
"header": "Which type was the property most recently let as?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Social rent basis",
"1": "Affordable rent basis",
"2": "Intermediate rent basis",
"3": "Do not know"
}
}
},
"depends_on": { "first_time_property_let_as_social_housing": "No", "tenant_same_property_renewal": "No" }
},
"property_vacancy_reason_not_first_let": {
"header": "",
"description": "",
"questions": {
@ -1471,36 +1546,72 @@
"header": "What is the reason for the property vacancy?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "First let of newbuild property",
"1": "First let of conversion/rehabilitation/acquired property",
"2": "First let of leased property",
"3": "Relet - tenant evicted due to arrears",
"4": "Relet - tenant evicted due to ASB or other reason",
"5": "Relet - tenant died (no succession)",
"6": "Relet - tenant moved to other social housing provider",
"7": "Relet - tenant abandoned property",
"8": "Relet - tenant moved to private sector or other accommodation",
"9": "Relet - to tenant who occupied same property as temporary accommodation",
"10": "Relet – internal transfer (excluding renewals of a fixed-term tenancy)",
"11": "Relet – renewal of fixed-term tenancy",
"12": "Relet – tenant moved to care home",
"13": "Relet – tenant involved in a succession downsize"
"answer_options": {
"0": "Renewal of fixed-term tenancy",
"1": "Internal transfer (excluding renewals of a fixed-term tenancy)",
"2": "Relet to tenant who occupied same property as temporary accommodation",
"3": "Tenant involved in a succession downsize",
"4": "Tenant moved to private sector or other accommodation",
"5": "Tenant moved to other social housing provider",
"6": "Tenant moved to care home",
"7": "Tenant abandoned property",
"8": "Tenant evicted due to arrears",
"9": "Tenant evicted due to ASB or other reason",
"10": "Previous tenant passed away (no succession)"
}
}
}
},
"depends_on": { "first_time_property_let_as_social_housing": "No", "tenant_same_property_renewal": "No" }
},
"property_reference": {
"property_vacancy_reason_first_let": {
"header": "",
"description": "",
"questions": {
"propcode": {
"check_answer_label": "What’s the property reference?",
"header": "What's the property reference?",
"rsnvac": {
"check_answer_label": "What is the reason for the property vacancy?",
"header": "What is the reason for the property vacancy?",
"hint_text": "",
"type": "text"
"type": "radio",
"answer_options": {
"11": "First let of newbuild property",
"12": "First let of conversion/rehabilitation/acquired property",
"13": "First let of leased property"
}
}
}
},
"depends_on": { "first_time_property_let_as_social_housing": "Yes", "tenant_same_property_renewal": "No" }
},
"property_number_of_times_relet_not_social_let": {
"header": "",
"description": "",
"questions": {
"offered": {
"check_answer_label": "How many times has this unit been previously offered since becoming available for relet since becoming available for relet (after the last tenancy ended)?",
"header": "How many times has this unit been previously offered since becoming available for relet since becoming available for relet (after the last tenancy ended)?",
"hint_text": "If the property is being let for the first time, enter 0",
"type": "numeric",
"min": 0,
"max": 150,
"step": 1
}
},
"depends_on": { "first_time_property_let_as_social_housing": "No", "tenant_same_property_renewal": "No" }
},
"property_number_of_times_relet_social_let": {
"header": "",
"description": "",
"questions": {
"offered": {
"check_answer_label": "How many times has the property been previously offered since becoming available?",
"header": "How many times has the property been previously offered since becoming available?",
"hint_text": "If the property is being let for the first time, enter 0",
"type": "numeric",
"min": 0,
"max": 150,
"step": 1
}
},
"depends_on": { "first_time_property_let_as_social_housing": "Yes", "tenant_same_property_renewal": "No" }
},
"property_unit_type": {
"header": "",
@ -1524,6 +1635,38 @@
}
}
},
"property_building_type": {
"header": "",
"description": "",
"questions": {
"builtype": {
"check_answer_label": "Building type",
"header": "Which type of building is the property?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Purpose built",
"1": "Converted from previous residential or non-residential property"
}
}
}
},
"property_wheelchair_accessible": {
"header": "",
"description": "",
"questions": {
"wchair": {
"check_answer_label": "Is property built or adapted to wheelchair user standards?",
"header": "Is property built or adapted to wheelchair user standards?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Yes",
"1": "No"
}
}
}
},
"property_number_of_bedrooms": {
"header": "",
"description": "",
@ -1537,7 +1680,21 @@
"max": 150,
"step": 1
}
}
},
"depends_on": { "needstype": "General Needs" }
},
"void_or_renewal_date": {
"header": "",
"description": "",
"questions": {
"beds": {
"check_answer_label": "void/renewal date",
"header": "What is the void or renewal date?",
"hint_text": "",
"type": "date"
}
},
"depends_on": { "rsnvac": "First let of newbuild property", "tenant_same_property_renewal": "No" }
},
"property_major_repairs": {
"header": "",
@ -1549,12 +1706,13 @@
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Yes",
"1": "No"
"0": "No",
"1": "Yes"
},
"conditional_for": {
"mrcdate": ["Yes"]
}
},
"depends_on": { "tenant_same_property_renewal": "No" }
},
"mrcdate": {
"check_answer_label": "What was the major repairs completion date?",
@ -1562,35 +1720,28 @@
"hint_text": "For example, 27 3 2007",
"type": "date"
}
}
},
"property_number_of_times_relet": {
"header": "",
"description": "",
"questions": {
"offered": {
"check_answer_label": "How many times has this unit been previously offered since becoming available for relet since the last tenancy ended or as a first let?",
"header": "How many times has this unit been previously offered since becoming available for relet since the last tenancy ended or as a first let? ",
"hint_text": "For an Affordable Rent or Intermediate Rent Letting, only include number of offers as that type. For a property let at the first attempt enter '0' ",
"type": "numeric",
"min": 0,
"max": 150,
"step": 1
}
}
},
"depends_on": { "rsnvac": "First let of newbuild property" }
},
"property_wheelchair_accessible": {
"new_build_handover_date": {
"header": "",
"description": "",
"questions": {
"wchair": {
"check_answer_label": "Is property built or adapted to wheelchair user standards?",
"header": "Is property built or adapted to wheelchair user standards?",
"majorrepairs": {
"check_answer_label": "Were major repairs carried out during the void period?",
"header": "Were any major repairs completed during the void period?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Yes",
"1": "No"
"0": "No",
"1": "Yesx"
},
"conditional_for": {
"mrcdate": ["Yes"]
},
"depends_on": {
"tenant_same_property_renewal": "No",
"rsnvac": ["First let of conversion, rehabilitation or acquired property?", "First let of leased property"]
}
}
}

4
config/initializers/active_admin.rb

@ -54,7 +54,7 @@ ActiveAdmin.setup do |config|
#
# This setting changes the method which Active Admin calls
# within the application controller.
# config.authentication_method = :authenticate_admin_user!
config.authentication_method = :authenticate_admin_user!
# == User Authorization
#
@ -91,7 +91,7 @@ ActiveAdmin.setup do |config|
#
# This setting changes the method which Active Admin calls
# (within the application controller) to return the currently logged in user.
# config.current_user_method = :current_admin_user
config.current_user_method = :current_admin_user
# == Logging Out
#

13
config/routes.rb

@ -1,5 +1,6 @@
Rails.application.routes.draw do
devise_for :users, :controllers => { passwords: "users/passwords" }, :skip => [:registrations]
devise_for :admin_users, ActiveAdmin::Devise.config
devise_for :users, controllers: { passwords: "users/passwords" }, :skip => [:registrations]
devise_scope :user do
get "confirmations/reset", to: "users/passwords#reset_confirmation"
get 'users/edit' => 'devise/registrations#edit', :as => 'edit_user_registration'
@ -27,13 +28,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
db/migrate/20211119104835_add_property_info_fields.rb

@ -0,0 +1,10 @@
class AddPropertyInfoFields < ActiveRecord::Migration[6.1]
def change
change_table :case_logs, bulk: true do |t|
t.column :first_time_property_let_as_social_housing, :int
t.column :why_dont_you_know_la, :string
t.column :type_property_most_recently_let_as, :string
t.column :builtype, :string
end
end
end

18
db/migrate/20211119120910_add_admin_users.rb

@ -0,0 +1,18 @@
class AddAdminUsers < ActiveRecord::Migration[6.1]
def change
create_table :admin_users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
t.timestamps null: false
end
end
end

19
db/migrate/20211124152204_change_recently_let_as_to_enum.rb

@ -0,0 +1,19 @@
class ChangeRecentlyLetAsToEnum < ActiveRecord::Migration[6.1]
def up
change_table :case_logs, bulk: true do |t|
t.remove :type_property_most_recently_let_as
t.column :unitletas, :int
t.remove :builtype
t.column :builtype, :int
end
end
def down
change_table :case_logs, bulk: true do |t|
t.remove :unitletas
t.column :type_property_most_recently_let_as, :string
t.remove :builtype
t.remove :builtype, :string
end
end
end

9
db/migrate/20211124174732_remove_old_fields.rb

@ -0,0 +1,9 @@
class RemoveOldFields < ActiveRecord::Migration[6.1]
def up
remove_column :case_logs, :property_building_type
end
def down
add_column :case_logs, :property_building_type, :string
end
end

15
db/schema.rb

@ -15,6 +15,16 @@ ActiveRecord::Schema.define(version: 2021_11_25_114400) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "admin_users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "case_logs", force: :cascade do |t|
t.integer "status", default: 0
t.datetime "created_at", precision: 6, null: false
@ -72,7 +82,6 @@ ActiveRecord::Schema.define(version: 2021_11_25_114400) do
t.string "previous_postcode"
t.integer "rsnvac"
t.integer "unittype_gn"
t.string "property_building_type"
t.integer "beds"
t.string "property_void_date"
t.integer "offered"
@ -153,6 +162,10 @@ ActiveRecord::Schema.define(version: 2021_11_25_114400) do
t.datetime "sale_completion_date"
t.datetime "startdate"
t.integer "armedforces"
t.integer "first_time_property_let_as_social_housing"
t.string "why_dont_you_know_la"
t.integer "unitletas"
t.integer "builtype"
t.index ["discarded_at"], name: "index_case_logs_on_discarded_at"
end

1
db/seeds.rb

@ -7,3 +7,4 @@
# Character.create(name: 'Luke', movie: movies.first)
User.create!(email: "test@example.com", password: "password")
AdminUser.create!(email: "admin@example.com", password: "password")

9
docs/adr/adr-010-admin-users-vs-users.md

@ -0,0 +1,9 @@
### ADR - 010: Admin Users vs Users
#### Why do we have 2 User classes, AdminUser and User?
This is modelling a real life split. `AdminUsers` are internal DLUHC users or helpdesk employees. While `Users` are external users working at data providing organisations. So local authority/housing association's "admin" users, i.e. Data Co-ordinators are a type of the User class. They have the ability to add or remove other users to or from their organisation, and to update their organisation details etc, but only through the designed UI. They do not get direct access to ActiveAdmin.
AdminUsers on the other hand get direct access to ActiveAdmin. From there they can download entire datasets (via CSV, XML, JSON), view any log from any organisation, and add or remove users of any type including other Admin users. This means TDA will likely also require more stringent authentication for them using MFA (which users will likely not require). So the class split also helps there.
A potential downside to this approach is that it does not currently allow for `AdminUsers` to sign into the application UI itself with their Admin credentials. However, we need to see if there's an actual use case for this and what it would be (since they aren't part of an organisation to be uploading data for, but could add or amend data or user or org details through ActiveAdmin anyway). If there is a strong use case for it this could be work around by either: providing them with two sets of credentials, or modifying the `authenticate_user` method to also check `AdminUser` credentials.

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.

2
docs/api/DLUHC-CORE-Data.v1.json

@ -317,7 +317,7 @@
"property_building_type": "dummy",
"beds": 3,
"property_void_date": "03/11/2019",
"majorrepairs": "Yes",
"majorrepairs": "No",
"mrcdate": "05/05/2020",
"offered": 2,
"wchair": true,

30
spec/controllers/admin/admin_users_controller_spec.rb

@ -0,0 +1,30 @@
require "rails_helper"
require_relative "../../support/devise"
describe Admin::AdminUsersController, type: :controller do
render_views
let(:page) { Capybara::Node::Simple.new(response.body) }
let(:resource_title) { "Admin Users" }
let(:valid_session) { {} }
login_admin_user
describe "Get admin users" do
before do
get :index, session: valid_session
end
it "returns a table of admin users" do
expect(page).to have_content(resource_title)
expect(page).to have_table("index_table_admin_users")
expect(page).to have_link(AdminUser.first.id.to_s)
end
end
describe "Create admin users" do
let(:params) { { admin_user: { email: "test2@example.com", password: "pAssword1" } } }
it "creates a new admin users" do
expect { post :create, session: valid_session, params: params }.to change(AdminUser, :count).by(1)
end
end
end

29
spec/controllers/admin/case_logs_controller_spec.rb

@ -0,0 +1,29 @@
require "rails_helper"
require_relative "../../support/devise"
describe Admin::CaseLogsController, type: :controller do
render_views
let(:page) { Capybara::Node::Simple.new(response.body) }
let(:resource_title) { "Case Logs" }
let(:valid_session) { {} }
login_admin_user
describe "Get case logs" do
let!(:case_log) { FactoryBot.create(:case_log, :in_progress) }
before do
get :index, session: valid_session
end
it "returns a table of case logs" do
expect(page).to have_content(resource_title)
expect(page).to have_table("index_table_case_logs")
expect(page).to have_link(case_log.id.to_s)
end
end
describe "Create case logs" do
it "creates a new case log" do
expect { post :create, session: valid_session }.to change(CaseLog, :count).by(1)
end
end
end

41
spec/controllers/admin/dashboard_controller_spec.rb

@ -0,0 +1,41 @@
require "rails_helper"
require_relative "../../support/devise"
describe Admin::DashboardController, type: :controller do
render_views
let(:page) { Capybara::Node::Simple.new(response.body) }
let(:resource_title) { "Dashboard" }
let!(:case_log) { FactoryBot.create(:case_log, :in_progress) }
let!(:case_log_2) { FactoryBot.create(:case_log, :in_progress) }
let!(:completed_case_log) { FactoryBot.create(:case_log, :completed) }
let(:valid_session) { {} }
login_admin_user
describe "Get case logs" do
before do
get :index, session: valid_session
end
it "returns a dashboard page" do
expect(page).to have_content(resource_title)
end
it "returns a panel of recent case logs" do
expect(page).to have_xpath("//div[contains(@class, 'panel') and contains(//h3, 'Recent Case Logs')]")
end
it "returns a panel of in progress case logs" do
panel_xpath = "//div[@class='panel' and .//h3[contains(., 'Total case logs in progress')]]"
panel_content_xpath = "#{panel_xpath}//div[@class='panel_contents' and .//p[contains(., 2)]]"
expect(page).to have_xpath(panel_xpath)
expect(page).to have_xpath(panel_content_xpath)
end
it "returns a panel of completed case logs" do
panel_xpath = "//div[@class='panel' and .//h3[contains(., 'Total case logs completed')]]"
panel_content_xpath = "#{panel_xpath}//div[@class='panel_contents' and .//p[contains(., 1)]]"
expect(page).to have_xpath(panel_xpath)
expect(page).to have_xpath(panel_content_xpath)
end
end
end

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

8
spec/factories/admin_user.rb

@ -0,0 +1,8 @@
FactoryBot.define do
factory :admin_user do
email { "admin@example.com" }
password { "pAssword1" }
created_at { Time.zone.now }
updated_at { Time.zone.now }
end
end

9
spec/factories/case_log.rb

@ -54,14 +54,13 @@ FactoryBot.define do
lettype { "Affordable Rent - General Needs" }
landlord { "This landlord" }
previous_postcode { "SE2 6RT" }
rsnvac { "Relet - tenant abandoned property" }
rsnvac { "Tenant abandoned property" }
unittype_gn { "House" }
property_building_type { "dummy" }
beds { 3 }
property_void_date { "03/11/2019" }
offered { 2 }
wchair { "Yes" }
earnings { 60 }
earnings { 68 }
incfreq { "Weekly" }
benefits { "Some" }
period { "Fortnightly" }
@ -133,8 +132,10 @@ FactoryBot.define do
mrcyear { 2020 }
incref { 554_355 }
sale_completion_date { nil }
startdate { nil }
startdate { Time.zone.now }
armedforces { 1 }
builtype { 1 }
unitletas { 2 }
end
created_at { Time.zone.now }
updated_at { Time.zone.now }

1
spec/factories/user.rb

@ -1,6 +1,5 @@
FactoryBot.define do
factory :user do
sequence(:id) { |i| i }
email { "test@example.com" }
password { "pAssword1" }
created_at { Time.zone.now }

45
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
@ -93,49 +93,6 @@ RSpec.describe "Form Features" do
)
end
context "Validate pregnancy questions" do
it "Cannot answer yes if no female tenants" do
expect {
CaseLog.create!(preg_occ: "Yes",
sex1: "Male",
age1: 20)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "Cannot answer yes if no female tenants within age range" do
expect {
CaseLog.create!(preg_occ: "Yes",
sex1: "Female",
age1: 51)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "Cannot answer prefer not to say if no valid tenants" do
expect {
CaseLog.create!(preg_occ: "Prefer not to say",
sex1: "Male",
age1: 20)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "Can answer yes if valid tenants" do
expect {
CaseLog.create!(preg_occ: "Yes",
sex1: "Female",
age1: 20)
}.not_to raise_error
end
it "Can answer yes if valid second tenant" do
expect {
CaseLog.create!(preg_occ: "Yes",
sex1: "Male", age1: 99,
sex2: "Female",
age2: 20)
}.not_to raise_error
end
end
it "can be accessed by url" do
visit("/case_logs/#{id}/person_1_age")
expect(page).to have_field("case-log-age1-field")

17
spec/fixtures/complete_case_log.json vendored

@ -7,6 +7,8 @@
"national": "UK national resident in UK",
"prevten": "Private sector tenancy",
"armedforces": "A current or former regular in the UK Armed Forces (exc. National Service)",
"armed_forces": "A current or former regular in the UK Armed Forces (exc. National Service)",
"armed_forces_partner": "",
"ecstat1": "Full-time - 30 hours or more",
"other_hhmemb": 7,
"hhmemb": 8,
@ -57,13 +59,13 @@
"la": "Barnet",
"property_postcode": "NW1 5TY",
"property_relet": "No",
"rsnvac": "Relet - tenant abandoned property",
"rsnvac": "First let of newbuild property",
"property_reference": "P9876",
"unittype_gn": "House",
"property_building_type": "dummy",
"beds": 3,
"property_void_date": "03/11/2019",
"majorrepairs": "Yes",
"majorrepairs": "No",
"mrcdate": "05/05/2020",
"mrcday": 5,
"mrcmonth": 5,
@ -124,7 +126,6 @@
"property_owner_organisation": "",
"property_manager_organisation": "",
"sale_or_letting": "",
"tenant_same_property_renewal": "",
"rent_type": "",
"intermediate_rent_product_name": "",
"needs_type": "",
@ -134,6 +135,14 @@
"postcode": "a1",
"postcod2": "w3",
"ppostc1": "w3",
"ppostc2": "w3"
"ppostc2": "w3",
"why_dont_you_know_la": "Forgot",
"first_time_property_let_as_social_housing": "Yes",
"unitletas": "Affordable rent basis",
"builtype": "Purpose built",
"property_wheelchair_accessible": "Yes",
"void_or_renewal_date": "05/05/2020",
"tenant_same_property_renewal": "Yes",
"new_build_handover_date": "01/01/2019"
}
}

179
spec/fixtures/forms/test_aboutthislog.json vendored

@ -1,179 +0,0 @@
{
"form_type": "lettings",
"sections": {
"about_this_log": {
"label": "About this log",
"subsections": {
"about_this_log": {
"label": "About this log",
"pages": {
"gdpr_acceptance": {
"header": "DLUHC Privacy Notice Acceptance",
"description": "",
"questions": {
"gdpr_acceptance": {
"check_answer_label": "GDPR acceptance",
"header": "Has the tenant or buyer seen the DLUHC privacy notice?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Yes",
"1": "No"
}
}
},
"conditional_route_to": {
"organisation_details": { "gdpr_acceptance": "Yes" }
}
},
"gdpr_declined": {
"header": "You cannot use this service",
"hint_text": "",
"description": "We cannot accept data about a tenant or buyer unless they’ve seen the DLUHC privacy notice.",
"questions": {
}
},
"organisation_details": {
"header": "About this log",
"description": "Organisation Details",
"questions": {
"property_owner_organisation": {
"check_answer_label": "",
"header": "Which organisation owns this property?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "A",
"1": "B"
}
},
"property_manager_organisation": {
"check_answer_label": "",
"header": "Which organisation manages this property?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "A",
"1": "B"
}
}
}
},
"sale_or_letting": {
"header": "About this log",
"description": "Is this a sale or a letting?",
"questions": {
"sale_or_letting": {
"check_answer_label": "",
"header": "Is this a sale or a letting?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Sale",
"1": "Letting"
}
}
}
},
"tenant_same_property_renewal": {
"header": "About this log",
"description": "Is this a renewal to the same tenant in the same property?",
"questions": {
"tenant_same_property_renewal": {
"check_answer_label": "",
"header": "Is this a renewal to the same tenant in the same property?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Yes",
"1": "No"
}
}
},
"depends_on": { "sale_or_letting": "Letting" }
},
"tenancy_start_date": {
"header": "About this log",
"description": "",
"questions": {
"tenancy_start_date": {
"check_answer_label": "When is the tenancy start date?",
"header": "What is the tenancy start date?",
"hint_text": "For example, 27 3 2007",
"type": "date"
}
},
"depends_on": { "sale_or_letting": "Letting" }
},
"letting_type": {
"header": "About this log",
"description": "",
"questions": {
"rent_type": {
"check_answer_label": "What is the rent type?",
"header": "What is the rent type?",
"hint_text": "",
"type": "select",
"answer_options": {
"0": "Social Rent",
"1": "Affordable Rent",
"2": "London Affordable Rent",
"3": "Rent To Buy",
"4": "London Living Rent",
"5": "Other Intermediate Rent Product"
},
"conditional_for": {
"intermediate_rent_product_name": ["Other Intermediate Rent Product"]
}
},
"intermediate_rent_product_name": {
"check_answer_label": "Enter the product name",
"header": "What is intermediate rent product name?",
"type": "text"
},
"needs_type": {
"check_answer_label": "What is the needs type?",
"header": "What is the needs type?",
"hint_text": "",
"type": "select",
"answer_options": {
"0": "Supported Housing",
"1": "General Needs"
}
}
},
"depends_on": { "sale_or_letting": "Letting" }
},
"sale_completion_date": {
"header": "About this log",
"description": "",
"questions": {
"sale_completion_date": {
"check_answer_label": "What is the sale completion date?",
"header": "What is the sale completion date?",
"hint_text": "For example, 27 3 2007",
"type": "date"
}
},
"depends_on": { "sale_or_letting": "Sale" }
},
"purchaser_code": {
"header": "About this log",
"description": "",
"questions": {
"purchaser_code": {
"check_answer_label": "What is the purchaser code?",
"header": "What is the purchaser code?",
"hint_text": "",
"type": "text"
}
},
"depends_on": { "sale_or_letting": "Sale" }
}
}
}
}
}
}
}

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

2
spec/lib/tasks/form_definition_validator_spec.rb

@ -11,7 +11,7 @@ describe "rake form_definition:validate_all", type: :task do
end
it "runs the validate task for each form definition in the project" do
expect(Rake::Task["form_definition:validate"]).to receive(:invoke).exactly(4).times
expect(Rake::Task["form_definition:validate"]).to receive(:invoke).exactly(3).times
task.invoke
end
end

101
spec/models/case_log_spec.rb

@ -26,8 +26,8 @@ RSpec.describe Form, type: :model do
expect { CaseLog.create!(offered: 0) }.to raise_error(ActiveRecord::RecordInvalid)
end
context "reasonable preference validation" do
it "if given reasonable preference is yes a reason must be selected" do
context "reasonable preference is yes" do
it "validates a reason must be selected" do
expect {
CaseLog.create!(reasonpref: "Yes",
rp_homeless: nil,
@ -38,7 +38,7 @@ RSpec.describe Form, type: :model do
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "if not previously homeless reasonable preference should not be selected" do
it "validates that previously homeless should be selected" do
expect {
CaseLog.create!(
homeless: "No",
@ -46,17 +46,16 @@ RSpec.describe Form, type: :model do
)
}.to raise_error(ActiveRecord::RecordInvalid)
end
end
it "if not given reasonable preference a reason should not be selected" do
context "reasonable preference is no" do
it "validates no reason is needed" do
expect {
CaseLog.create!(
homeless: "Yes - other homelessness",
reasonpref: "No",
rp_homeless: "Yes",
)
}.to raise_error(ActiveRecord::RecordInvalid)
CaseLog.create!(reasonpref: "No", rp_homeless: "No")
}.not_to raise_error
end
end
context "reason for leaving last settled home validation" do
it "Reason for leaving must be don't know if reason for leaving settled home (Q9a) is don't know." do
expect {
@ -97,6 +96,49 @@ RSpec.describe Form, type: :model do
end
end
context "Validate pregnancy questions" do
it "Cannot answer yes if no female tenants" do
expect {
CaseLog.create!(preg_occ: "Yes",
sex1: "Male",
age1: 20)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "Cannot answer yes if no female tenants within age range" do
expect {
CaseLog.create!(preg_occ: "Yes",
sex1: "Female",
age1: 51)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "Cannot answer prefer not to say if no valid tenants" do
expect {
CaseLog.create!(preg_occ: "Prefer not to say",
sex1: "Male",
age1: 20)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "Can answer yes if valid tenants" do
expect {
CaseLog.create!(preg_occ: "Yes",
sex1: "Female",
age1: 20)
}.not_to raise_error
end
it "Can answer yes if valid second tenant" do
expect {
CaseLog.create!(preg_occ: "Yes",
sex1: "Male", age1: 99,
sex2: "Female",
age2: 20)
}.not_to raise_error
end
end
context "Shared accomodation bedrooms validation" do
it "you must have more than zero bedrooms" do
expect {
@ -325,12 +367,43 @@ RSpec.describe Form, type: :model do
)
}.to raise_error(ActiveRecord::RecordInvalid)
end
context "given an income in upper soft validation range" do
let(:case_log) do
FactoryBot.create(:case_log,
ecstat1: "Full-time - 30 hours or more",
earnings: 750,
incfreq: "Weekly")
end
it "updates soft errors" do
expect(case_log.has_no_unresolved_soft_errors?).to be false
expect(case_log.soft_errors["override_net_income_validation"].message)
.to match(/Net income is higher than expected/)
end
end
context "given an income in lower soft validation range" do
let(:case_log) do
FactoryBot.create(:case_log,
ecstat1: "Full-time - 30 hours or more",
earnings: 120,
incfreq: "Weekly")
end
it "updates soft errors" do
expect(case_log.has_no_unresolved_soft_errors?).to be false
expect(case_log.soft_errors["override_net_income_validation"].message)
.to match(/Net income is lower than expected/)
end
end
end
end
describe "status" do
let!(:empty_case_log) { FactoryBot.create(:case_log) }
let!(:in_progress_case_log) { FactoryBot.create(:case_log, :in_progress) }
let!(:completed_case_log) { FactoryBot.create(:case_log, :completed) }
it "is set to not started for an empty case log" do
expect(empty_case_log.not_started?).to be(true)
@ -338,11 +411,17 @@ RSpec.describe Form, type: :model do
expect(empty_case_log.completed?).to be(false)
end
it "is set to not started for an empty case log" do
it "is set to in progress for a started case log" do
expect(in_progress_case_log.in_progress?).to be(true)
expect(in_progress_case_log.not_started?).to be(false)
expect(in_progress_case_log.completed?).to be(false)
end
it "is set to completed for a completed case log" do
expect(completed_case_log.in_progress?).to be(false)
expect(completed_case_log.not_started?).to be(false)
expect(completed_case_log.completed?).to be(true)
end
end
describe "weekly_net_income" do

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")

8
spec/support/controller_macros.rb

@ -13,4 +13,12 @@ module ControllerMacros
sign_in user
end
end
def login_admin_user
before(:each) do
@request.env["devise.mapping"] = Devise.mappings[:admin_user]
admin_user = FactoryBot.create(:admin_user)
sign_in admin_user
end
end
end

Loading…
Cancel
Save