Browse Source

Merge branch 'main' into CLDC-576-ReproduceAboutThisLog

pull/78/head
Milo 3 years ago committed by GitHub
parent
commit
ffbc3064d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      Gemfile
  2. 50
      Gemfile.lock
  3. 20
      app/admin/case_logs.rb
  4. 32
      app/admin/dashboard.rb
  5. 27
      app/controllers/case_logs_controller.rb
  6. 7
      app/javascript/packs/active_admin.js
  7. 2
      app/javascript/packs/active_admin/print.scss
  8. 17
      app/javascript/stylesheets/active_admin.scss
  9. 179
      app/models/case_log.rb
  10. 9
      app/models/form.rb
  11. 15
      app/models/income_range.rb
  12. 37
      app/validations/financial_validations.rb
  13. 167
      app/validations/household_validations.rb
  14. 9
      app/validations/property_validations.rb
  15. 45
      app/validations/soft_validations.rb
  16. 21
      app/validations/tenancy_validations.rb
  17. 11
      app/views/form/_validation_override_question.html.erb
  18. 10
      app/views/form/page.html.erb
  19. 2
      config/application.rb
  20. 24
      config/forms/2021_2022.json
  21. 335
      config/initializers/active_admin.rb
  22. 3
      config/routes.rb
  23. 2
      config/webpack/environment.js
  24. 7
      config/webpack/plugins/jquery.js
  25. 9
      db/migrate/20211028083105_change_net_income_type.rb
  26. 5
      db/migrate/20211028090049_add_net_income_override.rb
  27. 7
      db/migrate/20211028095000_add_net_income_known_field.rb
  28. 4
      db/schema.rb
  29. 18
      docs/api/DLUHC-CORE-Data.v1.json
  30. 3
      package.json
  31. 14
      spec/controllers/case_logs_controller_spec.rb
  32. 2
      spec/factories/case_log.rb
  33. 74
      spec/features/case_log_spec.rb
  34. 1
      spec/fixtures/complete_case_log.json
  35. 9
      spec/fixtures/forms/test_form.json
  36. 115
      spec/models/case_log_spec.rb
  37. 12
      spec/requests/case_log_controller_spec.rb
  38. 50
      yarn.lock

4
Gemfile

@ -25,6 +25,10 @@ gem "govuk_design_system_formbuilder"
gem "hotwire-rails"
# Soft delete ActiveRecords objects
gem "discard"
# Administration framework
gem "activeadmin"
# Admin charts
gem "chartkick"
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console

50
Gemfile.lock

