Search code examples
ruby-on-railsdatetimerails-postgresqlransack

Filtering datetime not working when using timezone


In my setup I want to filter Schedule by Activity, City, Date, Starting Time and End Time using select menus.

I've made it work with Ransack:

# db
create_table "schedules", force: :cascade do |t|
  t.integer  "city_id"
  t.integer  "activity_id"
  t.datetime "starts_at"
  t.datetime "ends_at"
  ...
end

# Schedule model
scope :six_hours_from_now, -> { where('starts_at >= ?', Time.zone.now + 6.hours) }

ransacker :start_date, type: :date do
  Arel.sql('starts_at::date')   # filter only the date from starts_at attribute
end

ransacker :start_time, type: :time do
  Arel.sql('starts_at::time')   # filter only the time from starts_at attribute
end

ransacker :end_time, type: :time do
  Arel.sql('ends_at::time')     # filter only the time from ends_at attribute
end

# Schedule controller
def index
  @q          = Schedule.ransack(params[:q])
  @q.sorts    = ['starts_at asc', 'ends_at asc'] if @q.sorts.empty?
  @schedules  = @q.result.six_hours_from_now
end

# view
<%= search_form_for @q do |f| %>
  <%= f.collection_select :activity_id_eq, @activities, :id, :name, { include_blank: 'All' } %>
  <%= f.collection_select :city_id_eq, @cities, :id, :name, { include_blank: 'All' } %>
  <%= f.select :start_date_eq, options_for_select([
        ['Today', Time.zone.today],
        ['Tomorrow', Time.zone.tomorrow],
        [(Time.zone.today + 2).strftime("%d %b"), Time.zone.today + 2],
        [(Time.zone.today + 3).strftime("%d %b"), Time.zone.today + 3],
        [(Time.zone.today + 4).strftime("%d %b"), Time.zone.today + 4],
        [(Time.zone.today + 5).strftime("%d %b"), Time.zone.today + 5],
        [(Time.zone.today + 6).strftime("%d %b"), Time.zone.today + 6]
      ], @q.start_date_eq), { include_blank: true } %>
  <%= f.time_select :start_time_gteq, ampm: true, default: { hour: '07' } %>
  <%= f.time_select :end_time_lteq, ampm: true, default: { hour: '23', minute: '45' } %>

  <%= f.submit "Filter" %>
<% end %>

Unfortunately the filters don't work anymore when I put in some code to handle time zone:

# application.rb
default settings, no changes

# application_controller.rb
before_filter :set_time_zone

def set_time_zone
  if current_user
    Time.zone = current_user.time_zone
  elsif current_admin
    Time.zone = current_admin.time_zone
  end
end

If I filter only on city, I got this query:

SELECT "schedules".* 
FROM "schedules" 
WHERE (
    "schedules"."city_id" = 6 AND 
    starts_at::time >= '2015-09-30 23:00:00.000000' AND 
    ends_at::time <= '2015-10-01 15:45:00.000000'
) AND (
    starts_at >= '2015-10-01 20:52:39.866880'
) ORDER BY "schedules"."starts_at" ASC, "schedules"."ends_at" ASC`

Basically if the user's time zone is GMT+8 then it will take the value from the starts_at and ends_at time select menu using today's date (1 Oct 2015) and automatically substract 8 hours from it before running the query. How do I prevent Ransack from substracting the offset hours?


Solution

  • Apparently the sql query automatically subtracting 8 hours according to my time zone is the expected behaviour and the right thing to do.

    Rather than filtering data without any date involved, I removed the blank option from the date select and made sure the default schedules shown are filtered to tomorrow's date, rather than showing all the schedules. This way the user must choose a date first before filtering the schedules.

    I had to modify the incoming params from the time select so it uses the params from the date select before handing all the params to Ransack.

    Schedule model:

    # Delete the custom Ransack methods as we don't need them anymore:
    
    ransacker :start_date, type: :date ...
    ransacker :start_time, type: :time ...
    ransacker :end_time, type: :time ...
    
    # ...and add these scopes:
    
    scope :sort_by_datetime_asc, -> { order(:starts_at, :ends_at) }
    
    def self.only_tomorrow
      where('starts_at BETWEEN ? AND ?', Time.zone.tomorrow.beginning_of_day, Time.zone.tomorrow.end_of_day)
    end
    

    Schedule controller:

    params[:q] = {} unless params[:q]
    
    # Copies the date params from date select to replace the date params from the time select menu.
    if params["start_date"].present?
      date  = Time.zone.parse(params["start_date"])
    
      year  = date.strftime("%Y")
      month = date.strftime("%m")
      day   = date.strftime("%e")
    
      params[:q]["starts_at_gteq(1i)"] = year
      params[:q]["starts_at_gteq(2i)"] = month
      params[:q]["starts_at_gteq(3i)"] = day
    
      params[:q]["ends_at_lteq(1i)"]   = year
      params[:q]["ends_at_lteq(2i)"]   = month
      params[:q]["ends_at_lteq(3i)"]   = day
    end
    
    @q = Schedule.ransack(params[:q])
    
    if params[:q].present?
      @schedules = @q.result
    else
      @schedules = Schedule.only_tomorrow
    end
    
    @schedules = @schedules.six_hours_from_now.sort_by_datetime_asc
    

    Schedule#index view:

    <%= select_tag "start_date", options_for_select([...], selected: default_date_select) %>
    <%= f.time_select :starts_at_gteq ...
    <%= f.time_select :ends_at_lteq ...
    

    Schedule helper:

    # Show selected date for Schedule's date filter.
    def default_date_select
      if params["start_date"].present?
        params["start_date"]
      else
        ['Tomorrow', Time.zone.tomorrow]
      end
    end