I'm a rails noob trying to follow the DRY methods of those who've come before me.
I know that I'm doing something wrong – I'm just not sure what that is or how to overcome it.
Basically, my question is how can I make this code more object-oriented?
I have a class Podcast, which at this point just contains a bunch of class methods that scrape various data from around the web.
So, for example, this class method tries to discover a podcast twitter or facebook feed from their website:
def self.social_discovery(options = {})
new_podcasts_only = options[:new_podcasts_only] || false
if new_podcasts_only
podcast = Podcast.find(:all, :select => 'id, siteurl, name', :conditions => ['created_at > ? and siteurl IS NOT ?', Time.now - 24.hours, nil])
Podcast.podcast_logger.info("#{podcast.count}")
else
podcast = Podcast.find(:all, :select => 'id, siteurl, name', :conditions => ['siteurl IS NOT ?', nil])
end
podcast.each do | pod |
puts "#{pod.name}"
begin
# Make sure the url is readable by open-uri
if pod.siteurl.include? 'http://'
pod_site = pod.siteurl
else
pod_site = pod.siteurl.insert 0, "http://"
end
# Skip all of this if we're dealing with a feed
unless pod_site.downcase =~ /.rss|.xml|libsyn/i
pod_doc = Nokogiri.HTML(open(pod_site))
pod_name_fragment = pod.name.split(" ")[0].to_s
if pod_name_fragment.downcase == "the"
pod_name_fragment = pod.name.split(" ")[1].to_s unless pod.name.split(" ")[1].to_s.nil?
end
doc_links = pod_doc.css('a')
# If a social url contains part of the podcast name, grab that
# If not, grab the first one you find within our conditions
# Give Nokogiri some room to breathe with pessimistic exception handling
begin
begin
twitter_url = doc_links.find {|link| link['href'] =~ /twitter.com\// and link['href'].match(/#{pod_name_fragment}/i).to_s != "" unless link['href'] =~ /share|status/i}.attribute('href').to_s
rescue Exception => ex
if doc_links.find {|link| link['href'] =~ /twitter.com\// unless link['href'] =~ /share|status/i}.nil?
twitter_url = nil
else
twitter_url = doc_links.find {|link| link['href'] =~ /twitter.com\// unless link['href'] =~ /share|status/i}.attribute('href').to_s
end
end
begin
facebook_url = doc_links.find {|link| link['href'] =~ /facebook.com\// and link['href'].match(/#{pod_name_fragment}/i).to_s != "" unless link['href'] =~ /share|.event/i}.attribute('href').to_s
rescue Exception => ex
if doc_links.find {|link| link['href'] =~ /facebook.com\// unless link['href'] =~ /share|.event/i}.nil?
facebook_url = nil
else
facebook_url = doc_links.find {|link| link['href'] =~ /facebook.com\// unless link['href'] =~ /share|.event/i}.attribute('href').to_s
end
end
rescue Exception => ex
puts "ANTISOCIAL"
# Ensure that the urls gets saved regardless of what else happens
ensure
pod.update_attributes(:twitter => twitter_url, :facebook => facebook_url)
end
puts "#{twitter_url}" + "#{facebook_url}"
Podcast.podcast_logger.info("#{twitter_url}" + "#{facebook_url}")
end
rescue Exception => ex
puts "FINAL EXCEPTION: #{ex.class} + #{ex.message}"
end
end
end
Again, I know this is bad code. Please help me understand why?
I will be forever in your debt.
Thanks,
Harris
The major thing I can see in your code is code duplication. If you look closely, the code for fetching the twitter url and the code for the facebook url is almost exactly the same, except for the 'twitter.com' and 'facebook.com' part. My suggestion is to pull that out into a separate method that takes the doc_links variable as a parameter as well as the regex for finding the link. Also, I'm not quite sure why you're doing the "unless ..." part here:
if pod_name_fragment.downcase == "the"
pod_name_fragment = pod.name.split(" ")[1].to_s unless pod.name.split(" ")[1].to_s.nil?
end
If you don't do the "unless ..." part of the line, pod_name_fragment will be defined but nil, but if you don't include it you will get an exception if you try to refer to pod_name_fragment.
Also, you should almost never rescue Exception. Use StandardError instead. Let's say your program is running and you try to cancel it with Ctrl-C. That throws a SystemExit (I'm not 100% sure about the name) exception, which is a subclass of the Exception exit. In most cases you would then want to exit right away. I know this isn't that much applicable for a Rails app, but I'm pretty sure there's other reasons to catch SystemError instead.
There might be more, one easy way to find "bad code" is to look at metrics. Ryan Bates made an excellent Railscast on metrics (http://railscasts.com/episodes/252-metrics-metrics-metrics), and I'd suggest you look at Reek in particular for finding "code smells". Check their wiki for information on what the different things mean.
Related
I'm fetching videos' data from youtube, including general video info, and likes number of them.
The problem is that sometimes, there is no corresponding data. Maybe the video is private, or the likes number is 0.
In either case, there will report NoMethodError: undefined method 'likes' for nil:NilClass
How can I handle this? I just want to ignore them and continue to the next youtube video.
I think I can use something like rescue, but I don't find many tutorial of it.
Here is my code:
client = YouTubeIt::Client.new(:dev_key => my_key)
Video.all.each do |video|
video_info = client.video_by(video.url)
like_number = video_info.rating.likes.to_i
video.likes = like_number
if video.save
puts "#{video.title} likes: #{video.likes}"
end
end
Rather that using the exception handling, which is slower, in this case just check for the presence of the value before calling the method.
Assuming the error is generated on the line
like_number = video_info.rating.likes.to_i
simply use
client = YouTubeIt::Client.new(:dev_key => my_key)
Video.all.each do |video|
video_info = client.video_by(video.url)
# here check for the presence.
if video_info.rating
like_number = video_info.rating.likes.to_i
video.likes = like_number
if video.save
puts "#{video.title} likes: #{video.likes}"
end
end
end
You should check for all possible nil values with, eg, video.nil? and corresponding conditionals. Rescuing should always be your last resort (it's slow and dirty), but, if there is no way to predict where the script will fail, you can always rescue some piece of code:
begin
#your error-prone code
rescue
#action you want to take in case your error-prone code raises error
end
Here is a good article on exceptions: http://www.tutorialspoint.com/ruby/ruby_exceptions.htm.
In this example, the method body acts as default begin block so need of begin block
def any_method
client = YouTubeIt::Client.new(:dev_key => my_key)
Video.all.each do |video|
video_info = client.video_by(video.url)
# try tries to find the method, if not found returns nil
like_number = video_info.try(:rating).try(:likes).to_i
video.likes = like_number
if video.save
puts "#{video.title} likes: #{video.likes}"
end
end
# For any type of standard errors, use rescue block
rescue => error_object
p 'Sorry, some error occured'
p error_object
end
I've got a text area called body in a model. I'd like to change every image to a link to that image. I've got this method which is supposed to do that but it doesn't seem to work. It breaks at each image.
def get_images_in(body)
body_with_links = body.gsub( %r{http://[^\s<]+} ) do |url|
if url[/(?:png|jpe?g|gif|svg)$/]
"<a href='#{url}' target='_blank'><img src='#{url}' /></a>"
end
end
return body_with_links
end
Any ideas? Thank-you!
UPDATE
Here's a link to a gist with sample body text.
First things first, you don't need to use a return statement in ruby. Ruby will return the last thing by default. In your case, this is the string returned from the gsub:
def wrap_image_with_link(body)
body.gsub( %r{http://[^\s<]+} ) do |url|
if url[/(?:png|jpe?g|gif|svg)$/]
"<a href='#{url}' target='_blank'><img src='#{url}' /></a>"
end
end
end
This still isn't quite right. I would then focus my initial regular expression on the img tag:
def wrap_image_with_link(body)
body.gsub(/\<img.*src\=['"](.*)['"].*\/\>/) do |url|
"<a href='#{url}' target='_blank'><img src='#{url}' /></a>"
end
end
Rails has a couple helpers to clean this up.
def wrap_images_with_links(body)
body.gsub(/\<img.*src\=['"](.*)['"].*\/\>/) do
link_to(image_tag($1), $1, :target => '_blank')
end
end
You probably want to make the reg ex case insensitive:
def wrap_images_with_links(body)
body.gsub(/.*\<img.*src\=['"](.*)['"].*\/\>/i) do
link_to(image_tag($1), $1, :target => '_blank')
end
end
So gsub, as opposed to sub, changes every instance of matched reg-ex, so need to tweak the reg ex slightly to accommodate more explicit matches:
def wrap_images_with_links(body)
body.gsub(/\<img[\s\w"=]+src\=['"](http[s]?:\/\/[\w.\/]+)['"]\s?[\/]?\>/i) do
link_to(image_tag($1), $1, :target => '_blank')
end
end
Depending on the complexities of your urls, you might need to tweak the reg-ex to support ? or characters other than white space, but this work pretty well.
I have little experience in rails exception handling. I have this snippet
def update
#game = Game.find(params[:id])
begin
params[:game][:tier] = eval(params[:game][:tier])
rescue
#game.errors.add(:tier, "Please make sure the correct format for tier, example [100, 1000, 10000]")
end
#.... more code
end
In case params[:game][:tier] = "[100,200]" everything is perfect.
In case of error case of ruby syntax like params[:game][:tier] = "[100,200] abc" it catch the error however the application just crush.
How can I handle exception with 'eval()' such that it won't crush the app? Why begin and rescue does not work in this case? Appreciate any help for ruby enlightenment thanks :)
What if params[:game][:tier] was "[100,200]; system('rm -rf /')"?
Since the incoming data is expected to be an array, I would not use eval but JSON.parse instead:
> JSON.parse("[100,200]")
=> [100, 200]
> JSON.parse("[100,200] abc")
JSON::ParserError: 746: unexpected token at 'abc'...
Then rescue from only a JSON::ParserError exception
rescue JSON::ParserError => e
This will also solve the rescue not catching the exception problem you're having.
duplicate of this
however you should rescue in this way
def update
#game = Game.find(params[:id])
begin
params[:game][:tier] = eval(params[:game][:tier])
rescue Exception => e
#game.errors.add(:tier, "Please make sure the correct format for tier, example [100, 1000, 10000]")
end
#.... more code
end
in order to make it work
Ok so i have this helper
def current_company_title
(Company.find_by_id(params["company_id"]).name rescue nil) || (#companies.first.name rescue nil) current_user.company.name
end
Basically what I am achieving with this is the following ...
If the param["company_id"] exists then try to get the company and if not then
if #companies exists grab the first company name and if not then get the current users company name
This works but the rescues seem like a hack...any idea on another way to achieve this
Indeed rescue is kind of a hack, id' probably split it up into two methods and then use try to fetch the name if available: http://api.rubyonrails.org/classes/Object.html#method-i-try
def current_company
#current_company ||= Company.find_by_id(params[:company_id]) || #companies.try(:first) || current_user.try(:company)
end
def current_company_name
current_company.try(:name)
end
Company.find_by_id(params["company_id"]).name`
find and its derivates are meant to be used when you're sure-ish you'll have a positive result, and only in some cases (row was deleted, etc) errors. That's why it raises an exception. In your case, you're assuming it's gonna fail, so a regular where, which would return nil if no rows was found, would do better, and remove the first rescue
#companies.first.name rescue nil
could be replaced by
#companies.first.try(:name)
I'll let you check the api for more on the topic of try. It's not regular ruby, it's a Rails addition.
Less "magic", simple code, simple to read:
def current_company_title
company = Company.where(id: params["company_id"]).presence
company ||= #companies.try(:first)
company ||= current_user.company
company.name
end
Ps. Not a big fan of Rails' try method, but it solves the problem.
def current_company_title
if params["company_id"]
return Company.find_by_id(params["company_id"]).name
elsif #companies
return #companies.first.name
else
return current_user.company.name
end
end
The rescues are a hack, and will obscure other errors if they occur.
Try this:
(Company.find_by_id(params["company_id"].name if Company.exists?(params["company_id"]) ||
(#companies.first.name if #companies && #companies.first) ||
current_user.company.name
then you can extract each of the bracketed conditions to their own methods to make it more readable, and easier to tweak the conditions:
company_name_from_id(params["company_id"]) || name_from_first_in_collection(#companies) || current_user_company_name
def company_name_from_id(company_id)
company=Company.find_by_id(company_id)
company.name if company
end
def name_from_first_in_collection(companies)
companies.first.name if companies && companies.first
end
def current_user_company_name
current_user.company.name if current_user.company
end
[Company.find_by_id(params["company_id"]),
#companies.to_a.first,
current_user.company
].compact.first.name
I have an array like this
a = []
a << B.new(:name => "c")
a << B.new(:name => "s")
a << B.new(:name => "e")
a << B.new(:name => "t")
How i can save it at once?
B.transaction do
a.each(&:save!)
end
This will create a transaction that loops through each element of the array and calls element.save on it.
You can read about ActiveRecord Transactions and the each method in the Rails and Ruby APIs.
a.each(&:save)
This will call B#save on each item in the array.
So I think we need a middle ground to Alexey's raising exceptions and aborting the transaction and Jordan's one-liner solution. May I propose:
B.transaction do
success = a.map(&:save)
unless success.all?
errored = a.select{|b| !b.errors.blank?}
# do something with the errored values
raise ActiveRecord::Rollback
end
end
This will give you a bit of both worlds: a transaction with rollback, knowledge of which records failed and even gives you access to the validation errors therein.
Wrapping save in transaction will not be enough: if a validation is not passed, there will be no exception raised and no rollback triggered.
I can suggest this:
B.transaction do
a.each do |o|
raise ActiveRecord::Rollback unless o.save
end
end
Just doing B.transaction do a.each(&:save!) end is not an option either, because the transaction block will not rescue any exception other than ActiveRecord::Rollback, and the application would crash on failed validation.
I do not know how to check afterwards if the records have been saved.
Update. As someone has downrated my answer, i assume that the person was looking for a cut-and-paste solution :), so here is some (ugly :)) way to process fail/success value:
save_failed = nil
B.transaction do
a.each do |o|
unless o.save
save_failed = true
raise ActiveRecord::Rollback
end
end
end
if save_failed
# ...
else
# ...
end
I know this is an old question but I'm suprised no one thought of this:
B.transaction do
broken = a.reject { |o| o.save }
raise ActiveRecord::Rollback if broken.present?
end
if broken.present?
# error message
end
In case you're looking for more efficient solution than save each row in the loop please look my answer here Ruby on Rails - Import Data from a CSV file
I'm suggesting to use gem activerecord-import there.