Browse Source

Merge branch 'main' into CLDC-1315-set-foreign-key-constraint-on-users

pull/726/head
Ted-U 3 years ago committed by GitHub
parent
commit
397472c4cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 37
      app/controllers/form_controller.rb
  2. 2
      app/controllers/locations_controller.rb
  3. 2
      app/frontend/controllers/accessible_autocomplete_controller.js
  4. 28
      app/frontend/modules/search.js
  5. 34
      app/models/case_log.rb
  6. 6
      app/models/derived_variables/case_log_variables.rb
  7. 32
      app/models/form/question.rb
  8. 2
      app/models/form/setup/questions/location_id.rb
  9. 6
      app/models/form/setup/questions/scheme_id.rb
  10. 20
      app/models/location.rb
  11. 12
      app/models/scheme.rb
  12. 10
      app/models/validations/submission_validations.rb
  13. 68
      app/services/imports/case_logs_import_service.rb
  14. 45
      app/services/imports/scheme_import_service.rb
  15. 13
      app/services/imports/scheme_location_import_service.rb
  16. 21
      app/services/postcode_service.rb
  17. 23
      app/views/form/_select_question.html.erb
  18. 11
      app/views/form/page.html.erb
  19. 22
      app/views/locations/edit.html.erb
  20. 22
      app/views/locations/new.html.erb
  21. 66
      config/forms/2021_2022.json
  22. 70
      config/forms/2022_2023.json
  23. 17
      config/locales/en.yml
  24. 7
      db/migrate/20220713095713_add_mobility_type_to_locations.rb
  25. 7
      db/migrate/20220714080044_add_location_startdate.rb
  26. 5
      db/migrate/20220715133937_remove_county_from_location.rb
  27. 5
      db/schema.rb
  28. 5
      spec/factories/location.rb
  29. 27
      spec/features/form/accessible_autocomplete_spec.rb
  30. 2
      spec/features/form/check_answers_page_spec.rb
  31. 76
      spec/features/form/form_navigation_spec.rb
  32. 1
      spec/features/form/page_routing_spec.rb
  33. 4
      spec/features/form/progressive_total_field_spec.rb
  34. 1
      spec/features/form/saving_data_spec.rb
  35. 47
      spec/features/schemes_spec.rb
  36. 524
      spec/fixtures/imports/case_logs/0b4a68df-30cc-474a-93c0-a56ce8fdad3b.xml
  37. 15
      spec/models/case_log_spec.rb
  38. 69
      spec/models/form/setup/questions/location_id_spec.rb
  39. 14
      spec/models/form/setup/questions/scheme_id_spec.rb
  40. 11
      spec/models/location_spec.rb
  41. 4
      spec/models/scheme_spec.rb
  42. 1
      spec/rails_helper.rb
  43. 2
      spec/requests/case_logs_controller_spec.rb
  44. 8
      spec/requests/locations_controller_spec.rb
  45. 68
      spec/services/imports/case_logs_import_service_spec.rb
  46. 13
      spec/services/imports/scheme_import_service_spec.rb
  47. 2
      spec/services/imports/scheme_location_import_service_spec.rb

37
app/controllers/form_controller.rb

@ -7,11 +7,16 @@ class FormController < ApplicationController
if @case_log if @case_log
@page = @case_log.form.get_page(params[:case_log][:page]) @page = @case_log.form.get_page(params[:case_log][:page])
responses_for_page = responses_for_page(@page) responses_for_page = responses_for_page(@page)
if @case_log.update(responses_for_page) mandatory_questions_with_no_response = mandatory_questions_with_no_response(responses_for_page)
session[:errors] = nil
if mandatory_questions_with_no_response.empty? && @case_log.update(responses_for_page)
session[:errors] = session[:fields] = nil
redirect_to(successful_redirect_path) redirect_to(successful_redirect_path)
else else
redirect_path = "case_log_#{@page.id}_path" redirect_path = "case_log_#{@page.id}_path"
mandatory_questions_with_no_response.map do |question|
@case_log.errors.add question.id.to_sym, question.unanswered_error_message
end
session[:errors] = @case_log.errors.to_json session[:errors] = @case_log.errors.to_json
Rails.logger.info "User triggered validation(s) on: #{@case_log.errors.map(&:attribute).join(', ')}" Rails.logger.info "User triggered validation(s) on: #{@case_log.errors.map(&:attribute).join(', ')}"
redirect_to(send(redirect_path, @case_log)) redirect_to(send(redirect_path, @case_log))
@ -48,6 +53,7 @@ class FormController < ApplicationController
messages.each { |message| @case_log.errors.add field.to_sym, message } messages.each { |message| @case_log.errors.add field.to_sym, message }
end end
end end
session["fields"].each { |field, value| @case_log[field] = value } if session["fields"]
@subsection = @case_log.form.subsection_for_page(page) @subsection = @case_log.form.subsection_for_page(page)
@page = @case_log.form.get_page(page.id) @page = @case_log.form.get_page(page.id)
if @page.routed_to?(@case_log, current_user) if @page.routed_to?(@case_log, current_user)
@ -120,4 +126,31 @@ private
redirect_path = @case_log.form.next_page_redirect_path(@page, @case_log, current_user) redirect_path = @case_log.form.next_page_redirect_path(@page, @case_log, current_user)
send(redirect_path, @case_log) send(redirect_path, @case_log)
end end
def mandatory_questions_with_no_response(responses_for_page)
session["fields"] = {}
calc_questions = @page.questions.map(&:result_field)
@page.questions.select do |question|
next if calc_questions.include?(question.id)
question_is_required?(question) && question_missing_response?(responses_for_page, question)
end
end
def question_is_required?(question)
CaseLog::OPTIONAL_FIELDS.exclude?(question.id) &&
@page.subsection.applicable_questions(@case_log).map(&:id).include?(question.id)
end
def question_missing_response?(responses_for_page, question)
if %w[checkbox validation_override].include?(question.type)
question.answer_options.keys.reject { |x| x.match(/divider/) }.all? do |option|
session["fields"][option] = @case_log[option] = params["case_log"][question.id].include?(option) ? 1 : 0
params["case_log"][question.id].exclude?(option)
end
else
session["fields"][question.id] = @case_log[question.id] = responses_for_page[question.id]
responses_for_page[question.id].nil? || responses_for_page[question.id].blank?
end
end
end end

2
app/controllers/locations_controller.rb

@ -69,7 +69,7 @@ private
end end
def location_params def location_params
required_params = params.require(:location).permit(:postcode, :name, :units, :type_of_unit, :wheelchair_adaptation, :add_another_location).merge(scheme_id: @scheme.id) required_params = params.require(:location).permit(:postcode, :name, :units, :type_of_unit, :wheelchair_adaptation, :add_another_location, :startdate).merge(scheme_id: @scheme.id)
required_params[:postcode] = PostcodeService.clean(required_params[:postcode]) if required_params[:postcode] required_params[:postcode] = PostcodeService.clean(required_params[:postcode]) if required_params[:postcode]
required_params required_params
end end

2
app/frontend/controllers/accessible_autocomplete_controller.js

