Rails 6 call action if specific field changed - ruby-on-rails

In my Rails 6 app I've got User model which has first_name and last_name fields. Both of these fields are required to create a new record. Now I want to trigger the IdentityCheck.new(self).call service each time when user updates his first_name or last_name.
Naturally I've tried something like below:
class User < ApplicationRecord
after_update :restart_identity_check, if: :first_name_changed? || :last_name_changed?
private
def restart_identity_check
IdentityCheck.new(self).call
end
end
But I don't know why it won't worked. Is there a way to do this?

That's not the correct syntax for if.
after_update :restart_identity_check, if: Proc.new { |user| user.first_name_changed? || user.last_name_changed? }
You may wish to create an alternate method, like this:
after_update :restart_identity_check, if: :must_restart_identity_check?
# I'd make this private
private
def must_restart_identity_check?
first_name_changed? || last_name_changed?
end
Remember that after_update runs before record is commited to the database. So, if something happens in between, you are going to restart_identity_check even if your User changes is not commited to the database. I'd use after_update_commit just in case.

Related

How to determine changed attributes on after_save callback

I can't catch the changed attributes on after_save callback but I can catch them on after_update callback. I think after_save is just a combination of after_create and after_update. I'd appreciate it if someone give me at least a hint.
class Student < ApplicationRecord
after_save: after_save_callback
def after_save_callback
if username_changed? ### This is always false
# do something
end
end
end
student = Student.create(name: 'John Doe', username: 'abcdef')
student.username = '123456'
student.save
My Rails version is 5.0.7.
You can use saved_change_to_username?
The behavior of attribute_changed? inside of after callbacks will be changing in the next version of Rails. The new return value will reflect the behavior of calling the method after save returned (e.g. the opposite of what it returns now). To maintain the current behavior, use saved_change_to_attribute? instead.

Run a job after 30 minutes when a specific field of object was set

I need to run a job that sends email to the user when a contest's field named published_at will be set. So I have a Contest model and a method that runs a job:
class Contest < ApplicationRecord
after_create :send_contest
private
def send_contest
SendContestJob.set(wait: 30.minutes).perform_later(self)
end
end
But the job will run even if published_at field is blank. Validating the field to be present is not an option because published_at can be set later. So are there any solutions how can I run the job after setting the field? Thanks ahead.
ActiveModel::Dirty might be useful here. With it, you can inspect what fields are about to change/have been changed:
person.name # => "bob"
person.name = 'robert'
person.save
person.previous_changes # => {"name" => ["bob", "robert"]}
So, say, if published_at_changed? returns true, you schedule the job.
Instead of using after_create you can use before_save which is fired both for new and existing records.
The if: and unless: options allow you to specify conditions that need to be met for a callback to be called, you can pass a Proc, Lambda or the name of a method to be called.
class Contest < ApplicationRecord
before_save :send_contest, if: -> { published_at.present? && published_at_changed? }
# or
before_save :send_contest, if: :publishable?
private
def send_contest
SendContestJob.set(wait: 30.minutes).perform_later(self)
end
def publishable?
published_at.present? && published_at_changed?
end
end
As recommended by Sergio Tulentsev you can use ActiveRecord::Dirty to check for changes to the value of the column. Be sure to read the docs carefully though as there are plenty of gotchas.

Cannot access children in after_create method in Rails 5

In my app I have bookings and passengers where one booking has many passengers.
After creating a new booking I want to send a notification to all passengers, so in my Booking model I have:
after_create :send_notification
def send_notification
self.passengers.each do |passenger|
#DO STUFF
end
end
This does not do anything and if I try
puts(self.passengers.count)
it returns 0.
When I user after_save everything works, so I assume that after_create it created the parent but not the children yet.
Problem is that I can't use after_save because this trigger also after updating.
Any ideas?
You can add condition for after_save:
after_save :send_notification, if: :id_changed?
# or :id_previously_changed? I don't remember if in after_save you can access dirty attributes
Change your callback to this:
after_save :send_notification, on: :create
So that your callback will fire after saving instead of creating the object, but will only occur when it's first created.

Update another column if specific column exist on updation

