Rails Geocoder Testing with rspec - ruby-on-rails

I am trying to set up my RSpec tests to use stubs rather then using networking to do the geocoding.
I added this:
before(:each) do
Geocoder.configure(:lookup => :test)
Geocoder::Lookup::Test.add_stub(
"Los Angeles, CA", [{
:latitude => 34.052363,
:longitude => -118.256551,
:address => 'Los Angeles, CA, USA',
:state => 'California',
:state_code => 'CA',
:country => 'United States',
:country_code => 'US'
}],
)
end
I am using FactoryGirl to create the test data like so:
FactoryGirl.define do
factory :market do
city 'Los Angeles'
state 'CA'
radius 20.0
end
end
The latitude/longitude are correctly being geocoded and stored in latitude/longitude. However, when I try:
Market.near(params[:search])
it returns nil.. But, if I just use the lookup => :google it works just as I intend it to. Has anyone got this working before, specifically the near method of geocoder?

I ended up coming back to this on a new project and figured it out.
The docs on geocoder actually state that the hash has to have string keys and not symbols. geocoder docs - see notes
i.e.
before(:each) do
Geocoder.configure(:lookup => :test)
Geocoder::Lookup::Test.add_stub(
"Los Angeles, CA", [{
"latitude" => 34.052363,
"longitude" => -118.256551,
"address" => 'Los Angeles, CA, USA',
"state" => 'California',
"state_code" => 'CA',
"country" => 'United States',
"country_code" => 'US'
}],
)
end
and not how I did it in the original post:
i.e. :latitude => 34.052363
I ended up doing something a bit more dynamic:
# frozen_string_literal: true
module GeocoderStub
def self.stub_with(facility)
Geocoder.configure(lookup: :test)
results = [
{
'latitude' => Faker::Address.latitude.first(9),
'longitude' => Faker::Address.longitude.first(9)
}
]
queries = [facility.full_address, facility.zip]
queries.each { |q| Geocoder::Lookup::Test.add_stub(q, results) }
end
end
and in my factory:
require './spec/support/geocoder_stub'
FactoryGirl.define do
factory :facility do
name { Faker::Company.name }
rating { rand(1..5) }
street { Faker::Address.street_address }
city { Faker::Address.city }
state { Faker::Address.state }
zip { Faker::Address.zip_code }
after(:build) { |facility| GeocoderStub.stub_with(facility) }
end
end
This adds a geocoder stub for every Facility factory that is built for both full address (method defined in facility) and zip.

I found a simpler approach to be just stubbing everything with the same values by default:
# test/test_helper.rb
Geocoder.configure(lookup: :test)
Geocoder::Lookup::Test.set_default_stub([{ coordinates: [40.7143528, -74.0059731] }])
Also, to avoid unnecessary calls, it's also a good idea to restrict your callbacks:
class Account < ActiveRecord
after_validation :geocode, if: ->(obj) { obj.address.present? and obj.address_changed? }
end
Source: https://github.com/alexreisner/geocoder#testing

Related

Rails Rspec and Geocoder IP lookup

Geocoder DOCS explain how to fake a Geocoder lookup with Street addresses, but it doesn't seem to work with IP addresses.
This is my spec/support/geocoder.rb
Geocoder.configure(:lookup => :test)
Geocoder::Lookup::Test.add_stub(
"1.1.1.1", [
{
'latitude' => 10,
'longitude' => 10,
'address' => 'Test Address',
'state' => 'Test State',
'state_code' => 'TS',
'country' => 'Test Country',
'country_code' => 'TC'
}
]
)
Geocoder::Lookup::Test.set_default_stub(
[
{
'latitude' => 10,
'longitude' => 10,
'address' => 'Test Address',
'state' => 'Test State',
'state_code' => 'TS',
'country' => 'Test Country',
'country_code' => 'TC'
}
]
)
this is my service:
if params[:ip]
lat = Geocoder.search(params[:ip])[0].latitude
lng = Geocoder.search(params[:ip])[0].longitude
venues = venues.near([lat, lng], APP_CONFIG['geo_location_radius'], order: 'average_rating DESC')
end
And this is my service spec:
context "find featured venues by ip" do
let(:params) do
{
date: Date.today,
featured: true,
ip: '1.1.1.1',
}
end
it do
aggregate_failures do
expect(raw_venues.map { |v| v.id }.sort).to eq [venue1.id]
end
end
end
If I enter with pry in the it block and I try Geocoder.search('1.1.1.1') I get a "real" object response with a location in Australia instead of my stub, while I correctly get my stub if I enter something like Geocoder.search('Florence, Italy') ..
How can I setup stub responses to work with IP lookup?
Solution is simply:
Geocoder.configure(ip_lookup: :test)

