Browse Source

Prototype

pull/2902/head
Manny Dinssa 1 month ago
parent
commit
c30fbdb867
  1. 33
      app/controllers/address_options_controller.rb
  2. 115
      app/frontend/controllers/address_autocomplete_controller.js
  3. 3
      app/frontend/controllers/index.js
  4. 2
      app/helpers/address_options_helper.rb
  5. 8
      app/models/derived_variables/sales_log_variables.rb
  6. 24
      app/models/form/sales/pages/address_search.rb
  7. 32
      app/models/form/sales/questions/address_search.rb
  8. 2
      app/models/form/sales/questions/uprn_confirmation.rb
  9. 1
      app/models/form/sales/subsections/property_information.rb
  10. 36
      app/models/log.rb
  11. 34
      app/services/address_client.rb
  12. 23
      app/views/form/_address_autocomplete_question.html.erb
  13. 21
      app/views/form/_select_question.html.erb
  14. 6
      config/locales/forms/2024/sales/property_information.en.yml
  15. 6
      config/locales/forms/2025/sales/property_information.en.yml
  16. 2
      config/routes.rb
  17. 5
      db/migrate/20241204145927_add_address_search_to_sales_logs.rb
  18. 7
      db/schema.rb
  19. 2
      spec/models/form/lettings/questions/uprn_selection_spec.rb
  20. 2
      spec/models/form/sales/questions/uprn_selection_spec.rb
  21. 7
      spec/requests/address_options_spec.rb

33
app/controllers/address_options_controller.rb

@ -0,0 +1,33 @@
class AddressOptionsController < ApplicationController
def index
query = params[:query]
service = AddressClient.new(address: query)
service.call
if service.error.present?
render json: { error: service.error }, status: :unprocessable_entity
else
render json: service.result.map { |result| { address: result["ADDRESS"], uprn: result["UPRN"] } }
end
end
def current
log_id = params[:log_id]
sales_log = SalesLog.find_by(id: log_id)
uprn = sales_log&.address_search
if uprn.present?
service = AddressClient.new(uprn: uprn)
service.call
if service.error.present?
render json: { error: service.error }, status: :unprocessable_entity
else
address = service.result.find { |result| result["UPRN"] == uprn }&.dig("ADDRESS")
render json: { stored_value: { uprn:, address: } }
end
else
render json: { stored_value: nil }
end
end
end

115
app/frontend/controllers/address_autocomplete_controller.js

@ -0,0 +1,115 @@
import {Controller} from '@hotwired/stimulus'
import accessibleAutocomplete from 'accessible-autocomplete'
import 'accessible-autocomplete/dist/accessible-autocomplete.min.css'
import {searchableName} from "../modules/search";
const options = []
const fetchOptions = async (query) => {
const response = await fetch(`/address_options?query=${query}`)
const data = await response.json()
console.log(data)
return data
}
const fetchAndPopulateSearchResults = async (query, populateResults, populateOptions, selectEl) => {
if (/\S/.test(query)) {
const results = await fetchOptions(query)
console.log(results) // address and uprn keys returned per result
populateOptions(results, selectEl)
populateResults(Object.values(results).map((o) => o.address))
// populateResults(results)
}
}
const populateOptions = (results, selectEl) => {
selectEl.innerHTML = ''
results.forEach((result) => {
const option = document.createElement('option')
option.value = result.uprn
option.innerHTML = result.address
option.setAttribute('address', result.address)
selectEl.appendChild(option)
options.push(option)
})
}
// const populateOptions = (results, selectEl) => {
// selectEl.innerHTML = ''
//
// Object.keys(results).forEach((key) => {
// const option = document.createElement('option')
// option.value = key
// option.innerHTML = results[key].value
// if (results[key].hint) { option.setAttribute('data-hint', results[key].hint) }
// option.setAttribute('text', searchableName(results[key]))
// selectEl.appendChild(option)
// options.push(option)
// })
// }
export default class extends Controller {
connect () {
const selectEl = this.element
const currentValue = this.getCurrentValue()
console.log(selectEl)
if (currentValue && currentValue.stored_value) {
console.log(currentValue)
const option = document.createElement('option')
option.value = currentValue.stored_value.uprn
option.innerHTML = currentValue.stored_value.address
option.selected = true
selectEl.appendChild(option)
}
accessibleAutocomplete.enhanceSelectElement({
defaultValue: '',
selectElement: selectEl,
minLength: 1,
source: (query, populateResults) => {
fetchAndPopulateSearchResults(query, populateResults, populateOptions, selectEl)
},
autoselect: true,
showNoOptionsFound: true,
placeholder: currentValue?.stored_value?.address || 'Start typing to search',
templates: { suggestion: (value) => value },
onConfirm: (val) => {
const selectedResult = Array.from(selectEl.options).find(option => option.address === val)
if (selectedResult) {
selectedResult.selected = true
}
}
})
}
fetchOptions(query, populateResults) {
fetch(`/address_options?query=${query}`)
.then(response => response.json())
.then(data => {
console.log(data)
const results = data.map(result => result.uprn)
populateResults(results.slice(0, 10))
})
}
async getCurrentValue() {
const currentPageUrl = window.location.href;
console.log(currentPageUrl);
const match = currentPageUrl.match(/sales-logs\/(\d+)\/address-search/);
const id = match ? match[1] : null;
if (id) {
const response = await fetch(`/address_options/current?log_id=${id}`);
const data = await response.json();
console.log(data);
return data;
}
return null;
}
}

