How to override a rails 4 module from the app - ruby-on-rails

In my application I need to record the created_at based on the current user's time as I need to apply different rules and conditions based on the time user created a record.
This is what I have done:
in the application.rb:
config.time_zone = 'Melbourne'
config.active_record.default_timezone = :local
However when user from different timezone create a record or update a record it saves as Melbourne timezone. I thought default timezone would be set to where user create the record.
Basically I need to be able to do this for the created_at and updated_at:
Time.zone.now
So I did the following in my initialiser folder:
I have added timestamp.rb with the following code:
module ActiveRecord
module Timestamp
def self.included(base) #:nodoc:
base.alias_method_chain :create, :timestamps
base.alias_method_chain :update, :timestamps
base.class_inheritable_accessor :record_timestamps, :instance_writer => false
base.record_timestamps = true
end
private
def create_with_timestamps #:nodoc:
if record_timestamps
t = Time.zone.now
write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil?
write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil?
write_attribute('updated_at', t) if respond_to?(:updated_at) && updated_at.nil?
write_attribute('updated_on', t) if respond_to?(:updated_on) && updated_on.nil?
end
create_without_timestamps
end
def update_with_timestamps(*args) #:nodoc:
t = Time.zone.now
write_attribute('updated_at', t) if respond_to?(:updated_at)
write_attribute('updated_on', t) if respond_to?(:updated_on)
end
end
end
I restarted the server however nothing has changed, still I get the wrong timezone recordings. If somehow I force Rails to use my Timestamp override this problem would be solved.
This is on my Local machine at about 10:15 AM
Here is an example of created_at after create is triggered:
created_at: "2014-12-09 22:15:09"
Now when I run the Time.zone.now in the console I get
Wed, 10 Dec 2014 10:15:09 EST +11:00
FYI: (Not sure this is matter.)
Database is located on Amazon RDS which is in different timezone too and it is Postgresql.

Related

How to convert datetime on a per user basis

I have left my application.rb as it was and by default it is storing datetime in UTC.
Now at the UI level (for displaying only) I want to convert the datetime to the user specific timezone.
How can I convert the UTC datetime I get from postgresql and then convert it to the timezone that I will store for each user.
I want to make this conversion using the Offset so like: -5:00 or +4:00
Is it ok to do this, because I was just checking some locations and it seems their offset changes during the season.
E.g. virginia goes from UTC -5 to UTC -4 depending on the month.
http://www.timeanddate.com/time/zone/usa/richmond
If UTC is not consistant then I should just store the zone in the database for each user?
When I made an app with scheduling events, I had to add timezones to the User model to offset their scheduling.
When I recorded the offset, I used the following code:
<%= time_zone_select "user",
"timezone",
ActiveSupport::TimeZone.us_zones,
{default: "Mountain Time (US & Canada)"},
{class: "form-control"} %>
Because Rails has time zones built into their ActiveSupport model. You could use ActiveSupport::TimeZone.all if you wanted users to be global.
Here's some info on time_zone_select
Then depending on how you use it, you can just set
Time.zone = current_user.timezone unless current_user.nil?
or something similar in your application.rb file.
UPDATE
I personally used it to set the timezone in the controller on the only 2 actions where it was necessary.
def index
#user = current_user
#pending = #user.share_events.where("time > ?", DateTime.now.in_time_zone(#user.timezone))
#complete = #user.share_events.where("time <= ?", DateTime.now.in_time_zone(#user.timezone))
end
def new
#user = current_user
Time.zone = #user.timezone
#post = Post.find(params[:post_id])
#share_event = ShareEvent.new
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.

Invalid argument to TimeZone?