How to combine two as_json methods to one correctly?

In my Model I have a working as_json method as follows:
def as_json(options = {})
super(options.merge(include: [:user, comments: {include: :user}]))
end
This method is for including users in comments.
Now I need to add almost the same thing in the same model for answers:
def as_json(options = {})
super(options.merge(include: [:user, answers: {include: :user}]))
end
How do I combine these two as_json methods, so that I have one as_json method?
Don't laugh but I am struggling with this for 3 days.
This is one of the reasons why you should not use the built-in to_json to serialize ActiveRecord models.
Instead, you should delegate the task to another object called serializer. Using a serializer allows you to have illimitate representations (serializations) of the same object (useful if the object can have different variants such as with/without comments, etc) and separation of concerns.
Creating your own serializer is stupid simply, as simple as having
class ModelWithCommentsSerializer
def initialize(object)
#object = object
end
def as_json
#object.as_json(include: [:user, comments: {include: :user}]))
end
end
class ModelWithAnswersSerializer
def initialize(object)
#object = object
end
def as_json
#object.as_json(include: [:user, answers: {include: :user}]))
end
end
Of course, that's just an example. You can extract the feature to avoid duplications.
There are also gems such as ActiveModelSerializers that provides that feature, however I prefer to avoid them as they tend to provide a lot of more of what most of users really need.
Why are you trying to override core Rails functionality - not good practice unless absolutely necessary.
--
This says the following:
To include associations use :include:
user.as_json(include: :posts)
# => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
# "created_at" => "2006/08/01", "awesome" => true,
# "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
# { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
You could call:
#answers.as_json(include :users)
--
Ohhhhhhhh:
Second level and higher order associations work as well:
user.as_json(include: { posts: {
include: { comments: {
only: :body } },
only: :title } })
# => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
# "created_at" => "2006/08/01", "awesome" => true,
# "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
# "title" => "Welcome to the weblog" },
# { "comments" => [ { "body" => "Don't think too hard" } ],
# "title" => "So I was thinking" } ] }
So looks like you could call:
#answers.to_json(include: comments: { include: :users })
def as_json(other_arg, options = {})
as_json(options.merge(include: [:user, other_arg: {include: :user}]))
end
And then you can call:
MyModel.as_json(:comments)
MyModel.as_json(:answers)

Elasticsearch nested query for auto-complete