@ -15,7 +15,7 @@ export default class extends Controller {
accessibleAutocomplete.enhanceSelectElement({ accessibleAutocomplete.enhanceSelectElement({
defaultValue: '', defaultValue: '',
selectElement: selectEl, selectElement: selectEl,
minLength: 2, minLength: 1,
source: (query, populateResults) => { source: (query, populateResults) => {
if (/\S/.test(query)) { if (/\S/.test(query)) {
populateResults(sort(query, options)) populateResults(sort(query, options))

28
app/frontend/modules/search.js

@ -12,9 +12,13 @@ const clean = (text) =>
.toLowerCase() .toLowerCase()
const cleanseOption = (option) => { const cleanseOption = (option) => {
const synonyms = (option.synonyms || []).map(clean)
option.clean = { option.clean = {
name: clean(option.name), name: clean(option.name),
nameWithoutStopWords: removeStopWords(option.name), nameWithoutStopWords: removeStopWords(option.name),
synonyms,
synonymsWithoutStopWords: synonyms.map(removeStopWords),
boost: option.boost || 1 boost: option.boost || 1
} }
@ -41,19 +45,34 @@ const startsWith = (word, query) => word.search(startsWithRegExp(query)) === 0
const wordsStartsWithQuery = (word, regExps) => const wordsStartsWithQuery = (word, regExps) =>
regExps.every((regExp) => word.search(regExp) >= 0) regExps.every((regExp) => word.search(regExp) >= 0)
const calculateWeight = ({ name, nameWithoutStopWords }, query) => { const anyMatch = (words, query, evaluatorFunc) => words.some((word) => evaluatorFunc(word, query))
const synonymsExactMatch = (synonyms, query) => anyMatch(synonyms, query, exactMatch)
const synonymsStartsWith = (synonyms, query) => anyMatch(synonyms, query, startsWith)
const wordInSynonymStartsWithQuery = (synonyms, startsWithQueryWordsRegexes) =>
anyMatch(synonyms, startsWithQueryWordsRegexes, wordsStartsWithQuery)
const calculateWeight = ({ name, synonyms, nameWithoutStopWords, synonymsWithoutStopWords }, query) => {
const queryWithoutStopWords = removeStopWords(query) const queryWithoutStopWords = removeStopWords(query)
if (exactMatch(name, query)) return 100 if (exactMatch(name, query)) return 100
if (exactMatch(nameWithoutStopWords, queryWithoutStopWords)) return 95 if (exactMatch(nameWithoutStopWords, queryWithoutStopWords)) return 95
if (synonymsExactMatch(synonyms, query)) return 75
if (synonymsExactMatch(synonymsWithoutStopWords, queryWithoutStopWords)) return 70
if (startsWith(name, query)) return 60 if (startsWith(name, query)) return 60
if (startsWith(nameWithoutStopWords, queryWithoutStopWords)) return 55 if (startsWith(nameWithoutStopWords, queryWithoutStopWords)) return 55
if (synonymsStartsWith(synonyms, query)) return 50
if (synonymsStartsWith(synonyms, queryWithoutStopWords)) return 40
const startsWithRegExps = queryWithoutStopWords const startsWithRegExps = queryWithoutStopWords
.split(/\s+/) .split(/\s+/)
.map(startsWithRegExp) .map(startsWithRegExp)
if (wordsStartsWithQuery(nameWithoutStopWords, startsWithRegExps)) return 25 if (wordsStartsWithQuery(nameWithoutStopWords, startsWithRegExps)) return 25
if (wordInSynonymStartsWithQuery(synonymsWithoutStopWords, startsWithRegExps)) return 10
return 0 return 0
} }
@ -91,8 +110,8 @@ export const sort = (query, options) => {
export const suggestion = (value, options) => { export const suggestion = (value, options) => {
const option = options.find((o) => o.name === value) const option = options.find((o) => o.name === value)
if (option) { if (option) {
const html = `<span>${value}</span>` const html = option.append ? `<span>${value}</span> <span class="autocomplete__option__append">${option.append}</span>` : `<span>${value}</span>`
return html return option.hint ? `${html}<div class="autocomplete__option__hint">${option.hint}</div>` : html
} else { } else {
return '<span>No results found</span>' return '<span>No results found</span>'
} }
@ -101,6 +120,9 @@ export const suggestion = (value, options) => {
export const enhanceOption = (option) => { export const enhanceOption = (option) => {
return { return {
name: option.label, name: option.label,
synonyms: (option.getAttribute('data-synonyms') ? option.getAttribute('data-synonyms').split('|') : []),
append: option.getAttribute('data-append'),
hint: option.getAttribute('data-hint'),
boost: parseFloat(option.getAttribute('data-boost')) || 1 boost: parseFloat(option.getAttribute('data-boost')) || 1
} }
} }

34
app/models/case_log.rb

@ -8,7 +8,6 @@ class CaseLogValidator < ActiveModel::Validator
include Validations::TenancyValidations include Validations::TenancyValidations
include Validations::DateValidations include Validations::DateValidations
include Validations::LocalAuthorityValidations include Validations::LocalAuthorityValidations
include Validations::SubmissionValidations
def validate(record) def validate(record)
validation_methods = public_methods.select { |method| method.starts_with?("validate_") } validation_methods = public_methods.select { |method| method.starts_with?("validate_") }
@ -103,6 +102,22 @@ class CaseLog < ApplicationRecord
attribute_names - AUTOGENERATED_FIELDS attribute_names - AUTOGENERATED_FIELDS
end end
def la
if location
location.location_code
else
super
end
end
def postcode_full
if location
location.postcode
else
super
end
end
def completed? def completed?
status == "completed" status == "completed"
end end
@ -466,7 +481,7 @@ class CaseLog < ApplicationRecord
private private
PIO = Postcodes::IO.new PIO = PostcodeService.new
def update_status! def update_status!
self.status = if all_fields_completed? && errors.empty? self.status = if all_fields_completed? && errors.empty?
@ -588,20 +603,7 @@ private
end end
def get_inferred_la(postcode) def get_inferred_la(postcode)
# Avoid network calls when postcode is invalid PIO.infer_la(postcode)
return unless postcode.match(POSTCODE_REGEXP)
postcode_lookup = nil
begin
# URI encoding only supports ASCII characters
ascii_postcode = PostcodeService.clean(postcode)
Timeout.timeout(5) { postcode_lookup = PIO.lookup(ascii_postcode) }
rescue Timeout::Error
Rails.logger.warn("Postcodes.io lookup timed out")
end
if postcode_lookup && postcode_lookup.info.present?
postcode_lookup.codes["admin_district"]
end
end end
def get_has_benefits def get_has_benefits

6
app/models/derived_variables/case_log_variables.rb

@ -73,10 +73,8 @@ module DerivedVariables::CaseLogVariables
self.location = scheme.locations.first self.location = scheme.locations.first
end end
if location if location
self.la = location.county # TODO: Remove and replace with mobility type
self.postcode_full = location.postcode self.wchair = location.wheelchair_adaptation_before_type_cast
wheelchair_adaptation_map = { 1 => 1, 0 => 2 }
self.wchair = wheelchair_adaptation_map[location.wheelchair_adaptation.to_i]
end end
if is_renewal? if is_renewal?
self.voiddate = startdate self.voiddate = startdate

32
app/models/form/question.rb

@ -136,7 +136,11 @@ class Form::Question
labels = answer_options[value.to_s] labels = answer_options[value.to_s]
labels["value"] if labels labels["value"] if labels
when "select" when "select"
if answer_options[value.to_s].respond_to?(:service_name)
answer_options[value.to_s].service_name
else
answer_options[value.to_s] answer_options[value.to_s]
end
else else
value.to_s value.to_s
end end
@ -173,6 +177,16 @@ class Form::Question
type == "radio" && RADIO_REFUSED_VALUE[id.to_sym]&.include?(value) type == "radio" && RADIO_REFUSED_VALUE[id.to_sym]&.include?(value)
end end
def display_label
check_answer_label || header || id.humanize
end
def unanswered_error_message
return I18n.t("validations.declaration.missing") if id == "declaration"
I18n.t("validations.not_answered", question: display_label.downcase)
end
def suffix_label(case_log) def suffix_label(case_log)
return "" unless suffix return "" unless suffix
return suffix if suffix.is_a?(String) return suffix if suffix.is_a?(String)
@ -191,6 +205,24 @@ class Form::Question
label label
end end
def answer_option_synonyms(resource)
return unless resource.respond_to?(:synonyms)
resource.synonyms
end
def answer_option_append(resource)
return unless resource.respond_to?(:appended_text)
resource.appended_text
end
def answer_option_hint(resource)
return unless resource.respond_to?(:hint)
resource.hint
end
private private
def selected_answer_option_is_derived?(case_log) def selected_answer_option_is_derived?(case_log)

2
app/models/form/setup/questions/location_id.rb

@ -13,7 +13,7 @@ class Form::Setup::Questions::LocationId < ::Form::Question
answer_opts = {} answer_opts = {}
return answer_opts unless ActiveRecord::Base.connected? return answer_opts unless ActiveRecord::Base.connected?
Location.select(:id, :postcode, :name).each_with_object(answer_opts) do |location, hsh| Location.select(:id, :postcode, :name).where("startdate <= ? or startdate IS NULL", Time.zone.today).each_with_object(answer_opts) do |location, hsh|
hsh[location.id.to_s] = { "value" => location.postcode, "hint" => location.name } hsh[location.id.to_s] = { "value" => location.postcode, "hint" => location.name }
hsh hsh
end end

6
app/models/form/setup/questions/scheme_id.rb

@ -13,8 +13,8 @@ class Form::Setup::Questions::SchemeId < ::Form::Question
answer_opts = {} answer_opts = {}
return answer_opts unless ActiveRecord::Base.connected? return answer_opts unless ActiveRecord::Base.connected?
Scheme.select(:id, :service_name).each_with_object(answer_opts) do |scheme, hsh| Scheme.select(:id, :service_name, :primary_client_group, :secondary_client_group).each_with_object(answer_opts) do |scheme, hsh|
hsh[scheme.id.to_s] = scheme.service_name hsh[scheme.id.to_s] = scheme
hsh hsh
end end
end end
@ -22,7 +22,7 @@ class Form::Setup::Questions::SchemeId < ::Form::Question
def displayed_answer_options(case_log) def displayed_answer_options(case_log)
return {} unless case_log.created_by return {} unless case_log.created_by
user_org_scheme_ids = Scheme.select(:id).where(owning_organisation_id: case_log.created_by.organisation_id).map(&:id) user_org_scheme_ids = Scheme.select(:id).where(owning_organisation_id: case_log.created_by.organisation_id).joins(:locations).merge(Location.where("startdate <= ? or startdate IS NULL", Time.zone.today)).map(&:id)
answer_options.select do |k, _v| answer_options.select do |k, _v|
user_org_scheme_ids.include?(k.to_i) user_org_scheme_ids.include?(k.to_i)
end end

20
app/models/location.rb

@ -2,15 +2,27 @@ class Location < ApplicationRecord
validate :validate_postcode validate :validate_postcode
belongs_to :scheme belongs_to :scheme
before_save :infer_la!, if: :postcode_changed?
attr_accessor :add_another_location attr_accessor :add_another_location
WHEELCHAIR_ADAPTATIONS = { WHEELCHAIR_ADAPTATIONS = {
Yes: 1, Yes: 1,
No: 0, No: 2,
}.freeze }.freeze
enum wheelchair_adaptation: WHEELCHAIR_ADAPTATIONS enum wheelchair_adaptation: WHEELCHAIR_ADAPTATIONS
MOBILITY_TYPE = {
"Property fitted with equipment and adaptations (if not designed to above standards)": "A",
"Property designed to accessible general standard": "M",
"None": "N",
"Property designed to wheelchair user standard": "W",
"Missing": "X",
}.freeze
enum mobility_type: MOBILITY_TYPE
TYPE_OF_UNIT = { TYPE_OF_UNIT = {
"Self-contained flat or bedsit": 1, "Self-contained flat or bedsit": 1,
"Self-contained flat or bedsit with common facilities": 2, "Self-contained flat or bedsit with common facilities": 2,
@ -34,10 +46,16 @@ class Location < ApplicationRecord
private private
PIO = PostcodeService.new
def validate_postcode def validate_postcode
if postcode.nil? || !postcode&.match(POSTCODE_REGEXP) if postcode.nil? || !postcode&.match(POSTCODE_REGEXP)
error_message = I18n.t("validations.postcode") error_message = I18n.t("validations.postcode")
errors.add :postcode, error_message errors.add :postcode, error_message
end end
end end
def infer_la!
self.location_code = PIO.infer_la(postcode)
end
end end

12
app/models/scheme.rb

@ -142,4 +142,16 @@ class Scheme < ApplicationRecord
{ name: "Intended length of stay", value: intended_stay }, { name: "Intended length of stay", value: intended_stay },
] ]
end end
def synonyms
locations.map(&:postcode).join(",")
end
def appended_text
"(#{locations.count { |location| location.startdate.blank? || location.startdate <= Time.zone.today }} locations)"
end
def hint
[primary_client_group, secondary_client_group].filter(&:present?).join(", ")
end
end end

10
app/models/validations/submission_validations.rb

@ -1,10 +0,0 @@
module Validations::SubmissionValidations
# Validations methods need to be called 'validate_<page_name>' to run on model save
# or 'validate_' to run on submit as well
def validate_declaration(record)
if record.declaration&.zero?
record.errors.add :declaration, I18n.t("validations.declaration.missing")
end
end
end

68
app/services/imports/case_logs_import_service.rb

@ -15,6 +15,12 @@ module Imports
private private
FORM_NAME_INDEX = {
start_year: 0,
rent_type: 2,
needs_type: 3,
}.freeze
GN_SH = { GN_SH = {
general_needs: 1, general_needs: 1,
supported_housing: 2, supported_housing: 2,
@ -175,15 +181,20 @@ module Imports
attributes["first_time_property_let_as_social_housing"] = first_time_let(attributes["rsnvac"]) attributes["first_time_property_let_as_social_housing"] = first_time_let(attributes["rsnvac"])
attributes["declaration"] = declaration(xml_doc) attributes["declaration"] = declaration(xml_doc)
# Set charges to 0 if others are partially populated set_partial_charges_to_zero(attributes)
unless attributes["brent"].nil? &&
attributes["scharge"].nil? && # Supported Housing fields
attributes["pscharge"].nil? && if attributes["needstype"] == GN_SH[:supported_housing]
attributes["supcharg"].nil? old_visible_id = safe_string_as_integer(xml_doc, "_1cschemecode")
attributes["brent"] ||= BigDecimal("0.0") location = Location.find_by(old_visible_id:)
attributes["scharge"] ||= BigDecimal("0.0") scheme = location.scheme
attributes["pscharge"] ||= BigDecimal("0.0") # Set the scheme via location, because the scheme old visible ID is not unique
attributes["supcharg"] ||= BigDecimal("0.0") attributes["location_id"] = location.id
attributes["scheme_id"] = scheme.id
attributes["sheltered"] = unsafe_string_as_integer(xml_doc, "Q1e")
attributes["chcharge"] = safe_string_as_decimal(xml_doc, "Q18b")
attributes["household_charge"] = household_charge(xml_doc)
attributes["is_carehome"] = is_carehome(scheme)
end end
# Handles confidential schemes # Handles confidential schemes
@ -306,7 +317,7 @@ module Imports
end end
def needs_type(xml_doc) def needs_type(xml_doc)
gn_sh = get_form_name_component(xml_doc, -1) gn_sh = get_form_name_component(xml_doc, FORM_NAME_INDEX[:needs_type])
case gn_sh case gn_sh
when "GN" when "GN"
GN_SH[:general_needs] GN_SH[:general_needs]
@ -319,7 +330,7 @@ module Imports
# This does not match renttype (CDS) which is derived by case log logic # This does not match renttype (CDS) which is derived by case log logic
def rent_type(xml_doc, lar, irproduct) def rent_type(xml_doc, lar, irproduct)
sr_ar_ir = get_form_name_component(xml_doc, -2) sr_ar_ir = get_form_name_component(xml_doc, FORM_NAME_INDEX[:rent_type])
case sr_ar_ir case sr_ar_ir
when "SR" when "SR"
@ -590,5 +601,40 @@ module Imports
end end
end end
end end
def household_charge(xml_doc)
value = string_or_nil(xml_doc, "Q18c")
start_year = Integer(get_form_name_component(xml_doc, FORM_NAME_INDEX[:start_year]))
if start_year <= 2021
# Yes means that there are no charges (2021 or earlier)
value && value.include?("Yes") ? 1 : 0
else
# Yes means that there are charges (2022 onwards)
value && value.include?("Yes") ? 0 : 1
end
end
def set_partial_charges_to_zero(attributes)
unless attributes["brent"].nil? &&
attributes["scharge"].nil? &&
attributes["pscharge"].nil? &&
attributes["supcharg"].nil?
attributes["brent"] ||= BigDecimal("0.0")
attributes["scharge"] ||= BigDecimal("0.0")
attributes["pscharge"] ||= BigDecimal("0.0")
attributes["supcharg"] ||= BigDecimal("0.0")
end
end
def is_carehome(scheme)
return nil unless scheme
if [2, 3, 4].include?(scheme.registered_under_care_act_before_type_cast)
1
else
0
end
end
end end
end end

45
app/services/imports/scheme_import_service.rb

@ -5,20 +5,18 @@ module Imports
end end
def create_scheme(xml_document) def create_scheme(xml_document)
old_id = string_or_nil(xml_document, "id") attributes = scheme_attributes(xml_document)
status = string_or_nil(xml_document, "status") if attributes["status"] == "Approved"
if status == "Approved"
Scheme.create!( Scheme.create!(
owning_organisation_id: find_owning_organisation_id(xml_document), owning_organisation_id: attributes["owning_organisation_id"],
managing_organisation_id: find_managing_organisation_id(xml_document), managing_organisation_id: attributes["managing_organisation_id"],
service_name: string_or_nil(xml_document, "name"), service_name: attributes["service_name"],
arrangement_type: string_or_nil(xml_document, "arrangement_type"), arrangement_type: attributes["arrangement_type"],
old_id:, old_id: attributes["old_id"],
old_visible_id: safe_string_as_integer(xml_document, "visible-id"), old_visible_id: attributes["old_visible_id"],
) )
else else
@logger.warn("Scheme with legacy ID #{old_id} is not approved (#{status}), skipping") @logger.warn("Scheme with legacy ID #{attributes['old_id']} is not approved (#{attributes['status']}), skipping")
end end
end end
@ -39,16 +37,33 @@ module Imports
Integer(str, exception: false) Integer(str, exception: false)
end end
def find_owning_organisation_id(xml_doc) def scheme_attributes(xml_doc)
old_org_id = string_or_nil(xml_doc, "institution") attributes = {}
attributes["old_id"] = string_or_nil(xml_doc, "id")
attributes["old_visible_id"] = string_or_nil(xml_doc, "visible-id")
attributes["status"] = string_or_nil(xml_doc, "status")
attributes["service_name"] = string_or_nil(xml_doc, "name")
attributes["arrangement_type"] = string_or_nil(xml_doc, "arrangement_type")
attributes["owning_org_old_id"] = string_or_nil(xml_doc, "institution")
attributes["owning_organisation_id"] = find_owning_organisation_id(attributes["owning_org_old_id"])
attributes["management_org_old_visible_id"] = safe_string_as_integer(xml_doc, "agent")
attributes["managing_organisation_id"] = find_managing_organisation_id(attributes["management_org_old_visible_id"])
if attributes["arrangement_type"] == "D" && attributes["managing_organisation_id"].nil?
attributes["managing_organisation_id"] = attributes["owning_organisation_id"]
end
attributes
end
def find_owning_organisation_id(old_org_id)
organisation = Organisation.find_by(old_org_id:) organisation = Organisation.find_by(old_org_id:)
raise "Organisation not found with legacy ID #{old_org_id}" if organisation.nil? raise "Organisation not found with legacy ID #{old_org_id}" if organisation.nil?
organisation.id organisation.id
end end
def find_managing_organisation_id(xml_doc) def find_managing_organisation_id(old_visible_id)
old_visible_id = safe_string_as_integer(xml_doc, "agent")
return unless old_visible_id return unless old_visible_id
organisation = Organisation.find_by(old_visible_id:) organisation = Organisation.find_by(old_visible_id:)

13
app/services/imports/scheme_location_import_service.rb

@ -67,11 +67,13 @@ module Imports
attributes["registered_under_care_act"] = registered_under_care_act.zero? ? nil : registered_under_care_act attributes["registered_under_care_act"] = registered_under_care_act.zero? ? nil : registered_under_care_act
attributes["support_type"] = safe_string_as_integer(xml_doc, "support-type") attributes["support_type"] = safe_string_as_integer(xml_doc, "support-type")
attributes["intended_stay"] = string_or_nil(xml_doc, "intended-stay") attributes["intended_stay"] = string_or_nil(xml_doc, "intended-stay")
attributes["mobility_type"] = string_or_nil(xml_doc, "mobility-type")
attributes["primary_client_group"] = string_or_nil(xml_doc, "client-group-1") attributes["primary_client_group"] = string_or_nil(xml_doc, "client-group-1")
attributes["secondary_client_group"] = string_or_nil(xml_doc, "client-group-2") attributes["secondary_client_group"] = string_or_nil(xml_doc, "client-group-2")
attributes["secondary_client_group"] = nil if attributes["primary_client_group"] == attributes["secondary_client_group"] attributes["secondary_client_group"] = nil if attributes["primary_client_group"] == attributes["secondary_client_group"]
attributes["sensitive"] = sensitive(xml_doc) attributes["sensitive"] = sensitive(xml_doc)
attributes["end_date"] = parse_end_date(xml_doc) attributes["start_date"] = parse_date(xml_doc, "start-date")
attributes["end_date"] = parse_date(xml_doc, "end-date")
attributes["location_name"] = string_or_nil(xml_doc, "name") attributes["location_name"] = string_or_nil(xml_doc, "name")
attributes["postcode"] = string_or_nil(xml_doc, "postcode") attributes["postcode"] = string_or_nil(xml_doc, "postcode")
attributes["units"] = safe_string_as_integer(xml_doc, "total-units") attributes["units"] = safe_string_as_integer(xml_doc, "total-units")
@ -84,15 +86,16 @@ module Imports
def add_location(scheme, attributes) def add_location(scheme, attributes)
if attributes["end_date"].nil? || attributes["end_date"] >= Time.zone.now if attributes["end_date"].nil? || attributes["end_date"] >= Time.zone.now
# wheelchair_adaptation: string_or_nil(xml_doc, "mobility-type"),
begin begin
Location.create!( Location.create!(
name: attributes["location_name"], name: attributes["location_name"],
postcode: attributes["postcode"], postcode: attributes["postcode"],
mobility_type: attributes["mobility_type"],
units: attributes["units"], units: attributes["units"],
type_of_unit: attributes["type_of_unit"], type_of_unit: attributes["type_of_unit"],
old_visible_id: attributes["location_old_visible_id"], old_visible_id: attributes["location_old_visible_id"],
old_id: attributes["location_old_id"], old_id: attributes["location_old_id"],
startdate: attributes["start_date"],
scheme:, scheme:,
) )
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
@ -185,9 +188,9 @@ module Imports
end end
end end
def parse_end_date(xml_doc) def parse_date(xml_doc, attribute)
end_date = string_or_nil(xml_doc, "end-date") date = string_or_nil(xml_doc, attribute)
Time.zone.parse(end_date) if end_date Time.zone.parse(date) if date
end end
end end
end end

21
app/services/postcode_service.rb

@ -1,4 +1,25 @@
class PostcodeService class PostcodeService
def initialize
@pio = Postcodes::IO.new
end
def infer_la(postcode)
# Avoid network calls when postcode is invalid
return unless postcode.match(POSTCODE_REGEXP)
postcode_lookup = nil
begin
# URI encoding only supports ASCII characters
ascii_postcode = self.class.clean(postcode)
Timeout.timeout(5) { postcode_lookup = @pio.lookup(ascii_postcode) }
rescue Timeout::Error
Rails.logger.warn("Postcodes.io lookup timed out")
end
if postcode_lookup && postcode_lookup.info.present?
postcode_lookup.codes["admin_district"]
end
end
def self.clean(postcode) def self.clean(postcode)
postcode.encode("ASCII", "UTF-8", invalid: :replace, undef: :replace, replace: "").delete(" ").upcase postcode.encode("ASCII", "UTF-8", invalid: :replace, undef: :replace, replace: "").delete(" ").upcase
end end

23
app/views/form/_select_question.html.erb

@ -1,13 +1,18 @@
<%= render partial: "form/guidance/#{question.guidance_partial}" if question.guidance_partial %> <%= render partial: "form/guidance/#{question.guidance_partial}" if question.guidance_partial %>
<% selected = @case_log.public_send(question.id) || "" %> <% selected = @case_log.public_send(question.id) || "" %>
<% answers = question.displayed_answer_options(@case_log).map { |key, value| OpenStruct.new(id: key, name: value) } %> <% answers = question.displayed_answer_options(@case_log).map { |key, value| OpenStruct.new(id: key, name: value.respond_to?(:service_name) ? value.service_name : nil, resource: value) } %>
<%= f.govuk_collection_select question.id.to_sym, <%= f.govuk_select(question.id.to_sym,
answers,
:id,
:name,
caption: caption(caption_text, page_header, conditional),
label: legend(question, page_header, conditional), label: legend(question, page_header, conditional),
hint: { text: question.hint_text&.html_safe }, "data-controller": "accessible-autocomplete",
options: { disabled: [""], selected: }, caption: caption(caption_text, page_header, conditional),
"data-controller": "accessible-autocomplete" %> hint: { text: question.hint_text&.html_safe }) do %>
<% answers.each do |answer| %>
<option value="<%= answer.id %>"
data-synonyms="<%= question.answer_option_synonyms(answer.resource) %>"
data-append="<%= question.answer_option_append(answer.resource) %>"
data-hint="<%= question.answer_option_hint(answer.resource) %>"
<%= @case_log[question.id] == answer.name || @case_log[question.id] == answer.resource || @case_log[question.id].to_s == answer.id ? "selected" : "" %>
<%= answer.id == "" ? "disabled" : "" %>><%= answer.name || answer.resource %></option>
<% end %>
<% end %>

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

@ -5,7 +5,6 @@
<% end %> <% end %>
<div data-controller="govukfrontend"></div> <div data-controller="govukfrontend"></div>
<%= form_with model: @case_log, url: form_case_log_path(@case_log), method: "post", local: true do |f| %> <%= form_with model: @case_log, url: form_case_log_path(@case_log), method: "post", local: true do |f| %>
<div class="govuk-grid-row"> <div class="govuk-grid-row">
<div class="govuk-grid-column-<%= @page.questions[0].type == "interruption_screen" ? "full-from-desktop" : "two-thirds-from-desktop" %>"> <div class="govuk-grid-column-<%= @page.questions[0].type == "interruption_screen" ? "full-from-desktop" : "two-thirds-from-desktop" %>">
@ -39,9 +38,17 @@
<% end %> <% end %>
<%= f.hidden_field :page, value: @page.id %> <%= f.hidden_field :page, value: @page.id %>
<% if !@page.id.include?("value_check") %>
<div class="govuk-button-group">
<% if !@page.id.include?("value_check") && if request.query_parameters["referrer"] != "check_answers" %>
<%= f.govuk_submit "Save and continue" %> <%= f.govuk_submit "Save and continue" %>
<%= govuk_link_to "Skip for now", send(@case_log.form.next_page_redirect_path(@page, @case_log, current_user), @case_log) %>
<% else %>
<%= f.govuk_submit "Save changes" %>
<%= govuk_link_to "Cancel", "/logs/#{@case_log.id}/setup/check-answers" %>
<% end %> <% end %>
<% end %>
</div>
</div> </div>
</div> </div>
<% end %> <% end %>

22
app/views/locations/edit.html.erb

@ -16,17 +16,17 @@
<%= f.govuk_text_field :postcode, <%= f.govuk_text_field :postcode,
label: { size: "m" }, label: { size: "m" },
hint: { text: "For example, SW1P 4DF." }, hint: { text: I18n.t("hints.location.postcode") },
width: 5 %> width: 5 %>
<%= f.govuk_text_field :name, <%= f.govuk_text_field :name,
label: { text: "Location name (optional)", size: "m" }, label: { text: I18n.t("questions.location.name"), size: "m" },
hint: { text: "This is how you refer to this location within your organisation" } %> hint: { text: I18n.t("hints.location.name") } %>
<%= f.govuk_number_field :units, <%= f.govuk_number_field :units,
label: { text: "Total number of units at this location", size: "m" }, label: { text: I18n.t("questions.location.units"), size: "m" },
width: 2, width: 2,
hint: { text: "A unit can be a bedroom in a shared house or flat, or a house with 4 bedrooms. Do not include bedrooms used for wardens, managers, volunteers or sleep-in staff.s" }, hint: { text: I18n.t("hints.location.units") },
autofocus: true %> autofocus: true %>
<% type_of_units_selection = Location.type_of_units.keys.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %> <% type_of_units_selection = Location.type_of_units.keys.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
@ -34,15 +34,19 @@
type_of_units_selection, type_of_units_selection,
:id, :id,
:name, :name,
legend: { text: "What is this type of scheme?", size: "m" } %> legend: { text: I18n.t("questions.location.type_of_unit"), size: "m" } %>
<% wheelchair_user_selection = Location.wheelchair_adaptations.keys.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %> <% wheelchair_user_selection = Location.wheelchair_adaptations.keys.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
<%= f.govuk_collection_radio_buttons :wheelchair_adaptation, <%= f.govuk_collection_radio_buttons :wheelchair_adaptation,
wheelchair_user_selection, wheelchair_user_selection,
:id, :id,
:name, :name,
hint: { text: "This includes stairlifts, ramps, level-access showers or grab rails" }, hint: { text: I18n.t("hints.location.wheelchair_adaptation") },
legend: { text: "Are the majority of units in this location built or adapted to wheelchair-user standards?", size: "m" } %> legend: { text: I18n.t("questions.location.wheelchair_adaptation"), size: "m" } %>
<%= f.govuk_date_field :startdate,
legend: { text: I18n.t("questions.location.startdate"), size: "m" },
width: 20 %>
<%= govuk_section_break(visible: true, size: "m") %> <%= govuk_section_break(visible: true, size: "m") %>
@ -52,7 +56,7 @@
:id, :id,
:name, :name,
inline: true, inline: true,
legend: { text: "Do you want to add another location?", size: "m" } %> legend: { text: I18n.t("questions.location.add_another_location"), size: "m" } %>
<%= f.hidden_field :page, value: "edit" %> <%= f.hidden_field :page, value: "edit" %>

22
app/views/locations/new.html.erb

@ -16,17 +16,17 @@
<%= f.govuk_text_field :postcode, <%= f.govuk_text_field :postcode,
label: { size: "m" }, label: { size: "m" },
hint: { text: "For example, SW1P 4DF." }, hint: { text: I18n.t("hints.location.postcode") },
width: 5 %> width: 5 %>
<%= f.govuk_text_field :name, <%= f.govuk_text_field :name,
label: { text: "Location name (optional)", size: "m" }, label: { text: I18n.t("questions.location.name"), size: "m" },
hint: { text: "This is how you refer to this location within your organisation" } %> hint: { text: I18n.t("hints.location.name") } %>
<%= f.govuk_number_field :units, <%= f.govuk_number_field :units,
label: { text: "Total number of units at this location", size: "m" }, label: { text: I18n.t("questions.location.units"), size: "m" },
width: 2, width: 2,
hint: { text: "A unit can be a bedroom in a shared house or flat, or a house with 4 bedrooms. Do not include bedrooms used for wardens, managers, volunteers or sleep-in staff.s" }, hint: { text: I18n.t("hints.location.units") },
autofocus: true %> autofocus: true %>
<% type_of_units_selection = Location.type_of_units.keys.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %> <% type_of_units_selection = Location.type_of_units.keys.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
@ -35,7 +35,7 @@
type_of_units_selection, type_of_units_selection,
:id, :id,
:name, :name,
legend: { text: "What is this type of scheme?", size: "m" } %> legend: { text: I18n.t("questions.location.type_of_unit"), size: "m" } %>
<% wheelchair_user_selection = Location.wheelchair_adaptations.keys.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %> <% wheelchair_user_selection = Location.wheelchair_adaptations.keys.map { |key, _| OpenStruct.new(id: key, name: key.to_s.humanize) } %>
@ -43,8 +43,12 @@
wheelchair_user_selection, wheelchair_user_selection,
:id, :id,
:name, :name,
hint: { text: "This includes stairlifts, ramps, level-access showers or grab rails" }, hint: { text: I18n.t("hints.location.wheelchair_adaptation") },
legend: { text: "Are the majority of units in this location built or adapted to wheelchair-user standards?", size: "m" } %> legend: { text: I18n.t("questions.location.wheelchair_adaptation"), size: "m" } %>
<%= f.govuk_date_field :startdate,
legend: { text: I18n.t("questions.location.startdate"), size: "m" },
width: 20 %>
<%= govuk_section_break(visible: true, size: "m") %> <%= govuk_section_break(visible: true, size: "m") %>
@ -55,7 +59,7 @@
:id, :id,
:name, :name,
inline: true, inline: true,
legend: { text: "Do you want to add another location?", size: "m" } %> legend: { text: I18n.t("questions.location.add_another_location"), size: "m" } %>
<%= f.govuk_submit "Save and continue" %> <%= f.govuk_submit "Save and continue" %>
</div> </div>

66
config/forms/2021_2022.json

@ -1078,7 +1078,8 @@
"step": 1, "step": 1,
"width": 2 "width": 2
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"no_females_pregnant_household_lead_hhmemb_value_check": { "no_females_pregnant_household_lead_hhmemb_value_check": {
"depends_on": [{ "no_females_in_a_pregnant_household?": true }], "depends_on": [{ "no_females_in_a_pregnant_household?": true }],
@ -1120,11 +1121,7 @@
} }
}, },
"females_in_soft_age_range_in_pregnant_household_lead_hhmemb_value_check": { "females_in_soft_age_range_in_pregnant_household_lead_hhmemb_value_check": {
"depends_on": [ "depends_on": [{ "female_in_pregnant_household_in_soft_validation_range?": true }],
{
"female_in_pregnant_household_in_soft_validation_range?": true
}
],
"title_text": { "title_text": {
"translation": "soft_validations.pregnancy.title", "translation": "soft_validations.pregnancy.title",
"arguments": [ "arguments": [
@ -1207,7 +1204,8 @@
"value": "Not known" "value": "Not known"
} }
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"no_females_pregnant_household_lead_age_value_check": { "no_females_pregnant_household_lead_age_value_check": {
"depends_on": [{ "no_females_in_a_pregnant_household?": true }], "depends_on": [{ "no_females_in_a_pregnant_household?": true }],
@ -1249,11 +1247,7 @@
} }
}, },
"females_in_soft_age_range_in_pregnant_household_lead_age_value_check": { "females_in_soft_age_range_in_pregnant_household_lead_age_value_check": {
"depends_on": [ "depends_on": [{ "female_in_pregnant_household_in_soft_validation_range?": true }],
{
"female_in_pregnant_household_in_soft_validation_range?": true
}
],
"title_text": { "title_text": {
"translation": "soft_validations.pregnancy.title", "translation": "soft_validations.pregnancy.title",
"arguments": [ "arguments": [
@ -1318,7 +1312,8 @@
} }
} }
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"no_females_pregnant_household_lead_value_check": { "no_females_pregnant_household_lead_value_check": {
"depends_on": [{ "no_females_in_a_pregnant_household?": true }], "depends_on": [{ "no_females_in_a_pregnant_household?": true }],
@ -1360,11 +1355,7 @@
} }
}, },
"females_in_soft_age_range_in_pregnant_household_lead_value_check": { "females_in_soft_age_range_in_pregnant_household_lead_value_check": {
"depends_on": [ "depends_on": [{ "female_in_pregnant_household_in_soft_validation_range?": true }],
{
"female_in_pregnant_household_in_soft_validation_range?": true
}
],
"title_text": { "title_text": {
"translation": "soft_validations.pregnancy.title", "translation": "soft_validations.pregnancy.title",
"arguments": [ "arguments": [
@ -1435,7 +1426,8 @@
} }
} }
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"lead_tenant_ethnic_background_arab": { "lead_tenant_ethnic_background_arab": {
"header": "", "header": "",
@ -1456,11 +1448,7 @@
} }
} }
}, },
"depends_on": [ "depends_on": [{ "ethnic_group": 4 }]
{
"ethnic_group": 4
}
]
}, },
"lead_tenant_ethnic_background_asian": { "lead_tenant_ethnic_background_asian": {
"header": "", "header": "",
@ -1490,11 +1478,7 @@
} }
} }
}, },
"depends_on": [ "depends_on": [{ "ethnic_group": 2 }]
{
"ethnic_group": 2
}
]
}, },
"lead_tenant_ethnic_background_black": { "lead_tenant_ethnic_background_black": {
"header": "", "header": "",
@ -1518,11 +1502,7 @@
} }
} }
}, },
"depends_on": [ "depends_on": [{ "ethnic_group": 3 }]
{
"ethnic_group": 3
}
]
}, },
"lead_tenant_ethnic_background_mixed": { "lead_tenant_ethnic_background_mixed": {
"header": "", "header": "",
@ -1549,11 +1529,7 @@
} }
} }
}, },
"depends_on": [ "depends_on": [{ "ethnic_group": 1 }]
{
"ethnic_group": 1
}
]
}, },
"lead_tenant_ethnic_background_white": { "lead_tenant_ethnic_background_white": {
"header": "", "header": "",
@ -1580,11 +1556,7 @@
} }
} }
}, },
"depends_on": [ "depends_on": [{ "ethnic_group": 0 }]
{
"ethnic_group": 0
}
]
}, },
"lead_tenant_nationality": { "lead_tenant_nationality": {
"header": "", "header": "",
@ -1650,7 +1622,8 @@
} }
} }
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"lead_tenant_working_situation": { "lead_tenant_working_situation": {
"header": "", "header": "",
@ -1697,7 +1670,8 @@
} }
} }
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"lead_tenant_under_retirement_value_check": { "lead_tenant_under_retirement_value_check": {
"depends_on": [{ "person_1_retired_under_soft_min_age?": true }], "depends_on": [{ "person_1_retired_under_soft_min_age?": true }],

70
config/forms/2022_2023.json

@ -1113,7 +1113,8 @@
"step": 1, "step": 1,
"width": 2 "width": 2
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"no_females_pregnant_household_lead_hhmemb_value_check": { "no_females_pregnant_household_lead_hhmemb_value_check": {
"depends_on": [{ "no_females_in_a_pregnant_household?": true }], "depends_on": [{ "no_females_in_a_pregnant_household?": true }],
@ -1155,11 +1156,7 @@
} }
}, },
"females_in_soft_age_range_in_pregnant_household_lead_hhmemb_value_check": { "females_in_soft_age_range_in_pregnant_household_lead_hhmemb_value_check": {
"depends_on": [ "depends_on": [{ "female_in_pregnant_household_in_soft_validation_range?": true }],
{
"female_in_pregnant_household_in_soft_validation_range?": true
}
],
"title_text": { "title_text": {
"translation": "soft_validations.pregnancy.title", "translation": "soft_validations.pregnancy.title",
"arguments": [ "arguments": [
@ -1242,7 +1239,8 @@
"value": "Not known" "value": "Not known"
} }
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"no_females_pregnant_household_lead_age_value_check": { "no_females_pregnant_household_lead_age_value_check": {
"depends_on": [{ "no_females_in_a_pregnant_household?": true }], "depends_on": [{ "no_females_in_a_pregnant_household?": true }],
@ -1284,11 +1282,7 @@
} }
}, },
"females_in_soft_age_range_in_pregnant_household_lead_age_value_check": { "females_in_soft_age_range_in_pregnant_household_lead_age_value_check": {
"depends_on": [ "depends_on": [{ "female_in_pregnant_household_in_soft_validation_range?": true }],
{
"female_in_pregnant_household_in_soft_validation_range?": true
}
],
"title_text": { "title_text": {
"translation": "soft_validations.pregnancy.title", "translation": "soft_validations.pregnancy.title",
"arguments": [ "arguments": [
@ -1353,7 +1347,8 @@
} }
} }
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"no_females_pregnant_household_lead_value_check": { "no_females_pregnant_household_lead_value_check": {
"depends_on": [{ "no_females_in_a_pregnant_household?": true }], "depends_on": [{ "no_females_in_a_pregnant_household?": true }],
@ -1395,11 +1390,7 @@
} }
}, },
"females_in_soft_age_range_in_pregnant_household_lead_value_check": { "females_in_soft_age_range_in_pregnant_household_lead_value_check": {
"depends_on": [ "depends_on": [{ "female_in_pregnant_household_in_soft_validation_range?": true }],
{
"female_in_pregnant_household_in_soft_validation_range?": true
}
],
"title_text": { "title_text": {
"translation": "soft_validations.pregnancy.title", "translation": "soft_validations.pregnancy.title",
"arguments": [ "arguments": [
@ -1470,7 +1461,8 @@
} }
} }
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"lead_tenant_ethnic_background_arab": { "lead_tenant_ethnic_background_arab": {
"header": "", "header": "",
@ -1491,11 +1483,7 @@
} }
} }
}, },
"depends_on": [ "depends_on": [{ "ethnic_group": 4 }]
{
"ethnic_group": 4
}
]
}, },
"lead_tenant_ethnic_background_asian": { "lead_tenant_ethnic_background_asian": {
"header": "", "header": "",
@ -1525,11 +1513,7 @@
} }
} }
}, },
"depends_on": [ "depends_on": [{ "ethnic_group": 2 }]
{
"ethnic_group": 2
}
]
}, },
"lead_tenant_ethnic_background_black": { "lead_tenant_ethnic_background_black": {
"header": "", "header": "",
@ -1553,11 +1537,7 @@
} }
} }
}, },
"depends_on": [ "depends_on": [{ "ethnic_group": 3 }]
{
"ethnic_group": 3
}
]
}, },
"lead_tenant_ethnic_background_mixed": { "lead_tenant_ethnic_background_mixed": {
"header": "", "header": "",
@ -1584,11 +1564,7 @@
} }
} }
}, },
"depends_on": [ "depends_on": [{ "ethnic_group": 1 }]
{
"ethnic_group": 1
}
]
}, },
"lead_tenant_ethnic_background_white": { "lead_tenant_ethnic_background_white": {
"header": "", "header": "",
@ -1615,11 +1591,7 @@
} }
} }
}, },
"depends_on": [ "depends_on": [{ "ethnic_group": 0 }]
{
"ethnic_group": 0
}
]
}, },
"lead_tenant_nationality": { "lead_tenant_nationality": {
"header": "", "header": "",
@ -1649,7 +1621,8 @@
} }
} }
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"lead_tenant_working_situation": { "lead_tenant_working_situation": {
"header": "", "header": "",
@ -1696,7 +1669,8 @@
} }
} }
} }
} },
"depends_on": [{ "declaration": 1 }]
}, },
"lead_tenant_under_retirement_value_check": { "lead_tenant_under_retirement_value_check": {
"depends_on": [{ "person_1_retired_under_soft_min_age?": true }], "depends_on": [{ "person_1_retired_under_soft_min_age?": true }],
@ -1743,9 +1717,7 @@
} }
}, },
"lead_tenant_over_retirement_value_check": { "lead_tenant_over_retirement_value_check": {
"depends_on": [ "depends_on": [{ "person_1_not_retired_over_soft_max_age?": true }],
{ "person_1_not_retired_over_soft_max_age?": true }
],
"title_text": { "title_text": {
"translation": "soft_validations.retirement.max.title", "translation": "soft_validations.retirement.max.title",
"arguments": [ "arguments": [

17
config/locales/en.yml

@ -51,6 +51,7 @@ en:
organisation: organisation:
name_missing: "Enter the organisation’s name" name_missing: "Enter the organisation’s name"
provider_type_missing: "Select the organisation’s type" provider_type_missing: "Select the organisation’s type"
not_answered: "You must answer %{question}"
other_field_missing: "If %{main_field_label} is other then %{other_field_label} must be provided" other_field_missing: "If %{main_field_label} is other then %{other_field_label} must be provided"
other_field_not_required: "%{other_field_label} must not be provided if %{main_field_label} was not other" other_field_not_required: "%{other_field_label} must not be provided if %{main_field_label} was not other"
@ -287,6 +288,22 @@ en:
code_required: "Security code is required" code_required: "Security code is required"
code_incorrect: "Security code is incorrect" code_incorrect: "Security code is incorrect"
questions:
location:
name: "Location name (optional)"
units: "Total number of units at this location"
type_of_unit: "What is this type of scheme?"
wheelchair_adaptation: "Are the majority of units in this location built or adapted to wheelchair-user standards?"
startdate: "When did the first property in this location become available under this scheme?"
add_another_location: "Do you want to add another location?"
hints:
location:
postcode: "For example, SW1P 4DF."
name: "This is how you refer to this location within your organisation"
units: "A unit can be a bedroom in a shared house or flat, or a house with 4 bedrooms. Do not include bedrooms used for wardens, managers, volunteers or sleep-in staff."
wheelchair_adaptation: "This includes stairlifts, ramps, level-access showers or grab rails"
test: test:
one_argument: "This is based on the tenant’s work situation: %{ecstat1}" one_argument: "This is based on the tenant’s work situation: %{ecstat1}"
title_text: title_text:

7
db/migrate/20220713095713_add_mobility_type_to_locations.rb

@ -0,0 +1,7 @@
class AddMobilityTypeToLocations < ActiveRecord::Migration[7.0]
def change
change_table :locations, bulk: true do |t|
t.column :mobility_type, :string
end
end
end

7
db/migrate/20220714080044_add_location_startdate.rb

@ -0,0 +1,7 @@
class AddLocationStartdate < ActiveRecord::Migration[7.0]
def change
change_table :locations, bulk: true do |t|
t.column :startdate, :datetime
end
end
end

5
db/migrate/20220715133937_remove_county_from_location.rb

@ -0,0 +1,5 @@
class RemoveCountyFromLocation < ActiveRecord::Migration[7.0]
def change
remove_column :locations, :county, :string
end
end

5
db/schema.rb

@ -11,7 +11,7 @@
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2022_07_12_143943) do ActiveRecord::Schema[7.0].define(version: 2022_07_15_133937) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -245,13 +245,14 @@ ActiveRecord::Schema[7.0].define(version: 2022_07_12_143943) do
t.integer "wheelchair_adaptation" t.integer "wheelchair_adaptation"
t.bigint "scheme_id", null: false t.bigint "scheme_id", null: false
t.string "name" t.string "name"
t.string "county"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "units" t.integer "units"
t.integer "type_of_unit" t.integer "type_of_unit"
t.string "old_id" t.string "old_id"
t.integer "old_visible_id" t.integer "old_visible_id"
t.string "mobility_type"
t.datetime "startdate", precision: nil
t.index ["old_id"], name: "index_locations_on_old_id", unique: true t.index ["old_id"], name: "index_locations_on_old_id", unique: true
t.index ["scheme_id"], name: "index_locations_on_scheme_id" t.index ["scheme_id"], name: "index_locations_on_scheme_id"
end end

5
spec/factories/location.rb

@ -1,12 +1,11 @@
FactoryBot.define do FactoryBot.define do
factory :location do factory :location do
location_code { Faker::Name.initials(number: 10) }
postcode { Faker::Address.postcode.delete(" ") } postcode { Faker::Address.postcode.delete(" ") }
name { Faker::Address.street_name } name { Faker::Address.street_name }
type_of_unit { [1, 2, 3, 4, 6, 7].sample } type_of_unit { [1, 2, 3, 4, 6, 7].sample }
type_of_building { "Purpose built" } type_of_building { "Purpose built" }
wheelchair_adaptation { 0 } mobility_type { %w[A M N W X].sample }
county { Faker::Address.state } wheelchair_adaptation { 2 }
scheme scheme
end end
end end

27
spec/features/form/accessible_autocomplete_spec.rb

@ -14,6 +14,7 @@ RSpec.describe "Accessible Automcomplete" do
is_la_inferred: false, is_la_inferred: false,
owning_organisation: user.organisation, owning_organisation: user.organisation,
managing_organisation: user.organisation, managing_organisation: user.organisation,
created_by: user,
) )
end end
@ -58,6 +59,32 @@ RSpec.describe "Accessible Automcomplete" do
end end
end end
context "when searching schemes" do
let(:scheme) { FactoryBot.create(:scheme, owning_organisation_id: case_log.created_by.organisation_id, primary_client_group: "Q", secondary_client_group: "P") }
before do
FactoryBot.create(:location, scheme:, postcode: "W6 0ST")
FactoryBot.create(:location, scheme:, postcode: "SE6 1LB")
case_log.update!(needstype: 2)
visit("/logs/#{case_log.id}/scheme")
end
it "can match on synonyms", js: true do
find("#case-log-scheme-id-field").click.native.send_keys("w", "6", :down, :enter)
expect(find("#case-log-scheme-id-field").value).to eq(scheme.service_name)
end
it "displays appended text next to the options", js: true do
find("#case-log-scheme-id-field").click.native.send_keys("w", "6", :down, :enter)
expect(find(".autocomplete__option__append", visible: :hidden, text: /(2 locations)/)).to be_present
end
it "displays hint text under the options", js: true do
find("#case-log-scheme-id-field").click.native.send_keys("w", "6", :down, :enter)
expect(find(".autocomplete__option__hint", visible: :hidden, text: /Young people at risk, Young people leaving care/)).to be_present
end
end
it "has the correct option selected if one has been saved" do it "has the correct option selected if one has been saved" do
case_log.update!(postcode_known: 0, previous_la_known: 1, prevloc: "E07000178") case_log.update!(postcode_known: 0, previous_la_known: 1, prevloc: "E07000178")
visit("/logs/#{case_log.id}/accessible-select") visit("/logs/#{case_log.id}/accessible-select")

2
spec/features/form/check_answers_page_spec.rb

@ -136,7 +136,7 @@ RSpec.describe "Form Check Answers Page" do
first("a", text: /Change/).click first("a", text: /Change/).click
uncheck("case-log-accessibility-requirements-housingneeds-c-field") uncheck("case-log-accessibility-requirements-housingneeds-c-field")
check("case-log-accessibility-requirements-housingneeds-b-field") check("case-log-accessibility-requirements-housingneeds-b-field")
click_button("Save and continue") click_button("Save changes")
expect(page).to have_current_path("/logs/#{empty_case_log.id}/household-needs/check-answers") expect(page).to have_current_path("/logs/#{empty_case_log.id}/household-needs/check-answers")
end end
end end

76
spec/features/form/form_navigation_spec.rb

@ -13,6 +13,15 @@ RSpec.describe "Form Navigation" do
created_by: user, created_by: user,
) )
end end
let(:empty_case_log) do
FactoryBot.create(
:case_log,
owning_organisation: user.organisation,
managing_organisation: user.organisation,
created_by: user,
)
end
let(:id) { case_log.id } let(:id) { case_log.id }
let(:question_answers) do let(:question_answers) do
{ {
@ -47,11 +56,23 @@ RSpec.describe "Form Navigation" do
pages = question_answers.map { |_key, val| val[:path] } pages = question_answers.map { |_key, val| val[:path] }
pages[0..-2].each_with_index do |val, index| pages[0..-2].each_with_index do |val, index|
visit("/logs/#{id}/#{val}") visit("/logs/#{id}/#{val}")
click_button("Save and continue") click_link("Skip for now")
expect(page).to have_current_path("/logs/#{id}/#{pages[index + 1]}") expect(page).to have_current_path("/logs/#{id}/#{pages[index + 1]}")
end end
end end
it "a question page has a Skip for now link that lets you move on to the next question without inputting anything" do
visit("logs/#{empty_case_log.id}/tenant-code-test")
click_link(text: "Skip for now")
expect(page).to have_current_path("/logs/#{empty_case_log.id}/person-1-age")
end
it "routes to check answers when skipping on the last page in the form" do
visit("logs/#{empty_case_log.id}/propcode")
click_link(text: "Skip for now")
expect(page).to have_current_path("/logs/#{empty_case_log.id}/household-characteristics/check-answers")
end
describe "Back link directs correctly", js: true do describe "Back link directs correctly", js: true do
it "go back to tasklist page from tenant code" do it "go back to tasklist page from tenant code" do
visit("/logs/#{id}") visit("/logs/#{id}")
@ -89,4 +110,57 @@ RSpec.describe "Form Navigation" do
end end
end end
end end
describe "Editing a log" do
it "a question page has a link allowing you to cancel your input and return to the check answers page" do
visit("logs/#{id}/tenant-code-test?referrer=check_answers")
click_link(text: "Cancel")
expect(page).to have_current_path("/logs/#{id}/setup/check-answers")
end
context "when clicking save and continue on a mandatory question with no input" do
let(:id) { empty_case_log.id }
it "shows a validation error on radio questions" do
visit("/logs/#{id}/renewal")
click_button("Save and continue")
expect(page).to have_selector("#error-summary-title")
expect(page).to have_selector("#case-log-renewal-error")
expect(page).to have_title("Error")
end
it "shows a validation error on date questions" do
visit("/logs/#{id}/tenancy-start-date")
click_button("Save and continue")
expect(page).to have_selector("#error-summary-title")
expect(page).to have_selector("#case-log-startdate-error")
expect(page).to have_title("Error")
end
context "when the page has a main and conditional question" do
context "when the conditional question is required but not answered" do
it "shows a validation error for the conditional question" do
visit("/logs/#{id}/armed-forces")
choose("case-log-armedforces-1-field", allow_label_click: true)
click_button("Save and continue")
expect(page).to have_selector("#error-summary-title")
expect(page).to have_selector("#case-log-leftreg-error")
expect(page).to have_title("Error")
end
end
end
end
context "when clicking save and continue on an optional question with no input" do
let(:id) { empty_case_log.id }
it "does not show a validation error" do
visit("/logs/#{id}/tenant-code")
click_button("Save and continue")
expect(page).not_to have_selector("#error-summary-title")
expect(page).not_to have_title("Error")
expect(page).to have_current_path("/logs/#{id}/property-reference")
end
end
end
end end

1
spec/features/form/page_routing_spec.rb

@ -43,6 +43,7 @@ RSpec.describe "Form Page Routing" do
choose("case-log-preg-occ-2-field", allow_label_click: true) choose("case-log-preg-occ-2-field", allow_label_click: true)
click_button("Save and continue") click_button("Save and continue")
expect(page).to have_current_path("/logs/#{id}/conditional-question-no-page") expect(page).to have_current_path("/logs/#{id}/conditional-question-no-page")
choose("case-log-cbl-0-field", allow_label_click: true)
click_button("Save and continue") click_button("Save and continue")
expect(page).to have_current_path("/logs/#{id}/conditional-question/check-answers") expect(page).to have_current_path("/logs/#{id}/conditional-question/check-answers")
end end

4
spec/features/form/progressive_total_field_spec.rb

@ -33,6 +33,7 @@ RSpec.describe "Accessible Automcomplete" do
it "total displays despite error message", js: true do it "total displays despite error message", js: true do
visit("/logs/#{case_log.id}/rent") visit("/logs/#{case_log.id}/rent")
choose("case-log-period-1-field", allow_label_click: true)
fill_in("case-log-brent-field", with: 500) fill_in("case-log-brent-field", with: 500)
fill_in("case-log-scharge-field", with: 50) fill_in("case-log-scharge-field", with: 50)
fill_in("case-log-pscharge-field", with: 50) fill_in("case-log-pscharge-field", with: 50)
@ -40,6 +41,9 @@ RSpec.describe "Accessible Automcomplete" do
expect(find("#case-log-tcharge-field").value).to eq("5600.00") expect(find("#case-log-tcharge-field").value).to eq("5600.00")
click_button("Save and continue") click_button("Save and continue")
expect(page).to have_selector(".govuk-error-summary") expect(page).to have_selector(".govuk-error-summary")
fill_in("case-log-scharge-field", with: nil)
fill_in("case-log-pscharge-field", with: nil)
fill_in("case-log-supcharg-field-error", with: nil)
fill_in("case-log-brent-field", with: 500) fill_in("case-log-brent-field", with: 500)
expect(find("#case-log-tcharge-field").value).to eq("500.00") expect(find("#case-log-tcharge-field").value).to eq("500.00")
fill_in("case-log-supcharg-field-error", with: 50) fill_in("case-log-supcharg-field-error", with: 50)

1
spec/features/form/saving_data_spec.rb

@ -26,7 +26,6 @@ RSpec.describe "Form Saving Data" do
tenancycode: { type: "text", answer: "BZ737", path: "tenant-code-test" }, tenancycode: { type: "text", answer: "BZ737", path: "tenant-code-test" },
age1: { type: "numeric", answer: 25, path: "person_1_age" }, age1: { type: "numeric", answer: 25, path: "person_1_age" },
sex1: { type: "radio", answer: { "F" => "Female" }, path: "person_1_gender" }, sex1: { type: "radio", answer: { "F" => "Female" }, path: "person_1_gender" },
hhmemb: { type: "numeric", answer: 3, path: "household_number_of_members" },
} }
end end

47
spec/features/schemes_spec.rb

@ -772,4 +772,51 @@ RSpec.describe "Schemes scheme Features" do
end end
end end
end end
context "when selecting a scheme" do
let!(:user) { FactoryBot.create(:user, :data_coordinator, last_sign_in_at: Time.zone.now) }
let!(:schemes) { FactoryBot.create_list(:scheme, 5, owning_organisation: user.organisation) }
let(:location) { FactoryBot.create(:location, scheme: schemes[2]) }
let!(:case_log) { FactoryBot.create(:case_log, created_by: user, needstype: 2) }
before do
Timecop.freeze(Time.utc(2022, 6, 3))
location.update!(startdate: nil)
FactoryBot.create(:location, scheme: schemes[0], startdate: nil)
FactoryBot.create(:location, scheme: schemes[1], startdate: nil)
FactoryBot.create(:location, scheme: schemes[1], startdate: nil)
FactoryBot.create(:location, scheme: schemes[1], startdate: Time.utc(2023, 6, 3))
visit("/logs")
fill_in("user[email]", with: user.email)
fill_in("user[password]", with: user.password)
click_button("Sign in")
end
after do
Timecop.unfreeze
end
it "does not display the schemes without a location" do
visit("/logs/#{case_log.id}/scheme")
expect(find("#case-log-scheme-id-field").all("option").count).to eq(3)
end
it "does not display the schemes with a location with a startdate in the future" do
location.update!(startdate: Time.utc(2022, 7, 4))
visit("/logs/#{case_log.id}/scheme")
expect(find("#case-log-scheme-id-field").all("option").count).to eq(2)
end
it "does display the schemes with a location with a startdate in the past" do
location.update!(startdate: Time.utc(2022, 5, 2))
visit("/logs/#{case_log.id}/scheme")
expect(find("#case-log-scheme-id-field").all("option").count).to eq(3)
end
it "does display the schemes with a location with a startdate being today" do
location.update!(startdate: Time.utc(2022, 6, 3))
visit("/logs/#{case_log.id}/scheme")
expect(find("#case-log-scheme-id-field").all("option").count).to eq(3)
end
end
end end

524
spec/fixtures/imports/case_logs/0b4a68df-30cc-474a-93c0-a56ce8fdad3b.xml vendored

@ -0,0 +1,524 @@
<Group xmlns="http://data.gov.uk/core/logs/2021-CORE-SR-SH" xmlns:app="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:meta="http://data.gov.uk/core/metadata" xmlns:svc="http://www.w3.org/2007/app" xmlns:xf="http://www.w3.org/2002/xforms" xmlns:xfimpl="http://www.w3.org/2002/xforms/implementation" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xxf="http://orbeon.org/oxf/xml/xforms" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<meta:metadata xmlns:es="http://www.ecmascript.org/" xmlns:xqx="http://www.w3.org/2005/XQueryX" xmlns:XSLT="http://www.w3.org/1999/XSL/Transform/compile">
<meta:form-name>2021-CORE-SR-SH</meta:form-name>
<meta:document-id>0b4a68df-30cc-474a-93c0-a56ce8fdad3b</meta:document-id>
<meta:owner-user-id>c3061a2e6ea0b702e6f6210d5c52d2a92612d2aa</meta:owner-user-id>
<meta:owner-institution-id>7c5bd5fb549c09z2c55d9cb90d7ba84927e64618</meta:owner-institution-id>
<meta:managing-institution-id>7c5bd5fb549c09z2c55d9cb90d7ba84927e64618</meta:managing-institution-id>
<meta:created-date>2022-01-05T12:50:20.39153Z</meta:created-date>
<meta:modified-date>2022-01-05T12:50:20.39153Z</meta:modified-date>
<meta:status>submitted-valid</meta:status>
<meta:reporting-year>2021</meta:reporting-year>
<meta:upload-method>Manual Entry</meta:upload-method>
<meta:schema assert-valid="true"/>
<meta:rules assert-valid="true"/>
</meta:metadata>
<Group>
<Qdp>Yes</Qdp>
<KeyDate>2021-11-05</KeyDate>
<FORM>123456</FORM>
<Landlord source-value="2">2 Local Authority</Landlord>
<Group>
<_1cmangroupcode>123</_1cmangroupcode>
<_1cschemecode>10</_1cschemecode>
<schPC/>
<Q1e>3 No</Q1e>
</Group>
</Group>
<Group>
<_2a>2 No</_2a>
<Q2b>1 Secure (inc flexible)</Q2b>
<Q2ba/>
<_2bTenCode>14044912001</_2bTenCode>
<_2cYears>2</_2cYears>
</Group>
<Group>
<P1Age override-field="">72</P1Age>
<P1AR/>
<P1Sex override-field="">Female</P1Sex>
<P1Eco>5) Retired</P1Eco>
<P1Eth>1 White: English/Scottish/Welsh/Northern Irish/British</P1Eth>
<P1Nat>1 UK national resident in UK</P1Nat>
<P2Age override-field="">74</P2Age>
<P2AR/>
<P2Sex override-field="">Male</P2Sex>
<P2Rel>Partner</P2Rel>
<P2Eco>5) Retired</P2Eco>
<P3Age override-field=""/>
<P3AR/>
<P3Sex override-field=""/>
<P3Rel/>
<P3Eco/>
<P4Age override-field=""/>
<P4AR/>
<P4Sex override-field=""/>
<P4Rel/>
<P4Eco/>
<P5Age override-field=""/>
<P5AR/>
<P5Sex override-field=""/>
<P5Rel/>
<P5Eco/>
<P6Age override-field=""/>
<P6AR/>
<P6Sex override-field=""/>
<P6Rel/>
<P6Eco/>
<P7Age override-field=""/>
<P7AR/>
<P7Sex override-field=""/>
<P7Rel/>
<P7Eco/>
<P8Age override-field=""/>
<P8AR/>
<P8Sex override-field=""/>
<P8Rel/>
<P8Eco/>
<Group>
<ArmedF>2 No</ArmedF>
<LeftAF/>
<Inj/>
<Preg override-field="">2 No</Preg>
</Group>
<Group>
<Q6Ben>9 Not in receipt of either UC or HB</Q6Ben>
</Group>
<Group>
<Q7Ben>2 Some</Q7Ben>
<Q8Refused>Refused</Q8Refused>
<Q8Money override-field=""/>
<Q8a/>
</Group>
<Group>
<Q9a>13 Property unsuitable because of ill health / disability</Q9a>
<Q9aa/>
</Group>
<Group>
<_9b override-field="">2 No</_9b>
<Q10-a/>
<Q10-b/>
<Q10-c/>
<Q10-f/>
<Q10-g>Yes</Q10-g>
<Q10-h/>
<Q10ia>1 Yes</Q10ia>
<Q10ib-1/>
<Q10ib-2/>
<Q10ib-3>Yes</Q10ib-3>
<Q10ib-4/>
<Q10ib-5/>
<Q10ib-6/>
<Q10ib-7/>
<Q10ib-8/>
<Q10ib-9/>
<Q10ib-10/>
<Q11 override-field="">26 Owner occupation (private)</Q11>
<Q12a>DLUHC</Q12a>
<Q12aONS>E08000035</Q12aONS>
<Q12b override-field="">S80 4DJ</Q12b>
<Q12bnot/>
<Q12c>5 5 years or more</Q12c>
<Q12d>9 3 years but under 4 years</Q12d>
</Group>
<Group>
<Q13>1 Not homeless</Q13>
<Q14a>2 No</Q14a>
<Q14b1/>
<Q14b2/>
<Q14b3/>
<Q14b4/>
<Q14b5/>
</Group>
<Group>
<Q15CBL>1 Yes</Q15CBL>
<Q15CHR>1 Yes</Q15CHR>
<Q15CAP>1 Yes</Q15CAP>
</Group>
<Group>
<Q16>2 Tenant applied direct (no referral or nomination)</Q16>
</Group>
</Group>
<Group>
<Q17>7 Weekly for 48 weeks</Q17>
<Q18ai override-field="true">125.00</Q18ai>
<Q18aii override-field=""/>
<Q18aiii override-field=""/>
<Q18aiv override-field="">7.00</Q18aiv>
<Q18av override-field="">132.00</Q18av>
<Q18b override-field=""/>
<Q18c/>
<Q18d/>
<Q18dyes override-field=""/>
<Q19void>2021-08-24</Q19void>
<Q19repair/>
<Q19supsch/>
<Q20 override-field="">0</Q20>
<Q21a>14044912</Q21a>
</Group>
<Group>
<Q25>1 Yes</Q25>
<Q27>15 First let of newbuild property</Q27>
</Group>
<Group>
<F1Age>0</F1Age>
<F2Age>0</F2Age>
<F3Age>0</F3Age>
<F4Age>0</F4Age>
<F5Age>0</F5Age>
<F6Age>0</F6Age>
<F7Age>0</F7Age>
<F8Age>0</F8Age>
<FAge>0</FAge>
<F1>1</F1>
<F2>0</F2>
<F3>0</F3>
<F4>0</F4>
<F5>0</F5>
<F6>0</F6>
<F7>0</F7>
<F8>0</F8>
<F>1</F>
<P1100>0</P1100>
<P2100>0</P2100>
<P3100>0</P3100>
<P4100>0</P4100>
<P5100>0</P5100>
<P6100>0</P6100>
<P7100>0</P7100>
<P8100>0</P8100>
<_100>0</_100>
<P170>1</P170>
<P270>1</P270>
<P370>0</P370>
<P470>0</P470>
<P570>0</P570>
<P670>0</P670>
<P770>0</P770>
<P870>0</P870>
<_70>1</_70>
<P1PT>0</P1PT>
<P2PT>0</P2PT>
<P3PT>0</P3PT>
<P4PT>0</P4PT>
<P5PT>0</P5PT>
<P6PT>0</P6PT>
<P7PT>0</P7PT>
<P8PT>0</P8PT>
<PT>0</PT>
<P1FT>0</P1FT>
<P2FT>0</P2FT>
<P3FT>0</P3FT>
<P4FT>0</P4FT>
<P5FT>0</P5FT>
<P6FT>0</P6FT>
<P7FT>0</P7FT>
<P8FT>0</P8FT>
<FT>0</FT>
<P1Stud>0</P1Stud>
<P2Stud>0</P2Stud>
<P3Stud>0</P3Stud>
<P4Stud>0</P4Stud>
<P5Stud>0</P5Stud>
<P6Stud>0</P6Stud>
<P7Stud>0</P7Stud>
<P8Stud>0</P8Stud>
<Stud>0</Stud>
<P2Child>0</P2Child>
<P3Child>0</P3Child>
<P4Child>0</P4Child>
<P5Child>0</P5Child>
<P6Child>0</P6Child>
<P7Child>0</P7Child>
<P8Child>0</P8Child>
<Child>0</Child>
<P2Partner>1</P2Partner>
<P3Partner>0</P3Partner>
<P4Partner>0</P4Partner>
<P5Partner>0</P5Partner>
<P6Partner>0</P6Partner>
<P7Partner>0</P7Partner>
<P8Partner>0</P8Partner>
<Partner>1</Partner>
<Q1cV1>1</Q1cV1>
<Q1cV2>1</Q1cV2>
<Q1cVT>2</Q1cVT>
<P1Adult>1</P1Adult>
<P2Adult>1</P2Adult>
<P3Adult>0</P3Adult>
<P4Adult>0</P4Adult>
<P5Adult>0</P5Adult>
<P6Adult>0</P6Adult>
<P7Adult>0</P7Adult>
<P8Adult>0</P8Adult>
<PAdultT>2</PAdultT>
<P2PAge>74</P2PAge>
<P3PAge>0</P3PAge>
<P4PAge>0</P4PAge>
<P5PAge>0</P5PAge>
<P6PAge>0</P6PAge>
<P7PAge>0</P7PAge>
<P8PAge>0</P8PAge>
<PAGE>74</PAGE>
<P2ChildAge>0</P2ChildAge>
<P3ChildAge>0</P3ChildAge>
<P4ChildAge>0</P4ChildAge>
<P5ChildAge>0</P5ChildAge>
<P6ChildAge>0</P6ChildAge>
<P7ChildAge>0</P7ChildAge>
<P8ChildAge>0</P8ChildAge>
<ChildAgeMin>0</ChildAgeMin>
<AgeDiff1>72</AgeDiff1>
<AgeDiff2>0</AgeDiff2>
<AgeDiff3>74</AgeDiff3>
<TODAY>2022-01-05Z</TODAY>
<FutureLimit>2022-01-20Z</FutureLimit>
<minmax1/>
<minmax2/>
<minmax3/>
<minmax4/>
<minmax5/>
<minmax6/>
<minmax7/>
<minmax8/>
<minmax9/>
<minmax0/>
<minmax10/>
<minmaxT/>
<Q10av>0</Q10av>
<Q10bv>0</Q10bv>
<Q10cv>0</Q10cv>
<Q10fv>0</Q10fv>
<Q10gv>20</Q10gv>
<Q10hv>0</Q10hv>
<Q10Validate>20</Q10Validate>
<Q2bv>A</Q2bv>
<P2Agev>1</P2Agev>
<P2Sexv>1</P2Sexv>
<P2Relv>1</P2Relv>
<P2Ecov>1</P2Ecov>
<P2valid>4</P2valid>
<P3Agev>0</P3Agev>
<P3Sexv>0</P3Sexv>
<P3Relv>0</P3Relv>
<P3Ecov>0</P3Ecov>
<P3valid>0</P3valid>
<P4Agev>0</P4Agev>
<P4Sexv>0</P4Sexv>
<P4Relv>0</P4Relv>
<P4Ecov>0</P4Ecov>
<P4valid>0</P4valid>
<P5Agev>0</P5Agev>
<P5Sexv>0</P5Sexv>
<P5Relv>0</P5Relv>
<P5Ecov>0</P5Ecov>
<P5valid>0</P5valid>
<P6Agev>0</P6Agev>
<P6Sexv>0</P6Sexv>
<P6Relv>0</P6Relv>
<P6Ecov>0</P6Ecov>
<P6valid>0</P6valid>
<P7Agev>0</P7Agev>
<P7Sexv>0</P7Sexv>
<P7Relv>0</P7Relv>
<P7Ecov>0</P7Ecov>
<P7valid>0</P7valid>
<P8Agev>0</P8Agev>
<P8Sexv>0</P8Sexv>
<P8Relv>0</P8Relv>
<P8Ecov>0</P8Ecov>
<P8valid>0</P8valid>
<Q14b1v>0</Q14b1v>
<Q14b2v>0</Q14b2v>
<Q14b3v>0</Q14b3v>
<Q14b4v>0</Q14b4v>
<Q14b5v>0</Q14b5v>
<Q14bv>0</Q14bv>
<P2Other>0</P2Other>
<P3Other>0</P3Other>
<P4Other>0</P4Other>
<P5Other>0</P5Other>
<P6Other>0</P6Other>
<P7Other>0</P7Other>
<P8Other>0</P8Other>
<Other>0</Other>
<P2ARefused>0</P2ARefused>
<P3ARefused>0</P3ARefused>
<P4ARefused>0</P4ARefused>
<P5ARefused>0</P5ARefused>
<P6ARefused>0</P6ARefused>
<P7ARefused>0</P7ARefused>
<P8ARefused>0</P8ARefused>
<TAREUSED>0</TAREUSED>
<P2RRefused>0</P2RRefused>
<P3RRefused>0</P3RRefused>
<P4RRefused>0</P4RRefused>
<P5RRefused>0</P5RRefused>
<P6RRefused>0</P6RRefused>
<P7RRefused>0</P7RRefused>
<P8RRefused>0</P8RRefused>
<TotRRefused>0</TotRRefused>
<TOTREFUSED>0</TOTREFUSED>
</Group>
<Group>
<ChildBen>0.00</ChildBen>
<TOTADULT>2</TOTADULT>
<NEW_OLD>1 New Tenant</NEW_OLD>
<WCHCHRG/>
<VACDAYS>73</VACDAYS>
<HHMEMB>2</HHMEMB>
<HHTYPEP1A>0</HHTYPEP1A>
<HHTYPEP2A>0</HHTYPEP2A>
<HHTYPEP3A>0</HHTYPEP3A>
<HHTYPEP4A>0</HHTYPEP4A>
<HHTYPEP5A>0</HHTYPEP5A>
<HHTYPEP6A>0</HHTYPEP6A>
<HHTYPEP7A>0</HHTYPEP7A>
<HHTYPEP8A>0</HHTYPEP8A>
<TADULT>0</TADULT>
<HHTYPEP1E>1</HHTYPEP1E>
<HHTYPEP2E>1</HHTYPEP2E>
<HHTYPEP3E>0</HHTYPEP3E>
<HHTYPEP4E>0</HHTYPEP4E>
<HHTYPEP5E>0</HHTYPEP5E>
<HHTYPEP6E>0</HHTYPEP6E>
<HHTYPEP7E>0</HHTYPEP7E>
<HHTYPEP8E>0</HHTYPEP8E>
<TELDER>2</TELDER>
<HHTYPEP1C>0</HHTYPEP1C>
<HHTYPEP2C>0</HHTYPEP2C>
<HHTYPEP3C>0</HHTYPEP3C>
<HHTYPEP4C>0</HHTYPEP4C>
<HHTYPEP5C>0</HHTYPEP5C>
<HHTYPEP6C>0</HHTYPEP6C>
<HHTYPEP7C>0</HHTYPEP7C>
<HHTYPEP8C>0</HHTYPEP8C>
<TCHILD>0</TCHILD>
<Q18aValid>1</Q18aValid>
<Q18bValid>0</Q18bValid>
<Q18cValid>0</Q18cValid>
<Q18Valid>1</Q18Valid>
<HHTYPE>2 = 2 Adults at least one is an Elder</HHTYPE>
<WEEKLYINC/>
<INCOME/>
<TYPEHB>15.00</TYPEHB>
<AFFRATE/>
<Weekinc/>
<LETTYPE>2 Local Authority</LETTYPE>
<PLOACODE/>
<OACODE/>
<GOVREG>E12000004</GOVREG>
<OWNINGORGID>1</OWNINGORGID>
<OWNINGORGNAME>DLUHC</OWNINGORGNAME>
<MANINGORGNAME>DLUHC</MANINGORGNAME>
<HCNUM>N/A</HCNUM>
<MANHCNUM>N/A</MANHCNUM>
<LAHA/>
<MANINGORGID>1</MANINGORGID>
<Q28same1>false</Q28same1>
<HBTYPE1/>
<HBTYPE2/>
<HBTYPE3/>
<HBTYPE4/>
<HBTYPE5/>
<HBTYPE6/>
<HBTYPE7/>
<HBTYPE8/>
<HBTYPE9/>
<HBTYPE10/>
<HBTYPE11/>
<HBTYPE12/>
<HBTYPE13/>
<HBTYPE14/>
<HBTYPE15>15</HBTYPE15>
<HBTYPE>15</HBTYPE>
<SCHEME>000001005048</SCHEME>
<MANTYPE>D</MANTYPE>
<UNITS>15</UNITS>
<UNITTYPE>6</UNITTYPE>
<SCHTYPE>7</SCHTYPE>
<REGHOME>1</REGHOME>
<SUPPORT>2</SUPPORT>
<MOBSTAND>A</MOBSTAND>
<INTSTAY>P</INTSTAY>
<CLIGRP1>M</CLIGRP1>
<CLIGRP2/>
<Q28Auth>DLUHC</Q28Auth>
<Q28ONS>E08000035</Q28ONS>
<Q28pc override-field="">S80 4QE</Q28pc>
<Q28same/>
<P1R>0</P1R>
<P2R>0</P2R>
<P3R>0</P3R>
<P4R>0</P4R>
<P5R>0</P5R>
<P6R>0</P6R>
<P7R>0</P7R>
<P8R>0</P8R>
<REFUSEDTOT>0</REFUSEDTOT>
<REFUSED/>
<WTSHORTFALL/>
<WTSHORTFALLHB/>
<WTSHORTFALLHE/>
<WRENT>115.38</WRENT>
<WTCHARGE>121.85</WTCHARGE>
<WSCHARGE/>
<WPSCHRGE/>
<WSUPCHRG>6.46</WSUPCHRG>
<WTSHORTFALL1/>
<WRENT1>115.38</WRENT1>
<WTCHARGE1>121.85</WTCHARGE1>
<WSCHARGE1/>
<WPSCHRGE1/>
<WSUPCHRG1>6.46</WSUPCHRG1>
</Group>
<Group>
<BSa>1</BSa>
<BSb>0</BSb>
<BSc>0</BSc>
<BScm>0</BScm>
<BScf>0</BScf>
<BSd>0</BSd>
<BSdm>0</BSdm>
<BSdf>0</BSdf>
<BSe>0</BSe>
<BSem>0</BSem>
<BSef>0</BSef>
<BSf>0</BSf>
<BSfm>0</BSfm>
<BSff>0</BSff>
<BSfmx>0</BSfmx>
<BSffx>0</BSffx>
<BEDROOMSTAND>1</BEDROOMSTAND>
<BEDMINUSBEDS/>
<WRENTreduced>115.38</WRENTreduced>
<NonDepDeduct>0</NonDepDeduct>
<RENTHB/>
<ChildAllowan>0</ChildAllowan>
<PrsnlAllowan>117.4</PrsnlAllowan>
<HousBenDisAl>10</HousBenDisAl>
<PAIDHB/>
<HCNETAF/>
<ChldAlloCat1>0</ChldAlloCat1>
<ChldAlloCat2>0</ChldAlloCat2>
<P2NnDepDedct>0</P2NnDepDedct>
<P3NnDepDedct>0</P3NnDepDedct>
<P4NnDepDedct>0</P4NnDepDedct>
<P5NnDepDedct>0</P5NnDepDedct>
<P6NnDepDedct>0</P6NnDepDedct>
<P7NnDepDedct>0</P7NnDepDedct>
<P8NnDepDedct>0</P8NnDepDedct>
<DAY>5</DAY>
<MONTH>11</MONTH>
<YEAR>2021</YEAR>
<VDAY>24</VDAY>
<VMONTH>8</VMONTH>
<VYEAR>2021</VYEAR>
<MRCDAY/>
<MRCMONTH/>
<MRCYEAR/>
<PPOSTC1>LS16</PPOSTC1>
<PPOSTC2>6FT</PPOSTC2>
<POSTCODE>LS16</POSTCODE>
<POSTCOD2>6FT</POSTCOD2>
</Group>
</Group>

