Skip to content

Commit 495f154

Browse files
authored
Merge pull request #6680 from NHSDigital/fetch-location-lat-lng
Fetch latitude and longitude for locations
2 parents 1feba03 + ea4c433 commit 495f154

13 files changed

Lines changed: 357 additions & 9 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
class EnqueueLocationPositionUpdaterJob < ApplicationJob
4+
queue_as :third_party_data_imports
5+
6+
def perform
7+
ids = Location.where(position: nil).has_address.ids
8+
LocationPositionUpdaterJob.perform_bulk(ids.zip)
9+
end
10+
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
class LocationPositionUpdaterJob
4+
include Sidekiq::Job
5+
6+
sidekiq_options queue: :third_party_data_imports, lock: :until_executing
7+
8+
def perform(location_id)
9+
location = Location.find(location_id)
10+
LocationPositionUpdater.call(location)
11+
rescue LocationPositionUpdater::NoResults => e
12+
Sentry.capture_exception(e, level: "warning")
13+
end
14+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
##
4+
# This class fetches the latitude and longitude of a location's address
5+
# using the +OrdnanceSurvey::PlacesAPI+ and stores it in the position column.
6+
class LocationPositionUpdater
7+
class MissingAddress < StandardError
8+
end
9+
10+
class NoResults < StandardError
11+
end
12+
13+
def initialize(location)
14+
@location = location
15+
end
16+
17+
attr_reader :location
18+
19+
def call
20+
raise MissingAddress unless location.has_address?
21+
22+
location.update!(position:)
23+
end
24+
25+
def self.call(...) = new(...).call
26+
27+
private_class_method :new
28+
29+
private
30+
31+
def full_address = location.address_parts.join(", ")
32+
33+
def position
34+
response = OrdnanceSurvey::PlacesAPI.find(full_address)
35+
36+
results = response[:results]
37+
raise NoResults if results.blank?
38+
39+
first_result = results.first
40+
41+
latitude = first_result.dig(:dpa, :lat)
42+
longitude = first_result.dig(:dpa, :lng)
43+
44+
raise NoResults if latitude.blank? || longitude.blank?
45+
46+
"POINT(#{longitude} #{latitude})"
47+
end
48+
end

app/lib/mavis_cli/clinics/create.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,17 @@ def call(
2626
)
2727
MavisCLI.load_rails
2828

29-
Location.create!(
30-
type: :community_clinic,
31-
name:,
32-
address_line_1:,
33-
address_town:,
34-
address_postcode:,
35-
ods_code:
36-
)
29+
location =
30+
Location.create!(
31+
type: :community_clinic,
32+
name:,
33+
address_line_1:,
34+
address_town:,
35+
address_postcode:,
36+
ods_code:
37+
)
38+
39+
LocationPositionUpdaterJob.perform_async(location.id)
3740
end
3841
end
3942
end

app/models/concerns/address_concern.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ module AddressConcern
1818
has_one :local_authority_from_postcode,
1919
through: :local_authority_postcode,
2020
source: :local_authority
21+
22+
scope :has_address,
23+
-> do
24+
where("address_line_1 IS NOT NULL AND address_line_1 <> ''")
25+
.or(where("address_line_2 IS NOT NULL AND address_line_2 <> ''"))
26+
.or(where("address_town IS NOT NULL AND address_town <> ''"))
27+
.or(
28+
where("address_postcode IS NOT NULL AND address_postcode <> ''")
29+
)
30+
end
2131
end
2232

2333
def address_parts

app/models/onboarding.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ def save!(include_previous_academic_year: false)
249249
end
250250

251251
PatientTeamUpdater.call(team_scope: Team.where(id: team.id))
252+
253+
location_ids =
254+
schools.map(&:id) + clinics.keys.filter(&:has_address?).map(&:id)
255+
LocationPositionUpdaterJob.perform_bulk(location_ids.zip)
252256
end
253257
end
254258

@@ -311,7 +315,7 @@ def location
311315
@location ||= Location.gias_school.find_by_urn_and_site(urn)
312316
end
313317

314-
delegate :status, to: :location, allow_nil: true
318+
delegate :id, :status, to: :location, allow_nil: true
315319
delegate :team, to: :subteam
316320

317321
def save!
@@ -347,6 +351,8 @@ class NewSchoolSite
347351
validates :name, presence: true
348352
validates :site, presence: true
349353

354+
delegate :id, to: :location
355+
350356
def original_location
351357
@original_location ||= Location.gias_school.find_by_urn_and_site(urn)
352358
end

config/sidekiq.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
description: Send vaccination confirmation emails to parents
4040

4141
# Nightly
42+
EnqueueLocationPositionUpdaterJob:
43+
cron: "0 0 * * *"
44+
description:
45+
Enqueue jobs to fetch latitude and longitude for locations with addresses but no position
4246
RemoveImportCSVJob:
4347
cron: "0 1 * * *"
4448
description: Remove CSV data from old cohort and immunisation imports

spec/factories/locations.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@
5858
end
5959
end
6060

61+
trait :without_address do
62+
address_line_1 { nil }
63+
address_line_2 { nil }
64+
address_town { nil }
65+
address_postcode { nil }
66+
position { nil }
67+
end
68+
6169
factory :community_clinic do
6270
type { :community_clinic }
6371
name { "#{Faker::University.name} Clinic" }

spec/features/cli_clinics_create_spec.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
it "runs successfully" do
88
when_i_run_the_command
99
then_the_clinic_is_created
10+
and_a_location_position_updater_job_is_enqueued
1011
end
1112
end
1213

@@ -29,4 +30,10 @@ def then_the_clinic_is_created
2930
expect(clinic.address_town).to eq("Town")
3031
expect(clinic.address_postcode).to eq("SW1A 1AA")
3132
end
33+
34+
def and_a_location_position_updater_job_is_enqueued
35+
expect(LocationPositionUpdaterJob).to have_enqueued_sidekiq_job(
36+
Location.last.id
37+
)
38+
end
3239
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
describe EnqueueLocationPositionUpdaterJob do
4+
describe "#perform" do
5+
subject(:perform_now) { described_class.perform_now }
6+
7+
let!(:location_with_address_no_position) do
8+
create(:community_clinic, position: nil)
9+
end
10+
11+
let!(:location_with_position) { create(:community_clinic) }
12+
13+
let!(:location_without_address) do
14+
create(:community_clinic, :without_address)
15+
end
16+
17+
it "enqueues jobs for locations with address but no position" do
18+
expect { perform_now }.to enqueue_sidekiq_job(
19+
LocationPositionUpdaterJob
20+
).with(location_with_address_no_position.id)
21+
end
22+
23+
it "does not enqueue jobs for locations with position" do
24+
expect { perform_now }.not_to enqueue_sidekiq_job(
25+
LocationPositionUpdaterJob
26+
).with(location_with_position.id)
27+
end
28+
29+
it "does not enqueue jobs for locations without address" do
30+
expect { perform_now }.not_to enqueue_sidekiq_job(
31+
LocationPositionUpdaterJob
32+
).with(location_without_address.id)
33+
end
34+
end
35+
end

0 commit comments

Comments
 (0)