In my Contacts class, after a contact is created with their email address, I try to pull as much contact data from FullContact's API as possible.
I'm having this issue where if one column of data doesn't exist for a "person" in FullContact, it throws a NoMethodError and I don't get to save the rest of the data that potentially does exist to the contact, because my method stops at the error.
How can I rescue from a NoMethodError and get my method to continue running the rest of it? Like for it to just skip over the error and try the rest of the code. I've tried next and continue in my rescue code but that doesn't work.
Thanks for any help.
class Contact < ActiveRecord::Base
belongs_to :user
after_create do |contact|
contact.delay.update_fullcontact_data
end
def update_fullcontact_data
person = FullContact.person(self.email)
if person.contact_info.given_name.present?
self.name = person.contact_info.given_name
end
if person.contact_info.family_name.present?
self.last_name = person.contact_info.family_name
end
if person.demographics.location_general.present?
self.city = person.demographics.location_general
end
save!
rescue NoMethodError => exception
puts "Hit a NoMethodError"
save!
end
end
in general what may be a solution for your problem is try method (http://apidock.com/rails/Object/try). To make the story short - it returns nil instead of raising exception if method does not exist on specific object
If you just want to make sure it saves, you can use ensure do something like this:
class Contact < ActiveRecord::Base
belongs_to :user
after_create do |contact|
contact.delay.update_fullcontact_data
end
def update_fullcontact_data
person = FullContact.person(self.email)
if person.contact_info.given_name.present?
self.name = person.contact_info.given_name
end
if person.contact_info.family_name.present?
self.last_name = person.contact_info.family_name
end
if person.demographics.location_general.present?
self.city = person.demographics.location_general
end
save!
ensure
save!
end
end
More info:
http://blog.rubybestpractices.com/posts/rklemme/003-The_Universe_between_begin_and_end.html
Related
How can I go about rescuing an error that was generated in an after_save callback, and then ultimately display it to the user? The code in my model looks something like this:
Class MyModel
after_save :call_other_class_responsible_for_parsing
def call_other_class_responsible_for_parsing
# this method is used by multiple models
ModelTwo.parse_css
end
end
In my controller, I currently redirect the user elsewhere if the update was successful, however, I consider the update to be successful if it passed all of the existing validations and there were no errors in the callback (from the Less::Parser).
EDIT:
I mixed up my thoughts in my original question. MyModel gets saved from it's corresponding controller, which then runs the after_save callback from the model. Inside call_other_class_responsible_for_parsing, there is a call to another model, let's say ModelTwo, which does the Less parsing. I've tried using code like this:
def self.parse_css
#my_model = MyModel.find(1)
css_to_compile = Less::Parser.new.parse(css).to_css
rescue Less::Error => error
#my_model.errors[:base] << "Error message"
false
end
end
But the false does not prevent the transaction from succeeding, therefore a redirect happens.
You could use transaction :
def create
MyModel.transaction do
#my_model.save
#my_model.my_method
end
rescue ActiveRecord::RecordInvalid => exception
# rescue active record exception here
rescue Less::Error => exception
# rescue less error here
end
This remove the need for a callback.
I hope this helps!
Callback chains are implicitly wrapped in a transaction. When a callback returns false or raises an exception then the whole transaction is rolled back and saving fails.
In your case, you're parsing some CSS, so I'm not sure whether after_save is the right place for this. I recommend you give validations a try. Consider the following:
class MyModel < ActiveRecord::Base
validate :valid_css
private
def parsed_css
#parsed_css ||= Less::Parser.new.parse(css).to_css
end
def valid_css
parsed_css
rescue Less::Error => error
errors.add(:css, "Cannot parse CSS: #{error}")
end
end
This will parse the CSS before saving the object and add an error if it's invalid. Also, the result of #to_css will be stored so that you won't need to recompute it. This is the approach that I'd recommend.
If you'd like to stick with after_save then you should raise an exception to abort the transaction. In your case, it's simply about not rescuing Less::Error:
class MyModel < ActiveRecord::Base
after_save :compile_css
private
def compile_css
css_to_compile = Less::Parser.new.parse(css).to_css
end
end
If you're having trouble deciding which approach to use then leave a question below and I'll help.
I'm trying to handle the situation where the user has entered info incorrectly, so I have a path that follows roughly:
class Thing < AR
before_validation :byebug_hook
def byebug_hook
byebug
end
end
thing = Thing.find x
thing.errors.add(:foo, "bad foo")
# Check byebug here, and errors added
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
byebug for byebug_hook> errors.messages #=> {}
Originally I thought that maybe the model was running its own validations and overwriting the ones I added, but as you can see even when I add the before hook the errors are missing, and I'm not sure what's causing it
ACTUAL SOLUTION
So, #SteveTurczyn was right that the errors needed to happen in a certain place, in this case a service object called in my controller
The change I made was
class Thing < AR
validate :includes_builder_added_errors
def builder_added_errors
#builder_added_errors ||= Hash.new { |hash, key| hash[key] = [] }
end
def includes_builder_added_errors
builder_added_errors.each {|k, v| errors.set(k, v) }
end
end
and in the builder object
thing = Thing.find x
# to my thinking this mirrors the `errors.add` syntax better
thing.builder_added_errors[:foo].push("bad foo") if unshown_code_does_stuff?
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
update_attributes will validate the model... this includes clearing all existing errors and then running any before_validation callbacks. Which is why there are never any errors at the pont of before_validation
If you want to add an error condition to the "normal" validation errors you would be better served to do it as a custom validation method in the model.
class Thing < ActiveRecord::Base
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo")
end
end
If you want some validations to occur only in certain controllers or conditions, you can do that by setting an attr_accessor value on the model, and setting a value before you run validations directly (:valid?) or indirectly (:update, :save).
class Thing < ActiveRecord::Base
attr_accessor :check_foo
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo") if check_foo
end
end
In the controller...
thing = Thing.find x
thing.check_foo = true
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
Given a model Orderstatus with attributes private_status:string, and private_status_history:json(I'm using Postgresql's json). I would like to record each status transition, together with the user who made the change.
Ideally it would be something like:
class Orderstatus < ActiveRecord::Base
after_save :track_changes
def track_changes
changes = self.changes
if self.private_status_changed?
self.private_status_history_will_change!
self.private_status_history.append({
type: changes[:private_status],
user: current_user.id
})
end
end
end
class OrderstatusController <ApplicationController
def update
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
#Desired behaviour (process not run with console)
status = Orderstatus.new(private_status:'one')
status.private_status #=> 'one'
status.private_status_history #=> []
status.update_attributes({:private_status=>'two'}) #=>true
status.private_status #=> 'two'
status.private_status_history #=> [{type:['one','two'],user:32]
What would be the recommended practice to achieve this? Apart from the usual one using Thread. Or maybe, any suggestion to refactor the structure of the app?
So, I finally settled for this option ( I hope it's not alarming to anyone :S)
class Orderstatus < ActiveRecord::Base
after_save :track_changes
attr_accessor :modifying_user
def track_changes
changes = self.changes
if self.private_status_changed?
newchange = {type:changes[:private_status],user: modifying_user.id}
self.update_column(:private_status_history,
self.private_status_history.append(newchange))
end
end
end
class OrderstatusController <ApplicationController
def update
#status.modifying_user = current_user # <---- HERE!
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
Notes:
- I pass the from the Controller to the Model through an instance attribute modifying_user of the class Orderstatus. That attribute is ofc not saved to the db.
- Change of method to append new changes to the history field. I.e. attr_will_change! + save to update_column + append
I have defined a callback after_find for checking some settings based on the retrieved instance of the model. If the settings aren't fulfilled I don't want the instance to be return from the find method. Is that possible?
an example
the controller looks like:
class UtilsController < ApplicationController
def show
#util = Util.find(params[:id])
end
end
the model:
class Util < ActiveRecord::Base
after_find :valid_util_setting
def valid_util_setting
# calculate_availability? complex calculation
# that can not be part of the sql statement or a scope
unless self.setting.calculate_availability?(User.current.session)
#if not available => clear the record for view
else
#nothing to do here
end
end
end
Instead of trying to clear the record, you could just raise an exception?
E.g.
unless self.setting.calculate_availability?(User.current.session)
raise ActiveRecord::RecordNotFound
else
...
I'm afraid you can't clear found record in this callback
Maybe you should find in scope with all your options from the beginning?
I.e. #util = Util.scoped.find(params[:id])
I found a solution
def valid_util_setting
Object.const_get(self.class.name).new().attributes.symbolize_keys!.each do |k,v|
begin
self.assign_attributes({k => v})#, :without_protection => true)
rescue ActiveModel::MassAssignmentSecurity::Error => e; end
end
end
With this I'm able to create an almost empty object
Consider the following:
class Manager < ActiveRecord::Base
has_many :employees
end
class Employee < ActiveRecord::Base
belongs_to :manager
end
employee = Employee.first
puts employee.manager.name
If for some reason an employee does not have a manager I get this:
undefined method `name' for nil:NilClass
Which makes sense. However, is there a clean/suggested way to handle this, so I don't always have to check and see if an employee actually has a manager before I ask for the manager's name?
Try:
puts employee.manager.name unless employee.manager.nil?
Or:
puts (employee.manager.nil? ? "No manager" : employee.manager.name)
Which is equivalent in this case to:
puts (employee.manager ? employee.manager.name : "No manager")
(Equivalent as long as employee.manager can't return false.)
Don't try to be too clever. If you're going to use this repeatedly, make it a function.
class Employee
def manager_name
manager.try(:name).to_s # force empty string for nil
end
end
You can check using the has_attribute? method:
employee.has_attribute? :manager
So something like:
puts employee.manager.name if employee.has_attribute? :manager
I tend to prefer puts employee.manager.try(:name)
The try method returns nil if it was called on nil, but will execute the method properly if manager wasn't nil.
If you want default text:
puts employee.manager.try(:name) || "No Manager"