Rails - collection_check_boxes not triggering callbacks on join model - ruby-on-rails

Since the original question didn't hit the mark, here's a re-written version, which better describes the issue.
I have the following models:
class LineItem < ApplicationRecord
has_many :line_item_options, :dependent => :destroy
has_many :options, through: :line_item_options
end
class LineItemOption < ApplicationRecord
belongs_to :option
belongs_to :line_item
has_many :charges, as: :chargeable, dependent: :destroy
after_create :build_charges
def build_charges
surcharges.each do |surcharge|
self.charges.create!(
surcharge_id: surcharge.id,
name: surcharge.name
)
end
end
end
class Charge < ApplicationRecord
belongs_to :chargeable, polymorphic: true
end
LineItemOption is the join model which joins an Option(not shown) to a LineItem. In some cases the LineItemOption will also have a child Charge model.
In my LineItem form I have the following code:
= line_item.collection_check_boxes :option_ids, group.options, :id, :name_and_price do |option|
.checkbox
= option.check_box(class: "check")
= option.label
When a LineItemOption is created using the collection_check_boxes form helper, the after_create callback fires as anticipated. However when a LineItemOption is destroyed using this same form helper no callback is fired. To test this I've used has_many :charges, as: :chargeable, dependent: :destroy as well as a before_destroy callback. In both cases the callbacks work from the rails console, but not the collection_check_boxes form helper.
Looking at the server log I can see that the destroy method is being called on the LineItemOption which happily runs without also running the appropriate callback
LineItemOption Destroy (0.7ms) DELETE FROM "line_item_options" WHERE "line_item_options"."line_item_id" = $1 AND "line_item_options"."option_id" = $2 [["line_item_id", 12], ["option_id", 1]]
(1.2ms) COMMIT
Redirected to http://localhost:3000/orders/6
Im sitting here scratching my head trying to figure out whats going on, and how to address it. Is this common behavior with the collection_check_boxes helper?

It looks like there's a bug or something in rails since a long time ago where the after_destroy callback is not triggered when you delete records from a has_many :through association https://github.com/rails/rails/issues/27099 (and I guess the dependant: :destroy option depends on that callback).
You'll have to implement some hacky solution like, before assigning the new line_items, do something like line_item.line_item_options.destroy_all or line_item.line_item_options.each(&:destroy) to delete the records by hand triggering the propper callbacks and then updating the record so rails can create the new associations without the buggy destoy behaviour.

You must include:
has_many :line_item_options, :dependent => :destroy
accepts_nested_attributes_for :line_item_options, :allow_destroy => true
You must add _destroy in your params:
def line_item_params
params.require(:line_item).permit(line_item_options_attributes: [ ......., :_destroy])
end

Related

How to identify newly added has_many association in rails after_commit

Have below association in author class
has_many :books,
class_name :"Learning::Books",
through: :elearning,
dependent: :destroy
with after_commit as,
after_commit :any_book_added?, on: :update
def any_book_added?
book = books.select { |book| book.previous_changes.key?('id') }
# book's previous_changes is always empty hash even when newly added
end
Unable to find the newly added association with this method. Is this due to class_name?
Rails has a couple methods that might help you, before_add and after_add
Using this, you can define a method to set an instance variable to true
class Author < ApplicationRecord
has_many :books, through: :elearning, after_add: :new_book_added
def any_book_added?
#new_book_added
end
private
def new_book_added
#new_book_added = true
end
end
Then when you add a book to an author, the new_book_added method will be called and you can at any future time ask your Author class if any_book_added?
i.e.
author = Author.last
author.any_book_added?
=> false
author.books = [Book.new]
author.any_book_added?
=> true
As you can see, the callback method new_book_added can accept the book that has been added as well so you can save that information.

How to detect changes in has_many through association?

