ActiveRecord where method call optimisation - ruby-on-rails

I have a piece of code witch looks like this:
Post.all.reject {|p| p.created_at.beginning_of_month != params[:date].to_date}
Is there a method to write the same code using where method and to not get all elements?

If you want to use where, I'd go by:
# x-month being a date from your desired month.
# .. defines the range between the beginning and the end
Post.where(:created_at => x-month.beginning_of_month..x-month.end_of_month)

AFAIK, there is no database-agnostic solution to this, because you need to extract the month from the date. So, in raw SQL you would have :
date = params[:date].to_date
Post.where("MONTH(created_at) != ? AND YEAR(created_at) = ?", [date.month, date.year])
Now it is possible to cheat a bit with normalization in order to use a db-agnostic solution.
Just add some created_at_month and created_at_year columns to your model, along with this callback :
after_create :denormalize_created_at
def denormalize_created_at
assign_attributes created_at_month: created_at.month,
created_at_year: created_at.year
save validate: false
end
Now you can do:
Rails < 4 :
date = params[:date].to_date
Post
.where(Post.arel_table[:created_at_month].not_eq date.month)
.where(created_at_year: date.year)
Rails 4+ :
date = params[:date].to_date
Post.not(created_at_month: date.month).where(created_at_year: date.year)

mysql has a MONTH function to get the month of a datetime column.
Post.where("MONTH(created_at) != ?", params[:date].to_date.month)

Related

Getting number of events within a season based on date

I have a model called Event with a datetime column set by the user.
I'm trying to get a total number of events in each season (spring, summer, fall, winter).
I'm trying with something like:
Event.where('extract(month from event_date) >= ? AND extract(day from event_date) >= ? AND extract(month from event_date) < ? AND extract(day from event_date) < ?', 6, 21, 9, 21).count
The example above would return the number of events in the Summer, for example (at least in the northern hemisphere).
It doesn't seem like my example above is working, i'm getting no events returned even though there are events in that range. I think my order of operations (ands) may be messing with it. Any idea of the best method to get what I need?
Edit: actually looking at this more this will not work at all. Is there anyway select dates within a range without the year?
Edit 2: I'm trying to somehow use the answer here to help me out, but this is Ruby and not SQL.
require 'date'
class Date
def season
day_hash = month * 100 + mday
case day_hash
when 101..320 then :winter
when 321..620 then :spring
when 621..920 then :summer
when 921..1220 then :fall
when 1221..1231 then :winter
end
end
end
You can concat the month and day and query everything in between.
e.g 621..921
In SQL it would be something like
SUMMER_START = 621
SUMMER_END = 921
Event.where("concat(extract(month from event_date), extract(day from event_date)) > ? AND concat(extract(month from event_date), extract(day from event_date)") < ?,
SUMMER_START, SUMMER_END)
This can be easily made into a scope method that accepts a season (e.g 'winter'), takes the appropriate season start and end and returns the result.
This is what I ended up doing:
in lib created a file called season.rb
require 'date'
class Date
def season
day_hash = month * 100 + mday
case day_hash
when 101..320 then :winter
when 321..620 then :spring
when 621..920 then :summer
when 921..1220 then :fall
when 1221..1231 then :winter
end
end
end
in lib created a file called count_by.rb:
module Enumerable
def count_by(&block)
list = group_by(&block)
.map { |key, items| [key, items.count] }
.sort_by(&:last)
Hash[list]
end
end
Now I can get the season for any date, as well as use count_by on the model.
So then ultimately I can run:
Event.all.count_by { |r| r.event_date.season }[:spring]

Custom ActiveAdmin Filter for Date Range - Complex Logic in Method

I am trying to create a custom ActiveAdmin filter that takes date_range as a parameter. Every solution I've found has been for excessively simple model methods.
Is there a way to pass both parameters into the model ransacker method, and/or at the very least to control the order in which these parameters are passed as well as to know which one is being passed? (end_date vs. start_date -- start_date is passed first, whereas I might be able to work around this is end_date were sent first). Any alternative solution, which would not break all other filters in the application (ie, overwriting activeadmin filters to use scopes - this is one filter out of hundreds in the application) welcome as well.
Thank you!
admin/model.rb
filter :model_method_in, as: :date_range
models/model.rb
ransacker :model_method, :formatter => proc { |start_date, end_date|
Model.complicated_method(start_date, end_date)
} do |parent|
parent.table[:id]
end
...
def method_for_base_queries(end_date)
Model.long_complicated_sql_call_using_end_date
end
def complicated_method(start_date, end_date)
model_instances = method_for_base_queries(end_date)
model_instances.logic_too_complex_for_sql_using_start_date
end
Similar question, but filter/model logic was simple enough for an alternative solution that didn't require both parameters to be passed in: Custom ActiveAdmin filter for Date Range
This might help. Given your index filter
filter :model_method, as: :date_range
you can write the following in your model:
scope :model_method_gteq_datetime, -> (start_date) {
self.where('users.your_date_column >= ?', start_date)
}
scope :model_method_lteq_datetime, -> (end_date) {
# added one day since apparently the '=' is not being counted in the query,
# otherwise it will return 0 results for a query on the same day (as "greater than")
self.where('users.your_date_column <= ?', (Time.parse(end_date) + 1.day).to_date.to_s)
}
def self.ransackable_scopes(auth_object = nil)
[model_method_gteq_datetime, :model_method_lteq_datetime]
end
..._gteq_datetime and ..._lteq_datetime is how Activeadmin interprets the two dates in a custom date_range index filter (see also the corresponding url generated after adding the filter).
I've written a sample query that fits my case (given that users is a model related to the current one), since I don't know the complexity of yours.
I'm using:
Ruby 2.3.1
Rails 5.0.7
Activeadmin 1.3.0