I want to make sure I'm thinking about this in the right way. I'm trying to use Elasticsearch for an auto-complete for nested items. I have a list, and a list has many items. I want to use ES to return matching item names, weighing them more strongly if they're present in the current list by passing both the list name, and item name to search in Elasticsearch.
I could in theory simply index Items separately and search them that way, but I'd rather search them through the List document so that I can control relevancy.
I can't figure out how to return an item that nearly matches what's put in. Here's my setup... (Rails using Elasticsearch-Rails and Elasticsearch-Model)
The Index Mappings:
settings :index => { :number_of_shards => 1 } do
mapping :dynamic => 'false' do
indexes :private, :type => 'boolean'
indexes :name, :type => 'string'
indexes :slug, :type => 'string'
indexes :bookmarks_count , :type => 'integer'
indexes :item_names, :type => 'string'
indexes :up_count, :type => 'integer'
indexes :permalink, :type => 'string'
indexes :sub_text, :type => 'string'
indexes :title_text, :type => 'string'
indexes :creator_name, :type => 'string'
indexes :creator_avatar, :type => 'string'
indexes :cover_image, :type => 'string'
indexes :items, :type => "nested" do
indexes :name
indexes :description
indexes :image_url
indexes :link
end
end
end
The JSON:
def as_indexed_json(options = {})
as_json(:include => [:items, :tags],
:methods => [:permalink, :sub_text, :title_text, :icon_url, :item_names, :creator_name, :creator_avatar, :cover_image]
)
end
Here's the method I call for the search:
def item_typeahead_search(list_name, search_query, page = 1, per = 5)
wildcarded_query = "*#{search_query}*"
::List.search(item_typeahead_querystring(list_name, wildcarded_query)).per(per).page(page)
end
def item_typeahead_querystring(list_name, query_string)
{
:query => {
:bool => {
:should => [
{ :match => { :name => list_name }},
{
:nested => {
:path => "items",
:score_mode => "max",
:query => {
:bool => {
:must => [
{ :match => { "items.name" => query_string }}
]
}}}}
]
}}}
end
Here's a sample query...
results = List.item_typeahead_search("try me", "crazy glue")
Here are the results...
=> #<Elasticsearch::Model::Response::Result:0x007fb8f25c5208
#result=
{"_index"=>"lists",
"_type"=>"list",
"_id"=>"54504855f29a589a2700003b",
"_score"=>8.652843,
"_source"=>
{"_id"=>"54504855f29a589a2700003b",
"bookmarks_count"=>0,
"carousel"=>nil,
"cid"=>"5j18j8aor",
"content_source_name"=>nil,
"content_source_url"=>nil,
"created_at"=>"2014-10-29T01:52:21Z",
"description"=>nil,
"down_count"=>0,
"down_voters"=>[],
"intralist_id"=>"545057f7692a270e03000266",
"items"=>
[{"_id"=>"545048d5f29a589a27000044",
"created_at"=>"2014-10-29T01:54:29Z",
"description"=>nil,
"down_count"=>0,
"down_voters"=>[],
"image_small_url"=>nil,
"image_thumb_url"=>nil,
"image_url"=>nil,
"link"=>nil,
"name"=>"a swiss army knife",
"order"=>nil,
"picture"=>nil,
"up_count"=>0,
"up_voters"=>[],
"updated_at"=>"2014-10-29T01:54:29Z",
"vote_count"=>0},
{"_id"=>"545048d5f29a589a27000045",
"created_at"=>"2014-10-29T01:54:29Z",
"description"=>nil,
"down_count"=>0,
"down_voters"=>[],
"image_small_url"=>nil,
"image_thumb_url"=>nil,
"image_url"=>nil,
"link"=>nil,
"name"=>"duct tape",
"order"=>nil,
"picture"=>nil,
"up_count"=>0,
"up_voters"=>[],
"updated_at"=>"2014-10-29T01:54:29Z",
"vote_count"=>0},
{"_id"=>"545048d5f29a589a27000046",
"created_at"=>"2014-10-29T01:54:29Z",
"description"=>nil,
"down_count"=>0,
"down_voters"=>[],
"image_small_url"=>nil,
"image_thumb_url"=>nil,
"image_url"=>nil,
"link"=>nil,
"name"=>"Crazy glue",
"order"=>nil,
"picture"=>nil,
"up_count"=>0,
"up_voters"=>[],
"updated_at"=>"2014-10-29T01:54:29Z",
"vote_count"=>0},
{"_id"=>"545048d5f29a589a27000047",
"created_at"=>"2014-10-29T01:54:29Z",
"description"=>nil,
"down_count"=>0,
"down_voters"=>[],
"image_small_url"=>nil,
"image_thumb_url"=>nil,
"image_url"=>nil,
"link"=>nil,
"name"=>"a nut",
"order"=>nil,
"picture"=>nil,
"up_count"=>0,
"up_voters"=>[],
"updated_at"=>"2014-10-29T01:54:29Z",
"vote_count"=>0}],
"name"=>"try me",
"parent_list_creator"=>nil,
"parent_list_id"=>nil,
"private"=>false,
"promoted"=>false,
"slug"=>"things-for-fixing-anything",
"up_count"=>0,
"up_voters"=>[],
"updated_at"=>"2014-10-29T02:59:03Z",
"user_id"=>"543809c4b64c402d6a000003",
"vote_count"=>0,
"permalink"=>"/lists/things-for-fixing-anything",
"sub_text"=>"List - created by: CreepyTimes",
"title_text"=>"try me",
"icon_url"=>"",
"item_names"=>"a swiss army knife duct tape Crazy glue a nut",
"creator_name"=>"CreepyTimes",
"creator_avatar"=>"https://blahblah.com/uploads/profile/image/543809c4b64c402d6a000004/thumb__old_",
"cover_image"=>nil,
"tags"=>[]}}>
So, I see "crazy glue" as an item in the list, but it's one of many - not exactly something I can use out of the box here for Auto-complete purposes when someone starts typing an Item name
Is there a way to do what I'm trying to do using nested queries, filters or something? Relatively new to Elasticsearch, so I could use some help on the end solution. If I'm not thinking about this the right way and should simply index Items, I can do that too, but just curious if there's a way to make this work!
EDIT - this is the query going to Elasticsearch:
#<Elasticsearch::Model::Searching::SearchRequest:0x007fc555032218
#definition=
{:index=>"lists",
:type=>"list",
:body=>
{:query=>
{:bool=>
{:should=>
[{:match=>{:name=>"try me"}},
{:nested=>{:path=>"items", :score_mode=>"max", :query=>{:bool=>{:must=>[{:match=>{"items.name"=>"*crazy glue*"}}]}}}}]}}},
:size=>5,
:from=>0},
#klass=[PROXY] List,
#options={}>>