I have the following models.
class Company < ApplicationRecord
has_many :company_users
has_many :users, :through => :company_users
after_update :do_something
private
def do_something
# check if users of the company have been updated here
end
end
class User < ApplicationRecord
has_many :company_users
has_many :companies, :through => :company_users
end
class CompanyUser < ApplicationRecord
belongs_to :company
belongs_to :user
end
Then I have these for the seeds:
Company.create :name => 'Company 1'
User.create [{:name => 'User1'}, {:name => 'User2'}, {:name => 'User3'}, {:name => 'User4'}]
Let's say I want to update Company 1 users, I will do the following:
Company.first.update :users => [User.first, User.second]
This will run as expected and will create 2 new records on CompanyUser model.
But what if I want to update again? Like running the following:
Company.first.update :users => [User.third, User.fourth]
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
The thing is I have technically "updated" the Company model so how can I detect these changes using after_update method on Company model?
However, updating an attribute works just fine:
Company.first.update :name => 'New Company Name'
How can I make it work on associations too?
So far I have tried the following but no avail:
https://coderwall.com/p/xvpafa/rails-check-if-has_many-changed
Rails: if has_many relationship changed
Detecting changes in a rails has_many :through relationship
How to determine if association changed in ActiveRecord?
Rails 3 has_many changed?
There is a collection callbacks before_add, after_add on has_many relation.
class Project
has_many :developers, after_add: :evaluate_velocity
def evaluate_velocity(developer)
#non persisted developer
...
end
end
For more details: https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
You can use attr_accessor for this and check if it changed.
class Company < ApplicationRecord
attr_accessor :user_ids_attribute
has_many :company_users
has_many :users, through: :company_users
after_initialize :assign_attribute
after_update :check_users
private
def assign_attribute
self.user_ids_attribute = user_ids
end
def check_users
old_value = user_ids_attribute
assign_attribute
puts 'Association was changed' unless old_value == user_ids_attribute
end
end
Now after association changed you will see message in console.
You can change puts to any other method.
I have the feelings you are asking the wrong question, because you can't update your association without destroy current associations. As you said:
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
Knowing that I will advice you to try the following code:
Company.first.users << User.third
In this way you will not override current associations.
If you want to add multiple records once try wrap them by [ ] Or ( ) not really sure which one to use.
You could find documentation here : https://guides.rubyonrails.org/association_basics.html#has-many-association-reference
Hope it will be helpful.
Edit:
Ok I thought it wasn't your real issue.
Maybe 2 solutions:
#1 Observer:
what I do it's an observer on your join table that have the responsability to "ping" your Company model each time a CompanyUser is changed.
gem rails-observers
Inside this observer call a service or whatever you like that will do what you want to do with the values
class CompanyUserObserver < ActiveRecord::Observer
def after_save(company_user)
user = company_user.user
company = company_user.company
...do what you want
end
def before_destroy(company_user)
...do what you want
end
end
You can user multiple callback in according your needs.
#2 Keep records:
It turn out what you need it keep records. Maybe you should considerate use a gem like PaperTrail or Audited to keep track of your changes.
Sorry for the confusion.

after_create doesn't have access to associated records created during before_created callback?

I am running into a weird issue, and reading the callbacks RoR guide didn't provide me an answer.
class User < ActiveRecord::Base
...
has_many :company_users, dependent: :destroy
has_many :companies, through: :company_users
has_many :user_teams, dependent: :destroy
has_many :teams, through: :user_teams
before_create :check_company!
after_create :check_team
def check_company!
return if self.companies.present?
domain = self.email_domain
company = Company.find_using_domain(domain)
if company.present?
assign_company(company)
else
create_and_assign_company(domain)
end
end
def check_team
self.companies.each do |company|
#do stuff
end
end
...
end
The after_create :check_team callback is facing issues because the line
self.companies.each do |company|
Here, self.companies is returning an empty array [] even though the Company and User were created and the User was associated with it. I know I can solve it by making it a before_create callback instead. But I am puzzled!
Why does the after_create callback not have access to self's associations after the commit?
Solution: Please read my comments in the accepted answer to see the cause of the problem and the solution.
inside before_create callbacks, the id of the record is not yet available, because it is before... create... So it is not yet persisting in the database to have an id. This means that the associated company_user record doesn't have a user_id value yet, precisely because the user.id is still nil at that point. However, Rails makes this easy for you to not worry about this "chicken-and-egg" problem, provided that you do it correctly:
I recreated your setup (Company, User, and CompanyUser models), and the following is what should work on your case (tested working):
class User < ApplicationRecord
has_many :company_users, dependent: :destroy
has_many :companies, through: :company_users
before_create :check_company!
after_create :check_team
def check_company!
# use `exists?` instead of `present?` because `exists?` is a lot faster and efficient as it generates with a `LIMIT 1` SQL.
return if companies.exists?
## when assigning an already persisted Company record:
example_company = Company.first
# 1) WORKS
companies << example_company
# 2) WORKS
company_users.build(company: example_company)
## when assigning and creating a new Company record:
# 1) WORKS (this company record will be automatically saved/created after this user record is saved in the DB)
companies.build(name: 'ahaasdfwer') # or... self.companies.new(name: 'ahaasdfwer')
# 2) DOES NOT WORK, because you'll receive an error `ActiveRecord::RecordNotSaved: You cannot call create unless the parent is saved`
companies.create(name: 'ahaasdfwer')
end
def check_team
puts companies.count
# => 1 if "worked"
puts companies.first.persisted?
# => true if "worked"
end
end

