NoMethodError when method is returning nil - ruby-on-rails

I have two methods which are used to determine whether to apply a class to the page to show that something is overdue and needs attention.
I'm getting an error when a brand new user registers:
undefined method `last_contact_done_date=' for #<User:0x6183708>
The line that it's referencing the error to is this:
2: <div class="span3 <%= "overdue" if (signed_in? && contact_overdue?(current_user.id)) %>">
The contact_overdue? method is this (in a home page helper)
def contact_overdue?(user_id)
#user = User.find_by_id(user_id)
return true if (Date.today - (#user.last_contact_done_date ||= Date.tomorrow)) > 6
end
and the last_contact_done_date method is this in the User model
def last_contact_done_date
self.contacts.order('date_done DESC').first.try(:date_done)
end
I thought that if I was using the ||= operator in the contact_overdue? method, then I would return -1 if the last_contact_done_date is nil. But it appears that's not working. What operator should I be using in last_contact_done_date or how should I change the contact_overdue? method so that if there are no contacts, then false is returned from the contact_overdue? method?

To return the default value of -1 when there is no last contact done date, use
#user.last_contact_done_date || -1
(it is unreasonable to expect Date.tomorrow to return -1 ;) )
||= is an assignment operator; a ||= b is equivalent to a = a || b; and if a is an attribute (i.e. is prefixed with a dot and an instance, c.a), assignment to it will call the method a=. Thus, your code necessitates that you have a method named last_contact_done_date= to handle the assignment, which you don't.

Related

ruby about attr_accessor, instance variables, local varibles

I'm so confused about that..
like this
class Box
attr_accessor :item ,:item2
def initialize(item2)
#item = []
#item2 = item2
end
def add(product)
item << product
end
def empty?
item.empty?
end
def increment(n=1)
item2 +=1
end
end
cart =Box.new(123)
cart.add(1)
puts cart.empty? #false
puts cart.item #1
in the 'add' and 'empty?' methods
I use local variable 'item' right?
why I can get the value from #items ??
and I try this
cart.item2 = 345
puts cart.item2 #345
puts cart.increment #'increment': undefined method `+' for nil:NilClass (NoMethodError)
now I can't get the value?
please fix my brain thx
First, read this answer, which is the most-upvoted Ruby post in StackOverflow history. It will help you understand attr_accessor and its cousins attr_reader and attr_writer.
Besides that, your code has many problems.
First, you should not name an Array with a singular variable name like item. Use a plural items to make its purpose clear.
Second, the name item2 is not good. For your attribute, use something descriptive like counter, and for the variable passed as an argument to initialize it, let's use something descriptive like initial_count.
Third, your increment method takes an optional argument but then ignores it. Wouldn't it be surprising if someone called box.increment(2) and the attribute was incremented by only 1? The intent of this method is to use counter += n instead of counter += 1.
Fourth, to set counter from within the class, we need to use self. So instead of counter += n, we have to do self.counter += n.
Finally, consider whether you want the attributes to be readable and writable from an outside source, or whether you want to reserve write privileges to the object itself. Because you have methods to add things to items and to increment counter, you probably want to conceal write privileges. I would use attr_reader publicly and attr_writer privately.
Incorporating these suggestions, here's the resulting code:
class Box
attr_reader :counter, :items
def initialize(initial_count)
#counter = initial_count
#items = []
end
def add(product)
items << product
end
def empty?
items.empty?
end
def increment(n = 1)
self.counter += n
end
private
attr_writer :counter, :items
end
Now you can do this, all of which makes sense, more or less:
>> cart = Box.new(123)
>> cart.increment(2)
>> cart.counter
#> 125
>> cart.add('A product')
>> cart.add('Another product')
>> cart.items
#> ["A product", "Another product"]
But if you try to set counter or items directly, you'll get an error:
>> cart.counter = 1
#> NoMethodError: private method `counter=' called for #<Box:0x007fc13e17dc50>

Interpolating an attribute's key before save

I'm using Rails 4 and have an Article model that has answer, side_effects, and benefits as attributes.
I am trying to create a before_save method that automatically looks at the side effects and benefits and creates links corresponding to another article on the site.
Instead of writing two virtually identical methods, one for side effects and one for benefits, I would like to use the same method and check to assure the attribute does not equal answer.
So far I have something like this:
before_save :link_to_article
private
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.key.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.key = linked_attrs.join('; ')
end
end
end
but chaining on the key like that gives me an undefined method 'key'.
How can I go about interpolating in the attribute?
in this bit: self.key you are asking for it to literally call a method called key, but what you want, is to call the method-name that is stored in the variable key.
you can use: self.send(key) instead, but it can be a little dangerous.
If somebody hacks up a new form on their browser to send you the attribute called delete! you don't want it accidentally called using send, so it might be better to use read_attribute and write_attribute.
Example below:
def link_to_article
self.attributes.each do |key, value|
unless key == "answer"
linked_attrs = []
self.read_attribute(key).split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.write_attribute(key, linked_attrs.join('; '))
end
end
end
I'd also recommend using strong attributes in the controller to make sure you're only permitting the allowed set of attributes.
OLD (before I knew this was to be used on all attributes)
That said... why do you go through every single attribute and only do something if the attribute is called answer? why not just not bother with going through the attributes and look directly at answer?
eg:
def link_to_article
linked_attrs = []
self.answer.split(';').each do |i|
a = Article.where('lower(specific) = ?', i.downcase.strip).first
if a && a.approved?
linked_attrs.push("<a href='/questions/#{a.slug}' target=_blank>#{i.strip}</a>")
else
linked_attrs.push(i.strip)
end
end
self.answer = linked_attrs.join('; ')
end

