Using Timecop gem For Scopes - ruby-on-rails

I'm spec'ing a scope in a Rails 3.0 app as follows:
class DrawingList < ActiveRecord::Base
scope :active_drawings, where('start_date <= ? AND end_date >= ?', Date.today, Date.today)
end
In my spec, I want to do:
before do
#list = DrawingList.create #things that include begin and end dates
end
it "doesn't find an active drawing if they are out of range" do
pending "really need to figure out how to work timecop in the presence of scopes"
Timecop.travel(2.days)
puts Date.today.to_s
DrawingList.active_drawings.first.should be_nil
end
As you might imagine, the puts really shows that Date.today is two days hence. However, the scope is evaluated in a different context, so it uses the old "today". How does one get today evaluated in a context that Timecop can affect.
Thanks!

This is a really common mistake. As you've written in the date used by the scope is the date as it was when the code was loaded. Were you to run this in production where code is only reloaded if you restart the app (unlike development where it is reloaded on each request), you'd get the right results on the day you restarted the app, but the next day the results would be out by one day, the day after by 2 days etc.
The correct way of defining a scope like that is
scope :active_drawings, lambda { where('start_date <= ? AND end_date >= ?', Date.today, Date.today)}
The lambda ensures that those dates are evaluated each time the scope is used.

Related

What is the best way to reuse a scope in Rails?

I'm confused to reuse or writing a new scope.
for example,
one of my methods will return future subscription or current subscription or sidekiq created subscriptions.
as scopes will look like:
scope :current_subscription, lambda {
where('(? between from_date and to_date) and (? between from_time and to_time)', Time.now, Time.now)
}
scope :sidekiq_created_subscription, lambda {
where.not(id: current_subscription).where("(meta->'special_sub_enqueued_at') is not null")
}
scope :future_subscription, lambda {
where.not(id: current_subscription).where("(meta->'special_sub_enqueued_at') is null")
}
so these were used for separate purposes in different methods, so for me what I tried is to check whether a particular account record will come under which of three subscriptions.
so I tried like:
def find_account_status
accounts = User.accounts
name = 'future' if accounts.future_subscription.where(id: #account.id).any?
name = 'ongoing' if accounts.current_subscription.where(id: #account.id).any?
name = 'sidekiq' if accounts.sidekiq_enqued_subscription.where(id: #account.id).any?
return name
end
so here what my doubt is, whether using like this is a good way, as here we will be fetching the records based on the particular subscriptions and then we are checking whether ours is there or not.
can anyone suggest any better way to achieve this?
Firstly, you are over using the scopes here.
The method #find_account_status will execute around 4 Queries as below:
Q1 => accounts = User.accounts
Q2 => accounts.future_subscription
Q3 => accounts.current_subscription
Q4 => accounts.sidekiq_enqued_subscription
Your functionality can be achived by simply using the #account object which is already present in memory as below:
Add below instance methods in the model:
def current_subscription?
# Here I think just from_time and to_time will do the work
# but I've added from_date and to_date as well based on the logic in the question
Time.now.between?(from_date, to_date) && Time.now.between?(from_time, to_time)
end
def future_subscription?
!current_subscription? && meta["special_sub_enqueued_at"].blank?
end
def sidekiq_future_subscription?
!current_subscription? && meta["special_sub_enqueued_at"].present?
end
#find_account_status can be refactored as below:
def find_account_status
if #account.current_subscription?
'ongoing'
elsif #account.future_subscription?
'future'
elsif #account.sidekiq_future_subscription?
'sidekiq'
end
end
Additionally, as far as I've understood the code, I think you should also handle a case wherein the from_date and to_date are past dates because if that is not handled, the status can be set based on the field meta["special_sub_enqueued_at"] which can provide incorrect status.
e.g. Let's say that the from_date in the account is set as 31st Dec 2021 and meta["special_sub_enqueued_at"] is false or nil.
In this case, #current_subscription? will return false but #future_subscription? will return true which is incorrect, and hence the case for past dates should be handled.

Rails - Query time shifts

currently I'm developing a system that manages work shifts.
The system contains an endpoint that returns the current shift based on server time. I created a query that works fine for regular shifts like 08:00 AM to 04:00 PM and so on but the challenge is when there is a dawn shift that starts at 10:00 PM until 06:00 AM from the other day.
My model contains the following fields:
name (String)
start_at (Time)
end_at (Time)
Ps.: I'm using Postgres as the database.
My code:
class Shift < ApplicationRecord
def self.current
current_time = Time.now()
current_time = current_time.change(year: 2000, month: 1, day: 1)
#current ||= Shift
.where('start_time <= ?', current_time)
.where('end_time >= ?', current_time)
.first()
end
end
Ps.: I guess, since I'm using the Time type in the database, I have to normalize the Time.now to use the 2000 - 01 - 01 date.
So, is there an easy/best way to do that?
Thanks for the help!
Interesting problem! So there's two cases: (1) a normal shift (where start_time <= end_time), and (2) a shift that overlaps midnight (where start_time > end_time).
You are already handling the first case by checking if the current time is between the start time and the end time.
I believe the second case can be handled by checking if the current time is either between the start time and midnight, or between midnight and the end time. Which translates to start_time <= ? OR end_time >= ?.
I haven't used Rails in a while, but I think you could do something like this:
#current ||= Shift
.where('start_time <= end_time')
.where('start_time <= ?', current_time)
.where('end_time >= ?', current_time)
.or(Shift
.where('start_time > end_time')
.or(Shift
.where('start_time <= ?', current_time)
.where('end_time >= ?', current_time)))
.first()
If you go with this, consider splitting the two cases into separate scopes, so you can write something more readable like this in this method:
#current ||= current_normal_shifts.or(current_dawn_shifts).first
You might be interested in the tod gem that provides a TimeOfDay class and a Shift class that takes two TimeOfDay objects to represent the start and end of the shift. It handles dawn shifts as expected.
An implementation might look like this:
require 'tod'
class Shift < ApplicationRecord
def self.current
find(&:current?)
end
def current?
schedule.include?(Tod::TimeOfDay(Time.now))
end
def schedule
Tod::Shift.new(Tod::TimeOfDay(start_time), Tod::TimeOfDay(end_time))
end
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.

ActiveRecord where method datetime passed

Thanks for your continuing support in my latest venture in a Rails app. It's been a while since I've made something in Rails so here is my latest question. I appreciate the help you've all given in the past.
I have a Model called Event.rb which contains a date:date and time:time fields for the Event.
I also have a method inside of the model which is..
def begins
DateTime.new(date.year, date.month, date.day, time.hour, time.min, time.sec)
end
As I can't see if something has truly passed because I only have Date and Time separate so I need them together.
My question is...
I want to be able to add in the DateTime :begins into the following other method in my Model...
def self.past
where("date <= ?", TIME_NOW)
end
Just like I have a method which is...
def upcoming?
self.date >= Time.now
end
Which I could easily change self.date to begins and would past I would imagine?
Thanks!
Perhaps something like this will work for querying the database for past events using your existing date and time columns:
scope :past, lambda {
where("date <= ? and time <= ?",
Time.now.strftime("%Y-%d-%m"),
Time.now.strftime("%H:%M:%S")
)
}
past_events = Event.past
For checking the current instance, you could continue to use your begins method:
def past?
begins < DateTime.now
end
#event = Event.first
#event.past?

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

Resources