diff --git a/Gemfile.lock b/Gemfile.lock index b21879881..328d2a9ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,7 +26,7 @@ GIT GIT remote: https://github.com/rspec/rspec-rails.git - revision: fdcd1df0b13f9b6547336b4d37dffb66f70f7228 + revision: 3f0e35085f5765decf96fee179ec9a2e132a67c1 branch: main specs: rspec-rails (5.1.0.pre) @@ -294,7 +294,7 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - stimulus-rails (0.6.1) + stimulus-rails (0.7.0) rails (>= 6.0.0) thor (1.1.0) turbo-rails (0.8.1) diff --git a/README.md b/README.md index 5028acab2..7a7a2efc5 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,10 @@ The JSON should follow the structure: "answer_options": { // checkbox and radio only "0": String, "1": String + }, + "conditional_for": { + "[snake_case_question_to_enable_1_name_string]": ["condition-that-enables"], + "[snake_case_question_to_enable_2_name_string]": ["condition-that-enables"] } } } @@ -131,6 +135,9 @@ Assumptions made by the format: - All pages have at least 1 question - The ActiveRecord case log model has a field for each question name (must match) - Text not required by a page/question such as a header or hint text should be passed as an empty string +- For conditionally shown questions conditions that have been implemented and can be used are: + - Radio question answer option selected matches one of conditional e.g. ["answer-options-1-string", "answer-option-3-string"] + - Numeric question value matches condition e.g. [">2"], ["<7"] or ["== 6"] ## Useful documentation (external dependencies) diff --git a/app/controllers/case_logs_controller.rb b/app/controllers/case_logs_controller.rb index 45bc05ee4..9dffdc533 100644 --- a/app/controllers/case_logs_controller.rb +++ b/app/controllers/case_logs_controller.rb @@ -37,11 +37,9 @@ class CaseLogsController < ApplicationController def check_answers @case_log = CaseLog.find(params[:case_log_id]) - form = Form.new(2021, 2022) current_url = request.env["PATH_INFO"] subsection = current_url.split("/")[-2] - subsection_pages = form.pages_for_subsection(subsection) - render "form/check_answers", locals: { case_log: @case_log, subsection_pages: subsection_pages, subsection: subsection.humanize(capitalize: false) } + render "form/check_answers", locals: { case_log: @case_log, subsection: subsection } end form = Form.new(2021, 2022) diff --git a/app/helpers/check_answers_helper.rb b/app/helpers/check_answers_helper.rb index 4fd46ce9d..d11706012 100644 --- a/app/helpers/check_answers_helper.rb +++ b/app/helpers/check_answers_helper.rb @@ -1,16 +1,44 @@ module CheckAnswersHelper - def get_answered_questions_total(subsection_pages, case_log) - questions = subsection_pages.values.flat_map do |page| - page["questions"].keys + def total_answered_questions(subsection, case_log) + total_questions(subsection, case_log).keys.count do |question_key| + case_log[question_key].present? end - questions.count { |question| case_log[question].present? } end - def get_total_number_of_questions(subsection_pages) - questions = subsection_pages.values.flat_map do |page| - page["questions"].keys + def total_number_of_questions(subsection, case_log) + total_questions(subsection, case_log).count + end + + def total_questions(subsection, case_log) + form = Form.new(2021, 2022) + questions = form.questions_for_subsection(subsection) + questions_not_applicable = [] + questions.reject do |question_key, question| + question.fetch("conditional_for", []).map do |conditional_question_key, condition| + if condition_not_met(case_log, question_key, question, condition) + questions_not_applicable << conditional_question_key + end + end + questions_not_applicable.include?(question_key) + end + end + + def condition_not_met(case_log, question_key, question, condition) + case question["type"] + when "numeric" + # rubocop:disable Security/Eval + case_log[question_key].blank? || !eval(case_log[question_key].to_s + condition) + # rubocop:enable Security/Eval + when "radio" + case_log[question_key].blank? || !condition.include?(case_log[question_key]) + else + raise "Not implemented yet" end - questions.count + end + + def subsection_pages(subsection) + form = Form.new(2021, 2022) + form.pages_for_subsection(subsection) end def create_update_answer_link(case_log_answer, case_log_id, page) @@ -18,9 +46,9 @@ module CheckAnswersHelper link_to(link_name, "/case_logs/#{case_log_id}/#{page}", class: "govuk-link").html_safe end - def create_next_missing_question_link(case_log_id, subsection_pages, case_log) + def create_next_missing_question_link(case_log_id, subsection, case_log) pages_to_fill_in = [] - subsection_pages.each do |page_title, page_info| + subsection_pages(subsection).each do |page_title, page_info| page_info["questions"].any? { |question| case_log[question].blank? } pages_to_fill_in << page_title end @@ -28,12 +56,12 @@ module CheckAnswersHelper link_to("Answer the missing questions", url, class: "govuk-link").html_safe end - def display_answered_questions_summary(subsection_pages, case_log) - if get_answered_questions_total(subsection_pages, case_log) == get_total_number_of_questions(subsection_pages) + def display_answered_questions_summary(subsection, case_log) + if total_answered_questions(subsection, case_log) == total_number_of_questions(subsection, case_log) '

You answered all the questions

'.html_safe else - "

You answered #{get_answered_questions_total(subsection_pages, case_log)} of #{get_total_number_of_questions(subsection_pages)} questions