I have a model which have two columns admin_approved and approval_date. Admin update admin_approved by using activeadmin. I want when admin update this column approval_date also update by current_time.
I cant understand how I do this.Which call_back I use.
#app/models/model.rb
class Model < ActiveRecord::Base
before_update 'self.approval_date = Time.now', if: "admin_approved?"
end
This assumes you have admin_approved (bool) and approval_date (datetime) in your table.
The way it works is to use a string to evaluate whether the admin_approved attribute is "true" before update. If it is, it sets the approval_date to the current time.
Use after_save callback inside your model.
It would be something like this:
after_save do
if admin_approved_changed?
self.approval_date = Time.now
save!
end
end
Or change the condition as you like!
You could set the approval_date before your model instance will be saved. So you save a database write process instead of usage of after_save where you save your instance and in the after_save callback you would save it again.
class MyModel < ActiveRecord::Base
before_save :set_approval_date
# ... your model code ...
private
def set_approval_date
if admin_approved_changed?
self.approval_date = Time.now
end
end
end
May be in your controller:
my_instance = MyModel.find(params[:id])
my_instance.admin_approved = true
my_instance.save

how to validate a record in table before saving in ruby on rails

I am new to Ruby on Rails
I have a scenario in which I have a form which has some fields. One of the field values I need to validate against a table which has the data .
I want to restrict the user from saving any data unless the field is validated with the table records.
Initially I added the code in controller to validate that but I have other fields which I need to validate as empty so it did not work .
Also I want the the validation error to be part of other errors.
I tried the below code in the model file
before_create :validate_company_id
def validate_company_id
cp = Company.find_by_company_id(self.company)
if #cp != nil
return
else
self.status ||= "Invalid"
end
end
But its not validating , could you help me how I can validate it .
regards
Surjan
The guys answered correctly, but provided the other way for solution. You could ask yourself: "Why doesn't my code get executed?"
First of all, you have errors in your code - #cp is undefined. Also, I don't know what are you trying to achieve with self.status ||= "Invalid".
You also don't have to use self when you're calling an attribute, but you do have to call it when you're assignig a new attribute value. So self.company is unnecessary, you can just use company.
I've also noticed you have the company_id attribute in your companies table. That's not neccessary, common convention is using just an id instead. If you don't want to alter your table you can set the id field on your model like so:
class Company < ActiveRecord::Base
set_primary_key :company_id
# ... the rest of your model code ...
end
After that you can use Company.find instead of Company.find_by_company_id.
Okay, let's say you have the following code after the fixes:
before_create :validate_company_id
def validate_company_id
cp = Company.find(company)
if cp != nil
return
else
self.status ||= "Invalid"
end
end
First of all I would like to use ternary operator here
before_create :validate_company_id
def validate_company_id
Company.find(company) ? return : self.status ||= "Invalid"
end
Isn't this cleaner? It does the exact same thing.
Now about that self.status of yours. If you would like to invalidate the object in ActiveModel you have to set some values in errors hash. You're in misconception if you think that a model with the status attribute of "Invalid" is invalid. It's still perfectly valid model in Rails.
So how do you invalidate?
You put some values into errors hash. You can also specify a message and the attribute you're validation error refers to.
So let's do it on your model
before_create :validate_company_id
def validate_company_id
Company.find(company) ? return : errors.add(:company,"Invalid Company ID")
end
Now if you try to save your model with invalid company_id it will still pass and get saved to the DB. Why is that?
It's because of the ActiveModels lifecycle. Your method gets called too late.
Here are all the callback methods you can use
Creating
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
Updating
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
Destroying
before_destroy
around_destroy
after_destroy
Notice how your method gets called long after the validation cycle. So you should not use before_create, but after_validation or before_validation callbacks instead.
And here we are with the working validation method of your model.
after_validation :validate_company_id
def validate_company_id
Company.find(company) ? return : errors.add(:company,"Invalid Company ID")
end
instead of using before_create. You can tell the model to use a custom method for validation as follows
validate :validate_company_id
def validate_company_id
cp = Company.find_by_company_id(self.company)
if cp.nil?
errors.add(:company, 'Invalid Company ID')
end
end
Inside you model, you can add custom validations as below:
validate :validate_company_id
def validate_company_id
Your validations
Add error messages can be added as below
errors.add(:company, "is invalid")
end

Resources