Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c8084d2
wip
merefield Nov 17, 2023
7e67c40
wip
merefield Nov 17, 2023
3fcb7d8
add other fields
merefield Nov 18, 2023
b94dd3b
evolve
merefield Nov 19, 2023
e47e9b5
simplify method names
merefield Nov 19, 2023
18dfcb3
wip not working
merefield Nov 20, 2023
4d4ac1f
add geocoder scheme to locations topic
merefield Nov 20, 2023
6fde82b
evolve closeness functions
merefield Nov 20, 2023
e00b067
further evolution
merefield Nov 22, 2023
571834a
evolve more
merefield Nov 27, 2023
5fda353
move generic to Geocode file
merefield Nov 27, 2023
689c279
evolve
merefield Nov 28, 2023
617ee8a
add table data migrations
merefield Nov 28, 2023
63050c2
Merge branch 'main' into vector_table
merefield Nov 28, 2023
b208d45
rubocopped
merefield Nov 28, 2023
6c35b87
bump patch
merefield Nov 28, 2023
181fef3
impose coordinates order convention
merefield Nov 28, 2023
a1fb037
fix units parameter
merefield Nov 29, 2023
e938343
fix user location update
merefield Nov 29, 2023
86037f4
rubocopped
merefield Nov 29, 2023
8fa49cb
attempt to fix failing test migration with loss of foreign key constr…
merefield Nov 29, 2023
1838492
alternative definition
merefield Nov 29, 2023
9fb76d7
cleanup comment
merefield Nov 29, 2023
2c5fe46
remove potentially redundant line
merefield Nov 29, 2023
296a890
migrate table population to SQL to avoid rake task in migration
merefield Dec 1, 2023
2678c33
support various state changes for topics and users
merefield Dec 1, 2023
e736e43
defend against bad data in migration
merefield Dec 1, 2023
0893230
make migration extra safe
merefield Dec 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions app/models/topic_location.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module ::Locations
class TopicLocation < ActiveRecord::Base
extend Geocoder::Model::ActiveRecord
self.table_name = 'locations_topic'

belongs_to :topic
validates :longitude, presence: true
validates :latitude, presence: true
geocoded_by :address
after_validation :geocode
reverse_geocoded_by :latitude, :longitude
after_validation :reverse_geocode

def address
[street, city, state, postalcode, country].compact.join(', ')
end
end
end
22 changes: 22 additions & 0 deletions app/models/user_location.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true
require "geocoder"

module ::Locations
class UserLocation < ActiveRecord::Base
extend Geocoder::Model::ActiveRecord
self.table_name = 'locations_user'

belongs_to :user
validates :user_id, presence: true, uniqueness: true
validates :longitude, presence: true
validates :latitude, presence: true
geocoded_by :address
after_validation :geocode
reverse_geocoded_by :latitude, :longitude
after_validation :reverse_geocode

def address
[street, city, state, postalcode, country].compact.join(', ')
end
end
end
22 changes: 22 additions & 0 deletions db/migrate/20231117010101_create_locations_user_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

class CreateLocationsUserTable < ActiveRecord::Migration[7.0]
def change
create_table :locations_user do |t|
t.integer :user_id, null: false, index: { unique: true }, foreign_key: true
t.float :latitude, null: false
t.float :longitude, null: false
t.string :street, null: true
t.string :district, null: true
t.string :city, null: true
t.string :state, null: true
t.string :postalcode, null: true
t.string :country, null: true
t.string :countrycode, null: true
t.string :international_code, null: true
t.string :locationtype, null: true
t.float :boundingbox, array: true, null: true
t.timestamps
end
end
end
6 changes: 6 additions & 0 deletions db/migrate/20231117010103_create_locations_user_index.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true
class CreateLocationsUserIndex < ActiveRecord::Migration[7.0]
def change
add_index :locations_user, [:latitude, :longitude], name: 'composite_index_on_locations_user'
end
end
23 changes: 23 additions & 0 deletions db/migrate/20231119010101_create_locations_topic_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

