Refactor Models of gems - pluralized variables in rake-task - ruby-on-rails

I'm an early ruby dev so excuse me if this is obvious.
Using the gioco gem to implement a gamification concept I renamed a few classes and references to better fit in the project (tested everything and it's working).
The problematic rename is Kind => BadgeKind.
This line in the following task:
r.points << Point.create({ :badge_kind_id => kinds.id, :value => '#{args.points}'})
What's the concept behind kinds.id and how do I solve this?
Error:
NameError: undefined local variable or method 'kinds' for main:Object
I tried: badge_kind, badge_kinds
Task:
task :add_badge, [:name, :points, :kind, :default] => :environment do |t, args|
arg_default = ( args.default ) ? eval(args.default) : false
if !args.name || !args.points || !args.kind
raise "There are missing some arguments"
else
badge_string = "kind = BadgeKind.find_or_create_by(name: '#{args.kind}')\n"
badge_string = badge_string + "badge = Badge.create({
:name => '#{args.name}',
:points => '#{args.points}',
:badge_kind_id => kind.id,
:default => '#{arg_default}'
})
"
if arg_default
# changing User.find(:all) to User.all
badge_string = badge_string + 'resources = User.all
'
badge_string = badge_string + "resources.each do |r|
r.points << Point.create({ :badge_kind_id => kinds.id, :value => '#{args.points}'})
r.badges << badge
r.save!
end
"
end
badge_string = badge_string + "puts '> Badge successfully created'"
# puts "badge_string:\n" + badge_string
eval badge_string
end

It looks like you already have tables in your application for Kind and Badge. If that's the case, then it's not a good idea to rename Badge to BadgeKind. The reason is because this notation is commonly used for join tables, and it looks like you might be colliding with one. Right now, I'm guessing a Badge can have a Kind or vice versa. In order to make that happen, there may be a BadgeKind table that contains their relationships. I can't tell for sure without seeing your schema file or migrations, but you're probably better off leaving that the way it is, or at least choosing a different name like BadgeType.
Also, it's worth noting that building up strings to be executed with eval is considered an antipattern. Eval is slow, and also exposes your application to malicious code injection. You're at less of a risk because it appears this is a rake task, but even so, if you refactor this code, you should try a different strategy. It looks like it would be relatively straightforward to refactor.

Related

Rails/Ruby incorrectly showing variable not defined

In debugging console, while app running (using binding.pry to interrupt it), I can see that my variable Rails.configuration.hardcoded_current_user_key is set:
pry(#<TasksController>)> Rails.configuration.hardcoded_current_user_key
=> "dev"
But it doesn't appear to be defined:
pry(#<TasksController>)> defined?(Rails.configuration.hardcoded_current_user_key)
=> nil
Yet it works fine to store and test its value:
pry(#<TasksController>)> tempVar = Rails.configuration.hardcoded_current_user_key
=> "dev"
pry(#<TasksController>)> defined?(tempVar)
=> "local-variable"
What is going on?
This is because Rails config implements respond_to? but not respond_to_missing?, and defined? only recognizes respond_to_missing?:
class X
def respond_to?(name, include_all = false)
name == :another_secret || super
end
private
def method_missing(name, *args, &block)
case name
when :super_secret
'Bingo!'
when :another_secret
'Nope.'
else
super
end
end
def respond_to_missing?(name, include_all = false)
name == :super_secret || super
end
end
x = X.new
puts x.super_secret # => Bingo!
p defined?(x.super_secret) # => "method"
puts x.another_secret # => Nope.
p defined?(x.another_secret) # => nil
It's recommended to implement respond_to_missing? along with method_missing, I too wonder why Rails did it that way.
You shouldn't be using defined? on anything but the "stub" of that, or in other words, merely this:
defined?(Rails)
Anything beyond that is highly unusual to see, and I'm not even sure it's valid.
defined? is not a method, but a construct that tests if the following thing is defined as a variable, constant or method, among other things. It won't evaluate your code, it will just test it as-is. This means method calls don't happen, and as such, can't be chained.
If you want to test that something is assigned, then you should use this:
Rails.configuration.hardcoded_current_user_key.nil?

Rails - Help with rake task

I have a rake task I need to run in order to sanitize (remove forward slashes) some data in the database. Here's the task:
namespace :db do
desc "Remove slashes from old-style URLs"
task :substitute_slashes => :environment do
puts "Starting"
contents = Content.all
contents.each do |c|
if c.permalink != nil
c.permalink.gsub!("/","")
c.save!
end
end
puts "Finished"
end
end
Which allows me to run rake db:substitute_slashes --trace
If I do puts c.permalink after the gsub! I can see it's setting the attribute properly. However the save! doesn't seem to be working because the data is not changed. Can someone spot what the issue may be?
Another thing, I have paperclip installed and this task is triggering [paperclip] Saving attachments. which I would rather avoid.
try this:
namespace :db do
desc "Remove slashes from old-style URLs"
task :substitute_slashes => :environment do
puts "Starting"
contents = Content.all
contents.each do |c|
unless c.permalink.nil?
c.permalink = c.permalink.gsub(/\//,'')
c.save!
end
end
puts "Finished"
end
end
1.) Change != nil to unless record.item.nil? (I don't know if it makes a different, but I've never used != nil. You may want to use .blank? also judging by your code)
2.) Your gsub was malformed. The pattern must be between two / (/ stuff /). The \ is necessary because you need to escape the /.
3.) Bang (!) updates the object in place. I think your biggest issue may be that you are overusing !.
4.) You're also making this very inefficient... You are looking at every record and updating every record. Rails isn't always the best option. Learn SQL and do this in one line:
"UPDATE contents SET permalink = replace(permalink, '/', '');"
If you MUST use Rails:
ActiveRecord::Base.connection.execute "UPDATE contents SET permalink = replace(permalink, '/', '');"
Wow! One query. Amazing! :)
The next thing I would try would be
c.permalink = c.permalink.gsub("/","")
As for saving without callbacks, this stackoverflow page has some suggestions.

