Rails: Making this query database-agnostic...? - ruby-on-rails

I have these two lines in my model, written for PostgreSQL:
named_scope :by_month, lambda { |month| { :conditions => ["EXTRACT(MONTH FROM recorded_on) = ?", month] }}
named_scope :by_year, lambda { |year| { :conditions => ["EXTRACT(YEAR FROM recorded_on) = ?", year] }}
I'm running PostgreSQL in production, but I'm developing with SQLite3. How can I write those lines in a way that is database-agnostic?
Btw, "recorded_on" is formed from the following:
Model.recorded_on = Time.parse("Fri, 01 May 2009 08:42:23 -0400")

Okay, found out there's a better way (thanks to this article):
Basically do nothing in the model!
date = Date.new(year, month, 1)
Model.find(:all, :conditions => { :recorded_on => date..date.end_of_month })

BETWEEN should be pretty universal: how about something like this:
named_scope :by_month, lambda { |month|
d1 = Date.new(2009, month, 1)
d2 = d1.end_of_month
{ :conditions => ['recorded_on between ? and ?', d1, d2]}
}
named_scope :by_year, lambda { |year|
d1 = Date.new(year, 1, 1)
d2 = d1.end_of_year
{ :conditions => ['recorded_on between ? and ?', d1, d2]}
}
If you have times, you'd need to get a little smarter with the hours, minutes and seconds.

Related

Rails how to group by date and count records?

In my model I have:
start = 3.weeks.ago
kliks = where(created_at: start.beginning_of_day..Time.zone.now)
kliks = kliks.group("date(created_at)")
How do I count all the records for each day as total?
I believe that this should work:
kliks = count(
:group => 'date(created_at)',
:conditions => { :created_at => start.beginning_of_day..Time.zone.now }
)
This will return you a hash mapping dates to their count.

Calculate Month Statistics

I have a donations table where I'm trying to calculate the total amount for each month. For months without without any donations, I'd like the result to return 0.
Here's my current query:
Donation.calculate(:sum, :amount, :conditions => {
:created_at => (Time.now.prev_year.all_year) },
:order => "EXTRACT(month FROM created_at)",
:group => ["EXTRACT(month FROM created_at)"])
which returns:
{7=>220392, 8=>334210, 9=>475188, 10=>323661, 11=>307689, 12=>439889}
Any ideas how to grab the empty months?
Normally you'd left join to a calendar table (or generate_series in PostgreSQL) to get the missing months but the easiest thing with Rails would be to merge your results into a Hash of zeroes; something like this:
class Donation
def self.by_month
h = Donation.calculate(:sum, :amount, :conditions => {
:created_at => (Time.now.prev_year.all_year) },
:order => "EXTRACT(month FROM created_at)",
:group => ["EXTRACT(month FROM created_at)"])
Hash[(1..12).map { |month| [ month, 0 ] }].merge(h)
end
end
then just call the class method, h = Donation.by_month, to get your results.
In addition to mu is too short answer, in Rails 3.2.12 did not work for me, ActiveRecord returns the keys as strings:
h = Donation.calculate(:sum, :amount, :conditions => {
:created_at => (Time.now.prev_year.all_year) },
:order => "EXTRACT(month FROM created_at)",
:group => ["EXTRACT(month FROM created_at)"])
Which returns:
{"7"=>220392, "8"=>334210, "9"=>475188, "10"=>323661, "11"=>307689, "12"=>439889}
So when I merge the hash with zeros:
{1=>0, 2=>0, 3=>0, 4=>0, 5=>0, 6=>0, 7=>0, 8=>0, 9=>0, 10=>0, 11=>0, 12=>0, "7"=>220392, "8"=>334210, "9"=>475188, "10"=>323661, "11"=>307689, "12"=>439889}
The little fix (to_s):
Hash[(1..12).map { |month| [ month.to_s, 0 ] }].merge(h)

rails data aggregation

I have to create a hash of the form h[:bill] => ["Billy", "NA", 20, "PROJ_A"] by login where 20 is the cumulative number of hours reported by the login for all task transactions returned by the query where each login has multiple reported transactions. Did I do this in a bad way or this seems alright.
h = Hash.new
Task.find_each(:include => [:user], :joins => :user, :conditions => ["from_date >= ? AND from_date <= ? AND category = ?", Date.today - 30, Date.today + 30, 'PROJ1']) do |t|
h[t.login.intern] = [t.user.name, 'NA', h[t.login.intern].nil? ? (t.hrs_per_day * t.num_days) : h[t.login.intern][2] + (t.hrs_day * t.workdays), t.category]
end
Also if I have to aggregate this data not just by login but login and category how do I accomplish this?
thanks,
ash
I would make this
h[:bill] => ["Billy", "NA", 20, "PROJ_A"]
a hash like so
{ :user => t.user.name, :your_key_name => 'NA', :cumulative_hours => 20, :category => 'PROJ_A' }
so the values are accessible with keys instead of element indexes which becomes a bit hard to see when you are not iterating through a array
To access the data by user and category you can do some thing like this
user_hash = {}
Task.find_each(:include => [:user], :joins => :user, :conditions => ["from_date >= ? AND from_date <= ? AND category = ?", Date.today - 30, Date.today + 30, 'PROJ1']) do |task|
user_hash[task.login.intern] ||= {}
user_hash[task.login.intern][task.category] = { :user => task.user.name, :your_key_name => 'NA', :cumulative_hours => cumulative_hours(user_hash, task), :category => task.category }
end
def cumulative_hours(user_hash, task)
if user_hash[task.login.intern] && user_hash[task.login.intern][task.category]
return user_hash[task.login.intern][task.category][:cumulative_hours] + (task.hrs_day * task.workdays)
else
return task.hrs_per_day * task.num_days
end
end
For readability reasons I have added meaningful variable names and also created a method to calculate cumulative_hours to keep the code clear, separate code concern and to follow Single Responsibility Principle.

