Rails business hours query for active record - ruby-on-rails-3

I have two models
OfficeTimeing < ActiveRecord::Base
belongs_to :office
end
Office < ActiveRecord::Base
has_many :office_timings
end
with two fields opening_time and closing_time these fields have string values like "09:00 AM" I want a query how can I find currently open offices I want something like this.
Office.joins(:office_timings).where("Datetime.now > office_timings.opening_time AND office_timings.closing_time > DateTime.now")

I don't know how to compare two times represented as a string without being able to parse them in rails first, however, have you considered storing your opening and closing times in seconds (counting from midnight) rather than strings? That way you would easily be able to write this query.
UPDATE: A few useful methods to achieve this.
To get the current seconds since midnight for a specific time:
seconds = specific_time.seconds_since_midnight
To convert the seconds since midnight into a Time object:
time_instance = Time.at(seconds)
To produce a string in the form of 09:00 AM
time_instance.strftime("%I:%M %p")
Details about Time class.
Details about strftime.

You need to inject Datetime.now into your query using ?:
Office.joins(:office_timings)
.where(
"? > office_timings.opening_time AND office_timings.closing_time > ?",
Datetime.now, Datetime.now
)
Each ? will be safely replaced by the arguments following the query string.
You could also do this directly in Postgres using now(), and make the condition a bit easier to read using BETWEEN:
.where("now() BETWEEN office_timings.opening_time AND office_timings.closing_time")

Related

RSpec: How to mock SQL NOW()

