Browse Source

CLDC-341 - Conditional Questions (#31)

* Make outstanding amount question conditional

* Make major repairs date conditional on major repairs having been done

* Add Other reason for leaving as a conditional question

* Make reason for reasonable preference conditional on reasonable preference being given

* Refactor conditional html attributes

* Pretty print config json

* Add numeric conditional

* Add the rest of the conditionals

* Rubocop

* Refactor how we add stimulus controller html attributes so that we can combine them

* Spec attrib merging

* Fix question counts for sections with conditional questions

* Some linting

* Spec question counting behaviour

* Linting

* Add some more readme explanation

* Rename controller helper

* Rename variables for clarity
pull/40/head
Daniel Baark 3 years ago committed by GitHub
parent
commit
f6734a60e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      Gemfile.lock
  2. 7
      README.md
  3. 4
      app/controllers/case_logs_controller.rb
  4. 56
      app/helpers/check_answers_helper.rb
  5. 12
      app/helpers/numeric_questions_helper.rb
  6. 39
      app/helpers/question_attribute_helper.rb
  7. 36
      app/javascript/controllers/conditional_question_controller.js
  8. 9
      app/models/form.rb
  9. 2
      app/views/form/_checkbox_question.html.erb
  10. 3
      app/views/form/_date_question.html.erb
  11. 2
      app/views/form/_numeric_question.html.erb
  12. 8
      app/views/form/_radio_question.html.erb
  13. 3
      app/views/form/_text_question.html.erb
  14. 8
      app/views/form/check_answers.html.erb
  15. 429
      config/forms/2021_2022.json
  16. 2
      config/routes.rb
  17. 9
      db/migrate/20211005115813_add_conditional_fields_to_case_logs.rb
  18. 5
      db/schema.rb
  19. 92
      spec/helpers/check_answers_helper_spec.rb
  20. 21
      spec/helpers/numeric_questions_helper_spec.rb
  21. 51
      spec/helpers/question_attribute_helper_spec.rb

4
Gemfile.lock

@ -26,7 +26,7 @@ GIT
GIT GIT
remote: https://github.com/rspec/rspec-rails.git remote: https://github.com/rspec/rspec-rails.git
revision: fdcd1df0b13f9b6547336b4d37dffb66f70f7228 revision: 3f0e35085f5765decf96fee179ec9a2e132a67c1
branch: main branch: main
specs: specs:
rspec-rails (5.1.0.pre) rspec-rails (5.1.0.pre)
@ -294,7 +294,7 @@ GEM
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
stimulus-rails (0.6.1) stimulus-rails (0.7.0)
rails (>= 6.0.0) rails (>= 6.0.0)
thor (1.1.0) thor (1.1.0)
turbo-rails (0.8.1) turbo-rails (0.8.1)

7
README.md

@ -111,6 +111,10 @@ The JSON should follow the structure:
"answer_options": { // checkbox and radio only "answer_options": { // checkbox and radio only
"0": String, "0": String,
"1": 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 - All pages have at least 1 question
- The ActiveRecord case log model has a field for each question name (must match) - 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 - 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) ## Useful documentation (external dependencies)

4
app/controllers/case_logs_controller.rb

@ -37,11 +37,9 @@ class CaseLogsController < ApplicationController
def check_answers def check_answers
@case_log = CaseLog.find(params[:case_log_id]) @case_log = CaseLog.find(params[:case_log_id])
form = Form.new(2021, 2022)
current_url = request.env["PATH_INFO"] current_url = request.env["PATH_INFO"]
subsection = current_url.split("/")[-2] subsection = current_url.split("/")[-2]
subsection_pages = form.pages_for_subsection(subsection) render "form/check_answers", locals: { case_log: @case_log, subsection: subsection }
render "form/check_answers", locals: { case_log: @case_log, subsection_pages: subsection_pages, subsection: subsection.humanize(capitalize: false) }
end end
form = Form.new(2021, 2022) form = Form.new(2021, 2022)

56
app/helpers/check_answers_helper.rb

@ -1,16 +1,44 @@
module CheckAnswersHelper module CheckAnswersHelper
def get_answered_questions_total(subsection_pages, case_log) def total_answered_questions(subsection, case_log)
questions = subsection_pages.values.flat_map do |page| total_questions(subsection, case_log).keys.count do |question_key|
page["questions"].keys case_log[question_key].present?
end end
questions.count { |question| case_log[question].present? }
end end
def get_total_number_of_questions(subsection_pages) def total_number_of_questions(subsection, case_log)
questions = subsection_pages.values.flat_map do |page| total_questions(subsection, case_log).count
page["questions"].keys
end end
questions.count
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
end
def subsection_pages(subsection)
form = Form.new(2021, 2022)
form.pages_for_subsection(subsection)
end end
def create_update_answer_link(case_log_answer, case_log_id, page) 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 link_to(link_name, "/case_logs/#{case_log_id}/#{page}", class: "govuk-link").html_safe
end 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 = [] 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? } page_info["questions"].any? { |question| case_log[question].blank? }
pages_to_fill_in << page_title pages_to_fill_in << page_title
end end
@ -28,12 +56,12 @@ module CheckAnswersHelper
link_to("Answer the missing questions", url, class: "govuk-link").html_safe link_to("Answer the missing questions", url, class: "govuk-link").html_safe
end end
def display_answered_questions_summary(subsection_pages, case_log) def display_answered_questions_summary(subsection, case_log)
if get_answered_questions_total(subsection_pages, case_log) == get_total_number_of_questions(subsection_pages) if total_answered_questions(subsection, case_log) == total_number_of_questions(subsection, case_log)
'<p class="govuk-body govuk-!-margin-bottom-7">You answered all the questions</p>'.html_safe '<p class="govuk-body govuk-!-margin-bottom-7">You answered all the questions</p>'.html_safe
else else
"<p class=\"govuk-body govuk-!-margin-bottom-7\">You answered #{get_answered_questions_total(subsection_pages, case_log)} of #{get_total_number_of_questions(subsection_pages)} questions</p> "<p class=\"govuk-body govuk-!-margin-bottom-7\">You answered #{total_answered_questions(subsection, case_log)} of #{total_number_of_questions(subsection, case_log)} questions</p>
#{create_next_missing_question_link(case_log['id'], subsection_pages, case_log)}".html_safe #{create_next_missing_question_link(case_log['id'], subsection, case_log)}".html_safe
end end
end end
end end

