Parsing daterange param as two attributes - Rails 4, Datetimepicker GEM - ruby-on-rails

I have a model Foo with two datetime attributes called start_time and end_time. Foo has a validation logic for each attribute:
Model:
...
validate :dates_logic_validation, on: :save
...
def dates_logic_validation
if start_time < Time.now
errors.add(:start_time, 'Start time must be greater or equal to today\'\s date')
return false
elsif start_time > end_time
errors.add(:end_time, 'End time must be greater than start time')
return false
else
true
end
end
The validation error handling works fine when I try to create / update Foo via the console. Using datetimepicker gem, I am getting both dates in params hash. In my foos#create, I want to be able to parse this daterange param and assign it foo.start_time and foo.end_time. To do so, I added an instance method:
def parse_date_range(daterange) #<= param is passed as "12/02/2015 - 01/14/2016"
start_time = daterange.split("-")[0].strip!
end_time = daterange.split("-")[1].strip!
update_attributes(start_time: start_time, end_time: end_time)
end
then calling it like so:
Controller (#create):
def create
#foo= Foo.new(foo_params)
#foo.parse_date_range(params[:daterange])
if #foo.valid?
#foo.save
redirect_to foos_path(#foo)
else
render :new
end
end
and here is where the music stops. If I log #{start_time} and #{end_time} after I call parse_data_range on #foo, both will be empty. Which suggests that I probably can't call update_attributes on a non-existing object. If that's the case, what would be the best solution?

I think it's a mistake to do a second database query (update_attributes) when instead you can extract the dates from params in the controller like this:
class FoosController < ApplicationController
before_action :extract_start_end_times, only: :create
def create
#foo = Foo.new(foo_params)
if #foo.valid?
#foo.save
redirect_to foos_path(#foo)
else
render :new
end
end
# ...
private
def extract_start_end_times
return unless params[:daterange].present?
daterange = params.delete(:daterange)
start_time, end_time = daterange.split(/\s*-\s*/)
.map {|date| Date.strptime(date, '%m/%d/%Y') }
params.merge!(start_time: start_time, end_time: end_time)
end
end
Note the use of Date.strptime(date, '%m/%d/%Y'). This is necessary because your dates are in an ambiguous format (Ruby has no way to know if "12/02/2015" is Dec. 2 or Feb. 12).
If you're using strong parameters you may have to modify your foo_params method to accommodate the start_time and end_time params.

This should get to work.
update_attributes(start_time: Date.parse(start_time), end_time: Date.parse(end_time))
You can also use
DateTime.parse("12/02/2015")
=> Thu, 12 Feb 2015 00:00:00 +0000

Related

Providing defaults if params aren't present

I'm trying to create a function that will return defaults if date parameters are not set.
If params[:start_time] not present? return DateTime.now and if params[:end_time] not present? return 1.week.from_now
I'd like to keep these two checks in one function but I can't get it working. If there a better way?
# Main search function
def self.search params, location
self
.join.not_booked
.close_to(Venue.close_to(location))
.activity(Activity.get_ids params[:activity])
.start_date(valid_date params[:start_time])
.endg_date(valid_date params[:end_time])
.ordered
end
# Check if date is nil
def self.valid_date date
if date
date.to_datetime
elsif date == params[:start_time]
DateTime.now
elsif date == params[:end_time]
1.week.from_now
end
end
Asked another way:
What's the best way to combine these two functions?
# Check if date is nil
def self.check_start date
date.present? ? date.to_datetime : DateTime.now
end
def self.check_end date
date.present? ? date.to_datetime : 1.week.from_now
end
If it's not a hard requirement to combine those two methods, you can simply and easily have these two different methods for checking the validity of start_time and end_time:
def self.validate_start_date start_date
start_date.present? ? start_date.to_datetime : DateTime.now
end
def self.validate_end_date end_date
end_date.present? ? end_date.to_datetime : 1.week.from_now
end
Then, in your main search function use them accordingly (start_date(validate_start_date params[:start_time]) and end_date(validate_end_date params[:end_time])):
# Main search function
def self.search params, location
self
.join.not_booked
.close_to(Venue.close_to(location))
.activity(Activity.get_ids params[:activity])
.start_date(validate_start_date params[:start_time])
.end_date(validate_end_date params[:end_time])
.ordered
end
Perhaps I'm misunderstanding but why not:
def self.check param
result = 1.week.from_now
if param[:end_time].present?
result = param[:end_time].to_datetime
end
return result
end
Your second "end_time" check will always overwrite any possible result from your "start_time" if we put it into one function.

How to define time intervals in Rails?

There is the following task: I need to store in db (PostgreSQL) some time intervals; for example, 1 PM - 2 PM in order to I can test if some time exists in some interval. How should I do it properly? May be there is some gem for it? Thanks for it.
You can save the columns as #Santosh said and then if you need to check something against a time range you can use this useful class:
Time-of-day range in Ruby?
class TimeRange
private
def coerce(time)
time.is_a? String and return time
return time.strftime("%H:%M")
end
public
def initialize(start,finish)
#start = coerce(start)
#finish = coerce(finish)
end
def include?(time)
time = coerce(time)
#start < #finish and return (#start..#finish).include?(time)
return !(#finish..#start).include?(time)
end
end
which can be used this way:
irb(main):013:0> TimeRange.new("02:00","01:00").include?(Time.mktime(2010,04,01,02,30))
=> true
irb(main):014:0> TimeRange.new("02:00","01:00").include?(Time.mktime(2010,04,01,01,30))
=> false
irb(main):015:0> TimeRange.new("01:00","02:00").include?(Time.mktime(2010,04,01,01,30))
=> true
irb(main):016:0> TimeRange.new("01:00","02:00").include?(Time.mktime(2010,04,01,02,30))
=> false

What is the best way to handle date in a Rails Form Objects

I'm using Form Object as described in 7 Patterns to Refactor Fat ActiveRecord Models #3 and currently I have an issue with storing date.
Here's what I've done:
class MyModelForm
# ...
def initialize(my_model: MyModel.new, params: {})
#my_model, #params = my_model, params
#my_model.date_field_on = Date.from_params #params, "date_field_on" if #params.present?
end
end
Where Date.from_params is implemented like:
class Date
def self.from_params(params, field_name)
begin
year = params["#{ field_name }(1i)"]
month = params["#{ field_name }(2i)"]
day = params["#{ field_name }(3i)"]
Date.civil year.to_i, month.to_i, day.to_i if year && month && day
rescue ArgumentError => e
# catch that because I don't want getting error when date cannot be parsed (invalid)
end
end
end
I cannot just use #my_model.assign_attributes #params.slice(*ACCEPTED_ATTRIBUTES) because my params["date_field_on(<n>i)"] will be skipped and date will not be stored.
Is there a better approach to handle date fields using Form Objects?
As #davidfurber mentioned in comments it works great with Virtus gem.
class MyModelForm
include Virtus
# ...
attribute :date_field_on, Date
def initialize(params: {})
#my_model.assign_attributes params
end
end

Rails: controller won't update model correctly

I apologize in advance, this is going to be a long question.
Short version:
I have a Meeting model that has a date, start_time, and end_time. These are time objects, which of course are a pain for users to input, so I'm using virtual attributes to accept strings which are parsed by Chronic before save.
I have a plain vanilla rails controller that receives these virtual attributes from the form and passes them along to the model. Here is the controller:
def create
#meeting = #member.meetings.build(params[:meeting])
if #meeting.save
redirect_to member_meetings_path(#member), :notice => "Meeting Added"
else
render :new
end
end
def update
#meeting = #member.meetings.find(params[:id])
if #meeting.update_attributes(params[:meeting])
redirect_to member_meetings_path(#member), :notice => "Meeting Updated"
else
render :new
end
end
I've verified that the controller receives the correct parameters from the form, for instance params[:meeting][:date_string] is set as expected.
Problems:
On create, the date gets set correctly, but the times are assigned to the year 2000, set in UTC, and won't display in local time on the front end.
On update, the date won't update. The times update but stay in UTC for 2000-01-01.
Longer Version
What makes this super bizarre to me is I have decent test coverage indicating all of this works at the model layer.
Here is the model:
# DEPENDENCIES
require 'chronic'
class Meeting < ActiveRecord::Base
# MASS ASSIGNMENT PROTECTION
attr_accessible :name, :location, :description, :contact_id, :member_id, :time_zone,
:date, :start_time, :end_time, :date_string, :start_time_string, :end_time_string
# RELATIONSHIPS
belongs_to :member
belongs_to :contact
# CALLBACKS
before_save :parse_time
# Time IO Formatting
attr_writer :date_string, :start_time_string, :end_time_string
# Display time as string, year optional
def date_string(year=true)
if date
str = "%B %e"
str += ", %Y" if year
date.strftime(str).gsub(' ',' ')
else
""
end
end
# Display time as string, AM/PM optional
def start_time_string(meridian=true)
if start_time
str = "%l:%M"
str += " %p" if meridian
start_time.strftime(str).lstrip
else
""
end
end
# Display time as string, AM/PM optional
def end_time_string(meridian=true)
if end_time
str = "%l:%M"
str += " %p" if meridian
end_time.strftime(str).lstrip
else
""
end
end
# Display Date and Time for Front-End
def time
date.year == Date.today.year ? y = false : y = true
start_time.meridian != end_time.meridian ? m = true : m = false
[date_string(y),'; ',start_time_string(m),' - ',end_time_string].join
end
private
# Time Input Processing, called in `before_save`
def parse_time
set_time_zone
self.date ||= #date_string ? Chronic.parse(#date_string).to_date : Date.today
self.start_time = Chronic.parse #start_time_string, :now => self.date
self.end_time = Chronic.parse #end_time_string, :now => self.date
end
def set_time_zone
if time_zone
Time.zone = time_zone
elsif member && member.time_zone
Time.zone = member.time_zone
end
Chronic.time_class = Time.zone
end
end
Here is the spec. Note that to test the parse_time callback in isolation I'm calling #meeting.send(:parse_time) in these tests whenever I'm not actually creating or updating a record.
require "minitest_helper"
describe Meeting do
before do
#meeting = Meeting.new
end
describe "accepting dates in natural language" do
it "should recognize months and days" do
#meeting.date_string = 'December 17'
#meeting.send(:parse_time)
#meeting.date.must_equal Date.new(Time.now.year,12,17)
end
it "should assume a start time is today" do
#meeting.start_time_string = '1pm'
#meeting.send(:parse_time)
#meeting.start_time.must_equal Time.zone.local(Date.today.year,Date.today.month,Date.today.day, 13,0,0)
end
it "should assume an end time is today" do
#meeting.end_time_string = '3:30'
#meeting.send(:parse_time)
#meeting.end_time.must_equal Time.zone.local(Date.today.year,Date.today.month,Date.today.day, 15,30,0)
end
it "should set start time to the given date" do
#meeting.date = Date.new(Time.now.year,12,1)
#meeting.start_time_string = '4:30 pm'
#meeting.send(:parse_time)
#meeting.start_time.must_equal Time.zone.local(Time.now.year,12,1,16,30)
end
it "should set end time to the given date" do
#meeting.date = Date.new(Time.now.year,12,1)
#meeting.end_time_string = '6pm'
#meeting.send(:parse_time)
#meeting.end_time.must_equal Time.zone.local(Time.now.year,12,1,18,0)
end
end
describe "displaying time" do
before do
#meeting.date = Date.new(Date.today.year,12,1)
#meeting.start_time = Time.new(Date.today.year,12,1,16,30)
#meeting.end_time = Time.new(Date.today.year,12,1,18,0)
end
it "should print a friendly time" do
#meeting.time.must_equal "December 1; 4:30 - 6:00 PM"
end
end
describe "displaying if nil" do
it "should handle nil date" do
#meeting.date_string.must_equal ""
end
it "should handle nil start_time" do
#meeting.start_time_string.must_equal ""
end
it "should handle nil end_time" do
#meeting.end_time_string.must_equal ""
end
end
describe "time zones" do
before do
#meeting.assign_attributes(
time_zone: 'Central Time (US & Canada)',
date_string: "December 1, #{Time.now.year}",
start_time_string: "4:30 PM",
end_time_string: "6:00 PM"
)
#meeting.save
end
it "should set meeting start times in the given time zone" do
Time.zone = 'Central Time (US & Canada)'
#meeting.start_time.must_equal Time.zone.local(Time.now.year,12,1,16,30)
end
it "should set the correct UTC offset" do
#meeting.start_time.utc_offset.must_equal -(6*60*60)
end
after do
#meeting.destroy
end
end
describe "updating" do
before do
#m = Meeting.create(
time_zone: 'Central Time (US & Canada)',
date_string: "December 1, #{Time.now.year}",
start_time_string: "4:30 PM",
end_time_string: "6:00 PM"
)
#m.update_attributes start_time_string: '2pm', end_time_string: '3pm'
Time.zone = 'Central Time (US & Canada)'
end
it "should update start time via mass assignment" do
#m.start_time.must_equal Time.zone.local(Time.now.year,12,1,14,00)
end
it "should update end time via mass assignment" do
#m.end_time.must_equal Time.zone.local(Time.now.year,12,1,15,00)
end
after do
#m.destroy
end
end
end
I have even specifically mixed in creating and updating records via mass assignment in later test methods to ensure that those work as expected. All those tests pass.
I appreciate any insight into the following:
Why doesn't the date update in the controller#update action?
Why aren't times getting the year from the date that is set? This works in the model and in specs, but not when submitted via form through the controller.
Why don't times get set to the time zone that is passed in from the form? Again, these specs pass, what is wrong on the controller?
Why won't times display in their time zone on the front end?
Thanks for the help, I feel like I must be losing the forest for the trees on this one as I've been going at it for hours.
Update:
Thanks to the help of AJcodez, I saw some of the issues:
Was assigning date wrong, thanks AJ! Now using:
if #date_string.present?
self.date = Chronic.parse(#date_string).to_date
elsif self.date.nil?
self.date = Date.today
end
I was using Chronic correctly, my mistake was at the database layer! I set the fields in the database to time instead of datetime, which ruins everything. Lesson to anyone reading this: never ever use time as a database field (unless you understand exactly what it does and why you're using it instead of datetime).
Same problem as above, changing the fields to datetime fixed the problem.
The problem here has to do with accessing time in the model vs. the view. If I move these time formatting methods into a helper so they're called in the current request scope they will work correctly.
Thanks AJ! Your suggestions got me past my blind spot.
Well here goes..
1 . Why doesn't the date update in the controller#update action?
I see two potential issues. Looks like you're not parsing the dates again. Try this:
def update
#meeting = #member.meetings.find(params[:id])
#meeting.assign_attributes params[:meeting]
#meeting.send :parse_time
if #meeting.save
...
assign_attributes sets but doesnt save new values: http://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_attributes
Also, in your parse_time method, you use this assignment: self.date ||= which will always set self.date back to itself if it is assigned. In other words you can't update the date unless its falsey.
2 . Why aren't times getting the year from the date that is set? This works in the model and in specs, but not when submitted via form through the controller.
No idea, looks like you are using Chronic#parse correctly.
3 . Why don't times get set to the time zone that is passed in from the form? Again, these specs pass, what is wrong on the controller?
Try debugging time_zone and make sure it is returning whats in params[:meeting][:time_zone]. Again it looks correct by Chronic.
Side note: if you pass an invalid string to Time#zone= it will blow up with an error. For instance Time.zone = 'utc' is all bad.
4 . Why won't times display in their time zone on the front end?
See Time#in_time_zone http://api.rubyonrails.org/classes/Time.html#method-i-in_time_zone and just explicitly name your time zone every time.
Not sure if you're already doing this, but try to explicitly save Times in UTC on the database, and then display them in local time.

Putting Date and 1.month.ago together?

How would I put Date and 1.month.ago together when I have a date attribute called :purchase_date and want to put it inside a class method?
def self.last_month # Show only products of last month.
where(:purchase_date => Date.today.1.month.ago.end_of_month..Date.1.month.ago.beginning_of_month)
end
console gives a syntax error and taking it away Date.today gives me blank results compared to my other method:
def self.this_month # Show only products of this month.
where(:purchase_date => Date.today.beginning_of_month..Date.today.end_of_month)
end
Just 1.month.ago is enough, you don't need to prepend Date.today to 1.month.ago because 1.month.ago starts from today
You have mistake in your Date syntax, you might want to use something like this:
def self.last_month # Show only products of last month.
where(:purchase_date => 1.month.ago.beginning_of_month..1.month.ago.end_of_month)
end
def self.this_month # Show only products of this month.
where(:purchase_date => Date.today.beginning_of_month..Date.today.end_of_month)
end
Maybe:
def self.this_month
where(:purchase_date =>(Date.today - 1.month)..Date.today
end
If the future time needs to be farther out, like in the case of planned subscription orders, remember to use .since
def self.next_quarter # Show only product order in the next 3 months
where(:purchase_date => Date.today.beginning_of_month..3.months.since)
end

Resources