- #{create_next_missing_question_link(case_log['id'], subsection_pages, case_log)}".html_safe + "

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

+ #{create_next_missing_question_link(case_log['id'], subsection, case_log)}".html_safe end end end diff --git a/app/helpers/numeric_questions_helper.rb b/app/helpers/numeric_questions_helper.rb deleted file mode 100644 index c0fe05ce3..000000000 --- a/app/helpers/numeric_questions_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -module NumericQuestionsHelper - def numeric_question_html_attributes(question) - 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, - } - end -end diff --git a/app/helpers/question_attribute_helper.rb b/app/helpers/question_attribute_helper.rb new file mode 100644 index 000000000..1010591f0 --- /dev/null +++ b/app/helpers/question_attribute_helper.rb @@ -0,0 +1,39 @@ +module QuestionAttributeHelper + def stimulus_html_attributes(question) + attribs = [ + numeric_question_html_attributes(question), + conditional_html_attributes(question), + ] + merge_controller_attributes(*attribs) + end + +private + + def numeric_question_html_attributes(question) + 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, + } + end + + def conditional_html_attributes(question) + return {} if question["conditional_for"].blank? + + { + "data-controller": "conditional-question", + "data-action": "conditional-question#displayConditional", + "data-info": question["conditional_for"].to_json, + } + end +end + +def merge_controller_attributes(*args) + args.flat_map(&:keys).uniq.each_with_object({}) do |key, hsh| + hsh[key] = args.map { |a| a.fetch(key, "") }.join(" ").strip + hsh + end +end diff --git a/app/javascript/controllers/conditional_question_controller.js b/app/javascript/controllers/conditional_question_controller.js index 0d058da12..c49ba490a 100644 --- a/app/javascript/controllers/conditional_question_controller.js +++ b/app/javascript/controllers/conditional_question_controller.js @@ -6,17 +6,29 @@ export default class extends Controller { } displayConditional() { + switch(this.element.type) { + case "number": + this.displayConditionalNumeric() + case "radio": + this.displayConditionalRadio() + default: + console.log("Not yet implemented for " + this.element.type) + break; + } + } + + displayConditionalRadio() { if(this.element.checked) { - let selected = this.element.value + let selectedValue = this.element.value let conditional_for = JSON.parse(this.element.dataset.info) - Object.entries(conditional_for).forEach(([key, values]) => { - let div = document.getElementById(key + "_div") - if(values.includes(selected)) { + Object.entries(conditional_for).forEach(([targetQuestion, conditions]) => { + let div = document.getElementById(targetQuestion + "_div") + if(conditions.includes(selectedValue)) { div.style.display = "block" } else { div.style.display = "none" - let buttons = document.getElementsByName(`case_log[${key}]`) + let buttons = document.getElementsByName(`case_log[${targetQuestion}]`) Object.entries(buttons).forEach(([idx, button]) => { button.checked = false; }) @@ -24,4 +36,18 @@ export default class extends Controller { }) } } + + displayConditionalNumeric() { + let enteredValue = this.element.value + let conditional_for = JSON.parse(this.element.dataset.info) + + Object.entries(conditional_for).forEach(([targetQuestion, condition]) => { + let div = document.getElementById(targetQuestion + "_div") + if(eval((enteredValue + condition))) { + div.style.display = "block" + } else { + div.style.display = "none" + } + }) + } } diff --git a/app/models/form.rb b/app/models/form.rb index 9812d890f..031c0093c 100644 --- a/app/models/form.rb +++ b/app/models/form.rb @@ -37,6 +37,11 @@ class Form 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 + def first_page_for_subsection(subsection) pages_for_subsection(subsection).keys.first end @@ -70,8 +75,4 @@ class Form pages_for_subsection(subsection).keys[current_page_idx - 1] end - - def questions_for_subsection(subsection) - pages_for_subsection(subsection).map { |title, _value| questions_for_page(title) }.reduce(:merge) - end end diff --git a/app/views/form/_checkbox_question.html.erb b/app/views/form/_checkbox_question.html.erb index 7d8d82b1b..99bd3fd49 100644 --- a/app/views/form/_checkbox_question.html.erb +++ b/app/views/form/_checkbox_question.html.erb @@ -6,7 +6,7 @@ <% if key.starts_with?("divider") %> <%= f.govuk_check_box_divider %> <% else %> - <%= f.govuk_check_box question_key, val, label: { text: val } %> + <%= f.govuk_check_box question_key, val, label: { text: val }, **stimulus_html_attributes(question) %> <% end %> <% end %> <% end %> diff --git a/app/views/form/_date_question.html.erb b/app/views/form/_date_question.html.erb index f4a4ddf93..417c7e163 100644 --- a/app/views/form/_date_question.html.erb +++ b/app/views/form/_date_question.html.erb @@ -1,5 +1,6 @@ <%= f.govuk_date_field question_key, hint: { text: question["hint_text"] }, legend: { text: question["header"].html_safe, size: "l"}, - width: 20 + width: 20, + **stimulus_html_attributes(question) %> diff --git a/app/views/form/_numeric_question.html.erb b/app/views/form/_numeric_question.html.erb index 6de29a111..47909c0ba 100644 --- a/app/views/form/_numeric_question.html.erb +++ b/app/views/form/_numeric_question.html.erb @@ -3,5 +3,5 @@ label: { text: question["header"].html_safe, size: "l"}, min: question["min"], max: question["max"], step: question["step"], width: 20, :readonly => question["readonly"], - **numeric_question_html_attributes(question) + **stimulus_html_attributes(question) %> diff --git a/app/views/form/_radio_question.html.erb b/app/views/form/_radio_question.html.erb index a74153456..3f653eb7d 100644 --- a/app/views/form/_radio_question.html.erb +++ b/app/views/form/_radio_question.html.erb @@ -6,14 +6,8 @@ <% question["answer_options"].map do |key, val| %> <% if key.starts_with?("divider") %> <%= f.govuk_radio_divider %> - <% elsif question["conditional_for"] %> - <%= f.govuk_radio_button question_key, val, label: { text: val }, - "data-controller": "conditional-question", - "data-action": "conditional-question#displayConditional", - "data-info": question["conditional_for"].to_json - %> <% else %> - <%= f.govuk_radio_button question_key, val, label: { text: val } %> + <%= f.govuk_radio_button question_key, val, label: { text: val }, **stimulus_html_attributes(question) %> <% end %> <% end %> <% end %> diff --git a/app/views/form/_text_question.html.erb b/app/views/form/_text_question.html.erb index 6bf73338e..4b45a8ceb 100644 --- a/app/views/form/_text_question.html.erb +++ b/app/views/form/_text_question.html.erb @@ -1,5 +1,6 @@ <%= f.govuk_text_field question_key, hint: { text: question["hint_text"] }, label: { text: question["header"].html_safe, size: "l"}, - width: 20 + width: 20, + **stimulus_html_attributes(question) %> diff --git a/app/views/form/check_answers.html.erb b/app/views/form/check_answers.html.erb index a2102146d..b6533b4c2 100644 --- a/app/views/form/check_answers.html.erb +++ b/app/views/form/check_answers.html.erb @@ -1,11 +1,13 @@ <%= turbo_frame_tag "case_log_form", target: "_top" do %>
-

