checking if adding an assocated record was successful - ruby-on-rails

I have a has many through relationship in my rails app where the uniqueness of the association is validated like so:
class Foo < ActiveRecord::Base
...
has_many :foo_bars
has_many :bars, :through => :foo_bars, :uniq => true
validates_associated :foo_bars
...
end
And this is working great, but what I want to do in my controller is find out if when I try to create a new association between existing foo and bar if it was successfull. Something like this:
if #myFoo.bars << #bar
...
end
but that doesn't work because the << method returns an array of all of myFoo's Bars not true/false. I know there must be a correct 'rails way' to do this but I don't know what it is (the only thing I can think of is to check if the number of associated bars changed after the query but that seems really sloppy). Any suggestions?

Try this:
link = my_foo.foo_bars.build(:bar => bar)
if link.save
# success
else
# error
end

Related

Rails use scopes on includes(:children)

models
class User
has_many :pictures
class Picture
belongs_to User
mount_uploader :picture, UserPictureUploader
def self.profile
find_by(is_profile: true)
end
controller
User.includes(:pictures).where(...
view
=user.pictures.profile.picture_url
This is causing the following Problem, that each picture will be queried (again).
if we use user.pictures.where(profile: true).picture_url it won't make any new sql-queries.
question:
How can we use scopes on the already included result?
With a little digging I found this question
What is the equivalent of the has_many 'conditions' option in Rails 4?
It looks like you can put conditions on your has_many relation to essentially be the scoped config you're looking for. It'd be something like:
has_many :old_pictures, :class_name => 'Picture',
:foreign_key => 'picture_id', -> { where('created_at < ?', Time.now - 7.days) }
Something like below will do
class User
has_many :pictures
scope :some_name, -> { includes(:pictures).where(your_query_here) }
end
Also, scopes are used only on a Class(in other words they are class methods), if you want to use it on the instance, then you need define an instance method something like below
class User
has_many :pictures
def some_name
self.includes(:pictures).where(your_query_here)
end
end

Rails callback not firing when building nested STI resource through an association

Take the following for example:
class Foo < AR::Base
has_many :bars, :as => :barable, :dependent=> :destroy
accepts_nested_attributes_for :bars, :allow_destroy => true
end
class Bar < AR::Base
belongs_to :barable, :polymorphic => true
end
class Baz < Bar
before_save do
raise "Hi"
end
end
In the form for 'Foo' - I have fields_for :bars_attributes where a hidden field sets type to 'Baz'. The 'Baz' is succesfully created but the callback never fires. (It does, however, fire when manually creating a 'Baz' in the console.)
Any advice appreciated!
Baz's callbacks will only be triggered if you create it as a Baz object, i.e Baz.new(...).
However, you're not creating a Baz record, but rather a Bar record: Bar.new(type: 'Baz').
This will only trigger Bar's callbacks, even though that later on it will be treated as a Baz.
you need to specify additonal association in your Foo.rb
has_many :bazs
# or
# has_many :bazs class_name: 'ModuleName::Baz' # if you scoped your child classed within some module
If you do that your
before_save do
raise "Hi"
end
will fire on for example #current_user.bazs.build

Accessing rails helper methods in models

In Rails 3, I am having a problem accessing a helper method from within a model
In my ApplicationController I have a helper method called current_account which returns the account associated with the currently logged in user. I have a project model which contains projects that are associated with that account. I have specified 'belongs_to :account' in my project model and 'has_many' in my account model. All of this seems to be working correctly and my projects are being associated with the right accounts.
However at the moment when I use 'Project.all', all of the project records are being returned as you would expect. I would like it to automatically filter the projects so that only those associated with the specified account are returned and I would like this to be the default behaviour
So I tried using 'default_scope'. The line in my model looks something like this
default_scope :order => :name, :conditions => ['account_id = ?', current_account.id]
This throws an error saying
Undefined local variable or method current_account for #
If I swap the call to current_account.id for an integer id - eg
default_scope :order => :name, :conditions => ['account_id = ?', 1]
Everything works correctly. How do I make my current_account method accessible to my model
Many Thanks
You can't access the session from models. Instead, pass the account as a parameter to a named scope.
#controller
Model.my_custom_find current_account
#model.rb
named_scope :my_custom_find, lambda { |account| { :order => :name, :conditions => ['account_id = ?', account.id]}}
I haven't used rails 3 yet so maybe named_scopes have changed.
The association setup is enough to deal with scoping on controller and view levels. I think the problem is to scope, for instance, finds in models.
In controller:
#products = current_account.products.all
Fine for view scoping, but...
In model:
class Inventory < ActiveRecord::Base
belongs_to :account # has fk account_id
has_many :inventory_items, :dependent => :destroy
has_many :products, :through => :inventory_items
end
class Product < ActiveRecord::Base
has_many :inventory_items
def level_in(inventory_id)
inventory_items.where(:inventory_id => inventory_id).size
end
def total_level
# Inventory.where(:account_id => ?????) <<<<<< ISSUE HERE!!!!
Inventory.sum { |inventory| level_in(inventory.id) }
end
end
How can this be scoped?
+1 for mark's answer. This should still work in Rails 3.
Just showing you the rails 3 way with scope and new query api:
scope :my_custom_find, lambda { |account| where(:account_id=>account.id).order(:name) }
You've got the association set up, so couldn't you just use:
#projects = current_account.projects.all
...in your controller?
I've not adjusted the syntax to be Rails 3 style as I'm still learning it myself.

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

Ruby on Rails: has_many through frustrations

I'm having a frustrating problem with a has_many through: namely the fact that the through models are not created until save. Unfortunately, I need to set data on these models prior to saving the parent.
Here's the loose setup:
class Wtf < ActiveRecord::Base
belongs_to :foo
belongs_to :bar
end
class Bar < ActiveRecord::Base
has_many :wtfs
has_many :foos, :through => :wtfs
end
class Foo < ActiveRecord::Base
has_many :wtfs
has_many :bars, :through => :wtfs
def after_initialize
Bar.all.each do |bar|
bars << bar
end
end
end
Everything is fine except that I need to access the "wtf"'s prior to save:
f = Foo.new
=> #
f.bars
=> [list of bars]
empty list here
f.wtfs
=> []
f.save!
=> true
now I get stuff
f.wtfs
=> [list of stuff]
I even went so far as to explicitly create the wtfs doing this:
def after_initialize
Bar.all.each do |bar|
wtfs << Wtf.new( :foo => self, :bar => bar, :data_i_need_to_set => 10)
end
end
This causes the f.wtfs to be populated, but not the bars. When I save and retrieve, I get double the expected wtfs.
Anyone have any ideas?
I think you have the right idea with creating the Wtfs directly. I think it will turn out OK if you just set the bars at the same time:
def after_initialize
Bar.all.each do |bar|
wtfs << Wtf.new(:bar => bar, :data_i_need_to_set => 10) # Rails should auto-assign :foo => self
bars << bar
end
end
Rails should save the records correctly because they are the same collection of objects. The only drag might be that if rails doesn't have the smarts to check if a new Bar record in the bars collection already has a Wtf associated, it might create one anyway. Try it out.
Couldn't you write a before_save handler on Wtf that would set the data you need to set? It would have access to both the foo and bar, if needed.
You could set the method that populates bar to an after_create, like this:
class Foo < ActiveRecord::Base
has_many :wtfs
has_many :bars, :through => :wtfs
after_create :associate_bars
def associate_bars
Bar.all.each do |bar|
bars << bar
end
end
end
This would make the wtfs be already be created when this method is called.

Resources