(Rails) Test fails during late night time - ruby-on-rails

My rails application is covered by many tests. All the tests pass in regular situations. That is, excluding when it's late at night.
There's actually a few tests that end up failing when it's night. All these tests involve modifying a model's time attribute and seeing if related models are affected.
test "changing time should affect hours" do
// ..User is loaded.
user.attend(event)
assert_equal 1, user.hours // User attends a 1 hour event that has passed.
// Move event to the future.
event.update_attributes(date: Date.today,
start_time: Time.now,
end_time: Time.now + 1.hour)
assert_equal 0, attendance_of(user).hours // Passes in day, fails during night
end
test "valid event creation" do
// Count does NOT change by 1 at night.
assert_difference '#group.events.count', 1 do
post group_events_path(#group), event: { ...
date: Date.today,
start_time: Time.now,
end_time: Time.now + 1.hour,
... }
end
end
What is going on here? For reference, here's what I currently use to determine when to update an attendance (which is something that an event has). This comes from the event controller:
def not_ended?
date.future? || (date.today? &&
(Time.now.seconds_since_midnight < end_time.seconds_since_midnight))
end
def update_attendances
// ... Determine the new date, start, and end time values through ActiveRecord::Dirty
if not_ended?
remove_checks = true
end
attendances.each do |attendance|
new_checked = remove_checks ? false : attendance.checked
attendance.update_attributes(went: new_start, left: new_end,
checked: new_checked)
end
end
end
Validating an event to make sure its times aren't weird:
def valid_time
if start_time == end_time
// Error...
end
if start_time > end_time
// Error...
end
end
Time zone in application.rb:
config.time_zone = 'Pacific Time (US & Canada)'

Use Time.zone.now or timecop.freeze to freeze the timezone as your test is probably using system time and not rails app config time

Your not_ended? method is broken. It doesn't work when the event starts before midnight, but ends after. In that case the date is today (assuming the data is based on start time) but the number of seconds since midnight of the end of the event is less that the current time.
In these situations you shouldn't be trying to deal with dates and times separately. You should have a way to retrieve the datetime of the end of the event and compare that with the current datetime.

Related

Rails Rspec test fails when it works in app

I have the following code and test that I can't seem to make pass. The code should be auto locking all bookings that have been completed over 24 hours ago.
When I put a pry into the test and run the first line of Booking.auto_lock_guests nothing happens. When I type booking_7 and after type Booking.auto_lock_guests then it changes locked to true. Is this something to do with the way let is set up that it is not showing up in Booking.all? Or is it the way I have written the test?
Any help would be greatly appreciated.
def self.auto_lock_guests
bookings = Booking.where(guests_completed: true, locked: false)
bookings.each do |booking|
next unless booking.guests_completed_at <= 1.day.ago
booking.locked = true
booking.save
end
end
context 'auto_lock_guests' do
let(:booking_6) { FactoryGirl.create(:booking, date: Date.today - 5.day, guests_completed: true, guests_completed_at: DateTime.now, locked: false )}
let(:booking_7) { FactoryGirl.create(:booking, date: Date.today - 5.day, guests_completed: true, guests_completed_at: DateTime.now - 3.day, locked: false )}
before do
Booking.auto_lock_guests
end
it 'should only lock bookings with a guests_completed date older than a day ago' do
expect(booking_7.locked).to eq(true)
expect(booking_6.locked).to eq(false)
end
end
let is lazily evaluated. When the before block is executed there are no records, because the let blocks haven't yet been called.
Either change let to let! to execute the block immediately or call booking_6 and booking_7 right before Booking.auto_lock_guests
EDIT:
Also you don't check wether the booking.save succeeded. If booking.save failed - you would never know. :)
The next unless booking.guests_completed_at <= 1.day.ago could probably be rewritten as a query: where(Booking.arel_table[:guests_completed_at].gt(1.day.ago))
You don't need to iterate through the records in the first place. In fact it will cause problems as your app scales since pulling all those records into memory will exhaust the servers (or dynos) memory.
You can select the records from the database and update them in a single query:
class Booking
def self.auto_lock_guests!
bookings = Booking.where(guests_completed: true, locked: false)
.where('guests_completed_at <= ?', 1.day.ago)
bookings.update_all(locked: true)
end
end
The difference in execution time between many individual UPDATE queries and an updating many rows at once can be massive.
To test it you can create multiple records and use change expectations:
# use describe and not context for methods.
describe ".auto_lock_guests" do
# let! is not lazy loading
let!(:old_booking) { FactoryGirl.create(:booking, date: 7.days.ago, guests_completed: true, guests_completed_at: 3.days.ago, locked: false )}
let!(:new_booking) { FactoryGirl.create(:booking, date: Date.today, guests_completed: true, guests_completed_at: DateTime.now, locked: false )}
it 'locks a booking with a guests_completed date older than a day ago' do
expect do
Bookings.auto_lock_guests! && old_booking.reload
end.to change { old_booking.locked }.from(false).to(true)
end
it 'does not lock a when guests_completed date is less than a day ago' do
expect do
Bookings.auto_lock_guests! && new_booking.reload
end.to_not change { new_booking.locked }.from(false).to(true)
end
end
Using change is a very good idea when testing methods that change the database as they verify both the initial state and the result.
I ended up having to add this into the before action after calling Booking.auto_lock_guests and it worked.
before do
Booking.auto_lock_guests
booking_7.reload
booking_6.reload
end

