Browse Source

Merge branch 'main' into CLDC-344/Implement-a-few-validations

pull/39/head
Matthew Phelan 3 years ago
parent
commit
78c3c6d867
  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. 2
      app/javascript/controllers/numeric_question_controller.js
  9. 9
      app/models/form.rb
  10. 2
      app/views/form/_checkbox_question.html.erb
  11. 3
      app/views/form/_date_question.html.erb
  12. 2
      app/views/form/_numeric_question.html.erb
  13. 8
      app/views/form/_radio_question.html.erb
  14. 3
      app/views/form/_text_question.html.erb
  15. 10
      app/views/form/check_answers.html.erb
  16. 2
      app/views/form/page.html.erb
  17. 537
      config/forms/2021_2022.json
  18. 2
      config/routes.rb
  19. 9
      db/migrate/20211005115813_add_conditional_fields_to_case_logs.rb
  20. 5
      db/schema.rb
  21. 2
      spec/factories/case_log.rb
  22. 38
      spec/features/case_log_spec.rb
  23. 92
      spec/helpers/check_answers_helper_spec.rb
  24. 21
      spec/helpers/numeric_questions_helper_spec.rb
  25. 51
      spec/helpers/question_attribute_helper_spec.rb
  26. 2
      spec/helpers/tasklist_helper_spec.rb

4
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)

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

4
app/controllers/case_logs_controller.rb

@ -38,11 +38,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)

56
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)
'<p class="govuk-body govuk-!-margin-bottom-7">You answered all the questions</p>'.html_safe
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>
#{create_next_missing_question_link(case_log['id'], subsection_pages, case_log)}".html_safe
"<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, case_log)}".html_safe
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": "#{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() {
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"
}
})
}
}

2
app/javascript/controllers/numeric_question_controller.js

@ -3,7 +3,7 @@ import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
calculateFields() {
const affectedField = this.element.dataset.target;
const fieldsToAdd = JSON.parse(this.element.dataset.calculated).map(x => `${x.replaceAll("_","-")}-field`);
const fieldsToAdd = JSON.parse(this.element.dataset.calculated).map(x => `case-log-${x.replaceAll("_","-")}-field`);
const valuesToAdd = fieldsToAdd.map(x => document.getElementById(x).value).filter(x => x);
const newValue = valuesToAdd.map(x => parseInt(x)).reduce((a, b) => a + b, 0);
const elementToUpdate = document.getElementById(affectedField);

9
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

2
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 %>

3
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)
%>

2
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)
%>

8
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 %>

3
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)
%>

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

@ -1,11 +1,13 @@
<%= turbo_frame_tag "case_log_form", target: "_top" do %>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds-from-desktop">
<h1 class="govuk-heading-l">Check the answers you gave for <%= subsection %></h1>
<%= display_answered_questions_summary(subsection_pages, case_log) %>
<% subsection_pages.each do |page, page_info| %>
<h1 class="govuk-heading-l">Check the answers you gave for <%= subsection.humanize(capitalize: false) %></h1>
<%= 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| %>

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

@ -16,7 +16,7 @@
<%= f.govuk_error_summary %>
<% page_info["questions"].map do |question_key, question| %>
<div id=<%= question_key + "_div " %><%= display_question_key_div(page_info, question_key) %> >
<%= render partial: "form/#{question["type"]}_question", locals: { question_key: question_key, question: question, f: f } %>
<%= render partial: "form/#{question["type"]}_question", locals: { question_key: question_key.to_sym, question: question, f: f } %>
</div>
<% end %>

