From a9a14724ccc3f1e320def28cb7b965223c856d7b Mon Sep 17 00:00:00 2001 From: magicmilo Date: Mon, 15 Nov 2021 15:39:48 +0000 Subject: [PATCH 01/29] initial --- config/forms/2021_2022.json | 103 ++++++++++++++++++++----- config/forms/property_information.json | 17 ++++ 2 files changed, 100 insertions(+), 20 deletions(-) create mode 100644 config/forms/property_information.json diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index 859e5fa14..0f59cad7f 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -1127,13 +1127,69 @@ "property_information": { "label": "Property information", "pages": { - "property_location": { + "property_reference": { + "header": "", + "description": "", + "questions": { + "propcode": { + "check_answer_label": "What’s the property reference?", + "header": "What's the property reference?", + "hint_text": "", + "type": "text" + } + } + }, + "property_postcode": { + "header": "", + "description": "", + "questions": { + "do_you_know_the_postcode": { + "check_answer_label": "Do you know the property postcode?", + "header": "do_you_know_the_postcode?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Yes", + "1": "No" + }, + "conditional_for": { + "property_postcode": ["Yes"] + } + }, + "property_postcode": { + "check_answer_label": "", + "header": "", + "hint_text": "", + "type": "text" + } + } + }, + "do_you_know_the_local_authority": { + "header": "", + "description": "", + "questions": { + "do_you_know_the_local_authority": { + "check_answer_label": "Do you know what local authority the property is located in?", + "header": "Do you know what local authority the property is located in?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Yes", + "1": "No" + } + } + }, + "conditional_route_to": { + "why_dont_you_know_la": { "do_you_know_the_local_authority": "No" } + } + }, + "select_local_authority": { "header": "", "description": "", "questions": { - "la": { - "check_answer_label": "Property Location", - "header": "Property location", + "select_local_authority": { + "check_answer_label": "Local Authority", + "header": "Select a local authority", "hint_text": "", "type": "radio", "answer_options": { @@ -1454,16 +1510,35 @@ } } }, - "property_postcode": { + "why_dont_you_know_la": { "header": "", "description": "", "questions": { - "property_postcode": { - "check_answer_label": "What was the previous postcode?", - "header": "What is the property's postcode?", + "why_dont_you_know_la": { + "check_answer_label": "Reason for not knowing local authority", + "header": "Give a reason why you don't know the postcode or local authority", "hint_text": "", "type": "text" } + }, + "conditional_route_to": { + "organisation_details": { "do_you_know_the_local_authority": "No" } + } + }, + "first_time_property_let_as_social_housing": { + "header": "", + "description": "", + "questions": { + "first_time_property_let_as_social_housing": { + "check_answer_label": "Which type was the property most recently let as?", + "header": "Is this property a relet?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Yes", + "1": "No" + } + } } }, "property_relet": { @@ -1510,18 +1585,6 @@ } } }, - "property_reference": { - "header": "", - "description": "", - "questions": { - "propcode": { - "check_answer_label": "What’s the property reference?", - "header": "What's the property reference?", - "hint_text": "", - "type": "text" - } - } - }, "property_unit_type": { "header": "", "description": "", diff --git a/config/forms/property_information.json b/config/forms/property_information.json new file mode 100644 index 000000000..e808bdd46 --- /dev/null +++ b/config/forms/property_information.json @@ -0,0 +1,17 @@ +{ + "form_type": "lettings", + "start_year": 2021, + "end_year": 2022, + "sections": { + "property_information": { + "label": "Property Information", + "subsections": { + "about_this_log": { + "label": "About this log", + "pages": { + } + } + } + } + } +} \ No newline at end of file From ebf0d5dddb0db204b27f03ba2b6dc6e2b75811ff Mon Sep 17 00:00:00 2001 From: magicmilo Date: Tue, 16 Nov 2021 17:42:43 +0000 Subject: [PATCH 02/29] intial --- config/forms/2021_2022.json | 126 ++++++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 50 deletions(-) diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index 62dd50fc3..5be33f2ae 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -1520,7 +1520,8 @@ "312": "York" } } - } + }, + "default_next_page": "first_time_property_let_as_social_housing" }, "why_dont_you_know_la": { "header": "", @@ -1532,9 +1533,6 @@ "hint_text": "", "type": "text" } - }, - "conditional_route_to": { - "organisation_details": { "do_you_know_the_local_authority": "No" } } }, "first_time_property_let_as_social_housing": { @@ -1578,25 +1576,40 @@ "header": "What is the reason for the property vacancy?", "hint_text": "", "type": "radio", - "answer_options": { - "0": "First let of newbuild property", - "1": "First let of conversion/rehabilitation/acquired property", - "2": "First let of leased property", - "3": "Relet - tenant evicted due to arrears", - "4": "Relet - tenant evicted due to ASB or other reason", - "5": "Relet - tenant died (no succession)", - "6": "Relet - tenant moved to other social housing provider", - "7": "Relet - tenant abandoned property", - "8": "Relet - tenant moved to private sector or other accommodation", - "9": "Relet - to tenant who occupied same property as temporary accommodation", - "10": "Relet – internal transfer (excluding renewals of a fixed-term tenancy)", - "11": "Relet – renewal of fixed-term tenancy", - "12": "Relet – tenant moved to care home", - "13": "Relet – tenant involved in a succession downsize" + "answer_options": { + "0": "Renewal of fixed-term tenancy", + "1": "Internal transfer (excluding renewals of a fixed-term tenancy)", + "2": "Relet to tenant who occupied same property as temporary accommodation", + "3": "Tenant involved in a succession downsize", + "4": "Tenant moved to private sector or other accommodation", + "5": "Tenant moved to other social housing provider", + "6": "Tenant moved to care home", + "7": "Tenant abandoned property", + "8": "Tenant evicted due to arrears", + "9": "Tenant evicted due to ASB or other reason", + "10": "Previous tenant passed away (no succession)", + "11": "First let of newbuild property", + "12": "First let of conversion/rehabilitation/acquired property", + "13": "First let of leased property" } } } }, + "property_number_of_times_relet": { + "header": "", + "description": "", + "questions": { + "offered": { + "check_answer_label": "How many times has this unit been previously offered since becoming available for relet since the last tenancy ended or as a first let?", + "header": "How many times has this unit been previously offered since becoming available for relet since the last tenancy ended or as a first let? ", + "hint_text": "For an Affordable Rent or Intermediate Rent Letting, only include number of offers as that type. For a property let at the first attempt enter '0' ", + "type": "numeric", + "min": 0, + "max": 150, + "step": 1 + } + } + }, "property_unit_type": { "header": "", "description": "", @@ -1619,54 +1632,46 @@ } } }, - "property_number_of_bedrooms": { + "property_building_type": { "header": "", "description": "", "questions": { - "beds": { - "check_answer_label": "How many bedrooms are there in the property?", - "header": "How many bedrooms are there in the property?", - "hint_text": "If shared accommodation, enter number of bedrooms occupied by this household; a bed-sit has 1 bedroom", - "type": "numeric", - "min": 0, - "max": 150, - "step": 1 + "": { + "check_answer_label": "Building type", + "header": "Which type of building is the property?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Purpose built", + "1": "Converted from previous residential or non-residential property" + } } } }, - "property_major_repairs": { + "property_wheelchair_accessible": { "header": "", "description": "", "questions": { - "majorrepairs": { - "check_answer_label": "Were major repairs carried out during the void period?", - "header": "Were any major repairs completed during the void period?", + "wchair": { + "check_answer_label": "Is property built or adapted to wheelchair user standards?", + "header": "Is property built or adapted to wheelchair user standards?", "hint_text": "", "type": "radio", "answer_options": { "0": "Yes", "1": "No" - }, - "conditional_for": { - "mrcdate": ["Yes"] } - }, - "mrcdate": { - "check_answer_label": "What was the major repairs completion date?", - "header": "What was the major repairs completion date?", - "hint_text": "For example, 27 3 2007", - "type": "date" } } }, - "property_number_of_times_relet": { + "property_number_of_bedrooms": { "header": "", "description": "", "questions": { - "offered": { - "check_answer_label": "How many times has this unit been previously offered since becoming available for relet since the last tenancy ended or as a first let?", - "header": "How many times has this unit been previously offered since becoming available for relet since the last tenancy ended or as a first let? ", - "hint_text": "For an Affordable Rent or Intermediate Rent Letting, only include number of offers as that type. For a property let at the first attempt enter '0' ", + "beds": { + "check_answer_label": "How many bedrooms are there in the property?", + "header": "How many bedrooms are there in the property?", + "hint_text": "If shared accommodation, enter number of bedrooms occupied by this household; a bed-sit has 1 bedroom", "type": "numeric", "min": 0, "max": 150, @@ -1674,19 +1679,40 @@ } } }, - "property_wheelchair_accessible": { + "void_or_renewal_date": { "header": "", "description": "", "questions": { - "wchair": { - "check_answer_label": "Is property built or adapted to wheelchair user standards?", - "header": "Is property built or adapted to wheelchair user standards?", + "beds": { + "check_answer_label": "void/renewal date", + "header": "What is the void or renewal date?", + "hint_text": "", + "type": "date" + } + } + }, + "property_major_repairs": { + "header": "", + "description": "", + "questions": { + "majorrepairs": { + "check_answer_label": "Were major repairs carried out during the void period?", + "header": "Were any major repairs completed during the void period?", "hint_text": "", "type": "radio", "answer_options": { "0": "Yes", "1": "No" + }, + "conditional_for": { + "mrcdate": ["Yes"] } + }, + "mrcdate": { + "check_answer_label": "What was the major repairs completion date?", + "header": "What was the major repairs completion date?", + "hint_text": "For example, 27 3 2007", + "type": "date" } } } From 3d729c1823d6489cfb86727b7f124707abe4e751 Mon Sep 17 00:00:00 2001 From: magicmilo Date: Tue, 16 Nov 2021 19:45:40 +0000 Subject: [PATCH 03/29] remove test json --- config/forms/property_information.json | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 config/forms/property_information.json diff --git a/config/forms/property_information.json b/config/forms/property_information.json deleted file mode 100644 index e808bdd46..000000000 --- a/config/forms/property_information.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "form_type": "lettings", - "start_year": 2021, - "end_year": 2022, - "sections": { - "property_information": { - "label": "Property Information", - "subsections": { - "about_this_log": { - "label": "About this log", - "pages": { - } - } - } - } - } -} \ No newline at end of file From 9647a3e56250c5adf06867878a777d88e8928079 Mon Sep 17 00:00:00 2001 From: magicmilo Date: Thu, 18 Nov 2021 16:24:09 +0000 Subject: [PATCH 04/29] Add in the depends_on --- config/forms/2021_2022.json | 73 ++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index 66caacf75..499728a88 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -1131,7 +1131,7 @@ "property_postcode": ["Yes"] } }, - "property_postcode": { + "postcode": { "check_answer_label": "", "header": "", "hint_text": "", @@ -1484,7 +1484,7 @@ } } }, - "default_next_page": "first_time_property_let_as_social_housing" + "depends_on": { "do_you_know_the_local_authority": "Yes" } }, "why_dont_you_know_la": { "header": "", @@ -1496,7 +1496,8 @@ "hint_text": "", "type": "text" } - } + }, + "depends_on": { "do_you_know_the_local_authority": "No" } }, "first_time_property_let_as_social_housing": { "header": "", @@ -1514,21 +1515,24 @@ } } }, - "property_relet": { + "type_property_most_recently_let_as": { "header": "", "description": "", "questions": { - "property_relet": { - "check_answer_label": "Which type was the property most recently let as?", - "header": "Is this property a relet?", + "type_property_most_recently_let_as": { + "check_answer_label": "Type property most recently let as", + "header": "Which type was the property most recently let as?", "hint_text": "", "type": "radio", "answer_options": { - "0": "Yes", - "1": "No" + "0": "Social rent basis", + "1": "Affordable rent basis", + "2": "Intermediate rent basis", + "3": "Do not know" } } - } + }, + "depends_on": { "first_time_property_let_as_social_housing": "No" } }, "property_vacancy_reason": { "header": "", @@ -1550,10 +1554,24 @@ "7": "Tenant abandoned property", "8": "Tenant evicted due to arrears", "9": "Tenant evicted due to ASB or other reason", - "10": "Previous tenant passed away (no succession)", + "10": "Previous tenant passed away (no succession)" + } + } + } + }, + "property_vacancy_reason": { + "header": "", + "description": "", + "questions": { + "rsnvac": { + "check_answer_label": "What is the reason for the property vacancy?", + "header": "What is the reason for the property vacancy?", + "hint_text": "", + "type": "radio", + "answer_options": { "11": "First let of newbuild property", "12": "First let of conversion/rehabilitation/acquired property", - "13": "First let of leased property" + "13": "First let of leased property" } } } @@ -1563,15 +1581,32 @@ "description": "", "questions": { "offered": { - "check_answer_label": "How many times has this unit been previously offered since becoming available for relet since the last tenancy ended or as a first let?", - "header": "How many times has this unit been previously offered since becoming available for relet since the last tenancy ended or as a first let? ", - "hint_text": "For an Affordable Rent or Intermediate Rent Letting, only include number of offers as that type. For a property let at the first attempt enter '0' ", + "check_answer_label": "How many times has this unit been previously offered since becoming available for relet since becoming available for relet (after the last tenancy ended)?", + "header": "How many times has this unit been previously offered since becoming available for relet since becoming available for relet (after the last tenancy ended)?", + "hint_text": "If the property is being let for the first time, enter 0", "type": "numeric", "min": 0, "max": 150, "step": 1 } - } + }, + "depends_on": { "first_time_property_let_as_social_housing": "No" } + }, + "property_number_of_times_relet": { + "header": "", + "description": "", + "questions": { + "offered": { + "check_answer_label": "How many times has the property been previously offered since becoming available?", + "header": "How many times has the property been previously offered since becoming available?", + "hint_text": "If the property is being let for the first time, enter 0", + "type": "numeric", + "min": 0, + "max": 150, + "step": 1 + } + }, + "depends_on": { "first_time_property_let_as_social_housing": "Yes" } }, "property_unit_type": { "header": "", @@ -1652,7 +1687,8 @@ "hint_text": "", "type": "date" } - } + }, + "depends_on": { "rsnvac": "First let of newbuild property" } }, "property_major_repairs": { "header": "", @@ -1677,7 +1713,8 @@ "hint_text": "For example, 27 3 2007", "type": "date" } - } + }, + "depends_on": { "rsnvac": "First let of newbuild property" } } } } From 8ba2cd26b361d5e337885b916cf7c8854f30905b Mon Sep 17 00:00:00 2001 From: magicmilo Date: Thu, 18 Nov 2021 16:37:34 +0000 Subject: [PATCH 05/29] more json --- Gemfile.lock | 3 +++ config/forms/2021_2022.json | 14 ++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 04961facf..e4a47433d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -225,6 +225,8 @@ GEM minitest (5.14.4) msgpack (1.4.2) nio4r (2.5.8) + nokogiri (1.12.5-x86_64-darwin) + racc (~> 1.4) nokogiri (1.12.5-x86_64-linux) racc (~> 1.4) overcommit (0.58.0) @@ -383,6 +385,7 @@ GEM zeitwerk (2.5.1) PLATFORMS + x86_64-darwin-20 x86_64-linux DEPENDENCIES diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index 499728a88..c1e47fdac 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -1534,7 +1534,7 @@ }, "depends_on": { "first_time_property_let_as_social_housing": "No" } }, - "property_vacancy_reason": { + "property_vacancy_reason_not_first_let": { "header": "", "description": "", "questions": { @@ -1557,9 +1557,10 @@ "10": "Previous tenant passed away (no succession)" } } - } + }, + "depends_on": { "first_time_property_let_as_social_housing": "No" } }, - "property_vacancy_reason": { + "property_vacancy_reason_first_let": { "header": "", "description": "", "questions": { @@ -1574,9 +1575,10 @@ "13": "First let of leased property" } } - } + }, + "depends_on": { "first_time_property_let_as_social_housing": "Yes" } }, - "property_number_of_times_relet": { + "property_number_of_times_relet_not_social_let": { "header": "", "description": "", "questions": { @@ -1592,7 +1594,7 @@ }, "depends_on": { "first_time_property_let_as_social_housing": "No" } }, - "property_number_of_times_relet": { + "property_number_of_times_relet_social_let": { "header": "", "description": "", "questions": { From 0ba94c9266d0ae15e55e3e5518fd7d59e9db264c Mon Sep 17 00:00:00 2001 From: magicmilo Date: Fri, 19 Nov 2021 09:55:43 +0000 Subject: [PATCH 06/29] remove conditional_route_to --- config/forms/2021_2022.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index c1e47fdac..32b8440e1 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -1123,6 +1123,7 @@ "header": "do_you_know_the_postcode?", "hint_text": "", "type": "radio", + "store": "false", "answer_options": { "0": "Yes", "1": "No" @@ -1153,9 +1154,6 @@ "1": "No" } } - }, - "conditional_route_to": { - "why_dont_you_know_la": { "do_you_know_the_local_authority": "No" } } }, "select_local_authority": { From 667902e5ab57a0112032528a4b04a5072f3e392a Mon Sep 17 00:00:00 2001 From: magicmilo Date: Fri, 19 Nov 2021 12:04:29 +0000 Subject: [PATCH 07/29] add renewal general needs routing --- config/forms/2021_2022.json | 50 ++++++++++++++----- ...20211119104835_add_property_info_fields.rb | 12 +++++ db/schema.rb | 11 +++- 3 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 db/migrate/20211119104835_add_property_info_fields.rb diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index 32b8440e1..c5c60fa46 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -1123,7 +1123,6 @@ "header": "do_you_know_the_postcode?", "hint_text": "", "type": "radio", - "store": "false", "answer_options": { "0": "Yes", "1": "No" @@ -1160,7 +1159,7 @@ "header": "", "description": "", "questions": { - "select_local_authority": { + "la": { "check_answer_label": "Local Authority", "header": "Select a local authority", "hint_text": "", @@ -1510,7 +1509,8 @@ "0": "Yes", "1": "No" } - } + }, + "depends_on": { "renewal": "Not Renewal"} } }, "type_property_most_recently_let_as": { @@ -1530,7 +1530,7 @@ } } }, - "depends_on": { "first_time_property_let_as_social_housing": "No" } + "depends_on": { "first_time_property_let_as_social_housing": "No", "renewal": "Not Renewal" } }, "property_vacancy_reason_not_first_let": { "header": "", @@ -1556,7 +1556,7 @@ } } }, - "depends_on": { "first_time_property_let_as_social_housing": "No" } + "depends_on": { "first_time_property_let_as_social_housing": "No", "renewal": "Not Renewal" } }, "property_vacancy_reason_first_let": { "header": "", @@ -1574,7 +1574,7 @@ } } }, - "depends_on": { "first_time_property_let_as_social_housing": "Yes" } + "depends_on": { "first_time_property_let_as_social_housing": "Yes", "renewal": "Not Renewal" } }, "property_number_of_times_relet_not_social_let": { "header": "", @@ -1590,7 +1590,7 @@ "step": 1 } }, - "depends_on": { "first_time_property_let_as_social_housing": "No" } + "depends_on": { "first_time_property_let_as_social_housing": "No", "renewal": "Not Renewal" } }, "property_number_of_times_relet_social_let": { "header": "", @@ -1606,7 +1606,7 @@ "step": 1 } }, - "depends_on": { "first_time_property_let_as_social_housing": "Yes" } + "depends_on": { "first_time_property_let_as_social_housing": "Yes", "renewal": "Not Renewal" } }, "property_unit_type": { "header": "", @@ -1634,7 +1634,7 @@ "header": "", "description": "", "questions": { - "": { + "builtype": { "check_answer_label": "Building type", "header": "Which type of building is the property?", "hint_text": "", @@ -1675,7 +1675,8 @@ "max": 150, "step": 1 } - } + }, + "depends_on": { "needstype": "General Needs" } }, "void_or_renewal_date": { "header": "", @@ -1688,7 +1689,7 @@ "type": "date" } }, - "depends_on": { "rsnvac": "First let of newbuild property" } + "depends_on": { "rsnvac": "First let of newbuild property", "renewal": "Not Renewal" } }, "property_major_repairs": { "header": "", @@ -1705,7 +1706,8 @@ }, "conditional_for": { "mrcdate": ["Yes"] - } + }, + "depends_on": { "renewal": "Not Renewal" } }, "mrcdate": { "check_answer_label": "What was the major repairs completion date?", @@ -1715,6 +1717,30 @@ } }, "depends_on": { "rsnvac": "First let of newbuild property" } + }, + "new_build_handover_date": { + "header": "", + "description": "", + "questions": { + "majorrepairs": { + "check_answer_label": "Were major repairs carried out during the void period?", + "header": "Were any major repairs completed during the void period?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Yes", + "1": "No" + }, + "conditional_for": { + "mrcdate": ["Yes"] + }, + "depends_on": { + "renewal": "Not Renewal", + "rsnvac": "First let of conversion, rehabilitation or acquired property?", + "rsnvac": "First let of leased property" + } + } + } } } } diff --git a/db/migrate/20211119104835_add_property_info_fields.rb b/db/migrate/20211119104835_add_property_info_fields.rb new file mode 100644 index 000000000..78362fe2a --- /dev/null +++ b/db/migrate/20211119104835_add_property_info_fields.rb @@ -0,0 +1,12 @@ +class AddPropertyInfoFields < ActiveRecord::Migration[6.1] + def change + change_table :case_logs, bulk: true do |t| + t.column :do_you_know_the_postcode, :integer + t.column :do_you_know_the_local_authority, :integer + t.column :why_dont_you_know_la, :string + t.column :first_time_property_let_as_social_housing, :integer + t.column :type_property_most_recently_let_as, :integer + t.column :builtype, :integer + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 72f15b7b2..9b4b2a4bd 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_11_18_090831) do +ActiveRecord::Schema.define(version: 2021_11_19_104835) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -25,6 +25,7 @@ ActiveRecord::Schema.define(version: 2021_11_18_090831) do t.integer "ethnic" t.integer "national" t.integer "prevten" + t.string "armed_forces" t.integer "ecstat1" t.integer "hhmemb" t.string "relat2" @@ -59,6 +60,7 @@ ActiveRecord::Schema.define(version: 2021_11_18_090831) do t.integer "underoccupation_benefitcap" t.integer "leftreg" t.integer "reservist" + t.string "armed_forces_partner" t.integer "illness" t.integer "preg_occ" t.string "accessibility_requirements" @@ -152,7 +154,12 @@ ActiveRecord::Schema.define(version: 2021_11_18_090831) do t.integer "incref" t.datetime "sale_completion_date" t.datetime "startdate" - t.integer "armedforces" + t.integer "do_you_know_the_postcode" + t.integer "do_you_know_the_local_authority" + t.string "why_dont_you_know_la" + t.integer "first_time_property_let_as_social_housing" + t.integer "type_property_most_recently_let_as" + t.integer "builtype" t.index ["discarded_at"], name: "index_case_logs_on_discarded_at" end From c7a63e46b8fb44bbf8bf77c18a7ec08b19a1a505 Mon Sep 17 00:00:00 2001 From: magicmilo Date: Fri, 19 Nov 2021 15:45:11 +0000 Subject: [PATCH 08/29] change column types --- app/models/form.rb | 1 + config/forms/2021_2022.json | 28 +++++++++---------- db/migrate/20211119142809_change_renewal.rb | 7 +++++ .../20211119154133_change_column_types.rb | 21 ++++++++++++++ db/schema.rb | 14 +++++----- 5 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 db/migrate/20211119142809_change_renewal.rb create mode 100644 db/migrate/20211119154133_change_column_types.rb diff --git a/app/models/form.rb b/app/models/form.rb index 34385af99..195a51f69 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -108,6 +108,7 @@ class Form end def page_routed_to?(page, case_log) + # binding.pry return true unless (conditions = page_dependencies(page)) conditions.all? do |question, value| diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index c5c60fa46..e12961300 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -81,7 +81,7 @@ "header": "About this log", "description": "Is this a renewal to the same tenant in the same property?", "questions": { - "tenant_same_property_renewal": { + "renewal": { "check_answer_label": "", "header": "Is this a renewal to the same tenant in the same property?", "hint_text": "", @@ -1120,7 +1120,7 @@ "questions": { "do_you_know_the_postcode": { "check_answer_label": "Do you know the property postcode?", - "header": "do_you_know_the_postcode?", + "header": "Do you know the postcode?", "hint_text": "", "type": "radio", "answer_options": { @@ -1128,7 +1128,7 @@ "1": "No" }, "conditional_for": { - "property_postcode": ["Yes"] + "postcode": ["Yes"] } }, "postcode": { @@ -1509,9 +1509,9 @@ "0": "Yes", "1": "No" } - }, - "depends_on": { "renewal": "Not Renewal"} - } + } + }, + "depends_on": { "renewal": "No"} }, "type_property_most_recently_let_as": { "header": "", @@ -1530,7 +1530,7 @@ } } }, - "depends_on": { "first_time_property_let_as_social_housing": "No", "renewal": "Not Renewal" } + "depends_on": { "first_time_property_let_as_social_housing": "No", "renewal": "No" } }, "property_vacancy_reason_not_first_let": { "header": "", @@ -1556,7 +1556,7 @@ } } }, - "depends_on": { "first_time_property_let_as_social_housing": "No", "renewal": "Not Renewal" } + "depends_on": { "first_time_property_let_as_social_housing": "No", "renewal": "No" } }, "property_vacancy_reason_first_let": { "header": "", @@ -1574,7 +1574,7 @@ } } }, - "depends_on": { "first_time_property_let_as_social_housing": "Yes", "renewal": "Not Renewal" } + "depends_on": { "first_time_property_let_as_social_housing": "Yes", "renewal": "No" } }, "property_number_of_times_relet_not_social_let": { "header": "", @@ -1590,7 +1590,7 @@ "step": 1 } }, - "depends_on": { "first_time_property_let_as_social_housing": "No", "renewal": "Not Renewal" } + "depends_on": { "first_time_property_let_as_social_housing": "No", "renewal": "No" } }, "property_number_of_times_relet_social_let": { "header": "", @@ -1606,7 +1606,7 @@ "step": 1 } }, - "depends_on": { "first_time_property_let_as_social_housing": "Yes", "renewal": "Not Renewal" } + "depends_on": { "first_time_property_let_as_social_housing": "Yes", "renewal": "No" } }, "property_unit_type": { "header": "", @@ -1689,7 +1689,7 @@ "type": "date" } }, - "depends_on": { "rsnvac": "First let of newbuild property", "renewal": "Not Renewal" } + "depends_on": { "rsnvac": "First let of newbuild property", "renewal": "No" } }, "property_major_repairs": { "header": "", @@ -1707,7 +1707,7 @@ "conditional_for": { "mrcdate": ["Yes"] }, - "depends_on": { "renewal": "Not Renewal" } + "depends_on": { "renewal": "No" } }, "mrcdate": { "check_answer_label": "What was the major repairs completion date?", @@ -1735,7 +1735,7 @@ "mrcdate": ["Yes"] }, "depends_on": { - "renewal": "Not Renewal", + "renewal": "No", "rsnvac": "First let of conversion, rehabilitation or acquired property?", "rsnvac": "First let of leased property" } diff --git a/db/migrate/20211119142809_change_renewal.rb b/db/migrate/20211119142809_change_renewal.rb new file mode 100644 index 000000000..f8c244600 --- /dev/null +++ b/db/migrate/20211119142809_change_renewal.rb @@ -0,0 +1,7 @@ +class ChangeRenewal < ActiveRecord::Migration[6.1] + def change + rename_column :case_logs, :tenant_same_property_renewal, :renewal + end +end + + diff --git a/db/migrate/20211119154133_change_column_types.rb b/db/migrate/20211119154133_change_column_types.rb new file mode 100644 index 000000000..75c1c05b8 --- /dev/null +++ b/db/migrate/20211119154133_change_column_types.rb @@ -0,0 +1,21 @@ +class ChangeColumnTypes < ActiveRecord::Migration[6.1] + def up + change_table :case_logs, bulk: true do |t| + t.change :first_time_property_let_as_social_housing, :string + t.change :type_property_most_recently_let_as, :string + t.change :builtype, :string + t.change :do_you_know_the_local_authority, :string + t.change :do_you_know_the_postcode, :string + end + end + + def down + change_table :case_logs, bulk: true do |t| + t.change :first_time_property_let_as_social_housing, :int + t.change :type_property_most_recently_let_as, :int + t.change :builtype, :int + t.change :do_you_know_the_local_authority, :int + t.change :do_you_know_the_postcode, :int + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9b4b2a4bd..5ae2fff35 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_11_19_104835) do +ActiveRecord::Schema.define(version: 2021_11_19_154133) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -128,7 +128,7 @@ ActiveRecord::Schema.define(version: 2021_11_19_104835) do t.string "property_owner_organisation" t.string "property_manager_organisation" t.string "sale_or_letting" - t.string "tenant_same_property_renewal" + t.string "renewal" t.string "rent_type" t.string "intermediate_rent_product_name" t.string "needs_type" @@ -154,12 +154,12 @@ ActiveRecord::Schema.define(version: 2021_11_19_104835) do t.integer "incref" t.datetime "sale_completion_date" t.datetime "startdate" - t.integer "do_you_know_the_postcode" - t.integer "do_you_know_the_local_authority" + t.string "do_you_know_the_postcode" + t.string "do_you_know_the_local_authority" t.string "why_dont_you_know_la" - t.integer "first_time_property_let_as_social_housing" - t.integer "type_property_most_recently_let_as" - t.integer "builtype" + t.string "first_time_property_let_as_social_housing" + t.string "type_property_most_recently_let_as" + t.string "builtype" t.index ["discarded_at"], name: "index_case_logs_on_discarded_at" end From 5c3bc1db2c3acba268e6503225017cc3bb62d7b3 Mon Sep 17 00:00:00 2001 From: magicmilo Date: Fri, 19 Nov 2021 16:19:53 +0000 Subject: [PATCH 09/29] add fields to completecaselog --- spec/fixtures/complete_case_log.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/spec/fixtures/complete_case_log.json b/spec/fixtures/complete_case_log.json index fe8393178..2091ba6da 100644 --- a/spec/fixtures/complete_case_log.json +++ b/spec/fixtures/complete_case_log.json @@ -57,7 +57,7 @@ "la": "Barnet", "property_postcode": "NW1 5TY", "property_relet": "No", - "rsnvac": "Relet - tenant abandoned property", + "rsnvac": "Renewal of fixed-term tenancy", "property_reference": "P9876", "unittype_gn": "House", "property_building_type": "dummy", @@ -134,6 +134,17 @@ "postcode": "a1", "postcod2": "w3", "ppostc1": "w3", - "ppostc2": "w3" + "ppostc2": "w3", + "do_you_know_the_postcode": "No", + "do_you_know_the_local_authority": "No", + "why_dont_you_know_la": "Forgot", + "first_time_property_let_as_social_housing": "Yes", + "type_property_most_recently_let_as": "", + "property_number_of_times_relet_social_let": "1", + "builtype": "", + "property_wheelchair_accessible": "Yes", + "property_number_of_bedrooms": 2, + "void_or_renewal_date": "05/05/2020", + "new_build_handover_date": "01/01/2019" } } From b85a2a333287362ffc01f3d9bff0c6f8b6443b79 Mon Sep 17 00:00:00 2001 From: magicmilo Date: Fri, 19 Nov 2021 16:22:46 +0000 Subject: [PATCH 10/29] change rsnvac enums to correct --- app/constants/db_enums.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/constants/db_enums.rb b/app/constants/db_enums.rb index 7a1c01b19..e0cc05497 100644 --- a/app/constants/db_enums.rb +++ b/app/constants/db_enums.rb @@ -186,17 +186,17 @@ module DbEnums "First let of newbuild property" => 15, "First let of conversion/rehabilitation/acquired property" => 16, "First let of leased property" => 17, - "Relet - tenant evicted due to arrears" => 10, - "Relet - tenant evicted due to ASB or other reason" => 11, - "Relet - tenant died (no succession)" => 5, - "Relet - tenant moved to other social housing provider" => 12, - "Relet - tenant abandoned property" => 6, - "Relet - tenant moved to private sector or other accommodation" => 8, - "Relet - to tenant who occupied same property as temporary accommodation" => 9, - "Relet – internal transfer (excluding renewals of a fixed-term tenancy)" => 13, - "Relet – renewal of fixed-term tenancy" => 14, - "Relet – tenant moved to care home" => 18, - "Relet – tenant involved in a succession downsize" => 19, + "Tenant evicted due to arrears" => 10, + "Tenant evicted due to ASB or other reason" => 11, + "Tenant died (no succession)" => 5, + "Tenant moved to other social housing provider" => 12, + "Tenant abandoned property" => 6, + "Tenant moved to private sector or other accommodation" => 8, + "Relet wdto tenant who occupied same property as temporary accommodation" => 9, + "Internal transfer (excluding renewals of a fixed-term tenancy)" => 13, + "Renewal of fixed-term tenancy" => 14, + "Tenant moved to care home" => 18, + "Tenant involved in a succession downsize" => 19, } end From b1c3cae3be44cca56cf1811669dd7397e1584efc Mon Sep 17 00:00:00 2001 From: magicmilo Date: Fri, 19 Nov 2021 16:33:39 +0000 Subject: [PATCH 11/29] put rsnvac as array of possibilities dependson --- app/constants/db_enums.rb | 2 +- config/forms/2021_2022.json | 3 +-- spec/fixtures/complete_case_log.json | 9 ++++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/constants/db_enums.rb b/app/constants/db_enums.rb index e0cc05497..c94d8f77c 100644 --- a/app/constants/db_enums.rb +++ b/app/constants/db_enums.rb @@ -192,7 +192,7 @@ module DbEnums "Tenant moved to other social housing provider" => 12, "Tenant abandoned property" => 6, "Tenant moved to private sector or other accommodation" => 8, - "Relet wdto tenant who occupied same property as temporary accommodation" => 9, + "Relet to tenant who occupied same property as temporary accommodation" => 9, "Internal transfer (excluding renewals of a fixed-term tenancy)" => 13, "Renewal of fixed-term tenancy" => 14, "Tenant moved to care home" => 18, diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index e12961300..578b9d1bc 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -1736,8 +1736,7 @@ }, "depends_on": { "renewal": "No", - "rsnvac": "First let of conversion, rehabilitation or acquired property?", - "rsnvac": "First let of leased property" + "rsnvac": ["First let of conversion, rehabilitation or acquired property?", "First let of leased property"] } } } diff --git a/spec/fixtures/complete_case_log.json b/spec/fixtures/complete_case_log.json index 2091ba6da..5b0b03c65 100644 --- a/spec/fixtures/complete_case_log.json +++ b/spec/fixtures/complete_case_log.json @@ -57,13 +57,13 @@ "la": "Barnet", "property_postcode": "NW1 5TY", "property_relet": "No", - "rsnvac": "Renewal of fixed-term tenancy", + "rsnvac": "First let of newbuild property", "property_reference": "P9876", "unittype_gn": "House", "property_building_type": "dummy", "beds": 3, "property_void_date": "03/11/2019", - "majorrepairs": "Yes", + "majorrepairs": "No", "mrcdate": "05/05/2020", "mrcday": 5, "mrcmonth": 5, @@ -139,11 +139,10 @@ "do_you_know_the_local_authority": "No", "why_dont_you_know_la": "Forgot", "first_time_property_let_as_social_housing": "Yes", - "type_property_most_recently_let_as": "", + "type_property_most_recently_let_as": "Affordable rent basis", "property_number_of_times_relet_social_let": "1", - "builtype": "", + "builtype": "Purpose built", "property_wheelchair_accessible": "Yes", - "property_number_of_bedrooms": 2, "void_or_renewal_date": "05/05/2020", "new_build_handover_date": "01/01/2019" } From aa6fb327101c82d55562d15ff0c3ec09ffc8e49b Mon Sep 17 00:00:00 2001 From: Daniel Baark <5101747+baarkerlounger@users.noreply.github.com> Date: Mon, 22 Nov 2021 09:31:29 +0000 Subject: [PATCH 12/29] CLDC-724: Admin Panel requires authentication (#104) * Admi Users must sign in to access panel * Add ADR doc for design decision * Add AdminUsers dashboard to ActiveAdmin --- app/admin/admin_users.rb | 27 ++++++++++++++++++++ app/models/admin_user.rb | 5 ++++ config/initializers/active_admin.rb | 4 +-- config/routes.rb | 1 + db/migrate/20211119120910_add_admin_users.rb | 18 +++++++++++++ db/schema.rb | 14 ++++++++-- db/seeds.rb | 1 + docs/adr/adr-010-admin-users-vs-users.md | 9 +++++++ 8 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 app/admin/admin_users.rb create mode 100644 app/models/admin_user.rb create mode 100644 db/migrate/20211119120910_add_admin_users.rb create mode 100644 docs/adr/adr-010-admin-users-vs-users.md diff --git a/app/admin/admin_users.rb b/app/admin/admin_users.rb new file mode 100644 index 000000000..fed0ec1a8 --- /dev/null +++ b/app/admin/admin_users.rb @@ -0,0 +1,27 @@ +ActiveAdmin.register AdminUser do + permit_params :email, :password, :password_confirmation + + index do + selectable_column + id_column + column :email + column :current_sign_in_at + column :sign_in_count + column :created_at + actions + end + + filter :email + filter :current_sign_in_at + filter :sign_in_count + filter :created_at + + form do |f| + f.inputs do + f.input :email + f.input :password + f.input :password_confirmation + end + f.actions + end +end diff --git a/app/models/admin_user.rb b/app/models/admin_user.rb new file mode 100644 index 000000000..14ea71789 --- /dev/null +++ b/app/models/admin_user.rb @@ -0,0 +1,5 @@ +class AdminUser < ApplicationRecord + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable + devise :database_authenticatable, :recoverable, :rememberable, :validatable +end diff --git a/config/initializers/active_admin.rb b/config/initializers/active_admin.rb index 5afdcdc12..6e78e39d6 100644 --- a/config/initializers/active_admin.rb +++ b/config/initializers/active_admin.rb @@ -54,7 +54,7 @@ ActiveAdmin.setup do |config| # # This setting changes the method which Active Admin calls # within the application controller. - # config.authentication_method = :authenticate_admin_user! + config.authentication_method = :authenticate_admin_user! # == User Authorization # @@ -91,7 +91,7 @@ ActiveAdmin.setup do |config| # # This setting changes the method which Active Admin calls # (within the application controller) to return the currently logged in user. - # config.current_user_method = :current_admin_user + config.current_user_method = :current_admin_user # == Logging Out # diff --git a/config/routes.rb b/config/routes.rb index 2c0b14673..10cc71b6c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + devise_for :admin_users, ActiveAdmin::Devise.config devise_for :users, controllers: { passwords: "users/passwords" } devise_scope :user do get "confirmations/reset", to: "users/passwords#reset_confirmation" diff --git a/db/migrate/20211119120910_add_admin_users.rb b/db/migrate/20211119120910_add_admin_users.rb new file mode 100644 index 000000000..05bce9747 --- /dev/null +++ b/db/migrate/20211119120910_add_admin_users.rb @@ -0,0 +1,18 @@ +class AddAdminUsers < ActiveRecord::Migration[6.1] + def change + create_table :admin_users do |t| + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 18a7999e6..408d07b07 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,21 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_11_18_090831) do +ActiveRecord::Schema.define(version: 2021_11_19_120910) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "admin_users", force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + create_table "case_logs", force: :cascade do |t| t.integer "status", default: 0 t.datetime "created_at", precision: 6, null: false @@ -88,7 +98,6 @@ ActiveRecord::Schema.define(version: 2021_11_18_090831) do t.integer "tcharge" t.integer "layear" t.integer "lawaitlist" - t.string "property_postcode" t.integer "reasonpref" t.string "reasonable_preference_reason" t.integer "cbl" @@ -153,6 +162,7 @@ ActiveRecord::Schema.define(version: 2021_11_18_090831) do t.datetime "sale_completion_date" t.datetime "startdate" t.integer "armedforces" + t.string "property_postcode" t.index ["discarded_at"], name: "index_case_logs_on_discarded_at" end diff --git a/db/seeds.rb b/db/seeds.rb index cb36e5af4..6156dff01 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -7,3 +7,4 @@ # Character.create(name: 'Luke', movie: movies.first) User.create!(email: "test@example.com", password: "password") +AdminUser.create!(email: "admin@example.com", password: "password") diff --git a/docs/adr/adr-010-admin-users-vs-users.md b/docs/adr/adr-010-admin-users-vs-users.md new file mode 100644 index 000000000..e293eea9f --- /dev/null +++ b/docs/adr/adr-010-admin-users-vs-users.md @@ -0,0 +1,9 @@ +### ADR - 010: Admin Users vs Users + +#### Why do we have 2 User classes, AdminUser and User? + +This is modelling a real life split. `AdminUsers` are internal DLUHC users or helpdesk employees. While `Users` are external users working at data providing organisations. So local authority/housing association's "admin" users, i.e. Data Co-ordinators are a type of the User class. They have the ability to add or remove other users to or from their organisation, and to update their organisation details etc, but only through the designed UI. They do not get direct access to ActiveAdmin. + +AdminUsers on the other hand get direct access to ActiveAdmin. From there they can download entire datasets (via CSV, XML, JSON), view any log from any organisation, and add or remove users of any type including other Admin users. This means TDA will likely also require more stringent authentication for them using MFA (which users will likely not require). So the class split also helps there. + +A potential downside to this approach is that it does not currently allow for `AdminUsers` to sign into the application UI itself with their Admin credentials. However, we need to see if there's an actual use case for this and what it would be (since they aren't part of an organisation to be uploading data for, but could add or amend data or user or org details through ActiveAdmin anyway). If there is a strong use case for it this could be work around by either: providing them with two sets of credentials, or modifying the `authenticate_user` method to also check `AdminUser` credentials. From 7babd2cb71ec28e2fdfdc70f5f0940addf36bfb7 Mon Sep 17 00:00:00 2001 From: Daniel Baark <5101747+baarkerlounger@users.noreply.github.com> Date: Mon, 22 Nov 2021 09:32:25 +0000 Subject: [PATCH 13/29] Fix reasonable preference validation (#105) --- app/validations/household_validations.rb | 8 ++++---- spec/models/case_log_spec.rb | 19 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/validations/household_validations.rb b/app/validations/household_validations.rb index 5671b1b75..d7724e85a 100644 --- a/app/validations/household_validations.rb +++ b/app/validations/household_validations.rb @@ -5,12 +5,12 @@ module HouseholdValidations if record.homeless == "No" && record.reasonpref == "Yes" record.errors.add :reasonpref, "Can not be Yes if Not Homeless immediately prior to this letting has been selected" elsif record.reasonpref == "Yes" - if !record.rp_homeless && !record.rp_insan_unsat && !record.rp_medwel && !record.rp_hardship && !record.rp_dontknow - record.errors.add :reasonable_preference_reason, "If reasonable preference is Yes, a reason must be given" + if [record.rp_homeless, record.rp_insan_unsat, record.rp_medwel, record.rp_hardship, record.rp_dontknow].none? { |a| a == "Yes" } + record.errors.add :reasonable_preference_reason, 'If reasonable preference is "Yes", a reason must be given' end elsif record.reasonpref == "No" - if record.rp_homeless || record.rp_insan_unsat || record.rp_medwel || record.rp_hardship || record.rp_dontknow - record.errors.add :reasonable_preference_reason, "If reasonable preference is No, no reasons should be given" + if [record.rp_homeless, record.rp_insan_unsat, record.rp_medwel, record.rp_hardship, record.rp_dontknow].any? { |a| a == "Yes" } + record.errors.add :reasonable_preference_reason, 'If reasonable preference is "No", no reasons should be given' end end end diff --git a/spec/models/case_log_spec.rb b/spec/models/case_log_spec.rb index 19909ccc3..7002999b3 100644 --- a/spec/models/case_log_spec.rb +++ b/spec/models/case_log_spec.rb @@ -26,8 +26,8 @@ RSpec.describe Form, type: :model do expect { CaseLog.create!(offered: 0) }.to raise_error(ActiveRecord::RecordInvalid) end - context "reasonable preference validation" do - it "if given reasonable preference is yes a reason must be selected" do + context "reasonable preference is yes" do + it "validates a reason must be selected" do expect { CaseLog.create!(reasonpref: "Yes", rp_homeless: nil, @@ -38,7 +38,7 @@ RSpec.describe Form, type: :model do }.to raise_error(ActiveRecord::RecordInvalid) end - it "if not previously homeless reasonable preference should not be selected" do + it "validates that previously homeless should be selected" do expect { CaseLog.create!( homeless: "No", @@ -46,17 +46,16 @@ RSpec.describe Form, type: :model do ) }.to raise_error(ActiveRecord::RecordInvalid) end + end - it "if not given reasonable preference a reason should not be selected" do + context "reasonable preference is no" do + it "validates no reason is needed" do expect { - CaseLog.create!( - homeless: "Yes - other homelessness", - reasonpref: "No", - rp_homeless: "Yes", - ) - }.to raise_error(ActiveRecord::RecordInvalid) + CaseLog.create!(reasonpref: "No", rp_homeless: "No") + }.not_to raise_error end end + context "reason for leaving last settled home validation" do it "Reason for leaving must be don't know if reason for leaving settled home (Q9a) is don't know." do expect { From ba51e66a560d7b21390df8ced7acfffcc09263a2 Mon Sep 17 00:00:00 2001 From: Daniel Baark <5101747+baarkerlounger@users.noreply.github.com> Date: Tue, 23 Nov 2021 14:55:08 +0000 Subject: [PATCH 14/29] Refactor the form parsing and navigation logic into OOP domain objects (#106) * OOP form * Add ADR * PR commits --- app/controllers/case_logs_controller.rb | 47 +++-- .../soft_validations_controller.rb | 6 +- app/helpers/check_answers_helper.rb | 61 ++---- app/helpers/conditional_questions_helper.rb | 8 +- app/helpers/question_attribute_helper.rb | 10 +- app/helpers/tasklist_helper.rb | 38 ++-- app/models/case_log.rb | 4 +- app/models/form.rb | 160 ++------------- app/models/form/page.rb | 30 +++ app/models/form/question.rb | 80 ++++++++ app/models/form/section.rb | 10 + app/models/form/subsection.rb | 65 ++++++ app/validations/household_validations.rb | 2 +- app/views/case_logs/_tasklist.html.erb | 13 +- app/views/case_logs/edit.html.erb | 2 +- app/views/form/_check_answers_table.html.erb | 6 +- app/views/form/_checkbox_question.html.erb | 10 +- app/views/form/_date_question.html.erb | 6 +- app/views/form/_numeric_question.html.erb | 10 +- app/views/form/_radio_question.html.erb | 12 +- app/views/form/_select_question.html.erb | 11 +- app/views/form/_text_question.html.erb | 6 +- .../_validation_override_question.html.erb | 6 +- app/views/form/check_answers.html.erb | 8 +- app/views/form/page.html.erb | 16 +- config/routes.rb | 10 +- docs/adr/adr-011-form-oop-refactor.md | 10 + spec/controllers/case_logs_controller_spec.rb | 39 ++-- spec/features/case_log_spec.rb | 2 +- spec/fixtures/forms/test_form.json | 30 ++- spec/helpers/check_answers_helper_spec.rb | 185 ++---------------- .../conditional_questions_helper_spec.rb | 12 +- .../helpers/question_attribute_helper_spec.rb | 22 ++- spec/helpers/tasklist_helper_spec.rb | 30 ++- spec/models/form/page_spec.rb | 66 +++++++ spec/models/form/question_spec.rb | 140 +++++++++++++ spec/models/form/section_spec.rb | 21 ++ spec/models/form/subsection_spec.rb | 72 +++++++ spec/models/form_handler_spec.rb | 2 +- spec/models/form_spec.rb | 64 +----- 40 files changed, 752 insertions(+), 580 deletions(-) create mode 100644 app/models/form/page.rb create mode 100644 app/models/form/question.rb create mode 100644 app/models/form/section.rb create mode 100644 app/models/form/subsection.rb create mode 100644 docs/adr/adr-011-form-oop-refactor.md create mode 100644 spec/models/form/page_spec.rb create mode 100644 spec/models/form/question_spec.rb create mode 100644 spec/models/form/section_spec.rb create mode 100644 spec/models/form/subsection_spec.rb diff --git a/app/controllers/case_logs_controller.rb b/app/controllers/case_logs_controller.rb index c2b4edc28..0b55c7c1f 100644 --- a/app/controllers/case_logs_controller.rb +++ b/app/controllers/case_logs_controller.rb @@ -57,14 +57,14 @@ class CaseLogsController < ApplicationController def submit_form form = FormHandler.instance.get_form("2021_2022") @case_log = CaseLog.find(params[:id]) - @case_log.page = params[:case_log][:page] - responses_for_page = responses_for_page(@case_log.page) + @case_log.page_id = params[:case_log][:page] + page = form.get_page(@case_log.page_id) + responses_for_page = responses_for_page(page) if @case_log.update(responses_for_page) && @case_log.has_no_unresolved_soft_errors? - redirect_path = form.next_page_redirect_path(@case_log.page, @case_log) + redirect_path = form.next_page_redirect_path(page, @case_log) redirect_to(send(redirect_path, @case_log)) else - page_info = form.all_pages[@case_log.page] - render "form/page", locals: { form: form, page_key: @case_log.page, page_info: page_info }, status: :unprocessable_entity + render "form/page", locals: { form: form, page: page }, status: :unprocessable_entity end end @@ -84,15 +84,15 @@ class CaseLogsController < ApplicationController form = FormHandler.instance.get_form("2021_2022") @case_log = CaseLog.find(params[:case_log_id]) current_url = request.env["PATH_INFO"] - subsection = current_url.split("/")[-2] + subsection = form.get_subsection(current_url.split("/")[-2]) render "form/check_answers", locals: { subsection: subsection, form: form } end form = FormHandler.instance.get_form("2021_2022") - form.all_pages.map do |page_key, page_info| - define_method(page_key) do |_errors = {}| + form.pages.map do |page| + define_method(page.id) do |_errors = {}| @case_log = CaseLog.find(params[:case_log_id]) - render "form/page", locals: { form: form, page_key: page_key, page_info: page_info } + render "form/page", locals: { form: form, page: page } end end @@ -101,29 +101,28 @@ private API_ACTIONS = %w[create show update destroy].freeze def responses_for_page(page) - form = FormHandler.instance.get_form("2021_2022") - form.expected_responses_for_page(page).each_with_object({}) do |(question_key, question_info), result| - question_params = params["case_log"][question_key] - if question_info["type"] == "date" - day = params["case_log"]["#{question_key}(3i)"] - month = params["case_log"]["#{question_key}(2i)"] - year = params["case_log"]["#{question_key}(1i)"] + page.expected_responses.each_with_object({}) do |question, result| + question_params = params["case_log"][question.id] + if question.type == "date" + day = params["case_log"]["#{question.id}(3i)"] + month = params["case_log"]["#{question.id}(2i)"] + year = params["case_log"]["#{question.id}(1i)"] next unless [day, month, year].any?(&:present?) - result[question_key] = if day.to_i.between?(1, 31) && month.to_i.between?(1, 12) && year.to_i.between?(2000, 2200) - Date.new(year.to_i, month.to_i, day.to_i) - else - Date.new(0, 1, 1) - end + result[question.id] = if day.to_i.between?(1, 31) && month.to_i.between?(1, 12) && year.to_i.between?(2000, 2200) + Date.new(year.to_i, month.to_i, day.to_i) + else + Date.new(0, 1, 1) + end end next unless question_params - if %w[checkbox validation_override].include?(question_info["type"]) - question_info["answer_options"].keys.reject { |x| x.match(/divider/) }.each do |option| + if %w[checkbox validation_override].include?(question.type) + question.answer_options.keys.reject { |x| x.match(/divider/) }.each do |option| result[option] = question_params.include?(option) ? 1 : 0 end else - result[question_key] = question_params + result[question.id] = question_params end result end diff --git a/app/controllers/soft_validations_controller.rb b/app/controllers/soft_validations_controller.rb index ad28ea25e..4f9881de6 100644 --- a/app/controllers/soft_validations_controller.rb +++ b/app/controllers/soft_validations_controller.rb @@ -1,9 +1,9 @@ class SoftValidationsController < ApplicationController def show @case_log = CaseLog.find(params[:case_log_id]) - page_key = request.env["PATH_INFO"].split("/")[-2] + page_id = request.env["PATH_INFO"].split("/")[-2] form = FormHandler.instance.get_form("2021_2022") - page = form.all_pages[page_key] + page = form.get_page(page_id) if page_requires_soft_validation_override?(page) errors = @case_log.soft_errors.values.first render json: { show: true, label: errors.message, hint: errors.hint_text } @@ -15,6 +15,6 @@ class SoftValidationsController < ApplicationController private def page_requires_soft_validation_override?(page) - @case_log.soft_errors.present? && @case_log.soft_errors.keys.first == page["soft_validations"]&.keys&.first + @case_log.soft_errors.present? && @case_log.soft_errors.keys.first == page.soft_validations&.first&.id end end diff --git a/app/helpers/check_answers_helper.rb b/app/helpers/check_answers_helper.rb index fed58b71e..ba0237f58 100644 --- a/app/helpers/check_answers_helper.rb +++ b/app/helpers/check_answers_helper.rb @@ -1,57 +1,20 @@ module CheckAnswersHelper - def total_answered_questions(subsection, case_log, form) - total_questions(subsection, case_log, form).keys.count do |question_key| - case_log[question_key].present? - end - end - - def total_number_of_questions(subsection, case_log, form) - total_questions(subsection, case_log, form).count - end - - def total_questions(subsection, case_log, form) - questions = form.questions_for_subsection(subsection) - form.filter_conditional_questions(questions, case_log) - end - - def get_next_page_name(form, page_name, case_log) - page = form.all_pages[page_name] - if page.key?("conditional_route_to") - page["conditional_route_to"].each do |conditional_page_name, condition| - unless condition.any? { |field, value| case_log[field].blank? || !value.include?(case_log[field]) } - return conditional_page_name - end - end + def display_answered_questions_summary(subsection, case_log) + total = subsection.applicable_questions_count(case_log) + answered = subsection.answered_questions_count(case_log) + if total == answered + '

