68 changed files with 1367 additions and 915 deletions
@ -0,0 +1,27 @@
|
||||
ActiveAdmin.register AdminUser do |
||||
permit_params :email, :password, :password_confirmation |
||||
|
||||
index do |
||||
selectable_column |
||||
id_column |
||||
column :email |
||||
column :current_sign_in_at |
||||
column :sign_in_count |
||||
column :created_at |
||||
actions |
||||
end |
||||
|
||||
filter :email |
||||
filter :current_sign_in_at |
||||
filter :sign_in_count |
||||
filter :created_at |
||||
|
||||
form do |f| |
||||
f.inputs do |
||||
f.input :email |
||||
f.input :password |
||||
f.input :password_confirmation |
||||
end |
||||
f.actions |
||||
end |
||||
end |
@ -1,57 +1,20 @@
|
||||
module CheckAnswersHelper |
||||
def total_answered_questions(subsection, case_log, form) |
||||
total_questions(subsection, case_log, form).keys.count do |question_key| |
||||
case_log[question_key].present? |
||||
end |
||||
end |
||||
|
||||
def total_number_of_questions(subsection, case_log, form) |
||||
total_questions(subsection, case_log, form).count |
||||
end |
||||
|
||||
def total_questions(subsection, case_log, form) |
||||
questions = form.questions_for_subsection(subsection) |
||||
form.filter_conditional_questions(questions, case_log) |
||||
end |
||||
|
||||
def get_next_page_name(form, page_name, case_log) |
||||
page = form.all_pages[page_name] |
||||
if page.key?("conditional_route_to") |
||||
page["conditional_route_to"].each do |conditional_page_name, condition| |
||||
unless condition.any? { |field, value| case_log[field].blank? || !value.include?(case_log[field]) } |
||||
return conditional_page_name |
||||
end |
||||
end |
||||
def display_answered_questions_summary(subsection, case_log) |
||||
total = subsection.applicable_questions_count(case_log) |
||||
answered = subsection.answered_questions_count(case_log) |
||||
if total == answered |
||||
'<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 #{answered} of #{total} questions</p> |
||||
#{create_next_missing_question_link(subsection, case_log)}".html_safe |
||||
end |
||||
form.next_page(page_name) |
||||
end |
||||
|
||||
def create_update_answer_link(question_title, question_info, case_log, form) |
||||
page = form.page_for_question(question_title) |
||||
link_name = if question_info["type"] == "checkbox" |
||||
question_info["answer_options"].keys.any? { |key| case_log[key] == "Yes" } ? "Change" : "Answer" |
||||
else |
||||
case_log[question_title].blank? ? "Answer" : "Change" |
||||
end |
||||
link_to(link_name, "/case_logs/#{case_log.id}/#{page}", class: "govuk-link").html_safe |
||||
end |
||||
private |
||||
|
||||
def create_next_missing_question_link(case_log_id, subsection, case_log, form) |
||||
pages_to_fill_in = [] |
||||
form.pages_for_subsection(subsection).each do |page_title, page_info| |
||||
page_info["questions"].any? { |question| case_log[question].blank? } |
||||
pages_to_fill_in << page_title |
||||
end |
||||
url = "/case_logs/#{case_log_id}/#{pages_to_fill_in.first}" |
||||
def create_next_missing_question_link(subsection, case_log) |
||||
pages_to_fill_in = subsection.unanswered_questions(case_log).map(&:page) |
||||
url = "/case_logs/#{case_log.id}/#{pages_to_fill_in.first.id}" |
||||
link_to("Answer the missing questions", url, class: "govuk-link").html_safe |
||||
end |
||||
|
||||
def display_answered_questions_summary(subsection, case_log, form) |
||||
if total_answered_questions(subsection, case_log, form) == total_number_of_questions(subsection, case_log, form) |
||||
'<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 #{total_answered_questions(subsection, case_log, form)} of #{total_number_of_questions(subsection, case_log, form)} questions</p> |
||||
#{create_next_missing_question_link(case_log['id'], subsection, case_log, form)}".html_safe |
||||
end |
||||
end |
||||
end |
||||
|
@ -1,11 +1,9 @@
|
||||
module ConditionalQuestionsHelper |
||||
def conditional_questions_for_page(page) |
||||
page["questions"].values.map { |question| |
||||
question["conditional_for"] |
||||
}.compact.map(&:keys).flatten |
||||
page.questions.map(&:conditional_for).compact.map(&:keys).flatten |
||||
end |
||||
|
||||
def display_question_key_div(page_info, question_key) |
||||
"style='display:none;'".html_safe if conditional_questions_for_page(page_info).include?(question_key) |
||||
def display_question_key_div(page, question) |
||||
"style='display:none;'".html_safe if conditional_questions_for_page(page).include?(question.id) |
||||
end |
||||
end |
||||
|
@ -0,0 +1,5 @@
|
||||
class AdminUser < ApplicationRecord |
||||
# Include default devise modules. Others available are: |
||||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable |
||||
devise :database_authenticatable, :recoverable, :rememberable, :validatable |
||||
end |
@ -0,0 +1,30 @@
|
||||
class Form::Page |
||||
attr_accessor :id, :header, :description, :questions, :soft_validations, |
||||
:depends_on, :subsection |
||||
|
||||
def initialize(id, hsh, subsection) |
||||
@id = id |
||||
@header = hsh["header"] |
||||
@description = hsh["description"] |
||||
@questions = hsh["questions"].map { |q_id, q| Form::Question.new(q_id, q, self) } |
||||
@depends_on = hsh["depends_on"] |
||||
@soft_validations = hsh["soft_validations"]&.map { |v_id, s| Form::Question.new(v_id, s, self) } |
||||
@subsection = subsection |
||||
end |
||||
|
||||
def expected_responses |
||||
questions + (soft_validations || []) |
||||
end |
||||
|
||||
def has_soft_validations? |
||||
soft_validations.present? |
||||
end |
||||
|
||||
def routed_to?(case_log) |
||||
return true unless depends_on |
||||
|
||||
depends_on.all? do |question, value| |
||||
case_log[question].present? && case_log[question] == value |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,80 @@
|
||||
class Form::Question |
||||
attr_accessor :id, :header, :hint_text, :description, :questions, |
||||
:type, :min, :max, :step, :fields_to_add, :result_field, |
||||
:conditional_for, :readonly, :answer_options, :page, :check_answer_label |
||||
|
||||
def initialize(id, hsh, page) |
||||
@id = id |
||||
@check_answer_label = hsh["check_answer_label"] |
||||
@header = hsh["header"] |
||||
@hint_text = hsh["hint_text"] |
||||
@type = hsh["type"] |
||||
@min = hsh["min"] |
||||
@max = hsh["max"] |
||||
@step = hsh["step"] |
||||
@fields_to_add = hsh["fields-to-add"] |
||||
@result_field = hsh["result-field"] |
||||
@readonly = hsh["readonly"] |
||||
@answer_options = hsh["answer_options"] |
||||
@conditional_for = hsh["conditional_for"] |
||||
@page = page |
||||
end |
||||
|
||||
delegate :subsection, to: :page |
||||
delegate :form, to: :subsection |
||||
|
||||
def answer_label(case_log) |
||||
return checkbox_answer_label(case_log) if type == "checkbox" |
||||
|
||||
case_log[id].to_s |
||||
end |
||||
|
||||
def read_only? |
||||
!!readonly |
||||
end |
||||
|
||||
def enabled?(case_log) |
||||
return true if conditional_on.blank? |
||||
|
||||
conditional_on.map { |condition| evaluate_condition(condition, case_log) }.all? |
||||
end |
||||
|
||||
def update_answer_link_name(case_log) |
||||
if type == "checkbox" |
||||
answer_options.keys.any? { |key| case_log[key] == "Yes" } ? "Change" : "Answer" |
||||
else |
||||
case_log[id].blank? ? "Answer" : "Change" |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def checkbox_answer_label(case_log) |
||||
answer = [] |
||||
answer_options.each { |key, value| case_log[key] == "Yes" ? answer << value : nil } |
||||
answer.join(", ") |
||||
end |
||||
|
||||
def conditional_on |
||||
@conditional_on ||= form.conditional_question_conditions.select do |condition| |
||||
condition[:to] == id |
||||
end |
||||
end |
||||
|
||||
def evaluate_condition(condition, case_log) |
||||
case page.questions.find { |q| q.id == condition[:from] }.type |
||||
when "numeric" |
||||
operator = condition[:cond][/[<>=]+/].to_sym |
||||
operand = condition[:cond][/\d+/].to_i |
||||
case_log[condition[:from]].present? && case_log[condition[:from]].send(operator, operand) |
||||
when "text" |
||||
case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]]) |
||||
when "radio" |
||||
case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]]) |
||||
when "select" |
||||
case_log[condition[:from]].present? && condition[:cond].include?(case_log[condition[:from]]) |
||||
else |
||||
raise "Not implemented yet" |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,10 @@
|
||||
class Form::Section |
||||
attr_accessor :id, :label, :subsections, :form |
||||
|
||||
def initialize(id, hsh, form) |
||||
@id = id |
||||
@label = hsh["label"] |
||||
@form = form |
||||
@subsections = hsh["subsections"].map { |s_id, s| Form::Subsection.new(s_id, s, self) } |
||||
end |
||||
end |
@ -0,0 +1,65 @@
|
||||
class Form::Subsection |
||||
attr_accessor :id, :label, :section, :pages, :depends_on, :form |
||||
|
||||
def initialize(id, hsh, section) |
||||
@id = id |
||||
@label = hsh["label"] |
||||
@depends_on = hsh["depends_on"] |
||||
@pages = hsh["pages"].map { |s_id, p| Form::Page.new(s_id, p, self) } |
||||
@section = section |
||||
end |
||||
|
||||
delegate :form, to: :section |
||||
|
||||
def questions |
||||
@questions ||= pages.flat_map(&:questions) |
||||
end |
||||
|
||||
def enabled?(case_log) |
||||
return true unless depends_on |
||||
|
||||
depends_on.all? do |subsection_id, dependent_status| |
||||
form.get_subsection(subsection_id).status(case_log) == dependent_status.to_sym |
||||
end |
||||
end |
||||
|
||||
def status(case_log) |
||||
unless enabled?(case_log) |
||||
return :cannot_start_yet |
||||
end |
||||
|
||||
qs = applicable_questions(case_log) |
||||
return :not_started if qs.all? { |question| case_log[question.id].blank? } |
||||
return :completed if qs.all? { |question| case_log[question.id].present? } |
||||
|
||||
:in_progress |
||||
end |
||||
|
||||
def is_incomplete?(case_log) |
||||
%i[not_started in_progress].include?(status(case_log)) |
||||
end |
||||
|
||||
def is_started?(case_log) |
||||
%i[in_progress completed].include?(status(case_log)) |
||||
end |
||||
|
||||
def applicable_questions_count(case_log) |
||||
applicable_questions(case_log).count |
||||
end |
||||
|
||||
def answered_questions_count(case_log) |
||||
answered_questions(case_log).count |
||||
end |
||||
|
||||
def applicable_questions(case_log) |
||||
questions.select { |q| q.page.routed_to?(case_log) && q.enabled?(case_log) } |
||||
end |
||||
|
||||
def answered_questions(case_log) |
||||
applicable_questions(case_log).select { |question| case_log[question.id].present? } |
||||
end |
||||
|
||||
def unanswered_questions(case_log) |
||||
applicable_questions(case_log) - answered_questions(case_log) |
||||
end |
||||
end |
@ -1,11 +1,11 @@
|
||||
<div class="govuk-summary-list__row"> |
||||
<dt class="govuk-summary-list__key"> |
||||
<%= question_info["check_answer_label"].to_s.present? ? question_info["check_answer_label"].to_s : question_info["header"].to_s%> |
||||
<%= question.check_answer_label.to_s.present? ? question.check_answer_label.to_s : question.header.to_s %> |
||||
<dt> |
||||
<dd class="govuk-summary-list__value"> |
||||
<%= form.get_answer_label(@case_log, question_title) %> |
||||
<%= question.answer_label(@case_log) %> |
||||
</dd> |
||||
<dd class="govuk-summary-list__actions"> |
||||
<%= create_update_answer_link(question_title, question_info, @case_log, form) %> |
||||
<%= link_to(question.update_answer_link_name(@case_log), "/case_logs/#{@case_log.id}/#{question.page.id}", class: "govuk-link").html_safe %> |
||||
</dd> |
||||
</div> |
||||
|
@ -1,6 +1,6 @@
|
||||
<%= f.govuk_date_field question_key, |
||||
hint: { text: question["hint_text"] }, |
||||
legend: { text: question["header"].html_safe, size: "l"}, |
||||
<%= f.govuk_date_field question.id.to_sym, |
||||
hint: { text: question.hint_text }, |
||||
legend: { text: question.header.html_safe, size: page_header.nil? ? "l" : "m", tag: page_header.nil? ? "h2" : "h1" }, |
||||
width: 20, |
||||
**stimulus_html_attributes(question) |
||||
%> |
||||
|
@ -1,7 +1,7 @@
|
||||
<%= f.govuk_number_field question_key, |
||||
hint: { text: question["hint_text"] }, |
||||
label: { text: question["header"].html_safe, size: "l"}, |
||||
min: question["min"], max: question["max"], step: question["step"], |
||||
width: 20, :readonly => question["readonly"], |
||||
<%= f.govuk_number_field question.id.to_sym, |
||||
hint: { text: question.hint_text }, |
||||
label: { text: question.header.html_safe, size: page_header.nil? ? "l" : "m", tag: page_header.nil? ? "h2" : "h1" }, |
||||
min: question.min, max: question.max, step: question.step, |
||||
width: 20, :readonly => question.read_only?, |
||||
**stimulus_html_attributes(question) |
||||
%> |
||||
|
@ -1,13 +1,13 @@
|
||||
<%= f.govuk_radio_buttons_fieldset question_key, |
||||
legend: { text: question["header"].html_safe, size: "l" }, |
||||
hint: { text: question["hint_text"] }, |
||||
small: (question["answer_options"].size > 5) do %> |
||||
<%= f.govuk_radio_buttons_fieldset question.id.to_sym, |
||||
legend: { text: question.header.html_safe, size: page_header.nil? ? "l" : "m", tag: page_header.nil? ? "h2" : "h1" }, |
||||
hint: { text: question.hint_text }, |
||||
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") %> |
||||
<%= f.govuk_radio_divider %> |
||||
<% else %> |
||||
<%= f.govuk_radio_button question_key, val, label: { text: val }, **stimulus_html_attributes(question) %> |
||||
<%= f.govuk_radio_button question.id, val, label: { text: val }, **stimulus_html_attributes(question) %> |
||||
<% end %> |
||||
<% end %> |
||||
<% end %> |
||||
|
@ -1,6 +1,6 @@
|
||||
<%= f.govuk_text_field question_key, |
||||
hint: { text: question["hint_text"] }, |
||||
label: { text: question["header"].html_safe, size: "l"}, |
||||
<%= f.govuk_text_field question.id.to_sym, |
||||
hint: { text: question.hint_text }, |
||||
label: { text: question.header.html_safe, size: page_header.nil? ? "l" : "m", tag: page_header.nil? ? "h2" : "h1" }, |
||||
width: 20, |
||||
**stimulus_html_attributes(question) |
||||
%> |
||||
|
@ -0,0 +1,10 @@
|
||||
class AddPropertyInfoFields < ActiveRecord::Migration[6.1] |
||||
def change |
||||
change_table :case_logs, bulk: true do |t| |
||||
t.column :first_time_property_let_as_social_housing, :int |
||||
t.column :why_dont_you_know_la, :string |
||||
t.column :type_property_most_recently_let_as, :string |
||||
t.column :builtype, :string |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,18 @@
|
||||
class AddAdminUsers < ActiveRecord::Migration[6.1] |
||||
def change |
||||
create_table :admin_users do |t| |
||||
## Database authenticatable |
||||
t.string :email, null: false, default: "" |
||||
t.string :encrypted_password, null: false, default: "" |
||||
|
||||
## Recoverable |
||||
t.string :reset_password_token |
||||
t.datetime :reset_password_sent_at |
||||
|
||||
## Rememberable |
||||
t.datetime :remember_created_at |
||||
|
||||
t.timestamps null: false |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,19 @@
|
||||
class ChangeRecentlyLetAsToEnum < ActiveRecord::Migration[6.1] |
||||
def up |
||||
change_table :case_logs, bulk: true do |t| |
||||
t.remove :type_property_most_recently_let_as |
||||
t.column :unitletas, :int |
||||
t.remove :builtype |
||||
t.column :builtype, :int |
||||
end |
||||
end |
||||
|
||||
def down |
||||
change_table :case_logs, bulk: true do |t| |
||||
t.remove :unitletas |
||||
t.column :type_property_most_recently_let_as, :string |
||||
t.remove :builtype |
||||
t.remove :builtype, :string |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,9 @@
|
||||
class RemoveOldFields < ActiveRecord::Migration[6.1] |
||||
def up |
||||
remove_column :case_logs, :property_building_type |
||||
end |
||||
|
||||
def down |
||||
add_column :case_logs, :property_building_type, :string |
||||
end |
||||
end |
@ -0,0 +1,9 @@
|
||||
### ADR - 010: Admin Users vs Users |
||||
|
||||
#### Why do we have 2 User classes, AdminUser and User? |
||||
|
||||
This is modelling a real life split. `AdminUsers` are internal DLUHC users or helpdesk employees. While `Users` are external users working at data providing organisations. So local authority/housing association's "admin" users, i.e. Data Co-ordinators are a type of the User class. They have the ability to add or remove other users to or from their organisation, and to update their organisation details etc, but only through the designed UI. They do not get direct access to ActiveAdmin. |
||||
|
||||
AdminUsers on the other hand get direct access to ActiveAdmin. From there they can download entire datasets (via CSV, XML, JSON), view any log from any organisation, and add or remove users of any type including other Admin users. This means TDA will likely also require more stringent authentication for them using MFA (which users will likely not require). So the class split also helps there. |
||||
|
||||
A potential downside to this approach is that it does not currently allow for `AdminUsers` to sign into the application UI itself with their Admin credentials. However, we need to see if there's an actual use case for this and what it would be (since they aren't part of an organisation to be uploading data for, but could add or amend data or user or org details through ActiveAdmin anyway). If there is a strong use case for it this could be work around by either: providing them with two sets of credentials, or modifying the `authenticate_user` method to also check `AdminUser` credentials. |
@ -0,0 +1,10 @@
|
||||
### ADR - 011: Splitting the form parsing into objects |
||||
|
||||
Initially a single "Form" class handled the parsing of the form definition JSON as well as a lot of the logic around what different sections meant. This works fine but led to a lot of places in code where we're passing around arguments to determine whether a page or section should or shouldn't do something rather than being able to ask it directly. Refactoring this into smaller form domain object classes has several benefits: |
||||
|
||||
- It's easier to compare the form definition JSON to the code classes and reason about what fields can be passed and what effect they'll have |
||||
- It moves business logic out of the helpers and keeps them to just dealing with display logic |
||||
- It makes it easier to unit test form functionality, and group that into smaller chunks |
||||
- It allows for less passing of arguments. e.g. `page.routed_to?(case_log)` vs `form.was_page_routed_to?(page, case_log)` |
||||
|
||||
This abstraction is likely still not the best (the form vs case log split) but this seems like an improvement that can be iterated on. |
@ -0,0 +1,30 @@
|
||||
require "rails_helper" |
||||
require_relative "../../support/devise" |
||||
|
||||
describe Admin::AdminUsersController, type: :controller do |
||||
render_views |
||||
let(:page) { Capybara::Node::Simple.new(response.body) } |
||||
let(:resource_title) { "Admin Users" } |
||||
let(:valid_session) { {} } |
||||
login_admin_user |
||||
|
||||
describe "Get admin users" do |
||||
before do |
||||
get :index, session: valid_session |
||||
end |
||||
|
||||
it "returns a table of admin users" do |
||||
expect(page).to have_content(resource_title) |
||||
expect(page).to have_table("index_table_admin_users") |
||||
expect(page).to have_link(AdminUser.first.id.to_s) |
||||
end |
||||
end |
||||
|
||||
describe "Create admin users" do |
||||
let(:params) { { admin_user: { email: "test2@example.com", password: "pAssword1" } } } |
||||
|
||||
it "creates a new admin users" do |
||||
expect { post :create, session: valid_session, params: params }.to change(AdminUser, :count).by(1) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,29 @@
|
||||
require "rails_helper" |
||||
require_relative "../../support/devise" |
||||
|
||||
describe Admin::CaseLogsController, type: :controller do |
||||
render_views |
||||
let(:page) { Capybara::Node::Simple.new(response.body) } |
||||
let(:resource_title) { "Case Logs" } |
||||
let(:valid_session) { {} } |
||||
login_admin_user |
||||
|
||||
describe "Get case logs" do |
||||
let!(:case_log) { FactoryBot.create(:case_log, :in_progress) } |
||||
before do |
||||
get :index, session: valid_session |
||||
end |
||||
|
||||
it "returns a table of case logs" do |
||||
expect(page).to have_content(resource_title) |
||||
expect(page).to have_table("index_table_case_logs") |
||||
expect(page).to have_link(case_log.id.to_s) |
||||
end |
||||
end |
||||
|
||||
describe "Create case logs" do |
||||
it "creates a new case log" do |
||||
expect { post :create, session: valid_session }.to change(CaseLog, :count).by(1) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,41 @@
|
||||
require "rails_helper" |
||||
require_relative "../../support/devise" |
||||
|
||||
describe Admin::DashboardController, type: :controller do |
||||
render_views |
||||
let(:page) { Capybara::Node::Simple.new(response.body) } |
||||
let(:resource_title) { "Dashboard" } |
||||
let!(:case_log) { FactoryBot.create(:case_log, :in_progress) } |
||||
let!(:case_log_2) { FactoryBot.create(:case_log, :in_progress) } |
||||
let!(:completed_case_log) { FactoryBot.create(:case_log, :completed) } |
||||
let(:valid_session) { {} } |
||||
login_admin_user |
||||
|
||||
describe "Get case logs" do |
||||
before do |
||||
get :index, session: valid_session |
||||
end |
||||
|
||||
it "returns a dashboard page" do |
||||
expect(page).to have_content(resource_title) |
||||
end |
||||
|
||||
it "returns a panel of recent case logs" do |
||||
expect(page).to have_xpath("//div[contains(@class, 'panel') and contains(//h3, 'Recent Case Logs')]") |
||||
end |
||||
|
||||
it "returns a panel of in progress case logs" do |
||||
panel_xpath = "//div[@class='panel' and .//h3[contains(., 'Total case logs in progress')]]" |
||||
panel_content_xpath = "#{panel_xpath}//div[@class='panel_contents' and .//p[contains(., 2)]]" |
||||
expect(page).to have_xpath(panel_xpath) |
||||
expect(page).to have_xpath(panel_content_xpath) |
||||
end |
||||
|
||||
it "returns a panel of completed case logs" do |
||||
panel_xpath = "//div[@class='panel' and .//h3[contains(., 'Total case logs completed')]]" |
||||
panel_content_xpath = "#{panel_xpath}//div[@class='panel_contents' and .//p[contains(., 1)]]" |
||||
expect(page).to have_xpath(panel_xpath) |
||||
expect(page).to have_xpath(panel_content_xpath) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,8 @@
|
||||
FactoryBot.define do |
||||
factory :admin_user do |
||||
email { "admin@example.com" } |
||||
password { "pAssword1" } |
||||
created_at { Time.zone.now } |
||||
updated_at { Time.zone.now } |
||||
end |
||||
end |
@ -1,179 +0,0 @@
|
||||
{ |
||||
"form_type": "lettings", |
||||
"sections": { |
||||
"about_this_log": { |
||||
"label": "About this log", |
||||
"subsections": { |
||||
"about_this_log": { |
||||
"label": "About this log", |
||||
"pages": { |
||||
"gdpr_acceptance": { |
||||
"header": "DLUHC Privacy Notice Acceptance", |
||||
"description": "", |
||||
"questions": { |
||||
"gdpr_acceptance": { |
||||
"check_answer_label": "GDPR acceptance", |
||||
"header": "Has the tenant or buyer seen the DLUHC privacy notice?", |
||||
"hint_text": "", |
||||
"type": "radio", |
||||
"answer_options": { |
||||
"0": "Yes", |
||||
"1": "No" |
||||
} |
||||
} |
||||
}, |
||||
"conditional_route_to": { |
||||
"organisation_details": { "gdpr_acceptance": "Yes" } |
||||
} |
||||
}, |
||||
"gdpr_declined": { |
||||
"header": "You cannot use this service", |
||||
"hint_text": "", |
||||
"description": "We cannot accept data about a tenant or buyer unless they’ve seen the DLUHC privacy notice.", |
||||
"questions": { |
||||
|
||||
} |
||||
}, |
||||
"organisation_details": { |
||||
"header": "About this log", |
||||
"description": "Organisation Details", |
||||
"questions": { |
||||
"property_owner_organisation": { |
||||
"check_answer_label": "", |
||||
"header": "Which organisation owns this property?", |
||||
"hint_text": "", |
||||
"type": "radio", |
||||
"answer_options": { |
||||
"0": "A", |
||||
"1": "B" |
||||
} |
||||
}, |
||||
"property_manager_organisation": { |
||||
"check_answer_label": "", |
||||
"header": "Which organisation manages this property?", |
||||
"hint_text": "", |
||||
"type": "radio", |
||||
"answer_options": { |
||||
"0": "A", |
||||
"1": "B" |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
"sale_or_letting": { |
||||
"header": "About this log", |
||||
"description": "Is this a sale or a letting?", |
||||
"questions": { |
||||
"sale_or_letting": { |
||||
"check_answer_label": "", |
||||
"header": "Is this a sale or a letting?", |
||||
"hint_text": "", |
||||
"type": "radio", |
||||
"answer_options": { |
||||
"0": "Sale", |
||||
"1": "Letting" |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
"tenant_same_property_renewal": { |
||||
"header": "About this log", |
||||
"description": "Is this a renewal to the same tenant in the same property?", |
||||
"questions": { |
||||
"tenant_same_property_renewal": { |
||||
"check_answer_label": "", |
||||
"header": "Is this a renewal to the same tenant in the same property?", |
||||
"hint_text": "", |
||||
"type": "radio", |
||||
"answer_options": { |
||||
"0": "Yes", |
||||
"1": "No" |
||||
} |
||||
} |
||||
}, |
||||
"depends_on": { "sale_or_letting": "Letting" } |
||||
}, |
||||
"tenancy_start_date": { |
||||
"header": "About this log", |
||||
"description": "", |
||||
"questions": { |
||||
"tenancy_start_date": { |
||||
"check_answer_label": "When is the tenancy start date?", |
||||
"header": "What is the tenancy start date?", |
||||
"hint_text": "For example, 27 3 2007", |
||||
"type": "date" |
||||
} |
||||
}, |
||||
"depends_on": { "sale_or_letting": "Letting" } |
||||
}, |
||||
"letting_type": { |
||||
"header": "About this log", |
||||
"description": "", |
||||
"questions": { |
||||
"rent_type": { |
||||
"check_answer_label": "What is the rent type?", |
||||
"header": "What is the rent type?", |
||||
"hint_text": "", |
||||
"type": "select", |
||||
"answer_options": { |
||||
"0": "Social Rent", |
||||
"1": "Affordable Rent", |
||||
"2": "London Affordable Rent", |
||||
"3": "Rent To Buy", |
||||
"4": "London Living Rent", |
||||
"5": "Other Intermediate Rent Product" |
||||
}, |
||||
"conditional_for": { |
||||
"intermediate_rent_product_name": ["Other Intermediate Rent Product"] |
||||
} |
||||
}, |
||||
"intermediate_rent_product_name": { |
||||
"check_answer_label": "Enter the product name", |
||||
"header": "What is intermediate rent product name?", |
||||
"type": "text" |
||||
}, |
||||
"needs_type": { |
||||
"check_answer_label": "What is the needs type?", |
||||
"header": "What is the needs type?", |
||||
"hint_text": "", |
||||
"type": "select", |
||||
"answer_options": { |
||||
"0": "Supported Housing", |
||||
"1": "General Needs" |
||||
} |
||||
} |
||||
}, |
||||
"depends_on": { "sale_or_letting": "Letting" } |
||||
}, |
||||
"sale_completion_date": { |
||||
"header": "About this log", |
||||
"description": "", |
||||
"questions": { |
||||
"sale_completion_date": { |
||||
"check_answer_label": "What is the sale completion date?", |
||||
"header": "What is the sale completion date?", |
||||
"hint_text": "For example, 27 3 2007", |
||||
"type": "date" |
||||
} |
||||
}, |
||||
"depends_on": { "sale_or_letting": "Sale" } |
||||
}, |
||||
"purchaser_code": { |
||||
"header": "About this log", |
||||
"description": "", |
||||
"questions": { |
||||
"purchaser_code": { |
||||
"check_answer_label": "What is the purchaser code?", |
||||
"header": "What is the purchaser code?", |
||||
"hint_text": "", |
||||
"type": "text" |
||||
} |
||||
}, |
||||
"depends_on": { "sale_or_letting": "Sale" } |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,66 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe Form::Page, type: :model do |
||||
let(:form) { FormHandler.instance.get_form("test_form") } |
||||
let(:section_id) { "rent_and_charges" } |
||||
let(:section_definition) { form.form_definition["sections"][section_id] } |
||||
let(:section) { Form::Section.new(section_id, section_definition, form) } |
||||
let(:subsection_id) { "income_and_benefits" } |
||||
let(:subsection_definition) { section_definition["subsections"][subsection_id] } |
||||
let(:subsection) { Form::Subsection.new(subsection_id, subsection_definition, section) } |
||||
let(:page_id) { "net_income" } |
||||
let(:page_definition) { subsection_definition["pages"][page_id] } |
||||
subject { Form::Page.new(page_id, page_definition, subsection) } |
||||
|
||||
it "has an id" do |
||||
expect(subject.id).to eq(page_id) |
||||
end |
||||
|
||||
it "has a header" do |
||||
expect(subject.header).to eq("Test header") |
||||
end |
||||
|
||||
it "has a description" do |
||||
expect(subject.description).to eq("Some extra text for the page") |
||||
end |
||||
|
||||
it "has questions" do |
||||
expected_questions = %w[earnings incfreq] |
||||
expect(subject.questions.map(&:id)).to eq(expected_questions) |
||||
end |
||||
|
||||
it "has soft validations" do |
||||
expected_soft_validations = %w[override_net_income_validation] |
||||
expect(subject.soft_validations.map(&:id)).to eq(expected_soft_validations) |
||||
end |
||||
|
||||
it "has a soft_validation helper" do |
||||
expect(subject.has_soft_validations?).to be true |
||||
end |
||||
|
||||
it "has expected form responses" do |
||||
expected_responses = %w[earnings incfreq override_net_income_validation] |
||||
expect(subject.expected_responses.map(&:id)).to eq(expected_responses) |
||||
end |
||||
|
||||
context "for a given case log" do |
||||
let(:case_log) { FactoryBot.build(:case_log, :in_progress) } |
||||
|
||||
it "knows if it's been routed to" do |
||||
expect(subject.routed_to?(case_log)).to be true |
||||
end |
||||
|
||||
context "given routing conditions" do |
||||
let(:page_id) { "dependent_page" } |
||||
|
||||
it "evaluates not met conditions correctly" do |
||||
expect(subject.routed_to?(case_log)).to be false |
||||
end |
||||
|
||||
it "evaluates not conditions correctly" do |
||||
case_log.incfreq = "Weekly" |
||||
expect(subject.routed_to?(case_log)).to be true |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,140 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe Form::Question, type: :model do |
||||
let(:form) { FormHandler.instance.get_form("test_form") } |
||||
let(:section_id) { "rent_and_charges" } |
||||
let(:section_definition) { form.form_definition["sections"][section_id] } |
||||
let(:section) { Form::Section.new(section_id, section_definition, form) } |
||||
let(:subsection_id) { "income_and_benefits" } |
||||
let(:subsection_definition) { section_definition["subsections"][subsection_id] } |
||||
let(:subsection) { Form::Subsection.new(subsection_id, subsection_definition, section) } |
||||
let(:page_id) { "net_income" } |
||||
let(:page_definition) { subsection_definition["pages"][page_id] } |
||||
let(:page) { Form::Page.new(page_id, page_definition, subsection) } |
||||
let(:question_id) { "earnings" } |
||||
let(:question_definition) { page_definition["questions"][question_id] } |
||||
subject { Form::Question.new(question_id, question_definition, page) } |
||||
|
||||
it "has an id" do |
||||
expect(subject.id).to eq(question_id) |
||||
end |
||||
|
||||
it "has a header" do |
||||
expect(subject.header).to eq("What is the tenant’s /and partner’s combined income after tax?") |
||||
end |
||||
|
||||
it "has a check answers label" do |
||||
expect(subject.check_answer_label).to eq("Income") |
||||
end |
||||
|
||||
it "has a question type" do |
||||
expect(subject.type).to eq("numeric") |
||||
end |
||||
|
||||
it "belongs to a page" do |
||||
expect(subject.page).to eq(page) |
||||
end |
||||
|
||||
it "belongs to a subsection" do |
||||
expect(subject.subsection).to eq(subsection) |
||||
end |
||||
|
||||
it "has a read only helper" do |
||||
expect(subject.read_only?).to be false |
||||
end |
||||
|
||||
context "when type is numeric" do |
||||
it "has a min value" do |
||||
expect(subject.min).to eq(0) |
||||
end |
||||
|
||||
it "has a step value" do |
||||
expect(subject.step).to eq(1) |
||||
end |
||||
end |
||||
|
||||
context "when type is radio" do |
||||
let(:question_id) { "incfreq" } |
||||
|
||||
it "has answer options" do |
||||
expected_answer_options = { "0" => "Weekly", "1" => "Monthly", "2" => "Yearly" } |
||||
expect(subject.answer_options).to eq(expected_answer_options) |
||||
end |
||||
end |
||||
|
||||
context "when type is checkbox" do |
||||
let(:page_id) { "dependent_page" } |
||||
let(:question_id) { "dependent_question" } |
||||
|
||||
it "has answer options" do |
||||
expected_answer_options = { "0" => "Option A", "1" => "Option B" } |
||||
expect(subject.answer_options).to eq(expected_answer_options) |
||||
end |
||||
end |
||||
|
||||
context "when the question is read only" do |
||||
let(:subsection_id) { "rent" } |
||||
let(:page_id) { "rent" } |
||||
let(:question_id) { "tcharge" } |
||||
|
||||
it "has a read only helper" do |
||||
expect(subject.read_only?).to be true |
||||
end |
||||
|
||||
context "when the answer is part of a sum" do |
||||
let(:question_id) { "pscharge" } |
||||
|
||||
it "has a result_field" do |
||||
expect(subject.result_field).to eq("tcharge") |
||||
end |
||||
|
||||
it "has fields to sum" do |
||||
expected_fields_to_sum = %w[brent scharge pscharge supcharg] |
||||
expect(subject.fields_to_add).to eq(expected_fields_to_sum) |
||||
end |
||||
end |
||||
end |
||||
|
||||
context "for a given case log" do |
||||
let(:case_log) { FactoryBot.build(:case_log, :in_progress) } |
||||
|
||||
it "has an answer label" do |
||||
case_log.earnings = 100 |
||||
expect(subject.answer_label(case_log)).to eq("100") |
||||
end |
||||
|
||||
it "has an update answer link text helper" do |
||||
expect(subject.update_answer_link_name(case_log)).to eq("Answer") |
||||
case_log[question_id] = 5 |
||||
expect(subject.update_answer_link_name(case_log)).to eq("Change") |
||||
end |
||||
|
||||
context "when type is checkbox" do |
||||
let(:section_id) { "household" } |
||||
let(:subsection_id) { "household_needs" } |
||||
let(:page_id) { "accessibility_requirements" } |
||||
let(:question_id) { "accessibility_requirements" } |
||||
|
||||
it "has a joined answers label" do |
||||
case_log.housingneeds_a = 1 |
||||
case_log.housingneeds_c = 1 |
||||
expected_answer_label = "Fully wheelchair accessible housing, Level access housing" |
||||
expect(subject.answer_label(case_log)).to eq(expected_answer_label) |
||||
end |
||||
end |
||||
|
||||
context "when a condition is present" do |
||||
let(:page_id) { "housing_benefit" } |
||||
let(:question_id) { "conditional_question" } |
||||
|
||||
it "knows whether it is enabled or not for unmet conditions" do |
||||
expect(subject.enabled?(case_log)).to be false |
||||
end |
||||
|
||||
it "knows whether it is enabled or not for met conditions" do |
||||
case_log.hb = "Housing Benefit, but not Universal Credit" |
||||
expect(subject.enabled?(case_log)).to be true |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,21 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe Form::Section, type: :model do |
||||
let(:form) { FormHandler.instance.get_form("test_form") } |
||||
let(:section_id) { "household" } |
||||
let(:section_definition) { form.form_definition["sections"][section_id] } |
||||
subject { Form::Section.new(section_id, section_definition, form) } |
||||
|
||||
it "has an id" do |
||||
expect(subject.id).to eq(section_id) |
||||
end |
||||
|
||||
it "has a label" do |
||||
expect(subject.label).to eq("About the household") |
||||
end |
||||
|
||||
it "has subsections" do |
||||
expected_subsections = %w[household_characteristics household_needs] |
||||
expect(subject.subsections.map(&:id)).to eq(expected_subsections) |
||||
end |
||||
end |
@ -0,0 +1,72 @@
|
||||
require "rails_helper" |
||||
|
||||
RSpec.describe Form::Subsection, type: :model do |
||||
let(:form) { FormHandler.instance.get_form("test_form") } |
||||
let(:section_id) { "household" } |
||||
let(:section_definition) { form.form_definition["sections"][section_id] } |
||||
let(:section) { Form::Section.new(section_id, section_definition, form) } |
||||
let(:subsection_id) { "household_characteristics" } |
||||
let(:subsection_definition) { section_definition["subsections"][subsection_id] } |
||||
subject { Form::Subsection.new(subsection_id, subsection_definition, section) } |
||||
|
||||
it "has an id" do |
||||
expect(subject.id).to eq(subsection_id) |
||||
end |
||||
|
||||
it "has a label" do |
||||
expect(subject.label).to eq("Household characteristics") |
||||
end |
||||
|
||||
it "has pages" do |
||||
expected_pages = %w[tenant_code person_1_age person_1_gender household_number_of_other_members] |
||||
expect(subject.pages.map(&:id)).to eq(expected_pages) |
||||
end |
||||
|
||||
it "has questions" do |
||||
expected_questions = %w[tenant_code age1 sex1 other_hhmemb relat2 age2 sex2 ecstat2] |
||||
expect(subject.questions.map(&:id)).to eq(expected_questions) |
||||
end |
||||
|
||||
context "for a given in progress case log" do |
||||
let(:case_log) { FactoryBot.build(:case_log, :in_progress) } |
||||
|
||||
it "has a status" do |
||||
expect(subject.status(case_log)).to eq(:in_progress) |
||||
end |
||||
|
||||
it "has status helpers" do |
||||
expect(subject.is_incomplete?(case_log)).to be(true) |
||||
expect(subject.is_started?(case_log)).to be(true) |
||||
end |
||||
|
||||
it "has question helpers for the number of applicable questions" do |
||||
expected_questions = %w[tenant_code age1 sex1 other_hhmemb] |
||||
expect(subject.applicable_questions(case_log).map(&:id)).to eq(expected_questions) |
||||
expect(subject.applicable_questions_count(case_log)).to eq(4) |
||||
end |
||||
|
||||
it "has question helpers for the number of answered questions" do |
||||
expected_questions = %w[tenant_code age1] |
||||
expect(subject.answered_questions(case_log).map(&:id)).to eq(expected_questions) |
||||
expect(subject.answered_questions_count(case_log)).to eq(2) |
||||
end |
||||
|
||||
it "has a question helpers for the unanswered questions" do |
||||
expected_questions = %w[sex1 other_hhmemb] |
||||
expect(subject.unanswered_questions(case_log).map(&:id)).to eq(expected_questions) |
||||
end |
||||
end |
||||
|
||||
context "for a given completed case log" do |
||||
let(:case_log) { FactoryBot.build(:case_log, :completed) } |
||||
|
||||
it "has a status" do |
||||
expect(subject.status(case_log)).to eq(:completed) |
||||
end |
||||
|
||||
it "has status helpers" do |
||||
expect(subject.is_incomplete?(case_log)).to be(false) |
||||
expect(subject.is_started?(case_log)).to be(true) |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue