Rails - Building Conditions for Use in a Query - ruby-on-rails

I usually like to build a conditions hash like this below:
conditions = {}
conditions[:color] = "black"
conditions[:doors] = 4
conditions[:type] = "sedan"
Cars.find(:all, :conditions=>conditions)
But how would I add a date range into this for something like:
year >= '2011-01-01' and year < '2011-02-01'

I am assuming you are using Rails 2.3.x and year is a date column.
conditions = {}
conditions[:color] = "black"
conditions[:doors] = 4
conditions[:type] = "sedan"
# create a date range
conditions[:year] = (Date.parse("2011-01-01")...Date.parse("2011-02-01"))
Car.all(:conditions => conditions)
If you want to do even more complex queries in 2.3.x use the AR Extensions gem.
Read this article for more details.

If you're on Rails 3, why not use AREL?
Cars.where(:color => "black").
where(:doors => 4).
where(:type => "sedan").
where("year >= '2011-01-01'").
where("year < '2011-02-01'")
Btw, don't use :type as a field name. Rails uses this for STI.
On Rails 2.3, I'd just build up conditions as a String instead.

You can build a up query through relations. The query will not be executed until it needs to be evaluated. This is nice for searches where some parameters are optional.
#cars = Cars.where(:color => "black")
#cars = #cars.where(:doors => 4)
#cars = #cars.where("year >= '2011-01-01'")
#cars = #cars.where("year <= '2011-02-01'")
Or you could just merge all that together into one:
Cars.where(["color=? AND doors=? AND year >= ? AND year <= ?", "black", 4, "2011-01-01", "2011-02-01"]
UPDATE:
For Rails < 3
#cars = Cars.scoped(:conditions => {:color => "black"})
#cars = #cars.scoped(:conditions => {:doors => 4})
#cars = #cars.scoped(:conditions => "year >= '2011-01-01'")
#cars = #cars.scoped(:conditions => "year <= '2011-02-01'")
OR
Cars.all(:conditions => ["color=? AND doors=? AND year >= ? AND year <= ?", "black", 4, "2011-01-01", "2011-02-01"]

Related

Order a model with conditions in Rails (3)

Lets say there is a model called Event. I want to display important and current events first, so i have the attributes important (boolean) and enddate (date).
Now I want to get all events where important == true and where enddate >= today first, all others should be ordered by created_at.
I want to avoid doing events = important_events + not_important_events as this would return an array insted of an activerecord. Does anyone knows an elegant way to order a model in rails?
Try (for ActiveRecord 5+):
#events = Event.where(:important => true, 'enddate >= ?', Date.today).or(Event.where.not(:important => true, 'enddate >= ?', Date.today).order(:created_at => :desc))
Try this, It will return newest record first and fulfil your conditions.
important == true
enddate >= today
#events1 = Event.where(:important => true).where('enddate >= ?', Date.today).order(:created_at => :desc)
#events2 = Event.where(:important => false).where('enddate <= ?', Date.today).order(:created_at => :desc)
#events = #events1.or(#events2)
"OR" works only in ActiveRecod(5+).

rails 3 sqlite pass array in query via placeholder

How can i give an array as a placeholder without sqlite seeing as 1 value but several values that are in the array
value = Array.new
value.push(broadcast_date_from)
value.push(broadcast_date_to)
puts value #["a", "2006-01-02 00:00", "2006-01-02 23:59"]
find(:all, :order => 'broadcast_date', :conditions => ['name LIKE ? and broadcast_date >= ? and broadcast_date <= ?', name, #value ])
But i get this error:
wrong number of bind variables (1 for 3) in: name LIKE ? and broadcast_date >= ? and broadcast_date <= ?
Is there anyway to make it see 3 values in the array and not 1.
You need to add the splat operator * before you call your array:
values = ['condition for name']
values.push(broadcast_date_from)
values.push(broadcast_date_to)
find(:all, :order => 'broadcast_date', :conditions => ['name LIKE ? and broadcast_date >= ? and broadcast_date <= ?', *values ])
Small article about the splat operator: http://theplana.wordpress.com/2007/03/03/ruby-idioms-the-splat-operator/
Improvement for you: use .where() instead of .find()
First, the excellent guide about it: http://guides.rubyonrails.org/active_record_querying.html#conditions
Then, a little example to show the benefits of the where:
class User < ActiveRecord::Base
def get_posts(options = {})
str_conditions = ['user_id = ?']
args_conditions = [self.id]
if options.has_key?(:active)
str_conditions << 'active = ?'
args_conditions << options[:active]
end
if options.has_key?(:after)
str_conditions << 'created_at >= ?'
args_conditions << options[:after]
end
if options.has_key?(:limit)
Post.find(:conditions => [str_conditions.join(' OR '), *args_conditions], :limit => options[:limit])
else
Post.find(:conditions => [str_conditions.join(' OR '), *args_conditions])
end
end
Different usages:
user = User.first
user.get_posts(:active => true, :after => Date.today, :limit => 10)
user.get_posts
The same method, but using the where method (very nice for chain-scoping):
def get_posts(options = {})
scope = self.posts
scope = scope.where(active: options[:active]) if options.has_key?(:active)
scope = scope.where('created_at >= ?', options[:after]) if options.has_key?(:after)
scope = scope.limit(options[:limit]) if options.has_key?(:limit)
return scope
end
Keep in mind that you can chain scope with the .where method:
User.where(active: true).where('created_at < ?', Date.today-1.weeks).includes(:posts).where(posts: { name: "Name of a specific post" })

Better way to build multiple conditions in Rails 2.3

I'm developing with Rails 2.3.8 and looking for a better way to build find conditions.
On search page, like user search, which user sets search conditions, find conditions are depends on the condition which user have chosen, e.g age, country, zip-code.
I've wrote code below to set multiple find conditions.
# Add condition if params post.
conditions_array = []
conditions_array << ['age > ?', params[:age_over]] if params[:age_over].present?
conditions_array << ['country = ?', params[:country]] if params[:country].present?
conditions_array << ['zip_code = ?', params[:zip_code]] if params[:zip_code].present?
# Build condition
i = 0
conditions = Array.new
columns = ''
conditions_array.each do |key, val|
key = " AND #{key}" if i > 0
columns += key
item_master_conditions[i] = val
i += 1
end
conditions.unshift(columns)
# condiitons => ['age > ? AND country = ? AND zip_code = ?', params[:age], params[country], prams[:zip_code]]
#users = User.find(:all,
:conditions => conditions
)
This code works fine but it is ugly and not smart.
Is there better way to build find conditions?
Named scopes could make it a bit more readable, albeit bulkier, while still preventing SQL injection.
named_scope :age_over, lambda { |age|
if !age.blank?
{ :conditions => ['age > ?', age] }
else
{}
end
}
named_scope :country, lambda { |country|
if !country.blank?
{ :conditions => ['country = ?', age] }
else
{}
end
}
named_scope :zip_code, lambda { |zip_code|
if !zip_code.blank?
{ :conditions => ['zip_code = ?', age] }
else
{}
end
}
And then when you do your search, you can simply chain them together:
#user = User.age_over(params[:age_over]).country(params[:country]).zip_code(params[:zip_code])
I have accidentally run on your questions, and even it is old one, here is the answer:
After defining your conditions, you could use it like this:
# Add condition if params post.
conditions_array = []
conditions_array << ["age > #{params[:age_over]}"] if params[:age_over].present?
conditions_array << ["country = #{params[:country]}"] if params[:country].present?
conditions_array << ["zip_code = #{params[:zip_code]}"] if params[:zip_code].present?
conditions = conditions_array.join(" AND ")
#users = User.find(:all, :conditions => conditions) #Rails 2.3.8
#users = User.where(conditions) #Rails 3+

Are there any model analytics gems?

I'm working on allowing clients to view analytics per day, week, month, in a period of time, grouped by hours or days or months, etc... All of that is based on the created_at attribute.
Is there any gem out there that already does this? Something like:
Posts.analytics(:by => :day, :period => :this_week, :column => :created_at)
Would return:
{
'2012-06-19' => 14,
'2012-06-20' => 0, // Empty rows padding support*
'2012-06-21' => 3
}
I'm trying to make it from scratch but it seems like a lot of unecessary work if there's already a gem to do the job.
Update
I tried to make an analytics module that gets included into all models for easy analytics generation, But it's really unreliable, Sometimed i get more days than i need, and it's really messy, Could anyone collaborate and rewrite/improve on this:
# Usage:
# include Analytics::Timeline
# Model.timeline(:period => :last_24_hours, :time_by => :hour)
module Analytics
module Timeline
def self.included(base)
base.class_eval {
def self.timeline(*filters)
filters = filters[0]
period = filters[:period] || :this_week
time_by = filters[:time_by] || :days
date_column = filters[:date_column] || :created_at
# Named periods conventions
period_range = case period
when :last_12_hours
[Time.now-12.hours, Time.now]
when :last_24_hours
[Time.now-24.hours, Time.now]
when :last_7_days
[Time.now-7.days, Time.now]
when :last_30_days
[Time.now-30.days, Time.now]
when :this_week
[Time.now.beginning_of_week, Time.now.end_of_week]
when :past_week
[(Time.now - 1.week).beginning_of_week, (Time.now - 1.week).end_of_week]
when :this_month
[Time.now.beginning_of_month, Time.now.end_of_month]
when :past_month
[(Time.now-1.month).beginning_of_month, (Time.now - 1.month).end_of_month]
when :this_year
[Time.now.beginning_of_year, Time.now.end_of_year]
end
period_range = period if period.kind_of?(Array)
period_range = [period, Time.now] if period.is_a?(String)
# determine the SQL group method
group_column = case time_by
when :months
time_suffix = "-01 00:00:00"
records = where("#{table_name}.#{date_column} > ? AND #{table_name}.#{date_column} <= ?", period_range[0].to_date, period_range[1].to_date)
"DATE_FORMAT(#{table_name}.#{date_column.to_s}, '%Y-%m')"
when :days
time_suffix = " 00:00:00"
records = where("#{table_name}.#{date_column} > ? AND #{table_name}.#{date_column} <= ?", period_range[0].to_date, period_range[1].to_date)
"DATE(#{table_name}.#{date_column.to_s})"
when :hours
time_suffix = ":00:00"
records = where("#{table_name}.#{date_column} > ? AND #{table_name}.#{date_column} <= ?", period_range[0], period_range[1])
"DATE_FORMAT(#{table_name}.#{date_column.to_s}, '%Y-%m-%d %H')"
when :minutes
time_suffix = ":00"
records = where("#{table_name}.#{date_column} > ? AND #{table_name}.#{date_column} <= ?", period_range[0], period_range[1])
"DATE_FORMAT(#{table_name}.#{date_column.to_s}, '%Y-%m-%d %H:%M')"
end
# Get counts per cycle
records = records.group(group_column).select("*, count(*) AS series_count, #{group_column} AS series_time")
series = {}
# Generate placeholder series
time_table = { :days => 60*60*24, :hours => 60*60, :minutes => 60, :seconds => 0 }
if time_by == :months
ticks = 12 * (period_range[1].year - period_range[0].year) + (period_range[1].month + 1) - period_range[0].month
else
ticks = (period_range[1] - period_range[0] + 1) / time_table[time_by]
end
ticks.to_i.times do |i|
time = period_range[1]-i.send(time_by)
time = case time_by
when :minutes
time.change(:sec => 0)
when :hours
time.change(:min => 0)
when :days
time.change(:hour => 0)
when :months
time.change(:day => 1, :hour => 0)
end
series[time.to_s(:db)] = 0
end
# Merge real counts with placeholder series
to_merge = {}
records.each do |r|
to_merge[r.series_time.to_s+time_suffix] = r.series_count
end
series.merge!(to_merge)
end
}
end
end
end
The ActiveRecord statistics gem seems like it could be really useful to you.
If the statistics gem doesn't help, the admin_data gem has some analytics built in. Check out the demo. Use of the entire admin system might be overkill but you could at least try to browse the source to mimic the analytics feature.

Ruby on Rails Demographic Data

I made an site for a PS3 game and I have quite a lot of users. I am wanting to make tournaments based on peoples locations and would also like to target age groups. When users sign up the input there date of birth in the format YYYY-MM-DD. I am pulling the data and making it into a hash like so:
# Site.rb
has_many :members
def ages
ages = {"Under 18" => 0, "19-24" => 0, "25-35" => 0, "36-50" => 0, "51-69" => 0,"70+" => 0}
ages_results = self.members.count("DATE_FORMAT(dob, '%Y')", :group =>"DATE_FORMAT(dob, '%Y')")
ages_results.each do |k,v|
k = k.to_i
if k.between?(18.years.ago.strftime("%Y").to_i, 0.years.ago.strftime("%Y").to_i)
ages["Under 18"] += v
elsif k.between?(24.years.ago.strftime("%Y").to_i, 19.years.ago.strftime("%Y").to_i)
ages["19-24"] += v
elsif k.between?(35.years.ago.strftime("%Y").to_i, 25.years.ago.strftime("%Y").to_i)
ages["25-35"] += v
elsif k.between?(50.years.ago.strftime("%Y").to_i, 36.years.ago.strftime("%Y").to_i)
ages["36-50"] += v
elsif k.between?(69.years.ago.strftime("%Y").to_i, 51.years.ago.strftime("%Y").to_i)
ages["51-69"] += v
elsif k > 70.years.ago.strftime("%Y").to_i
ages["70+"] += v
end
end
ages
end
I am not a expert ruby developer and not sure if the above approach is good or it can be done a much better way, could anyone give me some advice about this?
Cheers
Couple of things to note in your code:
you seem to disregard month and day when a user was born
you convert to and from strings unnecessarilly:
50.years.ago.strftime("%Y").to_i
could be written as
50.years.ago.year
hard-coded values all over the code
I would start rewriting by finding an adequate method for calculating exact age. This one seems to be ok:
require 'date'
def age(dob)
now = Time.now.utc.to_date
now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
end
Then I would extract age table to a separate structure, to be able to change it easily, if needed, and have it visually together:
INF = 1/0.0 # convenient infinity
age_groups = {
(0..18) => 'Under 18',
(19..24) => '19-24',
(25..35) => '25-35',
(36..50) => '36-50',
(51..69) => '51-69',
(70..INF) => '70+'
}
Next you can take as the input the array of users' birth dates:
users_dobs = [Date.new(1978,4,16), Date.new(2001,6,13), Date.new(1980,10,22)]
And starting to find a suitable method to group them based on your map, say using inject:
p users_dobs.each_with_object({}) {|dob, result|
age_group = age_groups.keys.find{|ag| ag === age(dob)}
result[age_group] ||= 0
result[age_group] += 1
}
#=>{25..35=>2, 0..18=>1}
or, perhaps, using group_by
p users_dobs.group_by{|dob|
age_groups.keys.find{|ag| ag === age(dob)}
}.map{|k,v| [age_groups[k], v.count]}
#=>[["25-35", 2], ["Under 18", 1]]
etc.

Resources