Validations during ActiveRecord callbacks - ruby-on-rails

Is it possible to perform validations when creating new instances of an associated model within a before_save callback in ruby?
class Podcast < ActiveRecord::Base
has_many :tracks, :dependent=>:destroy
before_save :generate_tracks
# creates the tracks played in the podcast
def generate_tracks
json = Hashie::Mash.new HTTParty.get("#{self.json_url}")
json.sections.each do |section|
if section.section_type=="track"
track = self.tracks.build :name=>section.track.name
end
end
end
end
The above code works fine but I was hoping to add something like this inside the if statement:
unless track.valid?
errors[:base] << "OOPS, something went wrong whilst trying to build tracklist."
return false
end
The problem with this code is that track.valid? always returns false because the Track model validates the presence of podcast_id. I don't feel so comfortable doing this in an after_create callback because I want to actually cancel podcast creation if the tracklist doesn't validate too. So what can I do?

Sounds to me as though what you want is validates_associated, which would let you do:
class Podcast < ActiveRecord::Base
has_many :tracks
validates_associated :tracks
end
That way, a podcast won't save unless it's associated tracks are valid.

Related

Create two models at same time with validation

I'm having a potluck where my friends are coming over and will be bringing one or more food items. I have a friend model and each friend has_many food_items. However I don't want any two friends to bring the same food_item so food_item has to have a validations of being unique. Also I don't want a friend to come (be created) unless they bring a food_item.
I figure the best place to conduct all of this will be in the friend model. Which looks like this:
has_many :food_items
before_create :make_food_item
def make_food_item
params = { "food_item" => food_item }
self.food_items.create(params)
end
And the only config I have in the food_item model is:
belongs_to :friend
validates_uniqueness_of :food_item
I forsee many problems with this but rails is telling me the following error: You cannot call create unless the parent is saved
So how do I create two models at the same time with validations being checked so that if the food_item isn't unique the error will report properly to the form view?
How about to use nested_attributes_for?
class Friend < ActiveRecord::Base
has_many :food_items
validates :food_items, :presence => true
accepts_nested_attributes_for :food_items, allow_destroy: true
end
You're getting the error because the Friend model hasn't been created yet since you're inside the before_create callback. Since the Friend model hasn't been created, you can't create the associated FoodItem model. So that's why you're getting the error.
Here are two suggestions of what you can do to achieve what you want:
1) Use a after_create call back (I wouldn't suggest this since you can't pass params to callbacks)
Instead of the before_create you can use the after_create callback instead. Here's an example of what you could do:
class Friend
after_create :make_food_item
def make_food_item
food_params = # callbacks can't really take parameters so you shouldn't really do this
food = FoodItem.create food_params
if food.valid?
food_items << food
else
destroy
end
end
end
2) Handle the logic creation in the controller's create route (probably best option)
In your controller's route do the same check for your food item, and if it's valid (meaning it passed the uniqueness test), then create the Friend model and associate the two. Here is what you might do:
def create
friend_params = params['friend']
food_params = params['food']
food = FoodItem.create food_params
if food.valid?
Friend.create(friend_params).food_items << food
end
end
Hope that helps.
As mentioned, you'll be be best using accepts_nested_attributes_for:
accepts_nested_attributes_for :food_items, allow_destroy: true, reject_if: reject_if: proc { |attributes| attributes['foot_item'].blank? }
This will create a friend, and not pass the foot_item unless one is defined. If you don't want a friend to be created, you should do something like this:
#app/models/food_item.rb
Class FootItem < ActiveRecord::Base
validates :[[attribute]], presence: { message: "Your Friend Needs To Bring Food Items!" }
end
On exception, this will not create the friend, and will show the error message instead

How can I invoke the after_save callback when using 'counter_cache'?

