I have form fields where the user enters in:
percents: 50.5%
money: $144.99
dates: Wednesday, Jan 12th, 2010
...
The percent and money type attributes are saved as decimal fields with ActiveRecord, and the dates are datetime or date fields.
It's easy to convert between formats in javascript, and you could theoretically convert them to the activerecord acceptable format onsubmit, but that's not a decent solution.
I would like to do something override the accessors in ActiveRecord so when they are set it converts them from any string to the appropriate format, but that's not the best either.
What I don't want is to have to run them through a separate processor object, which would require something like this in a controller:
def create
# params == {:product => {:price => "$144.99", :date => "Wednesday, Jan 12, 2011", :percent => "12.9%"}}
formatted_params = Product.format_params(params[:product])
# format_params == {:product => {:price => 144.99, :date => Wed, 12 Jan 2011, :percent => 12.90}}
#product = Product.new(format_params)
#product.save
# ...
end
I would like for it to be completely transparent. Where is the hook in ActiveRecord so I can do this the Rails Way?
Update
I am just doing this for now: https://gist.github.com/727494
class Product < ActiveRecord::Base
format :price, :except => /\$/
end
product = Product.new(:price => "$199.99")
product.price #=> #<BigDecimal:10b001ef8,'0.19999E3',18(18)>
You could override the setter or getter.
Overriding the setter:
class Product < ActiveRecord::Base
def price=(price)
self[:price] = price.to_s.gsub(/[^0-9\.]/, '')
end
end
Overriding the getter:
class Product < ActiveRecord::Base
def price
self[:price].to_s.gsub(/[^0-9\.]/, ''))
end
end
The difference is that the latter method still stores anything the user entered, but retrieves it formatted, while the first one, stores the formatted version.
These methods will be used when you call Product.new(...) or update_attributes, etc...
You can use a before validation hook to normalize out your params such as before_validation
class Product < ActiveRecord::Base
before_validation :format_params
.....
def format_params
self.price = price.gsub(/[^0-9\.]/, "")
....
end
Use monetize gem for parsing numbers.
Example
Monetize.parse(val).amount
Related
I have a table column - valid_to, which should represent a date: 30 days from the time the entry was saved into database.
But how can I do such a thing in model?
E.g. in controller I can do such thing this way:
#baby = baby.create(baby_params.
merge( :valid_to => DateTime.current + 30 )
In view I can use hidden field in the form:
<%= f.hidden_field :valid_to => DateTime.current + 30 %>
so is there a way to do such a thing in model? I tried defining self.valid_to before_validation but for no avail: in irb my valid_to column is just nil. To add: I store it as datetime not string.
UPDATE
solution in the end was:
before_validation :set_valid_to, :on => :create
def set_valid_to
self[:valid_to] = 30.days.from_now
end
and lived this thing in module, but that's another story...
The below should work if you only want it done on initial record creation. If you want it updated every time it's saved use before_save instead of before_create.
class Baby < ActiveRecord::Base
before_create :set_valid_to
private
def set_valid_to
self.valid_to = 30.days.from_now
end
end
in irb:
#baby = baby.create
#baby.valid_to = Time.now + 30.days
#baby.save
The question is : "given a company, how should I build the query that returns all events created by all employees during their respective employments periods ?"
Ex:
#company = Company.create
# Welcome John
#john = User.create
#event_a = #john.events.create date: Date.new(2013,6,1)
#event_b = #john.events.create date: Date.new(2014,1,1)
#company.employments.create user:#john, since: Date.new(2013,12,20)
# Welcome Jack
#jack = User.create
#event_c = #jack.events.create date: Date.new(2012,1,1)
#event_d = #jack.events.create date: Date.new(2013,1,1)
#company.employments.create user:#jack,
since: Date.new(2011,12,20), till: Date.new(2012,12,31)
#company.events
=> [#event_b, #event_c]
# #event_a is not returned because it was created prior to John's hiring
# #event_d is not returned because it was created after Jack's departure
I came up with a solution but I would like to know if there are any ways to improve it.
class Event
belongs_to :user
# attributes
# date: datetime
# …
end
class Employment
belongs_to :user
belongs_to :event
# attributes
# since: date
# till: date
# …
end
class Company
has_many :employments
def events
Event.where employments.map do |e|
if e.since && e.till
"(user_id = '#{e.user_id}' AND date BETWEEN '#{e.since}' AND '#{e.till}')"
elsif !e.since.nil?
"(user_id = '#{e.user_id}' AND date > '#{e.since}')"
else
"(user_id = '#{e.user_id}')"
end
end.join(' OR ')
end
end
Do you see any other way to do that ?
Events.joins(user: :employments).where(company_id: id)I believe you could use something like this:
def events
Event.where employments.map do |e|
user=User.find(e.user_id)
since = e.since
till = e.till || Date.parse('3100-12-31') # a day in the distant future if nil
user.events.where('(date is null) or (date > ? and date < ?)', since, till)
end
end
2nd try, after using the appropriate joins to connect Event and Employment model, you can use the final where clause to implement your filter.
The full method, after you've added the right set of joins and filters (thanks), is as follows.
def events
Events.joins(user: :employments).where(company_id: id). # <-This comes from the author, not me.
where('employments.since is null OR
(employments.since < events.date AND employments.till is null) OR
(employments.since < events.date AND employments.till > events.date)')
end
how can I query all events created by all employees during their
employment for a given company
You may wish to use an ActiveRecord Association Extension, which basically appends a new method to your association, allowing you to define filtration or some other specifications:
#app/models/company.rb
Class Company < ActiveRecord::Base
has_many :employments
has_many :events do
def by_date(since, till)
... logic here
end
end
end
This will allow you to call:
#app/controllers/events_controller.rb
Class EventsController < ApplicationController
def action
company = Company.find params[:id]
#events = company.events.by_date("01-01-2014", "04-01-2014")
end
end
If you let me know in the comments, I'll have a go at populating the method for you
I have a model
class Transaction < ActiveRecord::Base
end
I have a transaction_type column which is an integer.
How can I create an enumeration that I could map values to names like:
one_time = 1
monthly = 2
annually = 3
So in the db column, the values would be 1, 2 or 3.
Also, whenever I create a new instance, or save a model and the field wasn't set like:
#transaction = Transaction.new(params)
It should default to 1 (on_time).
I'm not sure how I can do this?
basically the same answer as Amit, slight variation
class TransactionType
TYPES = {
:one_time => 1,
:monthly => 2,
:annually => 3
}
# use to bind to select helpers in UI as needed
def self.options
TYPES.map { |item| [item[0], item[1].to_s.titleize] }
end
def self.default
TYPES[:one_time]
end
end
one way to control the default value
class Transaction < ActiveRecord::Base
before_create :set_default_for_type
def set_default_for_type
type = TransactionType.default unless type.present?
end
end
but - best way is to just apply the defaults on your database column and let ActiveRecord get it from there automatically
NOTE: it might also make sense to just have a TransactionType ActiveRecord object instead of above, depends on your situation, i.e.
# on Transaction with type_id:integer
belongs_to :type, class_name: "TransactionType"
You can map the values by creating a constant either in the same Transaction model or by creating a new module and place it inside that as explained by #KepaniHaole
In Transaction model, you can do it like :
class Transaction < ActiveRecord::Base
TRANSACTION_TYPES = { 'one_time' => 1, 'monthly' => 2, 'monthly' => 3 }
end
You can access these values by accessing the constant as
Transaction::TRANSACTION_TYPES['one_time'] # => 1
Transaction::TRANSACTION_TYPES['monthly'] # => 2
Transaction::TRANSACTION_TYPES['monthly'] # => 3
To add a default value to transaction_type column just create a new migration with :
def up
change_column :transactions, :transaction_type, :default => Transaction::TRANSACTION_TYPES['one_time']
end
With this, every time you create a Transaction object without passing transaction_type, the default value 1 with be stored in it.
Maybe you could try something like this? Ruby doesn't really support c-style enums..
module TransactionType
ONCE = 1
MONTHLY = 2
ANUALLY = 3
end
then you could access their values like so:
#transaction = Transaction.new(TransactionType::ONCE)
My application has a model "Appointments" which have a start and end attribute both which are datetimes. I am trying to set the date and time parts separately from my form so I can use a separate date and time picker. I thought I should be able to do it like this. From what I ahve read rails should combine the two parts and then parse the combined field as a datetime like it usually would
The error I am getting:
2 error(s) on assignment of multiparameter attributes [error on assignment ["2013-09-16", "15:30"] to start (Missing Parameter - start(3)),error on assignment ["2013-09-16", "16:30"] to end (Missing Parameter - end(3))]
These are the request parameters:
{"utf8"=>"✓", "authenticity_token"=>"OtFaIqpHQFnnphmBmDAcannq5Q9GizwqvvwyJffG6Nk=", "appointment"=>{"patient_id"=>"1", "provider_id"=>"1", "start(1s)"=>"2013-09-16", "start(2s)"=>"15:30", "end(1s)"=>"2013-09-16", "end(2s)"=>"16:30", "status"=>"Confirmed"}, "commit"=>"Create Appointment", "action"=>"create", "controller"=>"appointments"}
My Model
class Appointment < ActiveRecord::Base
belongs_to :patient
belongs_to :practice
belongs_to :provider
validates_associated :patient, :practice, :provider
end
And the relevant part of the view: (its a simple form)
<%= f.input :"start(1s)", :as => :string, :input_html => { :class => 'date_time_picker' , :value => Date.parse(params[:start]) }%>
<%= f.input :"start(2s)", :as => :string, :input_html => { :class => 'date_time_picker' , :value => Time.parse(params[:start]).strftime('%R') }%>
<%= f.input :"end(1s)", :as => :string, :input_html => { :class => 'date_time_picker' , :value => Date.parse(params[:end]) }%>
<%= f.input :"end(2s)", :as => :string, :input_html => { :class => 'date_time_picker' , :value => Time.parse(params[:end]).strftime('%R') }%>
UPDATE:
THis is now how my model looks like, Ive been trying to do getter/setter methods but I am stuck because start-dat, start_time etc are nil in the model and the parameters aren't sent through
class Appointment < ActiveRecord::Base
belongs_to :patient
belongs_to :practice
belongs_to :provider
validates_associated :patient, :practice, :provider
before_validation :make_start, :make_end
############ Getter Methods for start/end date/time
def start_time
return start.strftime("%X") if start
end
def end_time
return self.end.strftime("%X") if self.end
end
def start_date
return start.strftime("%x") if start
end
def end_date
return self.end.strftime("%x") if self.end
end
def start_time=(time)
end
def end_time=(time)
end
def start_date=(date)
end
def end_date=(date)
end
def make_start
if defined?(start_date)
self.start = DateTime.parse( self.start_date + " " + self.start_time)
end
end
def make_end
if defined?(end_date)
self.start = DateTime.parse( end_date + " " + end_time)
end
end
end
Are you trying to emulate #date_select ? If yes, see second part of answer.
Date database typecast
If you want to assign a DateTime to database, it has to be a DateTime object. Here you use an array of strings, ["2013-09-16", "15:30"].
You can easily compute a datetime from those strings using regexps :
/(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/ =~ params[ 'start(1s)' ]
/(?<hours>\d+):(?<minutes>\d+)/ =~ params[ 'start(2s)' ]
datetime = DateTime.new( year.to_i, month.to_i, day.to_i, hours.to_i, minutes.to_i )
This will store year, month, day, hours and minutes in local variables and create a new datatime based on it, which you can then assign to your model.
Yet, databases can't store ruby DateTime instances as is, so behind the hood, a conversion is made by rails when saving a date or datetime field to convert it as string. The method used is #to_s(:db), which gives, for example :
DateTime.now.to_s(:db) # => "2013-09-17 09:41:04"
Time.now.to_date.to_s(:db) # => "2013-09-17"
So you could theoretically simply join your strings to have proper date representation, but that wouldn't be a good idea, because :
that's implementation details, nothing say this date format won't change in next rails version
if you try to use the datetime after assigning it and before saving (like, in a before_save), it will be a string and not a datetime
Using active_record datetime helpers
As this would be a pain to do that all the time, rails has helpers to create and use datetime form inputs.
FormBuilder#datetime_select will take only the attribute you want and build all needed inputs :
<%= f.datetime_select :start %>
This will actually create 5 inputs, named respectively "start(1i)" (year), "start(2i)" (month), "start(3i)" (day), "start(4i)" (hours) and "start(5i)" (minutes).
If it feels familiar, it's because it's the exact data we retrieved for building a datetime in first part of this answer. When you assign a hash to a datatime field with those exact keys, it will build a datetime object using their values, like we did in first part.
The problem in your own code is that you've just provided "start(1i)" and "start(2i)". Rails doesn't understand, since you only passed it the year and month, a lot less than what is required to compute a datetime.
See How do ruby on rails multi parameter attributes *really* work (datetime_select)
According to this question, the multiparameter attribute method works for Date but not DateTime objects. In the case of a Date, you would pass year, month and day as separate values, hence the Missing Parameter - start(3), as the expected third parameter is not there.
DateTime, however, requires at least five params for instantiation DateTime.new(2013, 09, 16, 15, 30), so you cannot rely on the automated parsing in your case. You would have to split your params first and in that case, you could easily parse it yourself before saving the object using a before_filter or similar methods.
See the constructor:
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/date/rdoc/DateTime.html#method-c-new
and the multiparam description:
http://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_multiparameter_attributes
I have a model with a virtual attribute for a time interval:
attr_accessible :description, :time_end, :time_start, :duration
belongs_to :timesheet
def duration
if attribute_present?("time_start") and attribute_present?("time_end")
ChronicDuration.output(self.time_end - self.time_start)
else
ChronicDuration.output(0)
end
end
def duration=(d)
self.time_end = self.time_start + d
end
However, when creating a new object, Rails tries to set duration before start, leading to an error. How can I make sure that duration is set after start?
error:
undefined method `+' for nil:NilClass
params:
{"utf8"=>"✓",
"authenticity_token"=>"dg+CysIxZORyV3cwvD+LdWckFdHgecGDFDBNOip+iKo=",
"entry"=>{"time_start"=>"now",
"duration"=>"2h",
"description"=>""},
"commit"=>"Create Entry"}
A few things
Worth reading about: and vs && in ruby - http://devblog.avdi.org/2010/08/02/using-and-and-or-in-ruby/
some alternates to using attribute_present? method
# opposite of blank? - http://api.rubyonrails.org/classes/Object.html#method-i-present-3F
if time_start.present? && time_end.present?
# short hand syntax for present?
if time_start? && time_end?
I don't think your problem is with duration being set before time_start, assuming time_start is a datetime or time database type
try this in rails console
entry = Entry.new
entry.time_start = "now"
# => "now"
entry.time_start
# => nil
you are passing strings into time objects and rails / ruby just sets the value to nil.
If time_end and time_start were strings I still don't think your code would give you the result you want?
def duration=(d)
self.time_end = self.time_start + d
end
# params: time_start = "now"
# params: duration = "2h"
# time_end would be: now2h
if I am wrong about duration= running before time_start is set, an alternative would be something like this using a before_save callback
class Entry < ActiveRecord::Base
before_save :set_time_end
attr_accessor :duration
attr_accessible :description, :time_end, :time_start, :duration
belongs_to :timesheet
def duration
if time_start? && time_end?
ChronicDuration.output(self.time_end - self.time_start)
else
ChronicDuration.output(0)
end
end
def set_time_end
return nil if time_start.blank?
self.time_end = self.time_start + self.duration
end
end
1.) Its not clever to name an attribute end because thats a keyword and it might cause some trouble.
2.) Please post your params hash