I'm trying to make my users Time zone be the current Time zone of my application so everything they interact will be by it. I run into an ArgumentError for my method inside of my ApplicationController though.
application_controller.rb
before_filter :set_user_time_zone
private
def set_user_time_zone
if signed_in?
Time.zone = Time.now.in_time_zone(current_user.time_zone)
end
end
Notes: current_user is a Devise Helper and my User model as a :time_zone column.
Than the error:
invalid argument to TimeZone[]: Mon, 20 Aug 2012 13:16:20 JST +09:00
I don't know where to go from here. Any ideas on how to correct this?
Thanks.
UPDATE
class Price
attr_accessible :date
def self.today
where(:date => Date.today)
end
end
If my method does:
def set_user_time_zone
if signed_in?
Time.zone = current_user.time_zone
end
end
The problem I have my times are like this:
Time.now = US EAST- 2012-08-22 21:17:03 -0400
Time.zone = TOKYO - (GMT+09:00) Tokyo
Time.zone.now 2012-08-23 10:17:03 +0900
Which means all of my Date methods go by
Time.now = US EAST- 2012-08-22 21:17:03 -0400
when it should be
Time.zone.now 2012-08-23 10:17:03 +0900
How can I get it to the latter?
Time.now.in_time_zone(current_user.time_zone) returns instance of TimeWithZone class, but
Time#zone= expects to get something that can be converted to TimeZone.
Assuming you store TimeZone identifiers in your :time_zone column (“Eastern Time (US & Canada)”, "Hawaii", etc.) you can simply do
Time.zone = current_user.time_zone
Time#zone= method accepts only these params:
A Rails TimeZone object.
An identifier for a Rails TimeZone object (e.g., “Eastern Time (US &
Canada)”, -5.hours).
A TZInfo::Timezone object.
An identifier for a TZInfo::Timezone object (e.g.,
“America/New_York”).
So you should pass something from this list

Get objects size between relative dates

I have many User objects with created_at attribute e.g.
I get objects with #users = User.all
I want get the count of User objects with various ages from creation with
#users.size
for these date ranges:
yesterday
last week
last month
last year.
How can I do it?
I use mongoid.
You can write scopes for this:
class User
include Mongoid::Document
...
## Scopes for calculating relative users
scope :created_yesterday, lambda { where(:created_at.gte => (Time.now - 1.day)) }
scope :created_last_week, lambda { where(:created_at.gte => (Time.now - 1.week)) }
scope :created_last_month, lambda { where(:created_at.gte => (Time.now - 1.month)) }
scope :created_last_year, lambda { where(:created_at.gte => (Time.now - 1.year)) }
...
end
The reason we need to use a lambda here is that it delays the evaluation of the Time.now argument to when the scope is actually invoked. Without the lambda the time that would be used in the query logic would be the time that the class was first evaluated, not the scope itself.
Now we can get the counts, by simply calling:
User.created_yesterday.count
User.created_last_week.count
...
If you want the objects:
#users_created_yesterday = User.created_yesterday
You can use #users_created_yesterday in the views.
Update
Well with yesterday, if you mean time between, yesterday beginning of day 0:00 and yesterday end of day 23:59, you will have to take Time zones into consideration.
Fo example, in your application.rb, if you have written:
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
config.time_zone = 'Central Time (US & Canada)'
If you have use this, all the times fetched by activerecord queries will be converted to this time zone, Central Time (US & Canada). In the database, all the times will be stored in UTC and will be converted to the time zone on fetching from the database. If you have commented out this line, default will be UTC. You can get all the time zones using the rake task rake time:zones:all or only the US timezones using rake time:zones:us.
If you want the time zone specified in the application.rb, you need to use Time.zone.now, in the following code. If you use Time.now in the following code, you will get the time zone according to the time zone of your server machine.
class User
include Mongoid::Document
...
scope :created_between, lambda { |start_time, end_time| where(:created_at => (start_time...end_time)) }
class << self
## Class methods for calculating relative users
def created_yesterday
yesterday = Time.zone.now - 1.day
created_between(yesterday.beginning_of_day, yesterday.end_of_day)
end
def created_last_week
start_time = (Time.zone.now - 1.week).beginning_of_day
end_time = Time.zone.now
created_between(start_time, end_time)
end
def created_last_month
start_time = (Time.zone.now - 1.month).beginning_of_day
end_time = Time.zone.now
created_between(start_time, end_time)
end
def created_last_year
start_time = (Time.zone.now - 1.year).beginning_of_day
end_time = Time.zone.now
created_between(start_time, end_time)
end
end
..
..
end
So, you can write class methods and calculate start time and end time, supply it to the scope created_between, and you will be able to call them like User.created_yesterday, like we called before.
Credits: EdgeRails. Since it is Mongoid, I had to look up at Mongoid docs