Check the answers you gave for <%= subsection %>

- <%= display_answered_questions_summary(subsection_pages, case_log) %> - <% subsection_pages.each do |page, page_info| %> +

Check the answers you gave for <%= subsection.humanize(capitalize: false) %>

+ <%= display_answered_questions_summary(subsection, case_log) %> + <% subsection_pages(subsection).each do |page, page_info| %> <% page_info["questions"].each do |question_title, question_info| %> - <%= render partial: 'form/check_answers_table', locals: { question_title: question_title, question_info: question_info, case_log: case_log, page: page } %> + <% if total_questions(subsection, case_log).include?(question_title) %> + <%= render partial: 'form/check_answers_table', locals: { question_title: question_title, question_info: question_info, case_log: case_log, page: page } %> + <%end %> <%end %> <% end %> <%= form_with action: '/case_logs', method: "next_page", builder: GOVUKDesignSystemFormBuilder::FormBuilder do |f| %> diff --git a/config/forms/2021_2022.json b/config/forms/2021_2022.json index a5b3072ce..eb91f81e3 100644 --- a/config/forms/2021_2022.json +++ b/config/forms/2021_2022.json @@ -9,10 +9,10 @@ "household_characteristics": { "label": "Household characteristics", "pages": { - "tenant_code":{ + "tenant_code": { "header": "", "description": "", - "questions":{ + "questions": { "tenant_code": { "check_answer_label": "Tenant code", "header": "What is the tenant code?", @@ -21,10 +21,10 @@ } } }, - "tenant_age":{ + "tenant_age": { "header": "", "description": "", - "questions":{ + "questions": { "tenant_age": { "check_answer_label": "Tenant's age", "header": "What is the tenant's age?", @@ -36,10 +36,10 @@ } } }, - "tenant_gender":{ + "tenant_gender": { "header": "", "description": "", - "questions":{ + "questions": { "tenant_gender": { "check_answer_label": "Tenant's gender", "header": "Which of these best describes the tenant's gender identity?", @@ -54,10 +54,10 @@ } } }, - "tenant_ethnic_group":{ + "tenant_ethnic_group": { "header": "", "description": "", - "questions":{ + "questions": { "tenant_ethnic_group": { "check_answer_label": "Ethnicity", "header": "What is the tenant's ethnic group?", @@ -87,10 +87,10 @@ } } }, - "tenant_nationality":{ + "tenant_nationality": { "header": "", "description": "", - "questions":{ + "questions": { "tenant_nationality": { "check_answer_label": "Nationality", "header": "What is the tenant's nationality?", @@ -117,10 +117,10 @@ } } }, - "tenant_economic_status":{ + "tenant_economic_status": { "header": "", "description": "", - "questions":{ + "questions": { "tenant_economic_status": { "check_answer_label": "Work", "header": "Which of these best describes the tenant's working situation?", @@ -142,10 +142,10 @@ } } }, - "household_number_of_other_members":{ + "household_number_of_other_members": { "header": "", "description": "", - "questions":{ + "questions": { "household_number_of_other_members": { "check_answer_label": "Number of Other Household Members", "header": "How many other people are there in the household?", @@ -153,7 +153,401 @@ "type": "numeric", "min": 0, "max": 7, + "step": 1, + "conditional_for": { + "person_2_relationship": ">0", + "person_2_age": ">0", + "person_2_gender": ">0", + "person_2_economic_status": ">0", + "person_3_relationship": ">1", + "person_3_age": ">1", + "person_3_gender": ">1", + "person_3_economic_status": ">1", + "person_4_relationship": ">2", + "person_4_age": ">2", + "person_4_gender": ">2", + "person_4_economic_status": ">2", + "person_5_relationship": ">3", + "person_5_age": ">3", + "person_5_gender": ">3", + "person_5_economic_status": ">3", + "person_6_relationship": ">4", + "person_6_age": ">4", + "person_6_gender": ">4", + "person_6_economic_status": ">4", + "person_7_relationship": ">5", + "person_7_age": ">5", + "person_7_gender": ">5", + "person_7_economic_status": ">5", + "person_8_relationship": ">6", + "person_8_age": ">6", + "person_8_gender": ">6", + "person_8_economic_status": ">6" + } + }, + "person_2_relationship": { + "check_answer_label": "Person 2's relationship to lead tenant", + "header": "What's person 2's relationship to lead tenant", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Partner", + "1": "Child - includes young adult and grown-up", + "2": "Other", + "3": "Prefer not to say" + } + }, + "person_2_age": { + "check_answer_label": "Person 2's age", + "header": "What's person 2's age", + "hint_text": "", + "type": "numeric", + "min": 0, + "max": 150, + "step": 1 + }, + "person_2_gender": { + "check_answer_label": "Person 2's gender", + "header": "Which of these best describes person 2's gender identity?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Female", + "1": "Male", + "2": "Non-binary", + "3": "Prefer not to say" + } + }, + "person_2_economic_status": { + "check_answer_label": "Person 2's Work", + "header": "Which of these best describes person 2's working situation?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Part-time - Less than 30 hours", + "1": "Full-time - 30 hours or more", + "2": "In government training into work, such as New Deal", + "3": "Jobseeker", + "4": "Retired", + "5": "Not seeking work", + "6": "Full-time student", + "7": "Unable to work because of long term sick or disability", + "8": "Child under 16", + "9": "Other", + "10": "Prefer not to say" + } + }, + "person_3_relationship": { + "check_answer_label": "Person 3's relationship to lead tenant", + "header": "What's person 3's relationship to lead tenant", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Partner", + "1": "Child - includes young adult and grown-up", + "2": "Other", + "3": "Prefer not to say" + } + }, + "person_3_age": { + "check_answer_label": "Person 3's age", + "header": "What's person 3's age", + "hint_text": "", + "type": "numeric", + "min": 0, + "max": 150, + "step": 1 + }, + "person_3_gender": { + "check_answer_label": "Person 3's gender", + "header": "Which of these best describes person 3's gender identity?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Female", + "1": "Male", + "2": "Non-binary", + "3": "Prefer not to say" + } + }, + "person_3_economic_status": { + "check_answer_label": "Person 3's Work", + "header": "Which of these best describes person 3's working situation?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Part-time - Less than 30 hours", + "1": "Full-time - 30 hours or more", + "2": "In government training into work, such as New Deal", + "3": "Jobseeker", + "4": "Retired", + "5": "Not seeking work", + "6": "Full-time student", + "7": "Unable to work because of long term sick or disability", + "8": "Child under 16", + "9": "Other", + "10": "Prefer not to say" + } + }, + "person_4_relationship": { + "check_answer_label": "Person 4's relationship to lead tenant", + "header": "What's person 4's relationship to lead tenant", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Partner", + "1": "Child - includes young adult and grown-up", + "2": "Other", + "3": "Prefer not to say" + } + }, + "person_4_age": { + "check_answer_label": "Person 4's age", + "header": "What's person 4's age", + "hint_text": "", + "type": "numeric", + "min": 0, + "max": 150, + "step": 1 + }, + "person_4_gender": { + "check_answer_label": "Person 4's gender", + "header": "Which of these best describes person 4's gender identity?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Female", + "1": "Male", + "2": "Non-binary", + "3": "Prefer not to say" + } + }, + "person_4_economic_status": { + "check_answer_label": "Person 4's Work", + "header": "Which of these best describes person 4's working situation?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Part-time - Less than 30 hours", + "1": "Full-time - 30 hours or more", + "2": "In government training into work, such as New Deal", + "3": "Jobseeker", + "4": "Retired", + "5": "Not seeking work", + "6": "Full-time student", + "7": "Unable to work because of long term sick or disability", + "8": "Child under 16", + "9": "Other", + "10": "Prefer not to say" + } + }, + "person_5_relationship": { + "check_answer_label": "Person 5's relationship to lead tenant", + "header": "What's person 5's relationship to lead tenant", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Partner", + "1": "Child - includes young adult and grown-up", + "2": "Other", + "3": "Prefer not to say" + } + }, + "person_5_age": { + "check_answer_label": "Person 5's age", + "header": "What's person 5's age", + "hint_text": "", + "type": "numeric", + "min": 0, + "max": 150, + "step": 1 + }, + "person_5_gender": { + "check_answer_label": "Person 5's gender", + "header": "Which of these best describes person 5's gender identity?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Female", + "1": "Male", + "2": "Non-binary", + "3": "Prefer not to say" + } + }, + "person_5_economic_status": { + "check_answer_label": "Person 5's Work", + "header": "Which of these best describes person 5's working situation?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Part-time - Less than 30 hours", + "1": "Full-time - 30 hours or more", + "2": "In government training into work, such as New Deal", + "3": "Jobseeker", + "4": "Retired", + "5": "Not seeking work", + "6": "Full-time student", + "7": "Unable to work because of long term sick or disability", + "8": "Child under 16", + "9": "Other", + "10": "Prefer not to say" + } + }, + "person_6_relationship": { + "check_answer_label": "Person 6's relationship to lead tenant", + "header": "What's person 6's relationship to lead tenant", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Partner", + "1": "Child - includes young adult and grown-up", + "2": "Other", + "3": "Prefer not to say" + } + }, + "person_6_age": { + "check_answer_label": "Person 6's age", + "header": "What's person 6's age", + "hint_text": "", + "type": "numeric", + "min": 0, + "max": 150, + "step": 1 + }, + "person_6_gender": { + "check_answer_label": "Person 6's gender", + "header": "Which of these best describes person 6's gender identity?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Female", + "1": "Male", + "2": "Non-binary", + "3": "Prefer not to say" + } + }, + "person_6_economic_status": { + "check_answer_label": "Person 6's Work", + "header": "Which of these best describes person 6's working situation?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Part-time - Less than 30 hours", + "1": "Full-time - 30 hours or more", + "2": "In government training into work, such as New Deal", + "3": "Jobseeker", + "4": "Retired", + "5": "Not seeking work", + "6": "Full-time student", + "7": "Unable to work because of long term sick or disability", + "8": "Child under 16", + "9": "Other", + "10": "Prefer not to say" + } + }, + "person_7_relationship": { + "check_answer_label": "Person 7's relationship to lead tenant", + "header": "What's person 7's relationship to lead tenant", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Partner", + "1": "Child - includes young adult and grown-up", + "2": "Other", + "3": "Prefer not to say" + } + }, + "person_7_age": { + "check_answer_label": "Person 7's age", + "header": "What's person 7's age", + "hint_text": "", + "type": "numeric", + "min": 0, + "max": 150, + "step": 1 + }, + "person_7_gender": { + "check_answer_label": "Person 7's gender", + "header": "Which of these best describes person 7's gender identity?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Female", + "1": "Male", + "2": "Non-binary", + "3": "Prefer not to say" + } + }, + "person_7_economic_status": { + "check_answer_label": "Person 7's Work", + "header": "Which of these best describes person 7's working situation?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Part-time - Less than 30 hours", + "1": "Full-time - 30 hours or more", + "2": "In government training into work, such as New Deal", + "3": "Jobseeker", + "4": "Retired", + "5": "Not seeking work", + "6": "Full-time student", + "7": "Unable to work because of long term sick or disability", + "8": "Child under 16", + "9": "Other", + "10": "Prefer not to say" + } + }, + "person_8_relationship": { + "check_answer_label": "Person 8's relationship to lead tenant", + "header": "What's person 8's relationship to lead tenant", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Partner", + "1": "Child - includes young adult and grown-up", + "2": "Other", + "3": "Prefer not to say" + } + }, + "person_8_age": { + "check_answer_label": "Person 8's age", + "header": "What's person 8's age", + "hint_text": "", + "type": "numeric", + "min": 0, + "max": 150, "step": 1 + }, + "person_8_gender": { + "check_answer_label": "Person 8's gender", + "header": "Which of these best describes person 8's gender identity?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Female", + "1": "Male", + "2": "Non-binary", + "3": "Prefer not to say" + } + }, + "person_8_economic_status": { + "check_answer_label": "Person 8's Work", + "header": "Which of these best describes person 8's working situation?", + "hint_text": "", + "type": "radio", + "answer_options": { + "0": "Part-time - Less than 30 hours", + "1": "Full-time - 30 hours or more", + "2": "In government training into work, such as New Deal", + "3": "Jobseeker", + "4": "Retired", + "5": "Not seeking work", + "6": "Full-time student", + "7": "Unable to work because of long term sick or disability", + "8": "Child under 16", + "9": "Other", + "10": "Prefer not to say" + } } } } @@ -162,10 +556,10 @@ "household_situation": { "label": "Household situation", "pages": { - "previous_housing_situation":{ + "previous_housing_situation": { "header": "", "description": "", - "questions":{ + "questions": { "previous_housing_situation": { "header": "What was the tenant’s housing situation immediately before this letting?", "hint_text": "", @@ -198,10 +592,10 @@ } } }, - "homelessness":{ + "homelessness": { "header": "", "description": "", - "questions":{ + "questions": { "homelessness": { "header": "Did the tenant experience homelessness immediately before this letting?", "hint_text": "", @@ -214,10 +608,10 @@ } } }, - "last_settled_home":{ + "reason_for_leaving_last_settled_home": { "header": "Leaving their last settled home", "description": "", - "questions":{ + "questions": { "last_settled_home": { "header": "What is the tenant’s main reason for leaving?", "hint_text": "", @@ -257,8 +651,16 @@ "31": "Other", "32": "Do not know", "33": "Prefer not to say" + }, + "conditional_for": { + "other_reason_for_leaving_last_settled_home": ["Other"] } }, + "other_reason_for_leaving_last_settled_home": { + "header": "Please state the reason for leaving last settled home", + "hint_text": "", + "type": "text" + }, "benefit_cap_spare_room_subsidy": { "header": "Was the reason for leaving because of the benefit cap or removal of the spare room subsidy?", "hint_text": "", @@ -279,10 +681,10 @@ "household_needs": { "label": "Household needs", "pages": { - "armed_forces":{ + "armed_forces": { "header": "Experience of the UK Armed Forces", "description": "", - "questions":{ + "questions": { "armed_forces": { "header": "Has the tenant ever served in the UK armed forces?", "hint_text": "", @@ -339,7 +741,7 @@ "medical_conditions": { "header": "", "description": "", - "questions":{ + "questions": { "medical_conditions": { "header": "Does anyone in the household have any of the following that they expect to last for 12 months or more:", "hint_text": "", @@ -357,7 +759,7 @@ "pregnancy": { "header": "", "description": "", - "questions":{ + "questions": { "pregnancy": { "header": "Is anyone in the household pregnant?", "hint_text": "", @@ -374,7 +776,7 @@ "accessibility_requirements": { "header": "", "description": "", - "questions":{ + "questions": { "accessibility_requirements": { "header": "Are any of these affected by their condition or illness?", "hint_text": "Select all that apply", @@ -397,7 +799,7 @@ "condition_effects": { "header": "", "description": "", - "questions":{ + "questions": { "condition_effects": { "header": "Are any of these affected by their condition or illness?", "hint_text": "Select all that apply", @@ -430,10 +832,10 @@ "tenancy_information": { "label": "Tenancy information", "pages": { - "tenancy_code":{ + "tenancy_code": { "header": "", "description": "", - "questions":{ + "questions": { "tenancy_code": { "check_answer_label": "What is the tenancy code?", "header": "What is the tenancy code?", @@ -470,10 +872,10 @@ } } }, - "fixed_term_tenancy":{ + "fixed_term_tenancy": { "header": "", "description": "", - "questions":{ + "questions": { "fixed_term_tenancy": { "check_answer_label": "If the main tenancy is a fixed term tenancy, please provide the length of the fixed term (to the nearest year) excluding any starter/introductory period", "header": "If fixed-term, what is the length of the fixed-term tenancy after any starter period?", @@ -546,10 +948,10 @@ "property_information": { "label": "Property information", "pages": { - "property_location":{ + "property_location": { "header": "", "description": "", - "questions":{ + "questions": { "property_location": { "check_answer_label": "Property Location", "header": "Property location", @@ -873,10 +1275,10 @@ } } }, - "property_postcode":{ + "property_postcode": { "header": "", "description": "", - "questions":{ + "questions": { "property_postcode": { "check_answer_label": "What was the previous postcode?", "header": "What is the property's postcode?", @@ -932,7 +1334,7 @@ "property_reference": { "header": "", "description": "", - "questions":{ + "questions": { "property_reference": { "check_answer_label": "What’s the property reference?", "header": "What's the property reference?", @@ -966,7 +1368,7 @@ "property_number_of_bedrooms": { "header": "", "description": "", - "questions":{ + "questions": { "property_number_of_bedrooms": { "check_answer_label": "How many bedrooms are there in the property?", "header": "How many bedrooms are there in the property?", @@ -990,15 +1392,12 @@ "answer_options": { "0": "Yes", "1": "No" + }, + "conditional_for": { + "property_major_repairs_date": ["Yes"] } - } - } - }, - "property_major_repairs_date": { - "header": "", - "description": "", - "questions": { - "property_major_repairs": { + }, + "property_major_repairs_date": { "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", @@ -1009,7 +1408,7 @@ "property_number_of_times_relet": { "header": "", "description": "", - "questions":{ + "questions": { "property_number_of_times_relet": { "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? ", @@ -1047,10 +1446,10 @@ "income_and_benefits": { "label": "Income and benefits", "pages": { - "net_income":{ + "net_income": { "header": "", "description": "", - "questions":{ + "questions": { "net_income": { "check_answer_label": "Income", "header": "What is the tenant’s /and partner’s combined income after tax?", @@ -1072,10 +1471,10 @@ } } }, - "net_income_uc_proportion":{ + "net_income_uc_proportion": { "header": "", "description": "", - "questions":{ + "questions": { "net_income_uc_proportion": { "check_answer_label": "Benefits as a proportion of income", "header": "How much of the tenant’s income is from Universal Credit, state pensions or benefits?", @@ -1090,10 +1489,10 @@ } } }, - "housing_benefit":{ + "housing_benefit": { "header": "", "description": "", - "questions":{ + "questions": { "housing_benefit": { "check_answer_label": "Universal Credit & Housing Benefit\t", "header": "Is the tenant likely to be in receipt of any of these housing-related benefits?", @@ -1116,10 +1515,10 @@ "rent": { "label": "Rent", "pages": { - "rent":{ + "rent": { "header": "", "description": "", - "questions":{ + "questions": { "rent_frequency": { "check_answer_label": "Rent Period", "header": "Which period are rent and other charges due?", @@ -1187,7 +1586,7 @@ "step": 1, "readonly": true }, - "outstanding_amount": { + "outstanding_rent_or_charges": { "check_answer_label": "After housing benefit and/or housing element of UC payment is received, will there be an outstanding amount for basic rent and/or benefit eligible charges?", "header": "After housing benefit and/or housing element of UC payment is received, will there be an outstanding amount for basic rent and/or benefit eligible charges?", "hint_text": "", @@ -1195,7 +1594,18 @@ "answer_options": { "0": "Yes", "1": "No" + }, + "conditional_for": { + "outstanding_amount": ["Yes"] } + }, + "outstanding_amount": { + "check_answer_label": "Outstanding amount", + "header": "What do you expect the amount to be?", + "hint_text": "If the amount is unknown you can estimate", + "type": "numeric", + "min": 0, + "step": 1 } } } @@ -1209,10 +1619,10 @@ "local_authority": { "label": "Local authority", "pages": { - "time_lived_in_la":{ + "time_lived_in_la": { "header": "", "description": "", - "questions":{ + "questions": { "time_lived_in_la": { "check_answer_label": "How long has the household continuously lived in the local authority area where the new letting is located?", "header": "How long has the household continuously lived in the local authority area where the new letting is located?", @@ -1231,10 +1641,10 @@ } } }, - "time_on_la_waiting_list":{ + "time_on_la_waiting_list": { "header": "", "description": "", - "questions":{ + "questions": { "time_on_la_waiting_list": { "check_answer_label": "How long has the household been on the local authority waiting list where the new letting is located?", "header": "How long has the household been on the local authority waiting list where the new letting is located?", @@ -1253,10 +1663,10 @@ } } }, - "previous_la":{ + "previous_la": { "header": "", "description": "", - "questions":{ + "questions": { "previous_la": { "check_answer_label": "The LA in which household lived immediately before this letting\t", "header": "Which local authority area did the household live in immediately before this letting?", @@ -1604,6 +2014,9 @@ "answer_options": { "0": "Yes", "1": "No" + }, + "conditional_for": { + "reasonable_preference_reason": ["Yes"] } }, "reasonable_preference_reason": { @@ -1668,10 +2081,10 @@ "declaration": { "label": "Declaration", "pages": { - "declaration":{ + "declaration": { "header": "", "description": "", - "questions":{ + "questions": { "declaration": { "check_answer_label": "", "header": "What is the tenant code?", diff --git a/config/routes.rb b/config/routes.rb index 609124146..4f9db913d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,7 +3,7 @@ Rails.application.routes.draw do get "about", to: "about#index" get "/", to: "test#index" - post '/case_logs/:id', to: "case_logs#submit_form" + post "/case_logs/:id", to: "case_logs#submit_form" form = Form.new(2021, 2022) resources :case_logs do diff --git a/db/migrate/20211005115813_add_conditional_fields_to_case_logs.rb b/db/migrate/20211005115813_add_conditional_fields_to_case_logs.rb new file mode 100644 index 000000000..6f8c68a01 --- /dev/null +++ b/db/migrate/20211005115813_add_conditional_fields_to_case_logs.rb @@ -0,0 +1,9 @@ +class AddConditionalFieldsToCaseLogs < ActiveRecord::Migration[6.1] + def change + change_table :case_logs, bulk: true do |t| + t.column :outstanding_rent_or_charges, :string + t.column :other_reason_for_leaving_last_settled_home, :string + t.rename :last_settled_home, :reason_for_leaving_last_settled_home + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e3215a238..fb810f511 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_09_24_143031) do +ActiveRecord::Schema.define(version: 2021_10_05_115813) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -59,7 +59,7 @@ ActiveRecord::Schema.define(version: 2021_09_24_143031) do t.string "person_8_gender" t.string "person_8_economic" t.string "homelessness" - t.string "last_settled_home" + t.string "reason_for_leaving_last_settled_home" t.string "benefit_cap_spare_room_subsidy" t.string "armed_forces_active" t.string "armed_forces_injured" @@ -108,6 +108,7 @@ ActiveRecord::Schema.define(version: 2021_09_24_143031) do t.string "cbl_letting" t.string "chr_letting" t.string "cap_letting" + t.string "outstanding_rent_or_charges" end end diff --git a/spec/helpers/check_answers_helper_spec.rb b/spec/helpers/check_answers_helper_spec.rb index aabcc80cd..42cc71154 100644 --- a/spec/helpers/check_answers_helper_spec.rb +++ b/spec/helpers/check_answers_helper_spec.rb @@ -2,23 +2,105 @@ require "rails_helper" RSpec.describe CheckAnswersHelper do let(:case_log) { FactoryBot.create(:case_log) } - let(:form) { Form.new(2021, 2022) } - let(:subsection_pages) { form.pages_for_subsection("income_and_benefits") } + let(:case_log_with_met_numeric_condition) do + FactoryBot.create( + :case_log, + :in_progress, + household_number_of_other_members: 2, + person_2_relationship: "Partner", + ) + end + let(:case_log_with_met_radio_condition) do + FactoryBot.create(:case_log, armed_forces: "Yes - a regular") + end + let(:subsection) { "income_and_benefits" } + let(:subsection_with_numeric_conditionals) { "household_characteristics" } + let(:subsection_with_radio_conditionals) { "household_needs" } describe "Get answered questions total" do it "returns 0 if no questions are answered" do - expect(get_answered_questions_total(subsection_pages, case_log)).to equal(0) + expect(total_answered_questions(subsection, case_log)).to equal(0) end it "returns 1 if 1 question gets answered" do case_log["net_income"] = "123" - expect(get_answered_questions_total(subsection_pages, case_log)).to equal(1) + expect(total_answered_questions(subsection, case_log)).to equal(1) + end + + it "ignores questions with unmet numeric conditions" do + case_log["tenant_code"] = "T1234" + expect(total_answered_questions(subsection_with_numeric_conditionals, case_log)).to equal(1) + end + + it "includes conditional questions with met numeric conditions" do + expect(total_answered_questions( + subsection_with_numeric_conditionals, + case_log_with_met_numeric_condition, + )).to equal(4) + end + + it "ignores questions with unmet radio conditions" do + case_log["armed_forces"] = "No" + expect(total_answered_questions(subsection_with_radio_conditionals, case_log)).to equal(1) + end + + it "includes conditional questions with met radio conditions" do + case_log_with_met_radio_condition["armed_forces_injured"] = "No" + case_log_with_met_radio_condition["medical_conditions"] = "No" + expect(total_answered_questions( + subsection_with_radio_conditionals, + case_log_with_met_radio_condition, + )).to equal(3) end end describe "Get total number of questions" do it "returns the total number of questions for a subsection" do - expect(get_total_number_of_questions(subsection_pages)).to eq(4) + expect(total_number_of_questions(subsection, case_log)).to eq(4) + end + + it "ignores questions with unmet numeric conditions" do + expect(total_number_of_questions(subsection_with_numeric_conditionals, case_log)).to eq(7) + end + + it "includes conditional questions with met numeric conditions" do + expect(total_number_of_questions( + subsection_with_numeric_conditionals, + case_log_with_met_numeric_condition, + )).to eq(15) + end + + it "ignores questions with unmet radio conditions" do + expect(total_number_of_questions(subsection_with_radio_conditionals, case_log)).to eq(6) + end + + it "includes conditional questions with met radio conditions" do + expect(total_number_of_questions( + subsection_with_radio_conditionals, + case_log_with_met_radio_condition, + )).to eq(8) + end + + context "conditional questions with type that hasn't been implemented yet" do + let(:unimplemented_conditional) do + { "question_1" => + { "header" => "The actual question?", + "hint_text" => "", + "type" => "date", + "check_answer_label" => "Question Label", + "conditional_for" => { "question_2" => %w[12-12-2021] } }, + "question_2" => + { "header" => "The second actual question?", + "hint_text" => "", + "type" => "radio", + "check_answer_label" => "The second question label", + "answer_options" => { "0" => "Yes", "1" => "No" } } } + end + + it "raises an error" do + allow_any_instance_of(Form).to receive(:questions_for_subsection).and_return(unimplemented_conditional) + expect { total_number_of_questions(subsection, case_log) }.to raise_error(RuntimeError, "Not implemented yet") + end end end end diff --git a/spec/helpers/numeric_questions_helper_spec.rb b/spec/helpers/numeric_questions_helper_spec.rb deleted file mode 100644 index 5302d9eaa..000000000 --- a/spec/helpers/numeric_questions_helper_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "rails_helper" - -RSpec.describe NumericQuestionsHelper do - let(:form) { Form.new(2021, 2022) } - let(:questions) { form.questions_for_page("rent") } - - describe "html attributes" do - it "returns empty hash if fields-to-add or result-field are empty " do - expect(numeric_question_html_attributes(questions["total_charge"])).to eq({}) - end - - it "returns html attributes if fields-to-add or result-field are not empty " do - expect(numeric_question_html_attributes(questions["basic_rent"])).to eq({ - "data-controller": "numeric-question", - "data-action": "numeric-question#calculateFields", - "data-target": "case-log-#{questions['basic_rent']['result-field'].to_s.dasherize}-field", - "data-calculated": questions["basic_rent"]["fields-to-add"].to_json, - }) - end - end -end diff --git a/spec/helpers/question_attribute_helper_spec.rb b/spec/helpers/question_attribute_helper_spec.rb new file mode 100644 index 000000000..aec36ea31 --- /dev/null +++ b/spec/helpers/question_attribute_helper_spec.rb @@ -0,0 +1,51 @@ +require "rails_helper" + +RSpec.describe QuestionAttributeHelper do + let(:form) { Form.new(2021, 2022) } + let(:questions) { form.questions_for_page("rent") } + + describe "html attributes" do + it "returns empty hash if fields-to-add or result-field are empty " do + expect(stimulus_html_attributes(questions["total_charge"])).to eq({}) + end + + it "returns html attributes if fields-to-add or result-field are not empty " do + expect(stimulus_html_attributes(questions["basic_rent"])).to eq({ + "data-controller": "numeric-question", + "data-action": "numeric-question#calculateFields", + "data-target": "case-log-#{questions['basic_rent']['result-field'].to_s.dasherize}-field", + "data-calculated": questions["basic_rent"]["fields-to-add"].to_json, + }) + end + + context "a question that requires multiple controllers" do + let(:question) do + { + "check_answer_label" => "Basic Rent", + "header" => "What is the basic rent?", + "hint_text" => "Eligible for housing benefit or Universal Credit", + "type" => "numeric", + "min" => 0, + "step" => 1, + "fields-to-add" => %w[basic_rent service_charge personal_service_charge support_charge], + "result-field" => "total_charge", + "conditional_for" => { + "next_question": ">1", + }, + } + end + let(:expected_attribs) do + { + "data-controller": "numeric-question conditional-question", + "data-action": "numeric-question#calculateFields conditional-question#displayConditional", + "data-target": "case-log-#{question['result-field'].to_s.dasherize}-field", + "data-calculated": question["fields-to-add"].to_json, + "data-info": question["conditional_for"].to_json, + } + end + it "correctly merges html attributes" do + expect(stimulus_html_attributes(question)).to eq(expected_attribs) + end + end + end +end