@ -86,6 +86,15 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
activeadmin (2.9.0)
arbre (~> 1.2, >= 1.2.1)
formtastic (>= 3.1, < 5.0)
formtastic_i18n (~> 0.4)
inherited_resources (~> 1.7)
jquery-rails (~> 4.2)
kaminari (~> 1.0, >= 1.2.1)
railties (>= 5.2, < 6.2)
ransack (~> 2.1, >= 2.1.1)
activejob (6.1.4.1)
activesupport (= 6.1.4.1)
globalid (>= 0.3.6)
@ -109,6 +118,9 @@ GEM
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
arbre (1.4.0)
activesupport (>= 3.0.0, < 6.2)
ruby2_keywords (>= 0.0.2, < 1.0)
ast (2.4.2)
bindex (0.8.1)
bootsnap (1.9.1)
@ -124,6 +136,7 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
chartkick (4.1.0)
childprocess (4.1.0)
coderay (1.1.3)
concurrent-ruby (1.1.9)
@ -148,6 +161,9 @@ GEM
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
ffi (1.15.4)
formtastic (4.0.0)
actionpack (>= 5.2.0)
formtastic_i18n (0.7.0)
globalid (0.5.2)
activesupport (>= 5.0)
govuk-components (2.1.4)
@ -159,15 +175,39 @@ GEM
activemodel (>= 6.0)
activesupport (>= 6.0)
deep_merge (~> 1.2.1)
has_scope (0.8.0)
actionpack (>= 5.2)
activesupport (>= 5.2)
hotwire-rails (0.1.3)
rails (>= 6.0.0)
stimulus-rails
turbo-rails
i18n (1.8.10)
concurrent-ruby (~> 1.0)
inherited_resources (1.13.0)
actionpack (>= 5.2, < 6.2)
has_scope (~> 0.6)
railties (>= 5.2, < 6.2)
responders (>= 2, < 4)
iniparse (1.5.0)
jbuilder (2.11.2)
activesupport (>= 5.0.0)
jquery-rails (4.4.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
kaminari (1.2.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.2.1)
kaminari-actionview (1.2.1)
actionview
kaminari-core (= 1.2.1)
kaminari-activerecord (1.2.1)
activerecord
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
listen (3.7.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
@ -240,10 +280,17 @@ GEM
thor (~> 1.0)
rainbow (3.0.0)
rake (13.0.6)
ransack (2.4.2)
activerecord (>= 5.2.4)
activesupport (>= 5.2.4)
i18n
rb-fsevent (0.11.0)
rb-inotify (0.10.1)
ffi (~> 1.0)
regexp_parser (2.1.1)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.5)
rubocop (1.21.0)
parallel (~> 1.10)
@ -275,6 +322,7 @@ GEM
rubocop (~> 1.0)
rubocop-ast (>= 1.1.0)
ruby-progressbar (1.11.0)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
sass (3.7.4)
sass-listen (~> 4.0.0)
@ -337,9 +385,11 @@ PLATFORMS
x86_64-linux
DEPENDENCIES
activeadmin
bootsnap (>= 1.4.4)
byebug
capybara
chartkick
database_cleaner-active_record
discard
dotenv-rails

20
app/admin/case_logs.rb

@ -0,0 +1,20 @@
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 = [:status, :tenant_code, :person_1_age, :person_1_gender, :tenant_ethnic_group, :tenant_nationality, :previous_housing_situation, :armed_forces, :person_1_economic_status, :household_number_of_other_members, :person_2_relationship, :person_2_age, :person_2_gender, :person_2_economic_status, :person_3_relationship, :person_3_age, :person_3_gender, :person_3_economic_status, :person_4_relationship, :person_4_age, :person_4_gender, :person_4_economic_status, :person_5_relationship, :person_5_age, :person_5_gender, :person_5_economic_status, :person_6_relationship, :person_6_age, :person_6_gender, :person_6_economic_status, :person_7_relationship, :person_7_age, :person_7_gender, :person_7_economic_status, :person_8_relationship, :person_8_age, :person_8_gender, :person_8_economic_status, :homelessness, :reason_for_leaving_last_settled_home, :benefit_cap_spare_room_subsidy, :armed_forces_active, :armed_forces_injured, :armed_forces_partner, :medical_conditions, :pregnancy, :accessibility_requirements, :condition_effects, :tenancy_code, :tenancy_start_date, :starter_tenancy, :fixed_term_tenancy, :tenancy_type, :letting_type, :letting_provider, :property_location, :previous_postcode, :property_relet, :property_vacancy_reason, :property_reference, :property_unit_type, :property_building_type, :property_number_of_bedrooms, :property_void_date, :property_major_repairs, :property_major_repairs_date, :property_number_of_times_relet, :property_wheelchair_accessible, :net_income, :net_income_frequency, :net_income_uc_proportion, :housing_benefit, :rent_frequency, :basic_rent, :service_charge, :personal_service_charge, :support_charge, :total_charge, :outstanding_amount, :time_lived_in_la, :time_on_la_waiting_list, :previous_la, :property_postcode, :reasonable_preference, :reasonable_preference_reason, :cbl_letting, :chr_letting, :cap_letting, :outstanding_rent_or_charges, :other_reason_for_leaving_last_settled_home, :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
index do
selectable_column
id_column
column :created_at
column :updated_at
column :status
column :tenant_code
column :property_postcode
actions
end
end

32
app/admin/dashboard.rb

@ -0,0 +1,32 @@
ActiveAdmin.register_page "Dashboard" do
menu priority: 1, label: proc { I18n.t("active_admin.dashboard") }
content title: proc { I18n.t("active_admin.dashboard") } do
columns do
column do
panel "Recent Case Logs" do
table_for CaseLog.order(updated_at: :desc).limit(10) do
column :id
column :created_at
column :updated_at
column :status
column :tenant_code
column :property_postcode
end
end
end
column do
panel "Total case logs in progress" do
para CaseLog.in_progress.size
end
panel "Total case logs completed" do
para CaseLog.completed.size
end
panel "Total case logs completed" do
pie_chart CaseLog.group(:status).size
end
end
end
end
end

27
app/controllers/case_logs_controller.rb

@ -56,16 +56,14 @@ class CaseLogsController < ApplicationController
def submit_form
form = FormHandler.instance.get_form("2021_2022")
@case_log = CaseLog.find(params[:id])
previous_page = params[:case_log][:previous_page]
questions_for_page = form.questions_for_page(previous_page)
responses_for_page = question_responses(questions_for_page)
@case_log.previous_page = previous_page
if @case_log.update(responses_for_page)
redirect_path = get_next_page_path(form, previous_page, @case_log)
@case_log.page = params[:case_log][:page]
responses_for_page = responses_for_page(@case_log.page)
if @case_log.update(responses_for_page) && @case_log.has_no_unresolved_soft_errors?
redirect_path = get_next_page_path(form, @case_log.page, @case_log)
redirect_to(send(redirect_path, @case_log))
else
page_info = form.all_pages[previous_page]
render "form/page", locals: { form: form, page_key: previous_page, page_info: page_info }, status: :unprocessable_entity
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
end
end
@ -101,9 +99,12 @@ private
API_ACTIONS = %w[create show update destroy].freeze
def question_responses(questions_for_page)
questions_for_page.each_with_object({}) do |(question_key, question_info), result|
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]
next unless question_params
if question_info["type"] == "checkbox"
question_info["answer_options"].keys.reject { |x| x.match(/divider/) }.each do |option|
result[option] = question_params.include?(option)
@ -129,8 +130,8 @@ private
params.require(:case_log).permit(CaseLog.editable_fields)
end
def get_next_page_path(form, previous_page, case_log = {})
content = form.all_pages[previous_page]
def get_next_page_path(form, page, case_log = {})
content = form.all_pages[page]
if content.key?("conditional_route_to")
content["conditional_route_to"].each do |route, conditions|
@ -139,6 +140,6 @@ private
end
end
end
form.next_page_redirect_path(previous_page)
form.next_page_redirect_path(page)
end
end

7
app/javascript/packs/active_admin.js

@ -0,0 +1,7 @@
// Load Active Admin's styles into Webpacker,
// see `active_admin.scss` for customization.
import "../stylesheets/active_admin";
import "@activeadmin/activeadmin";
import "chartkick/chart.js"

2
app/javascript/packs/active_admin/print.scss

@ -0,0 +1,2 @@
/* Active Admin Print Stylesheet */
@import "~@activeadmin/activeadmin/src/scss/print";

17
app/javascript/stylesheets/active_admin.scss

@ -0,0 +1,17 @@
// Sass variable overrides must be declared before loading up Active Admin's styles.
//
// To view the variables that Active Admin provides, take a look at
// `app/assets/stylesheets/active_admin/mixins/_variables.scss` in the
// Active Admin source.
//
// For example, to change the sidebar width:
// $sidebar-width: 242px;
// Active Admin's got SASS!
@import "~@activeadmin/activeadmin/src/scss/mixins";
@import "~@activeadmin/activeadmin/src/scss/base";
// Overriding any non-variable Sass must be done after the fact.
// For example, to change the default status-tag color:
//
// .status_tag { background: #6090DB; }

179
app/models/case_log.rb

@ -1,123 +1,20 @@
class CaseLogValidator < ActiveModel::Validator
# Methods to be used on save and continue need to be named 'validate_'
# followed by field name this is how the metaprogramming of the method
# name being call in the validate method works.
def validate_person_1_age(record)
if record.person_1_age && !/^[1-9][0-9]?$|^120$/.match?(record.person_1_age.to_s)
record.errors.add :person_1_age, "Tenant age must be between 0 and 120"
end
end
def validate_property_number_of_times_relet(record)
if record.property_number_of_times_relet && !/^[1-9]$|^0[1-9]$|^1[0-9]$|^20$/.match?(record.property_number_of_times_relet.to_s)
record.errors.add :property_number_of_times_relet, "Must be between 0 and 20"
end
end
def validate_reasonable_preference(record)
if record.homelessness == "No" && record.reasonable_preference == "Yes"
record.errors.add :reasonable_preference, "Can not be Yes if Not Homeless immediately prior to this letting has been selected"
elsif record.reasonable_preference == "Yes"
if !record.reasonable_preference_reason_homeless && !record.reasonable_preference_reason_unsatisfactory_housing && !record.reasonable_preference_reason_medical_grounds && !record.reasonable_preference_reason_avoid_hardship && !record.reasonable_preference_reason_do_not_know
record.errors.add :reasonable_preference_reason, "If reasonable preference is Yes, a reason must be given"
end
elsif record.reasonable_preference == "No"
if record.reasonable_preference_reason_homeless || record.reasonable_preference_reason_unsatisfactory_housing || record.reasonable_preference_reason_medical_grounds || record.reasonable_preference_reason_avoid_hardship || record.reasonable_preference_reason_do_not_know
record.errors.add :reasonable_preference_reason, "If reasonable preference is No, no reasons should be given"
end
end
end
def validate_other_reason_for_leaving_last_settled_home(record)
validate_other_field(record, "reason_for_leaving_last_settled_home", "other_reason_for_leaving_last_settled_home")
end
def validate_reason_for_leaving_last_settled_home(record)
if record.reason_for_leaving_last_settled_home == "Do not know" && record.benefit_cap_spare_room_subsidy != "Do not know"
record.errors.add :benefit_cap_spare_room_subsidy, "must be do not know if tenant’s main reason for leaving is do not know"
end
end
def validate_armed_forces_injured(record)
if (record.armed_forces == "Yes - a regular" || record.armed_forces == "Yes - a reserve") && record.armed_forces_injured.blank?
record.errors.add :armed_forces_injured, "You must answer the armed forces injury question if the tenant has served in the armed forces"
end
if (record.armed_forces == "No" || record.armed_forces == "Prefer not to say") && record.armed_forces_injured.present?
record.errors.add :armed_forces_injured, "You must not answer the armed forces injury question if the tenant has not served in the armed forces or prefer not to say was chosen"
end
end
def validate_outstanding_rent_amount(record)
if record.outstanding_rent_or_charges == "Yes" && record.outstanding_amount.blank?
record.errors.add :outstanding_amount, "You must answer the oustanding amout question if you have outstanding rent or charges."
end
if record.outstanding_rent_or_charges == "No" && record.outstanding_amount.present?
record.errors.add :outstanding_amount, "You must not answer the oustanding amout question if you don't have outstanding rent or charges."
end
end
EMPLOYED_STATUSES = ["Full-time - 30 hours or more", "Part-time - Less than 30 hours"].freeze
def validate_net_income_uc_proportion(record)
(1..8).any? do |n|
economic_status = record["person_#{n}_economic_status"]
is_employed = EMPLOYED_STATUSES.include?(economic_status)
relationship = record["person_#{n}_relationship"]
is_partner_or_main = relationship == "Partner" || (relationship.nil? && economic_status.present?)
if is_employed && is_partner_or_main && record.net_income_uc_proportion == "All"
record.errors.add :net_income_uc_proportion, "income is from Universal Credit, state pensions or benefits cannot be All if the tenant or the partner works part or full time"
end
end
end
def validate_armed_forces_active_response(record)
if record.armed_forces == "Yes - a regular" && record.armed_forces_active.blank?
record.errors.add :armed_forces_active, "You must answer the armed forces active question if the tenant has served as a regular in the armed forces"
end
if record.armed_forces != "Yes - a regular" && record.armed_forces_active.present?
record.errors.add :armed_forces_active, "You must not answer the armed forces active question if the tenant has not served as a regular in the armed forces"
end
end
def validate_household_pregnancy(record)
if (record.pregnancy == "Yes" || record.pregnancy == "Prefer not to say") && !women_of_child_bearing_age_in_household(record)
record.errors.add :pregnancy, "You must answer no as there are no female tenants aged 16-50 in the property"
end
end
def validate_fixed_term_tenancy(record)
is_present = record.fixed_term_tenancy.present?
is_in_range = record.fixed_term_tenancy.to_i.between?(2, 99)
is_secure = record.tenancy_type == "Fixed term – Secure"
is_ast = record.tenancy_type == "Fixed term – Assured Shorthold Tenancy (AST)"
conditions = [
{ condition: !(is_secure || is_ast) && is_present, error: "You must only answer the fixed term tenancy length question if the tenancy type is fixed term" },
{ condition: is_ast && !is_in_range, error: "Fixed term – Assured Shorthold Tenancy (AST) should be between 2 and 99 years" },
{ condition: is_secure && (!is_in_range && is_present), error: "Fixed term – Secure should be between 2 and 99 years or not specified" },
]
conditions.each { |condition| condition[:condition] ? (record.errors.add :fixed_term_tenancy, condition[:error]) : nil }
end
def validate_other_tenancy_type(record)
validate_other_field(record, "tenancy_type", "other_tenancy_type")
end
# Validations methods need to be called 'validate_<page_name>' to run on model save
# or 'validate_' to run on submit as well
include HouseholdValidations
include PropertyValidations
include FinancialValidations
include TenancyValidations
def validate(record)
# 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.
question_to_validate = options[:previous_page]
if question_to_validate
if respond_to?("validate_#{question_to_validate}")
public_send("validate_#{question_to_validate}", record)
end
page_to_validate = options[:page]
if page_to_validate
public_send("validate_#{page_to_validate}", record) if respond_to?("validate_#{page_to_validate}")
else
# This assumes that all methods in this class other than this one are
# validations to be run
validation_methods = public_methods(false) - [__callee__]
validation_methods = public_methods.select { |method| method.starts_with?("validate_") }
validation_methods.each { |meth| public_send(meth, record) }
end
end
@ -135,37 +32,23 @@ private
record.errors.add other_field.to_sym, "#{other_field_label} must not be provided if #{main_field_label} was not other"
end
end
def women_of_child_bearing_age_in_household(record)
(1..8).any? do |n|
next if record["person_#{n}_gender"].nil? || record["person_#{n}_age"].nil?
record["person_#{n}_gender"] == "Female" && record["person_#{n}_age"] >= 16 && record["person_#{n}_age"] <= 50
end
end
end
class CaseLog < ApplicationRecord
include Discard::Model
include SoftValidations
default_scope -> { kept }
scope :not_started, -> { where(status: "not_started") }
scope :in_progress, -> { where(status: "in_progress") }
scope :not_completed, -> { where.not(status: "completed") }
scope :completed, -> { where(status: "completed") }
validate :instance_validations
validates_with CaseLogValidator, ({ page: @page } || {})
before_save :update_status!
attr_writer :previous_page
attr_accessor :page
enum status: { "not_started" => 0, "in_progress" => 1, "completed" => 2 }
AUTOGENERATED_FIELDS = %w[id status created_at updated_at discarded_at].freeze
def instance_validations
validates_with CaseLogValidator, ({ previous_page: @previous_page } || {})
end
def self.editable_fields
attribute_names - AUTOGENERATED_FIELDS
end
@ -182,6 +65,23 @@ class CaseLog < ApplicationRecord
status == "in_progress"
end
def weekly_net_income
case net_income_frequency
when "Weekly"
net_income
when "Monthly"
((net_income * 12) / 52.0).round(0)
when "Yearly"
(net_income / 12.0).round(0)
end
end
def applicable_income_range
return unless person_1_economic_status
IncomeRange::ALLOWED[person_1_economic_status.to_sym]
end
private
def update_status!
@ -219,10 +119,27 @@ private
dynamically_not_required << "fixed_term_tenancy"
end
if tenancy_type != "Other"
unless net_income_in_soft_max_range? || net_income_in_soft_min_range?
dynamically_not_required << "override_net_income_validation"
end
unless tenancy_type == "Other"
dynamically_not_required << "other_tenancy_type"
end
unless net_income_known == "Yes"
dynamically_not_required << "net_income"
dynamically_not_required << "net_income_frequency"
end
start_range = (household_number_of_other_members || 0) + 2
(start_range..8).each do |n|
dynamically_not_required << "person_#{n}_age"
dynamically_not_required << "person_#{n}_gender"
dynamically_not_required << "person_#{n}_relationship"
dynamically_not_required << "person_#{n}_economic_status"
end
required.delete_if { |key, _value| dynamically_not_required.include?(key) }
end
end

9
app/models/form.rb

@ -41,6 +41,15 @@ class Form
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
end

15
app/models/income_range.rb

@ -0,0 +1,15 @@
class IncomeRange
ALLOWED = {
"Full-time - 30 hours or more": OpenStruct.new(soft_min: 143, soft_max: 730, hard_min: 90, hard_max: 1230),
"Part-time - Less than 30 hours": OpenStruct.new(soft_min: 67, soft_max: 620, hard_min: 50, hard_max: 950),
"In government training into work, such as New Deal": OpenStruct.new(soft_min: 80, soft_max: 480, hard_min: 40, hard_max: 990),
"Jobseeker": OpenStruct.new(soft_min: 50, soft_max: 370, hard_min: 10, hard_max: 450),
"Retired": OpenStruct.new(soft_min: 50, soft_max: 380, hard_min: 10, hard_max: 690),
"Not seeking work": OpenStruct.new(soft_min: 53, soft_max: 540, hard_min: 10, hard_max: 890),
"Full-time student": OpenStruct.new(soft_min: 47, soft_max: 460, hard_min: 10, hard_max: 1300),
"Unable to work because of long term sick or disability": OpenStruct.new(soft_min: 54, soft_max: 460, hard_min: 10, hard_max: 820),
"Child under 16": OpenStruct.new(soft_min: 50, soft_max: 450, hard_min: 10, hard_max: 750),
"Other": OpenStruct.new(soft_min: 50, soft_max: 580, hard_min: 10, hard_max: 1040),
"Prefer not to say": OpenStruct.new(soft_min: 47, soft_max: 730, hard_min: 10, hard_max: 1300),
}.freeze
end

37
app/validations/financial_validations.rb

@ -0,0 +1,37 @@
module FinancialValidations
# Validations methods need to be called 'validate_<page_name>' to run on model save
# or 'validate_' to run on submit as well
def validate_outstanding_rent_amount(record)
if record.outstanding_rent_or_charges == "Yes" && record.outstanding_amount.blank?
record.errors.add :outstanding_amount, "You must answer the oustanding amout question if you have outstanding rent or charges."
end
if record.outstanding_rent_or_charges == "No" && record.outstanding_amount.present?
record.errors.add :outstanding_amount, "You must not answer the oustanding amout question if you don't have outstanding rent or charges."
end
end
EMPLOYED_STATUSES = ["Full-time - 30 hours or more", "Part-time - Less than 30 hours"].freeze
def validate_net_income_uc_proportion(record)
(1..8).any? do |n|
economic_status = record["person_#{n}_economic_status"]
is_employed = EMPLOYED_STATUSES.include?(economic_status)
relationship = record["person_#{n}_relationship"]
is_partner_or_main = relationship == "Partner" || (relationship.nil? && economic_status.present?)
if is_employed && is_partner_or_main && record.net_income_uc_proportion == "All"
record.errors.add :net_income_uc_proportion, "income is from Universal Credit, state pensions or benefits cannot be All if the tenant or the partner works part or full time"
end
end
end
def validate_net_income(record)
return unless record.person_1_economic_status && record.weekly_net_income
if record.weekly_net_income > record.applicable_income_range.hard_max
record.errors.add :net_income, "Net income cannot be greater than #{record.applicable_income_range.hard_max} given the tenant's working situation"
end
if record.weekly_net_income < record.applicable_income_range.hard_min
record.errors.add :net_income, "Net income cannot be less than #{record.applicable_income_range.hard_min} given the tenant's working situation"
end
end
end

167
app/validations/household_validations.rb

@ -0,0 +1,167 @@
module HouseholdValidations
# Validations methods need to be called 'validate_<page_name>' to run on model save
# or 'validate_' to run on submit as well
def validate_reasonable_preference(record)
if record.homelessness == "No" && record.reasonable_preference == "Yes"
record.errors.add :reasonable_preference, "Can not be Yes if Not Homeless immediately prior to this letting has been selected"
elsif record.reasonable_preference == "Yes"
if !record.reasonable_preference_reason_homeless && !record.reasonable_preference_reason_unsatisfactory_housing && !record.reasonable_preference_reason_medical_grounds && !record.reasonable_preference_reason_avoid_hardship && !record.reasonable_preference_reason_do_not_know
record.errors.add :reasonable_preference_reason, "If reasonable preference is Yes, a reason must be given"
end
elsif record.reasonable_preference == "No"
if record.reasonable_preference_reason_homeless || record.reasonable_preference_reason_unsatisfactory_housing || record.reasonable_preference_reason_medical_grounds || record.reasonable_preference_reason_avoid_hardship || record.reasonable_preference_reason_do_not_know
record.errors.add :reasonable_preference_reason, "If reasonable preference is No, no reasons should be given"
end
end
end
def validate_other_reason_for_leaving_last_settled_home(record)
validate_other_field(record, "reason_for_leaving_last_settled_home", "other_reason_for_leaving_last_settled_home")
end
def validate_reason_for_leaving_last_settled_home(record)
if record.reason_for_leaving_last_settled_home == "Do not know" && record.benefit_cap_spare_room_subsidy != "Do not know"
record.errors.add :benefit_cap_spare_room_subsidy, "must be do not know if tenant’s main reason for leaving is do not know"
end
end
def validate_armed_forces_injured(record)
if (record.armed_forces == "Yes - a regular" || record.armed_forces == "Yes - a reserve") && record.armed_forces_injured.blank?
record.errors.add :armed_forces_injured, "You must answer the armed forces injury question if the tenant has served in the armed forces"
end
if (record.armed_forces == "No" || record.armed_forces == "Prefer not to say") && record.armed_forces_injured.present?
record.errors.add :armed_forces_injured, "You must not answer the armed forces injury question if the tenant has not served in the armed forces or prefer not to say was chosen"
end
end
def validate_armed_forces_active_response(record)
if record.armed_forces == "Yes - a regular" && record.armed_forces_active.blank?
record.errors.add :armed_forces_active, "You must answer the armed forces active question if the tenant has served as a regular in the armed forces"
end
if record.armed_forces != "Yes - a regular" && record.armed_forces_active.present?
record.errors.add :armed_forces_active, "You must not answer the armed forces active question if the tenant has not served as a regular in the armed forces"
end
end
def validate_household_pregnancy(record)
if (record.pregnancy == "Yes" || record.pregnancy == "Prefer not to say") && !women_of_child_bearing_age_in_household(record)
record.errors.add :pregnancy, "You must answer no as there are no female tenants aged 16-50 in the property"
end
end
def validate_household_number_of_other_members(record)
(2..8).each do |n|
validate_person_age(record, n)
validate_person_age_matches_economic_status(record, n)
validate_person_age_matches_relationship(record, n)
validate_person_age_and_gender_match_economic_status(record, n)
validate_person_age_and_relationship_matches_economic_status(record, n)
end
validate_partner_count(record)
end
def validate_person_1_age(record)
return unless record.person_1_age
if !record.person_1_age.is_a?(Integer) || record.person_1_age < 16 || record.person_1_age > 120
record.errors.add "person_1_age", "Tenant age must be an integer between 16 and 120"
end
end
def validate_person_1_economic(record)
validate_person_age_matches_economic_status(record, 1)
end
def validate_shared_housing_rooms(record)
unless record.property_unit_type.nil?
if record.property_unit_type == "Bed-sit" && record.property_number_of_bedrooms != 1
record.errors.add :property_unit_type, "A bedsit can only have one bedroom"
end
if !record.household_number_of_other_members.nil? && record.household_number_of_other_members.positive? && (record.property_unit_type.include?("Shared") && !record.property_number_of_bedrooms.to_i.between?(1, 7))
record.errors.add :property_unit_type, "A shared house must have 1 to 7 bedrooms"
end
if record.property_unit_type.include?("Shared") && !record.property_number_of_bedrooms.to_i.between?(1, 3)
record.errors.add :property_unit_type, "A shared house with less than two tenants must have 1 to 3 bedrooms"
end
end
end
private
def women_of_child_bearing_age_in_household(record)
(1..8).any? do |n|
next if record["person_#{n}_gender"].nil? || record["person_#{n}_age"].nil?
record["person_#{n}_gender"] == "Female" && record["person_#{n}_age"] >= 16 && record["person_#{n}_age"] <= 50
end
end
def validate_person_age(record, person_num)
age = record.public_send("person_#{person_num}_age")
return unless age
if !age.is_a?(Integer) || age < 1 || age > 120
record.errors.add "person_#{person_num}_age".to_sym, "Tenant age must be an integer between 0 and 120"
end
end
def validate_person_age_matches_economic_status(record, person_num)
age = record.public_send("person_#{person_num}_age")
economic_status = record.public_send("person_#{person_num}_economic_status")
return unless age && economic_status
if age > 70 && economic_status != "Retired"
record.errors.add "person_#{person_num}_economic_status", "Tenant #{person_num} must be retired if over 70"
end
if age < 16 && economic_status != "Child under 16"
record.errors.add "person_#{person_num}_economic_status", "Tenant #{person_num} economic status must be Child under 16 if their age is under 16"
end
end
def validate_person_age_matches_relationship(record, person_num)
age = record.public_send("person_#{person_num}_age")
relationship = record.public_send("person_#{person_num}_relationship")
return unless age && relationship
if age < 16 && relationship != "Child - includes young adult and grown-up"
record.errors.add "person_#{person_num}_relationship", "Tenant #{person_num}'s relationship to tenant 1 must be Child if their age is under 16"
end
end
def validate_person_age_and_relationship_matches_economic_status(record, person_num)
age = record.public_send("person_#{person_num}_age")
economic_status = record.public_send("person_#{person_num}_economic_status")
relationship = record.public_send("person_#{person_num}_relationship")
return unless age && economic_status && relationship
if age >= 16 && age <= 19 && relationship == "Child - includes young adult and grown-up" && (economic_status != "Full-time student" || economic_status != "Prefer not to say")
record.errors.add "person_#{person_num}_economic_status", "If age is between 16 and 19 - tenant #{person_num} must be a full time student or prefer not to say."
end
end
def validate_person_age_and_gender_match_economic_status(record, person_num)
age = record.public_send("person_#{person_num}_age")
gender = record.public_send("person_#{person_num}_gender")
economic_status = record.public_send("person_#{person_num}_economic_status")
return unless age && economic_status && gender
if gender == "Male" && economic_status == "Retired" && age < 65
record.errors.add "person_#{person_num}_age", "Male tenant who is retired must be 65 or over"
end
if gender == "Female" && economic_status == "Retired" && age < 60
record.errors.add "person_#{person_num}_age", "Female tenant who is retired must be 60 or over"
end
end
def validate_partner_count(record)
# TODO: probably need to keep track of which specific field is wrong so we can highlight it in the UI
partner_count = (2..8).count { |n| record.public_send("person_#{n}_relationship") == "Partner" }
if partner_count > 1
record.errors.add :base, "Number of partners cannot be greater than 1"
end
end
end

9
app/validations/property_validations.rb

@ -0,0 +1,9 @@
module PropertyValidations
# Validations methods need to be called 'validate_<page_name>' to run on model save
# or 'validate_' to run on submit as well
def validate_property_number_of_times_relet(record)
if record.property_number_of_times_relet && !/^[1-9]$|^0[1-9]$|^1[0-9]$|^20$/.match?(record.property_number_of_times_relet.to_s)
record.errors.add :property_number_of_times_relet, "Property number of times relet must be between 0 and 20"
end
end
end

45
app/validations/soft_validations.rb

@ -0,0 +1,45 @@
module SoftValidations
def has_no_unresolved_soft_errors?
soft_errors.empty? || soft_errors_overridden?
end
def soft_errors
{}.merge(net_income_validations)
end
def soft_errors_overridden?
public_send(soft_errors.keys.first) if soft_errors.present?
end
private
def net_income_validations
net_income_errors = {}
if net_income_in_soft_min_range?
net_income_errors["override_net_income_validation"] = OpenStruct.new(
message: "Net income is lower than expected based on the main tenant's working situation. Are you sure this is correct?",
hint_text: "This is based on the tenant's work situation: #{person_1_economic_status}",
)
elsif net_income_in_soft_max_range?
net_income_errors["override_net_income_validation"] = OpenStruct.new(
message: "Net income is higher than expected based on the main tenant's working situation. Are you sure this is correct?",
hint_text: "This is based on the tenant's work situation: #{person_1_economic_status}",
)
else
update_column(:override_net_income_validation, nil)
end
net_income_errors
end
def net_income_in_soft_max_range?
return unless weekly_net_income && person_1_economic_status
weekly_net_income.between?(applicable_income_range.soft_max, applicable_income_range.hard_max)
end
def net_income_in_soft_min_range?
return unless weekly_net_income && person_1_economic_status
weekly_net_income.between?(applicable_income_range.soft_min, applicable_income_range.hard_min)
end
end

21
app/validations/tenancy_validations.rb

@ -0,0 +1,21 @@
module TenancyValidations
# Validations methods need to be called 'validate_<page_name>' to run on model save
# or 'validate_' to run on submit as well
def validate_fixed_term_tenancy(record)
is_present = record.fixed_term_tenancy.present?
is_in_range = record.fixed_term_tenancy.to_i.between?(2, 99)
is_secure = record.tenancy_type == "Fixed term – Secure"
is_ast = record.tenancy_type == "Fixed term – Assured Shorthold Tenancy (AST)"
conditions = [
{ condition: !(is_secure || is_ast) && is_present, error: "You must only answer the fixed term tenancy length question if the tenancy type is fixed term" },
{ condition: is_ast && !is_in_range, error: "Fixed term – Assured Shorthold Tenancy (AST) should be between 2 and 99 years" },
{ condition: is_secure && (!is_in_range && is_present), error: "Fixed term – Secure should be between 2 and 99 years or not specified" },
]
conditions.each { |condition| condition[:condition] ? (record.errors.add :fixed_term_tenancy, condition[:error]) : nil }
end
def validate_other_tenancy_type(record)
validate_other_field(record, "tenancy_type", "other_tenancy_type")
end
end

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

@ -0,0 +1,11 @@
<div class="govuk-form-group govuk-form-group--error">
<%= f.govuk_check_boxes_fieldset @case_log.soft_errors.keys.first,
legend: { text: @case_log.soft_errors.values.first.message, size: "l" },
hint: { text: @case_log.soft_errors.values.first.hint_text } do %>
<%= f.govuk_check_box @case_log.soft_errors.keys.first, @case_log.soft_errors.keys.first,
label: { text: "Yes" },
checked: f.object.send(@case_log.soft_errors.keys.first)
%>
<% end %>
</div>

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

@ -1,5 +1,7 @@
<meta http-equiv="Pragma" content="no-cache">
<% content_for :before_content do %>
<%= link_to 'Back', 'javascript:history.back()', class: "govuk-back-link" %>
<%= link_to 'Back', :back, class: "govuk-back-link" %>
<% end %>
<%= turbo_frame_tag "case_log_form", target: "_top" do %>
@ -17,7 +19,11 @@
</div>
<% end %>
<%= f.hidden_field :previous_page, value: page_key %>
<% if @case_log.soft_errors.present? && @case_log.soft_errors.keys.first == page_info["soft_validations"]&.keys&.first %>
<%= render partial: "form/validation_override_question", locals: { f: f } %>
<% end %>
<%= f.hidden_field :page, value: page_key %>
<%= f.govuk_submit "Save and continue" %>
<% end %>
<% end %>

2
config/application.rb

@ -29,7 +29,7 @@ module DataCollector
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
#
# config.time_zone = "Central Time (US & Canada)"
config.time_zone = "London"
# config.eager_load_paths << Rails.root.join("extras")
# Don't generate system test files.

24
config/forms/2021_2022.json

@ -1633,6 +1633,21 @@
"header": "",
"description": "",
"questions": {
"net_income_known": {
"check_answer_label": "Income known",
"header": "Do you know the tenant and their partner's net income?",
"hint_text": "",
"type": "radio",
"answer_options": {
"0": "Yes",
"1": "No",
"2": "Tenant prefers not to say"
},
"conditional_for": {
"net_income": ["Yes"],
"net_income_frequency": ["Yes"]
}
},
"net_income": {
"check_answer_label": "Income",
"header": "What is the tenant’s /and partner’s combined income after tax?",
@ -1652,6 +1667,15 @@
"2": "Yearly"
}
}
},
"soft_validations": {
"override_net_income_validation": {
"check_answer_label": "Net income confirmed?",
"type": "validation_override",
"answer_options": {
"override_net_income_validation": "Yes"
}
}
}
},
"net_income_uc_proportion": {

335
config/initializers/active_admin.rb

@ -0,0 +1,335 @@
ActiveAdmin.setup do |config|
# == Site Title
#
# Set the title that is displayed on the main layout
# for each of the active admin pages.
#
config.site_title = "DLUHC CORE"
# Set the link url for the title. For example, to take
# users to your main site. Defaults to no link.
#
# config.site_title_link = "/"
# Set an optional image to be displayed for the header
# instead of a string (overrides :site_title)
#
# Note: Aim for an image that's 21px high so it fits in the header.
#
# config.site_title_image = "logo.png"
# == Default Namespace
#
# Set the default namespace each administration resource
# will be added to.
#
# eg:
# config.default_namespace = :hello_world
#
# This will create resources in the HelloWorld module and
# will namespace routes to /hello_world/*
#
# To set no namespace by default, use:
# config.default_namespace = false
#
# Default:
# config.default_namespace = :admin
#
# You can customize the settings for each namespace by using
# a namespace block. For example, to change the site title
# within a namespace:
#
# config.namespace :admin do |admin|
# admin.site_title = "Custom Admin Title"
# end
#
# This will ONLY change the title for the admin section. Other
# namespaces will continue to use the main "site_title" configuration.
# == User Authentication
#
# Active Admin will automatically call an authentication
# method in a before filter of all controller actions to
# ensure that there is a currently logged in admin user.
#
# This setting changes the method which Active Admin calls
# within the application controller.
# config.authentication_method = :authenticate_admin_user!
# == User Authorization
#
# Active Admin will automatically call an authorization
# method in a before filter of all controller actions to
# ensure that there is a user with proper rights. You can use
# CanCanAdapter or make your own. Please refer to documentation.
# config.authorization_adapter = ActiveAdmin::CanCanAdapter
# In case you prefer Pundit over other solutions you can here pass
# the name of default policy class. This policy will be used in every
# case when Pundit is unable to find suitable policy.
# config.pundit_default_policy = "MyDefaultPunditPolicy"
# If you wish to maintain a separate set of Pundit policies for admin
# resources, you may set a namespace here that Pundit will search
# within when looking for a resource's policy.
# config.pundit_policy_namespace = :admin
# You can customize your CanCan Ability class name here.
# config.cancan_ability_class = "Ability"
# You can specify a method to be called on unauthorized access.
# This is necessary in order to prevent a redirect loop which happens
# because, by default, user gets redirected to Dashboard. If user
# doesn't have access to Dashboard, he'll end up in a redirect loop.
# Method provided here should be defined in application_controller.rb.
# config.on_unauthorized_access = :access_denied
# == Current User
#
# Active Admin will associate actions with the current
# user performing them.
#
# 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
# == Logging Out
#
# Active Admin displays a logout link on each screen. These
# settings configure the location and method used for the link.
#
# This setting changes the path where the link points to. If it's
# a string, the strings is used as the path. If it's a Symbol, we
# will call the method to return the path.
#
# Default:
config.logout_link_path = :destroy_admin_user_session_path
# This setting changes the http method used when rendering the
# link. For example :get, :delete, :put, etc..
#
# Default:
# config.logout_link_method = :get
# == Root
#
# Set the action to call for the root path. You can set different
# roots for each namespace.
#
# Default:
# config.root_to = 'dashboard#index'
# == Admin Comments
#
# This allows your users to comment on any resource registered with Active Admin.
#
# You can completely disable comments:
config.comments = false
#
# You can change the name under which comments are registered:
# config.comments_registration_name = 'AdminComment'
#
# You can change the order for the comments and you can change the column
# to be used for ordering:
# config.comments_order = 'created_at ASC'
#
# You can disable the menu item for the comments index page:
# config.comments_menu = false
#
# You can customize the comment menu:
# config.comments_menu = { parent: 'Admin', priority: 1 }
# == Batch Actions
#
# Enable and disable Batch Actions
#
config.batch_actions = true
# == Controller Filters
#
# You can add before, after and around filters to all of your
# Active Admin resources and pages from here.
#
# config.before_action :do_something_awesome
# == Attribute Filters
#
# You can exclude possibly sensitive model attributes from being displayed,
# added to forms, or exported by default by ActiveAdmin
#
config.filter_attributes = [:encrypted_password, :password, :password_confirmation]
# == Localize Date/Time Format
#
# Set the localize format to display dates and times.
# To understand how to localize your app with I18n, read more at
# https://guides.rubyonrails.org/i18n.html
#
# You can run `bin/rails runner 'puts I18n.t("date.formats")'` to see the
# available formats in your application.
#
config.localize_format = :long
# == Setting a Favicon
#
# config.favicon = 'favicon.ico'
# == Meta Tags
#
# Add additional meta tags to the head element of active admin pages.
#
# Add tags to all pages logged in users see:
# config.meta_tags = { author: 'My Company' }
# By default, sign up/sign in/recover password pages are excluded
# from showing up in search engine results by adding a robots meta
# tag. You can reset the hash of meta tags included in logged out
# pages:
# config.meta_tags_for_logged_out_pages = {}
# == Removing Breadcrumbs
#
# Breadcrumbs are enabled by default. You can customize them for individual
# resources or you can disable them globally from here.
#
# config.breadcrumb = false
# == Create Another Checkbox
#
# Create another checkbox is disabled by default. You can customize it for individual
# resources or you can enable them globally from here.
#
# config.create_another = true
# == Register Stylesheets & Javascripts
#
# We recommend using the built in Active Admin layout and loading
# up your own stylesheets / javascripts to customize the look
# and feel.
#
# To load a stylesheet:
# config.register_stylesheet 'my_stylesheet.css'
#
# You can provide an options hash for more control, which is passed along to stylesheet_link_tag():
# config.register_stylesheet 'my_print_stylesheet.css', media: :print
#
# To load a javascript file:
# config.register_javascript 'my_javascript.js'
# == CSV options
#
# Set the CSV builder separator
# config.csv_options = { col_sep: ';' }
#
# Force the use of quotes
# config.csv_options = { force_quotes: true }
# == Menu System
#
# You can add a navigation menu to be used in your application, or configure a provided menu
#
# To change the default utility navigation to show a link to your website & a logout btn
#
# config.namespace :admin do |admin|
# admin.build_menu :utility_navigation do |menu|
# menu.add label: "My Great Website", url: "http://www.mygreatwebsite.com", html_options: { target: :blank }
# admin.add_logout_button_to_menu menu
# end
# end
#
# If you wanted to add a static menu item to the default menu provided:
#
# config.namespace :admin do |admin|
# admin.build_menu :default do |menu|
# menu.add label: "My Great Website", url: "http://www.mygreatwebsite.com", html_options: { target: :blank }
# end
# end
# == Download Links
#
# You can disable download links on resource listing pages,
# or customize the formats shown per namespace/globally
#
# To disable/customize for the :admin namespace:
#
# config.namespace :admin do |admin|
#
# # Disable the links entirely
# admin.download_links = false
#
# # Only show XML & PDF options
# admin.download_links = [:xml, :pdf]
#
# # Enable/disable the links based on block
# # (for example, with cancan)
# admin.download_links = proc { can?(:view_download_links) }
#
# end
# == Pagination
#
# Pagination is enabled by default for all resources.
# You can control the default per page count for all resources here.
#
# config.default_per_page = 30
#
# You can control the max per page count too.
#
# config.max_per_page = 10_000
# == Filters
#
# By default the index screen includes a "Filters" sidebar on the right
# hand side with a filter for each attribute of the registered model.
# You can enable or disable them for all resources here.
#
# config.filters = true
#
# By default the filters include associations in a select, which means
# that every record will be loaded for each association (up
# to the value of config.maximum_association_filter_arity).
# You can enabled or disable the inclusion
# of those filters by default here.
#
# config.include_default_association_filters = true
# config.maximum_association_filter_arity = 256 # default value of :unlimited will change to 256 in a future version
# config.filter_columns_for_large_association = [
# :display_name,
# :full_name,
# :name,
# :username,
# :login,
# :title,
# :email,
# ]
# config.filter_method_for_large_association = '_starts_with'
# == Head
#
# You can add your own content to the site head like analytics. Make sure
# you only pass content you trust.
#
# config.head = ''.html_safe
# == Footer
#
# By default, the footer shows the current Active Admin version. You can
# override the content of the footer here.
#
# config.footer = 'my custom footer text'
# == Sorting
#
# By default ActiveAdmin::OrderClause is used for sorting logic
# You can inherit it with own class and inject it for all resources
#
# config.order_clause = MyOrderClause
# == Webpacker
#
# By default, Active Admin uses Sprocket's asset pipeline.
# You can switch to using Webpacker here.
#
config.use_webpacker = true
end

3
config/routes.rb

@ -1,7 +1,8 @@
Rails.application.routes.draw do
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
ActiveAdmin.routes(self)
root to: "test#index"
get "about", to: "about#index"
get "/", to: "test#index"
post "/case_logs/:id", to: "case_logs#submit_form"

2
config/webpack/environment.js

@ -1,3 +1,5 @@
const { environment } = require('@rails/webpacker')
const jquery = require('./plugins/jquery')
environment.plugins.prepend('jquery', jquery)
module.exports = environment

7
config/webpack/plugins/jquery.js vendored

@ -0,0 +1,7 @@
const webpack = require('webpack')
module.exports = new webpack.ProvidePlugin({
"$":"jquery",
"jQuery":"jquery",
"window.jQuery":"jquery"
});

9
db/migrate/20211028083105_change_net_income_type.rb

@ -0,0 +1,9 @@
class ChangeNetIncomeType < ActiveRecord::Migration[6.1]
def up
change_column :case_logs, :net_income, "integer USING CAST(property_number_of_times_relet AS integer)"
end
def down
change_column :case_logs, :net_income, :string
end
end

5
db/migrate/20211028090049_add_net_income_override.rb

@ -0,0 +1,5 @@
class AddNetIncomeOverride < ActiveRecord::Migration[6.1]
def change
add_column :case_logs, :override_net_income_validation, :boolean
end
end

7
db/migrate/20211028095000_add_net_income_known_field.rb

@ -0,0 +1,7 @@
class AddNetIncomeKnownField < ActiveRecord::Migration[6.1]
def change
change_table :case_logs, bulk: true do |t|
t.column :net_income_known, :string
end
end
end

4
db/schema.rb

@ -86,7 +86,7 @@ ActiveRecord::Schema.define(version: 2021_11_02_100820) do
t.string "property_major_repairs_date"
t.integer "property_number_of_times_relet"
t.string "property_wheelchair_accessible"
t.string "net_income"
t.integer "net_income"
t.string "net_income_frequency"
t.string "net_income_uc_proportion"
t.string "housing_benefit"
@ -144,6 +144,8 @@ ActiveRecord::Schema.define(version: 2021_11_02_100820) do
t.string "needs_type"
t.string "sale_completion_date"
t.string "purchaser_code"
t.boolean "override_net_income_validation"
t.string "net_income_known"
t.index ["discarded_at"], name: "index_case_logs_on_discarded_at"
end

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

@ -110,7 +110,7 @@
"value": {
"errors": {
"person_1_age": [
"Tenant age must be between 0 and 120"
"Tenant age must be an integer between 16 and 120"
]
}
}
@ -220,7 +220,7 @@
"If reasonable preference is Yes, a reason must be given"
],
"person_1_age": [
"Tenant age must be between 0 and 120"
"Tenant age must be an integer between 16 and 120"
]
}
}
@ -937,6 +937,15 @@
"property_wheelchair_accessible": {
"type": "boolean"
},
"net_income_known": {
"type": "string",
"minLength": 1,
"enum": [
"Yes",
"No",
"Tenant prefers not to say"
]
},
"net_income": {
"type": "number"
},
@ -1208,7 +1217,8 @@
"reasonable_preference_reason_medical_grounds",
"reasonable_preference_reason_avoid_hardship",
"reasonable_preference_reason_do_not_know",
"other_tenancy-type"
"other_tenancy-type",
"net_income_known"
]
}
},
@ -1225,4 +1235,4 @@
}
}
}
}
}