How can I limit an existing Rails AR query by a certain time frame?

I have a dashboard(esque) view in a Rails app which is showing some data in similar ways but broken out into many time periods.
I have some code in my controller like so:
#issues_this_month = Issue.where('issues.created_at BETWEEN ? AND ?', DateTime.now.in_time_zone.beginning_of_month, DateTime.now.in_time_zone.end_of_month)
and I also want to create a variables which shows issues this year and issues all time so I have this code:
#issues_this_year = Issue.where('issues.created_at BETWEEN ? AND ?', DateTime.now.in_time_zone.beginning_of_year, DateTime.now.in_time_zone.end_of_year)
I am curious if someone can think of a good way of doing one query, and from that inferring the date ranges all while avoiding the extra queries. Should I pass the results to a helper method and do the logic there?
in the model... you can define
def date
self.created_at.to_date
end
then in the controller
start = Date.today.beginning_of_year
end = Date.today.end_of_year
#issues_this_year = Issue.where(create_at: start..end).group_by(&:date)
now you have a hash of [month_1, {issues that exist in month_1}, month_2, {issues that exist in month_2}, etc]. play with it in the console to find the proper keys... #issues_this_year.keys
How about defining a method like
def self.in_timeframe(start_time=DateTime.now.in_time_zone.beginning_of_month,
end_time=DateTime.now.in_time_zone.end_of_month)
Issue.where('issues.created_at BETWEEN ? AND ?', start_time, end_time)
end
You can now invoke this as follows:
Issue.in_timeframe # For issues in the month
Issue.in_timeframe(x,y) # For issues within x-y timeframe
If you want the data in a single query, you could do stuff like:
def self.in_timeframes(time_frames)
data = {}
times_frames.each do |time_frame|
data[time_frame[:name]] = Issue.in_timeframe(time_frame[:srtart]. time_frame[:end])
end
data
end
You can invoke the above method using:
time_frames = [{:name=>"month"},
{:name=>"x-y", :start=>x, :end=>y}]
Issue.in_timeframes(time_frames)

How to get a Date from date_select or select_date in Rails?

Using select_date gives me back a params[:my_date] with year, month and day attributes. How do get a Date object easily? I'm hoping for something like params[:my_date].to_date.
I'm happy to use date_select instead as well.
Using date_select gives you 3 separate key/value pairs for the day, month, and year respectively. So you can pass them into Date.new as parameters to create a new Date object.
An example date_select returned params for an Event model:
"event"=>
{"name"=>"Birthday",
"date(1i)"=>"2012",
"date(2i)"=>"11",
"date(3i)"=>"28"},
Then to create the new Date object:
event = params[:event]
date = Date.new event["date(1i)"].to_i, event["date(2i)"].to_i, event["date(3i)"].to_i
You may instead decide to wrap this logic in a method:
def flatten_date_array hash
%w(1 2 3).map { |e| hash["date(#{e}i)"].to_i }
end
And then call it as date = Date.new *flatten_date_array params[:event]. But this is not logic that truly belongs in a controller, so you may decide to move it elsewhere. You could even extend this onto the Date class, and call it as date = Date.new_from_hash params[:event].
Here is another one:
# view
<%= date_select('event', 'date') %>
# controller
date = Date.civil(*params[:event].sort.map(&:last).map(&:to_i))
Found at http://kevinlochner.com/use-rails-dateselect-without-an-activerecord
I use the following method, which has the following benefits:
it doesn't have to explicitly name param keys xxx(1i) through xxx(3i) (and thus could be modified to capture hour and minute simply by changing Date to DateTime); and
it extracts a date from a set of params even when those params are populated with many other key-value pairs.
params is a hash of the format { xxx(1i): '2017', xxx(2i): 12, xxx(3i): 31, ... }; date_key is the common substring xxx of the target date parameters.
def date_from_params(params, date_key)
date_keys = params.keys.select { |k| k.to_s.match?(date_key.to_s) }.sort
date_array = params.values_at(*date_keys).map(&:to_i)
Date.civil(*date_array)
end
I chose to place this as a class method of ApplicationRecord, rather than as an instance helper method of ApplicationController. My reasoning is that similar logic exists within the ActiveRecord instantiator (i.e., Model.new) to parse dates passed in from Rails forms.
Here is the another one
Date.civil(params[:event]["date(1i)"].to_i,params[:event]["date(2i)"].to_i,params[:event]["date(3i)"].to_i)
Here is another one for rails 5:
module Convert
extend ActiveSupport::Concern
included do
before_action :convert_date
end
protected
def convert_date
self.params = ActionController::Parameters.new(build_date(params.to_unsafe_h))
end
def build_date(params)
return params.map{|e| build_date(e)} if params.is_a? Array
return params unless params.is_a? Hash
params.reduce({}) do |hash, (key, value)|
if result = (/(.*)\(\di\)\z/).match(key)
params_name = result[1]
date_params = (1..3).map do |index|
params.delete("#{params_name}(#{index}i)").to_i
end
hash[params_name] = Date.civil(*date_params)
else
hash[key] = build_date(value)
end
hash
end
end
end
You need to include it to your controller or application_controller.rb:
class ApplicationController < ActionController::Base
include Convert
end
With the date_select example #joofsh's answer, here's a "one liner" I use, presuming the date field is called start_date:
ev_params = params[:event]
date = Time.zone.local(*ev_params.select {|k,v| k.to_s.index('start_date(') == 0 }.sort.map {|p| p[1].to_i})
Rails 5 + use select_date, in your view note the prefix
<%= select_date Date.today - 3.months, prefix: :start_date %>
then in your controller you simply process:
your_date = Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i)
the prefix binds the three date components into a single object.
Or simply do this:
your_date_var = Time.parse(params[:my_date])

