Rails accepts_nested_attributes_for callbacks - ruby-on-rails

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

Related

Activerecord has_one build new when association not found

So I have this model relationships
class User
has_one :wallet, :foreign_key => :user_id
end
class Wallet
after_initialize :set_value
def set_value
# Whatever
end
end
And I'd like that when I do User.last.wallet, User.last.wallet.new gets called.
I could achieve this by creating another method in the User model:
def get_wallet
self.wallet||self.wallet.new
end
and call get_wallet when needed.
But can't I get this without this useless and dirty extra method?
Something like:
has_one :wallet, :foreign_key => :user_id #, :build_if_not_found => true
Gems like this one: https://github.com/febuiles/auto_build don't do what I want: they build Wallet after creating the User object instead of creating when User.last.wallet is called.
Thanks
You can try this:
class User
has_one :wallet, :foreign_key => :user_id
def wallet
super || build_wallet
end
end
You still need to add some extra code, but it will do exactly what you want without any additional calls.

In Rails, how can I create group of users as another association, such as "members"?

I am trying to create a special relationship between two existing models, User and Dwelling. A Dwelling has only one owner (Dwelling belongs_to :user, User has_one :dwelling) at the time of creation. But other Users can be added to this Dwelling as Roomies (there is no model created for this now, Roomie is a conceptual relationship).
I don't think I need a separate model but rather a special relationship with the existing models, but I could be wrong. I think the reference needs to be made with user_id from the Users table. I'm not really sure where to start this. Thank you for any and all help!
For example:
Dwelling1
user_id: 1
roomies: [1, 2, 3, 4]
Where 1, 2, 3, 4 are user_ids.
Updated Models
Dwelling Model
# dwelling.rb
class Dwelling < ActiveRecord::Base
attr_accessible :street_address, :city, :state, :zip, :nickname
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
has_many :roomies, :class_name => "User"
validates :street_address, presence: true
validates :city, presence: true
validates :state, presence: true
validates :zip, presence: true
end
User Model
# user.rb
class User < ActiveRecord::Base
attr_accessible :email, :first_name, :last_name, :password, :password_confirmation, :zip
has_secure_password
before_save { |user| user.email = email.downcase }
before_save :create_remember_token
belongs_to :dwelling
has_many :properties, :class_name => "Dwelling", :foreign_key => "owner_id"
validates :first_name, presence: true, length: { maximum: 50 }
...
Updated Dwelling Create Action
#dwellings_controller.rb
...
def create
#dwelling = current_user.properties.build(params[:dwelling])
if #dwelling.save
current_user.dwelling = #dwelling
if current_user.save
flash[:success] = "Woohoo! Your dwelling has been created. Welcome home!"
redirect_to current_user
else
render 'new'
end
end
end
...
My answer assumes you only want a user to be a roomie at one dwelling. If you want a user to be a roomie at more than one dwelling, I think #ari's answer is good, although I might opt for has_and_belongs_to_many instead of has_many :through.
Now for my answer:
I would set it up so that a dwelling belongs_to an owner and has_many roomies (including possibly the owner, but not necessarily).
You can use the User model both for owners and roomies. You don't need any additional tables or models, you just need to setup the proper relationships by using the :class_name and :foreign_key options.
In your Dwelling model:
# dwelling.rb
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
has_many :roomies, :class_name => "User"
In your User model:
# user.rb
belongs_to :dwelling # This is where the user lives
has_many :properties, :class_name => "Dwelling", :foreign_key => "owner_id" # This is the dwellings the user owns
In your dwellings table you need an owner_id column to store the user_id of the owner
In your users table you need a dwelling_id to store the dwelling_id of the dwelling where the user lives.
To answer your question in the comments regarding the controller:
If you want to setup current_user as the owner of the new dwelling, do this:
#dwelling = current_user.properties.build(params[:dwelling])
....
If you want to setup the current_user as the owner AND a roomie of the new dwelling, do this:
#dwelling = current_user.properties.build(params[:dwelling]
if #dwelling.save
current_user.dwelling = #dwelling
if current_user.save
# flash and redirect go here
else
# It's not clear why this wouldn't save, but you'll to determine
# What to do in such a case.
end
else
...
end
The trickiest part of above is handling the case that the dwelling is valid and saves, but for some unrelated reason the current_user can't be saved. Depending on your application, you may want the dwelling to save anyway, even if you can't assign the current_user as a roomie. Or, you might want the dwelling not to be saved --- if so, you'd need to use a model transaction, which is bit beyond the scope of this question.
Your controller code didn't work because saving the Dwelling doesn't actually update the current_user record to store the dwelling_id. Your code would be equivalent to the following:
#dwelling = Dwelling.new(params[:dwelling])
current_user.dwelling = #dwelling
if #dwelling.save
...
Note that current_user is never saved, so the current_user.dwelling = #dwelling line is useless.
This might seem counter-intuitive, but the bottom line is that build_dwelling isn't actually setting up things in memory as you might expect. You'd achieve more intuitive results if you saved the model you're building from rather than the model you're building:
#dwelling = current_user.build_dwelling(params[:dwelling])
if current_user.save # This will save the dwelling (if it is valid)
However, this (by default) won't save the dwelling if it has validation errors unless you turn :autosave on for the association, which is also a bit beyond the scope of this question. I really wouldn't recommend this approach.
Update:
Here is a more detailed code snippet:**
# dwellings_controller.rb
def create
#dwelling = current_user.properties.build(params[:dwelling])
if #dwelling.save
# The current user is now the owner, but we also want to try to assign
# his as a roomie:
current_user.dwelling = #dwelling
if current_user.save
flash[:notice] = "You have successfully created a dwelling"
else
# For some reason, current_user couldn't be assigned as a roomie at the
# dwelling. This could be for several reasons such as validations on the
# user model that prevent the current_user from being saved.
flash[:notice] = "You have successfully created a dwelling, but we could not assign you to it as a roomie"
end
redirect_to current_user
else
# Dwelling could not be saved, so re-display the creation form:
render :new
end
end
When a dwelling saves successfully, the current user will be the owner (owner_id in the database). However, if the current_user doesn't save, you'll need to decide how your application should respond to that. In the example above, I allow the dwelling to be saved (i.e. I don't rollback its creation), but I inform the user that he couldn't be assigned as a roomie. When this happens, it's most likely other code in your application causing the problem. You could examine the errors of current_user to see why. Or, you could use current_user.save! instead of current_user.save temporarily to troubleshoot.
Another way to do all of this is with an after_create callback in the Dwelling model. In many ways that would be a cleaner and simpler way to do it. However, catching the case when the current_user can't be saved could be even uglier than the method above, depending on how you want to handle it.
I believe the bottom line is that the current_user.save code is causing some problems. You'll need to diagnose why, and then determine what your application should do in that case. There are several ways to handle this, including at least the following
Put everything in a transaction block, and use current_use.save! instead of current_user.save so that an exception is raised and neither the user or dwelling is saved.
Save the dwelling, but inform the user that he isn't a roomie (As above)
Instead of saving the current_user, use update_column (which avoids callbacks, validations, etc.).
I believe the current problems you're experiencing are essentially unrelated to the original question. If you need further assistance, it might be best to break it off as a separate question.
You could do this by storing Roomie ids as a column in Dwelling
Make a migration:
class AddRoomiesToDwelling < ActiveRecord::Migration
def self.up
add_column :dwelling, :roomies, :text
end
def self.down
remove_column :dwelling, :roomies
end
end
In your Dwelling model:
class Dwelling < ActiveRecord::Base
serialize :roomies
end
You can then set the roomie ids with:
roomie_ids = [1, 2, 3, 4]
#dwelling.roomies = {:ids => roomie_ids}
#dwelling.save!
Taken from the Saving arrays, hashes, and other non-mappable objects in text columns section of this
You have two possible options.
Depending on your plan, it might be clearer for the dwelling to have_one owner instead of the owner having one dwelling. Then the dwelling would also be able to have users. You can add a column to User called dwelling_id and then you could do dwelling has_many users.
Another option would be to use the "has_many through" association. This means you would need to create a new model that would keep track of this association, say "Relationship.rb", which would belong to both User and Dwelling (and have columns for both for them). Then you would be able to write code like this:
//in Dwelling.rb
has_many :roomies, through: :relationships, source: :user
//in User.rb
has_many :dwellings, through: :relationships
This would let users also join more than one dwelling.

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

accepts_nested_attributes_for with find_or_create?

I'm using Rails' accepts_nested_attributes_for method with great success, but how can I have it not create new records if a record already exists?
By way of example:
Say I've got three models, Team, Membership, and Player, and each team has_many players through memberships, and players can belong to many teams. The Team model might then accept nested attributes for players, but that means that each player submitted through the combined team+player(s) form will be created as a new player record.
How should I go about doing things if I want to only create a new player record this way if there isn't already a player with the same name? If there is a player with the same name, no new player records should be created, but instead the correct player should be found and associated with the new team record.
When you define a hook for autosave associations, the normal code path is skipped and your method is called instead. Thus, you can do this:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
else
self.author.save!
end
end
end
This code is untested, but it should be pretty much what you need.
Don't think of it as adding players to teams, think of it as adding memberships to teams. The form doesn't work with the players directly. The Membership model can have a player_name virtual attribute. Behind the scenes this can either look up a player or create one.
class Membership < ActiveRecord::Base
def player_name
player && player.name
end
def player_name=(name)
self.player = Player.find_or_create_by_name(name) unless name.blank?
end
end
And then just add a player_name text field to any Membership form builder.
<%= f.text_field :player_name %>
This way it is not specific to accepts_nested_attributes_for and can be used in any membership form.
Note: With this technique the Player model is created before validation happens. If you don't want this effect then store the player in an instance variable and then save it in a before_save callback.
A before_validation hook is a good choice: it's a standard mechanism resulting in simpler code than overriding the more obscure autosave_associated_records_for_*.
class Quux < ActiveRecord::Base
has_and_belongs_to_many :foos
accepts_nested_attributes_for :foos, reject_if: ->(object){ object[:value].blank? }
before_validation :find_foos
def find_foos
self.foos = self.foos.map do |object|
Foo.where(value: object.value).first_or_initialize
end
end
end
When using :accepts_nested_attributes_for, submitting the id of an existing record will cause ActiveRecord to update the existing record instead of creating a new record. I'm not sure what your markup is like, but try something roughly like this:
<%= text_field_tag "team[player][name]", current_player.name %>
<%= hidden_field_tag "team[player][id]", current_player.id if current_player %>
The Player name will be updated if the id is supplied, but created otherwise.
The approach of defining autosave_associated_record_for_ method is very interesting. I'll certainly use that! However, consider this simpler solution as well.
Just to round things out in terms of the question (refers to find_or_create), the if block in Francois' answer could be rephrased as:
self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save!
This works great if you have a has_one or belongs_to relationship. But fell short with a has_many or has_many through.
I have a tagging system that utilizes a has_many :through relationship. Neither of the solutions here got me where I needed to go so I came up with a solution that may help others. This has been tested on Rails 3.2.
Setup
Here are a basic version of my Models:
Location Object:
class Location < ActiveRecord::Base
has_many :city_taggables, :as => :city_taggable, :dependent => :destroy
has_many :city_tags, :through => :city_taggables
accepts_nested_attributes_for :city_tags, :reject_if => :all_blank, allow_destroy: true
end
Tag Objects
class CityTaggable < ActiveRecord::Base
belongs_to :city_tag
belongs_to :city_taggable, :polymorphic => true
end
class CityTag < ActiveRecord::Base
has_many :city_taggables, :dependent => :destroy
has_many :ads, :through => :city_taggables
end
Solution
I did indeed override the autosave_associated_recored_for method as follows:
class Location < ActiveRecord::Base
private
def autosave_associated_records_for_city_tags
tags =[]
#For Each Tag
city_tags.each do |tag|
#Destroy Tag if set to _destroy
if tag._destroy
#remove tag from object don't destroy the tag
self.city_tags.delete(tag)
next
end
#Check if the tag we are saving is new (no ID passed)
if tag.new_record?
#Find existing tag or use new tag if not found
tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
else
#If tag being saved has an ID then it exists we want to see if the label has changed
#We find the record and compare explicitly, this saves us when we are removing tags.
existing = CityTag.find_by_id(tag.id)
if existing
#Tag labels are different so we want to find or create a new tag (rather than updating the exiting tag label)
if tag.label != existing.label
self.city_tags.delete(tag)
tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
end
else
#Looks like we are removing the tag and need to delete it from this object
self.city_tags.delete(tag)
next
end
end
tags << tag
end
#Iterate through tags and add to my Location unless they are already associated.
tags.each do |tag|
unless tag.in? self.city_tags
self.city_tags << tag
end
end
end
The above implementation saves, deletes and changes tags the way I needed when using fields_for in a nested form. I'm open to feedback if there are ways to simplify. It is important to point out that I am explicitly changing tags when the label changes rather than updating the tag label.
Answer by #François Beausoleil is awesome and solved a big problem. Great to learn about the concept of autosave_associated_record_for.
However, I found one corner case in this implementation. In case of update of existing post's author(A1), if a new author name(A2) is passed, it will end up changing the original(A1) author's name.
p = Post.first
p.author #<Author id: 1, name: 'JK Rowling'>
# now edit is triggered, and new author(non existing) is passed(e.g: Cal Newport).
p.author #<Author id: 1, name: 'Cal Newport'>
Oringinal code:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
else
self.author.save!
end
end
end
It is because, in case of edit, self.author for post will already be an author with id:1, it will go in else, block and will update that author instead of creating new one.
I changed the code(elsif condition) to mitigate this issue:
class Post < ActiveRecord::Base
belongs_to :author, :autosave => true
accepts_nested_attributes_for :author
# If you need to validate the associated record, you can add a method like this:
# validate_associated_record_for_author
def autosave_associated_records_for_author
# Find or create the author by name
if new_author = Author.find_by_name(author.name)
self.author = new_author
elsif author && author.persisted? && author.changed?
# New condition: if author is already allocated to post, but is changed, create a new author.
self.author = Author.new(name: author.name)
else
# else create a new author
self.author.save!
end
end
end
#dustin-m's answer was instrumental for me - I am doing something custom with a has_many :through relationship. I have a Topic which has one Trend, which has many children (recursive).
ActiveRecord does not like it when I configure this as a standard has_many :searches, through: trend, source: :children relationship. It retrieves topic.trend and topic.searches but won't do topic.searches.create(name: foo).
So I used the above to construct a custom autosave and am achieving the correct result with accepts_nested_attributes_for :searches, allow_destroy: true
def autosave_associated_records_for_searches
searches.each do | s |
if s._destroy
self.trend.children.delete(s)
elsif s.new_record?
self.trend.children << s
else
s.save
end
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

Resources