Browse Source

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

pull/39/head
Matthew Phelan 3 years ago
parent
commit
f6ee29c293
  1. 1
      .github/workflows/pipeline.yml
  2. 4
      .gitignore
  3. 2
      Gemfile
  4. 7
      Gemfile.lock
  5. 5
      README.md
  6. 21
      app/controllers/case_logs_controller.rb
  7. 39
      app/helpers/check_answers_helper.rb
  8. 15
      app/helpers/conditional_questions_helper.rb
  9. 49
      app/helpers/tasklist_helper.rb
  10. 10
      app/javascript/controllers/application.js
  11. 27
      app/javascript/controllers/conditional_question_controller.js
  12. 18
      app/javascript/controllers/hello_controller.js
  13. 6
      app/javascript/controllers/index.js
  14. 8
      app/javascript/controllers/tasklist_controller.js
  15. 6
      app/javascript/styles/_task-list.scss
  16. 6
      app/models/form.rb
  17. 9
      app/views/case_logs/_tasklist.html.erb
  18. 12
      app/views/case_logs/edit.html.erb
  19. 13
      app/views/form/_check_answers_table.html.erb
  20. 7
      app/views/form/_checkbox_question.html.erb
  21. 2
      app/views/form/_date_question.html.erb
  22. 2
      app/views/form/_numeric_question.html.erb
  23. 10
      app/views/form/_radio_question.html.erb
  24. 2
      app/views/form/_text_question.html.erb
  25. 16
      app/views/form/check_answers.html.erb
  26. 4
      app/views/form/page.html.erb
  27. 52
      config/forms/2021_2022.json
  28. 6
      config/routes.rb
  29. 0
      doc/adr/adr-001-initial-architecture-decisions.md
  30. 0
      doc/adr/adr-002-repositories.md
  31. 0
      doc/adr/adr-003-form-submission-flow.md
  32. 0
      doc/adr/adr-004-gov-paas.md
  33. 67
      doc/adr/adr-005-form-definition.md
  34. 23
      doc/adr/adr-006-saving-values.md
  35. 6
      package.json
  36. 164
      spec/features/case_log_spec.rb
  37. 24
      spec/helpers/check_answers_helper_spec.rb
  38. 28
      spec/helpers/conditional_questions_helper_spec.rb
  39. 84
      spec/helpers/tasklist_helper_spec.rb
  40. 9
      spec/models/form_spec.rb
  41. 8
      spec/rails_helper.rb
  42. 2785
      yarn.lock

1
.github/workflows/pipeline.yml

@ -5,6 +5,7 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
workflow_dispatch:
concurrency: 'sandbox' concurrency: 'sandbox'

4
.gitignore vendored

