Multiple scope in rails 3.0 - ruby-on-rails

I am a beginner in Rails and i have a problem with scope.
I have my class with 2 scopes :
class Event < ActiveRecord::Base
belongs_to :continent
belongs_to :event_type
scope :continent, lambda { |continent|
return if continent.blank?
composed_scope = self.scoped
composed_scope = composed_scope.where('continent_id IN ( ? )', continent).all
return composed_scope
}
scope :event_type, lambda { |eventType|
return if eventType.blank?
composed_scope = self.scoped
composed_scope = composed_scope.where('event_type_id IN ( ? )', eventType).all
return composed_scope
}
end
And in my controller i want to use this 2 scopes at the same time. I did :
def filter
#event = Event.scoped
#event = #event.continent(params[:continents]) unless params[:continents].blank?
#event = #event.event_type(params[:event_type]) unless params[:event_type].blank?
respond_with(#event)
end
But i doesn't work, I have this error :
undefined method `event_type' for #<Array:0x7f11248cca80>
It's because the first scope return an array.
How can I do to make it work?
Thank you !

You should not append '.all' in your scopes:
It transforms a chainable ActiveRelation into an Array, by triggering the SQL query.
So simply remove it.
Bonus:
Some refactoring:
scope :continent, lambda { |continent|   
self.scoped.where('continent_id IN ( ? )', continent) unless continent.blank?
}

I don't think you need .scoped in your scopes.
def filter
#event = Event.scoped
#event = #event.continent(params[:continents]) unless params[:continents].blank?
#event = #event.event_type(params[:event_type]) unless params[:event_type].blank?
respond_with(#event)
end
on the code above you already have everything returning as 'scoped'.
Plus, your scopes wouldnt need an 'unless' on them, since they will only be called if your params arent blank. So your scopes could become something like this
scope :continent, lambda { |continent|
where('continent_id IN ( ? )', continent)
}
or, on a more Rails 3 way,
scope :continent, lambda { |continent_id|
where(:continent_id => continent_id)
}
which is much shorter :)

Related

Why are the instance variables not set on the class using attr_accessor?

I tried to call an existing class using FileSearchable.new(current_user, params[:keyword]).call but I am getting the error, undefined method 'where' for nil:NilClass on my_folders = my_folders.where("name ILIKE ?", filter) unless filter.blank?
The class is below
class FileSearchable
attr_accessor :user, :my_files, :my_folders, :shared_folders, :shared_files, :filter
def initialize(user, filter)
#user = user
#my_folders = user.folders
#my_files = user.user_documents
#shared_folders = user.shared_folders
#shared_files = user.user_documents
#filter = filter
end
def apply_search_params
my_folders = my_folders.where("name ILIKE ?", filter) unless filter.blank?
end
end
I am not sure why the my_folders variable is not being set.
It's a very common gotcha. You create a local variable my_folders which shadows your accessor. Call the method explicitly:
def apply_search_params
self.my_folders = my_folders.where("name ILIKE ?", filter) unless filter.blank?
end
Or assign the instance var directly (like you do in the initializer)
def apply_search_params
#my_folders = my_folders.where("name ILIKE ?", filter) unless filter.blank?
end
But this somewhat defeats the purpose of attr_accessor. Why generate a writer and then not use it?

Virtual Column to count record

First, sorry for my English, I am totally new in ruby on rails even in very basic thing, so I hope you all can help me.
I have table Role and RoleUser
table Role have has_many relationship to RoleUser with role_id as foreign key
in table RoleUser is contain user_id, so I can call it 1 role have many users
and I want is to show all record in Role with additional field in every record called total_users,
total_users is in every record have role_id and count the user_id for every role, and put it in total_users,
I know this is must use the join table, but in rails I absolutely knew nothing about that, can you all give me a simple example how to do that.
and one more, same with case above, can I do for example Role.all and then the total_users in include in that without added it in database? is that use virtual column?
anyone have a good source of link to learn of that
I have following code in model
def with_filtering(params, holding_company_id)
order = []
if params[:sort].present?
JSON.parse(params[:sort]).each do |data|
order << "#{data['property']} #{data['direction']}"
end
end
order = 'id ASC' if order.blank?
if self.column_names.include? "holding_company_id"
string_conditions = ["holding_company_id = :holding_company_id"]
placeholder_conditions = { holding_company_id: holding_company_id.id }
else
string_conditions = []
placeholder_conditions = {}
end
if params[:filter].present?
JSON.parse(params[:filter]).each do |filter|
if filter['operation'] == 'between'
string_conditions << "#{filter['property']} >= :start_#{filter['property']} AND #{filter['property']} <= :end_#{filter['property']}"
placeholder_conditions["start_#{filter['property']}".to_sym] = filter['value1']
placeholder_conditions["end_#{filter['property']}".to_sym] = filter['value2']
elsif filter['operation'] == 'like'
string_conditions << "#{filter['property']} ilike :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = "%#{filter['value1']}%"
else
string_conditions << "#{filter['property']} = :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = filter['value1']
end
end
end
conditions = [string_conditions.join(' AND '), placeholder_conditions]
total_count = where(conditions).count
if params[:limit].blank? && params[:offset].blank?
data = where(conditions).order(order)
else
data = where(conditions).limit(params[:limit].to_i).offset(params[:offset].to_i).order(order)
end
return data, total_count.to_s
end
And I have follwing code in controllers
def crud_index(model)
data, total = Role.with_filtering(params, current_holding_company)
respond_to do |format|
format.json { render json: { data: data, total_count: total }.to_json, status: 200 }
end
end
My only purpose is to add virtual field called total_users, but i want added it in model and combine it with data in method with_filtering
If you have the models like this:
Class Role < ActiveRecord::Base
has_many :role_users
end
Class RoleUser < ActiveRecord::Base
belong_to :role
end
You could use select and joins to generate summary columns, but all the Role's attributes should be include in group.
roles = Role.select("roles.*, count(role_users.id) as total_users")
.joins(:role_users)
.group("roles.id")
Type those scripts in Rails console, Rails will generate a sql like :
SELECT roles.id, count(role_users.id) as total_users
FROM roles
INNER JOIN role_users
ON roles.id = role_users.role_id
GROUP BY roles.id
Then you can use roles.to_json to see the result. The summary column total_users can be accessed in every member of roles.
And there are many other way can match your requirement. Such as this. There is a reference of counter cache.
My suggestion is after searching, you can test those method by rails console, it's a useful tool.
UPDATE
According to OP's update and comment, seems you have more works to do.
STEP1: move with_filtering class method to controller
with_filtering handle a lot of parameter things to get conditions, it should be handled in controller instead of model. So we can transfer with_filtering into conditions and orders in controller.
class RolesController < ApplicationController
def conditions(params, holding_company_id)
if self.column_names.include? "holding_company_id"
string_conditions = ["holding_company_id = :holding_company_id"]
placeholder_conditions = { holding_company_id: holding_company_id.id }
else
string_conditions = []
placeholder_conditions = {}
end
if params[:filter].present?
JSON.parse(params[:filter]).each do |filter|
if filter['operation'] == 'between'
string_conditions << "#{filter['property']} >= :start_#{filter['property']} AND #{filter['property']} <= :end_#{filter['property']}"
placeholder_conditions["start_#{filter['property']}".to_sym] = filter['value1']
placeholder_conditions["end_#{filter['property']}".to_sym] = filter['value2']
elsif filter['operation'] == 'like'
string_conditions << "#{filter['property']} ilike :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = "%#{filter['value1']}%"
else
string_conditions << "#{filter['property']} = :#{filter['property']}"
placeholder_conditions["#{filter['property']}".to_sym] = filter['value1']
end
end
end
return [string_conditions.join(' AND '), placeholder_conditions]
end
def orders(params)
ord = []
if params[:sort].present?
JSON.parse(params[:sort]).each do |data|
ord << "#{data['property']} #{data['direction']}"
end
end
ord = 'id ASC' if ord.blank?
return ord
end
end
STEP2: update action crud_index with conditions and orders to get total_count of Roles.
class AnswersController < ApplicationController
def crud_index(model)
total = Role.where(conditions(params, current_holding_company)).count
if params[:limit].blank? && params[:offset].blank?
data = Role.where(conditions(params, current_holding_company)).order(orders(params))
else
data = Role.where(conditions(params, current_holding_company)).limit(params[:limit].to_i).offset(params[:offset].to_i).order(orders(params))
end
respond_to do |format|
format.json { render json: { data: data, total_count: total }.to_json, status: 200 }
end
end
end
STEP3: update action crud_index to get total_users by every role.
Make sure the two previous steps is pass the test.
class AnswersController < ApplicationController
def crud_index(model)
total = Role.where(conditions(params, current_holding_company)).count
if params[:limit].blank? && params[:offset].blank?
data =
Role.select(Role.column_names.map{|x| "Roles.#{x}"}.join(",") + " ,count(role_users.id) as total_users")
.joins(:role_users)
.group(Role.column_names.map{|x| "Roles.#{x}"}.join(","))
.where(conditions(params, current_holding_company))
.order(orders(params))
else
data =
Role.select(Role.column_names.map{|x| "Roles.#{x}"}.join(",") + " ,count(role_users.id) as total_users")
.joins(:role_users)
.group(Role.column_names.map{|x| "Roles.#{x}"}.join(","))
.where(conditions(params, current_holding_company))
.order(orders(params))
.limit(params[:limit].to_i)
.offset(params[:offset].to_i).order(orders(params))
end
respond_to do |format|
format.json { render json: { data: data, total_count: total }.to_json, status: 200 }
end
end
end
NOTE: step3 may need you to modify conditions and orders method to generate column_name with table_name prefix to avoid column name ambiguous error
If you can make these steps through, I suggest you can try will_paginate to simplify the part of your code about total_count ,limit and offset.
With what you explained, you could do something like this:
class Role < ActiveRecord::Base
has_many :role_users
has_many :users
def total_users
self.users.count
end
end
So you just need to call the total_users method on roles object which should get you what you desire. Something like this:
Role.first.total_users
# this will give you the total users for the first role found in your database.
Hope it helps
You might want to watch this Railscast too:
#app/models/role.rb
Class Role < ActiveRecord::Base
has_many :role_users
has_many :users, -> { select "users.*", "role_users.*", "count(role_users.user_id) as total_users" }, through: :role_users
end
This will allow you to call:
#roles = Role.find params[:id]
#roles.users.each do |role|
role.total_users
end
You can see more about how this works with a question I wrote some time ago - Using Delegate With has_many In Rails?
--
It's where I learnt about Alias columns, which Ryan Bates uses to count certain values:

Refactoring a complex filter in Rails

I'm trying to deal with a somewhat complicated query. I've read a few methods on how I might approach this but they don't really apply here because this isn't like a complicated search form (like on a vBulletin search post form), but rather a set of routes which filter both by 'category' (unreleased, popular, latest) and by 'time' (all time, last month, last week, today)
I realize the below code is very bad. My goal was only to get it working, and refactor after. Not to mention, it doesn't even truly work because it doesn't take into account BOTH category AND time, just one or the other, but I figured I would deal with that in this thread.
Also, to make this a lot clearer for this SO code paste, I excluded the .page(params[:page]).per(30) from every single line, however it needs to go on all of them.
So, does anyone know how I might go about doing this? I have mulled over it for some time and am kind of stumped
def index
case params[:category]
when "latest"
#books = Book.all.page(params[:page]).per(30)
when "downloads"
#books = Book.order('downloads DESC')
when "top100"
#books = Book.order('downloads DESC').limit(100)
when "unreleased"
#books = Book.unreleased
else
#books = Book.all.page(params[:page]).per(30)
end
case params[:time]
when "today"
#books = Book.days_old(1)
when "week"
#books = Book.days_old(7)
when "month"
#books = Book.days_old(30)
when "all-time"
#books = Book.all
else
#books = Book.all.page(params[:page]).per(30)
end
end
Routes:
# Books
get 'book/:id', to: 'books#show', as: 'book'
resources :books, only: [:index] do
get ':category/:time(/:page)', action: 'index', on: :collection
end
Move all queries to the model as scopes
class Book < ActiveRecord::Base
scope :downloads, -> { order('downloads DESC') }
scope :top100, -> { order('downloads DESC').limit(100) }
scope :unreleased, -> { unreleased }
scope :today, -> { days_old(1) }
scope :week, -> { days_old(7) }
scope :month, -> { days_old(30) }
scope :latest, -> { }
scope :all_time, -> { }
end
Create auxiliary methods to filter the params and avoid unmatching data
class BooksController < ApplicationController
private
def category_params
%w(downloads top100 unreleased).include?(params[:category]) ? params[:category].to_sym : nil
end
def time_params
%w(today week month latest all_time).include?(params[:time]) ? params[:time].to_sym : nil
end
end
Get rid of the case statement by applying the scope with the same name as the params
def index
query = Book.all
query = query.send(category_params) if category_params
query = query.send(time_params) if time_params
#books = query.page(params[:page]).per(30)
end
At four lines we're still within the boundaries of Sandi Metz' guidelines! :)
In rails you can 'chain' queries, for example
Book.where(:released => true).where(:popular => true)
is the same as
Book.where(:released => true, popular => true)
You can use this to help with your refactoring. Here is my take on it:
def index
# Start with all books, we are going to add other filters later
query = Book.scoped
# Lets handle the time filter first
query = query.where(['created_at > ?', start_date] if start_date
case params[:category]
when "latest"
query = query.order('created_at DESC')
when "downloads"
query = query.order('downloads DESC')
when "top100"
query = query.order('downloads DESC').limit(100)
when "unreleased"
query = query.where(:released => false)
end
# Finally, apply the paging
#books = query.page(params[:page]).per(30)
end
private
def start_date
case params[:time]
when "today"
1.day.ago
when "week"
7.days.ago
when "month"
1.month.ago
when "all-time"
nil
else
nil
end
end

How to refactor complicated logic in create_unique method?

I would like to simplify this complicated logic for creating unique Track object.
def self.create_unique(p)
f = Track.find :first, :conditions => ['user_id = ? AND target_id = ? AND target_type = ?', p[:user_id], p[:target_id], p[:target_type]]
x = ((p[:target_type] == 'User') and (p[:user_id] == p[:target_id]))
Track.create(p) if (!f and !x)
end
Here's a rewrite of with a few simple extract methods:
def self.create_unique(attributes)
return if exists_for_user_and_target?(attributes)
return if user_is_target?(attributes)
create(attributes)
end
def self.exists_for_user_and_target?(attributes)
exists?(attributes.slice(:user_id, :target_id, :target_type))
end
def self.user_is_target?(attributes)
attributes[:target_type] == 'User' && attributes[:user_id] == attributes[:target_id]
end
This rewrite shows my preference for small, descriptive methods to help explain intent. I also like using guard clauses in cases like create_unique; the happy path is revealed in the last line (create(attributes)), but the guards clearly describe exceptional cases. I believe my use of exists? in exists_for_user_and_target? could be a good replacement for find :first, though it assumes Rails 3.
You could also consider using uniqueness active model validation instead.
##keys = [:user_id, :target_id, :target_type]
def self.create_unique(p)
return if Track.find :first, :conditions => [
##keys.map{|k| "#{k} = ?"}.join(" and "),
*##keys.map{|k| p[k]}
]
return if p[##keys[0]] == p[##keys[1]]
return if p[##keys[2]] == "User"
Track.create(p)
end

Is it possible to have a scope with optional arguments?

Is it possible to write a scope with optional arguments so that i can call the scope with and without arguments?
Something like:
scope :with_optional_args, lambda { |arg|
where("table.name = ?", arg)
}
Model.with_optional_args('foo')
Model.with_optional_args
I can check in the lambda block if an arg is given (like described by Unixmonkey) but on calling the scope without an argument i got an ArgumentError: wrong number of arguments (0 for 1)
Ruby 1.9 extended blocks to have the same features as methods do (default values are among them):
scope :cheap, lambda{|max_price=20.0| where("price < ?", max_price)}
Call:
Model.cheap
Model.cheap(15)
Yes. Just use a * like you would in a method.
scope :print_args, lambda {|*args|
puts args
}
I used scope :name, ->(arg1, arg2 = value) { ... } a few weeks ago, it worked well, if my memory's correct. To use with ruby 1.9+
You can conditionally modify your scope based on a given argument.
scope :random, ->(num = nil){ num ? order('RANDOM()').limit(num) : order('RANDOM()') }
Usage:
Advertisement.random # => returns all records randomized
Advertisement.random(1) # => returns 1 random record
Or, you can provide a default value.
scope :random, ->(num = 1000){ order('RANDOM()').limit(num) }
Usage:
Product.random # => returns 1,000 random products
Product.random(5) # => returns 5 random products
NOTE: The syntax shown for RANDOM() is specific to Postgres. The syntax shown is Rails 4.
Just wanted to let you know that according to the guide, the recommended way for passing arguments to scopes is to use a class method, like this:
class Post < ActiveRecord::Base
def self.1_week_before(time)
where("created_at < ?", time)
end
end
This can give a cleaner approach.
Certainly.
scope :with_optional_args, Proc.new { |arg|
if arg.present?
where("table.name = ?", arg)
end
}
Use the *
scope :with_optional_args, -> { |*arg| where("table.name = ?", arg) }
You can use Object#then (or Object#yield_self, they are synonyms) for this. For instance:
scope :cancelled, -> (cancelled_at_range = nil) { joins(:subscriptions).merge(Subscription.cancelled).then {|relation| cancelled_at_range.present? ? relation.where(subscriptions: { ends_at: cancelled_at_range }) : relation } }

Resources