Skip to content

Commit

Permalink
Merge branch 'master' of git://github.com/glennpow/geokit-rails into …
Browse files Browse the repository at this point in the history
…glennpow/master
  • Loading branch information
Andre Lewis committed Feb 16, 2009
2 parents 5b69057 + 4199fb2 commit bd51b58
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 43 deletions.
22 changes: 22 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,28 @@ condition alone in your SQL (there's no use calculating the distance twice):
bounds=Bounds.from_point_and_radius(home,5)
stores=Store.find :all, :include=>[:reviews,:cities] :bounds=>bounds
stores.sort_by_distance_from(home)

## USING :through

You can also specify a model as mappable "through" another associated model. In other words, that associated model is the
actual mappable model with "lat" and "lng" attributes, but this "through" model can still utilize all of the above find methods
to search for records.

class Location < ActiveRecord::Base
belongs_to :locatable, :polymorphic => true
acts_as_mappable
end

class Company < ActiveRecord::Base
has_one :location, :as => :locatable # also works for belongs_to associations
acts_as_mappable :through => :location
end

Then you can still call:

Company.find_within(distance, :origin => @somewhere)

Remember that the notes above about USING INCLUDES apply to the results from this find, since an include is automatically used.

## IP GEOCODING

Expand Down
20 changes: 9 additions & 11 deletions init.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# Load modules and classes needed to automatically mix in ActiveRecord and
# ActionController helpers. All other functionality must be explicitly
# required.
if defined? Geokit
require 'geokit' #requires the geokit gem
require 'geokit-rails/defaults'
require 'geokit-rails/acts_as_mappable'
require 'geokit-rails/ip_geocode_lookup'

# Automatically mix in distance finder support into ActiveRecord classes.
ActiveRecord::Base.send :include, GeoKit::ActsAsMappable
require 'geokit' #requires the geokit gem
require 'geokit-rails/defaults'
require 'geokit-rails/acts_as_mappable'
require 'geokit-rails/ip_geocode_lookup'

# Automatically mix in ip geocoding helpers into ActionController classes.
ActionController::Base.send :include, GeoKit::IpGeocodeLookup
end
# Automatically mix in distance finder support into ActiveRecord classes.
ActiveRecord::Base.send :include, GeoKit::ActsAsMappable

# Automatically mix in ip geocoding helpers into ActionController classes.
ActionController::Base.send :include, GeoKit::IpGeocodeLookup
67 changes: 48 additions & 19 deletions lib/geokit-rails/acts_as_mappable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,29 +48,42 @@ module ClassMethods # :nodoc:
# and create your own AR callback to handle geocoding.
def acts_as_mappable(options = {})
# Mix in the module, but ensure to do so just once.
return if self.included_modules.include?(Geokit::ActsAsMappable::InstanceMethods)
return if !defined?(Geokit::Mappable) || self.included_modules.include?(Geokit::ActsAsMappable::InstanceMethods)
send :include, Geokit::ActsAsMappable::InstanceMethods
# include the Mappable module.
send :include, Geokit::Mappable

# Handle class variables.
cattr_accessor :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name
self.distance_column_name = options[:distance_column_name] || 'distance'
self.default_units = options[:default_units] || Geokit::default_units
self.default_formula = options[:default_formula] || Geokit::default_formula
self.lat_column_name = options[:lat_column_name] || 'lat'
self.lng_column_name = options[:lng_column_name] || 'lng'
self.qualified_lat_column_name = "#{table_name}.#{lat_column_name}"
self.qualified_lng_column_name = "#{table_name}.#{lng_column_name}"
if options.include?(:auto_geocode) && options[:auto_geocode]
# if the form auto_geocode=>true is used, let the defaults take over by suppling an empty hash
options[:auto_geocode] = {} if options[:auto_geocode] == true
cattr_accessor :auto_geocode_field, :auto_geocode_error_message
self.auto_geocode_field = options[:auto_geocode][:field] || 'address'
self.auto_geocode_error_message = options[:auto_geocode][:error_message] || 'could not locate address'
cattr_accessor :through
if self.through = options[:through]
if reflection = self.reflect_on_association(self.through)
(class << self; self; end).instance_eval do
[ :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name ].each do |method_name|
define_method method_name do
reflection.klass.send(method_name)
end
end
end
end
else
cattr_accessor :distance_column_name, :default_units, :default_formula, :lat_column_name, :lng_column_name, :qualified_lat_column_name, :qualified_lng_column_name
self.distance_column_name = options[:distance_column_name] || 'distance'
self.default_units = options[:default_units] || Geokit::default_units
self.default_formula = options[:default_formula] || Geokit::default_formula
self.lat_column_name = options[:lat_column_name] || 'lat'
self.lng_column_name = options[:lng_column_name] || 'lng'
self.qualified_lat_column_name = "#{table_name}.#{lat_column_name}"
self.qualified_lng_column_name = "#{table_name}.#{lng_column_name}"
if options.include?(:auto_geocode) && options[:auto_geocode]
# if the form auto_geocode=>true is used, let the defaults take over by suppling an empty hash
options[:auto_geocode] = {} if options[:auto_geocode] == true
cattr_accessor :auto_geocode_field, :auto_geocode_error_message
self.auto_geocode_field = options[:auto_geocode][:field] || 'address'
self.auto_geocode_error_message = options[:auto_geocode][:error_message] || 'could not locate address'

# set the actual callback here
before_validation_on_create :auto_geocode_address
# set the actual callback here
before_validation_on_create :auto_geocode_address
end
end
end
end
Expand Down Expand Up @@ -198,7 +211,10 @@ def distance_sql(origin, units=default_units, formula=default_formula)
# Prepares either a find or a count action by parsing through the options and
# conditionally adding to the select clause for finders.
def prepare_for_find_or_count(action, args)
options = defined?(args.extract_options!) ? args.extract_options! : extract_options_from_args!(args)
options = args.extract_options!
#options = defined?(args.extract_options!) ? args.extract_options! : extract_options_from_args!(args)
# Handle :through
apply_include_for_through(options)
# Obtain items affecting distance condition.
origin = extract_origin_from_options(options)
units = extract_units_from_options(options)
Expand All @@ -222,6 +238,19 @@ def prepare_for_find_or_count(action, args)
args.push(options)
end

def apply_include_for_through(options)
if self.through
case options[:include]
when Array
options[:include] << self.through
when Hash, String, Symbol
options[:include] = [ self.through, options[:include] ]
else
options[:include] = self.through
end
end
end

# If we're here, it means that 1) an origin argument, 2) an :include, 3) an :order clause were supplied.
# Now we have to sub some SQL into the :order clause. The reason is that when you do an :include,
# ActiveRecord drops the psuedo-column (specificically, distance) which we supplied for :select.
Expand Down Expand Up @@ -354,7 +383,7 @@ def add_distance_to_select(options, origin, units=default_units, formula=default
distance_selector = distance_sql(origin, units, formula) + " AS #{distance_column_name}"
selector = options.has_key?(:select) && options[:select] ? options[:select] : "*"
options[:select] = "#{selector}, #{distance_selector}"
end
end
end

# Looks for the distance column and replaces it with the distance sql. If an origin was not
Expand Down
40 changes: 32 additions & 8 deletions test/acts_as_mappable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,27 @@ def to_s
end
end

class ActsAsMappableTest < Test::Unit::TestCase #:nodoc: all
# Uses :through
class MockOrganization < ActiveRecord::Base #:nodoc: all
has_one :mock_address, :as => :addressable
acts_as_mappable :through => :mock_address
end

# Used by :through
class MockAddress < ActiveRecord::Base #:nodoc: all
belongs_to :addressable, :polymorphic => true
acts_as_mappable
end

class ActsAsMappableTest < ActiveSupport::TestCase #:nodoc: all

LOCATION_A_IP = "217.10.83.5"

#self.fixture_path = File.dirname(__FILE__) + '/fixtures'
#self.fixture_path = RAILS_ROOT + '/test/fixtures/'
#puts "Rails Path #{RAILS_ROOT}"
#puts "Fixture Path: #{self.fixture_path}"
#self.fixture_path = ' /Users/bill_eisenhauer/Projects/geokit_test/test/fixtures/'
fixtures :companies, :locations, :custom_locations, :stores
self.fixture_path = File.dirname(__FILE__) + '/fixtures'
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = true
fixtures :companies, :locations, :custom_locations, :stores, :mock_organizations, :mock_addresses

def setup
@location_a = GeoKit::GeoLoc.new
Expand All @@ -62,7 +73,10 @@ def setup
@loc_a = locations(:a)
@custom_loc_a = custom_locations(:a)
@loc_e = locations(:e)
@custom_loc_e = custom_locations(:e)
@custom_loc_e = custom_locations(:e)

@barnes_and_noble = mock_organizations(:barnes_and_noble)
@address = mock_addresses(:address_barnes_and_noble)
end

def test_override_default_units_the_hard_way
Expand Down Expand Up @@ -478,4 +492,14 @@ def test_auto_geocode_failure
assert store.new_record?
assert_equal 1, store.errors.size
end


# Test :through

def test_find_with_through
organizations = MockOrganization.find(:all, :origin => @location_a, :order => 'distance ASC')
assert_equal 2, organizations.size
organizations = MockOrganization.count(:origin => @location_a, :conditions => "distance < 3.97")
assert_equal 1, organizations
end
end
17 changes: 17 additions & 0 deletions test/fixtures/mock_addresses.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
address_starbucks:
addressable: starbucks (MockOrganization)
street: 106 N Denton Tap Rd # 350
city: Coppell
state: TX
postal_code: 75019
lat: 32.969527
lng: -96.990159

address_barnes_and_noble:
addressable: barnes_and_noble (MockOrganization)
street: 5904 N Macarthur Blvd # 160
city: Irving
state: TX
postal_code: 75039
lat: 32.895155
lng: -96.958444
5 changes: 5 additions & 0 deletions test/fixtures/mock_organizations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
starbucks:
name: Starbucks

barnes_and_noble:
name: Barnes & Noble
2 changes: 1 addition & 1 deletion test/ip_geocode_lookup_test.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require File.join(File.dirname(__FILE__), '../../../../config/environment')
require ENV['environment'] || File.join(plugin_test_dir, '../../../../config/environment')
require 'action_controller/test_process'
require 'test/unit'
require 'rubygems'
Expand Down
15 changes: 15 additions & 0 deletions test/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,19 @@
t.column :lat, :decimal, :precision => 15, :scale => 10
t.column :lng, :decimal, :precision => 15, :scale => 10
end

create_table :mock_organizations, :force => true do |t|
t.column :name, :string
end

create_table :mock_addresses, :force => true do |t|
t.column :addressable_id, :integer, :null => false
t.column :addressable_type, :string, :null => false
t.column :street, :string, :limit => 60
t.column :city, :string, :limit => 60
t.column :state, :string, :limit => 2
t.column :postal_code, :string, :limit => 16
t.column :lat, :decimal, :precision => 15, :scale => 10
t.column :lng, :decimal, :precision => 15, :scale => 10
end
end
12 changes: 8 additions & 4 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
require 'test/unit'
ENV["RAILS_ENV"] = "test"
require "test/unit"
require "rubygems"
#require File.dirname(__FILE__) + '/../init'
require 'geokit'

plugin_test_dir = File.dirname(__FILE__)

# Load the Rails environment
require File.join(plugin_test_dir, '../../../../config/environment')
require 'active_record/fixtures'
require ENV['environment'] || File.join(plugin_test_dir, '../../../../config/environment')
require 'test_help'
#require 'active_record/fixtures'
databases = YAML::load(IO.read(plugin_test_dir + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")

Expand All @@ -16,4 +20,4 @@
load(File.join(plugin_test_dir, 'schema.rb'))

# Load fixtures from the plugin
Test::Unit::TestCase.fixture_path = File.join(plugin_test_dir, 'fixtures/')
#Test::Unit::TestCase.fixture_path = File.join(plugin_test_dir, 'fixtures/')

1 comment on commit bd51b58

@buncis
Copy link

@buncis buncis commented on bd51b58 Sep 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any idea why it doesn't works now?

the workaround is using .joins(:your_through_table_name) to works

related issue #92

Please sign in to comment.