Rails scope created within month - ruby-on-rails

I've got some issues with writing my model scope. I want to filter my model objects based on a month in which they are published, i.e model BlogPost :
scope :published_in_month, ->(date) { where(published_date: date.at_beginning_of_month..date.at_end_of_month) }
So this is my controller method :
def list_by_month
#date = Time.parse("#{params[:year]}/#{params[:month]}")
puts "DATE IS #{#date}"
#posts = BlogPost.published_in_month(#date).page(params[:page]).per(10)
render :index
end
So date printed out I see is :
DATE IS 2013-12-01 00:00:00 +0100
But in my log and it the page I see post(s) from wrong month, this is a entry log :
SELECT "blog_posts".* FROM "blog_posts" WHERE ("blog_posts"."published_date" BETWEEN '2013-11-30 23:00:00.000000' AND '2013-12-31 22:59:59.999999') LIMIT 10 OFFSET 0
Where is this 2013-11-30 coming from when my input date is 2013-12-01, and how can I rewrite my scope if I made mistake with it to produce incorrect query

This could help:
scope :from_month, -> { where(created_at: DateTime.now.beginning_of_month..DateTime.now.end_of_month) }

Perhaps it is better to use SQL methods to determine the month of each record:
scope :published_in_month, ->(date) { where("MONTH(created_at) = ?", date.month) }
Postgresql version:
scope :published_in_month, ->(date) { where("DATE_PART('month', timestamp created_at) = ?", date.month) }
(not tested)

Related

Active Record Query ignoring nil values

