has_scope-0.8.0/0000755000004100000410000000000014021076126013512 5ustar www-datawww-datahas_scope-0.8.0/test/0000755000004100000410000000000014021076126014471 5ustar www-datawww-datahas_scope-0.8.0/test/has_scope_test.rb0000644000004100000410000003676614021076126020043 0ustar www-datawww-datarequire 'test_helper' HasScope::ALLOWED_TYPES[:date] = [[String], -> v { Date.parse(v) rescue nil }] class Tree; end class TreesController < ApplicationController has_scope :color, unless: :show_all_colors? has_scope :only_tall, type: :boolean, only: :index, if: :restrict_to_only_tall_trees? has_scope :shadown_range, default: 10, except: [ :index, :show, :new ] has_scope :root_type, as: :root, allow_blank: true has_scope :planted_before, default: proc { Date.today } has_scope :planted_after, type: :date has_scope :calculate_height, default: proc { |c| c.session[:height] || 20 }, only: :new has_scope :paginate, type: :hash has_scope :paginate_blank, type: :hash, allow_blank: true has_scope :paginate_default, type: :hash, default: { page: 1, per_page: 10 }, only: :edit has_scope :args_paginate, type: :hash, using: [:page, :per_page] has_scope :args_paginate_blank, using: [:page, :per_page], allow_blank: true has_scope :args_paginate_default, using: [:page, :per_page], default: { page: 1, per_page: 10 }, only: :edit has_scope :categories, type: :array has_scope :title, in: :q has_scope :content, in: :q has_scope :metadata, in: :q has_scope :metadata_blank, in: :q, allow_blank: true has_scope :metadata_default, in: :q, default: "default", only: :edit has_scope :conifer, type: :boolean, allow_blank: true has_scope :eval_plant, if: "params[:eval_plant].present?", unless: "params[:skip_eval_plant].present?" has_scope :proc_plant, if: -> c { c.params[:proc_plant].present? }, unless: -> c { c.params[:skip_proc_plant].present? } has_scope :only_short, type: :boolean do |controller, scope| scope.only_really_short!(controller.object_id) end has_scope :by_category do |controller, scope, value| scope.by_given_category(controller.object_id, value + "_id") end def index @trees = apply_scopes(Tree).all end def new @tree = apply_scopes(Tree).new end def show @tree = apply_scopes(Tree).find(params[:id]) end alias :edit :show protected # Silence deprecations in the test suite, except for the actual deprecated String if/unless options. # TODO: remove with the deprecation. def apply_scopes(*) if params[:eval_plant] super else ActiveSupport::Deprecation.silence { super } end end def restrict_to_only_tall_trees? true end def show_all_colors? false end def default_render render body: action_name end end class BonsaisController < TreesController has_scope :categories, if: :categories? protected def categories? false end end class HasScopeTest < ActionController::TestCase tests TreesController def test_boolean_scope_is_called_when_boolean_param_is_true Tree.expects(:only_tall).with().returns(Tree).in_sequence Tree.expects(:all).returns([mock_tree]).in_sequence get :index, params: { only_tall: 'true' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ only_tall: true }, current_scopes) end def test_boolean_scope_is_not_called_when_boolean_param_is_false Tree.expects(:only_tall).never Tree.expects(:all).returns([mock_tree]) get :index, params: { only_tall: 'false' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_boolean_scope_with_allow_blank_is_called_when_boolean_param_is_true Tree.expects(:conifer).with(true).returns(Tree).in_sequence Tree.expects(:all).returns([mock_tree]).in_sequence get :index, params: { conifer: 'true' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ conifer: true }, current_scopes) end def test_boolean_scope_with_allow_blank_is_called_when_boolean_param_is_false Tree.expects(:conifer).with(false).returns(Tree).in_sequence Tree.expects(:all).returns([mock_tree]).in_sequence get :index, params: { conifer: 'not_true' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ conifer: false }, current_scopes) end def test_boolean_scope_with_allow_blank_is_not_called_when_boolean_param_is_not_present Tree.expects(:conifer).never Tree.expects(:all).returns([mock_tree]) get :index assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_scope_is_called_only_on_index Tree.expects(:only_tall).never Tree.expects(:find).with('42').returns(mock_tree) get :show, params: { only_tall: 'true', id: '42' } assert_equal(mock_tree, assigns(:@tree)) assert_equal({ }, current_scopes) end def test_scope_is_skipped_when_if_option_is_false @controller.stubs(:restrict_to_only_tall_trees?).returns(false) Tree.expects(:only_tall).never Tree.expects(:all).returns([mock_tree]) get :index, params: { only_tall: 'true' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_scope_is_skipped_when_unless_option_is_true @controller.stubs(:show_all_colors?).returns(true) Tree.expects(:color).never Tree.expects(:all).returns([mock_tree]) get :index, params: { color: 'blue' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_scope_with_eval_string_if_and_unless_options_is_deprecated Tree.expects(:eval_plant).with('value').returns(Tree) Tree.expects(:all).returns([mock_tree]) assert_deprecated(/Passing a string to determine if the scope should be applied is deprecated/) do get :index, params: { eval_plant: 'value', skip_eval_plant: nil } end assert_equal([mock_tree], assigns(:@trees)) assert_equal({ eval_plant: 'value' }, current_scopes) end def test_scope_with_proc_if_and_unless_options Tree.expects(:proc_plant).with('value').returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { proc_plant: 'value', skip_proc_plant: nil } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ proc_plant: 'value' }, current_scopes) end def test_scope_is_called_except_on_index Tree.expects(:shadown_range).never Tree.expects(:all).returns([mock_tree]) get :index, params: { shadown_range: 20 } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_scope_is_called_with_arguments Tree.expects(:color).with('blue').returns(Tree).in_sequence Tree.expects(:all).returns([mock_tree]).in_sequence get :index, params: { color: 'blue' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ color: 'blue' }, current_scopes) end def test_scope_is_not_called_if_blank Tree.expects(:color).never Tree.expects(:all).returns([mock_tree]).in_sequence get :index, params: { color: '' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_scope_is_called_when_blank_if_allow_blank_is_given Tree.expects(:root_type).with('').returns(Tree) Tree.expects(:all).returns([mock_tree]).in_sequence get :index, params: { root: '' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ root: '' }, current_scopes) end def test_multiple_scopes_are_called Tree.expects(:only_tall).with().returns(Tree) Tree.expects(:color).with('blue').returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { color: 'blue', only_tall: 'true' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ color: 'blue', only_tall: true }, current_scopes) end def test_scope_of_type_hash hash = { "page" => "1", "per_page" => "10" } Tree.expects(:paginate).with(hash).returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { paginate: hash } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ paginate: hash }, current_scopes) end def test_scope_of_type_hash_with_using hash = { "page" => "1", "per_page" => "10" } Tree.expects(:args_paginate).with("1", "10").returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { args_paginate: hash } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ args_paginate: hash }, current_scopes) end def test_hash_with_blank_values_is_ignored hash = { "page" => "", "per_page" => "" } Tree.expects(:paginate).never Tree.expects(:all).returns([mock_tree]) get :index, params: { paginate: hash } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_hash_with_blank_values_and_allow_blank_is_called hash = { "page" => "", "per_page" => "" } Tree.expects(:paginate_blank).with({}).returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { paginate_blank: hash } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ paginate_blank: {} }, current_scopes) end def test_hash_with_using_and_blank_values_and_allow_blank_is_called hash = { "page" => "", "per_page" => "" } Tree.expects(:args_paginate_blank).with(nil, nil).returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { args_paginate_blank: hash } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ args_paginate_blank: {} }, current_scopes) end def test_nested_hash_with_blank_values_is_ignored hash = { "parent" => { "children" => "" } } Tree.expects(:paginate).never Tree.expects(:all).returns([mock_tree]) get :index, params: { paginate: hash } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_nested_blank_array_param_is_ignored hash = { "parent" => [""] } Tree.expects(:paginate).never Tree.expects(:all).returns([mock_tree]) get :index, params: { paginate: hash } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_scope_of_type_array array = %w(book kitchen sport) Tree.expects(:categories).with(array).returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { categories: array } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ categories: array }, current_scopes) end def test_array_of_blank_values_is_ignored Tree.expects(:categories).never Tree.expects(:all).returns([mock_tree]) get :index, params: { categories: [""] } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_scope_of_invalid_type_silently_fails Tree.expects(:all).returns([mock_tree]) get :index, params: { paginate: "1" } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ }, current_scopes) end def test_scope_is_called_with_default_value Tree.expects(:shadown_range).with(10).returns(Tree).in_sequence Tree.expects(:paginate_default).with('page' => 1, 'per_page' => 10).returns(Tree).in_sequence Tree.expects(:args_paginate_default).with(1, 10).returns(Tree).in_sequence Tree.expects(:metadata_default).with('default').returns(Tree).in_sequence Tree.expects(:find).with('42').returns(mock_tree).in_sequence get :edit, params: { id: '42' } assert_equal(mock_tree, assigns(:@tree)) assert_equal({ shadown_range: 10, paginate_default: { 'page' => 1, 'per_page' => 10 }, args_paginate_default: { 'page' => 1, 'per_page' => 10 }, q: { 'metadata_default' => 'default' } }, current_scopes) end def test_default_scope_value_can_be_overwritten Tree.expects(:shadown_range).with('20').returns(Tree).in_sequence Tree.expects(:paginate_default).with('page' => '2', 'per_page' => '20').returns(Tree).in_sequence Tree.expects(:args_paginate_default).with('3', '15').returns(Tree).in_sequence Tree.expects(:metadata_blank).with(nil).returns(Tree).in_sequence Tree.expects(:metadata_default).with('other').returns(Tree).in_sequence Tree.expects(:find).with('42').returns(mock_tree).in_sequence get :edit, params: { id: '42', shadown_range: '20', paginate_default: { page: 2, per_page: 20 }, args_paginate_default: { page: 3, per_page: 15}, q: { metadata_default: 'other' } } assert_equal(mock_tree, assigns(:@tree)) assert_equal({ shadown_range: '20', paginate_default: { 'page' => '2', 'per_page' => '20' }, args_paginate_default: { 'page' => '3', 'per_page' => '15' }, q: { 'metadata_default' => 'other' } }, current_scopes) end def test_scope_with_different_key Tree.expects(:root_type).with('outside').returns(Tree).in_sequence Tree.expects(:find).with('42').returns(mock_tree).in_sequence get :show, params: { id: '42', root: 'outside' } assert_equal(mock_tree, assigns(:@tree)) assert_equal({ root: 'outside' }, current_scopes) end def test_scope_with_default_value_as_a_proc_without_argument Date.expects(:today).returns("today") Tree.expects(:planted_before).with("today").returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index assert_equal([mock_tree], assigns(:@trees)) assert_equal({ planted_before: "today" }, current_scopes) end def test_scope_with_default_value_as_proc_with_argument session[:height] = 100 Tree.expects(:calculate_height).with(100).returns(Tree).in_sequence Tree.expects(:new).returns(mock_tree).in_sequence get :new assert_equal(mock_tree, assigns(:@tree)) assert_equal({ calculate_height: 100 }, current_scopes) end def test_scope_with_custom_type parsed = Date.civil(2014,11,11) Tree.expects(:planted_after).with(parsed).returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { planted_after: "2014-11-11" } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ planted_after: parsed }, current_scopes) end def test_scope_with_boolean_block Tree.expects(:only_really_short!).with(@controller.object_id).returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { only_short: 'true' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ only_short: true }, current_scopes) end def test_scope_with_other_block_types Tree.expects(:by_given_category).with(@controller.object_id, 'for_id').returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { by_category: 'for' } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ by_category: 'for' }, current_scopes) end def test_scope_with_nested_hash_and_in_option hash = { 'title' => 'the-title', 'content' => 'the-content' } Tree.expects(:title).with('the-title').returns(Tree) Tree.expects(:content).with('the-content').returns(Tree) Tree.expects(:metadata).never Tree.expects(:metadata_blank).with(nil).returns(Tree) Tree.expects(:all).returns([mock_tree]) get :index, params: { q: hash } assert_equal([mock_tree], assigns(:@trees)) assert_equal({ q: hash }, current_scopes) end def test_overwritten_scope assert_nil(TreesController.scopes_configuration[:categories][:if]) assert_equal(:categories?, BonsaisController.scopes_configuration[:categories][:if]) end protected def mock_tree(stubs = {}) @mock_tree ||= mock(stubs) end def current_scopes @controller.send :current_scopes end def assigns(ivar) @controller.instance_variable_get(ivar) end end class TreeHugger include HasScope has_scope :color def by_color apply_scopes(Tree, color: 'blue') end end class HasScopeOutsideControllerTest < ActiveSupport::TestCase def test_has_scope_usable_outside_controller Tree.expects(:color).with('blue') TreeHugger.new.by_color end end has_scope-0.8.0/test/test_helper.rb0000644000004100000410000000111214021076126017327 0ustar www-datawww-datarequire 'bundler/setup' require 'minitest/autorun' require 'mocha' require 'mocha/mini_test' # Configure Rails ENV['RAILS_ENV'] = 'test' $:.unshift File.expand_path('../../lib', __FILE__) require 'has_scope' HasScope::Routes = ActionDispatch::Routing::RouteSet.new HasScope::Routes.draw do resources :trees, only: %i[index new edit show] end class ApplicationController < ActionController::Base include HasScope::Routes.url_helpers end class ActiveSupport::TestCase self.test_order = :random if respond_to?(:test_order=) setup do @routes = HasScope::Routes end end has_scope-0.8.0/README.md0000644000004100000410000001402314021076126014771 0ustar www-datawww-data## HasScope [![Gem Version](https://fury-badge.herokuapp.com/rb/has_scope.svg)](http://badge.fury.io/rb/has_scope) [![Code Climate](https://codeclimate.com/github/heartcombo/has_scope.svg)](https://codeclimate.com/github/heartcombo/has_scope) Has scope allows you to map incoming controller parameters to named scopes in your resources. Imagine the following model called graduations: ```ruby class Graduation < ActiveRecord::Base scope :featured, -> { where(featured: true) } scope :by_degree, -> degree { where(degree: degree) } scope :by_period, -> started_at, ended_at { where("started_at = ? AND ended_at = ?", started_at, ended_at) } end ``` You can use those named scopes as filters by declaring them on your controller: ```ruby class GraduationsController < ApplicationController has_scope :featured, type: :boolean has_scope :by_degree end ``` Now, if you want to apply them to an specific resource, you just need to call `apply_scopes`: ```ruby class GraduationsController < ApplicationController has_scope :featured, type: :boolean has_scope :by_degree has_scope :by_period, using: %i[started_at ended_at], type: :hash def index @graduations = apply_scopes(Graduation).all end end ``` Then for each request: ``` /graduations #=> acts like a normal request /graduations?featured=true #=> calls the named scope and bring featured graduations /graduations?by_period[started_at]=20100701&by_period[ended_at]=20101013 #=> brings graduations in the given period /graduations?featured=true&by_degree=phd #=> brings featured graduations with phd degree ``` You can retrieve all the scopes applied in one action with `current_scopes` method. In the last case, it would return: `{ featured: true, by_degree: 'phd' }`. ## Installation Add `has_scope` to your Gemfile or install it from Rubygems. ```ruby gem 'has_scope' ``` ## Options HasScope supports several options: * `:type` - Checks the type of the parameter sent. By default, it does not allow hashes or arrays to be given, except if type `:hash` or `:array` are set. Symbols are never permitted to prevent memory leaks, so ensure any routing constraints you have that add parameters use string values. * `:only` - In which actions the scope is applied. * `:except` - In which actions the scope is not applied. * `:as` - The key in the params hash expected to find the scope. Defaults to the scope name. * `:using` - The subkeys to be used as args when type is a hash. * `:in` - A shortcut for combining the `:using` option with nested hashes. * `:if` - Specifies a method or proc to call to determine if the scope should apply. Passing a string is deprecated and it will be removed in a future version. * `:unless` - Specifies a method or proc to call to determine if the scope should NOT apply. Passing a string is deprecated and it will be removed in a future version. * `:default` - Default value for the scope. Whenever supplied the scope is always called. * `:allow_blank` - Blank values are not sent to scopes by default. Set to true to overwrite. ## Boolean usage If `type: :boolean` is set it just calls the named scope, without any arguments, when parameter is set to a "true" value. `'true'` and `'1'` are parsed as `true`, everything else as `false`. When boolean scope is set up with `allow_blank: true`, it will call the scope with the value as any usual scope. ```ruby has_scope :visible, type: :boolean has_scope :active, type: :boolean, allow_blank: true # and models with scope :visible, -> { where(visible: true) } scope :active, ->(value = true) { where(active: value) } ``` _Note_: it is not possible to apply a boolean scope with just the query param being present, e.g. `?active`, that's not considered a "true" value (the param value will be `nil`), and thus the scope will be called with `false` as argument. In order for the scope to receive a `true` argument the param value must be set to one of the "true" values above, e.g. `?active=true` or `?active=1`. ## Block usage `has_scope` also accepts a block. The controller, current scope and value are yielded to the block so the user can apply the scope on its own. This is useful in case we need to manipulate the given value: ```ruby has_scope :category do |controller, scope, value|  value != 'all' ? scope.by_category(value) : scope end ``` When used with booleans without `:allow_blank`, it just receives two arguments and is just invoked if true is given: ```ruby has_scope :not_voted_by_me, type: :boolean do |controller, scope| scope.not_voted_by(controller.current_user.id) end ``` ## Keyword arguments Scopes with keyword arguments need to be called in a block: ```ruby # in the model scope :for_course, lambda { |course_id:| where(course_id: course_id) } # in the controller has_scope :for_course do |controller, scope, value| scope.for_course(course_id: value) end ``` ## Apply scope on every request To apply scope on every request set default value and `allow_blank: true`: ```ruby has_scope :available, default: nil, allow_blank: true, only: :show, unless: :admin? # model: scope :available, ->(*) { where(blocked: false) } ``` This will allow usual users to get only available items, but admins will be able to access blocked items too. ## Check which scopes have been applied To check which scopes have been applied, you can call `current_scopes` from the controller or view. This returns a hash with the scope name as the key and the scope value as the value. For example, if a boolean `:active` scope has been applied, `current_scopes` will return `{ active: true }`. ## Supported Ruby / Rails versions We intend to maintain support for all Ruby / Rails versions that haven't reached end-of-life. For more information about specific versions please check [Ruby](https://www.ruby-lang.org/en/downloads/branches/) and [Rails](https://guides.rubyonrails.org/maintenance_policy.html) maintenance policies, and our test matrix. ## Bugs and Feedback If you discover any bugs or want to drop a line, feel free to create an issue on GitHub. MIT License. Copyright 2009-2019 Plataformatec. http://blog.plataformatec.com.br has_scope-0.8.0/has_scope.gemspec0000644000004100000410000000420014021076126017017 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: has_scope 0.8.0 ruby lib Gem::Specification.new do |s| s.name = "has_scope".freeze s.version = "0.8.0" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Jos\u{e9} Valim".freeze] s.date = "2021-02-15" s.description = "Maps controller filters to your resource scopes".freeze s.email = "opensource@plataformatec.com.br".freeze s.extra_rdoc_files = ["README.md".freeze] s.files = ["MIT-LICENSE".freeze, "README.md".freeze, "lib/has_scope.rb".freeze, "lib/has_scope/version.rb".freeze, "test/has_scope_test.rb".freeze, "test/test_helper.rb".freeze] s.homepage = "http://github.com/plataformatec/has_scope".freeze s.licenses = ["MIT".freeze] s.rdoc_options = ["--charset=UTF-8".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.5.0".freeze) s.rubygems_version = "2.5.2.1".freeze s.summary = "Maps controller filters to your resource scopes.".freeze s.test_files = ["test/has_scope_test.rb".freeze, "test/test_helper.rb".freeze] if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_runtime_dependency(%q.freeze, [">= 5.2"]) s.add_runtime_dependency(%q.freeze, [">= 5.2"]) s.add_development_dependency(%q.freeze, ["~> 1.0.0"]) s.add_development_dependency(%q.freeze, [">= 0"]) else s.add_dependency(%q.freeze, [">= 5.2"]) s.add_dependency(%q.freeze, [">= 5.2"]) s.add_dependency(%q.freeze, ["~> 1.0.0"]) s.add_dependency(%q.freeze, [">= 0"]) end else s.add_dependency(%q.freeze, [">= 5.2"]) s.add_dependency(%q.freeze, [">= 5.2"]) s.add_dependency(%q.freeze, ["~> 1.0.0"]) s.add_dependency(%q.freeze, [">= 0"]) end end has_scope-0.8.0/lib/0000755000004100000410000000000014021076126014260 5ustar www-datawww-datahas_scope-0.8.0/lib/has_scope/0000755000004100000410000000000014021076126016224 5ustar www-datawww-datahas_scope-0.8.0/lib/has_scope/version.rb0000644000004100000410000000005014021076126020231 0ustar www-datawww-datamodule HasScope VERSION = "0.8.0" end has_scope-0.8.0/lib/has_scope.rb0000644000004100000410000001653714021076126016565 0ustar www-datawww-datarequire 'active_support' require 'action_controller' module HasScope TRUE_VALUES = ["true", true, "1", 1] ALLOWED_TYPES = { array: [[ Array ]], hash: [[ Hash, ActionController::Parameters ]], boolean: [[ Object ], -> v { TRUE_VALUES.include?(v) }], default: [[ String, Numeric ]], } def self.included(base) base.class_eval do extend ClassMethods class_attribute :scopes_configuration, instance_writer: false self.scopes_configuration = {} end end module ClassMethods # Detects params from url and apply as scopes to your classes. # # == Options # # * :type - Checks the type of the parameter sent. If set to :boolean # it just calls the named scope, without any argument. By default, # it does not allow hashes or arrays to be given, except if type # :hash or :array are set. # # * :only - In which actions the scope is applied. By default is :all. # # * :except - In which actions the scope is not applied. By default is :none. # # * :as - The key in the params hash expected to find the scope. # Defaults to the scope name. # # * :using - If type is a hash, you can provide :using to convert the hash to # a named scope call with several arguments. # # * :in - A shortcut for combining the `:using` option with nested hashes. # # * :if - Specifies a method, proc or string to call to determine # if the scope should apply # # * :unless - Specifies a method, proc or string to call to determine # if the scope should NOT apply. # # * :default - Default value for the scope. Whenever supplied the scope # is always called. # # * :allow_blank - Blank values are not sent to scopes by default. Set to true to overwrite. # # == Block usage # # has_scope also accepts a block. The controller, current scope and value are yielded # to the block so the user can apply the scope on its own. This is useful in case we # need to manipulate the given value: # # has_scope :category do |controller, scope, value| # value != "all" ? scope.by_category(value) : scope # end # # has_scope :not_voted_by_me, type: :boolean do |controller, scope| # scope.not_voted_by(controller.current_user.id) # end # def has_scope(*scopes, &block) options = scopes.extract_options! options.symbolize_keys! options.assert_valid_keys(:type, :only, :except, :if, :unless, :default, :as, :using, :allow_blank, :in) if options.key?(:in) options[:as] = options[:in] options[:using] = scopes if options.key?(:default) && !options[:default].is_a?(Hash) options[:default] = scopes.each_with_object({}) { |scope, hash| hash[scope] = options[:default] } end end if options.key?(:using) if options.key?(:type) && options[:type] != :hash raise "You cannot use :using with another :type different than :hash" else options[:type] = :hash end options[:using] = Array(options[:using]) end options[:only] = Array(options[:only]) options[:except] = Array(options[:except]) self.scopes_configuration = scopes_configuration.dup scopes.each do |scope| scopes_configuration[scope] ||= { as: scope, type: :default, block: block } scopes_configuration[scope] = self.scopes_configuration[scope].merge(options) end end end protected # Receives an object where scopes will be applied to. # # class GraduationsController < ApplicationController # has_scope :featured, type: true, only: :index # has_scope :by_degree, only: :index # # def index # @graduations = apply_scopes(Graduation).all # end # end # def apply_scopes(target, hash = params) scopes_configuration.each do |scope, options| next unless apply_scope_to_action?(options) key = options[:as] if hash.key?(key) value, call_scope = hash[key], true elsif options.key?(:default) value, call_scope = options[:default], true if value.is_a?(Proc) value = value.arity == 0 ? value.call : value.call(self) end end value = parse_value(options[:type], value) value = normalize_blanks(value) if value && options.key?(:using) scope_value = value.values_at(*options[:using]) call_scope &&= scope_value.all?(&:present?) || options[:allow_blank] else scope_value = value call_scope &&= value.present? || options[:allow_blank] end if call_scope current_scopes[key] = value target = call_scope_by_type(options[:type], scope, target, scope_value, options) end end target end # Set the real value for the current scope if type check. def parse_value(type, value) #:nodoc: klasses, parser = ALLOWED_TYPES[type] if klasses.any? { |klass| value.is_a?(klass) } parser ? parser.call(value) : value end end # Screens pseudo-blank params. def normalize_blanks(value) #:nodoc: case value when Array value.select { |v| v.present? } when Hash value.select { |k, v| normalize_blanks(v).present? }.with_indifferent_access when ActionController::Parameters normalize_blanks(value.to_unsafe_h) else value end end # Call the scope taking into account its type. def call_scope_by_type(type, scope, target, value, options) #:nodoc: block = options[:block] if type == :boolean && !options[:allow_blank] block ? block.call(self, target) : target.send(scope) elsif options.key?(:using) block ? block.call(self, target, value) : target.send(scope, *value) else block ? block.call(self, target, value) : target.send(scope, value) end end # Given an options with :only and :except arrays, check if the scope # can be performed in the current action. def apply_scope_to_action?(options) #:nodoc: return false unless applicable?(options[:if], true) && applicable?(options[:unless], false) if options[:only].empty? options[:except].empty? || !options[:except].include?(action_name.to_sym) else options[:only].include?(action_name.to_sym) end end # Evaluates the scope options :if or :unless. Returns true if the proc # method, or string evals to the expected value. def applicable?(string_proc_or_symbol, expected) #:nodoc: case string_proc_or_symbol when String ActiveSupport::Deprecation.warn <<-DEPRECATION.squish [HasScope] Passing a string to determine if the scope should be applied is deprecated and it will be removed in a future version of HasScope. DEPRECATION eval(string_proc_or_symbol) == expected when Proc string_proc_or_symbol.call(self) == expected when Symbol send(string_proc_or_symbol) == expected else true end end # Returns the scopes used in this action. def current_scopes @current_scopes ||= {} end end ActiveSupport.on_load :action_controller do include HasScope helper_method :current_scopes if respond_to?(:helper_method) end has_scope-0.8.0/MIT-LICENSE0000644000004100000410000000211414021076126015144 0ustar www-datawww-dataCopyright 2009-2017 Plataforma Tecnologia. http://blog.plataformatec.com.br Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.