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
Related
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.
In my Rails 4 app I have the following models:
class Invoice < ActiveRecord::Base
has_many :allocations
has_many :payments, :through => :allocations
end
class Allocation < ActiveRecord::Base
belongs_to :invoice
belongs_to :payment
end
class Payment < ActiveRecord::Base
has_many :allocations, :dependent => :destroy
has_many :invoices, :through => :allocations
after_save :update_invoices
after_destroy :update_invoices # won't work
private
def update_invoices
invoices.each do |invoice|
invoice.save
end
end
end
The problem is that I need to update an invoice when one of its payments gets destroyed.
The update_invoices callback above obviously can't ever get triggered because at the time it gets called the connection with the invoice has already been destroyed.
So how can this be done?
Right now, I am doing this in my PaymentsController:
def destroy
#payment.destroy
current_user.invoices.each do |invoice|
invoice.save
end
...
end
However, this is very expensive of course because it goes through each and every invoice that a user has.
What might be a better alternative to this?
Thanks for any feedback.
One solution would be to grab the invoices before destroying the payment instance. Its add a bit more logic to the Controller however, but this is where the intent of both actions ( destroy payment and update invoices ) originate. It also reduces the iteration to just those invoices affected by the destroyed payment.
def destroy
invoices = #payment.invoices
#payment.destroy
invoices.each do |invoice|
invoice.save
end
...
end
Presumably you are overriding the save method of the Invoice model ( or have a callback on that as well), though I would choose a more explicit method for this intent. For example, removed_payment could be a method to handle this specific scenario and update the appropriate attributes - outstanding_amount and payment_status, etc.
def destroy
invoices = #payment.invoices
#payment.destroy
invoices.map(&:removed_payment)
...
end
The problem is that the associated allocation is also destroyed when destroying the payment. If you move the invoice updating to the Allocation model instead it will work as intended.
class Allocation < ActiveRecord::Base
belongs_to :invoice
belongs_to :payment
after_destroy :update_invoice
def update_invoice
if destroyed?
invoice.save!
end
end
end
Here's a Rails 4.1 test project with tests for this:
https://github.com/infused/update_parent_after_destroy
I have a survey and I would like to add participants to a Participant model whenever a user answers to a question for the first time. The survey is a bit special because it has many functions to answer questions such as Tag words, Multiple choices and Open Question and each function is actually a model that has its own records. Also I only want the Participant to be saved once.
The Participant model is fairly simple:
class Participant < ActiveRecord::Base
belongs_to :survey
attr_accessible :survey_id, :user_id
end
The Survey model is also straightforward:
class Survey < ActiveRecord::Base
...
has_many :participants, :through => :users
has_many :rating_questions, :dependent => :destroy
has_many :open_questions, :dependent => :destroy
has_many :tag_questions, :dependent => :destroy
belongs_to :account
belongs_to :user
accepts_nested_attributes_for :open_questions
accepts_nested_attributes_for :rating_questions
accepts_nested_attributes_for :tag_questions
...
end
Then you have models such as rating_answers that belong to a rating_question, open_answers that belong to open_questions and so on.
So initially I thought for within my model rating_answers I could add after_create callback to add_participant
like this:
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
In this case, I didn't know how to find the survey_id, so I tried using the params but I don't think that is the right way to do it. regardles it returned this error
NameError (undefined local variable or method `current_user' for #<RatingAnswer:0x0000010325ef00>):
app/models/rating_answer.rb:25:in `add_participant'
app/controllers/rating_answers_controller.rb:12:in `create'
Another idea I had was to create instead a module Participants.rb that I could use in each controllers
module Participants
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
and in the controller
class RatingAnswersController < ApplicationController
include Participants
def create
#rating_question = RatingQuestion.find_by_id(params[:rating_question_id])
#rating_answer = RatingAnswer.new(params[:rating_answer])
#survey = Survey.find(params[:survey_id])
if #rating_answer.save
add_participant
respond_to do |format|
format.js
end
end
end
end
And I got a routing error
ActionController::RoutingError (uninitialized constant RatingAnswersController::Participants):
I can understand this error, because I don't have a controller for participants with a create method and its routes resources
I am not sure what is the proper way to add a record to a model from a nested model and what is the cleaner approach.
Ideas are most welcome!
current_user is a helper that's accessible in views/controller alone. You need to pass it as a parameter into the model. Else, it ain't accessible in the models. May be, this should help.
In the end I ended up using the after_create callback but instead of fetching the data from the params, I used the associations. Also if #participant.nil? didn't work for some reason.
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => self.user.id, :survey_id => self.rating_question.survey.id)
unless #participant.any?
#new_participant = Participant.create(:user_id => self.user.id, :survey_id => self.survey.rating_question.id)
end
end
end
The cool thing with associations is if you have deeply nested associations for instead
Survey has_many questions
Question has_many answers
Answer has_many responses
in order to fetch the survey id from within the responses model you can do
self.answer.question.survey.id
very nifty!
I have a project with many items; and it's :dependent => :destroy.
I'm trying to tell rails when calling callbacks (specifically the after_destroy of Item), to run ONLY if the Item is destroyed "alone", but all of the project is NOT being destroyed.
When the whole project is being destroyed, I actually don't need this after_destroy method (of Item) to run at all.
I don't want to do :dependent => :delete since the Item has many other associations connected to it (with :dependent => :destroy).
It works for me only with class variable, but I wish it would had worked with an instance variable:
class Project < ActiveRecord::Base
has_many :items, :dependent => :destroy
before_destroy :destroying_the_project
def destroying_the_project
# this is a class variable, but I wish I could had #destroying_me
# instead of ##destroying_me.
##destroying_me = true
end
def destroying_the_project?
##destroying_me
end
end
class Item < ActiveRecord::Base
belongs_to :project
after_destroy :update_related_statuses
def update_related_statuses
# I with I could had return if project.destroying_the_project?
# but since the callback gets the project from the DB, it's another instance,
# so the instance variable is not relevant here
return if Project::destroying_the_project?
# do a lot of stuff which is IRRELEVANT if the project is being destroyed.
# this doesn't work well since if we destroy the project,
# we may have already destroyed the suites and the entity
suite.delay.suite_update_status
entity.delay.update_last_run
end
end
The other option I can think of is remove the :dependent => :destroy and manually handle the destroy of the items inside the Project after_destroy method, but it seems too ugly as well, especially since Project has many item types with :dependent => :destroy that would have to shift to that method.
Any ideas would be appreciated
I hope that's not the best solution, but at least it works and doesn't introduce any global state via class variables:
class Project < ActiveRecord::Base
has_many :items
before_destroy :destroying_the_project
def destroying_the_project
Rails.logger.info 'Project#destroying_the_project'
items.each &:destroy_without_statuses_update
end
end
class Item < ActiveRecord::Base
belongs_to :project
after_destroy :update_related_statuses,
:unless => :destroy_without_statuses_update?
def update_related_statuses
Rails.logger.info 'Item#update_related_statuses'
end
def destroy_without_statuses_update
#destroy_without_statuses_update = true
destroy
end
def destroy_without_statuses_update?
!!#destroy_without_statuses_update
end
end
If you don't need to use callbacks when deleting the whole project, you could use delete_all instead of destroy:
Rails :dependent => :destroy VS :dependent => :delete_all
(This is not the actual code I'm using, although this sums up the idea of what I want to do)
class Connection < ActiveRecord::Base
belongs_to :connection1, :polymorphic => true
belongs_to :connection2, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :followers, :class_name => 'Connection', :as => :connection1
has_many :followings, :class_name => 'Connection', :as => :connection2
end
My question is that I want to know how I will be able to create a method called "network" such that what is returned isn't an array. Like so,
u = User.first
u.network # this will return a merged version of :followings and :followers
So that I'll still be able to do this:
u.network.find_by_last_name("James")
ETA:
Or hmm, I think my question really boils down to if it is possible to create a method that will merge 2 has_many associations in such a way that I can still call on its find_by methods.
Are you sure that you want a collection of Connections, rather than a collection of Users?
If it's a collection of Connections that you need, it seems like you'll be well served by a class method on Connection (or scope, if you like such things).
connection.rb
class Connection < ActiveRecord::Base
class << self
def associated_with_model_id(model, model_id)
include([:connection1, :connection2]).
where("(connection1_type IS #{model} AND connection1_id IS #{model_id})
OR (connection2_type IS #{model} AND connection2_id IS #{model_id})")
end
end
end
user.rb
class User < ActiveRecord::Base
def network
Connection.associated_with_model_id(self.class.to_s, id)
end
end
Probably not as useful as you'd like, but maybe it'll give you some ideas.