rails 4 attr_accessor and new records - ruby-on-rails

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)

Related

Override timestamp attributes on an ActiveRecord model

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

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

Setting Time Zone Per Record

Any idea how I can use the time zone generated from a location to create a new record with that respective zipcode?
I have created a service object to help pull the zipcode information.
I am able to use this info in the terminal to set the zip code but it doesn't work when I try a before_save or before_create hook.
class ServiceObject
include ActiveModel::Model
def self.get_timezone_name(location)
z = GoogleTimeZone.fetch(location.latitude, location.longitude)
ActiveSupport::TimeZone.find_tzinfo(z.time_zone_id)
end
end
class Location < ActiveRecord::Base
has_many :events
#name - String
#start_time - DateTime
#end_time - DateTime
end
class Event < ActiveRecord::Base
belongs_to :location
geocoded_by :full_address
before_create :zoned
private
def zoned
Time.zone = ServiceObject.get_time_zone(self.location)
end
end
I also tried to use the date time attribute gem to set an event's time zone. Again this works in the console but not with a call back. Records are not created via a browser but rather in the console.
Here is a blog post I wrote on timezones with rails: http://jessehouse.com/blog/2013/11/15/working-with-timezones-and-ruby-on-rails/
There are two different ways to accomplish what you want:
Time.use_zone block when saving data
use in_time_zone when displaying data
I recommend you save the timezone on your location, update it if the long/lat changes; it looks like your Event and Location example above is flipped? Event should have a start and end, not location?
class Location
has_many :events
geocoded_by :full_address
before_save :set_time_zone
private
def set_time_zone
if new_record? || latitude_changed? || longitude_changed?
self.time_zone = ServiceObject.get_time_zone(self)
end
end
end
class Event
belongs_to :location
end
then in the console or controller code
location = Location.last
Time.use_zone(location.time_zone) do
location.events << Event.new({ ... })
end

How do I conditionally validate a 1-1 model relationship?

I have a model, Person, that requires a schedule if it's type is "scheduled". In it's controller (which inherits from InheritedResources::Base):
def create
super do
#person.schedule = Schedule.create params[:schedule] if #person.scheduled?
end
end
The thing is, I want to validate that all People of type "scheduled" have a schedule. Something like this:
validates :schedule, :presence => true, :if => :scheduled?
in the Person model. But because a schedule belongs_to a Person, it needs the person to be created prior to being created itself (so the person will have an ID). So with this in my controller, the person validation fails, since the schedule needs to be created later.
Is there something in Rails that I don't know about, which will enable me to perform these validations? If I used accepts_nested_attributes_for, will that allow these validations to pass?
Maybe just don't create it beforehand?
#person.schedule = Schedule.new params[:schedule] if #person.scheduled?
So #person and assigned Schedule shall be saved at the same time (transaction).
I think this is the only correct way.
UPDATED (due to super :create conception):
super action
def create(&block)
...
yield #person if block_given?
#person.save # line where #person get saved
end
inherited action
def create
super do |person|
person.schedule = Schedule.new params[:schedule] if person.scheduled?
end
end

How do I initialize attributes when I instantiate objects in Rails?

Clients have many Invoices. Invoices have a number attribute that I want to initialize by incrementing the client's previous invoice number.
For example:
#client = Client.find(1)
#client.last_invoice_number
> 14
#invoice = #client.invoices.build
#invoice.number
> 15
I want to get this functionality into my Invoice model, but I'm not sure how to. Here's what I'm imagining the code to be like:
class Invoice < ActiveRecord::Base
...
def initialize(attributes = {})
client = Client.find(attributes[:client_id])
attributes[:number] = client.last_invoice_number + 1
client.update_attributes(:last_invoice_number => client.last_invoice_number + 1)
end
end
However, attributes[:client_id] isn't set when I call #client.invoices.build.
How and when is the invoice's client_id initialized, and when can I use it to initialize the invoice's number? Can I get this logic into the model, or will I have to put it in the controller?
Generate a migration that adds invoices_number column to users table. Then in Invoice model write this:
class Invoice < ActiveRecord::Base
belongs_to :user, :counter_cache => true
...
end
This will automatically increase invoices_count attribute for user once the invoice is created.
how about this:
class Invoice < ActiveRecord::Base
...
def initialize(attributes = {})
super
self.number = self.client.invoices.size + 1 unless self.client.nil?
end
end
Here is some useful discussion on after_initialize per Jonathan R. Wallace's comment above:
http://blog.dalethatcher.com/2008/03/rails-dont-override-initialize-on.html
first of all, you don't need to use the attributes collection, you can just do self.client_id. Better yet, as long as you have a belongs_to :client in your Invoice, you could just do self.client.last_invoice_number. Lastly, you almost always want to raise an exception if an update or create fails, so get used to using update_attributes!, which is a better default choice. (if you have any questions about those points, ask, and I'll go into more detail)
Now that that is out of the way, you ran into a bit of a gotcha with ActiveRecord, initializer methods are almost never the right choice. AR gives you a bunch of methods to hook into whatever point of the lifecycle you need to. These are
after_create
after_destroy
after_save
after_update
after_validation
after_validation_on_create
after_validation_on_update
before_create
before_destroy
before_save
before_update
before_validation
before_validation_on_create
before_validation_on_update
What you probably want is to hook into before_create. Something like this
def before_create
self.number ||= self.client.last_invoice_number + 1 unless self.client
end
What that will do is it will hit up the database for your client, get the last invoice number, increment it by one, and set it as its new number, but only if you haven't already set a number (||= will assign, but only if the left side is nil), and only if you have set a client (or client_id) before the save.

Resources