From bea7c54e5a0922d551bf92e03f41cecfeef642e7 Mon Sep 17 00:00:00 2001 From: magicmilo Date: Wed, 27 Oct 2021 11:23:46 +0100 Subject: [PATCH 01/18] inital --- app/models/case_log.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/models/case_log.rb b/app/models/case_log.rb index 0b347a784..09b98f381 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -61,6 +61,23 @@ class CaseLogValidator < ActiveModel::Validator end end + def validate_shared_housing_rooms(record) + number_of_tenants = people_in_household(record) + 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 people_in_household(record) > 1 + if record.property_unit_type.include? == "Shared" && (record.property_number_of_bedrooms == 0 && record.property_number_of_bedrooms > 7) + record.errors.add :property_unit_type, "A shared house must have 1 to 7 bedrooms" + end + else + if record.property_unit_type.include? == "Shared" && (record.property_number_of_bedrooms == 0 && record.property_number_of_bedrooms > 3) + record.errors.add :property_unit_type, "A shared house with one tenant must have 1 to 3 bedrooms" + end + end + end + 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 @@ -87,6 +104,14 @@ private record["person_#{n}_gender"] == "Female" && record["person_#{n}_age"] >= 16 && record["person_#{n}_age"] <= 50 end end + + def people_in_household(record) + count = 0 + (1..8).any? do |n| + next if record["person_#{n}_gender"].nil? || record["person_#{n}_age"].nil? + count += 1 + end + end end class CaseLog < ApplicationRecord From 1cf119709660fc6059dffc1a22038ef6c266090e Mon Sep 17 00:00:00 2001 From: magicmilo Date: Wed, 27 Oct 2021 14:07:18 +0100 Subject: [PATCH 02/18] tests --- app/models/case_log.rb | 1 + spec/models/case_log_spec.rb | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/app/models/case_log.rb b/app/models/case_log.rb index 09b98f381..f4fa0b9cf 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -111,6 +111,7 @@ private next if record["person_#{n}_gender"].nil? || record["person_#{n}_age"].nil? count += 1 end + return count end end diff --git a/spec/models/case_log_spec.rb b/spec/models/case_log_spec.rb index c9339d719..111a69943 100644 --- a/spec/models/case_log_spec.rb +++ b/spec/models/case_log_spec.rb @@ -96,6 +96,36 @@ RSpec.describe Form, type: :model do }.to raise_error(ActiveRecord::RecordInvalid) 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) + }.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 end describe "status" do From 2367ad16d505fc86ef6ba0b81d0f64b4f7352bd0 Mon Sep 17 00:00:00 2001 From: magicmilo Date: Thu, 28 Oct 2021 10:41:54 +0100 Subject: [PATCH 03/18] tests and tweaks --- app/models/case_log.rb | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/app/models/case_log.rb b/app/models/case_log.rb index f4fa0b9cf..d77e59c7a 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -63,17 +63,20 @@ class CaseLogValidator < ActiveModel::Validator def validate_shared_housing_rooms(record) number_of_tenants = people_in_household(record) - 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 people_in_household(record) > 1 - if record.property_unit_type.include? == "Shared" && (record.property_number_of_bedrooms == 0 && record.property_number_of_bedrooms > 7) - record.errors.add :property_unit_type, "A shared house must have 1 to 7 bedrooms" + 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 - else - if record.property_unit_type.include? == "Shared" && (record.property_number_of_bedrooms == 0 && record.property_number_of_bedrooms > 3) - record.errors.add :property_unit_type, "A shared house with one tenant must have 1 to 3 bedrooms" + + if people_in_household(record) > 1 + if record.property_unit_type.include?("Shared") && (record.property_number_of_bedrooms.to_i == 0 || record.property_number_of_bedrooms.to_i > 7) + record.errors.add :property_unit_type, "A shared house must have 1 to 7 bedrooms" + end + end + + if record.property_unit_type.include?("Shared") && (record.property_number_of_bedrooms.to_i == 0 || record.property_number_of_bedrooms.to_i > 3) + record.errors.add :property_unit_type, "A shared house with less than two tenants must have 1 to 3 bedrooms" end end end From f372a4727e272e1aae5cf9604a2a7a4549300944 Mon Sep 17 00:00:00 2001 From: Daniel Baark <5101747+baarkerlounger@users.noreply.github.com> Date: Thu, 28 Oct 2021 13:57:56 +0100 Subject: [PATCH 04/18] Hard validation (#69) --- app/models/case_log.rb | 29 +++++++++++++ app/models/income_range.rb | 15 +++++++ .../20211028083105_change_net_income_type.rb | 9 ++++ db/schema.rb | 4 +- spec/models/case_log_spec.rb | 42 +++++++++++++++++++ spec/requests/case_log_controller_spec.rb | 8 ++++ 6 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 app/models/income_range.rb create mode 100644 db/migrate/20211028083105_change_net_income_type.rb diff --git a/app/models/case_log.rb b/app/models/case_log.rb index 8d912354b..1e59c0819 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -101,6 +101,18 @@ class CaseLogValidator < ActiveModel::Validator conditions.each { |condition| condition[:condition] ? (record.errors.add :fixed_term_tenancy, condition[:error]) : nil } 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 + def validate_other_tenancy_type(record) validate_other_field(record, "tenancy_type", "other_tenancy_type") end @@ -182,6 +194,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! diff --git a/app/models/income_range.rb b/app/models/income_range.rb new file mode 100644 index 000000000..b21b1fa57 --- /dev/null +++ b/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) + } +end diff --git a/db/migrate/20211028083105_change_net_income_type.rb b/db/migrate/20211028083105_change_net_income_type.rb new file mode 100644 index 000000000..85fedcceb --- /dev/null +++ b/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 diff --git a/db/schema.rb b/db/schema.rb index 346f047c2..e7038ce2a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_10_27_123535) do +ActiveRecord::Schema.define(version: 2021_10_28_083105) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -86,7 +86,7 @@ ActiveRecord::Schema.define(version: 2021_10_27_123535) 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" diff --git a/spec/models/case_log_spec.rb b/spec/models/case_log_spec.rb index d95f000a7..5d1f07918 100644 --- a/spec/models/case_log_spec.rb +++ b/spec/models/case_log_spec.rb @@ -230,6 +230,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 @@ -248,4 +270,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 diff --git a/spec/requests/case_log_controller_spec.rb b/spec/requests/case_log_controller_spec.rb index e6c866c90..6ddbd0130 100644 --- a/spec/requests/case_log_controller_spec.rb +++ b/spec/requests/case_log_controller_spec.rb @@ -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 From c7cd48c2e8860195915bc6157ea8f62cf9d70feb Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Thu, 28 Oct 2021 14:22:05 +0100 Subject: [PATCH 05/18] CLDC-513: Refused income validation (#71) * Add net income known field * only require the income if it is known * fix a typo --- app/models/case_log.rb | 7 ++++++- config/forms/2021_2022.json | 15 +++++++++++++++ .../20211028095000_add_net_income_known_field.rb | 7 +++++++ db/schema.rb | 3 ++- docs/api/DLUHC-CORE-Data.v1.json | 14 ++++++++++++-- spec/fixtures/complete_case_log.json | 1 + spec/models/case_log_spec.rb | 5 +++-- 7 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20211028095000_add_net_income_known_field.rb diff --git a/app/models/case_log.rb b/app/models/case_log.rb index 1e59c0819..2862eb087 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -248,10 +248,15 @@ private dynamically_not_required << "fixed_term_tenancy" end - if tenancy_type != "Other" + 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 + required.delete_if { |key, _value| dynamically_not_required.include?(key) } end end diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index f4cf447c3..9f185b120 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -1458,6 +1458,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?", diff --git a/db/migrate/20211028095000_add_net_income_known_field.rb b/db/migrate/20211028095000_add_net_income_known_field.rb new file mode 100644 index 000000000..89e82c29f --- /dev/null +++ b/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 diff --git a/db/schema.rb b/db/schema.rb index e7038ce2a..5c4ffd1ea 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_10_28_083105) do +ActiveRecord::Schema.define(version: 2021_10_28_095000) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -133,6 +133,7 @@ ActiveRecord::Schema.define(version: 2021_10_28_083105) do t.boolean "reasonable_preference_reason_do_not_know" t.datetime "discarded_at" t.string "other_tenancy_type" + t.string "net_income_known" t.index ["discarded_at"], name: "index_case_logs_on_discarded_at" end diff --git a/docs/api/DLUHC-CORE-Data.v1.json b/docs/api/DLUHC-CORE-Data.v1.json index 7a4d495e2..3820d9aee 100644 --- a/docs/api/DLUHC-CORE-Data.v1.json +++ b/docs/api/DLUHC-CORE-Data.v1.json @@ -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 @@ } } } -} \ No newline at end of file +} diff --git a/spec/fixtures/complete_case_log.json b/spec/fixtures/complete_case_log.json index 9238bba6a..45e402f4e 100644 --- a/spec/fixtures/complete_case_log.json +++ b/spec/fixtures/complete_case_log.json @@ -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", diff --git a/spec/models/case_log_spec.rb b/spec/models/case_log_spec.rb index 5d1f07918..5e42ebce9 100644 --- a/spec/models/case_log_spec.rb +++ b/spec/models/case_log_spec.rb @@ -198,10 +198,11 @@ RSpec.describe Form, type: :model do # Crossover over tests here as injured must be answered as well for no error it "must be answered if ever served in the forces as a regular" do - expect { + expect do CaseLog.create!(armed_forces: "Yes - a regular", armed_forces_active: "Yes", - armed_forces_injured: "Yes")} + armed_forces_injured: "Yes") + end end end From 971c3909eefb3d97b4f4190f4860e19719700ad0 Mon Sep 17 00:00:00 2001 From: Matthew Phelan Date: Thu, 28 Oct 2021 15:36:11 +0100 Subject: [PATCH 06/18] validate_other_household_members --- app/models/case_log.rb | 75 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/app/models/case_log.rb b/app/models/case_log.rb index 2862eb087..2cb25397d 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -117,6 +117,81 @@ class CaseLogValidator < ActiveModel::Validator validate_other_field(record, "tenancy_type", "other_tenancy_type") end + def validate_other_household_members(record) + index = 0 + number_of_other_members = record.household_number_of_other_members + partner = false + + while index < number_of_other_members + + member_number = index+2 + relationship = record["person_#{member_number}_relationship"] + age = record["person_#{member_number}_age"] + gender = record["person_#{member_number}_gender"] + economic_status = record["person_#{member_number}_economic_status"] + + binding.pry + if relationship || age || gender || economic_status + if relationship.nil? || age.nil? || gender.nil? || economic_status.nil? + record.errors.add "person_#{member_number}_age", "If any of the person is filled out it must all be filled" + end + end + + if age<1 || age>120 + record.errors.add "person_#{member_number}_age", "Tenant #{member_number} age must be between 1 and 120 (i.e. infants must be entered as 1)" + end + + if age>70 && economic_status != "Retired" + record.errors.add "person_#{member_number}_economic_status", "Tenant #{member_number} must be retired if over 70" + end + + if gender=="Male" && economic_status == "Retired" && age<65 + record.errors.add "person_#{member_number}_age", "Male tenant who is retired must be 65 or over" + end + + if gender=="Female" && economic_status == "Retired" && age<60 + record.errors.add "person_#{member_number}_age", "Female tenant who is retired must be 60 or over" + end + + if age>70 && economic_status != "Retired" + record.errors.add "person_#{member_number}_economic_status", "Tenant #{member_number} must be retired if over 70" + end + + if age<16 + if relationship != "Child - includes young adult and grown-up" + record.errors.add "person_#{member_number}_relationship", "Tenant #{member_number}'s relationship to tenant 1 must be Child if their age is under 16" + end + if economic_status != "Child under 16" + record.errors.add "person_#{member_number}_economic_status", "Tenant #{member_number} economic status must be Child under 16 if their age is under 16" + end + end + + if relationship == "Partner" + if partner + record.errors.add "person_#{member_number}_relationship", "Tenant can not have multiple partners" + elsif age<16 || economic_status == "Child under 16" + record.errors.add "person_#{member_number}_relationship", "Tenant can not be tenant 1's partner if they are under 16" + else + partner = true + end + end + + if relationship == "Child - includes young adult and grown-up" + if economic_status!="Unable to work because of long term sick or disability" || economic_status!="Other" || economic_status!="Prefer not to say" + record.errors.add "person_#{member_number}_economic_status", "This is not a valid economic status for a child" + end + + if age>=16 && age<=19 + if economic_status != "Full-time student" || economic_status != "Prefer not to say" + record.errors.add "person_#{member_number}_economic_status", "If relationship is child and age is between 16 and 19 - tenant #{member_number} must be a full time student or prefer not to say." + end + end + end + + index = index+1 + end + end + 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 From fff5da304eece3d6b7c956143797b9faf74b9de1 Mon Sep 17 00:00:00 2001 From: Daniel Baark <5101747+baarkerlounger@users.noreply.github.com> Date: Fri, 29 Oct 2021 09:31:28 +0100 Subject: [PATCH 07/18] Refactor validations into modules (#73) * Refactor validations into modules * PropertyValidations * Add financial and tenancy validations * Shorten comment * Simplify --- app/models/case_log.rb | 134 +---------------------- app/models/income_range.rb | 4 +- app/validations/financial_validations.rb | 36 ++++++ app/validations/household_validations.rb | 68 ++++++++++++ app/validations/property_validations.rb | 8 ++ app/validations/tenancy_validations.rb | 20 ++++ 6 files changed, 140 insertions(+), 130 deletions(-) create mode 100644 app/validations/financial_validations.rb create mode 100644 app/validations/household_validations.rb create mode 100644 app/validations/property_validations.rb create mode 100644 app/validations/tenancy_validations.rb diff --git a/app/models/case_log.rb b/app/models/case_log.rb index 2862eb087..ffe5e5397 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -1,121 +1,9 @@ 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_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 - - def validate_other_tenancy_type(record) - validate_other_field(record, "tenancy_type", "other_tenancy_type") - end + # Validations methods need to be called 'validate_' to run on model save + 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 @@ -127,9 +15,7 @@ class CaseLogValidator < ActiveModel::Validator public_send("validate_#{question_to_validate}", record) end 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 @@ -147,14 +33,6 @@ 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 diff --git a/app/models/income_range.rb b/app/models/income_range.rb index b21b1fa57..2d0013d33 100644 --- a/app/models/income_range.rb +++ b/app/models/income_range.rb @@ -10,6 +10,6 @@ class IncomeRange "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) - } + "Prefer not to say": OpenStruct.new(soft_min: 47, soft_max: 730, hard_min: 10, hard_max: 1300), + }.freeze end diff --git a/app/validations/financial_validations.rb b/app/validations/financial_validations.rb new file mode 100644 index 000000000..90093e134 --- /dev/null +++ b/app/validations/financial_validations.rb @@ -0,0 +1,36 @@ +module FinancialValidations + # Validations methods need to be called 'validate_' to run on model save + 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 diff --git a/app/validations/household_validations.rb b/app/validations/household_validations.rb new file mode 100644 index 000000000..8ff4e684a --- /dev/null +++ b/app/validations/household_validations.rb @@ -0,0 +1,68 @@ +module HouseholdValidations + # Validations methods need to be called 'validate_' to run on model save + 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_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 + +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 +end diff --git a/app/validations/property_validations.rb b/app/validations/property_validations.rb new file mode 100644 index 000000000..9d40a2ee5 --- /dev/null +++ b/app/validations/property_validations.rb @@ -0,0 +1,8 @@ +module PropertyValidations + # Validations methods need to be called 'validate_' to run on model save + 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 +end diff --git a/app/validations/tenancy_validations.rb b/app/validations/tenancy_validations.rb new file mode 100644 index 000000000..063b4672c --- /dev/null +++ b/app/validations/tenancy_validations.rb @@ -0,0 +1,20 @@ +module TenancyValidations + # Validations methods need to be called 'validate_' to run on model save + 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 From c3e1f07797d892c8a0ad7d128b3b20e5b66585a3 Mon Sep 17 00:00:00 2001 From: Matthew Phelan Date: Fri, 29 Oct 2021 11:17:21 +0100 Subject: [PATCH 08/18] validate_other_household_members refactor Co-authored-by: Daniel Baark --- app/models/case_log.rb | 87 +++--------------------- app/validations/household_validations.rb | 74 ++++++++++++++++++-- spec/models/case_log_spec.rb | 6 ++ 3 files changed, 83 insertions(+), 84 deletions(-) diff --git a/app/models/case_log.rb b/app/models/case_log.rb index 0123a06de..9e23699e1 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -5,90 +5,13 @@ class CaseLogValidator < ActiveModel::Validator include FinancialValidations include TenancyValidations - def validate_other_household_members(record) - index = 0 - number_of_other_members = record.household_number_of_other_members - partner = false - - while index < number_of_other_members - - member_number = index+2 - relationship = record["person_#{member_number}_relationship"] - age = record["person_#{member_number}_age"] - gender = record["person_#{member_number}_gender"] - economic_status = record["person_#{member_number}_economic_status"] - - binding.pry - if relationship || age || gender || economic_status - if relationship.nil? || age.nil? || gender.nil? || economic_status.nil? - record.errors.add "person_#{member_number}_age", "If any of the person is filled out it must all be filled" - end - end - - if age<1 || age>120 - record.errors.add "person_#{member_number}_age", "Tenant #{member_number} age must be between 1 and 120 (i.e. infants must be entered as 1)" - end - - if age>70 && economic_status != "Retired" - record.errors.add "person_#{member_number}_economic_status", "Tenant #{member_number} must be retired if over 70" - end - - if gender=="Male" && economic_status == "Retired" && age<65 - record.errors.add "person_#{member_number}_age", "Male tenant who is retired must be 65 or over" - end - - if gender=="Female" && economic_status == "Retired" && age<60 - record.errors.add "person_#{member_number}_age", "Female tenant who is retired must be 60 or over" - end - - if age>70 && economic_status != "Retired" - record.errors.add "person_#{member_number}_economic_status", "Tenant #{member_number} must be retired if over 70" - end - - if age<16 - if relationship != "Child - includes young adult and grown-up" - record.errors.add "person_#{member_number}_relationship", "Tenant #{member_number}'s relationship to tenant 1 must be Child if their age is under 16" - end - if economic_status != "Child under 16" - record.errors.add "person_#{member_number}_economic_status", "Tenant #{member_number} economic status must be Child under 16 if their age is under 16" - end - end - - if relationship == "Partner" - if partner - record.errors.add "person_#{member_number}_relationship", "Tenant can not have multiple partners" - elsif age<16 || economic_status == "Child under 16" - record.errors.add "person_#{member_number}_relationship", "Tenant can not be tenant 1's partner if they are under 16" - else - partner = true - end - end - - if relationship == "Child - includes young adult and grown-up" - if economic_status!="Unable to work because of long term sick or disability" || economic_status!="Other" || economic_status!="Prefer not to say" - record.errors.add "person_#{member_number}_economic_status", "This is not a valid economic status for a child" - end - - if age>=16 && age<=19 - if economic_status != "Full-time student" || economic_status != "Prefer not to say" - record.errors.add "person_#{member_number}_economic_status", "If relationship is child and age is between 16 and 19 - tenant #{member_number} must be a full time student or prefer not to say." - end - end - end - - index = index+1 - end - end - 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 + public_send("validate_#{question_to_validate}", record) if respond_to?("validate_#{question_to_validate}") else validation_methods = public_methods.select { |method| method.starts_with?("validate_") } validation_methods.each { |meth| public_send(meth, record) } @@ -209,6 +132,14 @@ private 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 diff --git a/app/validations/household_validations.rb b/app/validations/household_validations.rb index 8ff4e684a..e0e17d97d 100644 --- a/app/validations/household_validations.rb +++ b/app/validations/household_validations.rb @@ -1,11 +1,5 @@ module HouseholdValidations # Validations methods need to be called 'validate_' to run on model save - 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_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" @@ -56,6 +50,16 @@ module HouseholdValidations end end + def validate_other_household_members(record) + (1..8).each do |n| + validate_person_age(record, n) + validate_person_age_matches_economic_status(record, n) + validate_person_age_matches_relationship(record, n) if n > 1 + validate_person_age_and_gender_match_economic_status(record, n) + end + validate_partner_count(record) + end + private def women_of_child_bearing_age_in_household(record) @@ -65,4 +69,62 @@ private 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 + if age >= 16 && age <= 19 && (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_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_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).map { |n| record.public_send("person_#{n}_relationship") }.uniq.count + if partner_count > 1 + record.errors.add :base, "Number of partners cannot be greater than 1" + end + end end diff --git a/spec/models/case_log_spec.rb b/spec/models/case_log_spec.rb index 5e42ebce9..9c39c3a1a 100644 --- a/spec/models/case_log_spec.rb +++ b/spec/models/case_log_spec.rb @@ -206,6 +206,12 @@ 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 + end + context "other tenancy type validation" do it "must be provided if tenancy type was given as other" do expect { From 656c7bda2637d07375dfb56fef9a702c1114f2c9 Mon Sep 17 00:00:00 2001 From: baarkerlounger Date: Fri, 29 Oct 2021 11:41:45 +0100 Subject: [PATCH 09/18] Specs passing --- app/models/case_log.rb | 5 +++-- app/validations/financial_validations.rb | 3 ++- app/validations/household_validations.rb | 23 ++++++++++++++--------- app/validations/property_validations.rb | 5 +++-- app/validations/tenancy_validations.rb | 3 ++- docs/api/DLUHC-CORE-Data.v1.json | 4 ++-- spec/requests/case_log_controller_spec.rb | 4 ++-- 7 files changed, 28 insertions(+), 19 deletions(-) diff --git a/app/models/case_log.rb b/app/models/case_log.rb index 9e23699e1..e9afa7807 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -1,5 +1,6 @@ class CaseLogValidator < ActiveModel::Validator - # Validations methods need to be called 'validate_' to run on model save + # Validations methods need to be called 'validate_' to run on model save + # or 'validate_' to run on submit as well include HouseholdValidations include PropertyValidations include FinancialValidations @@ -132,7 +133,7 @@ private 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" diff --git a/app/validations/financial_validations.rb b/app/validations/financial_validations.rb index 90093e134..3567e334d 100644 --- a/app/validations/financial_validations.rb +++ b/app/validations/financial_validations.rb @@ -1,5 +1,6 @@ module FinancialValidations - # Validations methods need to be called 'validate_' to run on model save + # Validations methods need to be called 'validate_' 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." diff --git a/app/validations/household_validations.rb b/app/validations/household_validations.rb index e0e17d97d..d458b7607 100644 --- a/app/validations/household_validations.rb +++ b/app/validations/household_validations.rb @@ -1,5 +1,6 @@ module HouseholdValidations - # Validations methods need to be called 'validate_' to run on model save + # Validations methods need to be called 'validate_' 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" @@ -50,16 +51,20 @@ module HouseholdValidations end end - def validate_other_household_members(record) - (1..8).each do |n| + 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) if n > 1 + validate_person_age_matches_relationship(record, n) validate_person_age_and_gender_match_economic_status(record, n) end validate_partner_count(record) end + def validate_person_1_age(record) + validate_person_age(record, 1) + end + private def women_of_child_bearing_age_in_household(record) @@ -72,8 +77,8 @@ private def validate_person_age(record, person_num) age = record.public_send("person_#{person_num}_age") - return unless 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 @@ -98,7 +103,7 @@ private 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 + 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" @@ -122,9 +127,9 @@ private 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).map { |n| record.public_send("person_#{n}_relationship") }.uniq.count + partner_count = (2..8).select { |n| record.public_send("person_#{n}_relationship") == "Partner" }.count if partner_count > 1 - record.errors.add :base, "Number of partners cannot be greater than 1" + record.errors.add :base, "Number of partners cannot be greater than 1" end end end diff --git a/app/validations/property_validations.rb b/app/validations/property_validations.rb index 9d40a2ee5..65dde4a08 100644 --- a/app/validations/property_validations.rb +++ b/app/validations/property_validations.rb @@ -1,8 +1,9 @@ module PropertyValidations - # Validations methods need to be called 'validate_' to run on model save + # Validations methods need to be called 'validate_' 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, "Must be between 0 and 20" + record.errors.add :property_number_of_times_relet, "Property number of times relet must be between 0 and 20" end end end diff --git a/app/validations/tenancy_validations.rb b/app/validations/tenancy_validations.rb index 063b4672c..5738bc2a3 100644 --- a/app/validations/tenancy_validations.rb +++ b/app/validations/tenancy_validations.rb @@ -1,5 +1,6 @@ module TenancyValidations - # Validations methods need to be called 'validate_' to run on model save + # Validations methods need to be called 'validate_' 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) diff --git a/docs/api/DLUHC-CORE-Data.v1.json b/docs/api/DLUHC-CORE-Data.v1.json index 3820d9aee..fed09b9d6 100644 --- a/docs/api/DLUHC-CORE-Data.v1.json +++ b/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 0 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 0 and 120" ] } } diff --git a/spec/requests/case_log_controller_spec.rb b/spec/requests/case_log_controller_spec.rb index 6ddbd0130..50a2250ed 100644 --- a/spec/requests/case_log_controller_spec.rb +++ b/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 0 and 120"]]]) end end @@ -165,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 0 and 120"] }) end end From d240e299bd27f363fba936196ab52b618babdeb2 Mon Sep 17 00:00:00 2001 From: baarkerlounger Date: Fri, 29 Oct 2021 11:56:57 +0100 Subject: [PATCH 10/18] Add some test for our validations --- app/validations/household_validations.rb | 15 ++++++++-- spec/models/case_log_spec.rb | 36 +++++++++++++++++++++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/app/validations/household_validations.rb b/app/validations/household_validations.rb index d458b7607..347b87c59 100644 --- a/app/validations/household_validations.rb +++ b/app/validations/household_validations.rb @@ -57,6 +57,7 @@ module HouseholdValidations 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 @@ -95,9 +96,6 @@ private 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 - if age >= 16 && age <= 19 && (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_matches_relationship(record, person_num) @@ -110,6 +108,17 @@ private 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") diff --git a/spec/models/case_log_spec.rb b/spec/models/case_log_spec.rb index 9c39c3a1a..80f45e64a 100644 --- a/spec/models/case_log_spec.rb +++ b/spec/models/case_log_spec.rb @@ -206,10 +206,38 @@ 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 + 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 From ee2323a805ec2e74af3899be72b81b207f647e98 Mon Sep 17 00:00:00 2001 From: magicmilo Date: Fri, 29 Oct 2021 13:24:53 +0100 Subject: [PATCH 11/18] Switch to household number of other members --- app/models/case_log.rb | 19 +++++-------------- spec/models/case_log_spec.rb | 11 ++++++++++- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/models/case_log.rb b/app/models/case_log.rb index fde1c3101..2f411021f 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -106,16 +106,16 @@ class CaseLogValidator < ActiveModel::Validator end def validate_shared_housing_rooms(record) - number_of_tenants = people_in_household(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 people_in_household(record) > 1 - if record.property_unit_type.include?("Shared") && (record.property_number_of_bedrooms.to_i == 0 || record.property_number_of_bedrooms.to_i > 7) - record.errors.add :property_unit_type, "A shared house must have 1 to 7 bedrooms" + unless record.household_number_of_other_members.nil? + if record.household_number_of_other_members > 0 + if record.property_unit_type.include?("Shared") && (record.property_number_of_bedrooms.to_i == 0 || record.property_number_of_bedrooms.to_i > 7) + record.errors.add :property_unit_type, "A shared house must have 1 to 7 bedrooms" + end end end @@ -163,15 +163,6 @@ private record["person_#{n}_gender"] == "Female" && record["person_#{n}_age"] >= 16 && record["person_#{n}_age"] <= 50 end end - - def people_in_household(record) - count = 0 - (1..8).any? do |n| - next if record["person_#{n}_gender"].nil? || record["person_#{n}_age"].nil? - count += 1 - end - return count - end end class CaseLog < ApplicationRecord diff --git a/spec/models/case_log_spec.rb b/spec/models/case_log_spec.rb index 317794b60..df480af78 100644 --- a/spec/models/case_log_spec.rb +++ b/spec/models/case_log_spec.rb @@ -108,7 +108,16 @@ RSpec.describe Form, type: :model do it "you must answer less than 8 bedrooms" do expect { CaseLog.create!(property_unit_type: "Shared bungalow", - property_number_of_bedrooms: 8) + 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 From 31b594d51c873d93834b2162e8a2ba6793f4531b Mon Sep 17 00:00:00 2001 From: magicmilo Date: Fri, 29 Oct 2021 13:37:27 +0100 Subject: [PATCH 12/18] switched to between for range --- app/models/case_log.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/case_log.rb b/app/models/case_log.rb index c0ab8bbd5..da8110796 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -119,13 +119,13 @@ class CaseLogValidator < ActiveModel::Validator unless record.household_number_of_other_members.nil? if record.household_number_of_other_members > 0 - if record.property_unit_type.include?("Shared") && (record.property_number_of_bedrooms.to_i == 0 || record.property_number_of_bedrooms.to_i > 7) + if 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 end end - if record.property_unit_type.include?("Shared") && (record.property_number_of_bedrooms.to_i == 0 || record.property_number_of_bedrooms.to_i > 3) + 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 From a83a0c53fd83c2fd71ec55a7c301d0d9de3d16f2 Mon Sep 17 00:00:00 2001 From: magicmilo Date: Mon, 1 Nov 2021 10:01:53 +0000 Subject: [PATCH 13/18] fix merge and move validation --- app/models/case_log.rb | 77 ------------------------ app/validations/household_validations.rb | 20 ++++++ 2 files changed, 20 insertions(+), 77 deletions(-) diff --git a/app/models/case_log.rb b/app/models/case_log.rb index da8110796..2761e2223 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -2,53 +2,12 @@ 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." @@ -70,22 +29,6 @@ class CaseLogValidator < ActiveModel::Validator 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? @@ -111,26 +54,6 @@ class CaseLogValidator < ActiveModel::Validator include FinancialValidations include TenancyValidations - 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 - - unless record.household_number_of_other_members.nil? - if record.household_number_of_other_members > 0 - if 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 - end - 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 - 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 diff --git a/app/validations/household_validations.rb b/app/validations/household_validations.rb index 8ff4e684a..769266748 100644 --- a/app/validations/household_validations.rb +++ b/app/validations/household_validations.rb @@ -56,6 +56,26 @@ module HouseholdValidations end 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 + + unless record.household_number_of_other_members.nil? + if record.household_number_of_other_members > 0 + if 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 + end + 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) From 6ca1fa1bc263dca0797576d62fa6f6f58b057fbc Mon Sep 17 00:00:00 2001 From: magicmilo Date: Mon, 1 Nov 2021 10:16:01 +0000 Subject: [PATCH 14/18] fix merge --- app/models/case_log.rb | 49 ------------------------------------------ 1 file changed, 49 deletions(-) diff --git a/app/models/case_log.rb b/app/models/case_log.rb index 2761e2223..ffe5e5397 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -1,53 +1,4 @@ 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_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_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_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_' to run on model save include HouseholdValidations include PropertyValidations From ba9cdd31a2799bfcb663602f1afce3014615d393 Mon Sep 17 00:00:00 2001 From: Matthew Phelan Date: Tue, 2 Nov 2021 09:00:35 +0000 Subject: [PATCH 15/18] Code review updates --- app/validations/household_validations.rb | 9 ++++++++- docs/api/DLUHC-CORE-Data.v1.json | 4 ++-- spec/factories/case_log.rb | 2 +- spec/features/case_log_spec.rb | 2 +- spec/requests/case_log_controller_spec.rb | 4 ++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/validations/household_validations.rb b/app/validations/household_validations.rb index 347b87c59..45e881a4f 100644 --- a/app/validations/household_validations.rb +++ b/app/validations/household_validations.rb @@ -63,7 +63,14 @@ module HouseholdValidations end def validate_person_1_age(record) - validate_person_age(record, 1) + 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 private diff --git a/docs/api/DLUHC-CORE-Data.v1.json b/docs/api/DLUHC-CORE-Data.v1.json index fed09b9d6..e4e08d15f 100644 --- a/docs/api/DLUHC-CORE-Data.v1.json +++ b/docs/api/DLUHC-CORE-Data.v1.json @@ -110,7 +110,7 @@ "value": { "errors": { "person_1_age": [ - "Tenant age must be an integer 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 an integer between 0 and 120" + "Tenant age must be an integer between 16 and 120" ] } } diff --git a/spec/factories/case_log.rb b/spec/factories/case_log.rb index 6e5960329..bed593844 100644 --- a/spec/factories/case_log.rb +++ b/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 } diff --git a/spec/features/case_log_spec.rb b/spec/features/case_log_spec.rb index 587fc80f1..6d26628f2 100644 --- a/spec/features/case_log_spec.rb +++ b/spec/features/case_log_spec.rb @@ -184,7 +184,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 diff --git a/spec/requests/case_log_controller_spec.rb b/spec/requests/case_log_controller_spec.rb index 50a2250ed..d3f67683b 100644 --- a/spec/requests/case_log_controller_spec.rb +++ b/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", ["Property number of times relet must be between 0 and 20"]], ["person_1_age", ["Tenant age must be an integer 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 @@ -165,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 an integer 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 From d92dd2d8b18bdc8ad9b546e2398bbe7002ae5824 Mon Sep 17 00:00:00 2001 From: baarkerlounger Date: Tue, 2 Nov 2021 09:18:19 +0000 Subject: [PATCH 16/18] Rubocop --- app/validations/household_validations.rb | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/app/validations/household_validations.rb b/app/validations/household_validations.rb index 0ee9012a3..0bfa142d5 100644 --- a/app/validations/household_validations.rb +++ b/app/validations/household_validations.rb @@ -64,6 +64,7 @@ module HouseholdValidations 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 @@ -71,23 +72,19 @@ module HouseholdValidations def validate_person_1_economic(record) validate_person_age_matches_economic_status(record, 1) - end - + 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 - unless record.household_number_of_other_members.nil? - if record.household_number_of_other_members > 0 - if 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 - 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) + 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 @@ -152,7 +149,6 @@ private 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 @@ -162,8 +158,8 @@ private 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).select { |n| record.public_send("person_#{n}_relationship") == "Partner" }.count + # 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 From 81ff68bb1162a51a4d1c3b53186a50f707fae93f Mon Sep 17 00:00:00 2001 From: Daniel Baark <5101747+baarkerlounger@users.noreply.github.com> Date: Tue, 2 Nov 2021 09:38:06 +0000 Subject: [PATCH 17/18] CLDC-510: Income range soft validations (#72) * Init * Add some tests * Add failing spec * Move soft validations into a module * Update test * Rubocop * Scope are auto created by enums * Rename folder to validations * No instance variable * Commit both lines * Add error indication * Make partial slightly more generic * Fix back link * Write failing test * All specs currently passing. Can this be real? * Check page should have an override question * Fix back button for check answers pages * Don't really need a wrapper method for the validations * We're really validating a page here not a question * Dup variable * Bit of a nasty hack but maybe better than deriving back link? * Set a no cache header instead of reloading * Move a teeny bit of logic out of the controller * Rubocop * Extract method --- app/controllers/case_logs_controller.rb | 27 +++---- app/models/case_log.rb | 22 +++--- app/models/form.rb | 9 +++ app/validations/soft_validations.rb | 45 ++++++++++++ .../_validation_override_question.html.erb | 11 +++ app/views/form/page.html.erb | 10 ++- config/forms/2021_2022.json | 9 +++ .../20211028090049_add_net_income_override.rb | 5 ++ db/schema.rb | 1 + spec/controllers/case_logs_controller_spec.rb | 14 ++-- spec/features/case_log_spec.rb | 72 +++++++++++++++++-- spec/fixtures/forms/test_form.json | 9 +++ 12 files changed, 196 insertions(+), 38 deletions(-) create mode 100644 app/validations/soft_validations.rb create mode 100644 app/views/form/_validation_override_question.html.erb create mode 100644 db/migrate/20211028090049_add_net_income_override.rb diff --git a/app/controllers/case_logs_controller.rb b/app/controllers/case_logs_controller.rb index 48fe5726d..f8bef385a 100644 --- a/app/controllers/case_logs_controller.rb +++ b/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 diff --git a/app/models/case_log.rb b/app/models/case_log.rb index e9afa7807..9c6660052 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -10,9 +10,9 @@ 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. - question_to_validate = options[:previous_page] - if question_to_validate - public_send("validate_#{question_to_validate}", record) if respond_to?("validate_#{question_to_validate}") + page_to_validate = options[:page] + if page_to_validate + public_send("validate_#{page_to_validate}", record) if respond_to?("validate_#{page_to_validate}") else validation_methods = public_methods.select { |method| method.starts_with?("validate_") } validation_methods.each { |meth| public_send(meth, record) } @@ -36,25 +36,19 @@ 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 @@ -125,6 +119,10 @@ private dynamically_not_required << "fixed_term_tenancy" end + 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 diff --git a/app/models/form.rb b/app/models/form.rb index fc11d2b36..ea803e5b6 100644 --- a/app/models/form.rb +++ b/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 diff --git a/app/validations/soft_validations.rb b/app/validations/soft_validations.rb new file mode 100644 index 000000000..b62490130 --- /dev/null +++ b/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 diff --git a/app/views/form/_validation_override_question.html.erb b/app/views/form/_validation_override_question.html.erb new file mode 100644 index 000000000..947791e75 --- /dev/null +++ b/app/views/form/_validation_override_question.html.erb @@ -0,0 +1,11 @@ +
+ <%= 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 %> +
diff --git a/app/views/form/page.html.erb b/app/views/form/page.html.erb index 878fefd66..dca0b7672 100644 --- a/app/views/form/page.html.erb +++ b/app/views/form/page.html.erb @@ -1,5 +1,7 @@ + + <% 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 @@ <% 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 %> diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index 9f185b120..cb74d6edc 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -1492,6 +1492,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": { diff --git a/db/migrate/20211028090049_add_net_income_override.rb b/db/migrate/20211028090049_add_net_income_override.rb new file mode 100644 index 000000000..f776ada87 --- /dev/null +++ b/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 diff --git a/db/schema.rb b/db/schema.rb index 5c4ffd1ea..7f75e5e87 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -133,6 +133,7 @@ ActiveRecord::Schema.define(version: 2021_10_28_095000) do t.boolean "reasonable_preference_reason_do_not_know" t.datetime "discarded_at" t.string "other_tenancy_type" + t.boolean "override_net_income_validation" t.string "net_income_known" t.index ["discarded_at"], name: "index_case_logs_on_discarded_at" end diff --git a/spec/controllers/case_logs_controller_spec.rb b/spec/controllers/case_logs_controller_spec.rb index 29b129e5e..3adcf8335 100644 --- a/spec/controllers/case_logs_controller_spec.rb +++ b/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 diff --git a/spec/features/case_log_spec.rb b/spec/features/case_log_spec.rb index 6d26628f2..f46a64a3e 100644 --- a/spec/features/case_log_spec.rb +++ b/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}") @@ -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") diff --git a/spec/fixtures/forms/test_form.json b/spec/fixtures/forms/test_form.json index 35590cb3e..2bcd34aaa 100644 --- a/spec/fixtures/forms/test_form.json +++ b/spec/fixtures/forms/test_form.json @@ -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": { From 0a9851b43e8eb198177686f4b11f783717f2d531 Mon Sep 17 00:00:00 2001 From: Daniel Baark <5101747+baarkerlounger@users.noreply.github.com> Date: Thu, 4 Nov 2021 09:49:44 +0000 Subject: [PATCH 18/18] CLDC-639: Active admin (#75) * Install ActiveAdmin * Tidy up dashboard * Set time zone as per Gov UK recommendations * Add chartkick for some basic admin dashboard charts --- Gemfile | 4 + Gemfile.lock | 50 +++ app/admin/case_logs.rb | 20 ++ app/admin/dashboard.rb | 32 ++ app/javascript/packs/active_admin.js | 7 + app/javascript/packs/active_admin/print.scss | 2 + app/javascript/stylesheets/active_admin.scss | 17 + config/application.rb | 2 +- config/initializers/active_admin.rb | 335 +++++++++++++++++++ config/routes.rb | 3 +- config/webpack/environment.js | 2 + config/webpack/plugins/jquery.js | 7 + package.json | 3 + yarn.lock | 50 +++ 14 files changed, 532 insertions(+), 2 deletions(-) create mode 100644 app/admin/case_logs.rb create mode 100644 app/admin/dashboard.rb create mode 100644 app/javascript/packs/active_admin.js create mode 100644 app/javascript/packs/active_admin/print.scss create mode 100644 app/javascript/stylesheets/active_admin.scss create mode 100644 config/initializers/active_admin.rb create mode 100644 config/webpack/plugins/jquery.js diff --git a/Gemfile b/Gemfile index c249007c2..fa743fa4a 100644 --- a/Gemfile +++ b/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 diff --git a/Gemfile.lock b/Gemfile.lock index 8abc24776..386f2d68b 100644 --- a/Gemfile.lock +++ b/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 diff --git a/app/admin/case_logs.rb b/app/admin/case_logs.rb new file mode 100644 index 000000000..cd303787e --- /dev/null +++ b/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 diff --git a/app/admin/dashboard.rb b/app/admin/dashboard.rb new file mode 100644 index 000000000..4105f2304 --- /dev/null +++ b/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 diff --git a/app/javascript/packs/active_admin.js b/app/javascript/packs/active_admin.js new file mode 100644 index 000000000..286b2e630 --- /dev/null +++ b/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" diff --git a/app/javascript/packs/active_admin/print.scss b/app/javascript/packs/active_admin/print.scss new file mode 100644 index 000000000..79ac0361c --- /dev/null +++ b/app/javascript/packs/active_admin/print.scss @@ -0,0 +1,2 @@ +/* Active Admin Print Stylesheet */ +@import "~@activeadmin/activeadmin/src/scss/print"; diff --git a/app/javascript/stylesheets/active_admin.scss b/app/javascript/stylesheets/active_admin.scss new file mode 100644 index 000000000..762d851fb --- /dev/null +++ b/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; } diff --git a/config/application.rb b/config/application.rb index 45edf3531..62eda1120 100644 --- a/config/application.rb +++ b/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. diff --git a/config/initializers/active_admin.rb b/config/initializers/active_admin.rb new file mode 100644 index 000000000..4ef658194 --- /dev/null +++ b/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 diff --git a/config/routes.rb b/config/routes.rb index 5a95075fd..63163937b 100644 --- a/config/routes.rb +++ b/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" diff --git a/config/webpack/environment.js b/config/webpack/environment.js index d16d9af74..34ab86b05 100644 --- a/config/webpack/environment.js +++ b/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 diff --git a/config/webpack/plugins/jquery.js b/config/webpack/plugins/jquery.js new file mode 100644 index 000000000..54c61ec83 --- /dev/null +++ b/config/webpack/plugins/jquery.js @@ -0,0 +1,7 @@ +const webpack = require('webpack') + +module.exports = new webpack.ProvidePlugin({ + "$":"jquery", + "jQuery":"jquery", + "window.jQuery":"jquery" +}); diff --git a/package.json b/package.json index c7f92f096..5da73dd8c 100644 --- a/package.json +++ b/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", diff --git a/yarn.lock b/yarn.lock index ecbe92bf6..a836c6244 100644 --- a/yarn.lock +++ b/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"