You answered all the questions

'.html_safe + else + "

You answered #{answered} of #{total} questions

+ #{create_next_missing_question_link(subsection, case_log)}".html_safe end - form.next_page(page_name) end - def create_update_answer_link(question_title, question_info, case_log, form) - page = form.page_for_question(question_title) - link_name = if question_info["type"] == "checkbox" - question_info["answer_options"].keys.any? { |key| case_log[key] == "Yes" } ? "Change" : "Answer" - else - case_log[question_title].blank? ? "Answer" : "Change" - end - link_to(link_name, "/case_logs/#{case_log.id}/#{page}", class: "govuk-link").html_safe - end +private - def create_next_missing_question_link(case_log_id, subsection, case_log, form) - pages_to_fill_in = [] - form.pages_for_subsection(subsection).each do |page_title, page_info| - page_info["questions"].any? { |question| case_log[question].blank? } - pages_to_fill_in << page_title - end - url = "/case_logs/#{case_log_id}/#{pages_to_fill_in.first}" + def create_next_missing_question_link(subsection, case_log) + pages_to_fill_in = subsection.unanswered_questions(case_log).map(&:page) + url = "/case_logs/#{case_log.id}/#{pages_to_fill_in.first.id}" link_to("Answer the missing questions", url, class: "govuk-link").html_safe end - - def display_answered_questions_summary(subsection, case_log, form) - if total_answered_questions(subsection, case_log, form) == total_number_of_questions(subsection, case_log, form) - '