15
spec/models/case_log_spec.rb

@ -1702,9 +1702,9 @@ RSpec.describe CaseLog do
context "and not renewal" do context "and not renewal" do
let(:scheme) { FactoryBot.create(:scheme) } let(:scheme) { FactoryBot.create(:scheme) }
let(:location) { FactoryBot.create(:location, scheme:, county: "E07000041", type_of_unit: 1, type_of_building: "Purpose built", wheelchair_adaptation: 0) } let(:location) { FactoryBot.create(:location, scheme:, postcode: "M11AE", type_of_unit: 1, type_of_building: "Purpose built") }
let!(:supported_housing_case_log) do let(:supported_housing_case_log) do
described_class.create!({ described_class.create!({
managing_organisation: owning_organisation, managing_organisation: owning_organisation,
owning_organisation:, owning_organisation:,
@ -1716,14 +1716,21 @@ RSpec.describe CaseLog do
}) })
end end
before do
stub_request(:get, /api.postcodes.io/)
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"admin_district\":\"Manchester\",\"codes\":{\"admin_district\": \"E08000003\"}}}", headers: {})
end
it "correctly infers and saves la" do it "correctly infers and saves la" do
record_from_db = ActiveRecord::Base.connection.execute("SELECT la from case_logs WHERE id=#{supported_housing_case_log.id}").to_a[0] record_from_db = ActiveRecord::Base.connection.execute("SELECT la from case_logs WHERE id=#{supported_housing_case_log.id}").to_a[0]
expect(record_from_db["la"]).to eq(location.county) expect(record_from_db["la"]).to be_nil
expect(supported_housing_case_log.la).to eq("E08000003")
end end
it "correctly infers and saves postcode" do it "correctly infers and saves postcode" do
record_from_db = ActiveRecord::Base.connection.execute("SELECT postcode_full from case_logs WHERE id=#{supported_housing_case_log.id}").to_a[0] record_from_db = ActiveRecord::Base.connection.execute("SELECT postcode_full from case_logs WHERE id=#{supported_housing_case_log.id}").to_a[0]
expect(record_from_db["postcode_full"]).to eq(location.postcode) expect(record_from_db["postcode_full"]).to be_nil
expect(supported_housing_case_log.postcode_full).to eq("M11AE")
end end
it "unittype_sh method returns the type_of_unit of the location" do it "unittype_sh method returns the type_of_unit of the location" do