How do I access the data in JSON converted to hash by crack in ruby?

Here is the example from the crack documentation:
json = '{"posts":[{"title":"Foobar"}, {"title":"Another"}]}'
Crack::JSON.parse(json)
=> {"posts"=>[{"title"=>"Foobar"}, {"title"=>"Another"}]}
But how do I actually access the data in the hash?
I've tried the following:
array = Crack::JSON.parse(json)
array["posts"]
array["posts"] shows all the values, but I tried array["posts"]["title"] and it didn't work.
Here is what I am trying to parse as an example:
{"companies"=>[{"city"=>"San Mateo", "name"=>"Jigsaw", "address"=>"777 Mariners Island Blvd Ste 400", "zip"=>"94404-5059", "country"=>"USA", "companyId"=>4427170, "activeContacts"=>168, "graveyarded"=>false, "state"=>"CA"}], "totalHits"=>1}
I want to access the individual elements under companies....like city and name.
Like this?
hash = {
"companies" => [
{
"city" => "San Mateo",
"name" => "Jigsaw",
"address" => "777 Mariners Island Blvd Ste 400",
"zip" => "94404-5059",
"country" => "USA",
"companyId" => 4427170,
"activeContacts" => 168,
"graveyarded" => false,
"state" => "CA"
}
],
"totalHits" => 1
}
hash['companies'].each{ |i|
puts "city => #{i['city']}"
puts "name => #{i['name']}"
}
# >> city => San Mateo
# >> name => Jigsaw
hash['companies'][0]['city'] # => "San Mateo"
hash['companies'][0]['name'] # => "Jigsaw"
The problem is you didn't account for the array that companies points to.

Retrieve all association's attributes of an AR model?