Memory Leak in Ruby net/ldap Module

As part of my Rails application, I've written a little importer that sucks in data from our LDAP system and crams it into a User table. Unfortunately, the LDAP-related code leaks huge amounts of memory while iterating over our 32K users, and I haven't been able to figure out how to fix the issue.
The problem seems to be related to the LDAP library in some way, as when I remove the calls to the LDAP stuff, memory usage stabilizes nicely. Further, the objects that are proliferating are Net::BER::BerIdentifiedString and Net::BER::BerIdentifiedArray, both part of the LDAP library.
When I run the import, memory usage eventually peaks at over 1GB. I need to find some way to correct my code if the problem is there, or to work around the LDAP memory issues if that's where the problem lies. (Or if there's a better LDAP library for large imports for Ruby, I'm open to that as well.)
Here's the pertinent bit of our my code:
require 'net/ldap'
require 'pp'
class User < ActiveRecord::Base
validates_presence_of :name, :login, :email
# This method is resonsible for populating the User table with the
# login, name, and email of anybody who might be using the system.
def self.import_all
# initialization stuff. set bind_dn, bind_pass, ldap_host, base_dn and filter
ldap = Net::LDAP.new
ldap.host = ldap_host
ldap.auth bind_dn, bind_pass
ldap.bind
begin
# Build the list
records = records_updated = new_records = 0
ldap.search(:base => base_dn, :filter => filter ) do |entry|
name = entry.givenName.to_s.strip + " " + entry.sn.to_s.strip
login = entry.name.to_s.strip
email = login + "#txstate.edu"
user = User.find_or_initialize_by_login :name => name, :login => login, :email => email
if user.name != name
user.name = name
user.save
logger.info( "Updated: " + email )
records_updated = records_updated + 1
elsif user.new_record?
user.save
new_records = new_records + 1
else
# update timestamp so that we can delete old records later
user.touch
end
records = records + 1
end
# delete records that haven't been updated for 7 days
records_deleted = User.destroy_all( ["updated_at < ?", Date.today - 7 ] ).size
logger.info( "LDAP Import Complete: " + Time.now.to_s )
logger.info( "Total Records Processed: " + records.to_s )
logger.info( "New Records: " + new_records.to_s )
logger.info( "Updated Records: " + records_updated.to_s )
logger.info( "Deleted Records: " + records_deleted.to_s )
end
end
end
Thanks in advance for any help/pointers!
By the way, I did ask about this in the net/ldap support forum as well, but didn't get any useful pointers there.
One very important thing to note is that you never use the result of the method call. That means that you should pass :return_result => false to ldap.search:
ldap.search(:base => base_dn, :filter => filter, :return_result => false ) do |entry|
From the docs: "When :return_result => false, #search will return only a Boolean, to indicate whether the operation succeeded. This can improve performance with very large result sets, because the library can discard each entry from memory after your block processes it."
In other words, if you don't use this flag, all entries will be stored in memory, even if you do not need them outside the block! So, use this option.

Rails NoMethodError in loop when method exists

