I've started creating a model based solution for creating short URLs, but I'm wondering if it wouldn't be better to do it in it's own collection (using mongoid) build an index for the tokens between models then search? Or if there's a gem that exists instead of rolling my own solution.
Right now i'm using Mongoid::Token which generates a unique token (ie cUaIxBu) for the particular collection and then using an additional letter (->(c)UaIxBu) to figure how which controller to route the particular request to.
Any ideas or pointers?
In this example alternatedoma.in/cUaIxBu would point to realdomain.com/cities/1234
routes
get '/:id' => 'home#tiny_url', :constraints => { :domain => 'alternatedoma.in' }
controller
def tiny_url
case params[:id].slice!(0)
when 'f'
#article = Article.find_by_token(params[:id])
redirect_to feature_url(#article)
when 'c'
#city = City.find_by_token(params[:id])
redirect_to city_url(#city)
when 'p'
#place = Place.find_by_token(params[:id])
redirect_to feature_url(#place)
end
end
We're employing an almost identical system in an application that I'm currently working on - and it seems to be working out okay (so far!). The only thing I could think of, is that you could boil down your LoC, as well as easily adding support for other models (if required) in the future:
supported_models = {:a => Article, :c => City, :p => Place}
prefix = params[:id].slice!(0).to_sym
if supported_models.has_key?(prefix)
#obj = supported_models[prefix].find_by_token(params[:id])
redirect_to send(:"#{supported_models[prefix].name.underscore}_url", #obj)
end
Obviously, this would require your routing helpers to follow the the same naming as your models. I.e: Article > article_url, City > city_url, etc.
I've got ActiveResource setup to consume the freebase api in json, and it should work fine except that the json freebase returns causes ActiveResource to blowup.
NoMethodError: undefined method `collect!' for #<Hash:0x007fd674831dd0>
How can I define a custom json parser to fix whatever is wrong?
class Freebase < ActiveResource::Base
self.site = "https://www.googleapis.com/"
self.format = :json
def self.search(word)
self.find(:all, :from => "/freebase/v1/search/", :params => { :query => word })
end
#https://www.googleapis.com/freebase/v1/search?query=nirvana
#Freebase.get('search', :query => 'nirvana')
end
Json being returned:
https://www.googleapis.com/freebase/v1/search?query=nirvana
{"status":"200 OK","result":[{"mid":"/m/05b3c","name":"Nirvana","notable":{"name":"Belief","id":"/religion/belief"},"lang":"en","score":67.540009},{"mid":"/m/0b1zz","name":"Nirvana","notable":{"name":"Musical Artist","id":"/music/artist"},"lang":"en","score":64.311432},{"mid":"/m/092bf5","name":"Buddhism","notable":{"name":"Religion","id":"/religion/religion"},"lang":"en","score":33.647118},{"mid":"/m/02_6qh","name":"Nirvana","notable":{"name":"Film","id":"/film/film"},"lang":"en","score":30.068491},{"mid":"/m/01h89tx","name":"Nirvana","notable":{"name":"Musical Album","id":"/music/album"},"lang":"en","score":27.799274},{"mid":"/m/01rn9fm","name":"Nirvana","notable":{"name":"Musical Group","id":"/music/musical_group"},"lang":"en","score":27.445602},{"mid":"/m/015k7","name":"Gautama Buddha","notable":{"name":"Deity","id":"/religion/deity"},"lang":"en","score":24.129679},{"mid":"/m/01rkx5","name":"Mahayana Mahaparinirvana Sutra","lang":"en","score":22.359026},{"mid":"/m/03d7q7v","name":"Nirvana","lang":"en","score":21.034473},{"mid":"/m/055ym7w","name":"Nirvana bootleg recordings","notable":{"name":"Musical Album","id":"/music/album"},"lang":"en","score":19.241596},{"mid":"/m/0122_j","name":"Nevermind","notable":{"name":"Musical Album","id":"/music/album"},"lang":"en","score":18.366383},{"mid":"/m/04n7mt","name":"Nirvana fallacy","lang":"en","score":17.212397},{"mid":"/m/0484q","name":"Kurt Cobain","notable":{"name":"Musician","id":"/m/09jwl"},"lang":"en","score":16.594929},{"mid":"/m/027_k8j","name":"Nirvana","lang":"en","score":16.336584},{"mid":"/m/0285c","name":"Dave Grohl","notable":{"name":"Musician","id":"/m/09jwl"},"lang":"en","score":16.115103},{"mid":"/m/068shv","name":"Smells Like Nirvana","notable":{"name":"Musical Album","id":"/music/album"},"lang":"en","score":15.350652},{"mid":"/m/01kq85c","name":"Manic Nirvana","notable":{"name":"Musical Album","id":"/music/album"},"lang":"en","score":15.275189},{"mid":"/m/0437sc","name":"Lithium","notable":{"name":"Composition","id":"/music/song"},"lang":"en","score":14.637386},{"mid":"/m/055kh1","name":"Mechanus","lang":"en","score":14.621847},{"mid":"/m/01f1vf","name":"Lucifer","notable":{"name":"Fictional Character","id":"/fictional_universe/fictional_character"},"lang":"en","score":13.504528}],"cursor":20,"cost":11,"hits":3104}
Using Rails 3.2.1
UPDATE:
Found this issue, but still not sure how to overcome it. https://github.com/rails/rails/issues/2318
The problem is that the JSON returned isn't formed the way ActiveResource expects it to be. ARes doesn't expect all of that metadata that is being returned, it is only expecting what is in the results portion of the response.
To be explicit, you're getting back:
{"status":"200 OK","result":[{"mid":"/m/05b3c","name":"Nirvana","notable":{"name" ...
But ARes wants:
[{"mid":"/m/05b3c","name":"Nirvana","notable":{"name"...
The easiest (and probably dirtiest) solution I can come up with is to overwrite the ActiveResource::Base#find_every private method in your model like so:
class Freebase < ActiveResource::Base
self.site = "https://www.googleapis.com/"
self.format = :json
def self.search(word)
self.find(:all, :from => "/freebase/v1/search/", :params => { :query => word })
end
private
def self.find_every(options)
begin
case from = options[:from]
when Symbol
instantiate_collection(get(from, options[:params]))
when String
path = "#{from}#{query_string(options[:params])}"
instantiate_collection(format.decode(connection.get(path, headers).body['result']) || [])
else
prefix_options, query_options = split_options(options[:params])
path = collection_path(prefix_options, query_options)
instantiate_collection( (format.decode(connection.get(path, headers).body['result']) || []), prefix_options )
end
rescue ActiveResource::ResourceNotFound
# Swallowing ResourceNotFound exceptions and return nil - as per
# ActiveRecord.
nil
end
end
end
The only change I made is the .body method calls are now .body['result']. With this addition, #instantiate_collection will now receive an Array as it expects rather than a Hash.
Ultimately, while this should make the error go away, I don't know that this will solve all of your problems as you might need some of the data that gets lost with this method. My suggestion would be to abandon ActiveResource if you can and use something like RestClient and build your model around that. Another reason for this approach is that the semantics as they are now are broken. You aren't getting back a collection of Freebases. You're using the Freebase API to get search results.
I followed Derek's advice and abandoned ActiveResource...
for anyone else I ended up using Faraday and Faraday_Middleware
class Freebase
def self.search(id)
connection = Faraday.new 'https://www.googleapis.com/freebase/v1' do |conn|
conn.adapter Faraday.default_adapter
conn.use FaradayMiddleware::ParseJson
#conn.use Faraday::Response::Mashify
end
response = connection.get do |req|
req.url('search', :query => id, :limit => 10)#, :filter => '(any namespace:/wikipedia/en_id namespace:/authority/imdb/title)')# #:filter => '(any namespace:/wikipedia/en_id namespace:/authority/imdb/title namespace:/authority/netflix/movie)' , :with => 'commons'
end
end
Supposedly you can use several built in modules to make rails style methods out of the returned hash, but I haven't figured that part out yet. ActiveResource seems only good at other Rails apps or RESTFUL routes that perfectly imitate it.
I am using Ruby on Rails 3.0.7 and I am trying to minimize database hitting. In order to do that I retrieve from the database all Article objects related to a User and then perform a search on those retrieved objects.
What I do is:
stored_objects = Article.where(:user_id => <id>) # => ActiveRecord::Relation
<some_iterative_function_1>.each { |...|
stored_object = stored_objects.where(:status => 'published').limit(1)
...
# perform operation on the current 'stored_object' considered
}
<some_iterative_function_2>.each { |...|
stored_object = stored_objects.where(:visibility => 'public').limit(1)
...
# perform operation on the current 'stored_object' considered
}
<some_iterative_function_n>.each { |...|
...
}
The stored_object = stored_objects.where(:status => 'published') code will really avoid to hitting the database (I ask this because in my log file it seams still run a database query for each iteration)? If no, how can I minimize database hitting?
P.S.: in few words, what I would like to do is to work on the ActiveRecord::Relation (an array of ) but the where method called on it seams to hit the database.
Rails has functionality to grab chunks of the database at one time, then iterate over the rows without having to hit the database again.
See "Retrieving Multiple Objects in Batches" for more information about find_each and find_in_batches.
Once you start iterating over stored_objects (if that's what you're doing), they'll be loaded from the database. If you want to load only the users's published articles, you could do this:
stored_objects = Article.where(:user_id => id, :status => 'published')
If you instead want to load published and unpublished articles and do something different with the published ones, you could do this:
stored_objects = Article.where(:user_id => id)
stored_objects.find_all { |a| a.status == 'published' }. each do |a|
# ... do something with a published article
end
Or perhaps:
Article.where(:user_id => id).each do |article|
case article.status
when 'published'
# ... do something with a published article
else
# ... do something with an article that's not published
end
end
Each of these examples performs only one database query. Choosing which one depends on which data you really want to work with.
I'm trying to test out a controller action on Rails 2.3.10 that connect to Google to retrieve contacts. I'm using Rspec and Mocha for testing. I want to stub out the actual call to Google since this is a unit test. I want to verify that the authsub_url method is called with the correct parameters. Stubbing the method out causes the expectation to fail.
Any advice would be appreciated.
Thanks!
My method that sets up the client to google is below:
def setup_client
#client = GData::Client::DocList.new(:authsub_scope => CONTACTS_SCOPE, :source => 'google-DocListManager-v1.1', :version => '3.0')
if params[:token].nil? && session[:google_token].nil?
#authsub_link = #client.authsub_url(import_method_gmail_url, false, true)
render :action => :index, :layout => "empty"
elsif params[:token] && session[:google_token].nil?
#client.authsub_token = params[:token]
session[:google_token] = #client.auth_handler.upgrade
end
#client.authsub_token = session[:google_token] if session[:google_token]
end
Here is my test:
describe "setup_client" do
it "has a authsub_link if there is no token parameter and the google token is not present in the session" do
GData::Client::DocList.any_instance.stubs(:authsub_url).returns("http://test.google.com/contacts")
user = Factory(:subscriber_user)
profile = Factory(:profile, :user => user)
login_as user
controller.instance_variable_get(:#client).expects(:authsub_url).with(import_method_gmail_url, false, true).once
get :index
assigns(:authsub_link).should == "http://test.google.com/contacts"
end
end
I would recommend FakeWeb. It allows you to fake web requests. Simple define the URL you're going to call and prepare the response(s). Makes your life very easy.
It looks like you are stubbing out the DocList#authsub_url method in two places :-
The first stub is on any instance of DocList and returns a URL :-
GData::Client::DocList.any_instance.stubs(:authsub_url).returns("http://test.google.com/contacts")
The second stub is on the actual instance of DocList but this returns nil because no there is no returns clause :-
controller.instance_variable_get(:#client).expects(:authsub_url).with(import_method_gmail_url, false, true).once
I think you can achieve what you want by combining them something like this :-
controller.instance_variable_get(:#client).expects(:authsub_url).with(import_method_gmail_url, false, true).returns("http://test.google.com/contacts")
Note that once is the default so is not needed unless you want to emphasise that the method should only be called once.
I'd like some advice on how I should synchronize a list of email addresses on 11k users against an external mailing list program, in this case Mailchimp.
Normally the way I'd do this is simply to have an :after_save callback, to send a single update to the external api.
But already each hour, a rake task is run to update a property on every user in the database. If I simply did that, every hour, the the poor mailchimp API would get be hit 11,000 times.
What's the most efficient, simple way to do this, to check only if a single attribute you're watching has changed from what it was before the save?
If there's a variable that persists across the transaction lifecycle I would simply do something like this, where I check if the value has changed, and if it's different execute come other code.
class User
:before_save :store_old_email
:after_save :sync_with_chimp
def store_old_email
$ugly_of_global_variable_to_store_email = user.email
end
:sync_with_chimp
if $ugly_of_global_variable_to_store_email != user.email
//update_mail_chimp_api
end
end
end
I've checked the rails api here, and I'm still slightly unclear on how I should be doing this.
Would you use the dirty? class here to do this?
This is the way I went with in the end.
It turns out Rails gives you loads of handy callbacks in the dirty to do this.
Any suggestions on how to make this code less repetitive wold be gratefully received.
def update_mailchimp(optin)
# Create a Hominid object (A wrapper to the mailchimp api), and pass in a hash from the yaml file
# telling which mailing list id to update with subscribe/unsubscribe notifications)
#hominid = Hominid.new
client_site_list_id = YAML.load(File.read(RAILS_ROOT + "/config/mailchimp.yml"))
case optin
when 'subscribe_newsletter'
logger.debug("subscribing to newsletter...")
"success!" if #hominid.subscribe(client_site_list_id['client_site_to_mailchimp_API_link'], email, {:FNAME => first_name, :LNAME => last_name}, 'html')
when 'unsubscribe_newsletter'
logger.debug("unsubscribing from newsletter...")
"success!" if #hominid.subscribe(client_site_list_id['client_site_to_mailchimp_API_link'], email, {:FNAME => first_name, :LNAME => last_name}, 'html')
when 'subscribe_monthly_update'
logger.debug("subscribing to monthly update...")
"success!" if #hominid.subscribe(client_site_list_id['monthly_update'], email, {:FNAME => first_name, :LNAME => last_name}, 'html')
when 'unsubscribe_monthly_update'
logger.debug("unsubscribing from monthly update...")
"success!" if #hominid.unsubscribe(client_site_list_id['monthly_update'], email, {:FNAME => first_name, :LNAME => last_name}, 'html')
end
end
# Keep the users in sync with mailchimp's own records - by only firing requests to the API if details on a user have changed after saving.
def check_against_mailchimp
logger.info("Checking if changes need to be sent to mailchimp...")
if newsletter_changed?
logger.info("Newsletter changed...")
newsletter ? update_mailchimp('subscribe_newsletter') : update_mailchimp('unsubscribe_newsletter')
end
if monthly_update_changed?
logger.info("update preferences changed...")
monthly_update ? update_mailchimp('subscribe_monthly_update') : update_mailchimp('unsubscribe_monthly_update')
end
end
you could change your users model to an active resource instead of active record and just use mailchimps api as your db for users
this is an older post about active resource but might get you started down the right path
http://www.therailsway.com/2007/9/3/using-activeresource-to-consume-web-services