I have an ActiveRecord model User where I am overriding the timestamp attributes as follows:
class User < ActiveRecord::Base
def updated_at
<some calculation>
end
def updated_at=
<some calculation>
end
def created_at
<some calculation>
end
def created_at=
<some calculation>
end
end
Everything works fine when I pass in those attributes explicitly on creation. I want to be able to do those calculations even on regular updates and creates.
Eg:
User.create
User.update_attributes(:not_timestamp_attributes => <some value>)
should also update the timestamps with the calculations.
Is there a best practice around this? I have Googled and I couldn't find anything on overriding timestamp attributes.
The best practice would be to let ActiveRecord handle updating those values for you.
But if you still need to do some sort of computation you could try adding some callbacks to before_save and before_create to explicitly do that, something like this:
class User < ActiveRecord::Base
before_save :compute_updated_at
before_create :compute_created_at, :compute_updated_at
def created_at
read_attribute(:created_at)
end
def created_at=(value)
compute_created_at
end
def updated_at
read_attribute(:updated_at)
end
def updated_at=(value)
compute_updated_at
end
private
def compute_updated_at
write_attribute(:updated_at, Time.now + 1.month)
end
def compute_created_at
write_attribute(:created_at, Time.now + 2.month)
end
end
You could use a separate column for your calculated values and one for the system updated values using a before_save action and ActiveRecord::Dirty's "column_changed?" method
before_save :calculate_created_at, if: :created_at_changed?
def calculate_created_at
update_column(:calculated_created_at, created_at - 1.days)
end
Related
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
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
Is there was a way to assign attr_readonly after update?
attr_readonly, on: :update
If not, perhaps a method
#post.update(content: params[:content])
#post.readonly
You could override readonly? in that model like this:
def readonly?
super || created_at != updated_at
end
Rails checks if a record is readonly before it tries to saves an updated record to the database and would raise an ActiveRecord::ReadOnlyRecord exception if the record is marked as readonly. This overridden readonly? method protects a record from being changed twice, by returning always true if the record was changed at least once (indicated by different timestamp on updated_at and created_at).
Furthermore this allows you to check in your view item.readonly? to hide links to the edit page.
you can create a before_update
before_update :forbid_second_update
def forbid_second_update
if created_at != updated_at_was
errors.add :base, "Cannot updated!"
false
end
end
first time update will be successful as created_at and updated_at will be same
second time it will fail
or alternatively if you want to lock some attributes and don't want to fail the update, you can just add for eg.
self.email = self.email_was
this will override the email attribute to its old value
You can add a count into your Model
rails g scaffold sport name
rails g migration add_modified_count_to_sports modified_count:integer
I'm assigning a default value
class AddModifiedCountToSports < ActiveRecord::Migration
def change
add_column :sports, :modified_count, :integer, default: 0
end
end
rake db:migrate
On my Sport model I create a before_update sort of validation
class Sport < ActiveRecord::Base
before_update :validate_update_status
def validate_update_status
unless self.modified_count.eql?(1)
#if the field 'modified_count = 0'
self.modified_count = 1
else
errors.add(:name,'You can only modified your account once')
false
end#end unless
end#def
end#class
You could also implement the same with a State Machine like gem (i.e. assm)
voilĂ !
I have a model Booking with attr_acessor :date and time:
class Booking < ActiveRecord::Base
# ...
before_save :convert_date_and_time
attr_accessor :date, :time
def convert_date_and_time
self.date_and_time = DateTime.parse("#{date.to_s} #{time.to_s}")
end
end
I am trying to define getter methods for date and time:
def date
date_and_time.to_date if self.id.present?
end
def time
date_and_time.to_time if self.id.present?
end
but I think this is not quite the way to do it. I need self.id.present? because when I am trying to create a new record, obviously date_and_time still has no value and the getters will yield errors.
If that is the case, how should the getters look like so that I can handle new records that are not yet saved? Should I leave them like how they are now?
Thanks!
To detect new record you can use new_record?, but in your case you can use try :
date_and_time.try(:to_date)
date_and_time.try(:to_time)
I have the following class:
class Profile < ActiveRecord::Base
serialize :data
end
Profile has a single column data that holds a serialized hash. I would like to define accessors into that hash such that I can execute profile.name instead of profile.data['name']. Is that possible in Rails?
The simple straightforward way:
class Profile < ActiveRecord::Base
serialize :data
def name
self.data['name']
end
def some_other_attribute
self.data['some_other_attribute']
end
end
You can see how that can quickly become cumbersome if you have lots of attributes within the data hash that you want to access.
So here's a more dynamic way to do it and it would work for any such top level attribute you want to access within data:
class Profile < ActiveRecord::Base
serialize :data
def method_missing(attribute, *args, &block)
return super unless self.data.key? attribute
self.data.fetch(attribute)
end
# good practice to extend respond_to? when using method_missing
def respond_to?(attribute, include_private = false)
super || self.data.key?(attribute)
end
end
With the latter approach you can just define method_missing and then call any attribute on #profile that is a key within data. So calling #profile.name would go through method_missing and grab the value from self.data['name']. This will work for whatever keys are present in self.data. Hope that helps.
Further reading:
http://www.trottercashion.com/2011/02/08/rubys-define_method-method_missing-and-instance_eval.html
http://technicalpickles.com/posts/using-method_missing-and-respond_to-to-create-dynamic-methods/
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
def name
data[:name] if data
end
end
I'm going to answer my own question. It looks like ActiveRecord::Store is what I want:
http://api.rubyonrails.org/classes/ActiveRecord/Store.html
So my class would become:
class Profile < ActiveRecord::Base
store :data, accessors: [:name], coder: JSON
end
I'm sure everyone else's solutions work just fine, but this is so clean.
class Profile < ActiveRecord::Base
serialize :data # always a hash or nil
["name", "attr2", "attr3"].each do |method|
define_method(method) do
data[method.to_sym] if data
end
end
end
Ruby is extremely flexible and your model is just a Ruby Class. Define the "accessor" method you want and the output you desire.
class Profile < ActiveRecord::Base
serialize :data
def name
data['name'] if data
end
end
However, that approach is going to lead to a lot of repeated code. Ruby's metaprogramming features can help you solve that problem.
If every profile contains the same data structure you can use define_method
[:name, :age, :location, :email].each do |method|
define_method method do
data[method] if data
end
end
If the profile contains unique information you can use method_missing to attempt to look into the hash.
def method_missing(method, *args, &block)
if data && data.has_key?(method)
data[method]
else
super
end
end