Good day all.
I'm running into a bit of a problem getting a script running on my production environment, even though it works just fine on my dev box. I've verified that all the requisite gems and such are the same version.
I should mention that the script is intended to be run with the script/runner command.
Here is a super-condensed version of what I'm trying to do, centered around the part that's broken:
def currentDeal
marketTime = self.convertToTimeZone(Time.new)
deal = Deal.find(:first, :conditions => ["start_time ? AND market_id = ? AND published = ?", marketTime, marketTime, self.id, 1])
return deal
end
markets = Market.find(all)
markets.each do |market|
deal = market.currentDeal
puts deal.subject
end
Now convertToTimeZone is a method attached to the model. So, this code works just fine on my dev machine, as stated. However, attempting to run it on my production machine results in:
undefined method `subject' for nil:NilClass (NoMethodError)
If, however, I go into the console on the production box and do this:
def currentDeal
marketTime = self.convertToTimeZone(Time.new)
deal = Deal.find(:first, :conditions => ["start_time ? AND market_id = ? AND published = ?", marketTime, marketTime, self.id, 1])
return deal
end
market = Market.find(1)
deal = market.currentDeal
puts deal.subject
It returns the correct value, no problem. So what is going on?
This is on rails v 2.3.5, on both machines.
Thanks for any help
You are looping though all Markets on your production code, but your test snippet is only looking for one. The problem is that one of your Markets in your database has a currentDeal of nil (it has no object associated with it).
Run this on your production console instead.
markets = Market.find(all)
markets.each do |market|
deal = market.currentDeal
if deal
puts deal.subject
else
puts "NO currentDeal for Market with id: #{market.id}"
end
end
This will tell you exactly which Market record is exploding without a currentDeal.
So the question is how to fix it? Either all Markets are expected to have a currentDeal, or sometimes they don't and that's ok. If Market's should always have a currentDeal, then you need to adjust your validations to now allow a Market to be saved without a currentDeal. But given that the currentDeal is a time based thing, I would be that there is times when no deal is scheduled and therefore currentDeal will return nil.
So, more likely, you need to allow for the current deal to be nil. Your test code doesn't do this. It asks the market for the deal, and then the deal for it's subject. If the market return a nil deal, then you immediately ask nil for it's subject and you get the exception because nil does not have a method named subject. A few simple ways to nil protect you code:
deal = market.currentDeal
# simple if
if deal
puts deal.subject
end
# rails try method returns nil if the receiver is nil
# or executes the method if the object supports it
puts deal.try(:subject)
# ternary
puts deal ? deal.subject : "NO DEAL!"
# conditional execution
puts deal && deal.subject
Lastly, a ruby tip. This method is more complicated than it needs to be.
def currentDeal
marketTime = self.convertToTimeZone(Time.new)
deal = Deal.find(:first, :conditions => ["start_time ? AND market_id = ? AND published = ?", marketTime, marketTime, self.id, 1])
return deal
end
Ruby will always return the last expression's result in a method, and a has based conditions finder will clean up that query quite a bit.
def currentDeal
marketTime = self.convertToTimeZone(Time.new)
Deal.find(:first, :conditions => ["start_time > ? AND market_id = ? AND published = ?", marketTime, marketTime, id, true])
end
But this looks more like an association anyway. So you may want to use the association methods to clean this up further.
Clearly you are calling nil.subject, so Deal.find is returning nil in the production code. Your test case is only looking at one specific Market object, but the general case loops through Market objects. Your code needs to handle not finding a currentDeal for a Market object

Rails find won't join when args appended

I'm hoping I'm doing something wrong here and someone can point me in the right direction...
I have written a method inside of one of my models, here is the code:
def self.find_by_user_id(user_id, *args)
self.find(:all, :joins => :case_map,
:conditions => ['case_maps.uer_id = ? and case_maps.case_id = cases.id', user_id],
*args)
end
I can call the code like this and it works as expected:
Case.find_by_user_id(some_user_id)
However, when this code is executed with any additional args, like this:
Case.find_by_user_id(some_user_id, :limit => 15)
I get back ALL cases. The query in my log file show that it executed this:
Case Load (0.6ms) SELECT * FROM `cases` LIMIT 15
I even put a logger.info message in that method to make sure it's the one that is executing... It seems that whenever *args is not nil, that it skips all of the conditions and joins I added to the find and just uses the *args.
Anyone see something that I'm doing wrong?
AR::B#find expects a variable number of arguments, but the last of those should be the options hash. It uses Array#extract_options!, and so can you:
def self.find_by_user_id(user_id, *args)
options = args.extract_options!
options.merge!(:joins => :case_map, :conditions =>
['case_maps.uer_id = ? and case_maps.case_id = cases.id', user_id])
self.find(:all, *args, options)
end
But you shouldn't. What possible values make sense in between the :all and the options fields?
What you are really after is:
def self.find_by_user_id(user_id, options)
self.find(:all, options.merge(:joins => :case_map, :conditions =>
['case_maps.user_id = ? and case_maps.case_id = cases.id', user_id]))
end
Or one better:
named_scope :find_by_user_id, lambda{|user_id|
{
:joins => :case_map,
:conditions => ['case_maps.user_id = ? and case_maps.case_id = cases.id', user_id]
}
}
The named scope will ensure all your options are cleanly merged (all your conditions are applied even if you add more later on). You can then call it with:
Case.find_by_user_id(user_id).all(:limit => 15)
(Named Scope are “Da Bom”. One should use them as much as possible. Even in general conversation. For example, if you were here in my house last night, you may have overheard this little ditty: “What do you want for your dinner?”, “Same thing I have every night, Named Scopes and chips.”. Because I'm committed and all that.)
Also, as a side note, assuming this is on your Case model, the "and case_maps.case_id = cases.id" clause is unnecessary, :joins => :case_map does that for you.

Resources