3
app/frontend/controllers/index.js

@ -19,6 +19,8 @@ import FilterLayoutController from './filter_layout_controller.js'
import TabsController from './tabs_controller.js'
import AddressAutocompleteController from './address_autocomplete_controller.js'
application.register('accessible-autocomplete', AccessibleAutocompleteController)
application.register('conditional-filter', ConditionalFilterController)
application.register('conditional-question', ConditionalQuestionController)
@ -27,3 +29,4 @@ application.register('numeric-question', NumericQuestionController)
application.register('filter-layout', FilterLayoutController)
application.register('search', SearchController)
application.register('tabs', TabsController)
application.register('address-autocomplete', AddressAutocompleteController)

2
app/helpers/address_options_helper.rb

@ -0,0 +1,2 @@
module AddressOptionsHelper
end

8
app/models/derived_variables/sales_log_variables.rb

@ -61,6 +61,7 @@ module DerivedVariables::SalesLogVariables
self.pcodenk = nil
self.postcode_full = nil
self.la = nil
self.address_search = nil
end
end
@ -75,6 +76,13 @@ module DerivedVariables::SalesLogVariables
self.pcodenk = nil
self.postcode_full = nil
self.la = nil
self.address_search = nil
end
if address_search
self.uprn = self.address_search
self.uprn_known = 1
self.uprn_confirmed = 1
end
if form.start_year_2025_or_later? && is_bedsit?

24
app/models/form/sales/pages/address_search.rb

@ -0,0 +1,24 @@
class Form::Sales::Pages::AddressSearch < ::Form::Page
def initialize(id, hsh, subsection)
super
@id = "address_search"
@copy_key = "sales.property_information.address_search"
@depends_on = [
{ "uprn_known" => nil },
{ "uprn_known" => 0 },
{ "uprn_confirmed" => 0 },
]
end
def questions
@questions ||= [
Form::Sales::Questions::AddressSearch.new(nil, nil, self),
]
end
def skip_href(log = nil)
return unless log
"/#{log.model_name.param_key.dasherize}s/#{log.id}/property-number-of-bedrooms"
end
end

32
app/models/form/sales/questions/address_search.rb