class CreateLocationsTopicTable < ActiveRecord::Migration[7.0]
def change
create_table :locations_topic do |t|
t.integer :topic_id, null: false, index: { unique: true }, foreign_key: true
t.float :latitude, null: false
t.float :longitude, null: false
t.string :name, null: true
t.string :street, null: true
t.string :district, null: true
t.string :city, null: true
t.string :state, null: true
t.string :postalcode, null: true
t.string :country, null: true
t.string :countrycode, null: true
t.string :international_code, null: true
t.string :locationtype, null: true
t.float :boundingbox, array: true, null: true
t.timestamps
end
end
end
6 changes: 6 additions & 0 deletions db/migrate/20231119010103_create_locations_topic_index.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true
class CreateLocationsTopicIndex < ActiveRecord::Migration[7.0]
def change
add_index :locations_topic, [:latitude, :longitude], name: 'composite_index_on_locations_topic'
end
end
40 changes: 40 additions & 0 deletions db/migrate/20231128010101_populate_locations_user_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true
class PopulateLocationsUserTable < ActiveRecord::Migration[7.0]
def up
DB.exec <<~SQL
INSERT INTO locations_user (user_id, latitude, longitude, street, district, city, state, postalcode, country, countrycode, international_code, locationtype, boundingbox, updated_at, created_at) (
SELECT
uc.user_id,
(uc.value::json->>'lat')::FLOAT,
(uc.value::json->>'lon')::FLOAT,
uc.value::json->>'street',
uc.value::json->>'district',
uc.value::json->>'city',
uc.value::json->>'state',
uc.value::json->>'postalcode',
uc.value::json->>'country',
uc.value::json->>'countrycode',
uc.value::json->>'international_code',
uc.value::json->>'type',
ARRAY[
(uc.value::json->'boundingbox'->>0)::FLOAT,
(uc.value::json->'boundingbox'->>1)::FLOAT,
(uc.value::json->'boundingbox'->>2)::FLOAT,
(uc.value::json->'boundingbox'->>3)::FLOAT
],
uc.updated_at,
uc.created_at
FROM user_custom_fields uc
WHERE uc.name = 'geo_location'
AND uc.value NOT IN ('"{}"', '{}', '')
AND uc.value::json->>'lat' IS NOT NULL
AND uc.value::json->>'lon' IS NOT NULL
)
ON CONFLICT DO NOTHING
SQL
end

def down
::Locations::UserLocation.delete_all
end
end
41 changes: 41 additions & 0 deletions db/migrate/20231128010103_populate_locations_topic_table.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true
class PopulateLocationsTopicTable < ActiveRecord::Migration[7.0]
def up
DB.exec <<~SQL
INSERT INTO locations_topic (topic_id, latitude, longitude, name, street, district, city, state, postalcode, country, countrycode, international_code, locationtype, boundingbox, updated_at, created_at) (
SELECT
tc.topic_id,
(tc.value::json->'geo_location'->>'lat')::FLOAT,
(tc.value::json->'geo_location'->>'lon')::FLOAT,
tc.value::json->'geo_location'->>'name',
tc.value::json->'geo_location'->>'street',
tc.value::json->'geo_location'->>'district',
tc.value::json->'geo_location'->>'city',
tc.value::json->'geo_location'->>'state',
tc.value::json->'geo_location'->>'postalcode',
tc.value::json->'geo_location'->>'country',
tc.value::json->'geo_location'->>'countrycode',
tc.value::json->'geo_location'->>'international_code',
tc.value::json->'geo_location'->>'type',
ARRAY[
(tc.value::json->'geo_location'->'boundingbox'->>0)::FLOAT,
(tc.value::json->'geo_location'->'boundingbox'->>1)::FLOAT,
(tc.value::json->'geo_location'->'boundingbox'->>2)::FLOAT,
(tc.value::json->'geo_location'->'boundingbox'->>3)::FLOAT
],
tc.updated_at,
tc.created_at
FROM topic_custom_fields tc
WHERE tc.name = 'location'
AND tc.value NOT IN ('"{}"', '{}', '')
AND tc.value::json->'geo_location'->>'lat' IS NOT NULL
AND tc.value::json->'geo_location'->>'lon' IS NOT NULL
)
ON CONFLICT DO NOTHING
SQL
end

def down
::Locations::TopicLocation.delete_all
end
end
9 changes: 9 additions & 0 deletions lib/geocode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,15 @@ def self.perform(query, options = {})
end
end

def self.return_coords(query)
result = self.perform(query).first
"#{result.data['lat']}, #{result.data['lon']}"
end

def self.return_distance(lat1, lon1, lat2, lon2)
Geocoder::Calculations.distance_between([lat1, lon1], [lat2, lon2], units: :km)
end

def self.sorted_validators
@sorted_validators ||= []
end
Expand Down
98 changes: 98 additions & 0 deletions lib/tasks/locations.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true
desc "Update location table for each user"
task "locations:refresh_user_location_table", %i[missing_only delay] => :environment do |_, args|
ENV["RAILS_DB"] ? refresh_user_location_table(args) : refresh_user_location_table_all_sites(args)
end

def refresh_user_location_table_all_sites(args)
RailsMultisite::ConnectionManagement.each_connection { |db| refresh_user_location_table(args) }
end

def refresh_user_location_table(args)
puts "-" * 50
puts "Refreshing data for user locations for '#{RailsMultisite::ConnectionManagement.current_db}'"
puts "-" * 50