Rails, working with named_scope

I need to select some dynamic price ranges submitted from a search form. How should I approach this with scopes? I am looking for something like this
Painting.price_range(['1..500', '2000..5000'])
SELECT * FROM paintings WHERE price BETWEEN 1..500 **OR** BETWEEN 2000..5000 etc.
Best regards.
Asbjørn Morell.
named_scope :price_range, :conditions => ["(price BETWEEN 1 AND 500) OR (price BETWEEN 2000 AND 5000)"]
OR
named_scope :price_range, :conditions => ["(price ?) OR (price ?)", (1..500).to_s(:db), (2000..5000).to_s(:db)]
Dynamic
named_scope :price_between, lambda { |from, to| { :conditions => ['price > ? AND price <= ?', from, to] } }
named_scope :price_between, lambda { |from, to| { :conditions => ['price BETWEEN ? AND ?', from, to] } }
->
MyModel.price_between(1,100)
You'll need to use a lambda on the named_scope. The following should work:
named_scope :price_range, lambda { |ranges|
{
:conditions => ["(" +
ranges.collect {"price between ? and ?"}.join(" or ") +
")"] +
ranges.collect {|r| [r.min, r.max]}.flatten
}
}
The first ranges.collect creates as many "between ? and ?" checks as you have ranges and then the second ranges.collect flattens out the ranges and adds them as values to be sanitized into the conditions. I've stuck brackets round the ors just to be on the safe side.

Filtering ActiveRecord queries in rails

I'm used to Django where you can run multiple filter methods on querysets, ie Item.all.filter(foo="bar").filter(something="else").
The however this is not so easy to do in Rails. Item.find(:all, :conditions => ["foo = :foo", { :foo = bar }]) returns an array meaning this will not work:
Item.find(:all, :conditions => ["foo = :foo", { :foo = 'bar' }]).find(:all, :conditions => ["something = :something", { :something = 'else' }])
So I figured the best way to "stack" filters is to modify the conditions array and then run the query.
So I came up with this function:
def combine(array1,array2)
conditions = []
conditions[0] = (array1[0]+" AND "+array2[0]).to_s
conditions[1] = {}
conditions[1].merge!(array1[1])
conditions[1].merge!(array2[1])
return conditions
end
Usage:
array1 = ["foo = :foo", { :foo = 'bar' }]
array2 = ["something = :something", { :something = 'else' }]
conditions = combine(array1,array2)
items = Item.find(:all, :conditions => conditions)
This has worked pretty well. However I want to be able to combine an arbitrary number of arrays, or basically shorthand for writing:
conditions = combine(combine(array1,array2),array3)
Can anyone help with this? Thanks in advance.
What you want are named scopes:
class Item < ActiveRecord::Base
named_scope :by_author, lambda {|author| {:conditions => {:author_id => author.id}}}
named_scope :since, lambda {|timestamp| {:conditions => {:created_at => (timestamp .. Time.now.utc)}}}
named_scope :archived, :conditions => "archived_at IS NOT NULL"
named_scope :active, :conditions => {:archived_at => nil}
end
In your controllers, use like this:
class ItemsController < ApplicationController
def index
#items = Item.by_author(current_user).since(2.weeks.ago)
#items = params[:archived] == "1" ? #items.archived : #items.active
end
end
The returned object is a proxy and the SQL query will not be run until you actually start doing something real with the collection, such as iterating (for display) or when you call Enumerable methods on the proxy.
I wouldn't do it like you proposed.
Since find return an array, you can use array methods to filter it, on example:
Item.find(:all).select {|i| i.foo == bar }.select {|i| i.whatever > 23 }...
You can also achive what you want with named scopes.
You can take a look at Searchlogic. It makes it easier to use conditions on
ActiveRecord sets, and even on Arrays.
Hope it helps.
You can (or at least used to be able to) filter like so in Rails:
find(:all, :conditions => { :foo => 'foo', :bar => 'bar' })
where :foo and :bar are field names in the active record. Seems like all you need to do is pass in a hash of :field_name => value pairs.

Resources