Ruby on Rails: has_many through frustrations - ruby-on-rails

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.

Related

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

How to handle removal of entries in a multi select check box are used

This is in continuation to the question which was raised here
How to add one-to-many objects to the parent object using ActiveRecord
class Foo < ActiveRecord::Base
has_many :foo_bars
end
class Bar < ActiveRecord::Base
end
class FooBar < ActiveRecord::Base
belongs_to :foo
belongs_to :bar
end
How to handle removal of entries in a multi select check box are used to represent one-to-many entities. I am able to add or update entries, but removal seems to fail since foo_id seems to be empty and the query seems to be updating instead of delete.
EDIT :
I tried with #charlysisto suggestion using the following code
My Controller code is as follows :
class Foo < ActiveRecord::Base
has_many :foo_bars
has_many :bars, :through => :foo_bars
end
def edit
#foo = Foo.find(params[:id])
#sites = Site.where(company_id: #current_user.company_id).all
end
def update
#foo = Foo.find(params[:id])
if #foo.update_attributes(params[:foo])
flash[:notice] = "Foo was successfully updated"
redirect_to foos_path
else
render :action => 'edit'
end
end
View code is as follows :
<% #bars.each do |bar| %>
<%= check_box_tag 'bar_ids[]', bar.id %>
<%= bar.name %>
<% end %>
So I tried with these changes, but still foo_bars doesn't seem to reflect the changes if I removed a record.
What's missing in your associations is this :
class Foo < ActiveRecord::Base
has_many :bars, :through => :foo_bars
end
... the has_many (or has_many :through) macro gives you a truck load of methods including bar_ids and bar_ids= [...]
So all you need to do in your views/controller is :
# edit.html.haml which will send an array of bar_ids
=f.select :bar_ids, Bar.all.map {|b| [b.name, b.id]}, {}, :multiple => true
# foo_controller
#foo.update_attributes(params[:foo])
And that's it! No need to manually set or delete the associations in FooController#update...

checking if adding an assocated record was successful

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

callbacks on active record associations

I have a vacation approval model that has_many :entries is there a way that if I destroy one of those entries to have the rest destroyed? I also want to send one email if they are, but not one for each entry. Is there a way to observe changes to the collection as a whole?
A callback probably isn't a good choice because:
class Entry < ActiveRecord::Base
def after_destroy
Entry.where(:vacation_id => self.vacation_id).each {|entry| entry.destroy}
end
end
would produce some bad recursion.
It could be that you should do it in the controller:
class EntriesController < ApplicationController
def destroy
#entry = Entry.find(params[:id])
#entries = Entry.where(:vacation_id => #entry.vacation_id).each {|entry| entry.destroy}
#send email here
...
end
end
You can use the before_destroy callback.
class VacationRequest < ActiveRecord::Base
has_many :entries
end
class Entry < ActiveRecord::Base
belongs_to :vacation_request
before_destroy :destroy_others
def destroy_others
self.vacation_request.entries.each do |e|
e.mark_for_destruction unless e.marked_for_destruction?
end
end
end
Definitely test that code before you use it on anything important, but it should give you some direction to get started.
I think this ought to work:
class Entry < ActiveRecord::Base
belongs_to :vacation_request, :dependent => :destroy
# ...
end
class VacationApproval < ActiveRecord::Base
has_many :entries, :dependent => :destroy
# ...
end
What should happen is that when an Entry is destroyed, the associated VacationApproval will be destroyed, and subsequently all of its associated Entries will be destroyed.
Let me know if this works for you.
So What i ended up doing is
class VacationApproval < ActiveRecord::Base
has_many :entries , :conditions => {:job_id => Job.VACATION.id }, :dependent => :nullify
class Entry < ActiveRecord::Base
validates_presence_of :vacation_approval_id ,:if => lambda {|entry| entry.job_id == Job.VACATION.id} , :message => "This Vacation Has Been Canceled. Please Delete These Entries."
and then
#entries.each {|entry| entry.destroy if entry.invalid? }
in the index action of my controller.
and
`raise "Entries are not valid, please check them and try again ( Did you cancel your vacation? )" if #entries.any? &:invalid?`
in the submit action
The problem with deleting the others at the same time is if my UI makes 10 Ajax calls to selete 10 rows, and it deletes all of them the first time I end up with 9 unahandled 404 responses, which was undesirable.
Since I don't care it they remain there, as long as the Entry cannot be submitted its OK.
This was the easiest / safest / recursion friendly way for me, but is probably not the best way. Thanks for all your help!
To anyone curious/ seeking info
I ended up solving this later by setting The Vacation APProval model like this
class VacationApproval < ActiveRecord::Base
has_many :entries , :conditions => {:job_id => Job.VACATION.id }, :dependent => :delete_all
end
and My Entry Model like this
class Entry < ActiveRecord::Base
after_destroy :cancel_vacation_on_destory
def cancel_vacation_on_destory
if !self.vacation_approval.nil?
self.vacation_approval.destroy
end
end
end
Using :delete_all does not process callbacks, it just deletes them

Model Relationship Problem

I am trying to calculate the average (mean) rating for all entries within a category based on the following model associations ...
class Entry < ActiveRecord::Base
acts_as_rateable
belongs_to :category
...
end
class Category < ActiveRecord::Base
has_many :entry
...
end
class Rating < ActiveRecord::Base
belongs_to :rateable, :polymorphic => true
...
end
The rating model is handled by the acts as rateable plugin, so the rateable model looks like this ...
module Rateable #:nodoc:
...
module ClassMethods
def acts_as_rateable
has_many :ratings, :as => :rateable, :dependent => :destroy
...
end
end
...
end
How can I perform the average calculation? Can this be accomplished through the rails model associations or do I have to resort to a SQL query?
The average method is probably what you're looking for. Here's how to use it in your situation:
#category.entries.average('ratings.rating', :joins => :ratings)
Could you use a named_scope or custom method on the model. Either way it would still require some SQL since, if I understand the question, your are calculating a value.
In a traditional database application this would be a view on the data tables.
So in this context you might do something like... (note not tested or sure it is 100% complete)
class Category
has_many :entry do
def avg_rating()
#entries = find :all
#entres.each do |en|
#value += en.rating
end
return #value / entries.count
end
end
Edit - Check out EmFi's revised answer.
I make no promises but try this
class Category
def average_rating
Rating.average :rating,
:conditions => [ "type = ? AND entries.category_id = ?", "Entry", id ],
:join => "JOIN entries ON rateable_id = entries.id"
end
end

Resources