Rails time zone not overrides as well

I have a noisy problems with UTC on my Rails project.
class ApplicationController < ActionController::Base
before_filter :set_timezone
def set_timezone
Time.zone = current_user.time_zone if current_user
end
Cool. I overrided the time zone.
And now, server' time zone is +3. User's time zone is +5. I hope that any requests to Time should get the User's time zone, but this code returns not expected values:
render :text => Time.zone.to_s + "<br/>" +
Time.now.to_s + "<br/>" +
Time.now.in_time_zone.to_s
RESULT:
(GMT+05:00) Tashkent
Thu Oct 20 19:41:11 +0300 2011
2011-10-20 21:41:11 +0500
Where does from +0300 offset comes??
To get the current time in the currently set timezone you can use
Time.zone.now
Your server' time zone is +3 and
Time.now.to_s is returning this
saha! Sorry, but I have not a 15 points of reputation to give you the level-up)).
Anyway thanks for your help.
I wrote a TimeUtil helper, an uses it for time correction. This is my current pseudo-code:
class RacesController < ApplicationController
def create
#race = Race.new(params[:race])
#race.correct_time_before_save #can be before_save
#race.save
end
class Race < ActiveRecord::Base
def correct_time_before_save
date = self.attributes["race_date"]
time = self.attributes["race_time"]
datetime = Time.local(date.year, date.month, date.day, time.hour, time.min, time.sec)
datetime_corrected = TimeUtil::override_offset(datetime)
self.race_date = datetime_corrected.to_date
self.race_time = datetime_corrected.to_time
end
# TimeUtil is uses for time correction. It should be very clear, please read description before using.
# It's for time correction, when server's and user's time zones are different.
# Example: User lives in Madrid where UTC +1 hour, Server had deployed in New York, UTC -5 hours.
# When user say: I want this race to be started in 10:00.
# Server received this request, and say: Okay man, I can do it!
# User expects to race that should be started in 10:00 (UTC +1hour) = 09:00 UTC+0
# Server will start the race in 10:00 (UTC -5 hour) = 15:00 UTC+0
#
# This module brings the workaround. All that you need is to make a preprocess for all incoming Time data from users.
# Check the methods descriptions for specific info.
#
# The Time formula is:
# UTC + offset = local_time
# or
# UTC = local_time - offset
#
module TimeUtil
# It returns the UTC+0 DateTime object, that computed from incoming parameter "datetime_arg".
# The offset data in "datetime_arg" is ignored - it replaces with Time.zone offset.
# Time.zone offset initialized in ApplicationController::set_timezone before-filter method.
#
def self.override_offset datetime_arg
Time.zone.parse(datetime_arg.strftime("%D %T")).utc
end
ActiveRecord getters adapted to user's time zones too. Time is stored in database (mysql) in "utc+0" format, and we want to get this time in current user's timezone format:
class Race < ActiveRecord::Base
def race_date
date = self.attributes["race_date"]
time = self.attributes["race_time"]
datetime = Time.utc(date.year, date.month, date.day, time.hour, time.min, time.sec).in_time_zone
datetime.to_date
end
def race_time
date = self.attributes["race_date"]
time = self.attributes["race_time"]
datetime = Time.utc(date.year, date.month, date.day, time.hour, time.min, time.sec).in_time_zone
datetime.to_time
end

Resources