acts_as_geocodable 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ class Geocode < ActiveRecord::Base
2
+ has_many :geocodings, :dependent => :destroy
3
+
4
+ validates_uniqueness_of :query
5
+
6
+ cattr_accessor :geocoder
7
+
8
+ def distance_to(destination, units = :miles, formula = :haversine)
9
+ if destination && destination.latitude && destination.longitude
10
+ Graticule::Distance.const_get(formula.to_s.camelize).distance(self, destination, units)
11
+ end
12
+ end
13
+
14
+ def geocoded?
15
+ !latitude.blank? && !longitude.blank?
16
+ end
17
+
18
+ def self.find_or_create_by_query(query)
19
+ find_by_query(query) || create_by_query(query)
20
+ end
21
+
22
+ def self.create_by_query(query)
23
+ create geocoder.locate(query).attributes.merge(:query => query)
24
+ end
25
+
26
+ def self.find_or_create_by_location(location)
27
+ find_by_query(location.to_s) || create_from_location(location)
28
+ end
29
+
30
+ def self.create_from_location(location)
31
+ create geocoder.locate(location).attributes.merge(:query => location.to_s)
32
+ rescue Graticule::Error => e
33
+ logger.warn e.message
34
+ nil
35
+ end
36
+
37
+ def precision=(name)
38
+ self[:precision] = name.to_s
39
+ end
40
+
41
+ def geocoded
42
+ @geocoded ||= geocodings.collect { |geocoding| geocoding.geocodable }
43
+ end
44
+
45
+ def on(geocodable)
46
+ geocodings.create :geocodable => geocodable
47
+ end
48
+
49
+ def coordinates
50
+ "#{longitude},#{latitude}"
51
+ end
52
+
53
+ def to_s
54
+ coordinates
55
+ end
56
+
57
+ # Create a Graticule::Location
58
+ def to_location
59
+ Graticule::Location.new(attributes.except('id', 'query'))
60
+ end
61
+ end
@@ -0,0 +1,12 @@
1
+ class Geocoding < ActiveRecord::Base
2
+ belongs_to :geocode
3
+ belongs_to :geocodable, :polymorphic => true
4
+
5
+ def self.geocoded_class(geocodable)
6
+ ActiveRecord::Base.send(:class_name_of_active_record_descendant, geocodable.class).to_s
7
+ end
8
+
9
+ def self.find_geocodable(geocoded_class, geocoded_id)
10
+ geocoded_class.constantize.find(geocoded_id)
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module RemoteLocation #:nodoc:
3
+
4
+ # Get the remote location of the request IP using http://hostip.info
5
+ def remote_location
6
+ if request.remote_ip == '127.0.0.1'
7
+ # otherwise people would complain that it doesn't work
8
+ Graticule::Location.new(:locality => 'localhost')
9
+ else
10
+ Graticule.service(:host_ip).new.locate(request.remote_ip)
11
+ end
12
+ rescue Graticule::Error => e
13
+ logger.warn "An error occurred while looking up the location of '#{request.remote_ip}': #{e.message}"
14
+ nil
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :acts_as_geocodable do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,5 @@
1
+ require 'graticule'
2
+ require 'acts_as_geocodable'
3
+
4
+ ActiveRecord::Base.send :include, CollectiveIdea::Acts::Geocodable
5
+ ActionController::Base.send :include, CollectiveIdea::RemoteLocation
@@ -0,0 +1,340 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'shoulda/rails'
3
+
4
+ class Vacation < ActiveRecord::Base
5
+ acts_as_geocodable :normalize_address => true
6
+ belongs_to :nearest_city, :class_name => 'City', :foreign_key => 'city_id'
7
+ end
8
+
9
+ class City < ActiveRecord::Base
10
+ acts_as_geocodable :address => {:postal_code => :zip}
11
+ end
12
+
13
+ class ValidatedVacation < ActiveRecord::Base
14
+ acts_as_geocodable
15
+ validates_as_geocodable
16
+ end
17
+
18
+ class AddressBlobVacation < ActiveRecord::Base
19
+ acts_as_geocodable :address => :address, :normalize_address => true
20
+ end
21
+
22
+ class CallbackLocation < ActiveRecord::Base
23
+ acts_as_geocodable :address => :address
24
+ after_geocoding :done_geocoding
25
+
26
+ def done_geocoding
27
+ true
28
+ end
29
+ end
30
+
31
+ class ActsAsGeocodableTest < ActiveSupport::TestCase
32
+ fixtures :vacations, :cities, :geocodes, :geocodings
33
+
34
+ # enable Should macros
35
+ def self.model_class
36
+ Vacation
37
+ end
38
+
39
+ should_have_one :geocoding
40
+
41
+ context "geocode" do
42
+ setup do
43
+ @location = vacations(:whitehouse)
44
+ end
45
+
46
+ should "be the geocode from the geocoding" do
47
+ @location.geocode.should == @location.geocoding.geocode
48
+ end
49
+
50
+ should "be nil without a geocoding" do
51
+ Vacation.new.geocode.should be(nil)
52
+ end
53
+
54
+ end
55
+
56
+ context "to_location" do
57
+ should "return a graticule location" do
58
+ expected = Graticule::Location.new :street => '1600 Pennsylvania Ave NW',
59
+ :locality => 'Washington', :region => 'DC', :postal_code => '20502',
60
+ :country => nil
61
+ vacations(:whitehouse).to_location.should == expected
62
+ end
63
+
64
+ should "return a graticule location for mapped locations" do
65
+ cities(:holland).to_location.should == Graticule::Location.new(:postal_code => '49423')
66
+ end
67
+ end
68
+
69
+ context "with address normalization" do
70
+ setup do
71
+ Vacation.acts_as_geocodable_options[:normalize_address] = true
72
+
73
+ Geocode.geocoder.stubs(:locate).returns(
74
+ Graticule::Location.new(:locality => "San Clemente", :region => "CA")
75
+ )
76
+ end
77
+
78
+ should "update address fields with result" do
79
+ vacation = Vacation.create! :locality => 'sanclemente', :region => 'ca'
80
+ vacation.locality.should == 'San Clemente'
81
+ vacation.region.should == 'CA'
82
+ end
83
+
84
+ should "update address blob" do
85
+ Geocode.geocoder.expects(:locate).returns(
86
+ Graticule::Location.new(:locality => "Grand Rapids", :region => "MI", :country => "US")
87
+ )
88
+
89
+ vacation = AddressBlobVacation.create! :address => "grand rapids, mi"
90
+ vacation.address.should == "Grand Rapids, MI US"
91
+ end
92
+
93
+ end
94
+
95
+ context "without address normalization" do
96
+ setup do
97
+ Vacation.acts_as_geocodable_options[:normalize_address] = false
98
+
99
+ Geocode.geocoder.stubs(:locate).returns(
100
+ Graticule::Location.new(:locality => "Portland", :region => "OR", :postal_code => '97212')
101
+ )
102
+
103
+ @vacation = Vacation.create! :locality => 'portland', :region => 'or'
104
+ end
105
+
106
+ should "not update address attributes" do
107
+ @vacation.locality.should == 'portland'
108
+ @vacation.region.should == 'or'
109
+ end
110
+
111
+ should "fill in blank attributes" do
112
+ @vacation.postal_code.should == '97212'
113
+ end
114
+ end
115
+
116
+ context "with blank location attributes" do
117
+ should "not create geocode" do
118
+ Geocode.geocoder.expects(:locate).never
119
+ assert_no_difference 'Geocode.count + Geocoding.count' do
120
+ Vacation.create!(:locality => "\n", :region => " ").geocoding.should be(nil)
121
+ end
122
+ end
123
+
124
+ should "destroy existing geocoding" do
125
+ whitehouse = vacations(:whitehouse)
126
+ [:name, :street, :locality, :region, :postal_code].each do |attribute|
127
+ whitehouse.send("#{attribute}=", nil)
128
+ end
129
+ assert_difference 'Geocoding.count', -1 do
130
+ whitehouse.save!
131
+ end
132
+ whitehouse.reload
133
+ whitehouse.geocoding.should be(nil)
134
+ end
135
+ end
136
+
137
+ context "on save" do
138
+ should "not create geocode without changes" do
139
+ whitehouse = vacations(:whitehouse)
140
+ assert_not_nil whitehouse.geocoding
141
+ original_geocode = whitehouse.geocode
142
+
143
+ assert_no_difference 'Geocode.count + Geocoding.count' do
144
+ whitehouse.save!
145
+ whitehouse.reload
146
+ end
147
+
148
+ assert_equal original_geocode, whitehouse.geocode
149
+ end
150
+
151
+ end
152
+
153
+ context "on save with an existing geocode" do
154
+ setup do
155
+ @location = vacations(:saugatuck)
156
+ @location.attributes = {:locality => 'Beverly Hills', :postal_code => '90210'}
157
+ end
158
+
159
+ should "destroy the old geocoding" do
160
+ assert_no_difference('Geocoding.count') { @location.save! }
161
+ end
162
+
163
+ should "set the new geocode" do
164
+ @location.save!
165
+ @location.geocode.should == geocodes(:beverly_hills)
166
+ end
167
+
168
+ end
169
+
170
+ context "validates_as_geocodable" do
171
+ setup do
172
+ @vacation = ValidatedVacation.new :locality => "Grand Rapids", :region => "MI"
173
+ end
174
+
175
+ should "be invalid without geocodable address" do
176
+ Geocode.geocoder.expects(:locate).raises(Graticule::Error)
177
+ assert !@vacation.valid?
178
+ assert_equal 1, @vacation.errors.size
179
+ assert_equal "Address could not be geocoded.", @vacation.errors.on(:base)
180
+ end
181
+
182
+ should "be valid with geocodable address" do
183
+ assert @vacation.valid?
184
+ end
185
+ end
186
+
187
+ context "find with origin" do
188
+ should "add distance to result" do
189
+ Vacation.find(1, :origin => "49406").distance.to_f.should be_close(0.794248231790402, 0.2)
190
+ end
191
+ end
192
+
193
+ context "find within" do
194
+ setup do
195
+ @results = Vacation.find(:all, :origin => 49406, :within => 10)
196
+ end
197
+
198
+ should "find locations within radius" do
199
+ @results.should include(vacations(:saugatuck))
200
+ end
201
+
202
+ should "add distance to results" do
203
+ @results.first.distance.to_f.should be_close(0.794248231790402, 0.2)
204
+ end
205
+
206
+ def test_find_within
207
+ spots = Vacation.find(:all, :origin => "49406", :within => 3)
208
+ assert_equal 1, spots.size
209
+ assert_equal vacations(:saugatuck), spots.first
210
+ end
211
+
212
+ def test_count_within
213
+ spots_count = Vacation.count(:origin => "49406", :within => 3)
214
+ assert_equal 1, spots_count
215
+ end
216
+
217
+ def test_within_kilometers
218
+ saugatuck = Vacation.find(:first, :within => 2, :units => :kilometers, :origin => "49406")
219
+ assert_equal vacations(:saugatuck), saugatuck
220
+ assert_in_delta 1.27821863, saugatuck.distance, 0.2
221
+ end
222
+
223
+ end
224
+
225
+ context "distance_to" do
226
+ setup do
227
+ @saugatuck = vacations(:saugatuck)
228
+ @douglas = Vacation.create!(:name => 'Douglas', :postal_code => '49406')
229
+ end
230
+
231
+ should 'calculate distance from a string' do
232
+ @douglas.distance_to(geocodes(:saugatuck_geocode).query).should be_close(0.794248231790402, 0.2)
233
+ end
234
+ should 'calculate distance from a geocode' do
235
+ @douglas.distance_to(geocodes(:saugatuck_geocode)).should be_close(0.794248231790402, 0.2)
236
+ end
237
+
238
+ should 'calculate distance from a geocodable model' do
239
+ @douglas.distance_to(@saugatuck).should be_close(0.794248231790402, 0.2)
240
+ @saugatuck.distance_to(@douglas).should be_close(0.794248231790402, 0.2)
241
+ end
242
+
243
+ should 'calculate distance in default miles' do
244
+ @douglas.distance_to(@saugatuck, :units => :miles).should be_close(0.794248231790402, 0.2)
245
+ end
246
+
247
+ should 'calculate distance in default kilometers' do
248
+ @douglas.distance_to(@saugatuck, :units => :kilometers).should be_close(1.27821863, 0.2)
249
+ end
250
+
251
+ should 'return nil with invalid geocode' do
252
+ @douglas.distance_to(Geocode.new).should be(nil)
253
+ @douglas.distance_to(nil).should be(nil)
254
+ end
255
+
256
+ end
257
+
258
+ def test_find_beyond
259
+ spots = Vacation.find(:all, :origin => "49406", :beyond => 3)
260
+ assert_equal 1, spots.size
261
+ assert_equal vacations(:whitehouse), spots.first
262
+ end
263
+
264
+ def test_count_beyond
265
+ spots = Vacation.count(:origin => "49406", :beyond => 3)
266
+ assert_equal 1, spots
267
+ end
268
+
269
+ def test_find_beyond_in_kilometers
270
+ whitehouse = Vacation.find(:first, :beyond => 3, :units => :kilometers, :origin => "49406")
271
+ assert_equal vacations(:whitehouse), whitehouse
272
+ assert_in_delta 877.554975851074, whitehouse.distance, 1
273
+ end
274
+
275
+ def test_find_nearest
276
+ assert_equal vacations(:saugatuck), Vacation.find(:nearest, :origin => "49406")
277
+ end
278
+
279
+ def test_find_nearest
280
+ assert_equal vacations(:whitehouse), Vacation.find(:farthest, :origin => "49406")
281
+ end
282
+
283
+ def test_find_nearest_with_include_raises_error
284
+ assert_raises(ArgumentError) { Vacation.find(:nearest, :origin => '49406', :include => :nearest_city) }
285
+ end
286
+
287
+ def test_uses_units_set_in_declared_options
288
+ Vacation.acts_as_geocodable_options.merge! :units => :kilometers
289
+ saugatuck = Vacation.find(:first, :within => 2, :units => :kilometers, :origin => "49406")
290
+ assert_in_delta 1.27821863, saugatuck.distance, 0.2
291
+ end
292
+
293
+ def test_find_with_order
294
+ expected = [vacations(:saugatuck), vacations(:whitehouse)]
295
+ actual = Vacation.find(:all, :origin => '49406', :order => 'distance')
296
+ assert_equal expected, actual
297
+ end
298
+
299
+ def test_location_to_geocode_nil
300
+ assert_nil Vacation.send(:location_to_geocode, nil)
301
+ end
302
+
303
+ def test_location_to_geocode_with_geocode
304
+ g = Geocode.new
305
+ assert(g === Vacation.send(:location_to_geocode, g))
306
+ end
307
+
308
+ def test_location_to_geocode_with_string
309
+ assert_equal geocodes(:douglas), Vacation.send(:location_to_geocode, '49406')
310
+ end
311
+
312
+ def test_location_to_geocode_with_fixnum
313
+ assert_equal geocodes(:douglas), Vacation.send(:location_to_geocode, 49406)
314
+ end
315
+
316
+ def test_location_to_geocode_with_geocodable
317
+ assert_equal geocodes(:white_house_geocode),
318
+ Vacation.send(:location_to_geocode, vacations(:whitehouse))
319
+ end
320
+
321
+ def test_find_nearest_raises_error_with_include
322
+ assert_raises(ArgumentError) { Vacation.find(:nearest, :include => :nearest_city, :origin => 49406) }
323
+ end
324
+
325
+ def test_callback_after_geocoding
326
+ location = CallbackLocation.new :address => "Holland, MI"
327
+ assert_nil location.geocoding
328
+ location.expects(:done_geocoding).once.returns(true)
329
+ assert location.save!
330
+ end
331
+
332
+ def test_does_not_run_the_callback_after_geocoding_if_object_dont_change
333
+ location = CallbackLocation.create(:address => "Holland, MI")
334
+ assert_not_nil location.geocoding
335
+ location.expects(:done_geocoding).never
336
+ assert location.save!
337
+ end
338
+
339
+
340
+ end
@@ -0,0 +1,18 @@
1
+ sqlite:
2
+ :adapter: sqlite
3
+ :dbfile: acts_as_geocodable_plugin.sqlite.db
4
+ sqlite3:
5
+ :adapter: sqlite3
6
+ :dbfile: acts_as_geocodable_plugin.sqlite3.db
7
+ postgresql:
8
+ :adapter: postgresql
9
+ :username: postgres
10
+ :password: postgres
11
+ :database: acts_as_geocodable_plugin_test
12
+ :min_messages: ERROR
13
+ mysql:
14
+ :adapter: mysql
15
+ :host: localhost
16
+ :username: root
17
+ :password:
18
+ :database: acts_as_geocodable_plugin_test