Takes a query using some methods like code bellow. How can I ignore the nil values? For example: if the method date returns nil I wanna that the query use just array_one and array_to.
def array_one
...
end
def array_two
...
end
def date
...
end
Record.where(array_one: array_one, array_two: array_two, date: date)
You can chain your queries together (it's called lazy loading). They won't actually be executed until the first thing that calls query. This allows you to 'build' the query
query = Record.where(array_one: array_one, array_two: array_two)
query = query.where(date: date) if date.present?
query.each do |row| # now the query is executed
# do stuff
end
Record.where({ array_one: array_one, array_two: array_two, date: date }.compact)
I would do something like this:
scope = Record.scope
scope = scope.where(array_one: array_one) if array_one
scope = scope.where(array_two: array_two) if array_two
scope = scope.where(date: date) if date
scope

Rails Iteration by month given range of dates

I'm trying to follow this Railscast and create a morris.js line chart for my Enquiry model.
I've grouped the counts using date_trunc into months, but now I'm not quite sure at how to get the X-axis to iterate over months (e.g Jun 2012, Jul 2013) as opposed to by date as in the railscasts notes.
I've tried the range#step method here, but now the graph displays only one date (2012-07-01) without a count and nothing else. Commenting out the .step(1.month) method from the range variable and the graph works fine but the x-axis iterates by date.
class Enquiry < ActiveRecord::Base
def self.chart_data(start = 1.year.ago)
total_count = total_count_by_month(start)
start = start.to_date.beginning_of_month
today = Date.today.beginning_of_month
range = (start..today).step(1.month)
range.map do |month|
{
created_at: month,
total_enquiries: total_count[] || 0
}
end
end
def self.total_count_by_month(start)
enquiries = unscoped.where(created_at: start.beginning_of_month..Time.now)
enquiries = enquiries.group("date_trunc('month', created_at)")
enquiries = enquiries.select("date_trunc('month', created_at) as created_at, count(*) as count")
enquiries.each_with_object({}) do |enquiry, counts|
counts[enquiry.created_at.to_date] = enquiry.count
end
end
end
How do I get the chart's x-axis to iterate by months (Jun 2013, Jul 2013 ....) instead of by dates?
For anyone else facing the same problem, the solution is outlined below:
def self.chart_data(start = 1.year.ago)
total_count = total_count_by_month(start)
##############################################
start = start.to_date.beginning_of_month
today = Date.today.beginning_of_month
range = (start..today).select {|d| d.day == 1}
##############################################
range.map do |month|
{
created_at: month,
total_enquiries: total_count[] || 0
}
end
end
The chart's x-axis now iterates by month.
The solution is found here.
I'm still looking for solutions on how the chart dates might display (%b %Y) as opposed to the current format of (yyyy-mm-dd).

Using scope to return results within multiple DateTime ranges in ActiveRecord

I've got a Session model that has a :created_at date and a :start_time date, both stored in the database as :time. I'm currently spitting out a bunch of results on an enormous table and allowing users to filter results by a single date and an optional range of time using scopes, like so:
class Session < ActiveRecord::Base
...
scope :filter_by_date, lambda { |date|
date = date.split(",")[0]
where(:created_at =>
DateTime.strptime(date, '%m/%d/%Y')..DateTime.strptime(date, '%m/%d/%Y').end_of_day
)
}
scope :filter_by_time, lambda { |date, time|
to = time[:to]
from = time[:from]
where(:start_time =>
DateTime.strptime("#{date} #{from[:digits]} #{from[:meridian]}", '%m/%d/%Y %r')..
DateTime.strptime("#{date} #{to[:digits]} #{to[:meridian]}", '%m/%d/%Y %r')
)
}
end
The controller looks more or less like this:
class SessionController < ApplicationController
def index
if params.include?(:date) ||
params.include?(:time) &&
( params[:time][:from][:digits].present? && params[:time][:to][:digits].present? )
i = Session.scoped
i = i.filter_by_date(params[:date]) unless params[:date].blank?
i = i.filter_by_time(params[:date], params[:time]) unless params[:time].blank? || params[:time][:from][:digits].blank? || params[:time][:to][:digits].blank?
#items = i
#items.sort_by! &params[:sort].to_sym if params[:sort].present?
else
#items = Session.find(:all, :order => :created_at)
end
end
end
I need to allow users to filter results using multiple dates. I'm receiving the params as a comma-separated list in string format, e.g. "07/12/2012,07/13/2012,07/17/2012", and need to be able to query the database for several different date ranges, and time ranges within those date ranges, and merge those results, so for example all of the sessions on 7/12, 7/13 and 7/17 between 6:30 pm and 7:30 pm.
I have been looking everywhere and have tried several different things but I can't figure out how to actually do this. Is this possible using scopes? If not what's the best way to do this?
My closest guess looks like this but it's not returning anything so I know it's wrong.
scope :filter_by_date, lambda { |date|
date = date.split(",")
date.each do |i|
where(:created_at =>
DateTime.strptime(i, '%m/%d/%Y')..DateTime.strptime(i, '%m/%d/%Y').end_of_day
)
end
}
scope :filter_by_time, lambda { |date, time|
date = date.split(",")
to = time[:to]
from = time[:from]
date.each do |i|
where(:start_time =>
DateTime.strptime("#{i} #{from[:digits]} #{from[:meridian]}", '%m/%d/%Y %r')..
DateTime.strptime("#{i} #{to[:digits]} #{to[:meridian]}", '%m/%d/%Y %r')
)
end
}
Another complication is that the start times are all stored as DateTime objects so they already include a fixed date, so if I want to return all sessions started between 6:30 pm and 7:30 pm on any date I need to figure something else out too. A third party is responsible for the data so I can't change how it's structured or stored, I just need to figure out how to do all these complex queries. Please help!
EDIT:
Here's the solution I've come up with by combining the advice of Kenichi and Chuck Vose below:
scope :filter_by_date, lambda { |dates|
clauses = []
args = []
dates.split(',').each do |date|
m, d, y = date.split '/'
b = "#{y}-#{m}-#{d} 00:00:00"
e = "#{y}-#{m}-#{d} 23:59:59"
clauses << '(created_at >= ? AND created_at <= ?)'
args.push b, e
end
where clauses.join(' OR '), *args
}
scope :filter_by_time, lambda { |times|
args = []
[times[:from], times[:to]].each do |time|
h, m, s = time[:digits].split(':')
h = (h.to_i + 12).to_s if time[:meridian] == 'pm'
h = '0' + h if h.length == 1
s = '00' if s.nil?
args.push "#{h}:#{m}:#{s}"
end
where("CAST(start_time AS TIME) >= ? AND
CAST(start_time AS TIME) <= ?", *args)
}
This solution allows me to return sessions from multiple non-consecutive dates OR return any sessions within a range of time without relying on dates at all, OR combine the two scopes to filter by non-consecutive dates and times within those dates. Yay!
An important point I overlooked is that the where statement must come last -- keeping it inside of an each loop returns nothing. Thanks to both of you for all your help! I feel smarter now.
something like:
scope :filter_by_date, lambda { |dates|
clauses = []
args = []
dates.split(',').each do |date|
m, d, y = date.split '/'
b = "#{y}-#{m}-#{d} 00:00:00"
e = "#{y}-#{m}-#{d} 23:59:59"
clauses << '(start_time >= ? AND start_time <= ?)'
args.push b, e
end
where clauses.join(' OR '), *args
}
and
scope :filter_by_time, lambda { |dates, time|
clauses = []
args = []
dates.split(',').each do |date|
m, d, y = date.split '/'
f = time[:from] # convert to '%H:%M:%S'
t = time[:to] # again, same
b = "#{y}-#{m}-#{d} #{f}"
e = "#{y}-#{m}-#{d} #{t}"
clauses << '(start_time >= ? AND start_time <= ?)'
args.push b, e
end
where clauses.join(' OR '), *args
}
So, the easy part of the question is what to do about datetimes. The nice thing about DateTimes is that they can be cast to times really easily with this:
CAST(datetime_col AS TIME)
So you can do things like:
i.where("CAST(start_time AS TIME) IN(?)", times.join(", "))
Now, the harder part, why aren't you getting any results. The first thing to try is to use i.to_sql to decide whether the scoped query looks reasonable. My guess is that when you print it out you'll find that all those where are chaining together with AND. So you're asking for objects with a date that is on 7/12, 7/13, and 7/21.
The last part here is that you've got a couple things that are concerning: sql injections and some overeager strptimes.
When you do a where you should never use #{} in the query. Even if you know where that input is coming from your coworkers may not. So make sure you're using ? like in the where I did above.
Secondly, strptime is extremely expensive in every language. You shouldn't know this, but it is. If at all possible avoid parsing dates, in this case you can probably just gsub / into - in that date and everything will be happy. MySQL expects dates in m/d/y form anyways. If you're still having trouble with it though and you really need a DateTime object you can just as easily do: Date.new(2001,2,3) without eating your cpu.

How to write a rails query that returns the count by Month?

take a standard NewsFeed model (id,user_id)
How can I query for the # of records per month in the NewsFeed model, and then exclude a few user_id's?
Results would yield:
Jan - 313
Feb - 3131
Mar - 44444
etc...
Is there a simple way to do this with rails or do you need to write a query for each month?
Thanks
In Rails 4, the way to do this is to create scopes on your model.
class NewsFeed < ActiveRecord::Base
scope :group_by_month, -> { group("date_trunc('month', created_at) ") }
scope :exclude_user_ids, -> (ids) { where("user_id is not in (?)",ids) }
end
And then you would call it like:
#counts = NewsFeed.exclude_user_ids(['1','2']).group_by_month.count
This will give you:
{2014-01-01 00:00:00 UTC=>313, 2014-02-01 00:00:00 UTC=>3131}
Then you output (haml):
- #counts.each do |m|
= "Month: #{m[0].strftime("%b")}, Count: #{m[1]}"
Which would result in:
Month: Jan, Count: 313
Month: Feb, Count: 3131
There are count and group statements available in active record
so you could do something similar to
NewsFeed.count(:group=>"date_trunc('month', created_at)",:conditions=>"user_id NOT IN (?)",[exluded_ids])
Maybe this will work:
monthly_counts = NewsFeed.select("date_trunc('month', created_at) as month, COUNT(id) as total").where("user_id NOT IN (?)",[exluded_ids]).group("month")
monthly_counts.each do |monthly_count|
puts "#{monthly_count.month} - #{monthly_count.total}"
end
http://railscasts.com/episodes/29-group-by-month
NewsFeed.where("user_id is not in (?)",[user_ids]).group_by { |t| t.created_at.beginning_of_month } => each {|month,feed| ...}
NewsFeed.select("*,MONTH(created_at) as month").where("user_id is not in (?)",[user_ids]).group("month") => ...
In Rails 5
NewsFeed.select('id').group("date_trunc('month', created_at)").count

select by nearest date

i've a performance problem with this query:
#event = Event.find_by_sql("
SELECT * FROM `IdeProNew_development`.`events`
WHERE device_id = #{session[:selected_cam_id]}
AND data_type = 'image'
AND created_at BETWEEN CONVERT('#{params[:selected_date].to_time.beginning_of_day}', DATETIME)
AND CONVERT('#{params[:selected_date].to_time.end_of_day}', DATETIME)
ORDER BY abs(CONVERT('#{params[:selected_date]}', DATETIME)- created_at) LIMIT 1
").first
i use this to select the nearest event by the "selected_date"...
it's ok but it's very slow because scan all table (it's very big) and sort by the difference between the selected date and the creation date of the record.
i try to use DATEDIFF like this:
#event = Event.find_by_sql("
SELECT * FROM `IdeProNew_development`.`events`
WHERE device_id = #{session[:selected_cam_id]}
AND data_type = 'image' AND created_at
BETWEEN CONVERT('#{params[:selected_date].to_time.beginning_of_day}', DATETIME)
AND CONVERT('#{params[:selected_date].to_time.end_of_day}', DATETIME)
ORDER BY abs(DATEDIFF(CONVERT('#{params[:selected_date]}', DATETIME), created_at)) LIMIT 1
").first`
but it's not work very well (sometimes give me a wrong result) and it's slow too.
where is my mistake? could i use some type of indexing to make this query fast?
Why don't you use active record to do this rather than an SQL query?
Something like this :
`#event = Event.where(:device_id => session[:selected_cam_id]).
where(:data_type => 'image').
where("created_at >= ? AND created_at <= ?",
params[:selected_date].to_time.beginning_of_day,
params[:selected_date].to_time.end_of_day).
order("created_at DESC").first`
I think it's more efficient.
You can Also try this
#event = Event.where(:device_id => session[:selected_cam_id])
.where(:data_type => 'image').to_a
.select{|i| i.created_at.to_time >= params[:selected_date].to_time.beginning_of_day
&& i.created_at.to_time <= params[:selected_date].to_time.end_of_day}
.sort{ |x,y| y.created_at <=> x.created_at}.first

Resources