Model callbacks not working with self referential association

I am having a model Evaluation that has many sub evaluations (self refential)
class Evaluation < ApplicationRecord
has_many :sub_evaluations, class_name: "Evaluation", foreign_key: "parent_id", dependent: :destroy
before_save :calculate_score
def calculate_score
# do something
end
end
I am creating and updating evaluation with sub evaluations as nested attributes.
calculate_score method is triggered on sub evaluation creation but not while updating. I have tried before_update and after_validation. But nothing seems to be working.
Evaluation form
= form_for #evaluation do |f|
...
= f.fields_for :sub_evaluations do |sub_evaluation|
...
What seems to be the issue?
This article helped me to fix the issue.
Child callback isn't triggered because the parent isn't "dirty".
The solution in the article is to "force" it to be dirty by calling attr_name_will_change! on a parent attribute that, in fact, does not change.
Here is the updated model code:
class Evaluation < ApplicationRecord
has_many :sub_evaluations, class_name: "Evaluation", foreign_key: "parent_id", dependent: :destroy
before_save :calculate_score
def calculate_score
# do something
end
def exam_id= val
exam_id_will_change!
#exam_id = val
end
end
See Active Model Dirty in the Rails API

Accessing singular_association_ids from model in Rails

I've been using the association_collection method "other_ids" throughout my Rails app with no issues. However whenever I try to access it from within the model defining the association, Rails has no idea what I'm taking about. For example:
class Membership < ActiveRecord::Base
belongs_to :course, :touch => true
belongs_to :person, :touch => true
end
class Day < ActiveRecord::Base
belongs_to :course, :touch => true, :counter_cache => true
has_many :presents, :dependent => :delete_all
has_many :people, :through => :presents
before_destroy :clear_attendance
def clear_attendance
mems = Membership.where(:course_id => course.id, :person_id => person_ids)
mems.update_all(["attendance = attendance - ?", (1 / course.days.size.to_f)])
end
end
In this case, person_ids is always null. I've tried self.person_ids, people.ids, etc. All nothing. I have used day.person_ids elsewhere with no issues, so why can't I use it here?
I am using Ruby 1.9.1 and Rails 3.0.3. Here is the SQL call from my log:
[1m[36mAREL (0.0ms)[0m [1mUPDATE "memberships" SET attendance = attendance - 0.3333333333333333 WHERE ("memberships"."course_id" = 4) AND ("memberships"."person_id" IN (NULL))[0m
edit: added more code to clarify question
What you really want there is:
def a_method
self.people.all
end
But to answer your question, person_ids is the correct method, and it should return an empty array, not nil. I just tried an association like that out in 2.3.10. Maybe you can post some more of your code, rails version, etc.
Thanks for your help - I figured it out myself. The problem was the order of my callbacks. I was trying to call person_ids after the association had been deleted. Changing the order to this solved my issues.
class Day < ActiveRecord::Base
before_destroy :clear_attendance
belongs_to :course, :touch => true, :counter_cache => true
has_many :presents, :dependent => :delete_all
has_many :people, :through => :presents

Resources