@ -0,0 +1,32 @@
class Form::Sales::Questions::AddressSearch < ::Form::Question
def initialize(id, hsh, page)
super
@id = "address_search"
@copy_key = "sales.property_information.address_search"
@error_label = "Enter search query"
@type = "address_autocomplete"
@plain_label = true
end
def answer_options(log = nil, _user = nil)
return {} unless ActiveRecord::Base.connected?
return {} unless log&.address_options
answer_opts = {}
(0...[log.address_options.count, 10].min).each do |i|
answer_opts[log.address_options[i][:uprn]] = { "value" => log.address_options[i][:address] }
end
answer_opts["divider"] = { "value" => true }
answer_opts
end
def displayed_answer_options(log, user = nil)
answer_options(log, user).transform_values { |value| value["value"] } || {}
end
def hidden_in_check_answers?(log, _current_user = nil)
(log.uprn_known == 1 || log.uprn_confirmed == 1)
end
end

2
app/models/form/sales/questions/uprn_confirmation.rb

@ -35,6 +35,6 @@ class Form::Sales::Questions::UprnConfirmation < ::Form::Question
end
def hidden_in_check_answers?(log, _current_user = nil)
log.uprn_known != 1 || log.uprn_confirmed.present?
log.uprn_known != 1 || log.uprn_confirmed.present? || log.address_search.present?
end
end

1
app/models/form/sales/subsections/property_information.rb