69
spec/models/form/setup/questions/location_id_spec.rb

@ -31,7 +31,74 @@ RSpec.describe Form::Setup::Questions::LocationId, type: :model do
expect(question).not_to be_derived expect(question).not_to be_derived
end end
it "has the correct answer_options" do context "when there are no locations" do
it "the answer_options is an empty hash" do
expect(question.answer_options).to eq({}) expect(question.answer_options).to eq({})
end end
end
context "when getting available locations" do
let(:scheme) { FactoryBot.create(:scheme) }
let(:case_log) { FactoryBot.create(:case_log, scheme:, needstype: 2) }
context "when there are no locations" do
it "the displayed_answer_options is an empty hash" do
expect(question.displayed_answer_options(case_log)).to eq({})
end
end
context "when selected scheme has locations" do
before do
Timecop.freeze(Time.utc(2022, 5, 12))
end
after do
Timecop.unfreeze
end
context "and all the locations have a future startdate" do
before do
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 13))
FactoryBot.create(:location, scheme:, startdate: Time.utc(2023, 1, 1))
end
it "the displayed_answer_options is an empty hash" do
expect(question.displayed_answer_options(case_log)).to eq({})
end
end
context "and the locations have a no startdate" do
before do
FactoryBot.create(:location, scheme:, startdate: nil)
FactoryBot.create(:location, scheme:, startdate: nil)
end
it "the displayed_answer_options shows the locations" do
expect(question.displayed_answer_options(case_log).count).to eq(2)
end
end
context "and the locations have a past startdate" do
before do
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 4, 10))
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 12))
end
it "the displayed_answer_options shows the locations" do
expect(question.displayed_answer_options(case_log).count).to eq(2)
end
end
context "and some locations have a past startdate" do
before do
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 10, 10))
FactoryBot.create(:location, scheme:, startdate: Time.utc(2022, 5, 12))
end
it "the displayed_answer_options shows the active location" do
expect(question.displayed_answer_options(case_log).count).to eq(1)
end
end
end
end
end end

