Attribute that indicates is activerecord model in saving state? - ruby-on-rails

I need to know if model is in saving state (between before_validate and after_save).
class ModelA < ActiveRecord::Base
before_save: do_before
def do_before
modelb.create(:attr => 123, :ref => self)
end
end
class ModelB < ActiveRecord::Base
before_create: do_before
def do_before
self.ref.my_attr = 321
self.ref.save! unless self.ref.is_saving?
end
end
I need "is_saving?" attribute for every model instance in my project. What is the best way to implement that?

Can't you rather set an instance variable in between :before_save and :after_save by using ActiveRecord's :around_save callback and then yield your save? Anyway, the question is not too clear to me. What is the purpose of is_saving? method?

Related

How do I invoke a method only when my object (model) is first created in Rails 5?

I'm using Rails 5. I want a method invoked on my model only when the model is first created. I have tried this ...
class UserSubscription < ApplicationRecord
belongs_to :user
belongs_to :scenario
def self.find_active_subscriptions_by_user(user)
UserSubscription.joins(:scenario)
.where(["user_id = ? and start_date < NOW() and end_date > NOW()", user.id])
end
after_initialize do |user_subscription|
self.consumer_key = SecureRandom.urlsafe_base64(10)
self.consumer_secret = SecureRandom.urlsafe_base64(25)
end
end
but I noticed this gets called every tiem I retrieve a model from a finder method in addition to its begin created. How can I create such functionality in my model?
You want to use after_create (from active record docs) or after_create_commit which was introduced in Rails 5 as a shortcut for after_commit :hook, on: :create.
after_create always executes after the transactions block whereas after_create_commit does so after the commit but within the same transactions block. These details likely don't matter here, but it's a new capability if you need that extra control for ensuring the model state is correct before you execute the after call.
Pyrce's answer is good. Another way is to keep the after_initialize method but only run if it's a new record:
after_initialize :set_defaults
def set_defaults
if self.new_record?
self.consumer_key = SecureRandom.urlsafe_base64(10)
self.consumer_secret = SecureRandom.urlsafe_base64(25)
end
end
(It's generally considered better to not override the after_initialize method. Instead provide the name of a method to run, as I did above.

How to get calculated value in hash (attr_accessor) within ActiveRecord query in rails 4?

I have been searching and trying to get this working but i can't find a simple solution.
I have model class called Task and this class has a calculated value that i can get through attr_accessor called overall_rating
class Task < ActiveRecord::Base
attr_accessor :overall_rating
def overall_rating
#returns calculated value
end
end
If i create an instance of ActiveRecord query like
t = Task.first
I can get the "overall_rating" value like this
t.overall_rating #returns calculated value
But there's a way to get the same value but accessing the object as a Hash?
t['id'] # returns ID
t['overall_rating'] #returns nil
Thank you very much for you help.
I would simply define a method that returns the record as a hash. It uses the attributes method from ActiveRecord and manually adds the calculated attribute to the hash:
class Task < ActiveRecord::Base
attr_reader :overall_rating
def overall_rating
# returns calculated value, the following is a simple example of a calculation
id + 100
end
def to_h
attributes.merge("overall_rating" => overall_rating)
end
end
Also note that I changed attr_accessor to attr_reader as you probably don't want to have a setter defined for the calculated value.
Example:
t = Task.find(5)
t.to_h
# => { "id" => 5, "overall_rating" => 105 }
simply remove attr_accessor :overall_rating
and do the following
class Task < ActiveRecord::Base
default_scope { select("*, [calculated_field] as overall_rating") }
end
replace [calculated_field] with the calculation you want
IF overall_rating is a column in the tasks table, you can do something like this:
class Task < ActiveRecord::Base
after_initialize :set_overall_rating
def set_overall_rating
value = #does calculations
self.overall_rating = value
end
end
In case you can't move your calculation to SQL and you have to calculate your overall_rating in Ruby then you can override the [] method. As it calls read_attribute I'd suggest to override this method.
Quick and dirty solution:
class Task < ActiveRecord::Base
def read_attribute(attr_name, &block)
if attr_name.to_s == "overall_rating"
overall_rating
else
super(attr_name, &block)
end
end
end

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

Proper way to do this with ActiveRecord?

Say I have two classes,
Image and Credit
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
end
I want associate a Credit when Image is created, acting a bit like a tag. Essentially, I want behavior like Credit.find_or_create_by_name, but in the client code using Credit, it would be much cleaner if it was just a Create. I can't seem to figure out a way to bake this into the model.
Try this:
class Image < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
after_create { Credit.associate_object(self) }
end
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
def self.associate_object(object, association='images')
credit = self.find_or_create_by_name(object.credit_name)
credit.send(association) << object
credit.save
end
end
Then when you create an image what you can do is something like
Image.create(:attr1 => 'value1', :attr2 => 'value2', ..., :credit_name => 'some_name')
And it will take the name that you feed into the :credit_name value and use it in the after_create callback.
Note that if you decided to have a different object associated with Credit later on (let's say a class called Text), you could do still use this method like so:
class Text < ActiveRecord::Base
belongs_to :credit
attr_accessor :credit_name
before_create { Credit.associate_object(self, 'texts') }
end
Although at that point you probably would want to consider making a SuperClass for all of the classes that belong_to credit, and just having the superclass handle the association. You might also want to look at polymorphic relationships.
This is probably more trouble than it's worth, and is dangerous because it involves overriding the Credit class's initialize method, but I think this might work. My advice to you would be to try the solution I suggested before and ditch those gems or modify them so they can use your method. That being said, here goes nothing:
First you need a way to get at the method caller for the Credit initializer. Let's use a class I found on the web called CallChain, but we'll modify it for our purposes. You would probably want to put this in your lib folder.
class CallChain
require 'active_support'
def self.caller_class
caller_file.split('/').last.chomp('.rb').classify.constantize
end
def self.caller_file(depth=1)
parse_caller(caller(depth+1).first).first
end
private
#Stolen from ActionMailer, where this was used but was not made reusable
def self.parse_caller(at)
if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
file = Regexp.last_match[1]
line = Regexp.last_match[2].to_i
method = Regexp.last_match[3]
[file, line, method]
end
end
end
Now we need to overwrite the Credit classes initializer because when you make a call to Credit.new or Credit.create from another class (in this case your Image class), it is calling the initializer from that class. You also need to ensure that when you make a call to Credit.create or Credit.new that you feed in :caller_class_id => self.id to the attributes argument since we can't get at it from the initializer.
class Credit < ActiveRecord::Base
#has a field called name
has_many :images
attr_accessor :caller_class_id
def initialize(args = {})
super
# only screw around with this stuff if the caller_class_id has been set
if caller_class_id
caller_class = CallChain.caller_class
self.send(caller_class.to_param.tableize) << caller_class.find(caller_class_id)
end
end
end
Now that we have that setup, we can make a simple method in our Image class which will create a new Credit and setup the association properly like so:
class Image < ActiveRecord::Base
belongs_to :credit
accepts_nested_attributes_for :credit
# for building
def build_credit
Credit.new(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
# for creating
# if you wanted to have this happen automatically you could make the method get called by an 'after_create' callback on this class.
def create_credit
Credit.create(:attr1 => 'val1', etc.., :caller_class_id => self.id)
end
end
Again, I really wouldn't recommend this, but I wanted to see if it was possible. Give it a try if you don't mind overriding the initialize method on Credit, I believe it's a solution that fits all your criteria.

Test if Has One Relationship has Changed in Rails

Is it possible to have a 'before_save' callback that detects if a 'has_one' relationship has changed (the relationship not the model at the end of the relationship)? For example, something that would act like this:
#person.picture = #picture
#person.picture_changed? # true
#person.save
#person.picture_changed? # false
Maybe it works with
#person.picture_id_changed?
Try relation.changed?... You may also want to look into observers, depending on what you are trying to accomblish.
Example:
class Model < ActiveRecord::Base
has_one :relation
before_save :check_relation_changed
private def check_relation_changed
do_something if relation.changed?
end
end
Ref: http://ryandaigle.com/articles/2008/3/31/what-s-new-in-edge-rails-dirty-objects

Resources