is it possible to have conditional default scope in rails? - ruby-on-rails

I am on rails 3.2.21, ruby version is 2.0
My requirement is to have role based conditional default scope for a particular model. eg
consider role variable as an attribute of logged in user
if role == 'xyz'
default_scope where(is_active: false)
elsif role == 'abc'
default_scope where(is_active: true)
end

Nothing is impossible in programming.
Using default_scope is a bad idea in general (lots of articles are written on the topic).
If you insist on using current user's atribute you can pass it as an argument to scope:
scope :based_on_role, lambda { |role|
if role == 'xyz'
where(is_active: false)
elsif role == 'abc'
where(is_active: true)
end
}
And then use it as follows:
Model.based_on_role(current_user.role)
Sidenote: Rails 3.2.x - seriously?...

default_scope where(
case role
when 'xyz' then { is_active: false }
when 'abc' then { is_active: true }
else '1 = 1'
end
)
Also, please read the answer by Andrey Deineko, specifically the part about default scopes usage.

Related

Rails CanCanCan & Defining Abilities

I am trying to make an app in Rails 4.
I am using CanCanCan for permissions and Role_Model for roles management.
In my ability.rb, I have defined student abilities as:
elsif user.try(:profile).present? && user.profile.has_role?(:student)
student_abilities
and then:
def student_abilities
can :read, Project.visible.current.available
In my project.rb I have defined scopes as:
scope :visible, lambda { joins(:sweep => :disclosure).where('disclosures.allusers' => 'true')
.joins(:sweep => :finalise).where('finalises.draft' => 'false') }
scope :current, lambda { where('project.start_date >= ?', Date.today)}
scope :available, lambda { where('closed =', 'false')}
When I try to start the server and generate a view, I get this error:
NoMethodError at /project_invitations
undefined method `available' for #<Project::ActiveRecord_Relation:0x007fdde41f2ee8>
When I try removing available from the end of the ability, so that its just:
can :read, Project.visible.current
I get this error:
entry for table "project"
LINE 1: ..." = 'true' AND "finalises"."draft" = 'false' AND (project.st...
^
I don't know why it won't let me read the end of the error message.
Can anyone see what I've done wrong?
Check the table name. Is it really called "project", not "projects"?
The way you describe scopes is a bit weird. E.g. instead of where('closed
=', 'false') I would describe it as where(closed: false), minimizing the number of SQL-aware fragments

Argument Error: The scope body needs to be callable

I'm working through the 'Ruby On Rails 3 Essential Training' and have received a problem when using name scopes. When finding records and using queries withing the Rails console everything went smoothly until I tried to use a name scope in my subject.rb file. This is my code in the subject.rb file.
Class Subject < ActiveRecord::Base
scope :visible, where(:visible => true)
end
I saved the .rb file and restarted my Rails console but when I run from my rails console:
subjects = Subject.visible
I get: ArgumentError: The scope body needs to be callable.
Does anyone know why I'm getting this error.
The scope's body needs to be wrapped in something callable like a Proc or Lambda:
scope :visible, -> {
where(:visible => true)
}
The reason for this is that it ensures the contents of the block is evaluated each time the scope is used.
I got the same error , while before my solution I had a space between where and ( like below
scope :registered , -> { where ( place_id: :place_id , is_registered: :true ) }
after i removed the space between where and ( like below i made my page working
scope :registered , -> { where( place_id: :place_id , is_registered: :true ) }
Yes, indeed, this is rails 4 way of calling scopes. You'd need to change it, if you're upgrading to Rails 4 from Rails 3.
What you are using: scope :visible, where(:visible => true) goes for eager loading, and has been deprecated in Rails 4.
scope :visible, where(:visible => true)
This line of code gets evaluated when the particular class is loaded, and not at the time, this very scope is called.
There are a few cases when this thing does matter, like:
scope :future, where('published_at > ?', Time.now)
scope :future, -> { where('published_at > ?', Time.now) }
In first case, ? will be replaced with the very time the class would have been loaded, but the second & correct case, that time will be used at which the scope would have been called on the class.

With a has_many relationship, in the returned collection, include? does not recognize equality

In one of my models I have defined equality to also work with strings and symbols. A role is equal to another role (or string or symbol), if its name attribute is the same:
class Role
def == other
other_name = case other
when Role then other.name
when String, Symbol then other.to_s
end
name == other_name
end
end
The equality checking works correct:
role = Role.create name: 'admin'
role == 'admin' # => true
role == :admin # => true
But when I use the Role model in a has_many relationship, in the collection I get, include? does not recognized this equality:
user = User.create
user.roles << role
User.roles.include? role # => true
User.roles.include? 'admin' # => false
User.roles.include? :admin # => false
In order to make this work, I have to explicitly convert this to an array:
User.roles.to_a.include? 'admin' # => true
User.roles.to_a.include? :admin # => true
So apparently Rails overrides the include? method in the array returned by user.roles. This sucks and is contrary to rubys specification of Enumerable#include? (which explicitly states, that "Equailty is tested using =="). This is not true for the array I get from user.roles. == is never even called.
Where is this modified behavior of include? specified?
Is there another way to test for inclusion that I missed? Or do I have to use to_a or an actual instance of Role everytime?
You are not implementing your equality operator correctly. It should be:
def == other
other_name = case other
when Role then other.name
when String, Symbol then other.to_s
end
name == other_name
end

How do I create a rails scope with an or or a not?

I have a two scopes in my user model:
scope :hard_deactivated, where(:hard_deactivated => true)
scope :soft_deactivated, where(:soft_deactivated => true)
So far so good
OR
I want to create a scope :deactivated, which will include all users where hard_deactivated is true OR soft deactivated is true. Obviously I could just do this:
scope :deactivated, where("hard_deactivated = ? or soft_deactivated = ?", true, true)
but this does not feel very dry.
NOT
Also I would like to create an inverse scope :not_hard_deactivated. I could do this:
scope :not_hard_deactivated, where(:hard_deactivated => false)
but again, this feels bad, especially if my scope becomes more complex. There should be some way or warpping the SQL generated by the previous scope in a not clause.
Use an arel table:
hard_deactivated_true = arel_table[:hard_deactivated].eq(true)
soft_deactivated_true = arel_table[:soft_deactivated].eq(true)
scope :deactivated, where(hard_deactivated_true.and(soft_deactivated_true))
scope :not_hard_deactivated, where(hard_deactivated_true.not)
See: Is it possible to invert a named scope in Rails3?
For the "NOT" part, you can do something like this:
extend ScopeUtils
positive_and_negative_scopes :deactivated do |value|
where(:hard_deactivated => value)
end
And implement this method in a separate module:
module ScopeUtils
def positive_and_negative_scopes(name)
[true, false].each do |filter_value|
prefix = ("not_" if filter_value == false)
scope :"#{prefix}#{name}", yield(filter_value)
end
end
end
Regarding the "OR" case, you might be something similar, depending on what your recurring pattern is. In the simple example above it's not worth it, as doesn't help readability.
scopes_with_adjectives_and_negatives :deactivated, [:soft, :hard]
module ScopeUtils
def scopes_with_adjectives_and_negatives(name, kinds)
kinds.each do |kind|
positive_and_negative_scopes name do |filter_value|
where("#{kind}_#{name}" => filter_value)
end
end
scope :"#{name}", where(kinds.map{|kind| "#{kind}_#{name} = ?"}.join(" OR "), true, true)
scope :"not_#{name}", where(kinds.map{|kind| "#{kind}_#{name} = ?"}.join(" AND "), false, false)
end
end
You should use sql snippet in where method (like in your second example), or more 'sugar' gems like squeel

Ransack: How to use existing scope?

Converting a Rails 2 application to Rails 3, I have to replace the gem searchlogic. Now, using Rails 3.2.8 with the gem Ransack I want to build a search form which uses an existing scope. Example:
class Post < ActiveRecord::Base
scope :year, lambda { |year|
where("posts.date BETWEEN '#{year}-01-01' AND '#{year}-12-31'")
}
end
So far as I know, this can be achieved by defining a custom ransacker. Sadly, I don't find any documentation about this. I tried this in the Postclass:
ransacker :year,
:formatter => proc {|v|
year(v)
}
But this does not work:
Post.ransack(:year_eq => 2012).result.to_sql
=> TypeError: Cannot visit ActiveRecord::Relation
I tried some variations of the ransacker declaration, but none of them work. I Need some help...
UPDATE: The scope above is just on example. I'm looking for a way to use every single existing scope within Ransack. In MetaSearch, the predecessor of Ransack, there is a feature called search_methods for using scopes. Ransack has no support for this out of the box yet.
ransack supports it out of the box after merging https://github.com/activerecord-hackery/ransack/pull/390 . you should declare ransakable_scopes method to add scopes visible for ransack.
From manual
Continuing on from the preceding section, searching by scopes requires defining a whitelist of ransackable_scopes on the model class. The whitelist should be an array of symbols. By default, all class methods (e.g. scopes) are ignored. Scopes will be applied for matching true values, or for given values if the scope accepts a value:
class Employee < ActiveRecord::Base
scope :activated, ->(boolean = true) { where(active: boolean) }
scope :salary_gt, ->(amount) { where('salary > ?', amount) }
# Scopes are just syntactical sugar for class methods, which may also be used:
def self.hired_since(date)
where('start_date >= ?', date)
end
private
def self.ransackable_scopes(auth_object = nil)
if auth_object.try(:admin?)
# allow admin users access to all three methods
%i(activated hired_since salary_gt)
else
# allow other users to search on `activated` and `hired_since` only
%i(activated hired_since)
end
end
end
Employee.ransack({ activated: true, hired_since: '2013-01-01' })
Employee.ransack({ salary_gt: 100_000 }, { auth_object: current_user })
Ransack let's you create custom predicates for this, unfortunately the documentation leaves room for improvement however checkout: https://github.com/ernie/ransack/wiki/Custom-Predicates
Also I believe the problem you're trying to tackle is up on their issue tracker. There's a good discussion going on there: https://github.com/ernie/ransack/issues/34
I wrote a gem called siphon which helps you translate parameters into activerelation scopes. Combining it with ransack can achieves this.
You can read full explanation here. Meanwhile here's the gist of it
The View
= form_for #product_search, url: "/admin/products", method: 'GET' do |f|
= f.label "has_orders"
= f.select :has_orders, [true, false], include_blank: true
-#
-# And the ransack part is right here...
-#
= f.fields_for #product_search.q, as: :q do |ransack|
= ransack.select :category_id_eq, Category.grouped_options
```
ok so now params[:product_search] holds the scopes and params[:product_search][:q] has the ransack goodness. We need to find a way, now, to distribute that data to the form object. So first let ProductSearch swallow it up in the controller:
The Controller
# products_controller.rb
def index
#product_search = ProductSearch.new(params[:product_search])
#products ||= #product_formobject.result.page(params[:page])
end
The Form Object
# product_search.rb
class ProductSearch
include Virtus.model
include ActiveModel::Model
# These are Product.scopes for the siphon part
attribute :has_orders, Boolean
attribute :sort_by, String
# The q attribute is holding the ransack object
attr_accessor :q
def initialize(params = {})
#params = params || {}
super
#q = Product.search( #params.fetch("q") { Hash.new } )
end
# siphon takes self since its the formobject
def siphoned
Siphon::Base.new(Product.scoped).scope( self )
end
# and here we merge everything
def result
Product.scoped.merge(q.result).merge(siphoned)
end
end

Resources