missing_only = args[:missing_only]&.to_i
delay = args[:delay]&.to_i

puts "for missing only" if !missing_only.to_i.zero?
puts "with a delay of #{delay} second(s) between API calls" if !delay.to_i.zero?
puts "-" * 50

if delay && delay < 1
puts "ERROR: delay parameter should be an integer and greater than 0"
exit 1
end

begin
total = User.count
refreshed = 0
batch = 1000

(0..(total - 1).abs).step(batch) do |i|
User
.order(id: :desc)
.offset(i)
.limit(batch)
.each do |user|
if !missing_only.to_i.zero? && ::Locations::UserLocation.find_by(user_id: user.id).nil? || missing_only.to_i.zero?
Locations::UserLocationProcess.upsert(user.id)
sleep(delay) if delay
end
print_status(refreshed += 1, total)
end
end
end

puts "", "#{refreshed} users done!", "-" * 50
end

desc "Update locations table for each topic"
task "locations:refresh_topic_location_table", %i[missing_only delay] => :environment do |_, args|
ENV["RAILS_DB"] ? refresh_topic_location_table(args) : refresh_topic_location_table_all_sites(args)
end

def refresh_topic_location_table_all_sites(args)
RailsMultisite::ConnectionManagement.each_connection { |db| refresh_topic_location_table(args) }
end

def refresh_topic_location_table(args)
puts "-" * 50
puts "Refreshing data for topic locations for '#{RailsMultisite::ConnectionManagement.current_db}'"
puts "-" * 50

missing_only = args[:missing_only]&.to_i
delay = args[:delay]&.to_i

puts "for missing only" if !missing_only.to_i.zero?
puts "with a delay of #{delay} second(s) between API calls" if !delay.to_i.zero?
puts "-" * 50

if delay && delay < 1
puts "ERROR: delay parameter should be an integer and greater than 0"
exit 1
end

begin
total = Topic.count
refreshed = 0
batch = 1000

(0..(total - 1).abs).step(batch) do |i|
Topic
.order(id: :desc)
.offset(i)
.limit(batch)
.each do |topic|
if !missing_only.to_i.zero? && ::Locations::TopicLocation.find_by(topic_id: topic.id).nil? || missing_only.to_i.zero?
Locations::TopicLocationProcess.upsert(topic.id)
sleep(delay) if delay
end
print_status(refreshed += 1, total)
end
end
end

puts "", "#{refreshed} topics done!", "-" * 50
end
61 changes: 61 additions & 0 deletions lib/topic_location_process.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

module Locations
class TopicLocationProcess

def self.upsert(topic_id)
topic = Topic.find_by(id: topic_id)

return if topic.nil? || !topic.custom_fields['location'].present? ||
topic.custom_fields['location']['geo_location'].blank? || !topic.custom_fields['location']['geo_location']['lat'].present? ||
!topic.custom_fields['location']['geo_location']['lon'].present?

::Locations::TopicLocation.upsert({
topic_id: topic_id,
latitude: topic.custom_fields['location']['geo_location']['lat'],
longitude: topic.custom_fields['location']['geo_location']['lon'],
name: topic.custom_fields['location']['name'] || nil,
street: topic.custom_fields['location']['street'] || nil,
district: topic.custom_fields['location']['district'] || nil,
city: topic.custom_fields['location']['city'] || nil,
state: topic.custom_fields['location']['state'] || nil,
postalcode: topic.custom_fields['location']['postalcode'] || nil,
country: topic.custom_fields['location']['country'] || nil,
countrycode: topic.custom_fields['location']['countrycode'] || nil,
international_code: topic.custom_fields['location']['international_code'] || nil,
locationtype: topic.custom_fields['location']['type'] || nil,
boundingbox: topic.custom_fields['location']['boundingbox'] || nil,
},
on_duplicate: :update, unique_by: :topic_id
)
end

def self.delete(topic_id)
location = ::Locations::TopicLocation.find_by(topic_id: topic_id)
location.delete if location
end

def self.search_topics_from_topic_location(topic_id, distance)
topic_location = TopicLocation.find_by(topic_id: topic_id)

return [] if !topic_location || !topic_location.geocoded?

topic_location.nearbys(distance, units: :km, select_distance: false, select_bearing: false).joins(:topic).pluck(:topic_id)
end

def self.search_topics_from_location(lat, lon, distance)

return [] if lat.nil? || lon.nil?

TopicLocation.near([lat.to_f, lon.to_f], distance, units: :km).joins(:topic).pluck(:topic_id)
end

def self.get_topic_distance_from_location(topic_id, lat, lon)
topic_location = TopicLocation.find_by(topic_id: topic_id)

return nil if !topic_location || !topic_location.geocoded?

topic_location.distance_to([lat, lon], units: :km)
end
end
end
Loading