Where is the Rails method that converts data from `datetime_select` into a DateTime object?

When I use <%= f.datetime_select :somedate %> in a form, it generates HTML like:
<select id="some_date_1i" name="somedate1(1i)"> #year
<select id="some_date_2i" name="somedate1(2i)"> #month
<select id="some_date_3i" name="somedate1(3i)"> #day
<select id="some_date_4i" name="somedate1(4i)"> #hour
<select id="some_date_5i" name="somedate1(5i)"> #minute
When that form is submitted, the somedate1(<n>i) values are received:
{"date1(1i)"=>"2011", "date1(2i)"=>"2", "date1(3i)"=>"21", "date1(4i)"=>"19", "date1(5i)"=>"25"}
How can I convert that into a DateTime object?
I could write my own method to do this, but since Rails already is able to do the conversion, I was wondering if I could call that Rails method to do it for me?
I don't know where to look for that method.
I'm ultimately trying to solve "How to handle date/times in POST parameters?" and this question is the first step in trying to find a solution to that other problem.
This conversion happens within ActiveRecord when you save your model.
You could work around it with something like this:
somedate = DateTime.new(params["date1(1i)"].to_i,
params["date1(2i)"].to_i,
params["date1(3i)"].to_i,
params["date1(4i)"].to_i,
params["date1(5i)"].to_i)
DateTime::new is an alias of DateTime::civil (ruby-doc)
The start of that code path, seems to be right about here:
https://github.com/rails/rails/blob/d90b4e2/activerecord/lib/active_record/base.rb#L1811
That was tricky to find! I hope this helps you find what you need
Hi I have added the following on the ApplicationController, and it does this conversion.
#extract a datetime object from params, useful for receiving datetime_select attributes
#out of any activemodel
def parse_datetime_params params, label, utc_or_local = :local
begin
year = params[(label.to_s + '(1i)').to_sym].to_i
month = params[(label.to_s + '(2i)').to_sym].to_i
mday = params[(label.to_s + '(3i)').to_sym].to_i
hour = (params[(label.to_s + '(4i)').to_sym] || 0).to_i
minute = (params[(label.to_s + '(5i)').to_sym] || 0).to_i
second = (params[(label.to_s + '(6i)').to_sym] || 0).to_i
return DateTime.civil_from_format(utc_or_local,year,month,mday,hour,minute,second)
rescue => e
return nil
end
end
Had to do something very similar, and ended up using this method:
def time_value(hash, field)
Time.zone.local(*(1..5).map { |i| hash["#{field}(#{i}i)"] })
end
time = time_value(params, 'start_time')
See also: TimeZone.local
Someone else here offered solution of using DateTime.new, but that won't work in Postgresql. That will save the record as is, that is, as it was inserted in form, and thus it won't save in database as utc time, if you are using "timestamp without timezone". I spent hours trying to figure out this one, and the solution was to use Time.new rather than DateTime.new:
datetime = Time.new(params["fuel_date(1i)"].to_i, params["fuel_date(2i)"].to_i,
params["fuel_date(3i)"].to_i, params["fuel_date(4i)"].to_i,
params["fuel_date(5i)"].to_i)
I had this issue with Rails 4. It turned out I forgot to add the permitted params to my controller:
def event_params
params.require(:event).permit(....., :start_time, :end_time,...)
end
This is the method I use - it returns the deleted keys as a new hash.
class Hash
def delete_by_keys(*keys)
keys.each_with_object({}) { |k, h| h[k] = delete(k) if include? k }
end
end

Resources