The date_validator in its examples has a comment:
Using Proc.new prevents production cache issues
Does it mean, that everywhere in my code, where I use current time related methods (Time.now, 1.day.since(Time.zone.now), etc.) I should surround them with Proc.new { }?
I don't completely understand this, since replacing
time_now = Time.now.utc
with
time_now = Proc.new { Time.now.utc }
just doesn't make sense to me (new type of object is returned).
So, the question is, when and how should I use Proc.new with time related methods? And does that still apply to the latest versions of Ruby (1.92) and Rails (3.1)?
No, it only references the given example:
validates :expiration_date,
:date => {:after => Proc.new { Time.now },
:before => Proc.new { Time.now + 1.year } }
If instead you'd written
validates :expiration_date,
:date => {:after => Time.now,
:before => Time.now + 1.year }
Time.now would be interpreted when the class is parsed and it would be validating against that value.
Using Proc.new in that validation means Time.new will be evaluated when the validation is actually run - not when it's initially being interpreted.
What Proc.new (and lambda) does is, save all your statements in their original form (in an anonymous function), and doesn't evaluate them.
Date Validator gem must have some kind of test to check if a Proc was passed, and it evaluates it when it's actually validating the stuff.
Edit: It does this here - https://github.com/codegram/date_validator/blob/master/lib/active_model/validations/date_validator.rb#L47
option_value = option_value.call(record) if option_value.is_a?(Proc)
A quick example :
pry(main)> time_now = Time.now
=> 2011-06-19 21:07:07 +0530
pry(main)> time_proc = Proc.new { Time.now }
=> #<Proc:0x9710cc4#(pry):1>
pry(main)> time_proc.call
=> 2011-06-19 21:07:28 +0530
pry(main)> time_proc.call
=> 2011-06-19 21:07:31 +0530
pry(main)>
Note that this will only work with libraries that do implement this kind of check, and not every function accepting a Time.
Related
I am using the timeliness gem to do time and date validation.
My event model has a time field storing a string like "11:15", a start_date and end_date storing a Date object.
I want the following validation behaviour:
If a user tries to create an event on the current day of the year (so start_date == Date.today), and the time is in the past (so if it was 15:30 when the event was created and the user entered 11:30), then the validation should fail. I.e we only want events created where the date is today or is in the future, and the time, if the date is today, is in the future.
I am trying the following validation for this:
validates :time, :presence => true,
:timeliness => { :type => :time, :on_or_after => lambda { if(:start_date == Date.today && :day_of_week.downcase == Date.today.strftime("%A").downcase) then return Time.new(:time.to_s).in_time_zone(:timezone)>Time.now.in_time_zone(:timezone) else return true end } }
However it is not behaving correctly. I.e I am able to create an event with the start_date of 03/08/2015 and a time of 09:00 despite it being 15:31 on 03/08/2015! Please help!
Your lambda doesn't make a lot of sense, because you're using symbols everywhere - :start_date == Date.today will always be false. The value of lambdas in validations is that they're passed your Event instance, so you can reference event.start_date. I agree with #arthur.karganyan in the comments that if you need something this complicated, it'll be easier to work with as a method.
But, you're also making this much more complicated than you need to. It looks like you have start_date and time as separate attributes, which is difficult to work with because they'll tend to rely on each other. I'd recommend making those a single start_time attribute instead, and then you can use validates_timeliness like so:
validates_datetime :start_time, on_or_after: -> { Time.now }
If you must have separate attributes, your validations might look like:
validates_date :start_date, on_or_after: -> { Date.today }
validates_time :time, on_or_after: lambda { |event| Time.now if event.start_date.today? }
This validates that the date is today or in the future, and if it's today, also checks the time. validates_timeliness appears to accept any time when on_or_after lambda evaluates to nil.
I'm trying to validate a date of birth field that has to be in a certain range:
validates :year_of_birth, :inclusion => { :in => 1900..Date.today.year - 5 }
Although for this case it wouldn't pose such a big problem, I realized that this is only valid in development where models are reloaded every time and thus the current year is calculated again.
When in production, how would I avoid that, say, at the turn of the year, the right end of the range remains the same as it was the year before?
in cases like these, I suggest you go for a custom validation.
validate :validates_year_of_birth
def validates_year_of_birth
five_years_ago = 5.years.ago.year
if year_or_birth && (year_of_birth < 1990 || year_of_birth > five_years_ago)
errors.add :year_of_birth, "should be between 1990 and #{five_years_ago}"
end
end
or you can pass the validation in a proc
validates :year_of_birth, :inclusion => { :in => proc { 1900..5.years.ago.year } }
Here's some of my production code (I had to force line breaks):
task = Task.find_or_create_by_username_and_timestamp_and_des \
cription_and_driver_spec_and_driver_spec_origin(username,tim \
estamp,description,driver_spec,driver_spec_origin)
Yes, I'm trying to find or create a unique ActiveRecord::Base object. But in current form it's very ugly. Instead, I'd like to use something like this:
task = Task.SOME_METHOD :username => username, :timestamp => timestamp ...
I know about find_by_something key=>value, but it's not an option here. I need all values to be unique. Is there a method that'll do the same as find_or_create_by, but take a hash as an input? Or something else with similat semantics?
Rails 3.2 first introduced first_or_create to ActiveRecord. Not only does it have the requested functionality, but it also fits in the rest of the ActiveRecord relations:
Task.where(attributes).first_or_create
In Rails 3.0 and 3.1:
Task.where(attributes).first || Task.create(attributes)
In Rails 2.1 - 2.3:
Task.first(:conditions => attributes) || Task.create(attributes)
In the older versions, you could always write a method called find_or_create to encapsulate this if you'd like. Definitely done it myself in the past:
class Task
def self.find_or_create(attributes)
# add one of the implementations above
end
end
I also extend the #wuputah's method to take in an array of hashes, which is very useful when used inside db/seeds.rb
class ActiveRecord::Base
def self.find_or_create(attributes)
if attributes.is_a?(Array)
attributes.each do |attr|
self.find_or_create(attr)
end
else
self.first(:conditions => attributes) || self.create(attributes)
end
end
end
# Example
Country.find_or_create({:name => 'Aland Islands', :iso_code => 'AX'})
# take array of hashes
Country.find_or_create([
{:name => 'Aland Islands', :iso_code => 'AX'},
{:name => 'Albania', :iso_code => 'AL'},
{:name => 'Algeria', :iso_code => 'DZ'}
])
Here is the setup:
end_date = DateTime.parse('2010-01-01 12:00:00-04')
and sometimes it's initialized:
end_date = 1.days.ago
The question... do these named_scope(s) generate the same EXACT SQL?
named_scope :before, lambda { |end_date|
{ :conditions =>
["overdraft_transaction_payments.created_at < ?", end_date] }
}
and
named_scope :before, lambda { |end_date|
{ :conditions =>
["overdraft_transaction_payments.created_at < ?", end_date.utc] }
}
In the first example I use end_date and in the second I use end_date.utc.
(It may be important to note.... The DB server's OS is set to CDT, and the DB uses UTC internally. The rails server's OS is set to CDT and the application instance is set to EDT. I realize that this is probably not the optimum way to configure these systems, however, the issue at hand is the ActiveRecord output.)
For what it is worth my intuition says that the first example is going to generate a time string for the local TZ where the second is going to generate a UTC-0.
PS: Is there a mini test case I can use to validate my intuition?
I believe under the hood the date is going to be converted with a call to ".to_s(:db)" and you can see in an IRB console session what that's going to return:
>> DateTime.parse('2010-01-01 12:00:00-04').to_s(:db)
=> "2010-01-01 12:00:00"
>> DateTime.parse('2010-01-01 12:00:00-04').utc.to_s(:db)
=> "2010-01-01 16:00:00"
>> 1.days.ago.to_s(:db)
=> "2010-08-12 18:01:09"
>> 1.days.ago.utc.to_s(:db)
=> "2010-08-12 18:01:13"
I have a named_scope in my model and want a different condition, based on the locale. On the development this code works perfect, after switching to production the named_scope doesn't work and I get every time the first condition ("de"). I think it's a caching problem?
I'm working with 2.3.5. and env settings out of the box.
named_scope :public,
:conditions => I18n.locale == "de" || I18n.locale == :de ? ["published=? AND trash=?", true, false] : ["published_#{I18n.locale} =? AND trash=?", true, false]
The value for the conditions option is evaluated during class loading. In the development mode class is loaded for every request. Hence your code works in the development mode.
Create the named scope using a parametrized lambda:
named_scope :published, lambda { |*args| {
name = "_de" if (args.first||"").to_s == 'de'
:conditions => ["published#{name} =? AND trash=?", true, false]
}
}
You can the lambda as follows:
Post.published # no locale passed
Post.published(locale) # with locale passed
Yes, it's a "problem" with the class caching of rails. In production the class is loaded and then cached, since the condition is evaluated in class context this should be the value of I18n.locale when the class was loaded.
To solve the problem you could simply turn it off by setting
config.cache_classes = false
in your 'config/enviroments/production.yml', but this slows down response time.
A better solution would be to use a lambda and pass in the locale
named_scope :public, lambda {|l|
if l == :de or l == 'de'
{:conditions => ["published=? AND trash=?", true, false]}
else
{:conditions => ["published_#{l} =? AND trash=?", true, false]}
end
}
Then you can use it like this:
Article.public(I18n.locale)