3
package.json

@ -2,6 +2,7 @@
"name": "data-collector",
"private": true,
"dependencies": {
"@activeadmin/activeadmin": "^2.9.0",
"@hotwired/stimulus": "^3.0.0",
"@hotwired/stimulus-webpack-helpers": "^1.0.1",
"@hotwired/turbo": "^7.0.0-rc.3",
@ -10,6 +11,8 @@
"@rails/activestorage": "^6.0.0",
"@rails/ujs": "^6.0.0",
"@rails/webpacker": "5.4.0",
"chart.js": "^3.6.0",
"chartkick": "^4.1.0",
"govuk-frontend": "^3.13.0",
"stimulus": "^3.0.0",
"webpack": "^4.46.0",

14
spec/controllers/case_logs_controller_spec.rb

@ -52,13 +52,13 @@ RSpec.describe CaseLogsController, type: :controller do
%w[ accessibility_requirements_fully_wheelchair_accessible_housing
accessibility_requirements_wheelchair_access_to_essential_rooms
accessibility_requirements_level_access_housing],
previous_page: "accessibility_requirements" }
page: "accessibility_requirements" }
end
let(:new_case_log_form_params) do
{
accessibility_requirements: %w[accessibility_requirements_level_access_housing],
previous_page: "accessibility_requirements",
page: "accessibility_requirements",
}
end
@ -88,7 +88,7 @@ RSpec.describe CaseLogsController, type: :controller do
accessibility_requirements_wheelchair_access_to_essential_rooms
accessibility_requirements_level_access_housing],
tenant_code: tenant_code,
previous_page: "accessibility_requirements" }
page: "accessibility_requirements" }
end
let(:questions_for_page) do
{ "accessibility_requirements" =>
@ -124,17 +124,21 @@ RSpec.describe CaseLogsController, type: :controller do
end
context "conditional routing" do
before do
allow_any_instance_of(CaseLogValidator).to receive(:validate_household_pregnancy).and_return(true)
end
let(:case_log_form_conditional_question_yes_params) do
{
pregnancy: "Yes",
previous_page: "conditional_question",
page: "conditional_question",
}
end
let(:case_log_form_conditional_question_no_params) do
{
pregnancy: "No",
previous_page: "conditional_question",
page: "conditional_question",
}
end

2
spec/factories/case_log.rb

@ -6,7 +6,7 @@ FactoryBot.define do
tenant_code { "TH356" }
property_postcode { "SW2 6HI" }
previous_postcode { "P0 5ST" }
person_1_age { "12" }
person_1_age { "18" }
end
trait :completed do
status { 2 }

74
spec/features/case_log_spec.rb

@ -32,10 +32,6 @@ RSpec.describe "Test Features" do
describe "Create new log" do
it "redirects to the task list for the new log" do
visit("/case_logs")
# Ensure that we've finished creating both case logs before running the
# Capybara click part to ensure we don't get creation race conditions
expect(page).to have_link(nil, href: "/case_logs/#{case_log.id}")
expect(page).to have_link(nil, href: "/case_logs/#{empty_case_log.id}")
click_link("Create new log")
id = CaseLog.order(created_at: :desc).first.id
expect(page).to have_content("Tasklist for log #{id}")
@ -184,7 +180,7 @@ RSpec.describe "Test Features" do
it "displays text answers in inputs if they are already saved" do
visit("/case_logs/#{id}/person_1_age")
expect(page).to have_field("case-log-person-1-age-field", with: "12")
expect(page).to have_field("case-log-person-1-age-field", with: "18")
end
it "displays checkbox answers in inputs if they are already saved" do
@ -233,6 +229,15 @@ RSpec.describe "Test Features" do
expect(page).to have_current_path("/case_logs/#{id}/#{pages[index + 1]}")
end
end
context "when changing an answer from the check answers page" do
it "the back button routes correctly" do
visit("/case_logs/#{id}/household_characteristics/check_answers")
first("a", text: /Answer/).click
click_link("Back")
expect(page).to have_current_path("/case_logs/#{id}/household_characteristics/check_answers")
end
end
end
end
@ -377,11 +382,66 @@ RSpec.describe "Test Features" do
end
end
describe "Soft Validation" do
context "given a weekly net income that is above the expected amount for the given economic status but below the hard max" do
let!(:case_log) { FactoryBot.create(:case_log, :in_progress, person_1_economic_status: "Full-time - 30 hours or more") }
let(:income_over_soft_limit) { 750 }
let(:income_under_soft_limit) { 700 }
it "prompts the user to confirm the value is correct" do
visit("/case_logs/#{case_log.id}/net_income")
fill_in("case-log-net-income-field", with: income_over_soft_limit)
choose("case-log-net-income-frequency-weekly-field", allow_label_click: true)
click_button("Save and continue")
expect(page).to have_content("Are you sure this is correct?")
check("case-log-override-net-income-validation-override-net-income-validation-field", allow_label_click: true)
click_button("Save and continue")
expect(page).to have_current_path("/case_logs/#{case_log.id}/net_income_uc_proportion")
end
it "does not require confirming the value if the value is amended" do
visit("/case_logs/#{case_log.id}/net_income")
fill_in("case-log-net-income-field", with: income_over_soft_limit)
choose("case-log-net-income-frequency-weekly-field", allow_label_click: true)
click_button("Save and continue")
fill_in("case-log-net-income-field", with: income_under_soft_limit)
click_button("Save and continue")
expect(page).to have_current_path("/case_logs/#{case_log.id}/net_income_uc_proportion")
case_log.reload
expect(case_log.override_net_income_validation).to be_nil
end
it "clears the confirmation question if the amount was amended and the page is returned to using the back button", js: true do
visit("/case_logs/#{case_log.id}/net_income")
fill_in("case-log-net-income-field", with: income_over_soft_limit)
choose("case-log-net-income-frequency-weekly-field", allow_label_click: true)
click_button("Save and continue")
fill_in("case-log-net-income-field", with: income_under_soft_limit)
click_button("Save and continue")
click_link(text: "Back")
expect(page).not_to have_content("Are you sure this is correct?")
end
it "does not clear the confirmation question if the page is returned to using the back button and the amount is still over the soft limit", js: true do
visit("/case_logs/#{case_log.id}/net_income")
fill_in("case-log-net-income-field", with: income_over_soft_limit)
choose("case-log-net-income-frequency-weekly-field", allow_label_click: true)
click_button("Save and continue")
check("case-log-override-net-income-validation-override-net-income-validation-field", allow_label_click: true)
click_button("Save and continue")
click_link(text: "Back")
expect(page).to have_content("Are you sure this is correct?")
end
end
end
describe "conditional page routing", js: true do
before do
allow_any_instance_of(CaseLogValidator).to receive(:validate_household_pregnancy).and_return(true)
end
it "can route the user to a different page based on their answer on the current page" do
visit("case_logs/#{id}/conditional_question")
# using a question name that is already in the db to avoid
# having to add a new column to the db for this test
choose("case-log-pregnancy-yes-field", allow_label_click: true)
click_button("Save and continue")
expect(page).to have_current_path("/case_logs/#{id}/conditional_question_yes_page")

1
spec/fixtures/complete_case_log.json vendored

@ -68,6 +68,7 @@
"property_major_repairs_date": "05/05/2020",
"property_number_of_times_relet": 2,
"property_wheelchair_accessible": true,
"net_income_known": "Yes",
"net_income": 0,
"net_income_frequency": null,
"net_income_uc_proportion": "Some",

9
spec/fixtures/forms/test_form.json vendored

@ -328,6 +328,15 @@
"2": "Yearly"
}
}
},
"soft_validations": {
"override_net_income_validation": {
"check_answer_label": "Net income confirmed?",
"type": "validation_override",
"answer_options": {
"override_net_income_validation": "Yes"
}
}
}
},
"net_income_uc_proportion": {

115
spec/models/case_log_spec.rb

@ -97,6 +97,45 @@ RSpec.describe Form, type: :model do
end
end
context "Shared accomodation bedrooms validation" do
it "you must have more than zero bedrooms" do
expect {
CaseLog.create!(property_unit_type: "Shared house",
property_number_of_bedrooms: 0)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "you must answer less than 8 bedrooms" do
expect {
CaseLog.create!(property_unit_type: "Shared bungalow",
property_number_of_bedrooms: 8,
household_number_of_other_members: 1)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "you must answer less than 8 bedrooms" do
expect {
CaseLog.create!(property_unit_type: "Shared bungalow",
property_number_of_bedrooms: 4,
household_number_of_other_members: 0)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "A bedsit must only have one room" do
expect {
CaseLog.create!(property_unit_type: "Bed-sit",
property_number_of_bedrooms: 2)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "A bedsit must only have one room" do
expect {
CaseLog.create!(property_unit_type: "Bed-sit",
property_number_of_bedrooms: 0)
}.to raise_error(ActiveRecord::RecordInvalid)
end
end
context "outstanding rent or charges validation" do
it "must be anwered if answered yes to outstanding rent or charges" do
expect {
@ -206,6 +245,40 @@ RSpec.describe Form, type: :model do
end
end
context "household_member_validations" do
it "validate that persons aged under 16 must have relationship Child" do
expect { CaseLog.create!(person_2_age: 14, person_2_relationship: "Partner") }.to raise_error(ActiveRecord::RecordInvalid)
end
it "validate that persons aged over 70 must be retired" do
expect { CaseLog.create!(person_2_age: 71, person_2_economic_status: "Full-time - 30 hours or more") }.to raise_error(ActiveRecord::RecordInvalid)
end
it "validate that a male, retired persons must be over 65" do
expect { CaseLog.create!(person_2_age: 64, person_2_gender: "Male", person_2_economic_status: "Retired") }.to raise_error(ActiveRecord::RecordInvalid)
end
it "validate that a female, retired persons must be over 60" do
expect { CaseLog.create!(person_2_age: 59, person_2_gender: "Female", person_2_economic_status: "Retired") }.to raise_error(ActiveRecord::RecordInvalid)
end
it "validate that persons aged under 16 must be a child (economically speaking)" do
expect { CaseLog.create!(person_2_age: 15, person_2_economic_status: "Full-time - 30 hours or more") }.to raise_error(ActiveRecord::RecordInvalid)
end
it "validate that persons aged between 16 and 19 that are a child must be a full time student or economic status refused" do
expect { CaseLog.create!(person_2_age: 17, person_2_relationship: "Child - includes young adult and grown-up", person_2_economic_status: "Full-time - 30 hours or more") }.to raise_error(ActiveRecord::RecordInvalid)
end
it "validate that persons aged under 16 must be a child relationship" do
expect { CaseLog.create!(person_2_age: 15, person_2_relationship: "Partner") }.to raise_error(ActiveRecord::RecordInvalid)
end
it "validate that no more than 1 partner relationship exists" do
expect { CaseLog.create!(person_2_relationship: "Partner", person_3_relationship: "Partner") }.to raise_error(ActiveRecord::RecordInvalid)
end
end
context "other tenancy type validation" do
it "must be provided if tenancy type was given as other" do
expect {
@ -231,6 +304,28 @@ RSpec.describe Form, type: :model do
}.not_to raise_error
end
end
context "income ranges" do
it "validates net income maximum" do
expect {
CaseLog.create!(
person_1_economic_status: "Full-time - 30 hours or more",
net_income: 5000,
net_income_frequency: "Weekly",
)
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "validates net income minimum" do
expect {
CaseLog.create!(
person_1_economic_status: "Full-time - 30 hours or more",
net_income: 1,
net_income_frequency: "Weekly",
)
}.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
describe "status" do
@ -249,4 +344,24 @@ RSpec.describe Form, type: :model do
expect(in_progress_case_log.completed?).to be(false)
end
end
describe "weekly_net_income" do
let(:net_income) { 5000 }
let(:case_log) { FactoryBot.build(:case_log, net_income: net_income) }
it "returns input income if frequency is already weekly" do
case_log.net_income_frequency = "Weekly"
expect(case_log.weekly_net_income).to eq(net_income)
end
it "calculates the correct weekly income from monthly income" do
case_log.net_income_frequency = "Monthly"
expect(case_log.weekly_net_income).to eq(1154)
end
it "calculates the correct weekly income from yearly income" do
case_log.net_income_frequency = "Yearly"
expect(case_log.weekly_net_income).to eq(417)
end
end
end

12
spec/requests/case_log_controller_spec.rb

@ -66,7 +66,7 @@ RSpec.describe CaseLogsController, type: :request do
it "validates case log parameters" do
json_response = JSON.parse(response.body)
expect(response).to have_http_status(:unprocessable_entity)
expect(json_response["errors"]).to match_array([["property_number_of_times_relet", ["Must be between 0 and 20"]], ["person_1_age", ["Tenant age must be between 0 and 120"]]])
expect(json_response["errors"]).to match_array([["property_number_of_times_relet", ["Property number of times relet must be between 0 and 20"]], ["person_1_age", ["Tenant age must be an integer between 16 and 120"]]])
end
end
@ -115,6 +115,14 @@ RSpec.describe CaseLogsController, type: :request do
json_response = JSON.parse(response.body)
expect(json_response["status"]).to eq(case_log.status)
end
context "invalid case log id" do
let(:id) { (CaseLog.order(:id).last&.id || 0) + 1 }
it "returns 404" do
expect(response).to have_http_status(:not_found)
end
end
end
describe "PATCH" do
@ -157,7 +165,7 @@ RSpec.describe CaseLogsController, type: :request do
it "returns an error message" do
json_response = JSON.parse(response.body)
expect(json_response["errors"]).to eq({ "person_1_age" => ["Tenant age must be between 0 and 120"] })
expect(json_response["errors"]).to eq({ "person_1_age" => ["Tenant age must be an integer between 16 and 120"] })
end
end

50
yarn.lock

@ -2,6 +2,15 @@
# yarn lockfile v1
"@activeadmin/activeadmin@^2.9.0":
version "2.9.0"
resolved "https://registry.yarnpkg.com/@activeadmin/activeadmin/-/activeadmin-2.9.0.tgz#ad277ef4a49b250377afd3df615f9acc3c84d577"
integrity sha512-KZqr1MrvpCXN/8SCF4MgqGscmuWPHivz5RPHKR9R8bKIB0InUHm7tzQoQOT9ZdYuHCBkf3cqlzBoKNNNV3x3ww==
dependencies:
jquery "^3.4.1"
jquery-ui "^1.12.1"
jquery-ujs "^1.2.2"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.14.5.tgz#23b08d740e83f49c5e59945fbf1b43e80bbf4edb"
@ -1800,6 +1809,25 @@ chalk@^2.0, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chart.js@>=3.0.2, chart.js@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.6.0.tgz#a87fce8431d4e7c5523d721f487f53aada1e42fe"
integrity sha512-iOzzDKePL+bj+ccIsVAgWQehCXv8xOKGbaU2fO/myivH736zcx535PGJzQGanvcSGVOqX6yuLZsN3ygcQ35UgQ==
chartjs-adapter-date-fns@>=2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-2.0.0.tgz#5e53b2f660b993698f936f509c86dddf9ed44c6b"
integrity sha512-rmZINGLe+9IiiEB0kb57vH3UugAtYw33anRiw5kS2Tu87agpetDDoouquycWc9pRsKtQo5j+vLsYHyr8etAvFw==
chartkick@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chartkick/-/chartkick-4.1.0.tgz#a60b3ad55059195317f504fae607106d5cdfb78d"
integrity sha512-BBf1JWV7SRsxDng9Fh4MRGuj93VPkF8F1pcrOfbCD/ey+/Fyo+tFF8vqpGAjOyu7oNd4cc1IC/OUixUtPsfNmg==
optionalDependencies:
chart.js ">=3.0.2"
chartjs-adapter-date-fns ">=2.0.0"
date-fns ">=2.0.0"
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
@ -2336,6 +2364,11 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
date-fns@>=2.0.0:
version "2.25.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
integrity sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -3820,6 +3853,23 @@ jest-worker@^26.5.0:
merge-stream "^2.0.0"
supports-color "^7.0.0"
jquery-ui@^1.12.1:
version "1.13.0"
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.13.0.tgz#ab5ac65f37ca093c51b3478c4097f55bbc008f36"
integrity sha512-Osf7ECXNTYHtKBkn9xzbIf9kifNrBhfywFEKxOeB/OVctVmLlouV9mfc2qXCp6uyO4Pn72PXKOnj09qXetopCw==
dependencies:
jquery ">=1.8.0 <4.0.0"
jquery-ujs@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.3.tgz#dcac6026ab7268e5ee41faf9d31c997cd4ddd603"
integrity sha512-59wvfx5vcCTHMeQT1/OwFiAj+UffLIwjRIoXdpO7Z7BCFGepzq9T9oLVeoItjTqjoXfUrHJvV7QU6pUR+UzOoA==
"jquery@>=1.8.0 <4.0.0", jquery@^3.4.1:
version "3.6.0"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470"
integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"

Loading…
Cancel
Save