You answered all the questions

'.html_safe - else - "

You answered #{total_answered_questions(subsection, case_log, form)} of #{total_number_of_questions(subsection, case_log, form)} questions

- #{create_next_missing_question_link(case_log['id'], subsection, case_log, form)}".html_safe - end - end end diff --git a/app/helpers/conditional_questions_helper.rb b/app/helpers/conditional_questions_helper.rb index c77119243..ec9ccae60 100644 --- a/app/helpers/conditional_questions_helper.rb +++ b/app/helpers/conditional_questions_helper.rb @@ -1,11 +1,9 @@ module ConditionalQuestionsHelper def conditional_questions_for_page(page) - page["questions"].values.map { |question| - question["conditional_for"] - }.compact.map(&:keys).flatten + page.questions.map(&:conditional_for).compact.map(&:keys).flatten end - def display_question_key_div(page_info, question_key) - "style='display:none;'".html_safe if conditional_questions_for_page(page_info).include?(question_key) + def display_question_key_div(page, question) + "style='display:none;'".html_safe if conditional_questions_for_page(page).include?(question.id) end end diff --git a/app/helpers/question_attribute_helper.rb b/app/helpers/question_attribute_helper.rb index 1010591f0..d292239d8 100644 --- a/app/helpers/question_attribute_helper.rb +++ b/app/helpers/question_attribute_helper.rb @@ -10,23 +10,23 @@ module QuestionAttributeHelper private def numeric_question_html_attributes(question) - return {} if question["fields-to-add"].blank? || question["result-field"].blank? + return {} if question.fields_to_add.blank? || question.result_field.blank? { "data-controller": "numeric-question", "data-action": "numeric-question#calculateFields", - "data-target": "case-log-#{question['result-field'].to_s.dasherize}-field", - "data-calculated": question["fields-to-add"].to_json, + "data-target": "case-log-#{question.result_field.to_s.dasherize}-field", + "data-calculated": question.fields_to_add.to_json, } end def conditional_html_attributes(question) - return {} if question["conditional_for"].blank? + return {} if question.conditional_for.blank? { "data-controller": "conditional-question", "data-action": "conditional-question#displayConditional", - "data-info": question["conditional_for"].to_json, + "data-info": question.conditional_for.to_json, } end end diff --git a/app/helpers/tasklist_helper.rb b/app/helpers/tasklist_helper.rb index 777cc598a..d31257f25 100644 --- a/app/helpers/tasklist_helper.rb +++ b/app/helpers/tasklist_helper.rb @@ -14,40 +14,30 @@ module TasklistHelper }.freeze def get_next_incomplete_section(form, case_log) - subsections = form.all_subsections.keys - subsections.find { |subsection| is_incomplete?(subsection, case_log, form) } + form.subsections.find { |subsection| subsection.is_incomplete?(case_log) } end def get_subsections_count(form, case_log, status = :all) - subsections = form.all_subsections.keys - return subsections.count if status == :all + return form.subsections.count if status == :all - subsections.count { |subsection| form.subsection_status(subsection, case_log) == status } + form.subsections.count { |subsection| subsection.status(case_log) == status } end - def get_first_page_or_check_answers(subsection, case_log, form) - path = if is_started?(subsection, case_log, form) - "case_log_#{subsection}_check_answers_path" + def first_page_or_check_answers(subsection, case_log) + path = if subsection.is_started?(case_log) + "case_log_#{subsection.id}_check_answers_path" else - "case_log_#{form.first_page_for_subsection(subsection)}_path" + "case_log_#{subsection.pages.first.id}_path" end send(path, case_log) end - def subsection_link(subsection_key, subsection_value, status, form, case_log) - next_page_path = status != :cannot_start_yet ? get_first_page_or_check_answers(subsection_key, case_log, form) : "#" - link_to(subsection_value["label"], next_page_path, class: "task-name govuk-link") - end - -private - - def is_incomplete?(subsection, case_log, form) - status = form.subsection_status(subsection, case_log) - %i[not_started in_progress].include?(status) - end - - def is_started?(subsection, case_log, form) - status = form.subsection_status(subsection, case_log) - %i[in_progress completed].include?(status) + def subsection_link(subsection, case_log) + next_page_path = if subsection.status(case_log) != :cannot_start_yet + first_page_or_check_answers(subsection, case_log) + else + "#" + end + link_to(subsection.label, next_page_path, class: "task-name govuk-link") end end diff --git a/app/models/case_log.rb b/app/models/case_log.rb index 32bc40739..542a6997a 100644 --- a/app/models/case_log.rb +++ b/app/models/case_log.rb @@ -11,7 +11,7 @@ class CaseLogValidator < ActiveModel::Validator # If we've come from the form UI we only want to validate the specific fields # that have just been submitted. If we're submitting a log via API or Bulk Upload # we want to validate all data fields. - page_to_validate = record.page + page_to_validate = record.page_id if page_to_validate public_send("validate_#{page_to_validate}", record) if respond_to?("validate_#{page_to_validate}") else @@ -44,7 +44,7 @@ class CaseLog < ApplicationRecord validates_with CaseLogValidator before_save :update_status! - attr_accessor :page + attr_accessor :page_id enum status: { "not_started" => 0, "in_progress" => 1, "completed" => 2 } diff --git a/app/models/form.rb b/app/models/form.rb index 6c3f5672f..5dc85384c 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -1,75 +1,34 @@ class Form - attr_reader :form_definition + attr_reader :form_definition, :sections, :subsections, :pages, :questions def initialize(form_path) raise "No form definition file exists for given year".freeze unless File.exist?(form_path) @form_definition = JSON.parse(File.open(form_path).read) + @sections = form_definition["sections"].map { |id, s| Form::Section.new(id, s, self) } + @subsections = sections.flat_map(&:subsections) + @pages = subsections.flat_map(&:pages) + @questions = pages.flat_map(&:questions) end - # Returns a hash with sections as keys - def all_sections - @all_sections ||= @form_definition["sections"] + def get_subsection(id) + subsections.find { |s| s.id == id } end - # Returns a hash with subsections as keys - def all_subsections - @all_subsections ||= all_sections.map { |_section_key, section_value| - section_value["subsections"] - }.reduce(:merge) - end - - # Returns a hash with pages as keys - def all_pages - @all_pages ||= all_subsections.map { |_subsection_key, subsection_value| - subsection_value["pages"] - }.reduce(:merge) - end - - # Returns a hash with the pages of a subsection as keys - def pages_for_subsection(subsection) - all_subsections[subsection]["pages"] - end - - # Returns a hash with the questions as keys - def questions_for_page(page) - all_pages[page]["questions"] - end - - # Returns a hash with the questions as keys - def questions_for_subsection(subsection) - pages_for_subsection(subsection).map { |title, _value| questions_for_page(title) }.reduce(:merge) - end - - # Returns a hash with soft validation questions as keys - def soft_validations_for_page(page) - all_pages[page]["soft_validations"] - end - - def expected_responses_for_page(page) - questions_for_page(page).merge(soft_validations_for_page(page) || {}) - end - - def first_page_for_subsection(subsection) - pages_for_subsection(subsection).keys.first + def get_page(id) + pages.find { |p| p.id == id } end def subsection_for_page(page) - all_subsections.find { |_subsection_key, subsection_value| - subsection_value["pages"].key?(page) - }.first - end - - def page_for_question(question) - all_pages.find { |_page_key, page_value| page_value["questions"].key?(question) }.first + subsections.find { |s| s.pages.find { |p| p.id == page.id } } end def next_page(page, case_log) - subsection = subsection_for_page(page) - page_idx = pages_for_subsection(subsection).keys.index(page) - nxt_page = pages_for_subsection(subsection).keys[page_idx + 1] + page_ids = subsection_for_page(page).pages.map(&:id) + page_index = page_ids.index(page.id) + nxt_page = get_page(page_ids[page_index + 1]) return :check_answers if nxt_page.nil? - return nxt_page if page_routed_to?(nxt_page, case_log) + return nxt_page.id if nxt_page.routed_to?(case_log) next_page(nxt_page, case_log) end @@ -77,95 +36,16 @@ class Form def next_page_redirect_path(page, case_log) nxt_page = next_page(page, case_log) if nxt_page == :check_answers - subsection = subsection_for_page(page) - "case_log_#{subsection}_check_answers_path" + "case_log_#{subsection_for_page(page).id}_check_answers_path" else "case_log_#{nxt_page}_path" end end - def all_questions - @all_questions ||= all_pages.map { |_page_key, page_value| - page_value["questions"] - }.reduce(:merge) - end - - def filter_conditional_questions(questions, case_log) - applicable_questions = questions - - questions.each do |k, question| - unless page_routed_to?(page_for_question(k), case_log) - applicable_questions = applicable_questions.reject { |z| z == k } - end - - question.fetch("conditional_for", []).each do |conditional_question_key, condition| - if condition_not_met(case_log, k, question, condition) - applicable_questions = applicable_questions.reject { |z| z == conditional_question_key } - end - end - end - applicable_questions - end - - def page_routed_to?(page, case_log) - return true unless (conditions = page_dependencies(page)) - - conditions.all? do |question, value| - case_log[question].present? && case_log[question] == value - end - end - - def page_dependencies(page) - all_pages[page]["depends_on"] - end - - def subsection_dependencies_met?(subsection_name, case_log) - conditions = all_subsections[subsection_name]["depends_on"] - return true unless conditions - - conditions.all? do |subsection, status| - subsection_status(subsection, case_log) == status.to_sym - end - end - - def subsection_status(subsection_name, case_log) - unless subsection_dependencies_met?(subsection_name, case_log) - return :cannot_start_yet - end - - questions = questions_for_subsection(subsection_name) - applicable_questions = filter_conditional_questions(questions, case_log).keys - return :not_started if applicable_questions.all? { |question| case_log[question].blank? } - return :completed if applicable_questions.all? { |question| case_log[question].present? } - - :in_progress - end - - def condition_not_met(case_log, question_key, question, condition) - case question["type"] - when "numeric" - operator = condition[/[<>=]+/].to_sym - operand = condition[/\d+/].to_i - case_log[question_key].blank? || !case_log[question_key].send(operator, operand) - when "text" - case_log[question_key].blank? || !condition.include?(case_log[question_key]) - when "radio" - case_log[question_key].blank? || !condition.include?(case_log[question_key]) - when "select" - case_log[question_key].blank? || !condition.include?(case_log[question_key]) - else - raise "Not implemented yet" - end - end - - def get_answer_label(case_log, question_title) - question = all_questions[question_title] - if question["type"] == "checkbox" - answer = [] - question["answer_options"].each { |key, value| case_log[key] == "Yes" ? answer << value : nil } - return answer.join(", ") - end - - case_log[question_title] + def conditional_question_conditions + conditions = questions.map { |q| Hash(q.id => q.conditional_for) if q.conditional_for.present? }.compact + conditions.map { |c| + c.map { |k, v| v.keys.map { |key| Hash(from: k, to: key, cond: v[key]) } } + }.flatten end end diff --git a/app/models/form/page.rb b/app/models/form/page.rb new file mode 100644 index 000000000..73a1ee143 --- /dev/null +++ b/app/models/form/page.rb @@ -0,0 +1,30 @@ +class Form::Page + attr_accessor :id, :header, :description, :questions, :soft_validations, + :depends_on, :subsection + + def initialize(id, hsh, subsection) + @id = id + @header = hsh["header"] + @description = hsh["description"] + @questions = hsh["questions"].map { |q_id, q| Form::Question.new(q_id, q, self) } + @depends_on = hsh["depends_on"] + @soft_validations = hsh["soft_validations"]&.map { |v_id, s| Form::Question.new(v_id, s, self) } + @subsection = subsection + end + + def expected_responses + questions + (soft_validations || []) + end + + def has_soft_validations? + soft_validations.present? + end + + def routed_to?(case_log) + return true unless depends_on + + depends_on.all? do |question, value| + case_log[question].present? && case_log[question] == value + end + end +end diff --git a/app/models/form/question.rb b/app/models/form/question.rb new file mode 100644 index 000000000..2362441c8 --- /dev/null +++ b/app/models/form/question.rb @@ -0,0 +1,80 @@ +class Form::Question + attr_accessor :id, :header, :hint_text, :description, :questions, + :type, :min, :max, :step, :fields_to_add, :result_field, + :conditional_for, :readonly, :answer_options, :page, :check_answer_label + + def initialize(id, hsh, page) + @id = id + @check_answer_label = hsh["check_answer_label"] + @header = hsh["header"] + @hint_text = hsh["hint_text"] + @type = hsh["type"] + @min = hsh["min"] + @max = hsh["max"] + @step = hsh["step"] + @fields_to_add = hsh["fields-to-add"] + @result_field = hsh["result-field"] + @readonly = hsh["readonly"] + @answer_options = hsh["answer_options"] + @conditional_for = hsh["conditional_for"] + @page = page + end + + delegate :subsection, to: :page + delegate :form, to: :subsection + + def answer_label(case_log) + return checkbox_answer_label(case_log) if type == "checkbox" + + case_log[id].to_s + end + + def read_only? + !!readonly + end + + def conditional_on + @conditional_on ||= form.conditional_question_conditions.select do |condition| + condition[:to] == id + end + end + + def enabled?(case_log) + return true if conditional_on.blank? + + conditional_on.map { |condition| evaluate_condition(condition, case_log) }.all? + end + + def update_answer_link_name(case_log) + if type == "checkbox" + answer_options.keys.any? { |key| case_log[key] == "Yes" } ? "Change" : "Answer" + else + case_log[id].blank? ? "Answer" : "Change" + end + end + +private + + def checkbox_answer_label(case_log) + answer = [] + answer_options.each { |key, value| case_log[key] == "Yes" ? answer << value : nil } + answer.join(", ") + end + + def evaluate_condition(condition, case_log) + case page.questions.find { |q| q.id == condition[:from] }.type + when "numeric" + operator = condition[:cond][/[<>=]+/].to_sym + operand = condition[:cond][/\d+/].to_i + case_log[condition[:from]].present? && case_log[condition[:from]].send(operator, operand) + when "text" + case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]]) + when "radio" + case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]]) + when "select" + case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]]) + else + raise "Not implemented yet" + end + end +end diff --git a/app/models/form/section.rb b/app/models/form/section.rb new file mode 100644 index 000000000..477fc9f18 --- /dev/null +++ b/app/models/form/section.rb @@ -0,0 +1,10 @@ +class Form::Section + attr_accessor :id, :label, :subsections, :form + + def initialize(id, hsh, form) + @id = id + @label = hsh["label"] + @form = form + @subsections = hsh["subsections"].map { |s_id, s| Form::Subsection.new(s_id, s, self) } + end +end diff --git a/app/models/form/subsection.rb b/app/models/form/subsection.rb new file mode 100644 index 000000000..19700d935 --- /dev/null +++ b/app/models/form/subsection.rb @@ -0,0 +1,65 @@ +class Form::Subsection + attr_accessor :id, :label, :section, :pages, :depends_on, :form + + def initialize(id, hsh, section) + @id = id + @label = hsh["label"] + @depends_on = hsh["depends_on"] + @pages = hsh["pages"].map { |s_id, p| Form::Page.new(s_id, p, self) } + @section = section + end + + delegate :form, to: :section + + def questions + @questions ||= pages.flat_map(&:questions) + end + + def enabled?(case_log) + return true unless depends_on + + depends_on.all? do |subsection_id, dependent_status| + form.get_subsection(subsection_id).status(case_log) == dependent_status.to_sym + end + end + + def status(case_log) + unless enabled?(case_log) + return :cannot_start_yet + end + + qs = applicable_questions(case_log) + return :not_started if qs.all? { |question| case_log[question.id].blank? } + return :completed if qs.all? { |question| case_log[question.id].present? } + + :in_progress + end + + def is_incomplete?(case_log) + %i[not_started in_progress].include?(status(case_log)) + end + + def is_started?(case_log) + %i[in_progress completed].include?(status(case_log)) + end + + def applicable_questions_count(case_log) + applicable_questions(case_log).count + end + + def answered_questions_count(case_log) + answered_questions(case_log).count + end + + def applicable_questions(case_log) + questions.select { |q| q.page.routed_to?(case_log) && q.enabled?(case_log) } + end + + def answered_questions(case_log) + applicable_questions(case_log).select { |question| case_log[question.id].present? } + end + + def unanswered_questions(case_log) + applicable_questions(case_log) - answered_questions(case_log) + end +end diff --git a/app/validations/household_validations.rb b/app/validations/household_validations.rb index d7724e85a..6286f2884 100644 --- a/app/validations/household_validations.rb +++ b/app/validations/household_validations.rb @@ -66,7 +66,7 @@ module HouseholdValidations return unless record.age1 if !record.age1.is_a?(Integer) || record.age1 < 16 || record.age1 > 120 - record.errors.add "age1", "Tenant age must be an integer between 16 and 120" + record.errors.add :age1, "Tenant age must be an integer between 16 and 120" end end diff --git a/app/views/case_logs/_tasklist.html.erb b/app/views/case_logs/_tasklist.html.erb index c46f5d364..87eb00a7b 100644 --- a/app/views/case_logs/_tasklist.html.erb +++ b/app/views/case_logs/_tasklist.html.erb @@ -1,17 +1,16 @@
    - <% @form.all_sections.map do |section_key, section_value| %> + <% @form.sections.map do |section| %>
  1. - <%= section_value["label"] %> + <%= section.label %>