537
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?",
@ -37,10 +37,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?",
@ -55,10 +55,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?",
@ -88,10 +88,10 @@
}
}
},
"tenant_nationality":{
"tenant_nationality": {
"header": "",
"description": "",
"questions":{
"questions": {
"tenant_nationality": {
"check_answer_label": "Nationality",
"header": "What is the tenant's nationality?",
@ -118,10 +118,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?",
@ -143,10 +143,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?",
@ -154,7 +154,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"
}
}
}
}
@ -163,10 +557,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": "",
@ -199,10 +593,10 @@
}
}
},
"homelessness":{
"homelessness": {
"header": "",
"description": "",
"questions":{
"questions": {
"homelessness": {
"header": "Did the tenant experience homelessness immediately before this letting?",
"hint_text": "",
@ -215,10 +609,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": "",
@ -258,8 +652,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": "",
@ -280,10 +682,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": "",
@ -340,7 +742,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:<ul><li>Physical Condition</li><li>Mental Health Condition</li><li>Other Illness</li></ul>",
"hint_text": "",
@ -358,7 +760,7 @@
"pregnancy": {
"header": "",
"description": "",
"questions":{
"questions": {
"pregnancy": {
"header": "Is anyone in the household pregnant?",
"hint_text": "",
@ -375,7 +777,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",
@ -398,7 +800,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",
@ -431,10 +833,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?",
@ -471,10 +873,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?",
@ -547,10 +949,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",
@ -874,10 +1276,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?",
@ -933,7 +1335,7 @@
"property_reference": {
"header": "",
"description": "",
"questions":{
"questions": {
"property_reference": {
"check_answer_label": "What’s the property reference?",
"header": "What's the property reference?",
@ -967,7 +1369,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?",
@ -991,15 +1393,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",
@ -1010,7 +1409,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? ",
@ -1048,10 +1447,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?",
@ -1073,10 +1472,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?",
@ -1091,10 +1490,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?",
@ -1117,10 +1516,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?",
@ -1188,7 +1587,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": "",
@ -1196,7 +1595,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
}
}
}
@ -1210,10 +1620,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?",
@ -1232,10 +1642,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?",
@ -1254,10 +1664,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?",
@ -1605,6 +2015,9 @@
"answer_options": {
"0": "Yes",
"1": "No"
},
"conditional_for": {
"reasonable_preference_reason": ["Yes"]
}
},
"reasonable_preference_reason": {
@ -1669,10 +2082,10 @@
"declaration": {
"label": "Declaration",
"pages": {
"declaration":{
"declaration": {
"header": "",
"description": "",
"questions":{
"questions": {
"declaration": {
"check_answer_label": "",
"header": "What is the tenant code?",

2
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

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.
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_economic"
t.string "postcode"
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

2
spec/factories/case_log.rb

@ -5,6 +5,8 @@ FactoryBot.define do
status { 0 }
tenant_code { "TH356" }
postcode { "SW2 6HI" }
previous_postcode { "P0 5ST" }
tenant_age { "12" }
end
trait :submitted do
status { 1 }

38
spec/features/case_log_spec.rb

@ -15,6 +15,12 @@ RSpec.describe "Test Features" do
household_number_of_other_members: { type: "numeric", answer: 2 },
}
def fill_in_number_question(case_log_id, question, value)
visit("/case_logs/#{case_log_id}/#{question}")
fill_in("case-log-#{question.to_s.dasherize}-field", with: value)
click_button("Save and continue")
end
def answer_all_questions_in_income_subsection
visit("/case_logs/#{empty_case_log.id}/net_income")
fill_in("case-log-net-income-field", with: 18_000)
@ -118,17 +124,27 @@ RSpec.describe "Test Features" do
it "updates total value of the rent", js: true do
visit("/case_logs/#{id}/rent")
fill_in("basic_rent", with: 3)
expect(page).to have_field("total-charge-field", with: "3")
fill_in("case-log-basic-rent-field", with: 3)
expect(page).to have_field("case-log-total-charge-field", with: "3")
fill_in("service_charge", with: 2)
expect(page).to have_field("total-charge-field", with: "5")
fill_in("case-log-service-charge-field", with: 2)
expect(page).to have_field("case-log-total-charge-field", with: "5")
fill_in("personal_service_charge", with: 1)
expect(page).to have_field("total-charge-field", with: "6")
fill_in("case-log-personal-service-charge-field", with: 1)
expect(page).to have_field("case-log-total-charge-field", with: "6")
fill_in("support_charge", with: 4)
expect(page).to have_field("total-charge-field", with: "10")
fill_in("case-log-support-charge-field", with: 4)
expect(page).to have_field("case-log-total-charge-field", with: "10")
end
it "displays number answers in inputs if they are already saved" do
visit("/case_logs/#{id}/previous_postcode")
expect(page).to have_field("case-log-previous-postcode-field", with: "P0 5ST")
end
it "displays text answers in inputs if they are already saved" do
visit("/case_logs/#{id}/tenant_age")
expect(page).to have_field("case-log-tenant-age-field", with: "12")
end
end
@ -164,12 +180,6 @@ RSpec.describe "Test Features" do
let(:subsection) { "household_characteristics" }
context "when the user needs to check their answers for a subsection" do
def fill_in_number_question(case_log_id, question, value)
visit("/case_logs/#{case_log_id}/#{question}")
fill_in("case-log-#{question.to_s.dasherize}-field", with: value)
click_button("Save and continue")
end
it "can be visited by URL" do
visit("case_logs/#{id}/#{subsection}/check_answers")
expect(page).to have_content("Check the answers you gave for #{subsection.tr('_', ' ')}")

92
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

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": "#{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

2
spec/helpers/tasklist_helper_spec.rb

@ -73,7 +73,7 @@ RSpec.describe TasklistHelper do
it "returns the number of sections in progress" do
@form = Form.new(2021, 2022)
expect(get_sections_count(@form, case_log, :in_progress)).to eq(1)
expect(get_sections_count(@form, case_log, :in_progress)).to eq(2)
end
it "returns 0 for invalid state" do

Loading…
Cancel
Save