@ -26,6 +26,7 @@ class Form::Sales::Subsections::PropertyInformation < ::Form::Subsection
[
Form::Sales::Pages::Uprn.new(nil, nil, self),
Form::Sales::Pages::UprnConfirmation.new(nil, nil, self),
Form::Sales::Pages::AddressSearch.new(nil, nil, self),
Form::Sales::Pages::AddressMatcher.new(nil, nil, self),
Form::Sales::Pages::NoAddressFound.new(nil, nil, self),
Form::Sales::Pages::UprnSelection.new(nil, nil, self),

36
app/models/log.rb

@ -89,7 +89,7 @@ class Log < ApplicationRecord
def process_address_change!
if uprn_selection.present? || select_best_address_match.present?
if select_best_address_match
service = AddressClient.new(address_string)
service = AddressClient.new(address: address_string)
service.call
return nil if service.result.blank? || service.error.present?
@ -125,27 +125,29 @@ class Log < ApplicationRecord
"#{address_line1_input}, #{postcode_full_input}"
end
def address_options
return @address_options if @address_options && @last_searched_address_string == address_string
def address_options # TODO: use this method for autocomplete
search_query = address_search.presence || address_string
return @address_options if @address_options && @last_searched_address_string == search_query
if [address_line1_input, postcode_full_input].all?(&:present?)
@last_searched_address_string = address_string
search_query = address_search.presence || address_string
return if search_query.blank?
service = AddressClient.new(address_string)
service.call
if service.result.blank? || service.error.present?
@address_options = []
return @answer_options
end
@last_searched_address_string = search_query
address_opts = []
service.result.first(10).each do |result|
presenter = AddressDataPresenter.new(result)
address_opts.append({ address: presenter.address, uprn: presenter.uprn })
end
service = AddressClient.new(address: address_string)
service.call
if service.result.blank? || service.error.present?
@address_options = []
return @answer_options
end
@address_options = address_opts
address_opts = []
service.result.first(10).each do |result|
presenter = AddressDataPresenter.new(result)
address_opts.append({ address: presenter.address, uprn: presenter.uprn })
end
@address_options = address_opts
end
def collection_start_year

34
app/services/address_client.rb

@ -1,14 +1,14 @@
require "net/http"
class AddressClient
attr_reader :address
attr_reader :address, :uprn
attr_accessor :error
ADDRESS = "api.os.uk".freeze
PATH = "/search/places/v1/find".freeze
PATH_FIND = "/search/places/v1/find".freeze
PATH_UPRN = "/search/places/v1/uprn".freeze
def initialize(address)
def initialize(address: nil, uprn: nil)
@address = address
@uprn = uprn
end
def call
@ -27,7 +27,15 @@ class AddressClient
end
end
private
def result_by_uprn
if response.is_a?(Net::HTTPSuccess)
@result ||= JSON.parse(response.body)["result"]
else
@result = nil
end
end
private
def http_client
client = Net::HTTP.new(ADDRESS, 443)
@ -39,7 +47,7 @@ private
end
def endpoint_uri
uri = URI(PATH)
uri = URI(PATH_FIND)
params = {
query: address,
key: ENV["OS_DATA_KEY"],
@ -50,7 +58,17 @@ private
uri.to_s
end
def endpoint_uri_by_uprn
uri = URI(PATH_UPRN)
params = {
uprn: uprn,
key: ENV["OS_DATA_KEY"],
}
uri.query = URI.encode_www_form(params)
uri.to_s
end
def response
@response ||= http_client.request_get(endpoint_uri)
@response ||= http_client.request_get(address ? endpoint_uri : endpoint_uri_by_uprn)
end
end

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

@ -0,0 +1,23 @@
<% selected = @log.public_send(question.id) || "" %>
<% answers = question.displayed_answer_options(@log, current_user).map { |key, value| OpenStruct.new(id: key, name: select_option_name(value), resource: value) } %>
<%= render partial: "form/guidance/#{question.top_guidance_partial}" if question.top_guidance? %>
<%= f.govuk_select(question.id.to_sym,
label: legend(question, page_header, conditional),
"data-controller": "address-autocomplete",
caption: caption(caption_text, page_header, conditional),
hint: { text: question.hint_text&.html_safe }) do %>
<% if answers.any? %>
<% answers.each do |answer| %>
<option value="<%= answer.id %>"
data-synonyms="<%= answer_option_synonyms(answer.resource) %>"
data-append="<%= answer_option_append(answer.resource) %>"
data-hint="<%= answer_option_hint(answer.resource) %>"
<%= question.answer_selected?(@log, answer) ? "selected" : "" %>><%= answer.name || answer.resource %></option>
<% end %>
<% else %>
<option value="" disabled></option>
<% end %>
<% end %>
<%= render partial: "form/guidance/#{question.bottom_guidance_partial}" if question.bottom_guidance? %>

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

@ -1,20 +1,23 @@
<% selected = @log.public_send(question.id) || "" %>
<% answers = question.displayed_answer_options(@log, current_user).map { |key, value| OpenStruct.new(id: key, name: select_option_name(value), resource: value) } %>
<%= render partial: "form/guidance/#{question.top_guidance_partial}" if question.top_guidance? %>
<%= f.govuk_select(question.id.to_sym,
label: legend(question, page_header, conditional),
"data-controller": "accessible-autocomplete",
caption: caption(caption_text, page_header, conditional),
hint: { text: question.hint_text&.html_safe }) do %>
label: legend(question, page_header, conditional),
"data-controller": "accessible-autocomplete",
caption: caption(caption_text, page_header, conditional),
hint: { text: question.hint_text&.html_safe }) do %>
<% if answers.any? %>
<% answers.each do |answer| %>
<option value="<%= answer.id %>"
data-synonyms="<%= answer_option_synonyms(answer.resource) %>"
data-append="<%= answer_option_append(answer.resource) %>"
data-hint="<%= answer_option_hint(answer.resource) %>"
<%= question.answer_selected?(@log, answer) ? "selected" : "" %>><%= answer.name || answer.resource %></option>
data-synonyms="<%= answer_option_synonyms(answer.resource) %>"
data-append="<%= answer_option_append(answer.resource) %>"
data-hint="<%= answer_option_hint(answer.resource) %>"
<%= question.answer_selected?(@log, answer) ? "selected" : "" %>><%= answer.name || answer.resource %></option>
<% end %>
<% else %>
<option value="" disabled></option>
<% end %>
<% end %>
<%= render partial: "form/guidance/#{question.bottom_guidance_partial}" if question.bottom_guidance? %>

6
config/locales/forms/2024/sales/property_information.en.yml

@ -60,6 +60,12 @@ en:
hint_text: ""
question_text: "Postcode"
address_search:
page_header: "Search and find address"
check_answer_label: "Search - Find address"
hint_text: ""
question_text: "Find an address"
la:
page_header: ""
check_answer_label: "Local authority"

6
config/locales/forms/2025/sales/property_information.en.yml

@ -60,6 +60,12 @@ en:
hint_text: ""
question_text: "Postcode"
address_search:
page_header: "Search and find address"
check_answer_label: "Search - Find address"
hint_text: ""
question_text: "Find an address"
la:
page_header: ""
check_answer_label: "Local authority"

2
config/routes.rb

@ -39,6 +39,8 @@ Rails.application.routes.draw do
get "/data-sharing-agreement", to: "content#data_sharing_agreement"
get "/service-moved", to: "maintenance#service_moved"
get "/service-unavailable", to: "maintenance#service_unavailable"
get "/address_options", to: "address_options#index"
get "/address_options/current", to: "address_options#current"
get "collection-resources", to: "collection_resources#index"
get "/collection-resources/:log_type/:year/:resource_type/download", to: "collection_resources#download_mandatory_collection_resource", as: :download_mandatory_collection_resource

5
db/migrate/20241204145927_add_address_search_to_sales_logs.rb

@ -0,0 +1,5 @@
class AddAddressSearchToSalesLogs < ActiveRecord::Migration[7.0]
def change
add_column :sales_logs, :address_search, :string
end
end

7
db/schema.rb

@ -84,7 +84,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_06_142944) do
t.datetime "last_accessed"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.check_constraint "log_type::text = ANY (ARRAY['lettings'::character varying, 'sales'::character varying]::text[])", name: "log_type_check"
t.check_constraint "log_type::text = ANY (ARRAY['lettings'::character varying::text, 'sales'::character varying::text])", name: "log_type_check"
t.check_constraint "year >= 2000 AND year <= 2099", name: "year_check"
end
@ -761,13 +761,14 @@ ActiveRecord::Schema[7.2].define(version: 2024_12_06_142944) do
t.integer "partner_under_16_value_check"
t.integer "multiple_partners_value_check"
t.bigint "created_by_id"
t.integer "has_management_fee"
t.decimal "management_fee", precision: 10, scale: 2
t.integer "firststair"
t.integer "numstair"
t.decimal "mrentprestaircasing", precision: 10, scale: 2
t.datetime "lasttransaction"
t.datetime "initialpurchase"
t.integer "has_management_fee"
t.decimal "management_fee", precision: 10, scale: 2
t.string "address_search"
t.index ["assigned_to_id"], name: "index_sales_logs_on_assigned_to_id"
t.index ["bulk_upload_id"], name: "index_sales_logs_on_bulk_upload_id"
t.index ["created_by_id"], name: "index_sales_logs_on_created_by_id"