12
app/helpers/numeric_questions_helper.rb

@ -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

39
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

36
app/javascript/controllers/conditional_question_controller.js

@ -6,17 +6,29 @@ export default class extends Controller {
} }
displayConditional() { 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) { if(this.element.checked) {
let selected = this.element.value let selectedValue = this.element.value
let conditional_for = JSON.parse(this.element.dataset.info) let conditional_for = JSON.parse(this.element.dataset.info)
Object.entries(conditional_for).forEach(([key, values]) => { Object.entries(conditional_for).forEach(([targetQuestion, conditions]) => {
let div = document.getElementById(key + "_div") let div = document.getElementById(targetQuestion + "_div")
if(values.includes(selected)) { if(conditions.includes(selectedValue)) {
div.style.display = "block" div.style.display = "block"
} else { } else {
div.style.display = "none" div.style.display = "none"
let buttons = document.getElementsByName(`case_log[${key}]`) let buttons = document.getElementsByName(`case_log[${targetQuestion}]`)
Object.entries(buttons).forEach(([idx, button]) => { Object.entries(buttons).forEach(([idx, button]) => {
button.checked = false; 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"
}
})
}
} }

9
app/models/form.rb

@ -37,6 +37,11 @@ class Form
all_pages[page]["questions"] all_pages[page]["questions"]
end 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) def first_page_for_subsection(subsection)
pages_for_subsection(subsection).keys.first pages_for_subsection(subsection).keys.first
end end
@ -70,8 +75,4 @@ class Form
pages_for_subsection(subsection).keys[current_page_idx - 1] pages_for_subsection(subsection).keys[current_page_idx - 1]
end end
def questions_for_subsection(subsection)
pages_for_subsection(subsection).map { |title, _value| questions_for_page(title) }.reduce(:merge)
end
end end

2
app/views/form/_checkbox_question.html.erb

@ -6,7 +6,7 @@
<% if key.starts_with?("divider") %> <% if key.starts_with?("divider") %>
<%= f.govuk_check_box_divider %> <%= f.govuk_check_box_divider %>
<% else %> <% 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 %> <% end %>
<% end %> <% end %>

3
app/views/form/_date_question.html.erb

@ -1,5 +1,6 @@
<%= f.govuk_date_field question_key, <%= f.govuk_date_field question_key,
hint: { text: question["hint_text"] }, hint: { text: question["hint_text"] },
legend: { text: question["header"].html_safe, size: "l"}, legend: { text: question["header"].html_safe, size: "l"},
width: 20 width: 20,
**stimulus_html_attributes(question)
%> %>

2
app/views/form/_numeric_question.html.erb

@ -3,5 +3,5 @@
label: { text: question["header"].html_safe, size: "l"}, label: { text: question["header"].html_safe, size: "l"},
min: question["min"], max: question["max"], step: question["step"], min: question["min"], max: question["max"], step: question["step"],
width: 20, :readonly => question["readonly"], width: 20, :readonly => question["readonly"],
**numeric_question_html_attributes(question) **stimulus_html_attributes(question)
%> %>

8
app/views/form/_radio_question.html.erb

@ -6,14 +6,8 @@
<% question["answer_options"].map do |key, val| %> <% question["answer_options"].map do |key, val| %>
<% if key.starts_with?("divider") %> <% if key.starts_with?("divider") %>
<%= f.govuk_radio_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 %> <% 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 %> <% end %>
<% end %> <% end %>

3
app/views/form/_text_question.html.erb

@ -1,5 +1,6 @@
<%= f.govuk_text_field question_key, <%= f.govuk_text_field question_key,
hint: { text: question["hint_text"] }, hint: { text: question["hint_text"] },
label: { text: question["header"].html_safe, size: "l"}, label: { text: question["header"].html_safe, size: "l"},
width: 20 width: 20,
**stimulus_html_attributes(question)
%> %>

8
app/views/form/check_answers.html.erb

@ -1,13 +1,15 @@
<%= turbo_frame_tag "case_log_form", target: "_top" do %> <%= turbo_frame_tag "case_log_form", target: "_top" do %>
<div class="govuk-grid-row"> <div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop"> <div class="govuk-grid-column-two-thirds-from-desktop">
<h1 class="govuk-heading-l">Check the answers you gave for <%= subsection %></h1> <h1 class="govuk-heading-l">Check the answers you gave for <%= subsection.humanize(capitalize: false) %></h1>
<%= display_answered_questions_summary(subsection_pages, case_log) %> <%= display_answered_questions_summary(subsection, case_log) %>
<% subsection_pages.each do |page, page_info| %> <% subsection_pages(subsection).each do |page, page_info| %>
<% page_info["questions"].each do |question_title, question_info| %> <% page_info["questions"].each do |question_title, question_info| %>
<% 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 } %> <%= render partial: 'form/check_answers_table', locals: { question_title: question_title, question_info: question_info, case_log: case_log, page: page } %>
<%end %> <%end %>
<%end %> <%end %>
<% end %>
<%= form_with action: '/case_logs', method: "next_page", builder: GOVUKDesignSystemFormBuilder::FormBuilder do |f| %> <%= form_with action: '/case_logs', method: "next_page", builder: GOVUKDesignSystemFormBuilder::FormBuilder do |f| %>
<%= f.govuk_submit "Save and continue" %> <%= f.govuk_submit "Save and continue" %>
<% end %> <% end %>

429
config/forms/2021_2022.json

@ -153,7 +153,401 @@
"type": "numeric", "type": "numeric",
"min": 0, "min": 0,
"max": 7, "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 "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"
}
} }
} }
} }
@ -214,7 +608,7 @@
} }
} }
}, },
"last_settled_home":{ "reason_for_leaving_last_settled_home": {
"header": "Leaving their last settled home", "header": "Leaving their last settled home",
"description": "", "description": "",
"questions": { "questions": {
@ -257,8 +651,16 @@
"31": "Other", "31": "Other",
"32": "Do not know", "32": "Do not know",
"33": "Prefer not to say" "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": { "benefit_cap_spare_room_subsidy": {
"header": "Was the reason for leaving because of the benefit cap or removal of the spare room subsidy?", "header": "Was the reason for leaving because of the benefit cap or removal of the spare room subsidy?",
"hint_text": "", "hint_text": "",
@ -990,15 +1392,12 @@
"answer_options": { "answer_options": {
"0": "Yes", "0": "Yes",
"1": "No" "1": "No"
} },
} "conditional_for": {
"property_major_repairs_date": ["Yes"]
} }
}, },
"property_major_repairs_date": { "property_major_repairs_date": {
"header": "",
"description": "",
"questions": {
"property_major_repairs": {
"check_answer_label": "What was the major repairs completion date?", "check_answer_label": "What was the major repairs completion date?",
"header": "What was the major repairs completion date?", "header": "What was the major repairs completion date?",
"hint_text": "For example, 27 3 2007", "hint_text": "For example, 27 3 2007",
@ -1187,7 +1586,7 @@
"step": 1, "step": 1,
"readonly": true "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?", "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?", "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": "", "hint_text": "",
@ -1195,7 +1594,18 @@
"answer_options": { "answer_options": {
"0": "Yes", "0": "Yes",
"1": "No" "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
} }
} }
} }
@ -1604,6 +2014,9 @@
"answer_options": { "answer_options": {
"0": "Yes", "0": "Yes",
"1": "No" "1": "No"
},
"conditional_for": {
"reasonable_preference_reason": ["Yes"]
} }
}, },
"reasonable_preference_reason": { "reasonable_preference_reason": {

2
config/routes.rb

@ -3,7 +3,7 @@ Rails.application.routes.draw do
get "about", to: "about#index" get "about", to: "about#index"
get "/", to: "test#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) form = Form.new(2021, 2022)
resources :case_logs do resources :case_logs do

9
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

5
db/schema.rb

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" 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_gender"
t.string "person_8_economic" t.string "person_8_economic"
t.string "homelessness" 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 "benefit_cap_spare_room_subsidy"
t.string "armed_forces_active" t.string "armed_forces_active"
t.string "armed_forces_injured" t.string "armed_forces_injured"
@ -108,6 +108,7 @@ ActiveRecord::Schema.define(version: 2021_09_24_143031) do
t.string "cbl_letting" t.string "cbl_letting"
t.string "chr_letting" t.string "chr_letting"
t.string "cap_letting" t.string "cap_letting"
t.string "outstanding_rent_or_charges"
end end
end end

92
spec/helpers/check_answers_helper_spec.rb

@ -2,23 +2,105 @@ require "rails_helper"
RSpec.describe CheckAnswersHelper do RSpec.describe CheckAnswersHelper do
let(:case_log) { FactoryBot.create(:case_log) } let(:case_log) { FactoryBot.create(:case_log) }
let(:form) { Form.new(2021, 2022) } let(:case_log_with_met_numeric_condition) do
let(:subsection_pages) { form.pages_for_subsection("income_and_benefits") } 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 describe "Get answered questions total" do
it "returns 0 if no questions are answered" 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 end
it "returns 1 if 1 question gets answered" do it "returns 1 if 1 question gets answered" do
case_log["net_income"] = "123" 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
end end
describe "Get total number of questions" do describe "Get total number of questions" do
it "returns the total number of questions for a subsection" 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 end
end end

21
spec/helpers/numeric_questions_helper_spec.rb

@ -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

51
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
Loading…
Cancel
Save