I have a model that has counter_cache enabled for an association:
class Post
belongs_to :author, :counter_cache => true
end
class Author
has_many :posts
end
I am also using a cache fragment for each 'author' and I want to expire that cache whenever #author.posts_count is updated since that value is showing in the UI. The problem is that the internals of counter_cache (increment_counter and decrement_counter) don't appear to invoke the callbacks on Author, so there's no way for me to know when it happens except to expire the cache from within a Post observer (or cache sweeper) which just doesn't seem as clean.
Any ideas?
I had a similar requirement to do something on a counter update, in my case I needed to do something if the counter_cache count exceeded a certain value, my solution was to override the update_counters method like so:
class Post < ApplicationRecord
belongs_to :author, :counter_cache => true
end
class Author < ApplicationRecord
has_many :posts
def self.update_counters(id, counters)
author = Author.find(id)
author.do_something! if author.posts_count + counters['posts_count'] >= some_value
super(id, counters) # continue on with the normal update_counters flow.
end
end
See update_counters documentation for more info.
I couldn't get it to work either. In the end, I gave up and wrote my own cache_counter-like method and call it from the after_save callback.
I ended up keeping the cache_counter as it was, but then forcing the cache expiry through the Post's after_create callback, like this:
class Post
belongs_to :author, :counter_cache => true
after_create :force_author_cache_expiry
def force_author_cache_expiry
author.force_cache_expiry!
end
end
class Author
has_many :posts
def force_cache_expiry!
notify :force_expire_cache
end
end
then force_expire_cache(author) is a method in my AuthorSweeper class that expires the cache fragment.
Well, I was having the same problem and ended up in your post, but I discovered that, since the "after_" and "before_" callbacks are public methods, you can do the following:
class Author < ActiveRecord::Base
has_many :posts
Post.after_create do
# Do whatever you want, but...
self.class == Post # Beware of this
end
end
I don't know how much standard is to do this, but the methods are public, so I guess is ok.
If you want to keep cache and models separated you can use Sweepers.
I also have requirement to watch counter's change. after digging rails source code, counter_column is changed via direct SQL update. In other words, it will not trigger any callback(in your case, it will not trigger any callback in Author model when Post update).
from rails source code, counter_column was also changed by after_update callback.
My approach is give rails's way up, update counter_column by myself:
class Post
belongs_to :author
after_update :update_author_posts_counter
def update_author_posts_counter
# need to update for both previous author and new author
# find_by will not raise exception if there isn't any record
author_was = Author.find_by(id: author_id_was)
if author_was
author_was.update_posts_count!
end
if author
author.update_posts_count!
end
end
end
class Author
has_many :posts
after_update :expires_cache, if: :posts_count_changed?
def expires_cache
# do whatever you want
end
def update_posts_count!
update(posts_count: posts.count)
end
end

Validate presence of nested attributes

How do I validate that a model has at least one associated model using nested attributes? This has been driving me crazy as I am sure that I am missing something simple. For example, I want to require that a List always has at least one Task.
class List < ActiveRecord::Base
has_many :tasks, :dependent => :destroy
accepts_nested_attributes_for :tasks, :allow_destroy => true
end
class Task < ActiveRecord::Base
belongs_to :list
end
I've tried many different options.
1- adding a validation to lists:
def validate
if self.tasks.length < 1
self.errors[:base] << "A list must have at least one task."
end
end
but this will still allow you to delete all the tasks of an existing list since when deleting tasks the validation of list happens before the tasks are destroyed.
2- checking to see if any tasks are not marked for destruction in a before_save callback
before_save :check_tasks
private
#look for any task which won't be deleted
def check_tasks
for t in self.tasks
return true if ! t.marked_for_destruction?
end
false
end
For some reason I can't get it to ever delete a task with anything that iterates over a list's tasks. The same is true if I do this check in def validate instead of a callback
3- requiring the presence of tasks validates_presence_of :tasks, but with this it won't ever delete any tasks
You can check both conditions together in validation method:
validate :check_tasks
def check_tasks
if self.tasks.size < 1 || self.tasks.all?{|task| task.marked_for_destruction? }
errors.add_to_base("A list must have at least one task.")
end
end
I ended up extending Magazine's save method to get around the problem. It worked like a charm.
def save
saved = false
ActiveRecord::Base.transaction do
saved = super
if self.conditions.size < 1
saved = false
errors[:base] << "A rule must have at least one condition."
raise ActiveRecord::Rollback
end
end
saved
end