14
spec/models/form/setup/questions/scheme_id_spec.rb

@ -50,9 +50,21 @@ RSpec.describe Form::Setup::Questions::SchemeId, type: :model do
FactoryBot.create(:scheme, owning_organisation: organisation_2) FactoryBot.create(:scheme, owning_organisation: organisation_2)
end end
context "when a scheme with at least 1 location exists" do
before do
FactoryBot.create(:location, scheme:)
end
it "has the correct answer_options based on the schemes the user's organisation owns or manages" do it "has the correct answer_options based on the schemes the user's organisation owns or manages" do
expected_answer = { scheme.id.to_s => scheme.service_name } expected_answer = { scheme.id.to_s => scheme }
expect(question.displayed_answer_options(case_log)).to eq(expected_answer) expect(question.displayed_answer_options(case_log)).to eq(expected_answer)
end end
end end
context "when there are no schemes with locations" do
it "returns an empty hash" do
expect(question.displayed_answer_options(case_log)).to eq({})
end
end
end
end end

11
spec/models/location_spec.rb

@ -4,9 +4,20 @@ RSpec.describe Location, type: :model do
describe "#new" do describe "#new" do
let(:location) { FactoryBot.build(:location) } let(:location) { FactoryBot.build(:location) }
before do
stub_request(:get, /api.postcodes.io/)
.to_return(status: 200, body: "{\"status\":200,\"result\":{\"admin_district\":\"Manchester\",\"codes\":{\"admin_district\": \"E08000003\"}}}", headers: {})
end
it "belongs to an organisation" do it "belongs to an organisation" do
expect(location.scheme).to be_a(Scheme) expect(location.scheme).to be_a(Scheme)
end end
it "infers the local authority" do
location.postcode = "M1 1AE"
location.save!
expect(location.location_code).to eq("E08000003")
end
end end
describe "#validate_postcode" do describe "#validate_postcode" do