Fetching Data based date and time

I am trying to find results from today onwards but also want to include the yesterdays plans if the time is between 12:00am-5:00am
Right now i have the following
def self.current
where(
"plan_date >= :today",
today: Date.current,
)
end
Is there a way i can know the time of the day based on the users timezone which am setting as bellow in the app controller and make sure that if its before 6:am the next day i want to include the previous days results as well.
def set_time_zone(&block)
if current_user
Time.use_zone(current_user.time_zone_name, &block)
else
yield
end
end
Try this:
def self.current
where(
"plan_date >= :today",
today: (Time.zone.now.in_time_zone(get_user_time_zone) - 6.hours).beginning_of_day,
)
end
...where get_user_time_zone returns the time zone for the user (E.G.: America/New_York). I'm using - 6.hours because you wanted it to be "before 6am" local time.

Rails/Postgres query rows grouped by day with time zone

I'm trying to display a count of impressions per day for the last 30 days in the specific users time zone. The trouble is that depending on the time zone, the counts are not always the same, and I'm having trouble reflecting that in a query.
For example, take two impressions that happen at 11:00pm in CDT (-5) on day one, and one impression that happens at 1:00am CDT. If you query using UTC (+0) you'll get all 3 impressions occurring on day two, instead of two the first day and one the second. Both CDT times land on the day two in UTC.
This is what I'm doing now, I know I must be missing something simple here:
start = 30.days.ago
finish = Time.now
# if the users time zone offset is less than 0 we need to make sure
# that we make it all the way to the newest data
if Time.now.in_time_zone(current_user.timezone) < 0
start += 1.day
finish += 1.day
end
(start.to_date...finish.to_date).map do |date|
# get the start of the day in the user's timezone in utc so we can properly
# query the database
day = date.to_time.in_time_zone(current_user.timezone).beginning_of_day.utc
[ (day.to_i * 1000), Impression.total_on(day) ]
end
Impressions model:
class Impression < ActiveRecord::Base
def self.total_on(day)
count(conditions: [ "created_at >= ? AND created_at < ?", day, day + 24.hours ])
end
end
I've been looking at other posts and it seems like I can let the database handle a lot of the heavy lifting for me, but I wasn't successful with using anything like AT TIME ZONE or INTERVAL.
What I have no seems really dirty, I know I must missing something obvious. Any help is appreciated.
Ok, with a little help from this awesome article I think I've figured it out. My problem stemmed from not knowing the difference between the system Ruby time methods and the time zone aware Rails methods. Once I set the correct time zone for the user using an around_filter like this I was able to use the built in Rails methods to simplify the code quite a bit:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_filter :set_time_zone
def set_time_zone
if logged_in?
Time.use_zone(current_user.time_zone) { yield }
else
yield
end
end
end
# app/controllers/charts_controller.rb
start = 30.days.ago
finish = Time.current
(start.to_date...finish.to_date).map do |date|
# Rails method that uses Time.zone set in application_controller.rb
# It's then converted to the proper time in utc
time = date.beginning_of_day.utc
[ (time.to_i * 1000), Impression.total_on(time) ]
end
# app/models/impression.rb
class Impression < ActiveRecord::Base
def self.total_on(time)
# time.tomorrow returns the time 24 hours after the instance time. so it stays UTC
count(conditions: [ "created_at >= ? AND created_at < ?", time, time.tomorrow ])
end
end
There might be some more that I can do, but I'm feeling much better about this now.
Presuming the around_filter correctly works and sets the Time.zone in the block, you should be able to refactor your query into this:
class Impression < ActiveRecord::Base
def self.days_ago(n, zone = Time.zone)
Impression.where("created_at >= ?", n.days.ago.in_time_zone(zone))
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.

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

Resources