@ -39,3 +39,7 @@ yarn-debug.log*
# Code coverage results # Code coverage results
/coverage /coverage
#IDE specific files
/.idea
/.idea/*

2
Gemfile

@ -29,7 +29,7 @@ group :development, :test do
gem "dotenv-rails" gem "dotenv-rails"
gem "factory_bot_rails" gem "factory_bot_rails"
gem "pry-byebug" gem "pry-byebug"
# gem "selenium-webdriver", require: false gem "selenium-webdriver", require: false
gem "simplecov", require: false gem "simplecov", require: false
%w[rspec-core rspec-expectations rspec-mocks rspec-rails rspec-support].each do |lib| %w[rspec-core rspec-expectations rspec-mocks rspec-rails rspec-support].each do |lib|
gem lib, git: "https://github.com/rspec/#{lib}.git", branch: "main", require: false gem lib, git: "https://github.com/rspec/#{lib}.git", branch: "main", require: false

7
Gemfile.lock

@ -147,7 +147,7 @@ GEM
activemodel (>= 6.0) activemodel (>= 6.0)
railties (>= 6.0) railties (>= 6.0)
view_component (~> 2.39.0) view_component (~> 2.39.0)
govuk_design_system_formbuilder (2.7.4) govuk_design_system_formbuilder (2.7.5)
actionview (>= 6.0) actionview (>= 6.0)
activemodel (>= 6.0) activemodel (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
@ -267,6 +267,7 @@ GEM
rubocop (~> 1.0) rubocop (~> 1.0)
rubocop-ast (>= 1.1.0) rubocop-ast (>= 1.1.0)
ruby-progressbar (1.11.0) ruby-progressbar (1.11.0)
rubyzip (2.3.2)
sass (3.7.4) sass (3.7.4)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
@ -276,6 +277,9 @@ GEM
sass (~> 3.5, >= 3.5.5) sass (~> 3.5, >= 3.5.5)
scss_lint-govuk (0.2.0) scss_lint-govuk (0.2.0)
scss_lint scss_lint
selenium-webdriver (3.142.7)
childprocess (>= 0.5, < 4.0)
rubyzip (>= 1.2.2)
semantic_range (3.0.0) semantic_range (3.0.0)
simplecov (0.21.2) simplecov (0.21.2)
docile (~> 1.1) docile (~> 1.1)
@ -349,6 +353,7 @@ DEPENDENCIES
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
scss_lint-govuk scss_lint-govuk
selenium-webdriver
simplecov simplecov
tzinfo-data tzinfo-data
web-console (>= 4.1.0) web-console (>= 4.1.0)

5
README.md

@ -1,3 +1,5 @@
[![CI/CD Pipeline](https://github.com/communitiesuk/mhclg-data-collection-beta/actions/workflows/pipeline.yml/badge.svg?branch=main&event=push)](https://github.com/communitiesuk/mhclg-data-collection-beta/actions/workflows/pipeline.yml)
# Data Collection App # Data Collection App
This is the codebase for the Ruby/Rails app that will handle the submission of Lettings and Sales of Social Housing in England data. This is the codebase for the Ruby/Rails app that will handle the submission of Lettings and Sales of Social Housing in England data.
@ -63,6 +65,9 @@ Once the app is deployed:
- Get a rails console <br/> - Get a rails console <br/>
`cf ssh dluhc-core -t -c "/tmp/lifecycle/launcher /home/vcap/app 'rails console' ''"` `cf ssh dluhc-core -t -c "/tmp/lifecycle/launcher /home/vcap/app 'rails console' ''"`
- Check logs <br />
`cf logs dluhc-core --recent`
### CI/CD ### CI/CD

21
app/controllers/case_logs_controller.rb

@ -1,5 +1,4 @@
class CaseLogsController < ApplicationController class CaseLogsController < ApplicationController
def index def index
@submitted_case_logs = CaseLog.where(status: 1) @submitted_case_logs = CaseLog.where(status: 1)
@in_progress_case_logs = CaseLog.where(status: 0) @in_progress_case_logs = CaseLog.where(status: 0)
@ -33,13 +32,29 @@ class CaseLogsController < ApplicationController
if @case_log_temp.valid? if @case_log_temp.valid?
@case_log.update!(answers_for_page) @case_log.update!(answers_for_page)
next_page = form.next_page(previous_page) next_page = form.next_page(previous_page)
redirect_to(send("case_log_#{next_page}_path", @case_log)) redirect_path = if next_page == :check_answers
subsection = form.subsection_for_page(previous_page)
"case_log_#{subsection}_check_answers_path"
else
"case_log_#{next_page}_path"
end
redirect_to(send(redirect_path, @case_log))
else else
@errors = @case_log_temp.errors.full_messages @errors = @case_log_temp.errors.full_messages
redirect_to(send("case_log_#{previous_page}_path", @case_log, errors: @errors)) redirect_to(send("case_log_#{previous_page}_path", @case_log, errors: @errors))
end end
end end
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) }
end
form = Form.new(2021, 2022) form = Form.new(2021, 2022)
form.all_pages.map do |page_key, page_info| form.all_pages.map do |page_key, page_info|
define_method(page_key) do |errors = {}| define_method(page_key) do |errors = {}|
@ -48,7 +63,7 @@ class CaseLogsController < ApplicationController
end end
end end
private private
def page_params(questions_for_page) def page_params(questions_for_page)
params.permit(questions_for_page) params.permit(questions_for_page)

39
app/helpers/check_answers_helper.rb

@ -0,0 +1,39 @@
module CheckAnswersHelper
def get_answered_questions_total(subsection_pages, case_log)
questions = subsection_pages.values.flat_map do |page|
page["questions"].keys
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
end
questions.count
end
def create_update_answer_link(case_log_answer, case_log_id, page)
link_name = case_log_answer.blank? ? "Answer" : "Change"
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)
pages_to_fill_in = []
subsection_pages.each do |page_title, page_info|
page_info["questions"].any? { |question| case_log[question].blank? }
pages_to_fill_in << page_title
end
url = "/case_logs/#{case_log_id}/#{pages_to_fill_in.first}"
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)
'<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
end
end
end

15
app/helpers/conditional_questions_helper.rb

@ -0,0 +1,15 @@
module ConditionalQuestionsHelper
def conditional_questions_for_page(page)
page["questions"].values.map { |question|
question["conditional_for"]
}.compact.map(&:keys).flatten
end
def display_question_key_div(page_info, question_key)
if conditional_questions_for_page(page_info).include?(question_key)
"<div id=#{question_key}_div style='display:none;'>".html_safe
else
"<div id=#{question_key}_div>".html_safe
end
end
end

49
app/helpers/tasklist_helper.rb

@ -0,0 +1,49 @@
module TasklistHelper
STATUSES = {
not_started: "Not started",
cannot_start_yet: "Cannot start yet",
completed: "Completed",
in_progress: "In progress",
}.freeze
STYLES = {
not_started: "govuk-tag--grey",
cannot_start_yet: "govuk-tag--grey",
completed: "",
in_progress: "govuk-tag--blue",
}.freeze
def get_subsection_status(subsection_name, case_log, questions)
if subsection_name == "declaration"
return all_questions_completed(case_log) ? :not_started : :cannot_start_yet
end
return :not_started if questions.all? { |question| case_log[question].blank? }
return :completed if questions.all? { |question| case_log[question].present? }
:in_progress
end
def get_next_incomplete_section(form, case_log)
subsections = form.all_subsections.keys
subsections.find { |subsection| is_incomplete?(subsection, case_log, form.questions_for_subsection(subsection).keys) }
end
def get_sections_count(form, case_log, status = :all)
subsections = form.all_subsections.keys
return subsections.count if status == :all
subsections.count { |subsection| get_subsection_status(subsection, case_log, form.questions_for_subsection(subsection).keys) == status }
end
private
def all_questions_completed(case_log)
case_log.attributes.all? { |_question, answer| answer.present? }
end
def is_incomplete?(subsection, case_log, questions)
status = get_subsection_status(subsection, case_log, questions)
%i[not_started in_progress].include?(status)
end
end

10
app/javascript/controllers/application.js

@ -0,0 +1,10 @@
import { Application } from "@hotwired/stimulus"
const application = Application.start()
// Configure Stimulus development experience
application.warnings = true
application.debug = false
window.Stimulus = application
export { application }

27
app/javascript/controllers/conditional_question_controller.js

@ -0,0 +1,27 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
initialize() {
this.displayConditional()
}
displayConditional() {
if(this.element.checked) {
let selected = 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)) {
div.style.display = "block"
} else {
div.style.display = "none"
let buttons = document.getElementsByName(key)
Object.entries(buttons).forEach(([idx, button]) => {
button.checked = false;
})
}
})
}
}
}

18
app/javascript/controllers/hello_controller.js

@ -1,18 +0,0 @@
// Visit The Stimulus Handbook for more details
// https://stimulusjs.org/handbook/introduction
//
// This example controller works with specially annotated HTML like:
//
// <div data-controller="hello">
// <h1 data-target="hello.output"></h1>
// </div>
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "output" ]
connect() {
this.outputTarget.textContent = 'Hello, Stimulus!'
}
}

6
app/javascript/controllers/index.js

@ -1,8 +1,8 @@
// Load all the controllers within this directory and all subdirectories. // Load all the controllers within this directory and all subdirectories.
// Controller files must be named *_controller.js. // Controller files must be named *_controller.js.
import { Application } from "stimulus" import { Application } from "@hotwired/stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers" import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers"
const application = Application.start() const application = Application.start()
const context = require.context("controllers", true, /_controller\.js$/) const context = require.context("controllers", true, /_controller\.js$/)

8
app/javascript/controllers/tasklist_controller.js

@ -0,0 +1,8 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
addHighlight() {
let section_to_highlight = this.element.dataset.info;
document.getElementById(section_to_highlight).classList.add('tasklist_item_highlight');
}
}

6
app/javascript/styles/_task-list.scss

@ -85,3 +85,9 @@
// display: block; // display: block;
// border: 1px solid blue // border: 1px solid blue
// } // }
.app-task-list__item:target,
.tasklist_item_highlight{
background-color: $govuk-focus-colour;
box-shadow: 0 -2px $govuk-focus-colour, 0 4px $govuk-focus-text-colour;
}

6
app/models/form.rb

@ -50,7 +50,7 @@ class Form
def next_page(previous_page) def next_page(previous_page)
subsection = subsection_for_page(previous_page) subsection = subsection_for_page(previous_page)
previous_page_idx = pages_for_subsection(subsection).keys.index(previous_page) previous_page_idx = pages_for_subsection(subsection).keys.index(previous_page)
pages_for_subsection(subsection).keys[previous_page_idx + 1] || previous_page # Placeholder until we have a check answers page pages_for_subsection(subsection).keys[previous_page_idx + 1] || :check_answers
end end
def previous_page(current_page) def previous_page(current_page)
@ -60,4 +60,8 @@ 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

9
app/views/case_logs/_tasklist.html.erb

@ -8,11 +8,12 @@
</h2> </h2>
<ul class="app-task-list__items"> <ul class="app-task-list__items">
<% section_value["subsections"].map do |subsection_key, subsection_value| %> <% section_value["subsections"].map do |subsection_key, subsection_value| %>
<li class="app-task-list__item"> <li class="app-task-list__item" id=<%= subsection_key %>>
<% first_page = @form.first_page_for_subsection(subsection_key) %> <% first_page = @form.first_page_for_subsection(subsection_key) %>
<%= link_to subsection_value["label"], send("case_log_#{first_page}_path", @case_log), class: "task-name" %> <%= link_to subsection_value["label"], send("case_log_#{first_page}_path", @case_log), class: "task-name govuk-link" %>
<strong class="govuk-tag govuk-tag--grey app-task-list__tag"> <% subsection_status=get_subsection_status(subsection_key, @case_log, @form.questions_for_subsection(subsection_key).keys) %>
Not started <strong class="govuk-tag app-task-list__tag <%= TasklistHelper::STYLES[subsection_status] %>">
<%= TasklistHelper::STATUSES[subsection_status] %>
</strong> </strong>
</li> </li>
<% end %> <% end %>

12
app/views/case_logs/edit.html.erb

@ -6,13 +6,17 @@
<h2 class="govuk-heading-s govuk-!-margin-bottom-2">This submission is <h2 class="govuk-heading-s govuk-!-margin-bottom-2">This submission is
<%= @case_log.status %></h2> <%= @case_log.status %></h2>
<p class="govuk-body govuk-!-margin-bottom-7">You've completed 0 of 9 sections.</p> <p class="govuk-body govuk-!-margin-bottom-7">You've completed <%= get_sections_count(@form, @case_log, :completed) %> of <%= get_sections_count(@form, @case_log, :all) %> sections.</p>
<p class="govuk-body govuk-!-margin-bottom-7"> <p class="govuk-body govuk-!-margin-bottom-7">
<a href="#">Skip to next incomplete section</a> <% next_incomplete_section=get_next_incomplete_section(@form, @case_log) %>
<a href="#<%= next_incomplete_section %>"
data-controller="tasklist"
data-action="tasklist#addHighlight"
data-info=<%= next_incomplete_section %>>
Skip to next incomplete section
</a>
</p> </p>
<%= render "tasklist", locals: { form: @form } %> <%= render "tasklist", locals: { form: @form } %>
</div> </div>
</div> </div>
<% end %> <% end %>

13
app/views/form/_check_answers_table.html.erb

@ -0,0 +1,13 @@
<dl class="govuk-summary-list govuk-!-margin-bottom-9">
<div class="govuk-summary-list__row">
<dt class="govuk-summary-list__key">
<%= question_info["check_answer_label"].to_s %>
<dt>
<dd class="govuk-summary-list__value">
<%= case_log[question_title] %>
</dd>
<dd class="govuk-summary-list__actions">
<%= create_update_answer_link(case_log[question_title], case_log["id"], page)%>
</dd>
</div>
</dl>

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

@ -1,15 +1,12 @@
<%= f.govuk_check_boxes_fieldset question_key, <%= f.govuk_check_boxes_fieldset question_key,
legend: { text: question["header"], size: "l" }, legend: { text: question["header"].html_safe, size: "l" },
hint: { text: question["hint_text"] } do %> hint: { text: question["hint_text"] } do %>
<% 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_check_box_divider %> <%= f.govuk_check_box_divider %>
<% else %> <% else %>
<%= f.govuk_check_box question_key, key, label: { text: val } %> <%= f.govuk_check_box question_key, val, label: { text: val } %>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>
<%= f.hidden_field :previous_page, value: page_key %>
<%= f.hidden_field :case_log_id, value: case_log.id %>

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

@ -1,5 +1,5 @@
<%= 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"], size: "l"}, legend: { text: question["header"].html_safe, size: "l"},
width: 20 width: 20
%> %>

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

@ -1,5 +1,5 @@
<%= f.govuk_number_field question_key, <%= f.govuk_number_field question_key,
hint: { text: question["hint_text"] }, hint: { text: question["hint_text"] },
label: { text: question["header"], size: "l"}, label: { text: question["header"].html_safe, size: "l"},
min: question["min"], max: question["max"], step: question["step"], width: 20 min: question["min"], max: question["max"], step: question["step"], width: 20
%> %>

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

@ -1,13 +1,19 @@
<%= f.govuk_radio_buttons_fieldset question_key, <%= f.govuk_radio_buttons_fieldset question_key,
legend: { text: question["header"], size: "l" }, legend: { text: question["header"].html_safe, size: "l" },
hint: { text: question["hint_text"] }, hint: { text: question["hint_text"] },
small: (question["answer_options"].size > 5) do %> small: (question["answer_options"].size > 5) do %>
<% 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, key, label: { text: val } %> <%= f.govuk_radio_button question_key, val, label: { text: val } %>
<% end %> <% end %>
<% end %> <% end %>
<% end %> <% end %>

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

@ -1,5 +1,5 @@
<%= 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"], size: "l"}, label: { text: question["header"].html_safe, size: "l"},
width: 20 width: 20
%> %>

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

@ -0,0 +1,16 @@
<%= 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| %>
<% 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 } %>
<%end %>
<% end %>
<%= form_with action: '/case_logs', method: "next_page", builder: GOVUKDesignSystemFormBuilder::FormBuilder do |f| %>
<%= f.govuk_submit "Save and continue" %>
<% end %>
</div>
</div>
<% end %>

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

@ -19,7 +19,9 @@
<% end %> <% end %>
<% page_info["questions"].map do |question_key, question| %> <% page_info["questions"].map do |question_key, question| %>
<%= render partial: "form/#{question["type"]}_question", locals: { question_key: question_key, question: question, f: f } %> <%= display_question_key_div(page_info, question_key)%>
<%= render partial: "form/#{question["type"]}_question", locals: { question_key: question_key, question: question, f: f } %>
</div>
<% end %> <% end %>
<%= f.hidden_field :previous_page, value: page_key %> <%= f.hidden_field :previous_page, value: page_key %>

52
config/forms/2021_2022.json

@ -14,6 +14,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"tenant_code": { "tenant_code": {
"check_answer_label": "Tenant code",
"header": "What is the tenant code?", "header": "What is the tenant code?",
"hint_text": "", "hint_text": "",
"type": "text" "type": "text"
@ -25,6 +26,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"tenant_age": { "tenant_age": {
"check_answer_label": "Tenant's age",
"header": "What is the tenant's age?", "header": "What is the tenant's age?",
"hint_text": "", "hint_text": "",
"type": "numeric", "type": "numeric",
@ -39,6 +41,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"tenant_gender": { "tenant_gender": {
"check_answer_label": "Tenant's gender",
"header": "Which of these best describes the tenant's gender identity?", "header": "Which of these best describes the tenant's gender identity?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -56,6 +59,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"tenant_ethnic_group": { "tenant_ethnic_group": {
"check_answer_label": "Ethnicity",
"header": "What is the tenant's ethnic group?", "header": "What is the tenant's ethnic group?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -88,6 +92,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"tenant_nationality": { "tenant_nationality": {
"check_answer_label": "Nationality",
"header": "What is the tenant's nationality?", "header": "What is the tenant's nationality?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -117,6 +122,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"tenant_economic_status": { "tenant_economic_status": {
"check_answer_label": "Work",
"header": "Which of these best describes the tenant's working situation?", "header": "Which of these best describes the tenant's working situation?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -141,6 +147,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"household_number_of_other_members": { "household_number_of_other_members": {
"check_answer_label": "Number of Other Household Members",
"header": "How many other people are there in the household?", "header": "How many other people are there in the household?",
"hint_text": "The maximum number of others is 7", "hint_text": "The maximum number of others is 7",
"type": "numeric", "type": "numeric",
@ -285,6 +292,10 @@
"1": "Yes - a reserve", "1": "Yes - a reserve",
"2": "No", "2": "No",
"3": "Prefer not to say" "3": "Prefer not to say"
},
"conditional_for": {
"armed_forces_active": ["Yes - a regular", "Yes - a reserve"],
"armed_forces_injured": ["Yes - a regular", "Yes - a reserve"]
} }
}, },
"armed_forces_active": { "armed_forces_active": {
@ -326,7 +337,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"medical_conditions": { "medical_conditions": {
"header": "Does anyone in the household have a physical condition, mental health condition, or other illness that they expect to last for 12 months or more?", "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": "", "hint_text": "",
"type": "radio", "type": "radio",
"answer_options": { "answer_options": {
@ -416,6 +427,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"tenancy_code": { "tenancy_code": {
"check_answer_label": "What is the tenancy code?",
"header": "What is the tenancy code?", "header": "What is the tenancy code?",
"hint_text": "", "hint_text": "",
"type": "text" "type": "text"
@ -427,6 +439,7 @@
"description": "", "description": "",
"questions": { "questions": {
"tenancy_start_date": { "tenancy_start_date": {
"check_answer_label": "When is the tenancy start date?",
"header": "What is the tenancy start date?", "header": "What is the tenancy start date?",
"hint_text": "For example, 27 3 2007", "hint_text": "For example, 27 3 2007",
"type": "date" "type": "date"
@ -438,6 +451,7 @@
"description": "", "description": "",
"questions": { "questions": {
"starter_tenancy": { "starter_tenancy": {
"check_answer_label": "Is this a starter or introductory tenancy?",
"header": "Is this a starter tenancy?", "header": "Is this a starter tenancy?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -453,6 +467,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"fixed_term_tenancy": { "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?", "header": "If fixed-term, what is the length of the fixed-term tenancy after any starter period?",
"hint_text": "To the nearest year", "hint_text": "To the nearest year",
"type": "numeric", "type": "numeric",
@ -467,6 +482,7 @@
"description": "", "description": "",
"questions": { "questions": {
"tenancy_type": { "tenancy_type": {
"check_answer_label": "Type of main tenancy (after any starter/introductory period)",
"header": "What is the type of tenancy after the starter period has ended?", "header": "What is the type of tenancy after the starter period has ended?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -486,6 +502,7 @@
"description": "", "description": "",
"questions": { "questions": {
"letting_type": { "letting_type": {
"check_answer_label": "Type of letting",
"header": "Which type of letting is this?", "header": "Which type of letting is this?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -505,6 +522,7 @@
"description": "", "description": "",
"questions": { "questions": {
"letting_provider": { "letting_provider": {
"check_answer_label": "Provider",
"header": "Who is the letting provider?", "header": "Who is the letting provider?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -525,6 +543,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"property_location": { "property_location": {
"check_answer_label": "Property Location",
"header": "Property location", "header": "Property location",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -851,6 +870,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"property_postcode": { "property_postcode": {
"check_answer_label": "What was the previous postcode?",
"header": "What is the property's postcode?", "header": "What is the property's postcode?",
"hint_text": "", "hint_text": "",
"type": "text" "type": "text"
@ -862,6 +882,7 @@
"description": "", "description": "",
"questions": { "questions": {
"property_relet": { "property_relet": {
"check_answer_label": "Which type was the property most recently let as?",
"header": "Is this property a relet?", "header": "Is this property a relet?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -877,6 +898,7 @@
"description": "", "description": "",
"questions": { "questions": {
"property_vacancy_reason": { "property_vacancy_reason": {
"check_answer_label": "What is the reason for the property vacancy?",
"header": "What is the reason for the property vacancy?", "header": "What is the reason for the property vacancy?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -904,6 +926,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"property_reference": { "property_reference": {
"check_answer_label": "What’s the property reference?",
"header": "What's the property reference?", "header": "What's the property reference?",
"hint_text": "", "hint_text": "",
"type": "text" "type": "text"
@ -915,6 +938,7 @@
"description": "", "description": "",
"questions": { "questions": {
"property_unit_type": { "property_unit_type": {
"check_answer_label": "Which type of unit is the property?",
"header": "Which type of unit is the property?", "header": "Which type of unit is the property?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -936,6 +960,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"property_number_of_bedrooms": { "property_number_of_bedrooms": {
"check_answer_label": "How many bedrooms are there in the property?",
"header": "How many bedrooms are there in the property?", "header": "How many bedrooms are there in the property?",
"hint_text": "If shared accommodation, enter number of bedrooms occupied by this household; a bed-sit has 1 bedroom", "hint_text": "If shared accommodation, enter number of bedrooms occupied by this household; a bed-sit has 1 bedroom",
"type": "numeric", "type": "numeric",
@ -950,6 +975,7 @@
"description": "", "description": "",
"questions": { "questions": {
"property_major_repairs": { "property_major_repairs": {
"check_answer_label": "Were major repairs carried out during the void period?",
"header": "Were any major repairs completed during the void period?", "header": "Were any major repairs completed during the void period?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -965,6 +991,7 @@
"description": "", "description": "",
"questions": { "questions": {
"property_major_repairs": { "property_major_repairs": {
"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",
"type": "date" "type": "date"
@ -976,6 +1003,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"property_number_of_times_relet": { "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? ", "header": "How many times has this unit been previously offered since becoming available for relet since the last tenancy ended or as a first let? ",
"hint_text": "For an Affordable Rent or Intermediate Rent Letting, only include number of offers as that type. For a property let at the first attempt enter '0' ", "hint_text": "For an Affordable Rent or Intermediate Rent Letting, only include number of offers as that type. For a property let at the first attempt enter '0' ",
"type": "numeric", "type": "numeric",
@ -990,6 +1018,7 @@
"description": "", "description": "",
"questions": { "questions": {
"property_wheelchair_accessible": { "property_wheelchair_accessible": {
"check_answer_label": "Is property built or adapted to wheelchair user standards?",
"header": "Is property built or adapted to wheelchair user standards?", "header": "Is property built or adapted to wheelchair user standards?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1015,6 +1044,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"net_income": { "net_income": {
"check_answer_label": "Income",
"header": "What is the tenant’s /and partner’s combined income after tax?", "header": "What is the tenant’s /and partner’s combined income after tax?",
"hint_text": "", "hint_text": "",
"type": "numeric", "type": "numeric",
@ -1022,6 +1052,7 @@
"step": "1" "step": "1"
}, },
"net_income_frequency": { "net_income_frequency": {
"check_answer_label": "Income Frequency",
"header": "How often do they receive this income?", "header": "How often do they receive this income?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1038,6 +1069,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"net_income_uc_proportion": { "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?", "header": "How much of the tenant’s income is from Universal Credit, state pensions or benefits?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1055,6 +1087,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"housing_benefit": { "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?", "header": "Is the tenant likely to be in receipt of any of these housing-related benefits?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1080,6 +1113,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"rent_frequency": { "rent_frequency": {
"check_answer_label": "Rent Period",
"header": "Which period are rent and other charges due?", "header": "Which period are rent and other charges due?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1097,6 +1131,7 @@
} }
}, },
"basic_rent": { "basic_rent": {
"check_answer_label": "Basic Rent",
"header": "What is the basic rent?", "header": "What is the basic rent?",
"hint_text": "Eligible for housing benefit or Universal Credit", "hint_text": "Eligible for housing benefit or Universal Credit",
"type": "numeric", "type": "numeric",
@ -1104,6 +1139,7 @@
"step": 1 "step": 1
}, },
"service_charge": { "service_charge": {
"check_answer_label": "Service Charge",
"header": "What is the service charge?", "header": "What is the service charge?",
"hint_text": "Eligible for housing benefit or Universal Credit", "hint_text": "Eligible for housing benefit or Universal Credit",
"type": "numeric", "type": "numeric",
@ -1111,6 +1147,7 @@
"step": 1 "step": 1
}, },
"personal_service_charge": { "personal_service_charge": {
"check_answer_label": "Personal Service Charge",
"header": "What is the personal service charge?", "header": "What is the personal service charge?",
"hint_text": "Not eligible for housing benefit or Universal Credit. For example, hot water excluding water rates.", "hint_text": "Not eligible for housing benefit or Universal Credit. For example, hot water excluding water rates.",
"type": "numeric", "type": "numeric",
@ -1118,6 +1155,7 @@
"step": 1 "step": 1
}, },
"support_charge": { "support_charge": {
"check_answer_label": "Support Charge",
"header": "What is the support charge?", "header": "What is the support charge?",
"hint_text": "This is to fund housing-related support services included in the tenancy agreement", "hint_text": "This is to fund housing-related support services included in the tenancy agreement",
"type": "numeric", "type": "numeric",
@ -1125,6 +1163,7 @@
"step": 1 "step": 1
}, },
"total_charge": { "total_charge": {
"check_answer_label": "Total Charge",
"header": "Total charge?", "header": "Total charge?",
"hint_text": "This is the total of rent and all charges", "hint_text": "This is the total of rent and all charges",
"type": "numeric", "type": "numeric",
@ -1132,6 +1171,7 @@
"step": 1 "step": 1
}, },
"outstanding_amount": { "outstanding_amount": {
"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": "",
"type": "radio", "type": "radio",
@ -1157,6 +1197,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"time_lived_in_la": { "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?", "header": "How long has the household continuously lived in the local authority area where the new letting is located?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1178,6 +1219,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"time_on_la_waiting_list": { "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?", "header": "How long has the household been on the local authority waiting list where the new letting is located?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1199,6 +1241,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"previous_la": { "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?", "header": "Which local authority area did the household live in immediately before this letting?",
"hint_text": "Includes temporary accommodation", "hint_text": "Includes temporary accommodation",
"type": "radio", "type": "radio",
@ -1525,6 +1568,7 @@
"description": "", "description": "",
"questions": { "questions": {
"previous_postcode": { "previous_postcode": {
"check_answer_label": "Postcode of previous accomodation if the household has moved from settled accommodation",
"header": "Postcode for the previous accommodation", "header": "Postcode for the previous accommodation",
"hint_text": "If the household has moved from settled accommodation immediately prior to being re-housed", "hint_text": "If the household has moved from settled accommodation immediately prior to being re-housed",
"type": "text" "type": "text"
@ -1536,6 +1580,7 @@
"description": "", "description": "",
"questions": { "questions": {
"reasonable_preference": { "reasonable_preference": {
"check_answer_label": "Was the household given Reasonable Preference (i.e. priority) for housing by the Local Authority?",
"header": "Was the household given reasonable preference by the local authority?", "header": "Was the household given reasonable preference by the local authority?",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1545,6 +1590,7 @@
} }
}, },
"reasonable_preference_reason": { "reasonable_preference_reason": {
"check_answer_label": "Reason for reasonable preference",
"header": "Why were they given reasonable preference?", "header": "Why were they given reasonable preference?",
"hint_text": "Select all that apply", "hint_text": "Select all that apply",
"type": "checkbox", "type": "checkbox",
@ -1564,6 +1610,7 @@
"description": "", "description": "",
"questions": { "questions": {
"cbl_letting": { "cbl_letting": {
"check_answer_label": "Choice-based letting?",
"header": "Was the letting made under choice-based lettings (CBL)? ", "header": "Was the letting made under choice-based lettings (CBL)? ",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1573,6 +1620,7 @@
} }
}, },
"chr_letting": { "chr_letting": {
"check_answer_label": "Common housing register letting?",
"header": "Was the letting made under common housing register (CHR)? ", "header": "Was the letting made under common housing register (CHR)? ",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1582,6 +1630,7 @@
} }
}, },
"cap_letting": { "cap_letting": {
"check_answer_label": "Common allocation policy letting?",
"header": "Was the letting made under common allocation policy (CAP)? ", "header": "Was the letting made under common allocation policy (CAP)? ",
"hint_text": "", "hint_text": "",
"type": "radio", "type": "radio",
@ -1607,6 +1656,7 @@
"description": "", "description": "",
"questions":{ "questions":{
"declaration": { "declaration": {
"check_answer_label": "",
"header": "What is the tenant code?", "header": "What is the tenant code?",
"hint_text": "", "hint_text": "",
"type": "text" "type": "text"

6
config/routes.rb

@ -3,10 +3,14 @@ Rails.application.routes.draw do
get "about", to: "about#index" get "about", to: "about#index"
get "/", to: "test#index" get "/", to: "test#index"
form = Form.new(2021, 2022)
resources :case_logs do resources :case_logs do
Form.new(2021, 2022).all_pages.keys.map do |page| form.all_pages.keys.map do |page|
get page.to_s, to: "case_logs##{page}" get page.to_s, to: "case_logs##{page}"
post page.to_s, to: "case_logs#next_page" post page.to_s, to: "case_logs#next_page"
form.all_subsections.keys.map do |subsection|
get "#{subsection}/check_answers", to: "case_logs#check_answers"
end
end end
end end
end end

0
doc/adr/adr-001.md → doc/adr/adr-001-initial-architecture-decisions.md

0
doc/adr/adr-002.md → doc/adr/adr-002-repositories.md

0
doc/adr/adr-003.md → doc/adr/adr-003-form-submission-flow.md

0
doc/adr/adr-004.md → doc/adr/adr-004-gov-paas.md

67
doc/adr/adr-005-form-definition.md

@ -0,0 +1,67 @@
### ADR - 005: Form Definition
#### Config driven front-end
We will initially try to model the form as a JSON structure that should describe all the information needed to display the form to the user. That means it will need to describe the sections, subsections, pages, questions, answer options etc.
The idea is to decouple the code that creates the required routes, controller methods, views etc to display the form from the actual wording of questions or order of pages such that it becomes possible to make changes to the form with little or no code changes.
This should also mean that in the future it could be possible to create a UI that can construct the JSON config, which would open up the ability to make form changes to a wider audience. Doing this fully would require generating and running the necessary migrations for data storage, generating the required ActiveRecord methods to validate the data server side, and generating/updating API endpoints and documentation. All of this is likely to be beyond the scope of initial MVP but could be looked at in the future.
Since initially the JSON config will not create database migrations or ActiveRecord model validations, it will instead assume that these have been correctly created for the config provided. The reasoning for this is the following assumptions:
- The form will be tweaked regularly (amending questions wording, changing the order of questions or the page a question is displayed on)
- The actual data collected will change very infrequently. Time series continuity is very important to ADD (Analysis and Data Directorate) so the actual data collected should stay largely consistent i.e. in general we can change the question wording in ways that makes the intent clearer or easier to understand, but not in ways that would make the data provider give a different answer.
A form parser class will parse this config into ruby objects/methods that can be used as an API by the rest of the application, such that we could change the underlying config if needed (for example swap JSON for YAML or for DataBase objects) without needing to change the rest of the application.
#### JSON Structure
First pass of a form definition
```
{
form_type: [lettings/sales]
start_year: yyyy
end_year: yyyy
sections: {
snake case section name string: {
label: string,
subsections: {
snake case subsection name string: {
label: string,
pages: {
snake case page name string: {
header: string,
description: string,
questions: {
snake case question name string: {
header: string,
hint_text: string,
type: [text / numeric / radio / checkbox / date ],
min: integer, (numeric only),
max: integer, (numeric only),
step: integer (numeric only),
answer_options: { (checkbox and radio only)
"0": string,
"1": string
}
}
}
}
}
}
}
}
}
}
```
Assumptions made by the format:
- All forms have at least 1 section
- All sections have at least 1 subsection
- All subsections have at least 1 page
- 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

23
doc/adr/adr-006-saving-values.md

@ -0,0 +1,23 @@
### ADR - 006: Saving values to the database
We have opted to save values to the database directly instead of saving keys/numbers that need to be converted with enums in models using active record.
### Saving values to the database
There are a few reasons we have opted to save the values directly, they are as follows
- The data will be easier to consume and analyse for anyone associated with the project who needs to do so but does not necessarily have the technical skills to access it through Rails i.e. A person could get the data directly from the database and it would require no additional work to be usable for reporting purposes
- Currently there is no need to abstract the data as the data should be safe from being accessed by anyone external to the project
- It doesn't require additional dev work to map keys/numbers to values, we can just pull the values out directly and use them in the code, for example on the check answers page
### Drawbacks
- Changing the wording/casing of the answers could result in discrepancies in the database
- There is a small risk that if the database is accessed by someone unauthorised they would have access to personally identifiable information if we were to collect Any. We will be mitigating this risk by encrypting the production database
This decision is not too difficult to change and can be revisited in the future if there is sufficient reason to switch to storing keys/numbers and using enums and active record to convert those to the appropriate values.

6
package.json

@ -2,7 +2,8 @@
"name": "data-collector", "name": "data-collector",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@hotwired/stimulus": "^3.0.0-beta.2", "@hotwired/stimulus": "^3.0.0",
"@hotwired/stimulus-webpack-helpers": "^1.0.1",
"@hotwired/turbo": "^7.0.0-rc.3", "@hotwired/turbo": "^7.0.0-rc.3",
"@hotwired/turbo-rails": "^7.0.0-rc.3", "@hotwired/turbo-rails": "^7.0.0-rc.3",
"@rails/actioncable": "^6.0.0", "@rails/actioncable": "^6.0.0",
@ -10,8 +11,7 @@
"@rails/ujs": "^6.0.0", "@rails/ujs": "^6.0.0",
"@rails/webpacker": "5.4.0", "@rails/webpacker": "5.4.0",
"govuk-frontend": "^3.13.0", "govuk-frontend": "^3.13.0",
"stimulus": "^2.0.0", "stimulus": "^3.0.0",
"stimulus-use": "^0.26.0",
"webpack": "^4.46.0", "webpack": "^4.46.0",
"webpack-cli": "^3.3.12" "webpack-cli": "^3.3.12"
}, },

164
spec/features/case_log_spec.rb

@ -1,19 +1,31 @@
require "rails_helper" require "rails_helper"
RSpec.describe "Test Features" do RSpec.describe "Test Features" do
let!(:case_log) { FactoryBot.create(:case_log, :in_progress) } let!(:case_log) { FactoryBot.create(:case_log, :in_progress) }
let!(:empty_case_log) { FactoryBot.create(:case_log) }
let(:id) { case_log.id } let(:id) { case_log.id }
let(:status) { case_log.status } let(:status) { case_log.status }
question_answers = { question_answers = {
tenant_code: { type: "text", answer: "BZ737" }, tenant_code: { type: "text", answer: "BZ737" },
tenant_age: { type: "numeric", answer: 25 }, tenant_age: { type: "numeric", answer: 25 },
tenant_gender: { type: "radio", answer: "1" }, tenant_gender: { type: "radio", answer: "Female" },
tenant_ethnic_group: { type: "radio", answer: "2" }, tenant_ethnic_group: { type: "radio", answer: "Prefer not to say" },
tenant_nationality: { type: "radio", answer: "0" }, tenant_nationality: { type: "radio", answer: "Lithuania" },
tenant_economic_status: { type: "radio", answer: "4" }, tenant_economic_status: { type: "radio", answer: "Jobseeker" },
household_number_of_other_members: { type: "numeric", answer: 2 }, household_number_of_other_members: { type: "numeric", answer: 2 },
} }
def answer_all_questions_in_income_subsection
visit("/case_logs/#{empty_case_log.id}/net_income")
fill_in("net_income", with: 18_000)
choose("net-income-frequency-yearly-field")
click_button("Save and continue")
choose("net-income-uc-proportion-all-field")
click_button("Save and continue")
choose("housing-benefit-housing-benefit-but-not-universal-credit-field")
click_button("Save and continue")
end
describe "Create new log" do describe "Create new log" do
it "redirects to the task list for the new log" do it "redirects to the task list for the new log" do
visit("/case_logs") visit("/case_logs")
@ -24,10 +36,45 @@ RSpec.describe "Test Features" do
end end
describe "Viewing a log" do describe "Viewing a log" do
it "displays a tasklist header" do context "tasklist page" do
visit("/case_logs/#{id}") it "displays a tasklist header" do
expect(page).to have_content("Tasklist for log #{id}") visit("/case_logs/#{id}")
expect(page).to have_content("This submission is #{status}") expect(page).to have_content("Tasklist for log #{id}")
expect(page).to have_content("This submission is #{status}")
end
it "displays a section status" do
visit("/case_logs/#{empty_case_log.id}")
assert_selector ".govuk-tag", text: /Not started/, count: 8
assert_selector ".govuk-tag", text: /Completed/, count: 0
assert_selector ".govuk-tag", text: /Cannot start yet/, count: 1
end
it "shows the correct status if one section is completed" do
answer_all_questions_in_income_subsection
visit("/case_logs/#{empty_case_log.id}")
assert_selector ".govuk-tag", text: /Not started/, count: 7
assert_selector ".govuk-tag", text: /Completed/, count: 1
assert_selector ".govuk-tag", text: /Cannot start yet/, count: 1
end
it "skips to the first section if no answers are completed" do
visit("/case_logs/#{empty_case_log.id}")
expect(page).to have_link("Skip to next incomplete section", href: /#household_characteristics/)
end
it "shows the number of completed sections if no sections are completed" do
visit("/case_logs/#{empty_case_log.id}")
expect(page).to have_content("You've completed 0 of 9 sections.")
end
it "shows the number of completed sections if one section is completed" do
answer_all_questions_in_income_subsection
visit("/case_logs/#{empty_case_log.id}")
expect(page).to have_content("You've completed 1 of 9 sections.")
end
end end
it "displays the household questions when you click into that section" do it "displays the household questions when you click into that section" do
@ -37,7 +84,7 @@ RSpec.describe "Test Features" do
click_button("Save and continue") click_button("Save and continue")
expect(page).to have_field("tenant-age-field") expect(page).to have_field("tenant-age-field")
click_button("Save and continue") click_button("Save and continue")
expect(page).to have_field("tenant-gender-0-field") expect(page).to have_field("tenant-gender-male-field")
visit page.driver.request.env["HTTP_REFERER"] visit page.driver.request.env["HTTP_REFERER"]
expect(page).to have_field("tenant-age-field") expect(page).to have_field("tenant-age-field")
end end
@ -58,7 +105,7 @@ RSpec.describe "Test Features" do
when "text" when "text"
fill_in(question.to_s, with: answer) fill_in(question.to_s, with: answer)
when "radio" when "radio"
choose("#{question.to_s.tr('_', '-')}-#{answer}-field") choose("#{question.to_s.dasherize}-#{answer.parameterize}-field")
else else
fill_in(question.to_s, with: answer) fill_in(question.to_s, with: answer)
end end
@ -96,4 +143,101 @@ RSpec.describe "Test Features" do
end end
end end
end end
describe "check answers page" 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(question.to_s, 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('_', ' ')}")
end
let(:last_question_for_subsection) { "household_number_of_other_members" }
it "redirects to the check answers page when answering the last question and clicking save and continue" do
fill_in_number_question(id, last_question_for_subsection, 0)
expect(page).to have_current_path("/case_logs/#{id}/#{subsection}/check_answers")
end
it "has question headings based on the subsection" do
visit("case_logs/#{id}/#{subsection}/check_answers")
question_labels = ["Tenant code", "Tenant's age", "Tenant's gender", "Ethnicity", "Nationality", "Work", "Number of Other Household Members"]
question_labels.each do |label|
expect(page).to have_content(label)
end
end
it "should display answers given by the user for the question in the subsection" do
fill_in_number_question(empty_case_log.id, "tenant_age", 28)
choose("tenant-gender-non-binary-field")
click_button("Save and continue")
visit("/case_logs/#{empty_case_log.id}/#{subsection}/check_answers")
expect(page).to have_content("28")
expect(page).to have_content("Non-binary")
end
it "should have an answer link for questions missing an answer" do
visit("case_logs/#{empty_case_log.id}/#{subsection}/check_answers")
assert_selector "a", text: /Answer\z/, count: 7
assert_selector "a", text: "Change", count: 0
expect(page).to have_link("Answer", href: "/case_logs/#{empty_case_log.id}/tenant_age")
end
it "should have a change link for answered questions" do
fill_in_number_question(empty_case_log.id, "tenant_age", 28)
visit("/case_logs/#{empty_case_log.id}/#{subsection}/check_answers")
assert_selector "a", text: /Answer\z/, count: 6
assert_selector "a", text: "Change", count: 1
expect(page).to have_link("Change", href: "/case_logs/#{empty_case_log.id}/tenant_age")
end
it "should have a link pointing to the first question if no questions are answered" do
visit("/case_logs/#{empty_case_log.id}/#{subsection}/check_answers")
expect(page).to have_content("You answered 0 of 7 questions")
expect(page).to have_link("Answer the missing questions", href: "/case_logs/#{empty_case_log.id}/tenant_code")
end
it "should have a link pointing to the next empty question if some questions are answered" do
fill_in_number_question(empty_case_log.id, "net_income", 18_000)
visit("/case_logs/#{empty_case_log.id}/income_and_benefits/check_answers")
expect(page).to have_content("You answered 1 of 4 questions")
expect(page).to have_link("Answer the missing questions", href: "/case_logs/#{empty_case_log.id}/net_income")
end
it "should not display the missing answer questions link if all questions are answered" do
answer_all_questions_in_income_subsection
expect(page).to have_content("You answered all the questions")
assert_selector "a", text: "Answer the missing questions", count: 0
end
end
end
describe "Conditional questions" do
context "given a page where some questions are only conditionally shown, depending on how you answer the first question" do
it "initially hides conditional questions" do
visit("/case_logs/#{id}/armed_forces")
expect(page).not_to have_selector("#armed_forces_injured_div")
end
it "shows conditional questions if the required answer is selected and hides it again when a different answer option is selected", js: true do
visit("/case_logs/#{id}/armed_forces")
# Something about our styling makes the selenium webdriver think the actual radio buttons are not visible so we allow label click here
choose("armed-forces-yes-a-regular-field", allow_label_click: true)
expect(page).to have_selector("#armed_forces_injured_div")
choose("armed-forces-injured-no-field", allow_label_click: true)
expect(find_field("armed-forces-injured-no-field", visible: false).checked?).to be_truthy
choose("armed-forces-no-field", allow_label_click: true)
expect(page).not_to have_selector("#armed_forces_injured_div")
choose("armed-forces-yes-a-regular-field", allow_label_click: true)
expect(find_field("armed-forces-injured-no-field", visible: false).checked?).to be_falsey
end
end
end
end end

24
spec/helpers/check_answers_helper_spec.rb

@ -0,0 +1,24 @@
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") }
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)
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)
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)
end
end
end

28
spec/helpers/conditional_questions_helper_spec.rb

@ -0,0 +1,28 @@
require "rails_helper"
RSpec.describe ConditionalQuestionsHelper do
let(:form) { Form.new(2021, 2022) }
let(:page_key) { "armed_forces" }
let(:page) { form.all_pages[page_key] }
describe "conditional questions for page" do
let(:conditional_pages) { %w[armed_forces_active armed_forces_injured] }
it "returns the question keys of all conditional questions on the given page" do
expect(conditional_questions_for_page(page)).to eq(conditional_pages)
end
end
describe "display question key div" do
let(:question_key) { "armed_forces" }
let(:conditional_question_key) { "armed_forces_injured" }
it "returns a non visible div for conditional questions" do
expect(display_question_key_div(page, conditional_question_key)).to match("style='display:none;'")
end
it "returns a visible div for conditional questions" do
expect(display_question_key_div(page, question_key)).not_to match("style='display:none;'")
end
end
end

84
spec/helpers/tasklist_helper_spec.rb

@ -0,0 +1,84 @@
require "rails_helper"
RSpec.describe TasklistHelper do
describe "get subsection status" do
@form = Form.new(2021, 2022)
income_and_benefits_questions = @form.questions_for_subsection("income_and_benefits").keys
declaration_questions = @form.questions_for_subsection("declaration").keys
local_authority_questions = @form.questions_for_subsection("local_authority").keys
let!(:case_log) { FactoryBot.create(:case_log) }
it "returns not started if none of the questions in the subsection are answered" do
status = get_subsection_status("income_and_benefits", case_log, income_and_benefits_questions)
expect(status).to eq(:not_started)
end
it "returns cannot start yet if the subsection is declaration" do
status = get_subsection_status("declaration", case_log, declaration_questions)
expect(status).to eq(:cannot_start_yet)
end
it "returns in progress if some of the questions have been answered" do
case_log["previous_postcode"] = "P0 5TT"
status = get_subsection_status("local_authority", case_log, local_authority_questions)
expect(status).to eq(:in_progress)
end
it "returns completed if all the questions in the subsection have been answered" do
%w[net_income net_income_frequency net_income_uc_proportion housing_benefit].each { |x| case_log[x] = "value" }
status = get_subsection_status("income_and_benefits", case_log, income_and_benefits_questions)
expect(status).to eq(:completed)
end
it "returns not started if the subsection is declaration and all the questions are completed" do
completed_case_log = CaseLog.new(case_log.attributes.map { |key, value| Hash[key, value || "value"] }.reduce(:merge))
status = get_subsection_status("declaration", completed_case_log, declaration_questions)
expect(status).to eq(:not_started)
end
end
describe "get next incomplete section" do
let!(:case_log) { FactoryBot.create(:case_log) }
it "returns the first subsection name if it is not completed" do
@form = Form.new(2021, 2022)
expect(get_next_incomplete_section(@form, case_log)).to eq("household_characteristics")
end
it "returns the first subsection name if it is partially completed" do
@form = Form.new(2021, 2022)
case_log["tenant_code"] = 123
expect(get_next_incomplete_section(@form, case_log)).to eq("household_characteristics")
end
end
describe "get sections count" do
let!(:empty_case_log) { FactoryBot.create(:case_log) }
let!(:case_log) { FactoryBot.create(:case_log, :in_progress) }
it "returns the total of sections if no status is given" do
@form = Form.new(2021, 2022)
expect(get_sections_count(@form, empty_case_log)).to eq(9)
end
it "returns 0 sections for completed sections if no sections are completed" do
@form = Form.new(2021, 2022)
expect(get_sections_count(@form, empty_case_log, :completed)).to eq(0)
end
it "returns the number of not started sections" do
@form = Form.new(2021, 2022)
expect(get_sections_count(@form, empty_case_log, :not_started)).to eq(8)
end
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)
end
it "returns 0 for invalid state" do
@form = Form.new(2021, 2022)
expect(get_sections_count(@form, case_log, :fake)).to eq(0)
end
end
end

9
spec/models/form_spec.rb

@ -32,4 +32,13 @@ RSpec.describe Form, type: :model do
end end
end end
end end
describe ".questions_for_subsection" do
let(:subsection) { "income_and_benefits" }
it "returns all questions for subsection" do
result = form.questions_for_subsection(subsection)
expect(result.length).to eq(4)
expect(result.keys).to eq(%w[net_income net_income_frequency net_income_uc_proportion housing_benefit])
end
end
end end

8
spec/rails_helper.rb

@ -6,6 +6,10 @@ require File.expand_path("../config/environment", __dir__)
abort("The Rails environment is running in production mode!") if Rails.env.production? abort("The Rails environment is running in production mode!") if Rails.env.production?
require "rspec/rails" require "rspec/rails"
require "capybara/rspec" require "capybara/rspec"
# Comment to run `js: true specs` with visible browser interaction
Capybara.javascript_driver = :selenium_headless
# Add additional requires below this line. Rails is not loaded until this point! # Add additional requires below this line. Rails is not loaded until this point!
# Requires supporting ruby files with custom matchers and macros, etc, in # Requires supporting ruby files with custom matchers and macros, etc, in
@ -62,4 +66,8 @@ RSpec.configure do |config|
config.filter_rails_from_backtrace! config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via: # arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name") # config.filter_gems_from_backtrace("gem name")
# Silence capybara logging puma start up messages to stdout on first js: true
# spec
Capybara.server = :puma, { Silent: true }
end end

2785
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save