Rails accepts_nested_attributes_for callbacks

I have two models Ticket and TicketComment, the TicketComment is a child of Ticket.
ticket.rb
class Ticket < ActiveRecord::Base
has_many :ticket_comments, :dependent => :destroy, :order => 'created_at DESC'
# allow the ticket comments to be created from within a ticket form
accepts_nested_attributes_for :ticket_comments, :reject_if => proc { |attributes| attributes['comment'].blank? }
end
ticket_comment.rb
class TicketComment < ActiveRecord::Base
belongs_to :ticket
validates_presence_of :comment
end
What I want to do is mimic the functionality in Trac, where if a user makes a change to the ticket, and/or adds a comment, an email is sent to the people assigned to the ticket.
I want to use an after_update or after_save callback, so that I know the information was all saved before I send out emails.
How can I detect changes to the model (ticket.changes) as well as whether a new comment was created or not (ticket.comments) and send this update (x changes to y, user added comment 'text') in ONE email in a callback method?
you could use the ActiveRecord::Dirty module, which allows you to track unsaved changes.
E.g.
t1 = Ticket.first
t1.some_attribute = some_new_value
t1.changed? => true
t1.some_attribute_changed? => true
t1.some_attribute_was => old_value
So inside a before_update of before_create you should those (you can only check before the save!).
A very nice place to gather all these methods is in a Observer-class TicketObserver, so you can seperate your "observer"-code from your actual model.
E.g.
class TicketObserver < ActiveRecord::Observer
def before_update
.. do some checking here ..
end
end
to enable the observer-class, you need to add this in your environment.rb:
config.active_record.observers = :ticket_observer
This should get you started :)
What concerns the linked comments. If you do this:
new_comment = ticket.ticket_comments.build
new_comment.new_record? => true
ticket.comments.changed => true
So that would be exactly what you would need. Does that not work for you?
Note again: you need to check this before saving, of course :)
I imagine that you have to collect the data that has changed in a before_create or before_update, and in an after_update/create actually send the mail (because then you are sure it succeeded).
Apparently it still is not clear. I will make it a bit more explicit. I would recommend using the TicketObserver class. But if you want to use the callback, it would be like this:
class Ticked
before_save :check_state
after_save :send_mail_if_needed
def check_state
#logmsg=""
if ticket_comments.changed
# find the comment
ticket_comments.each do |c|
#logmsg << "comment changed" if c.changed?
#logmsg << "comment added" if c.new_record?
end
end
end
end
def send_mail_if_needed
if #logmsg.size > 0
..send mail..
end
end

Using accepts_nested_attributes_for + mass assignment protection in Rails

Say you have this structure:
class House < ActiveRecord::Base
has_many :rooms
accepts_nested_attributes_for :rooms
attr_accessible :rooms_attributes
end
class Room < ActiveRecord::Base
has_one :tv
accepts_nested_attributes_for :tv
attr_accessible :tv_attributes
end
class Tv
belongs_to :user
attr_accessible :manufacturer
validates_presence_of :user
end
Notice that Tv's user is not accessible on purpose. So you have a tripple-nested form that allows you to enter house, rooms, and tvs on one page.
Here's the controller's create method:
def create
#house = House.new(params[:house])
if #house.save
# ... standard stuff
else
# ... standard stuff
end
end
Question: How in the world would you populate user_id for each tv (it should come from current_user.id)? What's the good practice?
Here's the catch22 I see in this.
Populate user_ids directly into params hash (they're pretty deeply nested)
Save will fail because user_ids are not mass-assignable
Populate user for every tv after #save is finished
Save will fail because user_id must be present
Even if we bypass the above, tvs will be without ids for a moment of time - sucks
Any decent way to do this?
Anything wrong with this?
def create
#house = House.new(params[:house])
#house.rooms.map {|room| room.tv }.each {|tv| tv.user = current_user }
if #house.save
# ... standard stuff
else
# ... standard stuff
end
end
I haven't tried this out, but it seems like the objects should be built and accessible at this point, even if not saved.

Resources