I can mock Time.now with a great timecop gem.
Time.now
=> 2018-05-13 18:04:46 +0300
Timecop.travel(Time.parse('2018.03.12, 12:00'))
Time.now
=> 2018-03-12 12:00:04 +0300
TeacherVacation.first.ends_at
Thu, 15 Mar 2018 12:00:00 MSK +03:00
TeacherVacation.where('ends_at > ?', Time.now).count
1
But (obviously) this wouldn't work while using NOW() in a query:
TeacherVacation.where('ends_at > NOW()').count
0
Can I mock NOW() so that it would return the results for a certain time?
Timecop is a great gem! I would recommend using Timecop.freeze instead of traveling for your instance; you want to keep your tests deterministic.
As far as I could find, there doesn't seem to be a way to mock SQL's functions. Some languages like Postgres allow overloading functions, but you would still need a way to interject, and there doesn't seem to be a way to use environment variables in SQL.
A co-worker seemed to be certain you could actually drop system/language functions and make your own, but I was concerned about how to recover them after you do that. Trying to go that route sounds like a pain.
Solutions?
Here are a couple of "solutions" that I've come up with today while fighting this problem. Note: I don't really care for them to be honest, but if it gets tests in place ¯\_(ツ)_/¯ They at least offer a way to get things "working".
Unfortunately there's no snazzy gem to control the time in SQL. I imagine you would need something crazy like a plugin to the DB, a hack, a hook, a man in the middle, a container that you could trick SQL into thinking the system time was something else. None of those hack ideas would surely be portable/platform agnostic unfortunately either.
Apparently there are some ways to set time in a docker container, but that sounds like a painful overhead for local testing, and doesn't fit the granularity of a per-test time to be set.
Another thing to note, for me we're running large complex raw SQL queries, so that's why it's important that when I run the SQL file for a test I can have proper dates, otherwise I would just be doing it through activerecord like you mentioned.
String Interpolation
I ran across this in some large queries that were being ran.
This definitely helps if you need to push some environment variables through, and you can inject your own "current_date" if you want. This would help too if you needed to utilize a certain time across multiple queries.
my_query.rb
<<~HEREDOC
SELECT *
FROM #{#prefix}.my_table
WHERE date < #{#current_date} - INTERVAL '5 DAYS'
HEREDOC
sql_runner.rb
class SqlRunner
def initialize(file_path)
#file_path = file_path
#prefix = ENV['table_prefix']
#current_date = Date.today
end
def run
execute(eval(File.read #file_path))
end
private
def execute(sql)
ActiveRecord::Base.connection.execute(sql)
end
end
The Dirty Update
The idea is to update the value from ruby land pushing your "time-copped" time into the database to overwrite the value generated by the SQL DB. You may need to get creative with your update for times, like querying for a time greater than a given time that doesn't target your timecop time that you'll be updating rows to.
The reason I don't care for this method is because it ends up feeling like you're just testing activerecord's functionality since you're not relying on the DB to set values it should be setting. You may have computations in your SQL that you're then recreating in the test to set some value to the right date, and then you're no longer doing the computation in the SQL so then you're not even actually testing it.
large_insert.sql
INSERT INTO some_table (
name,
created_on
)
SELECT
name,
current_date
FROM projects
JOIN people ON projects.id = people.project_id
insert_spec.rb
describe 'insert_test.sql' do
ACTUAL_DATE = Date.today
LARGE_INSERT_SQL = File.read('sql/large_insert.sql')
before do
Timecop.freeze Date.new(2018, 10, 28)
end
after do
Timecop.return
end
context 'populated same_table' do
before do
execute(LARGE_INSERT_SQL)
mock_current_dates(ACTUAL_DATE)
end
it 'has the right date' do
expect(SomeTable.last.created_on).to eq(Date.parse('2018.10.28')
end
end
def execute(sql_command)
ActiveRecord::Base.connection.execute(sql_command)
end
def mock_current_dates(actual_date)
rows = SomeTable.where(created_on: actual_date)
# Use our timecop datetime
rows.update_all(created_on: Date.today)
end
Fun Caveat: specs wrap in their own transactions (you can turn that off, but it's a nice feature) so if your SQL has a transaction in it, you'll need to write code to remove it for the specs, or have your runner wrap your code in transactions if you need them. They'll run but then your SQL will kill off the spec transaction and you'll have a bad time. You can create a spec/support to help out with this if you go the route of cleaning up during tests, if I were in a newer project I would go with writing a runner that wraps the queries in transactions if you need them -- even though this isn't evident in the SQL files #abstraction.
Maybe there's something out there that lets you set your system time, but that sounds terrifying modifying your system's actual time.
I think the solution for this is DI (dependency injection)
def NOW(time = Time.now)
time
end
In test
current_test = Time.new(2018, 5, 13)
p NOW(current_test)
In production
p NOW

How do I change the format ActiveRecord expects when parsing dates from a text field in a form?

The problem
I have a Ruby on Rails model with a Date attribute.
In the form for this model, I am using a single text field with a JQuery datepicker to represent this attribute (not a drop down for each of year, month, and day, as is the Rails custom).
The datepicker inserts dates with a mm/dd/yyyy format.
Rails is expecting dates with a dd/mm/yyyy format.
Examples
If a user selects March 12th, 2012, the datepicker puts 03/12/2012, which is interpreted by Rails as December 3rd, 2012.
If a user selects March 20th, 2012, the datepicker puts 03/20/2012, which is interpreted by Rails as the 3rd day of the 20th month of 2012. Since this date doesn't exist, Rails casts this to a nil value (I think).
Question
How do I change the date format Rails uses when parsing this date text field?
Notes:
1) I do not want to change the format of the date the datepicker inserts into the text field,
2) I am not asking about displaying my date attribute in a view.
I initially thought this could be solved through the Rails internationalization features, but it turns out I was wrong.
Ever since Ruby 1.9, the standard format for date parsing is dd/mm/yyyy, so as to better accomodate international users. More details can be found in this SO answer.
That standard is maintained in Rails, as Date.parse is now used to process data from form inputs. Using a before_validation callback won't work because the field is going to be received as nil by the callback method.
Right now there are two gems dealing with this specific issue, namely that date parsing in Rails does not follow the locale settings from I18n.locale. Both seem to work well.
delocalize, by clemens - Seems to have been applied successfully in a decent number or projects and has the highest number of stars at the moment.
i18n_alchemy by carlosantoniodasilva - This one has been released more recently. The author is a Rails core team member, and a very active one at that. Definitely deserves a look.
Since you don't want to change the picker's format, I would suggest you use a hidden field for the actual model property.
For example, add a hidden field for the model's date property, assuming you use a form builder as usual:
f.hidden_field :date
Then for the picker text input, don't bind it to the model's date property. Let's say the hidden field has ID 'modelname_date' and the picker text input has ID 'date_picker', use the following to make it work:
$(function(){
$("#date_picker").datepicker({altField: '#nodelname_date', altFormat: 'dd/mm/yyyy'});
});
In this way the date picker shows the date as 'mm/dd/yyyy' but Rails will see the date as 'dd/mm/yyyy'.
Update:
If you want to work this out on the Rails side, here's another solution I'd suggest:
Add a virtual property to your model: attr_accessor :bad_format_date
Add a before_validation callback in which you parse the input date and assign it to the real field:
before_validation do
self.date = Date.strptime(bad_format_date, "%m/%d/%Y")
end
Then for the form on the view use bad_format_date but initialize it with the date field value (if it's an edit form).
The timeliness gem makes ruby date/time parsing much more customizeable and integrates well with Rails.
Since you're working with Rails, be sure to check out the validates_timeliness project as well by the same guy. It includes all of timeliness plus sophisticated date/time validation methods for ActiveModel.
You could try do something like this.
$(function(){
$('#date_picker').datepicker( {
beforeShowDay: $.datepicker.noWeekends,
showOtherMonths: true,
selectOtherMonths: true,
dateFormat: 'dd-mm-yy',
defaultDate: date,
gotoCurrent: true
});
I just add the following monkey patch to config/time_formats.rb
class Date
class << self
alias :euro_parse :_parse
def _parse(str,comp=false)
str = str.to_s.strip
if str == ''
{}
elsif str =~ /^(\d{1,2})[-\/](\d{1,2})[-\/](\d{2,4})/
year,month,day = $3.to_i,$1,$2
date,*rest = str.split(' ')
year += (year < 35 ? 2000 : 1900) if year < 100
euro_parse("#{year}-#{month}-#{day} #{rest.join(' ')}",comp)
else
euro_parse(str,comp)
end
end
end
end

Rails fetch images from different months

Iam a Rails developer and working on a project which needs to a show collection set of different images. I want to implement an algorithm where I want to fetch images in different timespan , for instance, 30% images from current month and 20% images from previous month and so on and shuffle them all together and create a set and then show it on the website. Thus it makes me to target created_at attribute for Image model and fetch images on the basis of the creation date.
I looked for various possible solutions. One of them is Thinking Sphinx. As per the doc you can give field weight to an attribute. But my problem is I want to give weightage within the attribute i.e. 30 % to first month, 20% to second month. So I didnt find it a good solution.
Next I thought to achieve the same using scopes
Image model
class Image < ActiveRecord::Base
scope :current_month, where("created_at < ? and created_at > ?", Time.now, 1.month.ago).shuffle.take(6)
scope :previous_month, where("created_at < ? and created_at > ?", 1.month.ago, 2.month.ago).shuffle.take(5)
scope :last_month, where("created_at < ? and created_at > ?", 2.month.ago, 3.month.ago).shuffle.take(4)
end
Image Controller
class ImagesController < ApplicationController
#total_images = Image.current_month + Image.previous_month + Image.last_month
end
But this fetches the same set of Images for each request. Whereas I need different images everytime a request is made. Moreover this will make my website's performance really down since a single request hits my database so many times.
Please guide me a better and correct way to implement this. Thanks in advance.

Store time interval in PostgreSQL from Rails

I need to store time inverval in PosgreSQL. Data will be put from Ruby on Rails.
Say, PizzaHut accepts orders from 9:00 to 18:00.
I've created following migration:
class AddOrderTimeAndDeliveryTimeToMerchants < ActiveRecord::Migration
def self.up
add_column :merchants, :order_time, :interval
end
I've read the documentation, and have following code now:
Merchant.create( :delivery_time => "9:00 18:00" )
When i execute it, i get following error message:
PGError: ERROR: invalid input syntax for type interval: "9:00 18:00"
How to do it correctly ?
I don't think an interval is really what you want for that. An interval represents a timespan without any specific end points; for example, you add an interval to an existing time to get another time. You would be better off with two distinct times:
add_column :merchants, :order_from, :time, :null => false
add_column :merchants, :order_to, :time, :null => false
Then, if for some reason you need to know how many hours they're open for delivery, you can construct an interval by subtracting :order_from from :order_to.
If you really must use an interval, then you'll need to construct a value something like this:
:delivery_time => "interval '11 hour'"
Note how this illustrates that an interval is not a specific time range from A to B, it is just a time range of some certain length (with no specified end points).
It's likely that you want a time without timezone here, since if Dominoes in NY opens at 9:00 local time, and Dominoes in California also opens at 9:00 local time, then a time with a timezone would not work properly.
What you likely want is one of two things. either two times a start and an end time, or a start time and an interval. I would suggest two times, but the choice is yours. Note that you can an interval from two times by subtracting one from the other.
select '09:00:00'::time - '05:00:00'::time;
?column?
----------
04:00:00

date comparisons in Rails

I'm having trouble with a date comparison in a named scope. I'm trying to determine if an event is current based on its start and end date. Here's the named scope I'm using which kind of works, though not for events that have the same start and end date.
named_scope :date_current, :conditions => ["Date(start_date) <= ? AND Date(end_date) >= ?", Time.now, Time.now]
This returns the following record, though it should return two records, not one...
>> Event.date_current
=> [#<Event id: 2161, start_date: "2010-02-15 00:00:00", end_date: "2010-02-21 00:00:00", ...]
What it's not returning is this as well
>> Event.find(:last)
=> #<Event id: 2671, start_date: "2010-02-16 00:00:00", end_date: "2010-02-16 00:00:00", ...>
The server time seems to be in UTC and I presume that the entries are being stored in the DB in UTC. Any ideas as to what I'm doing wrong or what to try?
Thanks!
Easiest way to debug is to look in your rails log to see exactly what SQL statement is being generated by Rails.
Then post the sql if still having problems.
In the meantime, my guess is that something is not set to UTC. Where something is the set {operating system, rails environment, dbms}
Added: Also, why are you comparing "Date" (in the dbms) with "Time" values (from your statement)? Better to have the type classes match explicitly. I use the standard of a new day has time component 00:00:00. That way you can compare with the db without needing the date function in your SQL.
I struggled with the same issue. The records are stored in UTC, but the utc value is not inserted into the query. I solved it, like you, by converting it to utc manually, like this time.utc.
def find_production_since(start_time)
find(:all, :conditions => ["time > ?", start_time.utc])
end
This might help you.
named_scope :date_current, :conditions => ["Date(start_date) <= Date(NOW()) AND Date(end_date) >= Date(NOW())"]
Rather than using Time.now (and then laboriously converting to a date as per your second comment) you could just use Date.today, which will insert '2010-04-20' into your SQL, and should compare against Date(start_date) without worrying about times.