2
spec/models/form/lettings/questions/uprn_selection_spec.rb

@ -7,7 +7,7 @@ RSpec.describe Form::Lettings::Questions::UprnSelection, type: :model do
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, skip_href: "skip_href") }
let(:log) { build(:lettings_log, :in_progress, address_line1_input: "Address line 1", postcode_full_input: "AA1 1AA") }
let(:address_client_instance) { AddressClient.new(log.address_string) }
let(:address_client_instance) { AddressClient.new(address: log.address_string) }
before do
allow(AddressClient).to receive(:new).and_return(address_client_instance)

2
spec/models/form/sales/questions/uprn_selection_spec.rb

@ -7,7 +7,7 @@ RSpec.describe Form::Sales::Questions::UprnSelection, type: :model do
let(:question_definition) { nil }
let(:page) { instance_double(Form::Page, skip_href: "skip_href") }
let(:log) { build(:sales_log, :in_progress, address_line1_input: "Address line 1", postcode_full_input: "AA1 1AA") }
let(:address_client_instance) { AddressClient.new(log.address_string) }
let(:address_client_instance) { AddressClient.new(address: log.address_string) }
before do
allow(AddressClient).to receive(:new).and_return(address_client_instance)

7
spec/requests/address_options_spec.rb

@ -0,0 +1,7 @@
require 'rails_helper'
RSpec.describe "AddressOptions", type: :request do
describe "GET /index" do
pending "add some examples (or delete) #{__FILE__}"
end
end
Loading…
Cancel
Save