4

I have been looking everywhere but can't seem to find a good solution for this.

My form has a date (textfield with datepicker) and a time (textfield with timepicker), which I want to map to an model field called due_at.

So far I been handling it in my controller with separate parameters to join it up to a datetime then set the model field manually, but it's messy and really think this logic should be kept in model/view.

I would like to be able to handle the two form fields to an attribute in the model, then split it back out for errors, edit action etc. Basically a custom way of performing what the standard datetime_select does, but putting my own touch to it.

Is there something that I can put in my model like ?

def due_at=(date, time)
...
end

I been looking a number of places, but can't find out how you would do this. People say to use javascript to populate a hidden field, but just don't seem like the cleanest solution for a pretty simple problem.

Any advice/help would be much appreciated.

Thanks.

Andrew Cetinic
  • 2,805
  • 29
  • 44

2 Answers2

8

First: please rename your field because created_at may cause conflicts with ActiveRecord.

I did exactly this for a field with the format M/D/YYYY H:M (Hours/Minutes in 24hrs format)

In your model:

attr_accessor :due_date, :due_time

before_validation :make_due_at


def make_due_at
  if @due_date.present? && @due_time.present?
    self.due_at = DateTime.new(@due_date.year, @due_date.month, @due_date.day, @due_time.hour, @due_time.min)
  end
end


def due_date
  return @due_date if @due_date.present?
  return @due_at if @due_at.present?
  return Date.today
end

def due_time
  return @due_time if @due_time.present?
  return @due_at if @due_at.present?
  return Time.now
end 


def due_date=(new_date)
  @due_date = self.string_to_datetime(new_date, I18n.t('date.formats.default'))
end


def due_time=(new_time)
  @due_time = self.string_to_datetime(new_time, I18n.t('time.formats.time'))
end

protected

def string_to_datetime(value, format)
  return value unless value.is_a?(String)

  begin
    DateTime.strptime(value, format)
  rescue ArgumentError
    nil
  end
end

now in the view:

<%= text_field_tag :due_time, I18n.l(@mymodel.due_time, :format => :time) %>
<%= text_field_tag :due_date, I18n.l(@mymodel.due_date, :format => :default) %>

now in the config/locales/en.yml (if english)

  date:
    formats:
      default: "%m/%d/%Y"
  time:
    formats:
      time: "%H:%M"

You may change the date format of course.

sled
  • 14,525
  • 3
  • 42
  • 70
  • That works great for when you have one textfield with the datetime in it, but I need a solution for when you have two textfields, one with date other with time. – Andrew Cetinic Oct 11 '11 at 11:50
  • add two variables that are not db backed, make sure they come in the params and build the "real value" and assign to the "real variable"? This is code that you have to add to your model probably rather than kickass Rails magic, but it ought to do the trick... – jaydel Oct 11 '11 at 12:30
  • Thanks jaydel, I pretty sure I know what you mean by using virtual attribute. I was trying to achieve that in my example, could you provide an example ? – Andrew Cetinic Oct 11 '11 at 14:23
  • @Andrew Cetinick I extended my answer I hope it will fit now – sled Oct 11 '11 at 15:11
  • Sled, thanks for that and modifying your answer. In order to store the due_at as a datetime in the DB tho, should I put in the before_validation callback to combine the date and time and set the due_at ? – Andrew Cetinic Oct 11 '11 at 21:51
  • that's what `before_validation :make_due_at` does :) – sled Oct 12 '11 at 00:47
1

I think date_time_attribute gem is a good option for you:

class Task < ActiveRecord::Base
  include DateTimeAttribute

  date_time_attribute :due_at
end

It will allow you to set due_at_date and due_at_time separately:

form_for @event do |f|
  f.text_field :due_at_date
  f.text_field :due_at_time
  f.submit
end

# this will work too:
form_for @event do |f|
  f.date_select :due_at_date
  f.time_select :due_at_time, :ignore_date => true
  f.text_field  :due_at_time_zone
  f.submit
end

# or
form_for @event do |f|
  f.date_select :due_at_date
  f.text_field  :due_at_time
  f.submit
end

Here is an example:

task = Task.new
task.due_at                      # => nil

# Date set
task.due_at_date = '2001-02-03'
task.due_at_date                 # => 2001-02-03 00:00:00 +0700
task.due_at_time                 # => 2001-02-03 00:00:00 +0700
task.due_at                      # => 2001-02-03 00:00:00 +0700

# Time set
task.due_at_time = '10:00pm'
task.due_at_time                 # => 2013-12-02 22:00:00 +0800
task.due_at_date                 # => 2001-02-03 00:00:00 +0700
task.due_at                      # => 2001-02-03 22:00:00 +0700

# Time zone is applied as set
task.due_at_time_zone = 'Moscow'
task.due_at                      # => Mon, 03 Feb 2013 22:00:00 MSK +04:00
task.due_at_time_zone = 'London'
task.due_at                      # => Mon, 03 Feb 2013 22:00:00 GMT +00:00

It will also allow you to play with time zones, use Chronic etc.

Sergei Zinin
  • 161
  • 1
  • 3