4
spec/models/scheme_spec.rb

@ -46,6 +46,10 @@ RSpec.describe Scheme, type: :model do
end end
context "when searching by all searchable fields" do context "when searching by all searchable fields" do
before do
location_2.update!(postcode: location_2.postcode.gsub(scheme_1.id.to_s, "0"))
end
it "returns case insensitive matching records" do it "returns case insensitive matching records" do
expect(described_class.search_by(scheme_1.id.to_s).count).to eq(1) expect(described_class.search_by(scheme_1.id.to_s).count).to eq(1)
expect(described_class.search_by(scheme_1.id.to_s).first.id).to eq(scheme_1.id) expect(described_class.search_by(scheme_1.id.to_s).first.id).to eq(scheme_1.id)

1
spec/rails_helper.rb

@ -80,6 +80,7 @@ RSpec.configure do |config|
Capybara.server = :puma, { Silent: true } Capybara.server = :puma, { Silent: true }
config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::ControllerHelpers, type: :view
config.include Devise::Test::IntegrationHelpers, type: :request config.include Devise::Test::IntegrationHelpers, type: :request
config.include ViewComponent::TestHelpers, type: :component config.include ViewComponent::TestHelpers, type: :component
config.include Capybara::RSpecMatchers, type: :component config.include Capybara::RSpecMatchers, type: :component

