acts_as_geocodable 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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