What do you think is the most optimal way to retrieve all attributes for all the associations an AR model has?
i.e: let's say we have the model Target.
class Target < ActiveRecord::Base
has_many :countries
has_many :cities
has_many :towns
has_many :colleges
has_many :tags
accepts_nested_attributes_for :countries, :cities, ...
end
I'd like to retrieve all the association's attributes by calling a method on a Target instance:
target.associations_attributes
>> { :countries => { "1" => { :name => "United States", :code => "US", :id => 1 },
"2" => { :name => "Canada", :code => "CA", :id => 2 } },
:cities => { "1" => { :name => "New York", :region_id => 1, :id => 1 } },
:regions => { ... },
:colleges => { ... }, ....
}
Currently I make this work by iterating on each association, and then on each model of the association, But it's kind of expensive, How do you think I can optimize this?
Just a note: I realized you can't call target.countries_attributes on has_many associations with nested_attributes, one_to_one associations allow to call target.country_attributes
I'm not clear on what you mean with iterating on all associations. Are you already using reflections?
Still curious if there's a neater way, but this is what I could come up with, which more or less results in the hash you're showing in your example:
class Target < ActiveRecord::Base
has_many :tags
def associations_attributes
# Get a list of symbols of the association names in this class
association_names = self.class.reflect_on_all_associations.collect { |r| r.name }
# Fetch myself again, but include all associations
me = self.class.find self.id, :include => association_names
# Collect an array of pairs, which we can use to build the hash we want
pairs = association_names.collect do |association_name|
# Get the association object(s)
object_or_array = me.send(association_name)
# Build the single pair for this association
if object_or_array.is_a? Array
# If this is a has_many or the like, use the same array-of-pairs trick
# to build a hash of "id => attributes"
association_pairs = object_or_array.collect { |o| [o.id, o.attributes] }
[association_name, Hash[*association_pairs.flatten(1)]]
else
# has_one, belongs_to, etc.
[association_name, object_or_array.attributes]
end
end
# Build the final hash
Hash[*pairs.flatten(1)]
end
end
And here's an irb session through script/console to show how it works. First, some environment:
>> t = Target.create! :name => 'foobar'
=> #<Target id: 1, name: "foobar">
>> t.tags.create! :name => 'blueish'
=> #<Tag id: 1, name: "blueish", target_id: 1>
>> t.tags.create! :name => 'friendly'
=> #<Tag id: 2, name: "friendly", target_id: 1>
>> t.tags
=> [#<Tag id: 1, name: "blueish", target_id: 1>, #<Tag id: 2, name: "friendly", target_id: 1>]
And here's the output from the new method:
>> t.associations_attributes
=> {:tags=>{1=>{"id"=>1, "name"=>"blueish", "target_id"=>1}, 2=>{"id"=>2, "name"=>"friendly", "target_id"=>1}}}
try this with exception handling:
class Target < ActiveRecord::Base
def associations_attributes
tmp = {}
self.class.reflections.symbolize_keys.keys.each do |key|
begin
data = self.send(key) || {}
if data.is_a?(ActiveRecord::Base)
tmp[key] = data.attributes.symbolize_keys!
else
mapped_data = data.map { |item| item.attributes.symbolize_keys! }
tmp[key] = mapped_data.each_with_index.to_h.invert
end
rescue Exception => e
tmp[key] = e.message
end
end
tmp
end
end
This is updated version of Stéphan Kochen's code for Rails 4.2
def associations_attributes
association_names = self.class.reflect_on_all_associations.collect { |r| r.name }
me = self.class.includes(association_names).find self.id
pairs = association_names.collect do |association_name|
object_or_array = me.send(association_name)
if object_or_array.is_a? ActiveRecord::Associations::CollectionProxy
association_pairs = object_or_array.collect { |o| [o.id, o.attributes] }
[association_name, Hash[*association_pairs.flatten(1)]]
else
[association_name, object_or_array.attributes]
end
end
Hash[*pairs.flatten(1)]
end

Resources