Return all user profile except current users'

I am iterating over all user profiles to get their country. I wish not to include the current user's profile. If I have a total of 10 users, I want only 9 user's country:
# helper function
def method_name
Profile.all.select do |m|
n = m.country.class == String # Because most countries will be nil. A shorter way to not include nil profiles?
return n.reject! {|x| x == current_user.profile.user_id }
end
end
The method above should return all user's profile except for the current user.
The error I got is
undefined method `reject!' for false:FalseClass
The reason? In my views I could:
<%= method_name.map do |p| p.country end %>
Beware of using Profile.all here, because you will potentially use a lot of memory instantiating a profile object for every row in your database.
I think the best way to do this is to create a method within your Profile model, which performs the query, and uses pluck() to return an array containing just the countries you want.
def self.countries_except_for(user)
where.not(user_id: user.id, country: nil).pluck(:country)
end
Now you can just call:
Profile.countries_except(current_user)
To get the array of countries. This approach is much more efficient than looping through as in your question.
The line n = m.country.class == String sets n to true or false, thefore the next line fails, because you try to call reject! {|x| x == current_user.profile.user_id } on that boolean value.
One way to solve the problem might be to fix your code like this:
def method_name
Profile.all.select do |p|
p.country.present? && p != current_user.profile
end
end
Another way - and my preferred one - would be to only load matching record from the database:
def method_name
Profile.where.not(user_id: current_user.id).where.not(country: nil)
end

Writing a method that simply multiplies the object by something

I'm trying to convert a bunch of numbers from imperial to metric on the front end of my site depending on if the user has set their measurement_units to 'metric' or 'imperial'
I can just do #myWeight*.45 to convert the number, but what I want to do is write a helper method like this
def is_imperial?
if User.measurement_units == 'metric'
*0.453592
elsif User.measurement_units == 'imperial'
*1
end
end
then be able to do this: #myWeight*.is_imperial?
I'm just not sure how I would assign the *value to the method is_imperial?
Thanks for the help!
EDIT:
#myWeight is a float calculated from adding several numbers.
I'm just trying to find an elegant way of converting any number that shows up on the site to metric if the user has metric as the value in the measurement_units field on the User model.
I assumed I would need to create a helper method in the application_helper.rb. Is that not correct?
I think you mean something like this:
class User
def imperial
f_multiplier = 0.0
f_multiplier = !!(self.measurement_units == 'metric') ? 0.453592 : 1
imperial = self.weight * f_multiplier
end
end
puts #myWeight.imperial
If you want the measurement_units method to be dynamic based on the user, then I think you need to make it an instance method.
Modify the is_imperial? method to return the right number:
def is_imperial?
if measurement_units == 'metric'
0.453592
elsif measurement_units == 'imperial'
1
end
end
Then you can call the method with something like this:
#myWeight.send(:*, is_imperial?)
If #myWeight represents a User object you might have to change it to this:
#myWeight.weight.send(:*, is_imperial?)
Methods that end with a ? in Ruby are expected to return true or false, so you should rename the method to be something like weight_conversion_factor.
Assuming #myWeight is Float value, you seem like you are looking for how to monkey patch. Check in rails console, #myWeight.class.name returns Float.
For monkey patching in Rails,
Create config/initializers/extensions directory. This is where you will store any future monkey patched methods.
Create a file called, floats.rb.
Add the following code.
class Float
def is_imperial?
if User.measurement_units == 'metric'
self*0.453592
elsif User.measurement_units == 'imperial'
self*1
end
end
end
Make sure to restart the Rails server to reinitialize.

Ruby on Rails: how do I set a variable where the variable being changed can change?

i want to do
current_user.allow_????? = true
where ????? could be whatever I wanted it to be
I've seen it done before.. just don't remember where, or what the thing is called.
foo = "bar"
current_user.send("allow_#{foo}=", true)
EDIT:
what you're asking for in the comment is another thing. If you want to grab a constant, you should use for instance
role = "admin"
User.const_get(role)
That's a "magic method" and you implement the method_missing on your current_user object. Example from Design Patterns
#example method passed into computer builder class
builder.add_dvd_and_harddisk
#or
builder.add_turbo_and_dvd_dvd_and_harddisk
def method_missing(name, *args)
words = name.to_s.split("_")
return super(name, *args) unless words.shift == 'add'
words.each do |word|
#next is same as continue in for loop in C#
next if word == 'and'
#each of the following method calls are a part of the builder class
add_cd if word == 'cd'
add_dvd if word == 'dvd'
add_hard_disk(100000) if word == 'harddisk'
turbo if word == 'turbo'
end
end

Resources