2
spec/requests/case_logs_controller_spec.rb

@ -778,7 +778,7 @@ RSpec.describe CaseLogsController, type: :request do
it "dowloads searched logs" do it "dowloads searched logs" do
get "/logs?search=#{case_log.id}", headers:, params: {} get "/logs?search=#{case_log.id}", headers:, params: {}
csv = CSV.parse(response.body) csv = CSV.parse(response.body)
expect(csv.count).to eq(2) expect(csv.count).to eq(CaseLog.search_by(case_log.id.to_s).count + 1)
end end
context "when both filter and search applied" do context "when both filter and search applied" do

8
spec/requests/locations_controller_spec.rb

@ -90,7 +90,8 @@ RSpec.describe LocationsController, type: :request do
context "when signed in as a data coordinator" do context "when signed in as a data coordinator" do
let(:user) { FactoryBot.create(:user, :data_coordinator) } let(:user) { FactoryBot.create(:user, :data_coordinator) }
let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) } let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) }
let(:params) { { location: { name: "Test", units: "5", type_of_unit: "Bungalow", wheelchair_adaptation: "No", add_another_location: "No", postcode: "ZZ1 1ZZ" } } } let(:startdate) { Time.utc(2022, 2, 2) }
let(:params) { { location: { name: "Test", units: "5", type_of_unit: "Bungalow", wheelchair_adaptation: "No", add_another_location: "No", postcode: "ZZ1 1ZZ", startdate: } } }
before do before do
sign_in user sign_in user
@ -111,6 +112,7 @@ RSpec.describe LocationsController, type: :request do
expect(Location.last.units).to eq(5) expect(Location.last.units).to eq(5)
expect(Location.last.type_of_unit).to eq("Bungalow") expect(Location.last.type_of_unit).to eq("Bungalow")
expect(Location.last.wheelchair_adaptation).to eq("No") expect(Location.last.wheelchair_adaptation).to eq("No")
expect(Location.last.startdate).to eq(startdate)
end end
context "when postcode is submitted with lower case" do context "when postcode is submitted with lower case" do
@ -390,7 +392,8 @@ RSpec.describe LocationsController, type: :request do
let(:user) { FactoryBot.create(:user, :data_coordinator) } let(:user) { FactoryBot.create(:user, :data_coordinator) }
let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) } let!(:scheme) { FactoryBot.create(:scheme, owning_organisation: user.organisation) }
let!(:location) { FactoryBot.create(:location, scheme:) } let!(:location) { FactoryBot.create(:location, scheme:) }
let(:params) { { location: { name: "Test", units: "5", type_of_unit: "Bungalow", wheelchair_adaptation: "No", add_another_location: "No", postcode: "ZZ1 1ZZ", page: "edit" } } } let(:startdate) { Time.utc(2021, 1, 2) }
let(:params) { { location: { name: "Test", units: "5", type_of_unit: "Bungalow", wheelchair_adaptation: "No", add_another_location: "No", postcode: "ZZ1 1ZZ", startdate:, page: "edit" } } }
before do before do
sign_in user sign_in user
@ -410,6 +413,7 @@ RSpec.describe LocationsController, type: :request do
expect(Location.last.units).to eq(5) expect(Location.last.units).to eq(5)
expect(Location.last.type_of_unit).to eq("Bungalow") expect(Location.last.type_of_unit).to eq("Bungalow")
expect(Location.last.wheelchair_adaptation).to eq("No") expect(Location.last.wheelchair_adaptation).to eq("No")
expect(Location.last.startdate).to eq(startdate)
end end
context "when updating from edit-name page" do context "when updating from edit-name page" do

68
spec/services/imports/case_logs_import_service_spec.rb

@ -16,6 +16,9 @@ RSpec.describe Imports::CaseLogsImportService do
end end
before do before do
WebMock.stub_request(:get, /api.postcodes.io\/postcodes\/LS166FT/)
.to_return(status: 200, body: '{"status":200,"result":{"codes":{"admin_district":"E08000035"}}}', headers: {})
allow(Organisation).to receive(:find_by).and_return(nil) allow(Organisation).to receive(:find_by).and_return(nil)
allow(Organisation).to receive(:find_by).with(old_visible_id: organisation.old_visible_id.to_i).and_return(organisation) allow(Organisation).to receive(:find_by).with(old_visible_id: organisation.old_visible_id.to_i).and_return(organisation)
@ -23,12 +26,17 @@ RSpec.describe Imports::CaseLogsImportService do
FactoryBot.create(:user, old_user_id: "c3061a2e6ea0b702e6f6210d5c52d2a92612d2aa", organisation:) FactoryBot.create(:user, old_user_id: "c3061a2e6ea0b702e6f6210d5c52d2a92612d2aa", organisation:)
FactoryBot.create(:user, old_user_id: "e29c492473446dca4d50224f2bb7cf965a261d6f", organisation:) FactoryBot.create(:user, old_user_id: "e29c492473446dca4d50224f2bb7cf965a261d6f", organisation:)
# Scheme and Location
scheme = FactoryBot.create(:scheme, old_visible_id: 123)
FactoryBot.create(:location,
old_visible_id: 10,
scheme_id: scheme.id,
wheelchair_adaptation: 1,
postcode: "LS166FT")
# Stub the form handler to use the real form # Stub the form handler to use the real form
allow(FormHandler.instance).to receive(:get_form).with("2021_2022").and_return(real_2021_2022_form) allow(FormHandler.instance).to receive(:get_form).with("2021_2022").and_return(real_2021_2022_form)
allow(FormHandler.instance).to receive(:get_form).with("2022_2023").and_return(real_2022_2023_form) allow(FormHandler.instance).to receive(:get_form).with("2022_2023").and_return(real_2022_2023_form)
WebMock.stub_request(:get, /api.postcodes.io\/postcodes\/LS166FT/)
.to_return(status: 200, body: '{"status":200,"result":{"codes":{"admin_district":"E08000035"}}}', headers: {})
end end
context "when importing case logs" do context "when importing case logs" do
@ -36,11 +44,12 @@ RSpec.describe Imports::CaseLogsImportService do
let(:case_log_id) { "0ead17cb-1668-442d-898c-0d52879ff592" } let(:case_log_id) { "0ead17cb-1668-442d-898c-0d52879ff592" }
let(:case_log_id2) { "166fc004-392e-47a8-acb8-1c018734882b" } let(:case_log_id2) { "166fc004-392e-47a8-acb8-1c018734882b" }
let(:case_log_id3) { "00d2343e-d5fa-4c89-8400-ec3854b0f2b4" } let(:case_log_id3) { "00d2343e-d5fa-4c89-8400-ec3854b0f2b4" }
let(:case_log_id4) { "0b4a68df-30cc-474a-93c0-a56ce8fdad3b" }
before do before do
# Stub the S3 file listing and download # Stub the S3 file listing and download
allow(storage_service).to receive(:list_files) allow(storage_service).to receive(:list_files)
.and_return(%W[#{remote_folder}/#{case_log_id}.xml #{remote_folder}/#{case_log_id2}.xml #{remote_folder}/#{case_log_id3}.xml]) .and_return(%W[#{remote_folder}/#{case_log_id}.xml #{remote_folder}/#{case_log_id2}.xml #{remote_folder}/#{case_log_id3}.xml #{remote_folder}/#{case_log_id4}.xml])
allow(storage_service).to receive(:get_file_io) allow(storage_service).to receive(:get_file_io)
.with("#{remote_folder}/#{case_log_id}.xml") .with("#{remote_folder}/#{case_log_id}.xml")
.and_return(open_file(fixture_directory, case_log_id), open_file(fixture_directory, case_log_id)) .and_return(open_file(fixture_directory, case_log_id), open_file(fixture_directory, case_log_id))
@ -50,6 +59,9 @@ RSpec.describe Imports::CaseLogsImportService do
allow(storage_service).to receive(:get_file_io) allow(storage_service).to receive(:get_file_io)
.with("#{remote_folder}/#{case_log_id3}.xml") .with("#{remote_folder}/#{case_log_id3}.xml")
.and_return(open_file(fixture_directory, case_log_id3), open_file(fixture_directory, case_log_id3)) .and_return(open_file(fixture_directory, case_log_id3), open_file(fixture_directory, case_log_id3))
allow(storage_service).to receive(:get_file_io)
.with("#{remote_folder}/#{case_log_id4}.xml")
.and_return(open_file(fixture_directory, case_log_id4), open_file(fixture_directory, case_log_id4))
end end
it "successfully create all case logs" do it "successfully create all case logs" do
@ -57,45 +69,45 @@ RSpec.describe Imports::CaseLogsImportService do
expect(logger).not_to receive(:warn) expect(logger).not_to receive(:warn)
expect(logger).not_to receive(:info) expect(logger).not_to receive(:info)
expect { case_log_service.create_logs(remote_folder) } expect { case_log_service.create_logs(remote_folder) }
.to change(CaseLog, :count).by(3) .to change(CaseLog, :count).by(4)
end end
it "only updates existing case logs" do it "only updates existing case logs" do
expect(logger).not_to receive(:error) expect(logger).not_to receive(:error)
expect(logger).not_to receive(:warn) expect(logger).not_to receive(:warn)
expect(logger).to receive(:info).with(/Updating case log/).exactly(3).times expect(logger).to receive(:info).with(/Updating case log/).exactly(4).times
expect { 2.times { case_log_service.create_logs(remote_folder) } } expect { 2.times { case_log_service.create_logs(remote_folder) } }
.to change(CaseLog, :count).by(3) .to change(CaseLog, :count).by(4)
end end
context "when there are status discrepancies" do context "when there are status discrepancies" do
let(:case_log_id4) { "893ufj2s-lq77-42m4-rty6-ej09gh585uy1" } let(:case_log_id5) { "893ufj2s-lq77-42m4-rty6-ej09gh585uy1" }
let(:case_log_id5) { "5ybz29dj-l33t-k1l0-hj86-n4k4ma77xkcd" } let(:case_log_id6) { "5ybz29dj-l33t-k1l0-hj86-n4k4ma77xkcd" }
let(:case_log_file) { open_file(fixture_directory, case_log_id4) } let(:case_log_file) { open_file(fixture_directory, case_log_id5) }
let(:case_log_xml) { Nokogiri::XML(case_log_file) } let(:case_log_xml) { Nokogiri::XML(case_log_file) }
before do before do
allow(storage_service).to receive(:get_file_io)
.with("#{remote_folder}/#{case_log_id4}.xml")
.and_return(open_file(fixture_directory, case_log_id4), open_file(fixture_directory, case_log_id4))
allow(storage_service).to receive(:get_file_io) allow(storage_service).to receive(:get_file_io)
.with("#{remote_folder}/#{case_log_id5}.xml") .with("#{remote_folder}/#{case_log_id5}.xml")
.and_return(open_file(fixture_directory, case_log_id5), open_file(fixture_directory, case_log_id5)) .and_return(open_file(fixture_directory, case_log_id5), open_file(fixture_directory, case_log_id5))
allow(storage_service).to receive(:get_file_io)
.with("#{remote_folder}/#{case_log_id6}.xml")
.and_return(open_file(fixture_directory, case_log_id6), open_file(fixture_directory, case_log_id6))
end end
it "the logger logs a warning with the case log's old id/filename" do it "the logger logs a warning with the case log's old id/filename" do
expect(logger).to receive(:warn).with(/is not completed/).once expect(logger).to receive(:warn).with(/is not completed/).once
expect(logger).to receive(:warn).with(/Case log with old id:#{case_log_id4} is incomplete but status should be complete/).once expect(logger).to receive(:warn).with(/Case log with old id:#{case_log_id5} is incomplete but status should be complete/).once
case_log_service.send(:create_log, case_log_xml) case_log_service.send(:create_log, case_log_xml)
end end
it "on completion the ids of all logs with status discrepancies are logged in a warning" do it "on completion the ids of all logs with status discrepancies are logged in a warning" do
allow(storage_service).to receive(:list_files) allow(storage_service).to receive(:list_files)
.and_return(%W[#{remote_folder}/#{case_log_id4}.xml #{remote_folder}/#{case_log_id5}.xml]) .and_return(%W[#{remote_folder}/#{case_log_id5}.xml #{remote_folder}/#{case_log_id6}.xml])
allow(logger).to receive(:warn).with(/is not completed/) expect(logger).to receive(:warn).with(/is not completed/).twice
allow(logger).to receive(:warn).with(/is incomplete but status should be complete/) expect(logger).to receive(:warn).with(/is incomplete but status should be complete/).twice
expect(logger).to receive(:warn).with(/The following case logs had status discrepancies: \[893ufj2s-lq77-42m4-rty6-ej09gh585uy1, 5ybz29dj-l33t-k1l0-hj86-n4k4ma77xkcd\]/).once expect(logger).to receive(:warn).with(/The following case logs had status discrepancies: \[893ufj2s-lq77-42m4-rty6-ej09gh585uy1, 5ybz29dj-l33t-k1l0-hj86-n4k4ma77xkcd\]/)
case_log_service.create_logs(remote_folder) case_log_service.create_logs(remote_folder)
end end
@ -111,8 +123,8 @@ RSpec.describe Imports::CaseLogsImportService do
before { case_log_xml.at_xpath("//xmlns:VYEAR").content = 2023 } before { case_log_xml.at_xpath("//xmlns:VYEAR").content = 2023 }
it "does not import the voiddate" do it "does not import the voiddate" do
allow(logger).to receive(:warn).with(/is not completed/) expect(logger).to receive(:warn).with(/is not completed/)
allow(logger).to receive(:warn).with(/Case log with old id:#{case_log_id} is incomplete but status should be complete/) expect(logger).to receive(:warn).with(/Case log with old id:#{case_log_id} is incomplete but status should be complete/)
case_log_service.send(:create_log, case_log_xml) case_log_service.send(:create_log, case_log_xml)
@ -138,7 +150,7 @@ RSpec.describe Imports::CaseLogsImportService do
it "sets the economic status to child under 16" do it "sets the economic status to child under 16" do
# The update is done when calculating derived variables # The update is done when calculating derived variables
allow(logger).to receive(:warn).with(/Differences found when saving log/) expect(logger).to receive(:warn).with(/Differences found when saving log/)
case_log_service.send(:create_log, case_log_xml) case_log_service.send(:create_log, case_log_xml)
case_log = CaseLog.where(old_id: case_log_id).first case_log = CaseLog.where(old_id: case_log_id).first
@ -203,5 +215,19 @@ RSpec.describe Imports::CaseLogsImportService do
end end
end end
end end
context "and this is a supported housing log" do
let(:case_log_id) { "0b4a68df-30cc-474a-93c0-a56ce8fdad3b" }
it "sets the scheme and location values" do
expect(logger).not_to receive(:warn)
case_log_service.send(:create_log, case_log_xml)
case_log = CaseLog.find_by(old_id: case_log_id)
expect(case_log.scheme_id).not_to be_nil
expect(case_log.location_id).not_to be_nil
expect(case_log.status).to eq("completed")
end
end
end end
end end

13
spec/services/imports/scheme_import_service_spec.rb

@ -60,5 +60,18 @@ RSpec.describe Imports::SchemeImportService do
.not_to change(Scheme, :count) .not_to change(Scheme, :count)
end end
end end
context "and the scheme arrange type is direct" do
before do
scheme_xml.at_xpath("//mgmtgroup:arrangement_type").content = "D"
scheme_xml.at_xpath("//mgmtgroup:agent").content = ""
end
it "assigns both owning and managing organisation to the same one" do
scheme = scheme_service.create_scheme(scheme_xml)
expect(scheme.owning_organisation).to eq(owning_org)
expect(scheme.managing_organisation).to eq(owning_org)
end
end
end end
end end

2
spec/services/imports/scheme_location_import_service_spec.rb

@ -134,9 +134,11 @@ RSpec.describe Imports::SchemeLocationImportService do
expect(location.name).to eq("Location 1") expect(location.name).to eq("Location 1")
expect(location.postcode).to eq("S44 6EJ") expect(location.postcode).to eq("S44 6EJ")
expect(location.units).to eq(5) expect(location.units).to eq(5)
expect(location.mobility_type).to eq("Property fitted with equipment and adaptations (if not designed to above standards)")
expect(location.type_of_unit).to eq("Bungalow") expect(location.type_of_unit).to eq("Bungalow")
expect(location.old_id).to eq(first_location_id) expect(location.old_id).to eq(first_location_id)
expect(location.old_visible_id).to eq(10) expect(location.old_visible_id).to eq(10)
expect(location.startdate).to eq("1900-01-01")
expect(location.scheme).to eq(scheme) expect(location.scheme).to eq(scheme)
end end

Loading…
Cancel
Save